Skip to content

Commit 2ec8353

Browse files
committed
Move keyboard focus into the chat panel when it opens
1 parent 33356aa commit 2ec8353

2 files changed

Lines changed: 23 additions & 10 deletions

File tree

‎apps/sim/platform/app/(shell)/chat/_components/panel/chat-panels.tsx‎

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,18 @@ export function ChatPanels({ children, data, workspaces }: ChatPanelsProps) {
5757
const [current, setCurrent] = useState('initial')
5858
const [removed, setRemoved] = useState<string[]>([])
5959
const trigger = useRef<HTMLButtonElement>(null)
60+
const panel = useRef<HTMLElement>(null)
6061
const recents = (data?.recents ?? []).filter((item) => !removed.includes(item.id))
62+
function show(element: HTMLButtonElement) {
63+
trigger.current = element
64+
setVisible(true)
65+
requestAnimationFrame(() => {
66+
const controls = panel.current?.querySelectorAll<HTMLButtonElement>('button:not(:disabled)')
67+
Array.from(controls ?? [])
68+
.find((control) => control.tabIndex >= 0 && control.getClientRects().length > 0)
69+
?.focus()
70+
})
71+
}
6172
function hide() {
6273
setVisible(false)
6374
requestAnimationFrame(() => trigger.current?.focus())
@@ -113,15 +124,10 @@ export function ChatPanels({ children, data, workspaces }: ChatPanelsProps) {
113124
}
114125
})
115126
return (
116-
<OpenPanel.Provider
117-
value={(element) => {
118-
trigger.current = element
119-
setVisible(true)
120-
}}
121-
>
127+
<OpenPanel.Provider value={show}>
122128
<div className={styles.frame} data-panel-open={visible} data-expanded={visible && expanded}>
123129
<div className={styles.conversation}>{children}</div>
124-
<aside className={styles.panel} aria-label='Chat panel' hidden={!visible}>
130+
<aside ref={panel} className={styles.panel} aria-label='Chat panel' hidden={!visible}>
125131
<PanelTabs
126132
tabs={tabViews}
127133
current={current}

‎apps/sim/platform/checks/panels.spec.ts‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,16 @@ test('a chat panel preserves its file, tabs and focus across resize, collapse an
1010
const open = page.getByRole('button', { name: 'Open chat panel' })
1111
const title = page.getByRole('heading', { level: 1, name: 'Review the renewal brief' })
1212
const titleBefore = await title.boundingBox()
13-
await open.click()
13+
await open.focus()
14+
await page.keyboard.press('Enter')
1415
const titleAfter = await title.boundingBox()
1516
if (!titleBefore || !titleAfter) throw new Error('The title must remain visible in split view')
1617
expect(titleAfter.x).toBe(titleBefore.x)
1718
expect(titleAfter.y).toBe(titleBefore.y)
1819
const panel = page.getByRole('complementary', { name: 'Chat panel' })
20+
await expect(panel.getByRole('tab', { selected: true })).toBeFocused()
21+
await page.keyboard.press('Tab')
22+
await expect(panel.getByRole('button', { name: 'Close Workspaces tab' })).toBeFocused()
1923
await panel.getByRole('button', { name: 'Renewal brief', exact: true }).click()
2024
await expect(panel.getByRole('heading', { name: 'Aster Labs renewal brief' })).toBeVisible()
2125
await panel.getByRole('button', { name: 'New panel tab' }).click()
@@ -46,8 +50,11 @@ test('a chat panel preserves its file, tabs and focus across resize, collapse an
4650
await expect(panel.getByRole('heading', { name: 'Aster Labs renewal brief' })).toBeVisible()
4751
await panel.getByRole('button', { name: 'Close chat panel' }).click()
4852
await expect(open).toBeFocused()
49-
await open.click()
50-
await panel.getByRole('button', { name: 'Panel tabs' }).click()
53+
await page.keyboard.press('Enter')
54+
await expect(panel.getByRole('button', { name: 'New panel tab' })).toBeFocused()
55+
await page.keyboard.press('Tab')
56+
await expect(panel.getByRole('button', { name: 'Panel tabs' })).toBeFocused()
57+
await page.keyboard.press('Enter')
5158
await page.getByRole('menuitemcheckbox', { name: 'Workspaces' }).click()
5259
await panel.getByRole('button', { name: 'Customer operations', exact: true }).last().click()
5360
await expect(page).toHaveURL(/\/home\?.*state=full/)

0 commit comments

Comments
 (0)