Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 677
SelectPanel: Update SelectPanel to use modern ActionList#4794
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
9cee0a4c7aa5de40a0c45344f6cefa5f5a63cd9ecc0003e59713b5ad6fc7f2ba246981e209f4726cddc1b1199fbb669cff101266267dc5c9c66feb54fbb3c52d5e2b2cee77379c972c317e4ca826b6fe642c50c00d0004f3ba9bc792ab9c27b3d5e61cf1e0992303d04dc3b41f6730343a7d490ab3ea9c44ff86246c94e6ff8379b9353d710a126ad91cd5a340322c5f0150e4f72b9d485d02372eca9fac87522302a6dc925c574a5dc6adb85e0bb40ee74e1798008ebeca8db14fd206185eb17318aa99e9c523cefd1216cd9a931619ff96e3fe7b8ebac34105134742afa2360f38bf4f72aa556e9d7060b57206File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@primer/react": patch | ||
| --- | ||
| SelectPanel: Update SelectPanel to use modern ActionList behind a feature flag `primer_react_select_panel_with_modern_action_list` |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| import React from 'react' | ||
| import type {FilteredActionListProps} from './FilteredActionListWithDeprecatedActionList' | ||
| import {FilteredActionList as WithDeprecatedActionList} from './FilteredActionListWithDeprecatedActionList' | ||
| import {FilteredActionList as WithStableActionList} from './FilteredActionListWithModernActionList' | ||
| import {useFeatureFlag} from '../FeatureFlags' | ||
| export function FilteredActionList(props: FilteredActionListProps): JSX.Element { | ||
| const enabled = useFeatureFlag('primer_react_select_panel_with_modern_action_list') | ||
| if (enabled) return <WithStableActionList {...props} /> | ||
| else return <WithDeprecatedActionList {...props} /> | ||
| } | ||
| FilteredActionList.displayName = 'FilteredActionList' | ||
| export type {FilteredActionListProps} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,233 @@ | ||
| import type {ScrollIntoViewOptions} from '@primer/behaviors' | ||
| import {scrollIntoView} from '@primer/behaviors' | ||
| import type {KeyboardEventHandler} from 'react' | ||
| import React, {useCallback, useEffect, useRef} from 'react' | ||
| import styled from 'styled-components' | ||
| import Box from '../Box' | ||
| import Spinner from '../Spinner' | ||
| import type {TextInputProps} from '../TextInput' | ||
| import TextInput from '../TextInput' | ||
| import {get} from '../constants' | ||
| import {ActionList} from '../ActionList' | ||
| import type {GroupedListProps, ListPropsBase, ItemInput} from '../SelectPanel/types' | ||
| import {useFocusZone} from '../hooks/useFocusZone' | ||
| import {useId} from '../hooks/useId' | ||
| import {useProvidedRefOrCreate} from '../hooks/useProvidedRefOrCreate' | ||
| import {useProvidedStateOrCreate} from '../hooks/useProvidedStateOrCreate' | ||
| import useScrollFlash from '../hooks/useScrollFlash' | ||
| import {VisuallyHidden} from '../internal/components/VisuallyHidden' | ||
| import type {SxProp} from '../sx' | ||
| import {isValidElementType} from 'react-is' | ||
| import type {RenderItemFn} from '../deprecated/ActionList/List' | ||
| const menuScrollMargins: ScrollIntoViewOptions = {startMargin: 0, endMargin: 8} | ||
| export interface FilteredActionListProps | ||
| extends Partial<Omit<GroupedListProps, keyof ListPropsBase>>, | ||
| ListPropsBase, | ||
| SxProp { | ||
| loading?: boolean | ||
| placeholderText?: string | ||
| filterValue?: string | ||
| onFilterChange: (value: string, e: React.ChangeEvent<HTMLInputElement>) => void | ||
| textInputProps?: Partial<Omit<TextInputProps, 'onChange'>> | ||
| inputRef?: React.RefObject<HTMLInputElement> | ||
| } | ||
| const StyledHeader = styled.div` | ||
| box-shadow: 0 1px 0 ${get('colors.border.default')}; | ||
| z-index: 1; | ||
| ` | ||
| export function FilteredActionList({ | ||
| loading = false, | ||
| placeholderText, | ||
| filterValue: externalFilterValue, | ||
| onFilterChange, | ||
| items, | ||
| textInputProps, | ||
| inputRef: providedInputRef, | ||
| sx, | ||
| groupMetadata, | ||
| showItemDividers, | ||
| ...listProps | ||
| }: FilteredActionListProps): JSX.Element { | ||
| const [filterValue, setInternalFilterValue] = useProvidedStateOrCreate(externalFilterValue, undefined, '') | ||
| const onInputChange = useCallback( | ||
| (e: React.ChangeEvent<HTMLInputElement>) => { | ||
| const value = e.target.value | ||
| onFilterChange(value, e) | ||
| setInternalFilterValue(value) | ||
| }, | ||
| [onFilterChange, setInternalFilterValue], | ||
| ) | ||
| const scrollContainerRef = useRef<HTMLDivElement>(null) | ||
| const listContainerRef = useRef<HTMLUListElement>(null) | ||
| const inputRef = useProvidedRefOrCreate<HTMLInputElement>(providedInputRef) | ||
| const activeDescendantRef = useRef<HTMLElement>() | ||
| const listId = useId() | ||
| const inputDescriptionTextId = useId() | ||
| const onInputKeyPress: KeyboardEventHandler = useCallback( | ||
| event => { | ||
| if (event.key === 'Enter' && activeDescendantRef.current) { | ||
| event.preventDefault() | ||
| event.nativeEvent.stopImmediatePropagation() | ||
| // Forward Enter key press to active descendant so that item gets activated | ||
| const activeDescendantEvent = new KeyboardEvent(event.type, event.nativeEvent) | ||
| activeDescendantRef.current.dispatchEvent(activeDescendantEvent) | ||
| } | ||
| }, | ||
| [activeDescendantRef], | ||
| ) | ||
| useFocusZone( | ||
| { | ||
| containerRef: listContainerRef, | ||
| focusOutBehavior: 'wrap', | ||
| focusableElementFilter: element => { | ||
| return !(element instanceof HTMLInputElement) | ||
| }, | ||
| activeDescendantFocus: inputRef, | ||
| onActiveDescendantChanged: (current, previous, directlyActivated) => { | ||
| activeDescendantRef.current = current | ||
| if (current && scrollContainerRef.current && directlyActivated) { | ||
| scrollIntoView(current, scrollContainerRef.current, menuScrollMargins) | ||
| } | ||
| }, | ||
| }, | ||
| [ | ||
| // List ref isn't set while loading. Need to re-bind focus zone when it changes | ||
| loading, | ||
| ], | ||
| ) | ||
| useEffect(() => { | ||
| // if items changed, we want to instantly move active descendant into view | ||
| if (activeDescendantRef.current && scrollContainerRef.current) { | ||
| scrollIntoView(activeDescendantRef.current, scrollContainerRef.current, {...menuScrollMargins, behavior: 'auto'}) | ||
| } | ||
| }, [items]) | ||
| useScrollFlash(scrollContainerRef) | ||
| function getItemListForEachGroup(groupId: string) { | ||
| const itemsInGroup = [] | ||
| for (const item of items) { | ||
| // Look up the group associated with the current item. | ||
| if (item.groupId === groupId) { | ||
| itemsInGroup.push(item) | ||
| } | ||
| } | ||
| return itemsInGroup | ||
| } | ||
| return ( | ||
| <Box display="flex" flexDirection="column" overflow="hidden" sx={sx}> | ||
| <StyledHeader> | ||
| <TextInput | ||
| ref={inputRef} | ||
| block | ||
| width="auto" | ||
| color="fg.default" | ||
| value={filterValue} | ||
| onChange={onInputChange} | ||
| onKeyPress={onInputKeyPress} | ||
| placeholder={placeholderText} | ||
| aria-label={placeholderText} | ||
| aria-controls={listId} | ||
| aria-describedby={inputDescriptionTextId} | ||
| {...textInputProps} | ||
| /> | ||
| </StyledHeader> | ||
| <VisuallyHidden id={inputDescriptionTextId}>Items will be filtered as you type</VisuallyHidden> | ||
| <Box ref={scrollContainerRef} overflow="auto"> | ||
| {loading ? ( | ||
| <Box width="100%" display="flex" flexDirection="row" justifyContent="center" pt={6} pb={7}> | ||
| <Spinner /> | ||
| </Box> | ||
| ) : ( | ||
| <ActionList ref={listContainerRef} showDividers={showItemDividers} {...listProps} role="listbox" id={listId}> | ||
| {groupMetadata?.length | ||
| ? groupMetadata.map((group, index) => { | ||
| return ( | ||
| <ActionList.Group key={index}> | ||
| <ActionList.GroupHeading variant={group.header?.variant ? group.header.variant : undefined}> | ||
| {group.header?.title ? group.header.title : `Group ${group.groupId}`} | ||
| </ActionList.GroupHeading> | ||
| {getItemListForEachGroup(group.groupId).map((item, index) => { | ||
| return <MappedActionListItem key={index} {...item} renderItem={listProps.renderItem} /> | ||
| })} | ||
| </ActionList.Group> | ||
| ) | ||
| }) | ||
| : items.map((item, index) => { | ||
| return <MappedActionListItem key={index} {...item} renderItem={listProps.renderItem} /> | ||
| })} | ||
| </ActionList> | ||
| )} | ||
| </Box> | ||
| </Box> | ||
| ) | ||
| } | ||
| function MappedActionListItem(item: ItemInput & {renderItem?: RenderItemFn}) { | ||
| // keep backward compatibility for renderItem | ||
| // escape hatch for custom Item rendering | ||
| if (typeof item.renderItem === 'function') return item.renderItem(item) | ||
| const { | ||
| id, | ||
| description, | ||
| descriptionVariant, | ||
| text, | ||
| trailingVisual: TrailingVisual, | ||
| leadingVisual: LeadingVisual, | ||
| trailingText, | ||
| trailingIcon: TrailingIcon, | ||
| onAction, | ||
| children, | ||
| ...rest | ||
| } = item | ||
| return ( | ||
| <ActionList.Item | ||
| role="option" | ||
| // @ts-ignore - for now | ||
| onSelect={(e: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>) => { | ||
| if (typeof onAction === 'function') | ||
| onAction(item, e as React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>) | ||
| }} | ||
| data-id={id} | ||
| {...rest} | ||
| > | ||
| {LeadingVisual ? ( | ||
| <ActionList.LeadingVisual> | ||
| <LeadingVisual /> | ||
| </ActionList.LeadingVisual> | ||
| ) : null} | ||
| {children} | ||
| {text} | ||
| {description ? <ActionList.Description variant={descriptionVariant}>{description}</ActionList.Description> : null} | ||
| {TrailingVisual ? ( | ||
| <ActionList.TrailingVisual> | ||
| {typeof TrailingVisual !== 'string' && isValidElementType(TrailingVisual) ? ( | ||
| <TrailingVisual /> | ||
| ) : ( | ||
| TrailingVisual | ||
| )} | ||
| </ActionList.TrailingVisual> | ||
| ) : TrailingIcon || trailingText ? ( | ||
| <ActionList.TrailingVisual> | ||
| {trailingText} | ||
| {TrailingIcon && <TrailingIcon />} | ||
| </ActionList.TrailingVisual> | ||
| ) : null} | ||
| </ActionList.Item> | ||
| ) | ||
| } | ||
| FilteredActionList.displayName = 'FilteredActionList' | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,2 @@ | ||
| export {FilteredActionList} from './FilteredActionList' | ||
| export type {FilteredActionListProps} from './FilteredActionList' | ||
| export {FilteredActionList} from './FilteredActionListEntry' | ||
| export type {FilteredActionListProps} from './FilteredActionListEntry' |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is
itemsa memoized value or is a plain value typically?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They are memoized in SelectPanel before passing to FilteredActionList. Tell me more?