Skip to content

Commit 2014a84

Browse files
committed
feat: better accessibility
1 parent 3b6eed3 commit 2014a84

File tree

2 files changed

+155
-12
lines changed

2 files changed

+155
-12
lines changed

docs/registry/default/ui/kanban.tsx

+118-2
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ function Kanban<T>(props: KanbanProps<T>) {
219219
orientation = "horizontal",
220220
onMove,
221221
getItemValue: getItemValueProp,
222+
accessibility,
222223
flatCursor = false,
223224
...kanbanProps
224225
} = props;
@@ -443,6 +444,17 @@ function Kanban<T>(props: KanbanProps<T>) {
443444
[value, getContainer, getItemValue, onValueChange, onMove],
444445
);
445446

447+
const screenReaderInstructions = React.useMemo(
448+
() => ({
449+
draggable: `
450+
To pick up a kanban item or column, press space or enter.
451+
While dragging, use the arrow keys to move the item.
452+
Press space or enter again to drop the item in its new position, or press escape to cancel.
453+
`,
454+
}),
455+
[],
456+
);
457+
446458
const contextValue = React.useMemo<KanbanContextValue<T>>(
447459
() => ({
448460
id,
@@ -491,6 +503,110 @@ function Kanban<T>(props: KanbanProps<T>) {
491503
setActiveId(null);
492504
recentlyMovedToNewContainerRef.current = false;
493505
})}
506+
accessibility={{
507+
announcements: {
508+
onDragStart({ active }) {
509+
const isColumn = active.id in value;
510+
const activeValue = active.id.toString();
511+
if (isColumn) {
512+
const columnIndex = Object.keys(value).indexOf(activeValue);
513+
return `Grabbed kanban column "${activeValue}". Current position is ${columnIndex + 1} of ${Object.keys(value).length}. Use arrow keys to move, space to drop.`;
514+
}
515+
516+
const container = getContainer(active.id);
517+
if (!container) return;
518+
const containerItems = value[container];
519+
if (!containerItems?.length) return;
520+
const itemIndex = containerItems.findIndex(
521+
(item) => getItemValue(item) === active.id,
522+
);
523+
return `Grabbed kanban item "${activeValue}". Current position is ${itemIndex + 1} of ${containerItems.length} in column "${container}". Use arrow keys to move, space to drop.`;
524+
},
525+
onDragOver({ active, over }) {
526+
if (!over) {
527+
return "Item is no longer over a droppable area. Press escape to cancel.";
528+
}
529+
530+
const isColumn = active.id in value;
531+
const activeValue = active.id.toString();
532+
const overValue = over.id.toString();
533+
534+
if (isColumn) {
535+
const activeIndex = Object.keys(value).indexOf(activeValue);
536+
const overIndex = Object.keys(value).indexOf(overValue);
537+
const moveDirection =
538+
overIndex > activeIndex ? "right" : "left";
539+
return `Column "${activeValue}" moved ${moveDirection} to position ${overIndex + 1} of ${Object.keys(value).length}.`;
540+
}
541+
542+
const activeContainer = getContainer(active.id);
543+
const overContainer = getContainer(over.id);
544+
545+
if (!activeContainer || !overContainer) return;
546+
547+
if (activeContainer === overContainer) {
548+
const items = value[activeContainer];
549+
if (!items?.length) return;
550+
const activeIndex = items.findIndex(
551+
(item) => getItemValue(item) === active.id,
552+
);
553+
const overIndex = items.findIndex(
554+
(item) => getItemValue(item) === over.id,
555+
);
556+
const moveDirection = overIndex > activeIndex ? "down" : "up";
557+
return `Item "${activeValue}" moved ${moveDirection} to position ${overIndex + 1} of ${items.length} in column "${activeContainer}".`;
558+
}
559+
560+
return `Item "${activeValue}" moved to column "${overContainer}".`;
561+
},
562+
onDragEnd({ active, over }) {
563+
if (!over) {
564+
return `Dropped kanban item "${active.id}". No changes were made.`;
565+
}
566+
567+
const isColumn = active.id in value;
568+
const activeValue = active.id.toString();
569+
const overValue = over.id.toString();
570+
571+
if (isColumn) {
572+
const overIndex = Object.keys(value).indexOf(overValue);
573+
return `Column "${activeValue}" dropped at position ${overIndex + 1} of ${Object.keys(value).length}.`;
574+
}
575+
576+
const overContainer = getContainer(over.id);
577+
if (!overContainer) return;
578+
579+
const items = value[overContainer];
580+
if (!items?.length) return;
581+
const overIndex = items.findIndex(
582+
(item) => getItemValue(item) === over.id,
583+
);
584+
return `Item "${activeValue}" dropped at position ${overIndex + 1} of ${items.length} in column "${overContainer}".`;
585+
},
586+
onDragCancel({ active }) {
587+
const isColumn = active.id in value;
588+
const activeValue = active.id.toString();
589+
590+
if (isColumn) {
591+
const columnIndex = Object.keys(value).indexOf(activeValue);
592+
return `Sorting cancelled. Column "${activeValue}" returned to position ${columnIndex + 1} of ${Object.keys(value).length}.`;
593+
}
594+
595+
const container = getContainer(active.id);
596+
if (!container) return;
597+
598+
const items = value[container];
599+
if (!items?.length) return;
600+
const itemIndex = items.findIndex(
601+
(item) => getItemValue(item) === active.id,
602+
);
603+
return `Sorting cancelled. Item "${activeValue}" returned to position ${itemIndex + 1} of ${items.length} in column "${container}".`;
604+
},
605+
...accessibility?.announcements,
606+
},
607+
screenReaderInstructions,
608+
...accessibility,
609+
}}
494610
{...kanbanProps}
495611
/>
496612
</KanbanContext.Provider>
@@ -951,11 +1067,13 @@ const ItemHandle = KanbanItemHandle;
9511067
const Overlay = KanbanOverlay;
9521068

9531069
export {
1070+
Root,
9541071
Board,
9551072
Column,
9561073
ColumnHandle,
9571074
Item,
9581075
ItemHandle,
1076+
Overlay,
9591077
//
9601078
Kanban,
9611079
KanbanBoard,
@@ -964,6 +1082,4 @@ export {
9641082
KanbanItem,
9651083
KanbanItemHandle,
9661084
KanbanOverlay,
967-
Overlay,
968-
Root,
9691085
};

docs/registry/default/ui/sortable.tsx

+37-10
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ function Sortable<T>(props: SortableProps<T>) {
126126
orientation = "vertical",
127127
flatCursor = false,
128128
getItemValue: getItemValueProp,
129+
accessibility,
129130
...sortableProps
130131
} = props;
131132
const [activeId, setActiveId] = React.useState<UniqueIdentifier | null>(null);
@@ -176,6 +177,17 @@ function Sortable<T>(props: SortableProps<T>) {
176177
[value, onValueChange, onMove, getItemValue],
177178
);
178179

180+
const screenReaderInstructions = React.useMemo(
181+
() => ({
182+
draggable: `
183+
To pick up a sortable item, press space or enter.
184+
While dragging, use the ${orientation === "vertical" ? "up and down" : orientation === "horizontal" ? "left and right" : "arrow"} keys to move the item.
185+
Press space or enter again to drop the item in its new position, or press escape to cancel.
186+
`,
187+
}),
188+
[orientation],
189+
);
190+
179191
const contextValue = React.useMemo(
180192
() => ({
181193
id,
@@ -218,34 +230,48 @@ function Sortable<T>(props: SortableProps<T>) {
218230
setActiveId(null),
219231
)}
220232
accessibility={{
221-
...props.accessibility,
222233
announcements: {
223234
onDragStart({ active }) {
224-
return `Picked up sortable item ${active.id}. Use arrow keys to move, space to drop.`;
235+
const activeValue = active.id.toString();
236+
return `Grabbed sortable item "${activeValue}". Current position is ${active.data.current?.sortable.index + 1} of ${value.length}. Use arrow keys to move, space to drop.`;
225237
},
226238
onDragOver({ active, over }) {
227239
if (over) {
228-
return `Sortable item ${active.id} was moved over position ${over.id}.`;
240+
const overIndex = over.data.current?.sortable.index ?? 0;
241+
const activeIndex = active.data.current?.sortable.index ?? 0;
242+
const moveDirection = overIndex > activeIndex ? "down" : "up";
243+
const activeValue = active.id.toString();
244+
return `Sortable item "${activeValue}" moved ${moveDirection} to position ${overIndex + 1} of ${value.length}.`;
229245
}
230-
return `Sortable item ${active.id} is no longer over a droppable area.`;
246+
return "Sortable item is no longer over a droppable area. Press escape to cancel.";
231247
},
232248
onDragEnd({ active, over }) {
249+
const activeValue = active.id.toString();
233250
if (over) {
234-
return `Sortable item ${active.id} was dropped over position ${over.id}.`;
251+
const overIndex = over.data.current?.sortable.index ?? 0;
252+
return `Sortable item "${activeValue}" dropped at position ${overIndex + 1} of ${value.length}.`;
235253
}
236-
return `Sortable item ${active.id} was dropped.`;
254+
return `Sortable item "${activeValue}" dropped. No changes were made.`;
237255
},
238256
onDragCancel({ active }) {
239-
return `Sorting was cancelled. Sortable item ${active.id} was dropped.`;
257+
const activeIndex = active.data.current?.sortable.index ?? 0;
258+
const activeValue = active.id.toString();
259+
return `Sorting cancelled. Sortable item "${activeValue}" returned to position ${activeIndex + 1} of ${value.length}.`;
240260
},
241261
onDragMove({ active, over }) {
242262
if (over) {
243-
return `Sortable item ${active.id} was moved over position ${over.id}.`;
263+
const overIndex = over.data.current?.sortable.index ?? 0;
264+
const activeIndex = active.data.current?.sortable.index ?? 0;
265+
const moveDirection = overIndex > activeIndex ? "down" : "up";
266+
const activeValue = active.id.toString();
267+
return `Sortable item "${activeValue}" is moving ${moveDirection} to position ${overIndex + 1} of ${value.length}.`;
244268
}
245-
return `Sortable item ${active.id} is no longer over a droppable area.`;
269+
return "Sortable item is no longer over a droppable area. Press escape to cancel.";
246270
},
247-
...props.accessibility?.announcements,
271+
...accessibility?.announcements,
248272
},
273+
screenReaderInstructions,
274+
...accessibility,
249275
}}
250276
{...sortableProps}
251277
/>
@@ -432,6 +458,7 @@ const SortableItemHandle = React.forwardRef<
432458
return (
433459
<HandleSlot
434460
aria-controls={itemContext.id}
461+
aria-roledescription="sortable item handle"
435462
data-dragging={itemContext.isDragging ? "" : undefined}
436463
{...dragHandleProps}
437464
{...itemContext.attributes}

0 commit comments

Comments
 (0)