Prevent modal top from shrinking (#7167)

* fix: prevent modal top from shrinking

* fix: scroll and max height for share modal and popover

* fix: review
This commit is contained in:
Apoorv Mishra
2024-07-03 09:11:00 +05:30
committed by GitHub
parent 303125b682
commit de90f879f1
7 changed files with 157 additions and 66 deletions

View File

@@ -4,6 +4,7 @@ import {
} from "@getoutline/react-roving-tabindex";
import { LocationDescriptor } from "history";
import * as React from "react";
import scrollIntoView from "smooth-scroll-into-view-if-needed";
import styled, { useTheme } from "styled-components";
import { s, ellipsis } from "@shared/styles";
import Flex from "~/components/Flex";
@@ -47,22 +48,33 @@ const ListItem = (
keyboardNavigation,
...rest
}: Props,
ref?: React.Ref<HTMLAnchorElement>
ref: React.RefObject<HTMLAnchorElement>
) => {
const theme = useTheme();
const compact = !subtitle;
let itemRef: React.Ref<HTMLAnchorElement> =
let itemRef: React.RefObject<HTMLAnchorElement> =
React.useRef<HTMLAnchorElement>(null);
if (ref) {
itemRef = ref;
}
const { focused, ...rovingTabIndex } = useRovingTabIndex(
itemRef as React.RefObject<HTMLAnchorElement>,
itemRef,
keyboardNavigation || to ? false : true
);
useFocusEffect(focused, itemRef as React.RefObject<HTMLAnchorElement>);
useFocusEffect(focused, itemRef);
const handleFocus = React.useCallback(() => {
if (itemRef.current) {
scrollIntoView(itemRef.current, {
scrollMode: "if-needed",
behavior: "auto",
block: "center",
boundary: window.document.body,
});
}
}, [itemRef]);
const content = (selected: boolean) => (
<>
@@ -110,6 +122,10 @@ const ListItem = (
}
rovingTabIndex.onKeyDown(ev);
}}
onFocus={(ev) => {
rovingTabIndex.onFocus(ev);
handleFocus();
}}
as={NavLink}
to={to}
>
@@ -134,6 +150,10 @@ const ListItem = (
rest.onKeyDown?.(ev);
rovingTabIndex.onKeyDown(ev);
}}
onFocus={(ev) => {
rovingTabIndex.onFocus(ev);
handleFocus();
}}
>
{content(false)}
</Wrapper>

View File

@@ -254,7 +254,7 @@ const Header = styled(Flex)`
const Small = styled.div`
animation: ${fadeAndScaleIn} 250ms ease;
margin: auto auto;
margin: 25vh auto auto auto;
width: 75vw;
min-width: 350px;
max-width: 450px;
@@ -282,7 +282,7 @@ const Small = styled.div`
`;
const SmallContent = styled(Scrollable)`
padding: 12px 24px 24px;
padding: 12px 24px;
`;
export default observer(Modal);

View File

@@ -355,17 +355,15 @@ function SharePopover({ collection, visible, onRequestClose }: Props) {
)}
{picker && (
<div>
<Suggestions
ref={suggestionsRef}
query={query}
collection={collection}
pendingIds={pendingIds}
addPendingId={handleAddPendingId}
removePendingId={handleRemovePendingId}
onEscape={handleEscape}
/>
</div>
<Suggestions
ref={suggestionsRef}
query={query}
collection={collection}
pendingIds={pendingIds}
addPendingId={handleAddPendingId}
removePendingId={handleRemovePendingId}
onEscape={handleEscape}
/>
)}
<div style={{ display: picker ? "none" : "block" }}>

View File

