diff --git a/app/components/List/Item.tsx b/app/components/List/Item.tsx index 331131dda..662bdd547 100644 --- a/app/components/List/Item.tsx +++ b/app/components/List/Item.tsx @@ -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 + ref: React.RefObject ) => { const theme = useTheme(); const compact = !subtitle; - let itemRef: React.Ref = + let itemRef: React.RefObject = React.useRef(null); if (ref) { itemRef = ref; } const { focused, ...rovingTabIndex } = useRovingTabIndex( - itemRef as React.RefObject, + itemRef, keyboardNavigation || to ? false : true ); - useFocusEffect(focused, itemRef as React.RefObject); + 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)} diff --git a/app/components/Modal.tsx b/app/components/Modal.tsx index 49fee8c2f..ae8b8175e 100644 --- a/app/components/Modal.tsx +++ b/app/components/Modal.tsx @@ -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); diff --git a/app/components/Sharing/Collection/SharePopover.tsx b/app/components/Sharing/Collection/SharePopover.tsx index a5fcbd6e9..bada8cd23 100644 --- a/app/components/Sharing/Collection/SharePopover.tsx +++ b/app/components/Sharing/Collection/SharePopover.tsx @@ -355,17 +355,15 @@ function SharePopover({ collection, visible, onRequestClose }: Props) { )} {picker && ( -
- -
+ )}
diff --git a/app/components/Sharing/components/Suggestions.tsx b/app/components/Sharing/components/Suggestions.tsx index 3b83feef7..516c159b3 100644 --- a/app/components/Sharing/components/Suggestions.tsx +++ b/app/components/Sharing/components/Suggestions.tsx @@ -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(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 ( - - {() => [ - ...pending.map((suggestion) => ( - removePendingId(suggestion.id)} - onKeyDown={(ev) => { - if (ev.key === "Enter") { - ev.preventDefault(); - ev.stopPropagation(); - removePendingId(suggestion.id); + + {() => [ + ...pending.map((suggestion) => ( + removePendingId(suggestion.id)} + onKeyDown={(ev) => { + if (ev.key === "Enter") { + ev.preventDefault(); + ev.stopPropagation(); + removePendingId(suggestion.id); + } + }} + actions={ + <> + + + } - }} - actions={ - <> - - - - } - /> - )), - pending.length > 0 && - (suggestionsWithPending.length > 0 || isEmpty) && , - ...suggestionsWithPending.map((suggestion) => ( - addPendingId(suggestion.id)} - onKeyDown={(ev) => { - if (ev.key === "Enter") { - ev.preventDefault(); - ev.stopPropagation(); - addPendingId(suggestion.id); - } - }} - actions={} - /> - )), - isEmpty && {t("No matches")}, - ]} - + /> + )), + pending.length > 0 && + (suggestionsWithPending.length > 0 || isEmpty) && , + ...suggestionsWithPending.map((suggestion) => ( + addPendingId(suggestion.id)} + onKeyDown={(ev) => { + if (ev.key === "Enter") { + ev.preventDefault(); + ev.stopPropagation(); + addPendingId(suggestion.id); + } + }} + actions={} + /> + )), + isEmpty && ( + {t("No matches")} + ), + ]} + + ); }) ); @@ -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; +`; diff --git a/app/hooks/useMaxHeight.ts b/app/hooks/useMaxHeight.ts new file mode 100644 index 000000000..9fa0c8692 --- /dev/null +++ b/app/hooks/useMaxHeight.ts @@ -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; + /** The margin to apply to the positioning. */ + margin?: number; +}) => { + const [maxHeight, setMaxHeight] = React.useState(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; diff --git a/app/scenes/Collection/components/ShareButton.tsx b/app/scenes/Collection/components/ShareButton.tsx index bf1d95b8f..f505875c0 100644 --- a/app/scenes/Collection/components/ShareButton.tsx +++ b/app/scenes/Collection/components/ShareButton.tsx @@ -50,7 +50,12 @@ function ShareButton({ collection }: Props) { )} - + - +