diff --git a/.changeset/fluffy-waves-kiss.md b/.changeset/fluffy-waves-kiss.md new file mode 100644 index 00000000000..cad4be49476 --- /dev/null +++ b/.changeset/fluffy-waves-kiss.md @@ -0,0 +1,7 @@ +--- +"@primer/react": minor +--- + +Remove deprecated ActionList from SelectPanel and FilteredActionList. + + diff --git a/src/ActionList/Item.tsx b/src/ActionList/Item.tsx index 3c25f839911..bc66075b90e 100644 --- a/src/ActionList/Item.tsx +++ b/src/ActionList/Item.tsx @@ -178,9 +178,10 @@ export const Item = React.forwardRef( ) // use props.id if provided, otherwise generate one. - const labelId = useId(id) - const inlineDescriptionId = useId(id && `${id}--inline-description`) - const blockDescriptionId = useId(id && `${id}--block-description`) + const _id = id !== undefined ? id.toString() : undefined + const labelId = useId(_id) + const inlineDescriptionId = useId(_id && `${_id}--inline-description`) + const blockDescriptionId = useId(_id && `${_id}--block-description`) const ItemWrapper = _PrivateItemWrapper || React.Fragment diff --git a/src/ActionList/shared.ts b/src/ActionList/shared.ts index 10b79112915..47bbb1d32fc 100644 --- a/src/ActionList/shared.ts +++ b/src/ActionList/shared.ts @@ -37,7 +37,7 @@ export type ActionListItemProps = { /** * id to attach to the root element of the Item */ - id?: string + id?: string | number /** * Private API for use internally only. Used by LinkItem to wrap contents in an anchor */ diff --git a/src/Autocomplete/AutocompleteMenu.tsx b/src/Autocomplete/AutocompleteMenu.tsx index 30330666fab..f1bbad915cc 100644 --- a/src/Autocomplete/AutocompleteMenu.tsx +++ b/src/Autocomplete/AutocompleteMenu.tsx @@ -149,7 +149,7 @@ function AutocompleteMenu(props: AutocompleteMe const listContainerRef = useRef(null) const allItemsToRenderRef = useRef([]) const [highlightedItem, setHighlightedItem] = useState() - const [sortedItemIds, setSortedItemIds] = useState>(items.map(({id: itemId}) => itemId)) + const [sortedItemIds, setSortedItemIds] = useState>(items.map(({id: itemId}) => itemId.toString())) const generatedUniqueId = useSSRSafeId(id) const selectableItems = useMemo( @@ -160,10 +160,11 @@ function AutocompleteMenu(props: AutocompleteMe role: 'option', id: selectableItem.id, active: highlightedItem?.id === selectableItem.id, - selected: selectionVariant === 'multiple' ? selectedItemIds.includes(selectableItem.id) : undefined, + selected: + selectionVariant === 'multiple' ? selectedItemIds.includes(selectableItem.id.toString()) : undefined, onAction: (item: T) => { const otherSelectedItemIds = selectedItemIds.filter(selectedItemId => selectedItemId !== item.id) - const newSelectedItemIds = selectedItemIds.includes(item.id) + const newSelectedItemIds = selectedItemIds.includes(item.id.toString()) ? otherSelectedItemIds : [...otherSelectedItemIds, item.id] const onSelectedChangeFn = onSelectedChange @@ -171,7 +172,7 @@ function AutocompleteMenu(props: AutocompleteMe : getdefaultCheckedSelectionChange(setInputValue) onSelectedChangeFn( - newSelectedItemIds.map(newSelectedItemId => getItemById(newSelectedItemId, items)) as T[], + newSelectedItemIds.map(newSelectedItemId => getItemById(newSelectedItemId.toString(), items)) as T[], ) if (selectionVariant === 'multiple') { @@ -228,7 +229,8 @@ function AutocompleteMenu(props: AutocompleteMe role: 'option', key: addNewItem.id, active: highlightedItem?.id === addNewItem.id, - selected: selectionVariant === 'multiple' ? selectedItemIds.includes(addNewItem.id) : undefined, + selected: + selectionVariant === 'multiple' ? selectedItemIds.includes(addNewItem.id.toString()) : undefined, leadingVisual: () => , onAction: (item: T) => { // TODO: make it possible to pass a leadingVisual when using `addNewItem` @@ -289,7 +291,7 @@ function AutocompleteMenu(props: AutocompleteMe ) useEffect(() => { - if (highlightedItem?.text?.startsWith(inputValue) && !selectedItemIds.includes(highlightedItem.id)) { + if (highlightedItem?.text?.startsWith(inputValue) && !selectedItemIds.includes(highlightedItem.id.toString())) { setAutocompleteSuggestion(highlightedItem.text) } else { setAutocompleteSuggestion('') diff --git a/src/FilteredActionList/FilteredActionList.stories.tsx b/src/FilteredActionList/FilteredActionList.stories.tsx index 1b8d938c500..a7867d5cd44 100644 --- a/src/FilteredActionList/FilteredActionList.stories.tsx +++ b/src/FilteredActionList/FilteredActionList.stories.tsx @@ -43,13 +43,13 @@ function getColorCircle(color: string) { } const items = [ - {leadingVisual: getColorCircle('#a2eeef'), text: 'enhancement', id: 1}, - {leadingVisual: getColorCircle('#d73a4a'), text: 'bug', id: 2}, - {leadingVisual: getColorCircle('#0cf478'), text: 'good first issue', id: 3}, - {leadingVisual: getColorCircle('#ffd78e'), text: 'design', id: 4}, - {leadingVisual: getColorCircle('#ff0000'), text: 'blocker', id: 5}, - {leadingVisual: getColorCircle('#a4f287'), text: 'backend', id: 6}, - {leadingVisual: getColorCircle('#8dc6fc'), text: 'frontend', id: 7}, + {leadingVisual: getColorCircle('#a2eeef'), text: 'enhancement', id: '1'}, + {leadingVisual: getColorCircle('#d73a4a'), text: 'bug', id: '2'}, + {leadingVisual: getColorCircle('#0cf478'), text: 'good first issue', id: '3'}, + {leadingVisual: getColorCircle('#ffd78e'), text: 'design', id: '4'}, + {leadingVisual: getColorCircle('#ff0000'), text: 'blocker', id: '5'}, + {leadingVisual: getColorCircle('#a4f287'), text: 'backend', id: '6'}, + {leadingVisual: getColorCircle('#8dc6fc'), text: 'frontend', id: '7'}, ] export function Default(): JSX.Element { diff --git a/src/FilteredActionList/FilteredActionList.tsx b/src/FilteredActionList/FilteredActionList.tsx index 00fc8ae8bb6..87c0b9c72da 100644 --- a/src/FilteredActionList/FilteredActionList.tsx +++ b/src/FilteredActionList/FilteredActionList.tsx @@ -6,8 +6,7 @@ import Box from '../Box' import Spinner from '../Spinner' import TextInput, {TextInputProps} from '../TextInput' import {get} from '../constants' -import {ActionList} from '../deprecated/ActionList' -import {GroupedListProps, ListPropsBase} from '../deprecated/ActionList/List' +import {ActionList, ActionListProps, ActionListItemProps} from '../ActionList' import {useFocusZone} from '../hooks/useFocusZone' import {useId} from '../hooks/useId' import {useProvidedRefOrCreate} from '../hooks/useProvidedRefOrCreate' @@ -15,19 +14,30 @@ import {useProvidedStateOrCreate} from '../hooks/useProvidedStateOrCreate' import useScrollFlash from '../hooks/useScrollFlash' import {VisuallyHidden} from '../internal/components/VisuallyHidden' import {SxProp} from '../sx' +import {isValidElementType} from 'react-is' const menuScrollMargins: ScrollIntoViewOptions = {startMargin: 0, endMargin: 8} -export interface FilteredActionListProps - extends Partial>, - ListPropsBase, - SxProp { +export type ItemInput = Partial< + ActionListItemProps & { + description?: string | React.ReactElement + descriptionVariant?: 'inline' | 'block' + leadingVisual?: React.ElementType + onAction?: (itemFromAction: ItemInput, event: React.MouseEvent) => void + selected?: boolean + text?: string + trailingVisual?: React.ElementType | React.ReactNode + } +> + +export interface FilteredActionListProps extends ActionListProps, SxProp { loading?: boolean placeholderText?: string filterValue?: string onFilterChange: (value: string, e: React.ChangeEvent) => void textInputProps?: Partial> inputRef?: React.RefObject + items: ItemInput[] } const StyledHeader = styled.div` @@ -35,6 +45,39 @@ const StyledHeader = styled.div` z-index: 1; ` +const renderFn = ({ + description, + descriptionVariant, + id, + sx, + text, + trailingVisual: TrailingVisual, + leadingVisual: LeadingVisual, + onSelect, + selected, +}: ItemInput): React.ReactElement => { + return ( + + {!!LeadingVisual && ( + + + + )} + {text ? text : null} + {description ? {description} : null} + {!!TrailingVisual && ( + + {typeof TrailingVisual !== 'string' && isValidElementType(TrailingVisual) ? ( + + ) : ( + TrailingVisual + )} + + )} + + ) +} + export function FilteredActionList({ loading = false, placeholderText, @@ -57,7 +100,7 @@ export function FilteredActionList({ ) const scrollContainerRef = useRef(null) - const listContainerRef = useRef(null) + const listContainerRef = useRef(null) const inputRef = useProvidedRefOrCreate(providedInputRef) const activeDescendantRef = useRef() const listId = useId() @@ -84,7 +127,7 @@ export function FilteredActionList({ return !(element instanceof HTMLInputElement) }, activeDescendantFocus: inputRef, - onActiveDescendantChanged: (current, previous, directlyActivated) => { + onActiveDescendantChanged: (current, _previous, directlyActivated) => { activeDescendantRef.current = current if (current && scrollContainerRef.current && directlyActivated) { @@ -132,7 +175,15 @@ export function FilteredActionList({ ) : ( - + + {items.map(renderFn)} + )} diff --git a/src/FilteredActionList/index.ts b/src/FilteredActionList/index.ts index 3f8176fe71c..8a96b74fd6f 100644 --- a/src/FilteredActionList/index.ts +++ b/src/FilteredActionList/index.ts @@ -1,2 +1,2 @@ export {FilteredActionList} from './FilteredActionList' -export type {FilteredActionListProps} from './FilteredActionList' +export type {FilteredActionListProps, ItemInput} from './FilteredActionList' diff --git a/src/SelectPanel/SelectPanel.features.stories.tsx b/src/SelectPanel/SelectPanel.features.stories.tsx index 42b4f6ccf24..865a9a9875f 100644 --- a/src/SelectPanel/SelectPanel.features.stories.tsx +++ b/src/SelectPanel/SelectPanel.features.stories.tsx @@ -3,7 +3,7 @@ import {ComponentMeta} from '@storybook/react' import Box from '../Box' import {Button} from '../Button' -import {ItemInput} from '../deprecated/ActionList/List' +import {ItemInput} from '../FilteredActionList' import {SelectPanel} from './SelectPanel' import {TriangleDownIcon} from '@primer/octicons-react' import type {OverlayProps} from '../Overlay' @@ -31,13 +31,13 @@ function getColorCircle(color: string) { } const items = [ - {leadingVisual: getColorCircle('#a2eeef'), text: 'enhancement', id: 1}, - {leadingVisual: getColorCircle('#d73a4a'), text: 'bug', id: 2}, - {leadingVisual: getColorCircle('#0cf478'), text: 'good first issue', id: 3}, - {leadingVisual: getColorCircle('#ffd78e'), text: 'design', id: 4}, - {leadingVisual: getColorCircle('#ff0000'), text: 'blocker', id: 5}, - {leadingVisual: getColorCircle('#a4f287'), text: 'backend', id: 6}, - {leadingVisual: getColorCircle('#8dc6fc'), text: 'frontend', id: 7}, + {leadingVisual: getColorCircle('#a2eeef'), text: 'enhancement', id: '1'}, + {leadingVisual: getColorCircle('#d73a4a'), text: 'bug', id: '2'}, + {leadingVisual: getColorCircle('#0cf478'), text: 'good first issue', id: '3'}, + {leadingVisual: getColorCircle('#ffd78e'), text: 'design', id: '4'}, + {leadingVisual: getColorCircle('#ff0000'), text: 'blocker', id: '5'}, + {leadingVisual: getColorCircle('#a4f287'), text: 'backend', id: '6'}, + {leadingVisual: getColorCircle('#8dc6fc'), text: 'frontend', id: '7'}, ] export const SingleSelectStory = () => { @@ -63,7 +63,7 @@ export const SingleSelectStory = () => { selected={selected} onSelectedChange={setSelected} onFilterChange={setFilter} - showItemDividers={true} + showDividers={true} overlayProps={{width: 'small', height: 'xsmall'}} /> @@ -94,7 +94,7 @@ export const ExternalAnchorStory = () => { selected={selected} onSelectedChange={setSelected} onFilterChange={setFilter} - showItemDividers={true} + showDividers={true} overlayProps={{width: 'small', height: 'xsmall'}} /> @@ -125,7 +125,7 @@ export const SelectPanelHeightInitialWithOverflowingItemsStory = () => { selected={selected} onSelectedChange={setSelected} onFilterChange={setFilter} - showItemDividers={true} + showDividers={true} overlayProps={{width: 'small', height: 'initial', maxHeight: 'xsmall'}} /> @@ -157,7 +157,7 @@ export const SelectPanelHeightInitialWithUnderflowingItemsStory = () => { selected={selected} onSelectedChange={setSelected} onFilterChange={setFilter} - showItemDividers={true} + showDividers={true} overlayProps={{width: 'small', height: 'initial', maxHeight: 'xsmall'}} /> @@ -202,7 +202,7 @@ export const SelectPanelHeightInitialWithUnderflowingItemsAfterFetch = () => { selected={selected} onSelectedChange={setSelected} onFilterChange={setFilter} - showItemDividers={true} + showDividers={true} overlayProps={{width: 'small', height, maxHeight: 'xsmall'}} /> @@ -234,7 +234,7 @@ export const SelectPanelAboveTallBody = () => { selected={selected} onSelectedChange={setSelected} onFilterChange={setFilter} - showItemDividers={true} + showDividers={true} overlayProps={{width: 'small', height: 'xsmall'}} />
{ selected={selectedA} onSelectedChange={setSelectedA} onFilterChange={setFilter} - showItemDividers={true} + showDividers={true} overlayProps={{height: 'medium'}} />

With height:auto, maxheight:medium

@@ -293,7 +293,7 @@ export const SelectPanelHeightAndScroll = () => { selected={selectedB} onSelectedChange={setSelectedB} onFilterChange={setFilter} - showItemDividers={true} + showDividers={true} overlayProps={{ height: 'auto', maxHeight: 'medium', diff --git a/src/SelectPanel/SelectPanel.stories.tsx b/src/SelectPanel/SelectPanel.stories.tsx index c74b251991f..2881a52cb7e 100644 --- a/src/SelectPanel/SelectPanel.stories.tsx +++ b/src/SelectPanel/SelectPanel.stories.tsx @@ -5,7 +5,7 @@ import React, {useState} from 'react' import Box from '../Box' import {Button} from '../Button' import {SelectPanel} from '../SelectPanel' -import {ItemInput} from '../deprecated/ActionList/List' +import {ItemInput} from '../FilteredActionList' export default { title: 'Components/SelectPanel', @@ -32,13 +32,13 @@ function getColorCircle(color: string) { } const items = [ - {leadingVisual: getColorCircle('#a2eeef'), text: 'enhancement', id: 1}, - {leadingVisual: getColorCircle('#d73a4a'), text: 'bug', id: 2}, - {leadingVisual: getColorCircle('#0cf478'), text: 'good first issue', id: 3}, - {leadingVisual: getColorCircle('#ffd78e'), text: 'design', id: 4}, - {leadingVisual: getColorCircle('#ff0000'), text: 'blocker', id: 5}, - {leadingVisual: getColorCircle('#a4f287'), text: 'backend', id: 6}, - {leadingVisual: getColorCircle('#8dc6fc'), text: 'frontend', id: 7}, + {leadingVisual: getColorCircle('#a2eeef'), text: 'enhancement', id: '1'}, + {leadingVisual: getColorCircle('#d73a4a'), text: 'bug', id: '2'}, + {leadingVisual: getColorCircle('#0cf478'), text: 'good first issue', id: '3'}, + {leadingVisual: getColorCircle('#ffd78e'), text: 'design', id: '4'}, + {leadingVisual: getColorCircle('#ff0000'), text: 'blocker', id: '5'}, + {leadingVisual: getColorCircle('#a4f287'), text: 'backend', id: '6'}, + {leadingVisual: getColorCircle('#8dc6fc'), text: 'frontend', id: '7'}, ] export const Default = () => { @@ -65,7 +65,6 @@ export const Default = () => { selected={selected} onSelectedChange={setSelected} onFilterChange={setFilter} - showItemDividers={true} overlayProps={{width: 'small', height: 'xsmall'}} /> diff --git a/src/SelectPanel/SelectPanel.test.tsx b/src/SelectPanel/SelectPanel.test.tsx index 05332623326..551d742aaa3 100644 --- a/src/SelectPanel/SelectPanel.test.tsx +++ b/src/SelectPanel/SelectPanel.test.tsx @@ -5,7 +5,7 @@ import theme from '../theme' import {SelectPanel} from '../SelectPanel' import {behavesAsComponent, checkExports} from '../utils/testing' import {BaseStyles, SSRProvider, ThemeProvider} from '..' -import {ItemInput} from '../deprecated/ActionList/List' +import {ItemInput} from '../FilteredActionList' expect.extend(toHaveNoViolations) diff --git a/src/SelectPanel/SelectPanel.tsx b/src/SelectPanel/SelectPanel.tsx index 2c79006ccfa..4c24b1b2a51 100644 --- a/src/SelectPanel/SelectPanel.tsx +++ b/src/SelectPanel/SelectPanel.tsx @@ -3,12 +3,11 @@ import React, {useCallback, useMemo} from 'react' import {AnchoredOverlay, AnchoredOverlayProps} from '../AnchoredOverlay' import {AnchoredOverlayWrapperAnchorProps} from '../AnchoredOverlay/AnchoredOverlay' import Box from '../Box' -import {FilteredActionList, FilteredActionListProps} from '../FilteredActionList' +import {FilteredActionList, FilteredActionListProps, ItemInput} from '../FilteredActionList' +import {ActionListItemProps} from '../ActionList' import Heading from '../Heading' import {OverlayProps} from '../Overlay' import {TextInputProps} from '../TextInput' -import {ItemProps} from '../deprecated/ActionList' -import {ItemInput} from '../deprecated/ActionList/List' import {Button} from '../Button' import {useProvidedRefOrCreate} from '../hooks' import {FocusZoneHookSettings} from '../hooks/useFocusZone' @@ -130,9 +129,7 @@ export function SelectPanel({ ...item, role: 'option', selected: 'selected' in item && item.selected === undefined ? undefined : isItemSelected, - onAction: (itemFromAction, event) => { - item.onAction?.(itemFromAction, event) - + onSelect: (event: React.MouseEvent | React.KeyboardEvent) => { if (event.defaultPrevented) { return } @@ -151,7 +148,7 @@ export function SelectPanel({ singleSelectOnChange(item === selected ? undefined : item) onClose('selection') }, - } as ItemProps + } as ActionListItemProps }) }, [onClose, onSelectedChange, items, selected]) @@ -212,10 +209,10 @@ export function SelectPanel({ filterValue={filterValue} onFilterChange={onFilterChange} placeholderText={placeholderText} - {...listProps} role="listbox" aria-multiselectable={isMultiSelectVariant(selected) ? 'true' : 'false'} selectionVariant={isMultiSelectVariant(selected) ? 'multiple' : 'single'} + {...listProps} items={itemsToRender} textInputProps={extendedTextInputProps} inputRef={inputRef} diff --git a/src/drafts/MarkdownEditor/_SavedReplies.tsx b/src/drafts/MarkdownEditor/_SavedReplies.tsx index d10b4b4ee5f..b468280b4d2 100644 --- a/src/drafts/MarkdownEditor/_SavedReplies.tsx +++ b/src/drafts/MarkdownEditor/_SavedReplies.tsx @@ -9,6 +9,7 @@ import React, { useState, } from 'react' import {SelectPanel, SelectPanelProps} from '../../SelectPanel' +import Truncate from '../../Truncate' import {ToolbarButton} from './Toolbar' export type SavedReply = { @@ -53,7 +54,11 @@ export const SavedRepliesButton = () => { .map( (reply, i): Item => ({ text: reply.name, - description: reply.content, + description: ( + + {reply.content} + + ), descriptionVariant: 'block', trailingVisual: i < 9 ? `Ctrl + ${i + 1}` : undefined, sx: { @@ -66,6 +71,7 @@ export const SavedRepliesButton = () => { maxWidth: '100%', }, }, + id: i.toString(), }), )