Skip to content

Commit d1ebd8a

Browse files
committed
Adds example of interactions
1 parent 21cefb9 commit d1ebd8a

File tree

1 file changed

+277
-0
lines changed

1 file changed

+277
-0
lines changed
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
import type {Meta, StoryObj} from '@storybook/react-vite'
2+
import React, {useState} from 'react'
3+
import {within, userEvent, expect} from 'storybook/test'
4+
import {SelectPanel, type SelectPanelProps} from './SelectPanel'
5+
import type {ItemInput} from '.'
6+
import {TriangleDownIcon} from '@primer/octicons-react'
7+
import {Button} from '../Button'
8+
import FormControl from '../FormControl'
9+
import {FeatureFlags} from '../FeatureFlags'
10+
11+
export default {
12+
title: 'Components/SelectPanel/Interactions',
13+
} as Meta
14+
15+
const delay = (ms = 500) => new Promise(resolve => setTimeout(resolve, ms))
16+
17+
function getColorCircle(color: string) {
18+
return function () {
19+
return (
20+
<div
21+
style={{
22+
backgroundColor: color,
23+
borderColor: color,
24+
width: 14,
25+
height: 14,
26+
borderRadius: '50%',
27+
border: '1px solid',
28+
}}
29+
/>
30+
)
31+
}
32+
}
33+
34+
const items = [
35+
{leadingVisual: getColorCircle('#a2eeef'), text: 'enhancement', id: 1},
36+
{leadingVisual: getColorCircle('#d73a4a'), text: 'bug', id: 2},
37+
{leadingVisual: getColorCircle('#0cf478'), text: 'good first issue', id: 3},
38+
{leadingVisual: getColorCircle('#ffd78e'), text: 'design', id: 4},
39+
{leadingVisual: getColorCircle('#ff0000'), text: 'blocker', id: 5},
40+
{leadingVisual: getColorCircle('#a4f287'), text: 'backend', id: 6},
41+
{leadingVisual: getColorCircle('#8dc6fc'), text: 'frontend', id: 7},
42+
]
43+
44+
const KeyboardNavigationStory = () => {
45+
const [selected, setSelected] = useState<ItemInput[]>([])
46+
const [filter, setFilter] = useState('')
47+
const filteredItems = items.filter(item => item.text.toLowerCase().startsWith(filter.toLowerCase()))
48+
const [open, setOpen] = useState(false)
49+
50+
return (
51+
<FormControl>
52+
<FormControl.Label>Select labels</FormControl.Label>
53+
<SelectPanel
54+
title="Select labels"
55+
placeholder="Select labels"
56+
subtitle="Use labels to organize issues and pull requests"
57+
renderAnchor={({children, ...anchorProps}) => (
58+
<Button trailingAction={TriangleDownIcon} {...anchorProps}>
59+
{children}
60+
</Button>
61+
)}
62+
open={open}
63+
onOpenChange={setOpen}
64+
items={filteredItems}
65+
selected={selected}
66+
onSelectedChange={setSelected}
67+
onFilterChange={setFilter}
68+
width="medium"
69+
/>
70+
</FormControl>
71+
)
72+
}
73+
74+
const KeyboardNavigationWithFeatureFlagStory = () => {
75+
const [selected, setSelected] = useState<ItemInput[]>([])
76+
const [filter, setFilter] = useState('')
77+
const filteredItems = items.filter(item => item.text.toLowerCase().startsWith(filter.toLowerCase()))
78+
const [open, setOpen] = useState(false)
79+
80+
return (
81+
<FeatureFlags flags={{primer_react_select_panel_remove_active_descendant: true}}>
82+
<FormControl>
83+
<FormControl.Label>Select labels</FormControl.Label>
84+
<SelectPanel
85+
title="Select labels"
86+
placeholder="Select labels"
87+
subtitle="Use labels to organize issues and pull requests"
88+
renderAnchor={({children, ...anchorProps}) => (
89+
<Button trailingAction={TriangleDownIcon} {...anchorProps}>
90+
{children}
91+
</Button>
92+
)}
93+
open={open}
94+
onOpenChange={setOpen}
95+
items={filteredItems}
96+
selected={selected}
97+
onSelectedChange={setSelected}
98+
onFilterChange={setFilter}
99+
width="medium"
100+
/>
101+
</FormControl>
102+
</FeatureFlags>
103+
)
104+
}
105+
106+
export const KeyboardNavigation: StoryObj<SelectPanelProps> = {
107+
render: KeyboardNavigationStory,
108+
play: async ({canvasElement}: {canvasElement: HTMLElement}) => {
109+
const canvas = within(canvasElement)
110+
111+
// Wait for initial render
112+
await delay(300)
113+
114+
// Find and click the button to open the panel
115+
const button = canvas.getByRole('button', {name: /select labels/i})
116+
await userEvent.click(button)
117+
118+
// Wait for panel to open
119+
await delay(500)
120+
121+
// Find the search input
122+
const searchInput = canvas.getByRole('combobox', {name: /filter items/i})
123+
expect(searchInput).toBeInTheDocument()
124+
125+
// Type to filter items
126+
await delay(300)
127+
await userEvent.type(searchInput, 'b')
128+
129+
// Wait for filtering to occur
130+
await delay(500)
131+
132+
// Get all visible options (should be filtered to items starting with 'b')
133+
const options = canvas.getAllByRole('option')
134+
expect(options).toHaveLength(3) // bug, blocker, backend
135+
136+
// Use arrow keys to navigate
137+
await delay(300)
138+
await userEvent.keyboard('{ArrowDown}')
139+
140+
await delay(300)
141+
await userEvent.keyboard('{ArrowDown}')
142+
143+
await delay(300)
144+
await userEvent.keyboard('{ArrowDown}')
145+
146+
// Navigate back up
147+
await delay(300)
148+
await userEvent.keyboard('{ArrowUp}')
149+
150+
await delay(300)
151+
await userEvent.keyboard('{ArrowUp}')
152+
153+
// Select an item with Space or Enter
154+
await delay(300)
155+
await userEvent.keyboard('{Enter}')
156+
157+
// Wait to show the selection
158+
await delay(500)
159+
160+
// Clear the filter with backspace
161+
await delay(500)
162+
await userEvent.keyboard('{Backspace}')
163+
164+
await delay(500)
165+
await userEvent.type(searchInput, 'b')
166+
167+
await delay(500)
168+
await userEvent.type(searchInput, 'u')
169+
170+
await delay(500)
171+
await userEvent.keyboard('{Enter}')
172+
173+
// Escape to close the panel
174+
await delay(500)
175+
await userEvent.keyboard('{Escape}')
176+
},
177+
}
178+
179+
export const KeyboardNavigationWithRovingTabindex: StoryObj<SelectPanelProps> = {
180+
render: KeyboardNavigationWithFeatureFlagStory,
181+
play: async ({canvasElement}: {canvasElement: HTMLElement}) => {
182+
const canvas = within(canvasElement)
183+
184+
// Wait for initial render
185+
await delay(300)
186+
187+
// Find and click the button to open the panel
188+
const button = canvas.getByRole('button', {name: /select labels/i})
189+
await userEvent.click(button)
190+
191+
// Wait for panel to open
192+
await delay(500)
193+
194+
// Find the search input
195+
const searchInput = canvas.getByRole('combobox', {name: /Filter items/i})
196+
expect(searchInput).toBeInTheDocument()
197+
198+
// Type to filter items
199+
await delay(500)
200+
await userEvent.type(searchInput, 'f')
201+
202+
await delay(500)
203+
await userEvent.type(searchInput, 'r')
204+
205+
// Wait for filtering to occur
206+
await delay(500)
207+
208+
// Get all visible options (should be filtered to items starting with 'f')
209+
const options = canvas.getAllByRole('option')
210+
expect(options).toHaveLength(1) // frontend
211+
212+
// Clear the filter to see more items
213+
await delay(300)
214+
await userEvent.clear(searchInput)
215+
216+
await delay(300)
217+
await userEvent.type(searchInput, 'e')
218+
219+
// Wait for filtering
220+
await delay(500)
221+
222+
// Use arrow keys to navigate (roving tabindex behavior)
223+
await delay(300)
224+
await userEvent.keyboard('{ArrowDown}')
225+
226+
await delay(300)
227+
await userEvent.keyboard('{ArrowDown}')
228+
229+
await delay(300)
230+
await userEvent.keyboard('{ArrowDown}')
231+
232+
// Navigate back up
233+
await delay(300)
234+
await userEvent.keyboard('{ArrowUp}')
235+
236+
await delay(300)
237+
await userEvent.keyboard('{ArrowUp}')
238+
239+
// Select an item with Space or Enter
240+
await delay(300)
241+
await userEvent.keyboard('{Enter}')
242+
243+
// Wait to show the selection
244+
await delay(500)
245+
246+
// Shift+Tab to go back to the search input
247+
await delay(300)
248+
await userEvent.keyboard('{Shift}{Tab}')
249+
250+
// Backspace to clear the filter
251+
await delay(300)
252+
await userEvent.keyboard('{Backspace}')
253+
254+
// Select another option
255+
// Type to filter items
256+
await delay(500)
257+
await userEvent.type(searchInput, 'b')
258+
259+
await delay(500)
260+
await userEvent.type(searchInput, 'u')
261+
262+
// Wait for filtering to occur
263+
await delay(500)
264+
265+
// Use arrow keys to navigate (roving tabindex behavior)
266+
await delay(300)
267+
await userEvent.keyboard('{ArrowDown}')
268+
269+
// Select an item with Space or Enter
270+
await delay(300)
271+
await userEvent.keyboard('{Enter}')
272+
273+
// Escape to close the panel
274+
await delay(500)
275+
await userEvent.keyboard('{Escape}')
276+
},
277+
}

0 commit comments

Comments
 (0)