From 74463fa1e1198a083064c6986039da9fa1cca9ce Mon Sep 17 00:00:00 2001 From: Ernst Kaese Date: Thu, 23 Jul 2026 13:51:28 +0000 Subject: [PATCH 1/2] feat: add selectable variant to List component --- pages/list/selectable.page.tsx | 69 +++++++ .../__snapshots__/documenter.test.ts.snap | 112 +++++++++++ src/list/__tests__/list-selectable.test.tsx | 190 ++++++++++++++++++ src/list/interfaces.ts | 29 +++ src/list/internal.tsx | 154 +++++++++++++- src/list/styles.scss | 53 +++++ src/list/test-classes/styles.scss | 4 +- src/test-utils/dom/list/index.ts | 12 ++ 8 files changed, 621 insertions(+), 2 deletions(-) create mode 100644 pages/list/selectable.page.tsx create mode 100644 src/list/__tests__/list-selectable.test.tsx diff --git a/pages/list/selectable.page.tsx b/pages/list/selectable.page.tsx new file mode 100644 index 0000000000..2a7945b183 --- /dev/null +++ b/pages/list/selectable.page.tsx @@ -0,0 +1,69 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { useState } from 'react'; + +import { Box, SpaceBetween } from '~components'; +import List, { ListProps } from '~components/list'; + +import ScreenshotArea from '../utils/screenshot-area'; + +interface Item { + id: string; + content: string; + description?: string; + disabled?: boolean; +} + +const items: Item[] = [ + { id: 'apples', content: 'Apples', description: 'Crisp and sweet' }, + { id: 'bananas', content: 'Bananas', description: 'Rich in potassium' }, + { id: 'cherries', content: 'Cherries', description: 'Currently out of stock', disabled: true }, + { id: 'dates', content: 'Dates', description: 'Naturally sweet' }, + { id: 'elderberries', content: 'Elderberries', description: 'Best cooked' }, +]; + +const renderItem = (item: Item): ReturnType['renderItem']> => ({ + id: item.id, + content: item.content, + secondaryContent: item.description && {item.description}, +}); + +export default function SelectableListPage() { + const [singleSelected, setSingleSelected] = useState>([items[0]]); + const [multiSelected, setMultiSelected] = useState>([items[1], items[3]]); + + return ( + +

Selectable list

+ +
+

Single selection

+ !!item.disabled} + selectedItems={singleSelected} + onSelectionChange={({ detail }) => setSingleSelected(detail.selectedItems)} + /> + Selected: {singleSelected.map(item => item.content).join(', ') || 'none'} +
+ +
+

Multi selection

+ !!item.disabled} + selectedItems={multiSelected} + onSelectionChange={({ detail }) => setMultiSelected(detail.selectedItems)} + /> + Selected: {multiSelected.map(item => item.content).join(', ') || 'none'} +
+
+
+ ); +} diff --git a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap index b54c1b56ab..1a305c3f41 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap @@ -18586,6 +18586,23 @@ exports[`Components definition for list matches the snapshot: list 1`] = ` { "dashCaseName": "list", "events": [ + { + "cancelable": false, + "description": "Called when the selection changes. The event \`detail\` contains the updated \`selectedItems\`.", + "detailInlineType": { + "name": "ListProps.SelectionChangeDetail", + "properties": [ + { + "name": "selectedItems", + "optional": false, + "type": "ReadonlyArray", + }, + ], + "type": "object", + }, + "detailType": "ListProps.SelectionChangeDetail", + "name": "onSelectionChange", + }, { "cancelable": false, "description": "Called when items are reordered in a sortable list.", @@ -18740,6 +18757,24 @@ exports[`Components definition for list matches the snapshot: list 1`] = ` "optional": true, "type": "DndAreaI18nStrings", }, + { + "description": "A function that determines whether a given item can be selected. Return \`true\` to +prevent an item from being selected.", + "inlineType": { + "name": "(item: T) => boolean", + "parameters": [ + { + "name": "item", + "type": "T", + }, + ], + "returnType": "boolean", + "type": "function", + }, + "name": "isItemDisabled", + "optional": true, + "type": "((item: T) => boolean)", + }, { "description": "The items to display in the list.", "name": "items", @@ -18770,6 +18805,32 @@ exports[`Components definition for list matches the snapshot: list 1`] = ` "optional": false, "type": "(item: T) => { id: string; content: React.ReactNode; secondaryContent?: React.ReactNode; icon?: React.ReactNode; actions?: React.ReactNode; announcementLabel?: string | undefined; }", }, + { + "description": "The currently selected items. Use together with \`selectionType\` and \`onSelectionChange\` +to control the selection state. Items are matched using the \`id\` returned by \`renderItem\`.", + "name": "selectedItems", + "optional": true, + "type": "ReadonlyArray", + }, + { + "description": "Enables item selection and defines the selection behavior: +* \`single\` - Only one item can be selected at a time (renders as a radio control). +* \`multi\` - Multiple items can be selected at a time (renders as a checkbox control). + +When set, the list is rendered as an ARIA \`listbox\` and each item as an \`option\`. +Selection is not supported together with \`sortable\`; when both are provided, sorting takes precedence.", + "inlineType": { + "name": "ListProps.SelectionType", + "type": "union", + "values": [ + "single", + "multi", + ], + }, + "name": "selectionType", + "optional": true, + "type": "string", + }, { "description": "Makes the list sortable by enabling drag and drop functionality.", "name": "sortable", @@ -41775,6 +41836,20 @@ The dismiss button is only rendered when the \`dismissible\` property is set to ], }, }, + { + "description": "Returns all currently selected items. Only meaningful for selectable lists.", + "name": "findSelectedItems", + "parameters": [], + "returnType": { + "isNullable": false, + "name": "Array", + "typeArguments": [ + { + "name": "ListItemWrapper", + }, + ], + }, + }, ], "name": "ListWrapper", }, @@ -41857,6 +41932,20 @@ The dismiss button is only rendered when the \`dismissible\` property is set to ], }, }, + { + "description": "Returns the selection control (checkbox or radio) for a selectable list item, if present.", + "name": "findSelectionControl", + "parameters": [], + "returnType": { + "isNullable": true, + "name": "ElementWrapper", + "typeArguments": [ + { + "name": "HTMLElement", + }, + ], + }, + }, ], "name": "ListItemWrapper", }, @@ -51998,6 +52087,20 @@ The dismiss button is only rendered when the \`dismissible\` property is set to ], }, }, + { + "description": "Returns all currently selected items. Only meaningful for selectable lists.", + "name": "findSelectedItems", + "parameters": [], + "returnType": { + "isNullable": false, + "name": "MultiElementWrapper", + "typeArguments": [ + { + "name": "ListItemWrapper", + }, + ], + }, + }, ], "name": "ListWrapper", }, @@ -52055,6 +52158,15 @@ The dismiss button is only rendered when the \`dismissible\` property is set to "name": "ElementWrapper", }, }, + { + "description": "Returns the selection control (checkbox or radio) for a selectable list item, if present.", + "name": "findSelectionControl", + "parameters": [], + "returnType": { + "isNullable": false, + "name": "ElementWrapper", + }, + }, ], "name": "ListItemWrapper", }, diff --git a/src/list/__tests__/list-selectable.test.tsx b/src/list/__tests__/list-selectable.test.tsx new file mode 100644 index 0000000000..afbe93fb94 --- /dev/null +++ b/src/list/__tests__/list-selectable.test.tsx @@ -0,0 +1,190 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { useState } from 'react'; +import { fireEvent, render } from '@testing-library/react'; + +import { KeyCode } from '../../../lib/components/internal/keycode'; +import List, { ListProps } from '../../../lib/components/list'; +import createWrapper from '../../../lib/components/test-utils/dom'; +import { ListItemWrapper } from '../../../lib/components/test-utils/dom/list'; + +interface Item { + id: string; + content: string; + disabled?: boolean; +} + +const defaultItems: Item[] = [ + { id: 'item1', content: 'Item 1' }, + { id: 'item2', content: 'Item 2' }, + { id: 'item3', content: 'Item 3', disabled: true }, + { id: 'item4', content: 'Item 4' }, +]; + +const renderItemFn = (item: Item) => ({ id: item.id, content: item.content }); + +const isSelected = (item: ListItemWrapper) => item.getElement().getAttribute('aria-selected') === 'true'; + +function StatefulList(props: Partial> & { selectionType: ListProps.SelectionType }) { + const [selectedItems, setSelectedItems] = useState>(props.selectedItems ?? []); + return ( + !!item.disabled} + {...props} + selectedItems={selectedItems} + onSelectionChange={event => { + setSelectedItems(event.detail.selectedItems); + props.onSelectionChange?.(event); + }} + /> + ); +} + +const renderList = (props: Partial> & { selectionType: ListProps.SelectionType }) => { + const renderResult = render(); + return { + wrapper: createWrapper(renderResult.container).findList()!, + rerender: renderResult.rerender, + }; +}; + +describe('List - selectable variant', () => { + test('renders as a listbox with options when selectionType is set', () => { + const { wrapper } = renderList({ selectionType: 'multi' }); + expect(wrapper.getElement()).toHaveAttribute('role', 'listbox'); + const items = wrapper.findItems(); + expect(items).toHaveLength(4); + items.forEach(item => expect(item.getElement()).toHaveAttribute('role', 'option')); + }); + + test('does not add listbox semantics when selectionType is not set (backward compatible)', () => { + const renderResult = render(); + const wrapper = createWrapper(renderResult.container).findList()!; + expect(wrapper.getElement()).not.toHaveAttribute('role', 'listbox'); + expect(wrapper.findItemByIndex(1)!.getElement()).not.toHaveAttribute('role', 'option'); + expect(wrapper.findItemByIndex(1)!.findSelectionControl()).toBeNull(); + }); + + test('sets aria-multiselectable for multi selection only', () => { + const { wrapper: multi } = renderList({ selectionType: 'multi' }); + expect(multi.getElement()).toHaveAttribute('aria-multiselectable', 'true'); + + const { wrapper: single } = renderList({ selectionType: 'single' }); + expect(single.getElement()).not.toHaveAttribute('aria-multiselectable'); + }); + + test('renders a selection control for every item', () => { + const { wrapper } = renderList({ selectionType: 'multi' }); + wrapper.findItems().forEach(item => expect(item.findSelectionControl()).not.toBeNull()); + }); + + test('reflects the controlled selectedItems via aria-selected', () => { + const { wrapper } = renderList({ selectionType: 'multi', selectedItems: [defaultItems[0], defaultItems[3]] }); + expect(isSelected(wrapper.findItemById('item1')!)).toBe(true); + expect(isSelected(wrapper.findItemById('item2')!)).toBe(false); + expect(isSelected(wrapper.findItemById('item4')!)).toBe(true); + expect(wrapper.findSelectedItems()).toHaveLength(2); + }); + + test('multi selection toggles items on click', () => { + const onSelectionChange = jest.fn(); + const { wrapper } = renderList({ selectionType: 'multi', onSelectionChange }); + + wrapper.findItemById('item1')!.click(); + expect(onSelectionChange).toHaveBeenLastCalledWith( + expect.objectContaining({ detail: { selectedItems: [defaultItems[0]] } }) + ); + expect(isSelected(wrapper.findItemById('item1')!)).toBe(true); + + wrapper.findItemById('item2')!.click(); + expect(wrapper.findSelectedItems().map(i => i.getElement().getAttribute('data-testid'))).toEqual([ + 'item1', + 'item2', + ]); + + // toggling off + wrapper.findItemById('item1')!.click(); + expect(isSelected(wrapper.findItemById('item1')!)).toBe(false); + expect(wrapper.findSelectedItems()).toHaveLength(1); + }); + + test('single selection replaces the selection and does not deselect on re-click', () => { + const onSelectionChange = jest.fn(); + const { wrapper } = renderList({ selectionType: 'single', onSelectionChange }); + + wrapper.findItemById('item1')!.click(); + expect(wrapper.findSelectedItems().map(i => i.getElement().getAttribute('data-testid'))).toEqual(['item1']); + + wrapper.findItemById('item2')!.click(); + expect(wrapper.findSelectedItems().map(i => i.getElement().getAttribute('data-testid'))).toEqual(['item2']); + + onSelectionChange.mockClear(); + // re-clicking the selected item keeps it selected and fires no event + wrapper.findItemById('item2')!.click(); + expect(onSelectionChange).not.toHaveBeenCalled(); + expect(isSelected(wrapper.findItemById('item2')!)).toBe(true); + }); + + test('does not select disabled items', () => { + const onSelectionChange = jest.fn(); + const { wrapper } = renderList({ selectionType: 'multi', onSelectionChange }); + const disabledItem = wrapper.findItemById('item3')!; + expect(disabledItem.getElement()).toHaveAttribute('aria-disabled', 'true'); + + disabledItem.click(); + expect(onSelectionChange).not.toHaveBeenCalled(); + expect(isSelected(disabledItem)).toBe(false); + }); + + test('toggles selection with Space and Enter keys', () => { + const { wrapper } = renderList({ selectionType: 'multi' }); + const item1 = wrapper.findItemById('item1')!.getElement(); + + fireEvent.keyDown(item1, { keyCode: KeyCode.space }); + expect(isSelected(wrapper.findItemById('item1')!)).toBe(true); + + const item2 = wrapper.findItemById('item2')!.getElement(); + fireEvent.keyDown(item2, { keyCode: KeyCode.enter }); + expect(isSelected(wrapper.findItemById('item2')!)).toBe(true); + }); + + test('manages roving tabindex and arrow-key focus', () => { + const { wrapper } = renderList({ selectionType: 'single' }); + const items = wrapper.findItems().map(i => i.getElement()); + + // first item is focusable by default + expect(items[0]).toHaveAttribute('tabindex', '0'); + expect(items[1]).toHaveAttribute('tabindex', '-1'); + + fireEvent.keyDown(items[0], { keyCode: KeyCode.down }); + expect(items[1]).toHaveAttribute('tabindex', '0'); + expect(items[1]).toHaveFocus(); + + fireEvent.keyDown(items[1], { keyCode: KeyCode.up }); + expect(items[0]).toHaveAttribute('tabindex', '0'); + expect(items[0]).toHaveFocus(); + + fireEvent.keyDown(items[0], { keyCode: KeyCode.end }); + expect(items[3]).toHaveFocus(); + + fireEvent.keyDown(items[3], { keyCode: KeyCode.home }); + expect(items[0]).toHaveFocus(); + }); + + test('sorting takes precedence over selection when both are set', () => { + const renderResult = render( + {}} + /> + ); + const wrapper = createWrapper(renderResult.container).findList()!; + expect(wrapper.getElement()).not.toHaveAttribute('role', 'listbox'); + expect(wrapper.findItemByIndex(1)!.getElement()).not.toHaveAttribute('role', 'option'); + }); +}); diff --git a/src/list/interfaces.ts b/src/list/interfaces.ts index 87180fd461..8875f246e8 100644 --- a/src/list/interfaces.ts +++ b/src/list/interfaces.ts @@ -56,6 +56,29 @@ export interface ListProps { * Disables sorting drag handles. Use this to temporarily prevent users from reordering the list. */ sortDisabled?: boolean; + /** + * Enables item selection and defines the selection behavior: + * * `single` - Only one item can be selected at a time (renders as a radio control). + * * `multi` - Multiple items can be selected at a time (renders as a checkbox control). + * + * When set, the list is rendered as an ARIA `listbox` and each item as an `option`. + * Selection is not supported together with `sortable`; when both are provided, sorting takes precedence. + */ + selectionType?: ListProps.SelectionType; + /** + * The currently selected items. Use together with `selectionType` and `onSelectionChange` + * to control the selection state. Items are matched using the `id` returned by `renderItem`. + */ + selectedItems?: ReadonlyArray; + /** + * A function that determines whether a given item can be selected. Return `true` to + * prevent an item from being selected. + */ + isItemDisabled?: (item: T) => boolean; + /** + * Called when the selection changes. The event `detail` contains the updated `selectedItems`. + */ + onSelectionChange?: NonCancelableEventHandler>; /** * Removes padding around and inside list items. */ @@ -88,5 +111,11 @@ export namespace ListProps { items: ReadonlyArray; } + export type SelectionType = 'single' | 'multi'; + + export interface SelectionChangeDetail { + selectedItems: ReadonlyArray; + } + export type I18nStrings = DndAreaI18nStrings; } diff --git a/src/list/internal.tsx b/src/list/internal.tsx index 0ba04cfabb..79dabea244 100644 --- a/src/list/internal.tsx +++ b/src/list/internal.tsx @@ -1,10 +1,11 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import React, { ReactNode } from 'react'; +import React, { ReactNode, useRef, useState } from 'react'; import clsx from 'clsx'; import { useInternalI18n } from '../i18n/context'; import { getBaseProps } from '../internal/base-component'; +import CheckboxIcon from '../internal/components/checkbox-icon'; import InternalDragHandle from '../internal/components/drag-handle'; import SortableArea from '../internal/components/sortable-area'; import { @@ -15,6 +16,7 @@ import { import InternalStructuredItem, { StructuredItemProps } from '../internal/components/structured-item'; import { fireNonCancelableEvent } from '../internal/events'; import { InternalBaseComponentProps } from '../internal/hooks/use-base-component'; +import handleKey from '../internal/utils/handle-key'; import { ListProps } from './interfaces'; import styles from './styles.css.js'; @@ -29,11 +31,54 @@ const extractValidStructuredItemProps = ({ content, secondaryContent, icon, acti actions, }); +// Purely presentational selection control. Selection state is conveyed to +// assistive technology via `aria-selected` on the enclosing `option`, so this +// SVG is marked `aria-hidden`. +function SelectionControl({ + selectionType, + selected, + disabled, +}: { + selectionType: ListProps.SelectionType; + selected: boolean; + disabled: boolean; +}) { + return ( + + ); +} + export default function InternalList({ items, renderItem, sortable = false, sortDisabled = false, + selectionType, + selectedItems = [], + isItemDisabled, + onSelectionChange, tagOverride: Tag = sortable ? 'ol' : 'ul', ariaLabel, ariaLabelledby, @@ -48,6 +93,43 @@ export default function InternalList({ const baseProps = getBaseProps(rest); const i18n = useInternalI18n('list'); + // Selection is only active for non-sortable lists. + const selectable = !!selectionType && !sortable; + + const itemRefs = useRef>([]); + const selectedIds = new Set(selectedItems.map(item => renderItem(item).id)); + const firstSelectedIndex = items?.findIndex(item => selectedIds.has(renderItem(item).id)) ?? -1; + const [focusedIndex, setFocusedIndex] = useState(firstSelectedIndex > 0 ? firstSelectedIndex : 0); + + const moveFocus = (nextIndex: number) => { + if (!items || items.length === 0) { + return; + } + const clamped = Math.max(0, Math.min(nextIndex, items.length - 1)); + setFocusedIndex(clamped); + itemRefs.current[clamped]?.focus(); + }; + + const toggleSelection = (item: T) => { + if (!onSelectionChange) { + return; + } + const id = renderItem(item).id; + let nextSelected: Array; + if (selectionType === 'multi') { + nextSelected = selectedIds.has(id) + ? selectedItems.filter(selectedItem => renderItem(selectedItem).id !== id) + : [...selectedItems, item]; + } else { + // Single selection: re-clicking the selected item keeps it selected. + if (selectedIds.has(id)) { + return; + } + nextSelected = [item]; + } + fireNonCancelableEvent(onSelectionChange, { selectedItems: nextSelected }); + }; + let contents: ReactNode; if (sortable) { contents = ( @@ -112,6 +194,74 @@ export default function InternalList({ }} /> ); + } else if (selectable) { + contents = items?.map((item, index) => { + const { id, ...structuredItemProps } = renderItem(item); + const selected = selectedIds.has(id); + const disabled = isItemDisabled?.(item) ?? false; + + const onToggle = () => { + setFocusedIndex(index); + if (!disabled) { + toggleSelection(item); + } + }; + + return ( +
  • { + itemRefs.current[index] = element; + }} + data-testid={id} + role="option" + aria-selected={selected} + aria-disabled={disabled || undefined} + tabIndex={index === focusedIndex ? 0 : -1} + className={clsx( + styles.item, + testClasses.item, + styles['selectable-item'], + selected && styles.selected, + selected && testClasses.selected, + disabled && styles['item-disabled'], + disablePaddings && styles['disable-paddings'], + disableItemPaddings && styles['disable-item-paddings'] + )} + onClick={onToggle} + onKeyDown={event => + handleKey(event as any, { + onBlockEnd: () => { + event.preventDefault(); + moveFocus(index + 1); + }, + onBlockStart: () => { + event.preventDefault(); + moveFocus(index - 1); + }, + onHome: () => { + event.preventDefault(); + moveFocus(0); + }, + onEnd: () => { + event.preventDefault(); + moveFocus(items.length - 1); + }, + onActivate: () => { + event.preventDefault(); + onToggle(); + }, + }) + } + > + + +
  • + ); + }); } else { contents = items?.map(item => { const { id, ...structuredItemProps } = renderItem(item); @@ -140,6 +290,8 @@ export default function InternalList({ ref={__internalRootRef} {...baseProps} className={clsx(baseProps.className, styles.root, testClasses.root)} + role={selectable ? 'listbox' : undefined} + aria-multiselectable={selectable && selectionType === 'multi' ? true : undefined} aria-label={ariaLabel} aria-labelledby={ariaLabelledby} aria-describedby={ariaDescribedby} diff --git a/src/list/styles.scss b/src/list/styles.scss index 8b3b8d599e..d6f94369b7 100644 --- a/src/list/styles.scss +++ b/src/list/styles.scss @@ -37,3 +37,56 @@ column-gap: awsui.$space-xxs; } } + +.selectable-item { + display: flex; + align-items: flex-start; + column-gap: awsui.$space-xs; + cursor: pointer; + + &:not(.disable-item-paddings) { + padding-inline: awsui.$space-scaled-xs; + } + + &.item-disabled { + cursor: default; + } + + &.selected { + background-color: awsui.$color-background-item-selected; + } + + &:focus { + outline: none; + } + &:focus-visible { + @include styles.focus-highlight( + 2px, + awsui.$border-radius-control-default-focus-ring, + 0 0 0 2px awsui.$color-border-item-focused + ); + } +} + +.selection-control { + display: flex; + align-items: center; + block-size: awsui.$line-height-body-m; + flex-shrink: 0; +} + +.radio-icon { + inline-size: 14px; + block-size: 14px; +} +.radio-border { + fill: awsui.$color-background-control-default; + stroke: awsui.$color-border-control-default; +} +.radio-fill { + fill: awsui.$color-background-control-checked; +} +.radio-disabled { + fill: awsui.$color-background-control-disabled; + stroke: awsui.$color-background-control-disabled; +} diff --git a/src/list/test-classes/styles.scss b/src/list/test-classes/styles.scss index 1db1540677..cbc51fedc6 100644 --- a/src/list/test-classes/styles.scss +++ b/src/list/test-classes/styles.scss @@ -4,6 +4,8 @@ */ .root, -.item { +.item, +.selection-control, +.selected { /* used in test-utils */ } diff --git a/src/test-utils/dom/list/index.ts b/src/test-utils/dom/list/index.ts index 40cb66f13a..c0de57ca1d 100644 --- a/src/test-utils/dom/list/index.ts +++ b/src/test-utils/dom/list/index.ts @@ -11,6 +11,12 @@ export class ListItemWrapper extends StructuredItemWrapper { findDragHandle(): ElementWrapper | null { return this.findByClassName(dragHandleStyles.root); } + /** + * Returns the selection control (checkbox or radio) for a selectable list item, if present. + */ + findSelectionControl(): ElementWrapper | null { + return this.findByClassName(styles['selection-control']); + } } export default class ListWrapper extends ComponentWrapper { @@ -36,4 +42,10 @@ export default class ListWrapper extends ComponentWrapper { findItemById(id: string): ListItemWrapper | null { return this.findComponent(`.${styles.item}[data-testid="${id}"]`, ListItemWrapper); } + /** + * Returns all currently selected items. Only meaningful for selectable lists. + */ + findSelectedItems(): Array { + return this.findAllByClassName(styles.selected).map(wrapper => new ListItemWrapper(wrapper.getElement())); + } } From d2e0cdfd1704ec17c4542f84957a668e092b1ba3 Mon Sep 17 00:00:00 2001 From: Ernst Kaese Date: Thu, 23 Jul 2026 16:01:32 +0000 Subject: [PATCH 2/2] test: regenerate test-utils-selectors snapshot for selectable list variant --- .../__snapshots__/test-utils-selectors.test.tsx.snap | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap b/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap index 4b1af42bc5..2eee7fd0a1 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap @@ -469,6 +469,8 @@ exports[`test-utils selectors 1`] = ` "list": [ "awsui_item_rckk5", "awsui_root_rckk5", + "awsui_selected_rckk5", + "awsui_selection-control_rckk5", ], "live-region": [ "awsui_root_1pc7b",