Skip to content

Commit 21ad6b4

Browse files
committed
improve a11y keyboard actions
1 parent 94493d7 commit 21ad6b4

File tree

5 files changed

+66
-16
lines changed

5 files changed

+66
-16
lines changed

packages/ui/src/elements/DraggableWithClick/index.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { UseDraggableArguments } from '@dnd-kit/core'
2+
13
import { useDraggable } from '@dnd-kit/core'
24
import React, { useId, useRef } from 'react'
35

@@ -10,6 +12,12 @@ type Props = {
1012
* @default 'div'
1113
*/
1214
readonly as?: React.ElementType
15+
/**
16+
* Additional attributes to spread onto the root element.
17+
* This can be useful for accessibility or data attributes.
18+
* @default {}
19+
*/
20+
readonly attributes?: UseDraggableArguments['attributes']
1321
/** The content to be rendered inside the component. */
1422
readonly children?: React.ReactNode
1523
/**
@@ -45,6 +53,7 @@ type Props = {
4553

4654
export const DraggableWithClick = ({
4755
as = 'div',
56+
attributes: draggableAttributes = {},
4857
children,
4958
className,
5059
disabled = false,
@@ -58,6 +67,7 @@ export const DraggableWithClick = ({
5867
const initialPos = useRef({ x: 0, y: 0 })
5968
const { attributes, listeners, setNodeRef } = useDraggable({
6069
id,
70+
attributes: draggableAttributes,
6171
disabled,
6272
})
6373
const isDragging = useRef(false)

packages/ui/src/elements/TreeView/NestedSectionsTable/Row/index.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,10 @@
223223
opacity: 0.4;
224224
}
225225

226+
&__section--selected.nested-sections-table-row__section--focused {
227+
--cell-bg-color: var(--theme-success-150);
228+
}
229+
226230
&__section--invalid-target {
227231
opacity: 0.4;
228232
}

packages/ui/src/elements/TreeView/NestedSectionsTable/Row/index.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ interface DivTableRowProps {
4141
placement?: string
4242
targetItem: null | SectionRow
4343
}) => void
44+
onFocusChange?: (focusedIndex: number) => void
4445
onRowClick: ({ event, row }: { event: React.MouseEvent<HTMLElement>; row: SectionRow }) => void
4546
onRowDrag: (params: { event: PointerEvent; item: null | SectionRow }) => void
4647
onRowKeyPress?: (params: { event: React.KeyboardEvent; row: SectionRow }) => void
@@ -71,6 +72,7 @@ export const NestedSectionsTableRow: React.FC<DivTableRowProps> = ({
7172
level,
7273
loadingRowIDs,
7374
onDroppableHover,
75+
onFocusChange,
7476
onRowClick,
7577
onRowDrag,
7678
onRowKeyPress,
@@ -126,6 +128,17 @@ export const NestedSectionsTableRow: React.FC<DivTableRowProps> = ({
126128
.filter(Boolean)
127129
.join(' ')}
128130
onClick={handleClick}
131+
onFocus={(e) => {
132+
// Update focus index when row receives focus via tab, only if it's a valid focusable row
133+
// Only update if the row itself is being focused, not a child element (like the chevron button)
134+
if (
135+
focusedRowIndex !== absoluteRowIndex &&
136+
onFocusChange &&
137+
e.target === e.currentTarget
138+
) {
139+
onFocusChange(absoluteRowIndex)
140+
}
141+
}}
129142
onKeyDown={
130143
onRowKeyPress
131144
? (event) => {
@@ -135,12 +148,15 @@ export const NestedSectionsTableRow: React.FC<DivTableRowProps> = ({
135148
}
136149
ref={rowRef}
137150
role="button"
138-
tabIndex={0}
151+
tabIndex={hasSelectedAncestor ? -1 : 0}
139152
>
140153
<div className={baseClass}>
141154
<div className={`${baseClass}__cell`} ref={firstCellRef}>
142155
<div className={`${baseClass}__actions`}>
143156
<DraggableWithClick
157+
attributes={{
158+
tabIndex: -1,
159+
}}
144160
className={`${baseClass}__drag-handler`}
145161
disabled={
146162
hasSelectedAncestor ||
@@ -187,7 +203,10 @@ export const NestedSectionsTableRow: React.FC<DivTableRowProps> = ({
187203
<Button
188204
buttonStyle="none"
189205
className={`${baseClass}__tree-toggle`}
190-
extraButtonProps={{ [dataAttributeName]: actionNames.toggleExpand }}
206+
extraButtonProps={{
207+
[dataAttributeName]: actionNames.toggleExpand,
208+
tabIndex: hasSelectedAncestor ? -1 : 0,
209+
}}
191210
margin={false}
192211
size="small"
193212
>

packages/ui/src/elements/TreeView/NestedSectionsTable/index.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ interface NestedSectionsTableProps {
3030
placement?: string
3131
targetItem: null | SectionRow
3232
}) => void
33+
onFocusChange?: (focusedIndex: number) => void
3334
onRowClick?: ({ event, row }: { event: React.MouseEvent<HTMLElement>; row: SectionRow }) => void
3435
onRowDrag?: (params: { event: PointerEvent; item: null | SectionRow }) => void
3536
onRowKeyPress?: (params: { event: React.KeyboardEvent; row: SectionRow }) => void
@@ -59,6 +60,7 @@ interface DivTableSectionProps {
5960
placement?: string
6061
targetItem: null | SectionRow
6162
}) => void
63+
onFocusChange?: (focusedIndex: number) => void
6264
onRowClick: ({
6365
event,
6466
from,
@@ -93,6 +95,7 @@ export const NestedSectionsTable: React.FC<NestedSectionsTableProps> = ({
9395
isDragging = false,
9496
loadingRowIDs,
9597
onDroppableHover,
98+
onFocusChange,
9699
onRowClick,
97100
onRowDrag,
98101
onRowKeyPress,
@@ -119,6 +122,29 @@ export const NestedSectionsTable: React.FC<NestedSectionsTableProps> = ({
119122
className={[`${baseClass}__wrapper`, isDragging && `${baseClass}--dragging`, className]
120123
.filter(Boolean)
121124
.join(' ')}
125+
onBlur={(e) => {
126+
// Check if focus is leaving the table completely (not just moving between children)
127+
if (!e.currentTarget.contains(e.relatedTarget as Node) && onFocusChange) {
128+
onFocusChange(-1)
129+
}
130+
}}
131+
onFocus={(e) => {
132+
// Check if focus is entering the table from outside (not from a child)
133+
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
134+
// Focus the first selected row, or the first row if nothing is selected
135+
if (selectedRowIDs && selectedRowIDs.length > 0) {
136+
const firstSelectedID = selectedRowIDs[0]
137+
const firstSelectedIndex = sections?.findIndex((row) => row.rowID === firstSelectedID)
138+
if (firstSelectedIndex !== undefined && firstSelectedIndex >= 0) {
139+
onFocusChange?.(firstSelectedIndex)
140+
}
141+
} else if (focusedRowIndex === undefined || focusedRowIndex < 0) {
142+
onFocusChange?.(0)
143+
}
144+
}
145+
}}
146+
role="table"
147+
tabIndex={-1}
122148
>
123149
<div className={baseClass}>
124150
<Header columns={columns} />
@@ -134,6 +160,7 @@ export const NestedSectionsTable: React.FC<NestedSectionsTableProps> = ({
134160
isDragging={isDragging}
135161
loadingRowIDs={loadingRowIDs}
136162
onDroppableHover={onDroppableHover}
163+
onFocusChange={onFocusChange}
137164
onRowClick={onRowClick}
138165
onRowDrag={onRowDrag}
139166
onRowKeyPress={onRowKeyPress}
@@ -165,6 +192,7 @@ export const DivTableSection: React.FC<DivTableSectionProps> = ({
165192
level = 0,
166193
loadingRowIDs,
167194
onDroppableHover,
195+
onFocusChange,
168196
onRowClick,
169197
onRowDrag,
170198
onRowKeyPress,
@@ -258,6 +286,7 @@ export const DivTableSection: React.FC<DivTableSectionProps> = ({
258286
level={level}
259287
loadingRowIDs={loadingRowIDs}
260288
onDroppableHover={onDroppableHover}
289+
onFocusChange={onFocusChange}
261290
onRowClick={onRowClick}
262291
onRowDrag={onRowDrag}
263292
onRowKeyPress={onRowKeyPress}
@@ -285,6 +314,7 @@ export const DivTableSection: React.FC<DivTableSectionProps> = ({
285314
level={level + 1}
286315
loadingRowIDs={loadingRowIDs}
287316
onDroppableHover={onDroppableHover}
317+
onFocusChange={onFocusChange}
288318
onRowClick={onRowClick}
289319
onRowDrag={onRowDrag}
290320
onRowKeyPress={onRowKeyPress}

packages/ui/src/elements/TreeView/TreeViewTable/index.tsx

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -309,20 +309,6 @@ export function TreeViewTable() {
309309
})
310310
break
311311
}
312-
case 'Tab': {
313-
if (isShiftPressed) {
314-
const prevIndex = index - 1
315-
if (prevIndex < 0 && selectedItemKeys?.size > 0) {
316-
setFocusedRowIndex(prevIndex)
317-
}
318-
} else {
319-
const nextIndex = index + 1
320-
if (nextIndex === items.length && selectedItemKeys.size > 0) {
321-
setFocusedRowIndex(items.length - 1)
322-
}
323-
}
324-
break
325-
}
326312
}
327313
},
328314
[
@@ -370,6 +356,7 @@ export function TreeViewTable() {
370356
isDragging={isDragging}
371357
loadingRowIDs={loadingRowIDs}
372358
onDroppableHover={onDroppableHover}
359+
onFocusChange={setFocusedRowIndex}
373360
onRowClick={onRowClick}
374361
onRowDrag={onRowDrag}
375362
onRowKeyPress={onRowKeyPress}

0 commit comments

Comments
 (0)