From fca4ec98f2e3ef5e5c3298320e3ab2050593709c Mon Sep 17 00:00:00 2001 From: Siddharth Kshetrapal Date: Tue, 12 Nov 2024 03:05:09 +0100 Subject: [PATCH] Bring back #5255 "SelectPanel: Prepare for non-anchored variants" (#5262) * Revert "Revert "SelectPanel: Prepare for non-anchored variants (#5230)" (#5255)" This reverts commit 64802f1f6628579e4064c1c8e2c8bc13ed9f5280. * Revert "tiny formatting to make it easier to review" This reverts commit f0be48080b0f2fbeee2abb1334f08ef87851b85f. * different way of writing the same thing --- .changeset/fluffy-days-brake.md | 5 + .../react/src/SelectPanel/SelectPanel.tsx | 105 +++++++++++++----- 2 files changed, 84 insertions(+), 26 deletions(-) create mode 100644 .changeset/fluffy-days-brake.md diff --git a/.changeset/fluffy-days-brake.md b/.changeset/fluffy-days-brake.md new file mode 100644 index 00000000000..a965179d8da --- /dev/null +++ b/.changeset/fluffy-days-brake.md @@ -0,0 +1,5 @@ +--- +"@primer/react": patch +--- + +SelectPanel: (Implementation detail, should have no changes for users) Replace AnchoredOverlay with Overlay and useAnchoredPosition diff --git a/packages/react/src/SelectPanel/SelectPanel.tsx b/packages/react/src/SelectPanel/SelectPanel.tsx index 98a7a66d322..dc0cbfb38f2 100644 --- a/packages/react/src/SelectPanel/SelectPanel.tsx +++ b/packages/react/src/SelectPanel/SelectPanel.tsx @@ -1,7 +1,7 @@ import {SearchIcon, TriangleDownIcon} from '@primer/octicons-react' import React, {useCallback, useMemo} from 'react' import type {AnchoredOverlayProps} from '../AnchoredOverlay' -import {AnchoredOverlay} from '../AnchoredOverlay' +import Overlay from '../Overlay' import type {AnchoredOverlayWrapperAnchorProps} from '../AnchoredOverlay/AnchoredOverlay' import Box from '../Box' import type {FilteredActionListProps} from '../FilteredActionList' @@ -12,13 +12,14 @@ import type {TextInputProps} from '../TextInput' import type {ItemProps, ItemInput} from './types' import {Button} from '../Button' -import {useProvidedRefOrCreate} from '../hooks' -import type {FocusZoneHookSettings} from '../hooks/useFocusZone' +import {useAnchoredPosition, useProvidedRefOrCreate} from '../hooks' import {useId} from '../hooks/useId' import {useProvidedStateOrCreate} from '../hooks/useProvidedStateOrCreate' import {LiveRegion, LiveRegionOutlet, Message} from '../internal/components/LiveRegion' import {useFeatureFlag} from '../FeatureFlags' +import {useFocusTrap} from '../hooks/useFocusTrap' + interface SelectPanelSingleSelection { selected: ItemInput | undefined onSelectedChange: (selected: ItemInput | undefined) => void @@ -56,11 +57,6 @@ function isMultiSelectVariant( return Array.isArray(selected) } -const focusZoneSettings: Partial = { - // Let FilteredActionList handle focus zone - disabled: true, -} - const areItemsEqual = (itemA: ItemInput, itemB: ItemInput) => { // prefer checking equivality by item.id if (typeof itemA.id !== 'undefined') return itemA.id === itemB.id @@ -137,6 +133,57 @@ export function SelectPanel({ } }, [placeholder, renderAnchor, selected]) + /* Anchoring logic */ + const overlayRef = React.useRef(null) + const inputRef = React.useRef(null) + + const {position} = useAnchoredPosition( + { + anchorElementRef: anchorRef, + floatingElementRef: overlayRef, + side: 'outside-bottom', + align: 'start', + }, + [open, anchorRef.current, overlayRef.current], + ) + + const onAnchorClick = useCallback( + (event: React.MouseEvent) => { + if (event.defaultPrevented || event.button !== 0) { + return + } + + if (!open) { + onOpen('anchor-click') + } else { + onClose('anchor-click') + } + }, + [open, onOpen, onClose], + ) + + const onAnchorKeyDown = useCallback( + (event: React.KeyboardEvent) => { + if (!event.defaultPrevented) { + if (!open && ['ArrowDown', 'ArrowUp', ' ', 'Enter'].includes(event.key)) { + onOpen('anchor-key-press', event) + event.preventDefault() + } + } + }, + [open, onOpen], + ) + + const anchorProps = { + ref: anchorRef, + 'aria-haspopup': true, + 'aria-expanded': open, + onClick: onAnchorClick, + onKeyDown: onAnchorKeyDown, + } + // TODO: anchor should be called button because it's not an anchor anymore + const anchor = renderMenuAnchor ? renderMenuAnchor(anchorProps) : null + const itemsToRender = useMemo(() => { return items.map(item => { const isItemSelected = isMultiSelectVariant(selected) ? doesItemsIncludeItem(selected, item) : selected === item @@ -172,10 +219,13 @@ export function SelectPanel({ }) }, [onClose, onSelectedChange, items, selected]) - const inputRef = React.useRef(null) - const focusTrapSettings = { + /** Focus trap */ + useFocusTrap({ + containerRef: overlayRef, + disabled: !open || !position, initialFocusRef: inputRef, - } + returnFocusRef: anchorRef, + }) const extendedTextInputProps: Partial = useMemo(() => { return { @@ -189,22 +239,25 @@ export function SelectPanel({ const usingModernActionList = useFeatureFlag('primer_react_select_panel_with_modern_action_list') + if (!open) return {anchor} + return ( - onClose('escape')} + onClickOutside={() => onClose('click-outside')} + ignoreClickRefs={ + /* this is required so that clicking the button while the panel is open does not re-open the panel */ + [anchorRef] + } + {...position} + {...overlayProps} > {usingModernActionList ? null : ( @@ -260,7 +313,7 @@ export function SelectPanel({ )} - + ) }