diff --git a/packages/react/src/SelectPanel/SelectPanel.interactions.stories.tsx b/packages/react/src/SelectPanel/SelectPanel.interactions.stories.tsx new file mode 100644 index 00000000000..81eddc7fe02 --- /dev/null +++ b/packages/react/src/SelectPanel/SelectPanel.interactions.stories.tsx @@ -0,0 +1,277 @@ +import type {Meta, StoryObj} from '@storybook/react-vite' +import React, {useState} from 'react' +import {within, userEvent, expect} from 'storybook/test' +import {SelectPanel, type SelectPanelProps} from './SelectPanel' +import type {ItemInput} from '.' +import {TriangleDownIcon} from '@primer/octicons-react' +import {Button} from '../Button' +import FormControl from '../FormControl' +import {FeatureFlags} from '../FeatureFlags' + +export default { + title: 'Components/SelectPanel/Interactions', +} as Meta + +const delay = (ms = 500) => new Promise(resolve => setTimeout(resolve, ms)) + +function getColorCircle(color: string) { + return function () { + return ( +
+ ) + } +} + +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}, +] + +const KeyboardNavigationStory = () => { + const [selected, setSelected] = useState([]) + const [filter, setFilter] = useState('') + const filteredItems = items.filter(item => item.text.toLowerCase().startsWith(filter.toLowerCase())) + const [open, setOpen] = useState(false) + + return ( + + Select labels + ( + + )} + open={open} + onOpenChange={setOpen} + items={filteredItems} + selected={selected} + onSelectedChange={setSelected} + onFilterChange={setFilter} + width="medium" + /> + + ) +} + +const KeyboardNavigationWithFeatureFlagStory = () => { + const [selected, setSelected] = useState([]) + const [filter, setFilter] = useState('') + const filteredItems = items.filter(item => item.text.toLowerCase().startsWith(filter.toLowerCase())) + const [open, setOpen] = useState(false) + + return ( + + + Select labels + ( + + )} + open={open} + onOpenChange={setOpen} + items={filteredItems} + selected={selected} + onSelectedChange={setSelected} + onFilterChange={setFilter} + width="medium" + /> + + + ) +} + +export const KeyboardNavigation: StoryObj = { + render: KeyboardNavigationStory, + play: async ({canvasElement}: {canvasElement: HTMLElement}) => { + const canvas = within(canvasElement) + + // Wait for initial render + await delay(300) + + // Find and click the button to open the panel + const button = canvas.getByRole('button', {name: /select labels/i}) + await userEvent.click(button) + + // Wait for panel to open + await delay(500) + + // Find the search input + const searchInput = canvas.getByRole('combobox', {name: /filter items/i}) + expect(searchInput).toBeInTheDocument() + + // Type to filter items + await delay(300) + await userEvent.type(searchInput, 'b') + + // Wait for filtering to occur + await delay(500) + + // Get all visible options (should be filtered to items starting with 'b') + const options = canvas.getAllByRole('option') + expect(options).toHaveLength(3) // bug, blocker, backend + + // Use arrow keys to navigate + await delay(300) + await userEvent.keyboard('{ArrowDown}') + + await delay(300) + await userEvent.keyboard('{ArrowDown}') + + await delay(300) + await userEvent.keyboard('{ArrowDown}') + + // Navigate back up + await delay(300) + await userEvent.keyboard('{ArrowUp}') + + await delay(300) + await userEvent.keyboard('{ArrowUp}') + + // Select an item with Space or Enter + await delay(300) + await userEvent.keyboard('{Enter}') + + // Wait to show the selection + await delay(500) + + // Clear the filter with backspace + await delay(500) + await userEvent.keyboard('{Backspace}') + + await delay(500) + await userEvent.type(searchInput, 'b') + + await delay(500) + await userEvent.type(searchInput, 'u') + + await delay(500) + await userEvent.keyboard('{Enter}') + + // Escape to close the panel + await delay(500) + await userEvent.keyboard('{Escape}') + }, +} + +export const KeyboardNavigationWithRovingTabindex: StoryObj = { + render: KeyboardNavigationWithFeatureFlagStory, + play: async ({canvasElement}: {canvasElement: HTMLElement}) => { + const canvas = within(canvasElement) + + // Wait for initial render + await delay(300) + + // Find and click the button to open the panel + const button = canvas.getByRole('button', {name: /select labels/i}) + await userEvent.click(button) + + // Wait for panel to open + await delay(500) + + // Find the search input + const searchInput = canvas.getByRole('combobox', {name: /Filter items/i}) + expect(searchInput).toBeInTheDocument() + + // Type to filter items + await delay(500) + await userEvent.type(searchInput, 'f') + + await delay(500) + await userEvent.type(searchInput, 'r') + + // Wait for filtering to occur + await delay(500) + + // Get all visible options (should be filtered to items starting with 'f') + const options = canvas.getAllByRole('option') + expect(options).toHaveLength(1) // frontend + + // Clear the filter to see more items + await delay(300) + await userEvent.clear(searchInput) + + await delay(300) + await userEvent.type(searchInput, 'e') + + // Wait for filtering + await delay(500) + + // Use arrow keys to navigate (roving tabindex behavior) + await delay(300) + await userEvent.keyboard('{ArrowDown}') + + await delay(300) + await userEvent.keyboard('{ArrowDown}') + + await delay(300) + await userEvent.keyboard('{ArrowDown}') + + // Navigate back up + await delay(300) + await userEvent.keyboard('{ArrowUp}') + + await delay(300) + await userEvent.keyboard('{ArrowUp}') + + // Select an item with Space or Enter + await delay(300) + await userEvent.keyboard('{Enter}') + + // Wait to show the selection + await delay(500) + + // Shift+Tab to go back to the search input + await delay(300) + await userEvent.keyboard('{Shift}{Tab}') + + // Backspace to clear the filter + await delay(300) + await userEvent.keyboard('{Backspace}') + + // Select another option + // Type to filter items + await delay(500) + await userEvent.type(searchInput, 'b') + + await delay(500) + await userEvent.type(searchInput, 'u') + + // Wait for filtering to occur + await delay(500) + + // Use arrow keys to navigate (roving tabindex behavior) + await delay(300) + await userEvent.keyboard('{ArrowDown}') + + // Select an item with Space or Enter + await delay(300) + await userEvent.keyboard('{Enter}') + + // Escape to close the panel + await delay(500) + await userEvent.keyboard('{Escape}') + }, +}