From 0fd5278b62854dfa1077f560f8cd2b676162caa0 Mon Sep 17 00:00:00 2001 From: Van Anderson Date: Fri, 28 May 2021 10:09:44 -0500 Subject: [PATCH] anchored overlay can take an external ref for the anchor --- src/AnchoredOverlay/AnchoredOverlay.tsx | 67 +++++++++++---- src/AnchoredOverlay/RefAnchoredOverlay.tsx | 95 ---------------------- src/stories/RefAnchoredOverlay.stories.tsx | 19 +++-- 3 files changed, 65 insertions(+), 116 deletions(-) delete mode 100644 src/AnchoredOverlay/RefAnchoredOverlay.tsx diff --git a/src/AnchoredOverlay/AnchoredOverlay.tsx b/src/AnchoredOverlay/AnchoredOverlay.tsx index eeadec299ed..8d061366e9f 100644 --- a/src/AnchoredOverlay/AnchoredOverlay.tsx +++ b/src/AnchoredOverlay/AnchoredOverlay.tsx @@ -1,14 +1,21 @@ import React, {useCallback, useMemo, useRef} from 'react' -import {OverlayProps} from '../Overlay' -import {FocusZoneHookSettings} from '../hooks/useFocusZone' +import Overlay, {OverlayProps} from '../Overlay' +import {useFocusTrap} from '../hooks/useFocusTrap' +import {FocusZoneHookSettings, useFocusZone} from '../hooks/useFocusZone' +import {useAnchoredPosition, useRenderForcingRef} from '../hooks' import {uniqueId} from '../utils/uniqueId' -import {RefAnchoredOverlay} from './RefAnchoredOverlay' + export interface AnchoredOverlayProps extends Pick { /** * A custom function component used to render the anchor element. * Will receive the selected text as `children` prop when an item is activated. */ - renderAnchor: >(props: T) => JSX.Element + renderAnchor: >(props: T) => JSX.Element | null + + /** + * An override to the internal ref that will be spread on to the renderAnchor + */ + anchorRef?: React.RefObject /** * Determines whether the overlay portion of the component should be shown or not @@ -42,6 +49,7 @@ export interface AnchoredOverlayProps extends Pick = ({ renderAnchor, + anchorRef: externalAnchorRef, children, open, onOpen, @@ -51,9 +59,14 @@ export const AnchoredOverlay: React.FC = ({ overlayProps, focusZoneSettings }) => { - const anchorRef = useRef(null) + const internalAnchorRef = useRef(null) + const anchorRef = externalAnchorRef || internalAnchorRef + const [overlayRef, updateOverlayRef] = useRenderForcingRef() const anchorId = useMemo(uniqueId, []) + const onClickOutside = useCallback(() => onClose?.('click-outside'), [onClose]) + const onEscape = useCallback(() => onClose?.('escape'), [onClose]) + const onAnchorKeyDown = useCallback( (event: React.KeyboardEvent) => { if (!event.defaultPrevented) { @@ -74,6 +87,24 @@ export const AnchoredOverlay: React.FC = ({ [open, onOpen] ) + const {position} = useAnchoredPosition( + { + anchorElementRef: anchorRef, + floatingElementRef: overlayRef + }, + [overlayRef.current] + ) + const overlayPosition = useMemo(() => { + return position && {top: `${position.top}px`, left: `${position.left}px`} + }, [position]) + + useFocusZone({ + containerRef: overlayRef, + disabled: !open || !position, + ...focusZoneSettings + }) + useFocusTrap({containerRef: overlayRef, disabled: !open || !position}) + return ( <> {renderAnchor({ @@ -85,16 +116,22 @@ export const AnchoredOverlay: React.FC = ({ onClick: onAnchorClick, onKeyDown: onAnchorKeyDown })} - + {open ? ( + + {children} + + ) : null} ) } diff --git a/src/AnchoredOverlay/RefAnchoredOverlay.tsx b/src/AnchoredOverlay/RefAnchoredOverlay.tsx deleted file mode 100644 index bce04871fdf..00000000000 --- a/src/AnchoredOverlay/RefAnchoredOverlay.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import React, {useCallback, useMemo} from 'react' -import Overlay, {OverlayProps} from '../Overlay' -import {useFocusTrap} from '../hooks/useFocusTrap' -import {FocusZoneHookSettings, useFocusZone} from '../hooks/useFocusZone' -import {useAnchoredPosition, useRenderForcingRef} from '../hooks' - -export interface RefAnchoredOverlayProps extends Pick { - /** - * A custom function component used to render the anchor element. - * Will receive the selected text as `children` prop when an item is activated. - */ - anchorRef: React.RefObject - - /** - * Determines whether the overlay portion of the component should be shown or not - */ - open: boolean - - /** - * A callback which is called whenever the overlay is currently closed and an "open gesture" is detected. - */ - onOpen?: (gesture: 'anchor-click' | 'anchor-key-press') => unknown - - /** - * A callback which is called whenever the overlay is currently open and a "close gesture" is detected. - */ - onClose?: (gesture: 'click-outside' | 'escape') => unknown - - /** - * Props to be spread on the internal `Overlay` component. - */ - overlayProps?: Partial - - /** - * Settings to apply to the Focus Zone on the internal `Overlay` component. - */ - focusZoneSettings?: Partial -} - -/** - * An `AnchoredOverlay` provides an anchor that will open a floating overlay positioned relative to the anchor. - * The overlay can be opened and navigated using keyboard or mouse. - */ -export const RefAnchoredOverlay: React.FC = ({ - anchorRef, - children, - open, - onClose, - height, - width, - overlayProps, - focusZoneSettings -}) => { - const [overlayRef, updateOverlayRef] = useRenderForcingRef() - - const onClickOutside = useCallback(() => onClose?.('click-outside'), [onClose]) - const onEscape = useCallback(() => onClose?.('escape'), [onClose]) - - const {position} = useAnchoredPosition( - { - anchorElementRef: anchorRef, - floatingElementRef: overlayRef - }, - [overlayRef.current] - ) - const overlayPosition = useMemo(() => { - return position && {top: `${position.top}px`, left: `${position.left}px`} - }, [position]) - - useFocusZone({ - containerRef: overlayRef, - disabled: !open || !position, - ...focusZoneSettings - }) - useFocusTrap({containerRef: overlayRef, disabled: !open || !position}) - - return open ? ( - - {children} - - ) : null -} - -RefAnchoredOverlay.displayName = 'AnchoredOverlay' diff --git a/src/stories/RefAnchoredOverlay.stories.tsx b/src/stories/RefAnchoredOverlay.stories.tsx index 4bb25b0b62b..6fb96288ab3 100644 --- a/src/stories/RefAnchoredOverlay.stories.tsx +++ b/src/stories/RefAnchoredOverlay.stories.tsx @@ -1,11 +1,12 @@ import React, {useState, useRef} from 'react' import {Button, Text, ButtonDanger, Position, Flex, BaseStyles, ThemeProvider} from '..' +import {Meta} from '@storybook/react' -import {RefAnchoredOverlay} from '../AnchoredOverlay/RefAnchoredOverlay' +import {AnchoredOverlay} from '../AnchoredOverlay' export default { - title: 'Internal components/RefAnchoredOverlay', - component: RefAnchoredOverlay, + title: 'Internal components/AnchoredOverlay', + component: AnchoredOverlay, decorators: [ // @ts-ignore Story => { @@ -20,7 +21,7 @@ export default { ] } as Meta -export const RefAnchoredOverlayStory = () => { +export const RefAnchoredOverlay = () => { const [isOpen, setIsOpen] = useState(false) const buttonRef = useRef(null) const confirmButtonRef = useRef(null) @@ -32,7 +33,13 @@ export const RefAnchoredOverlayStory = () => { open overlay {/* @ts-ignore */} - + null} + anchorRef={buttonRef} + onClose={closeOverlay} + open={isOpen} + width="small" + > Are you sure? Cancel @@ -40,7 +47,7 @@ export const RefAnchoredOverlayStory = () => { Confirm - + ) }