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

View File

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

View File

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

View File

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