@@ -17,7 +17,9 @@ import Avatar from "~/components/Avatar";
import { AvatarSize, IAvatar } from "~/components/Avatar/Avatar";
import Empty from "~/components/Empty";
import Placeholder from "~/components/List/Placeholder";
import Scrollable from "~/components/Scrollable";
import useCurrentUser from "~/hooks/useCurrentUser";
import useMaxHeight from "~/hooks/useMaxHeight";
import useStores from "~/hooks/useStores";
import useThrottledCallback from "~/hooks/useThrottledCallback";
import { hover } from "~/styles";
@@ -65,6 +67,11 @@ export const Suggestions = observer(
const { t } = useTranslation();
const user = useCurrentUser();
const theme = useTheme();
const containerRef = React.useRef<HTMLDivElement | null>(null);
const maxHeight = useMaxHeight({
elementRef: containerRef,
maxViewportPercentage: 70,
});
const fetchUsersByQuery = useThrottledCallback(
(query: string) => {
@@ -182,55 +189,63 @@ export const Suggestions = observer(
neverRenderedList.current = false;
return (
<ArrowKeyNavigation
ref={ref}
onEscape={onEscape}
aria-label={t("Suggestions for invitation")}
items={concat(pending, suggestionsWithPending)}
<ScrollableContainer
ref={containerRef}
hiddenScrollbars
style={{ maxHeight }}
>
{() => [
...pending.map((suggestion) => (
<PendingListItem
keyboardNavigation
{...getListItemProps(suggestion)}
key={suggestion.id}
onClick={() => removePendingId(suggestion.id)}
onKeyDown={(ev) => {
if (ev.key === "Enter") {
ev.preventDefault();
ev.stopPropagation();
removePendingId(suggestion.id);
<ArrowKeyNavigation
ref={ref}
onEscape={onEscape}
aria-label={t("Suggestions for invitation")}
items={concat(pending, suggestionsWithPending)}
>
{() => [
...pending.map((suggestion) => (
<PendingListItem
keyboardNavigation
{...getListItemProps(suggestion)}
key={suggestion.id}
onClick={() => removePendingId(suggestion.id)}
onKeyDown={(ev) => {
if (ev.key === "Enter") {
ev.preventDefault();
ev.stopPropagation();
removePendingId(suggestion.id);
}
}}
actions={
<>
<InvitedIcon />
<RemoveIcon />
</>
}
}}
actions={
<>
<InvitedIcon />
<RemoveIcon />
</>
}
/>
)),
pending.length > 0 &&
(suggestionsWithPending.length > 0 || isEmpty) && <Separator />,
...suggestionsWithPending.map((suggestion) => (
<ListItem
keyboardNavigation
{...getListItemProps(suggestion as User)}
key={suggestion.id}
onClick={() => addPendingId(suggestion.id)}
onKeyDown={(ev) => {
if (ev.key === "Enter") {
ev.preventDefault();
ev.stopPropagation();
addPendingId(suggestion.id);
}
}}
actions={<InviteIcon />}
/>
)),
isEmpty && <Empty style={{ marginTop: 22 }}>{t("No matches")}</Empty>,
]}
</ArrowKeyNavigation>
/>
)),
pending.length > 0 &&
(suggestionsWithPending.length > 0 || isEmpty) && <Separator />,
...suggestionsWithPending.map((suggestion) => (
<ListItem
keyboardNavigation
{...getListItemProps(suggestion as User)}
key={suggestion.id}
onClick={() => addPendingId(suggestion.id)}
onKeyDown={(ev) => {
if (ev.key === "Enter") {
ev.preventDefault();
ev.stopPropagation();
addPendingId(suggestion.id);
}
}}
actions={<InviteIcon />}
/>
)),
isEmpty && (
<Empty style={{ marginTop: 22 }}>{t("No matches")}</Empty>
),
]}
</ArrowKeyNavigation>
</ScrollableContainer>
);
})
);
@@ -259,3 +274,8 @@ const Separator = styled.div`
border-top: 1px dashed ${s("divider")};
margin: 12px 0;
`;
const ScrollableContainer = styled(Scrollable)`
padding: 12px 24px;
margin: -12px -24px;
`;

43
app/hooks/useMaxHeight.ts Normal file
View File

@@ -0,0 +1,43 @@
import * as React from "react";
import useMobile from "./useMobile";
import useWindowSize from "./useWindowSize";
const useMaxHeight = ({
elementRef,
maxViewportPercentage = 90,
margin = 16,
}: {
/** The maximum height of the element as a percentage of the viewport. */
maxViewportPercentage?: number;
/** A ref pointing to the element. */
elementRef?: React.RefObject<HTMLElement | null>;
/** The margin to apply to the positioning. */
margin?: number;
}) => {
const [maxHeight, setMaxHeight] = React.useState<number | undefined>(10);
const isMobile = useMobile();
const { height: windowHeight } = useWindowSize();
React.useLayoutEffect(() => {
if (!isMobile && elementRef?.current) {
const mxHeight = (windowHeight / 100) * maxViewportPercentage;
setMaxHeight(
Math.min(
mxHeight,
elementRef?.current
? windowHeight -
elementRef.current.getBoundingClientRect().top -
margin
: 0
)
);
} else {
setMaxHeight(0);
}
}, [elementRef, windowHeight, margin, isMobile, maxViewportPercentage]);
return maxHeight;
};
export default useMaxHeight;

View File

@@ -50,7 +50,12 @@ function ShareButton({ collection }: Props) {
)}
</PopoverDisclosure>
<Popover {...popover} aria-label={t("Share")} width={400}>
<Popover
{...popover}
aria-label={t("Share")}
width={400}
scrollable={false}
>
<SharePopover
collection={collection}
onRequestClose={popover.hide}

View File

@@ -51,7 +51,12 @@ function ShareButton({ document }: Props) {
)}
</PopoverDisclosure>
<Popover {...popover} aria-label={t("Share")} width={400}>
<Popover
{...popover}
aria-label={t("Share")}
width={400}
scrollable={false}
>
<SharePopover
document={document}
share={share}