Skip to content

Commit bda8404

Browse files
snowopsdevcodex
andauthored
fix(cms): support keyboard selection of content templates (#91)
Co-Authored-By: Codex <noreply@openai.com>
1 parent b68a0fd commit bda8404

2 files changed

Lines changed: 56 additions & 1 deletion

File tree

cms/src/components/ops/NewContentFlow.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,18 @@ export function NewContentFlow({ templates, mode, pipelineReady, runActive }: Pr
4141

4242
const chosen = templates.find((t) => t.id === templateId) ?? null
4343

44+
const chooseWithKeyboard = (event: React.KeyboardEvent<HTMLButtonElement>, index: number) => {
45+
let next: number
46+
if (event.key === 'ArrowRight' || event.key === 'ArrowDown') next = (index + 1) % templates.length
47+
else if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') next = (index - 1 + templates.length) % templates.length
48+
else if (event.key === 'Home') next = 0
49+
else if (event.key === 'End') next = templates.length - 1
50+
else return
51+
event.preventDefault()
52+
setTemplateId(templates[next].id)
53+
event.currentTarget.parentElement?.querySelectorAll<HTMLButtonElement>('[role="radio"]')[next]?.focus()
54+
}
55+
4456
const createFromKeyword = (event: React.FormEvent) => {
4557
event.preventDefault()
4658
if (!chosen) return
@@ -79,13 +91,15 @@ export function NewContentFlow({ templates, mode, pipelineReady, runActive }: Pr
7991
</p>
8092
) : (
8193
<div className="datum-new__cards" role="radiogroup" aria-label="Kind of piece">
82-
{templates.map((t) => (
94+
{templates.map((t, index) => (
8395
<button
8496
aria-checked={templateId === t.id}
8597
className={`datum-new__card${templateId === t.id ? ' is-selected' : ''}`}
8698
key={t.id}
8799
onClick={() => setTemplateId(t.id)}
100+
onKeyDown={(event) => chooseWithKeyboard(event, index)}
88101
role="radio"
102+
tabIndex={templateId === t.id || (templateId === null && index === 0) ? 0 : -1}
89103
type="button"
90104
>
91105
<strong>{t.name}</strong>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import React from 'react'
2+
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
3+
import { afterEach, expect, it, vi } from 'vitest'
4+
5+
vi.mock('next/navigation', () => ({ useRouter: () => ({ push: vi.fn() }) }))
6+
vi.mock('next/link', () => ({
7+
default: ({ children }: { children: React.ReactNode }) => React.createElement('span', null, children),
8+
}))
9+
vi.mock('@/components/ops/topicDiscoveryActions', () => ({ createTopicsAction: vi.fn() }))
10+
vi.mock('@/components/ops/TopicDiscovery', () => ({ TopicDiscovery: () => null }))
11+
vi.mock('@/components/ops/ContentRunForm', () => ({ ContentRunForm: () => null }))
12+
const { NewContentFlow } = await import('@/components/ops/NewContentFlow')
13+
afterEach(cleanup)
14+
15+
it('offers one template tab stop and selects adjacent templates with arrow keys', () => {
16+
render(React.createElement(NewContentFlow, {
17+
templates: [
18+
{ id: 1, name: 'Listicle', intent: null, requiredSections: 2 },
19+
{ id: 2, name: 'How-To', intent: null, requiredSections: 3 },
20+
{ id: 3, name: 'Comparison', intent: null, requiredSections: 4 },
21+
], mode: 'mock', pipelineReady: true, runActive: false,
22+
}))
23+
const radios = screen.getAllByRole('radio')
24+
expect(radios.map((radio) => radio.tabIndex)).toEqual([0, -1, -1])
25+
radios[0].focus()
26+
fireEvent.keyDown(radios[0], { key: 'ArrowRight' })
27+
expect(document.activeElement).toBe(radios[1])
28+
expect(radios[1].getAttribute('aria-checked')).toBe('true')
29+
expect(radios.map((radio) => radio.tabIndex)).toEqual([-1, 0, -1])
30+
fireEvent.keyDown(radios[1], { key: 'ArrowDown' })
31+
expect(document.activeElement).toBe(radios[2])
32+
fireEvent.keyDown(radios[2], { key: 'ArrowRight' })
33+
expect(document.activeElement).toBe(radios[0])
34+
fireEvent.keyDown(radios[0], { key: 'ArrowLeft' })
35+
expect(document.activeElement).toBe(radios[2])
36+
fireEvent.keyDown(radios[2], { key: 'Home' })
37+
expect(document.activeElement).toBe(radios[0])
38+
fireEvent.keyDown(radios[0], { key: 'End' })
39+
expect(document.activeElement).toBe(radios[2])
40+
expect(radios[2].getAttribute('aria-checked')).toBe('true')
41+
})

0 commit comments

Comments
 (0)