From d20a5996aafdbc6446f13aaa7a489394926f083a Mon Sep 17 00:00:00 2001 From: Dusty Greif Date: Thu, 6 May 2021 13:46:14 -0700 Subject: [PATCH] AnchoredOverlay and ActionList fixes for SelectPanel (#1209) --- .changeset/chatty-nails-end.md | 8 ++++++++ src/ActionList/Item.tsx | 9 ++++++++- src/ActionList/List.tsx | 15 +++++---------- src/AnchoredOverlay/AnchoredOverlay.tsx | 18 ++++++++++++++---- 4 files changed, 35 insertions(+), 15 deletions(-) create mode 100644 .changeset/chatty-nails-end.md diff --git a/.changeset/chatty-nails-end.md b/.changeset/chatty-nails-end.md new file mode 100644 index 00000000000..3071dc0628e --- /dev/null +++ b/.changeset/chatty-nails-end.md @@ -0,0 +1,8 @@ +--- +"@primer/components": patch +--- + +Allow Overlay height and width to be set through AnchoredOverlay +Allow ActionList Items to supply an `id` instead of `key` +Performance imporvements when ActionList is not given any groups +Enable focus zone as soon as AnchoredOverlay opens diff --git a/src/ActionList/Item.tsx b/src/ActionList/Item.tsx index e6c505a7120..e4cdabca36a 100644 --- a/src/ActionList/Item.tsx +++ b/src/ActionList/Item.tsx @@ -8,7 +8,7 @@ import styled from 'styled-components' /** * Contract for props passed to the `Item` component. */ -export interface ItemProps extends React.ComponentPropsWithoutRef<'div'>, SxProp { +export interface ItemProps extends Omit, 'id'>, SxProp { /** * Primary text which names an `Item`. */ @@ -69,6 +69,11 @@ export interface ItemProps extends React.ComponentPropsWithoutRef<'div'>, SxProp * Callback that will trigger both on click selection and keyboard selection. */ onAction?: (item: ItemProps, event: React.MouseEvent | React.KeyboardEvent) => void + + /** + * An id associated with this item. Should be unique between items + */ + id?: number | string } const getItemVariant = (variant = 'default', disabled?: boolean) => { @@ -180,6 +185,7 @@ export function Item(itemProps: Partial & {item?: ItemInput}): JSX.El onKeyPress, children, onClick, + id, ...props } = itemProps @@ -215,6 +221,7 @@ export function Item(itemProps: Partial & {item?: ItemInput}): JSX.El variant={variant} aria-selected={selected} {...props} + data-id={id} onKeyPress={keyPressHandler} onClick={clickHandler} > diff --git a/src/ActionList/List.tsx b/src/ActionList/List.tsx index 48a9290aa09..a7fe1183bd4 100644 --- a/src/ActionList/List.tsx +++ b/src/ActionList/List.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, {useMemo} from 'react' import type {AriaRole} from '../utils/types' import {Group, GroupProps} from './Group' import {Item, ItemProps} from './Item' @@ -137,14 +137,8 @@ export function List(props: ListProps): JSX.Element { */ const renderItem = (itemProps: ItemInput, item: ItemInput) => { const ItemComponent = ('renderItem' in itemProps && itemProps.renderItem) || props.renderItem || Item - return ( - - ) + const key = itemProps.key ?? itemProps.id?.toString() ?? uniqueId() + return } /** @@ -153,10 +147,11 @@ export function List(props: ListProps): JSX.Element { let groups: (GroupProps | (Partial & {renderItem?: typeof Item; renderGroup?: typeof Group}))[] = [] // Collect rendered `Item`s into `Group`s, avoiding excess iteration over the lists of `items` and `groupMetadata`: + const singleGroupId = useMemo(uniqueId, []) if (!isGroupedListProps(props)) { // When no `groupMetadata`s is provided, collect rendered `Item`s into a single anonymous `Group`. - groups = [{items: props.items?.map(item => renderItem(item, item)), groupId: uniqueId()}] + groups = [{items: props.items?.map(item => renderItem(item, item)), groupId: singleGroupId}] } else { // When `groupMetadata` is provided, collect rendered `Item`s into their associated `Group`s. diff --git a/src/AnchoredOverlay/AnchoredOverlay.tsx b/src/AnchoredOverlay/AnchoredOverlay.tsx index 4596cfdba54..fa76121e4a6 100644 --- a/src/AnchoredOverlay/AnchoredOverlay.tsx +++ b/src/AnchoredOverlay/AnchoredOverlay.tsx @@ -1,5 +1,5 @@ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react' -import Overlay from '../Overlay' +import Overlay, {OverlayProps} from '../Overlay' import {useFocusTrap} from '../hooks/useFocusTrap' import {useFocusZone} from '../hooks/useFocusZone' import {useAnchoredPosition, useRenderForcingRef} from '../hooks' @@ -9,7 +9,7 @@ function stopPropagation(event: React.UIEvent) { event.stopPropagation() } -export interface AnchoredOverlayProps { +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. @@ -36,7 +36,15 @@ export interface AnchoredOverlayProps { * 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 AnchoredOverlay: React.FC = ({renderAnchor, children, open, onOpen, onClose}) => { +export const AnchoredOverlay: React.FC = ({ + renderAnchor, + children, + open, + onOpen, + onClose, + height, + width +}) => { const anchorRef = useRef(null) const [overlayRef, updateOverlayRef] = useRenderForcingRef() const [focusType, setFocusType] = useState(open ? 'list' : null) @@ -98,7 +106,7 @@ export const AnchoredOverlay: React.FC = ({renderAnchor, c return position && {top: `${position.top}px`, left: `${position.left}px`} }, [position]) - useFocusZone({containerRef: overlayRef, disabled: !open || focusType !== 'list' || !position}) + useFocusZone({containerRef: overlayRef, disabled: !open || !position}) useFocusTrap({containerRef: overlayRef, disabled: !open || focusType !== 'list' || !position}) return ( @@ -123,6 +131,8 @@ export const AnchoredOverlay: React.FC = ({renderAnchor, c visibility={position ? 'visible' : 'hidden'} onMouseDown={stopPropagation} onClick={stopPropagation} + height={height} + width={width} {...overlayPosition} > {children}