From a6404e3a4090c528ec65d88b23edc6e97efdede5 Mon Sep 17 00:00:00 2001 From: Mathias Skjulestad Date: Fri, 24 Jul 2026 15:41:59 +0200 Subject: [PATCH 1/2] Fix SortableList accessibility semantics for tree consumers #4602 Allowed consumers to provide container and row accessibility properties. Made drag focus restoration configurable with existing behavior as default. Disabled sortable behavior when the list is disabled. --- .../components/sortable-list/SortableList.tsx | 62 ++++++++++++++++--- .../form2/components/sortable-list/index.tsx | 2 + 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/src/main/resources/assets/admin/common/js/form2/components/sortable-list/SortableList.tsx b/src/main/resources/assets/admin/common/js/form2/components/sortable-list/SortableList.tsx index f9bfe5b4f..560ab2889 100644 --- a/src/main/resources/assets/admin/common/js/form2/components/sortable-list/SortableList.tsx +++ b/src/main/resources/assets/admin/common/js/form2/components/sortable-list/SortableList.tsx @@ -76,6 +76,25 @@ export type SortableDropHint = { allowed: boolean; }; +/** Semantic attributes applied to the list container. */ +export type SortableListContainerProps = { + role?: JSX.AriaRole; + 'aria-label'?: string; +}; + +/** Semantic attributes applied to a sortable row. */ +export type SortableListItemProps = { + role?: JSX.AriaRole; + tabIndex?: number; + 'aria-disabled'?: boolean; + 'aria-expanded'?: boolean; + 'aria-level'?: number; + 'aria-posinset'?: number; + 'aria-roledescription'?: string; + 'aria-selected'?: boolean; + 'aria-setsize'?: number; +}; + /** Vertical drag-to-reorder list with built-in drag handles. */ export type SortableListProps = { 'data-component'?: string; @@ -116,6 +135,12 @@ export type SortableListProps = { resolveDrop?: (info: SortableDragInfo, items: T[]) => SortableDropHint | null; /** Extra classes on each row wrapper; function form receives item context. */ itemClassName?: string | ((context: SortableListItemContext) => string); + /** Semantic attributes applied to the list container. */ + containerProps?: SortableListContainerProps; + /** Returns semantic attributes for each row. */ + getItemProps?: (context: SortableListItemContext) => SortableListItemProps; + /** Whether dnd-kit restores focus to the original activator after a keyboard drag. Defaults to `true`. */ + restoreFocus?: boolean; className?: string; }; @@ -163,7 +188,7 @@ type SortableListItemInternalProps = { animateLayoutChanges?: (args: {isSorting: boolean; wasDragging: boolean}) => boolean; renderItem: (context: SortableListItemContext, grip?: ReactNode) => ReactNode; itemClassName?: string | ((context: SortableListItemContext) => string); - itemTabIndex?: number; + getItemProps?: (context: SortableListItemContext) => SortableListItemProps; }; const SortableListItem = ({ @@ -181,12 +206,12 @@ const SortableListItem = ({ animateLayoutChanges, renderItem, itemClassName, - itemTabIndex, + getItemProps, }: SortableListItemInternalProps): ReactElement => { const [isFocused, setIsFocused] = useState(false); const {attributes, listeners, setNodeRef, transform, transition, isDragging} = useSortable({ id, - disabled: !isMovable, + disabled: !enabled || !isMovable, animateLayoutChanges, }); @@ -225,6 +250,9 @@ const SortableListItem = ({ }; const resolvedClassName = typeof itemClassName === 'function' ? itemClassName(context) : itemClassName; + const resolvedItemProps = getItemProps?.(context); + const resolvedRole = resolvedItemProps?.role ?? (attributes.role as JSX.AriaRole); + const hasCustomRole = resolvedItemProps?.role != null; const grip = isMovable && (