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:
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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" }}>
|
||||||
|
|||||||
@@ -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
43
app/hooks/useMaxHeight.ts
Normal 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;
|
||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user