From a6910bb59d5b6d58b856c95da21eef1f3f5f4135 Mon Sep 17 00:00:00 2001 From: Gethin Webster Date: Mon, 15 Jun 2026 09:54:55 +0000 Subject: [PATCH 1/8] feat: Add multi-column sort to table Add multi-column sorting to Table, activated by shift-clicking sortable column headers or via a per-column sort menu. - Per-column sort menu: set ascending/descending direction (shown as a checked state), add to / remove from the multi-column sort, with disabled-reason tooltips on unavailable actions. - Priority badges on sorted column headers and a Clear sort control. - A polite live region announcing the current sort order (and when sorting is cleared), reading localized header text from the DOM and joining columns with locale-appropriate separators. - Focus management so clearing the sort keeps keyboard focus on a sortable column header instead of dropping to the document body. - Styling fixes so the sort icon and priority badge do not overlap truncated header text in narrow columns. - i18n strings for all new labels and screen reader announcements, plus test-utils and unit tests covering the new behavior. --- pages/table/multi-column-sort.page.tsx | 231 +++++ .../multi-column-sort.permutations.page.tsx | 90 ++ .../__snapshots__/documenter.test.ts.snap | 938 +++++++++++++++++- .../test-utils-selectors.test.tsx.snap | 2 + .../button-dropdown-compact-trigger.test.tsx | 27 + src/button-dropdown/internal-interfaces.ts | 7 + src/button-dropdown/internal.tsx | 2 + src/button-dropdown/styles.scss | 9 + src/i18n/messages-types.ts | 18 + src/i18n/messages/all.en-GB.json | 12 +- src/i18n/messages/all.en.json | 17 +- .../multi-column-sort-behavior.test.tsx | 270 +++++ .../multi-column-sort-clear.test.tsx | 121 +++ ...lti-column-sort-live-announcement.test.tsx | 64 ++ .../multi-column-sort-test-utils.test.tsx | 119 +++ .../__tests__/multi-column-sort-utils.test.ts | 216 ++++ src/table/__tests__/warnings.test.tsx | 30 + src/table/clear-sort.tsx | 30 + src/table/header-cell/index.tsx | 199 +++- src/table/header-cell/sort-menu.tsx | 95 ++ src/table/header-cell/styles.scss | 53 +- src/table/header-cell/th-element.tsx | 14 +- src/table/index.tsx | 1 + src/table/interfaces.tsx | 63 +- src/table/internal.tsx | 46 +- .../multi-column-sort/live-announcement.tsx | 117 +++ src/table/multi-column-sort/utils.ts | 129 +++ src/table/styles.scss | 7 + src/table/thead.tsx | 12 + src/table/tools-header.tsx | 15 +- src/test-utils/dom/table/index.ts | 96 ++ 31 files changed, 2956 insertions(+), 94 deletions(-) create mode 100644 pages/table/multi-column-sort.page.tsx create mode 100644 pages/table/multi-column-sort.permutations.page.tsx create mode 100644 src/button-dropdown/__tests__/button-dropdown-compact-trigger.test.tsx create mode 100644 src/table/__tests__/multi-column-sort-behavior.test.tsx create mode 100644 src/table/__tests__/multi-column-sort-clear.test.tsx create mode 100644 src/table/__tests__/multi-column-sort-live-announcement.test.tsx create mode 100644 src/table/__tests__/multi-column-sort-test-utils.test.tsx create mode 100644 src/table/__tests__/multi-column-sort-utils.test.ts create mode 100644 src/table/clear-sort.tsx create mode 100644 src/table/header-cell/sort-menu.tsx create mode 100644 src/table/multi-column-sort/live-announcement.tsx create mode 100644 src/table/multi-column-sort/utils.ts diff --git a/pages/table/multi-column-sort.page.tsx b/pages/table/multi-column-sort.page.tsx new file mode 100644 index 0000000000..ae0f6ce9c0 --- /dev/null +++ b/pages/table/multi-column-sort.page.tsx @@ -0,0 +1,231 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { useState } from 'react'; + +import { Checkbox, Pagination, SpaceBetween, Table, TableProps } from '~components'; + +import { useAppContext } from '../app/app-context'; +import { SimplePage } from '../app/templates'; + +interface Item { + id: string; + name: string; + type: string; + state: string; + cpu: number; +} + +const allItems: Item[] = [ + { id: '1', name: 'alpha-server', type: 't3.micro', state: 'running', cpu: 12 }, + { id: '2', name: 'beta-server', type: 'm5.large', state: 'stopped', cpu: 0 }, + { id: '3', name: 'gamma-worker', type: 'c5.xlarge', state: 'running', cpu: 87 }, + { id: '4', name: 'delta-proxy', type: 't3.micro', state: 'running', cpu: 45 }, + { id: '5', name: 'epsilon-db', type: 'r5.large', state: 'running', cpu: 23 }, + { id: '6', name: 'zeta-cache', type: 'r5.large', state: 'stopped', cpu: 0 }, + { id: '7', name: 'eta-gateway', type: 't3.micro', state: 'running', cpu: 56 }, + { id: '8', name: 'theta-batch', type: 'c5.xlarge', state: 'running', cpu: 91 }, +]; + +const shortHeaders: Record = { + name: 'Name', + type: 'Type', + state: 'State', + cpu: 'CPU %', +}; + +const longHeaders: Record = { + name: 'Fully qualified instance name and identifier', + type: 'Compute instance type and family classification', + state: 'Current lifecycle and operational state', + cpu: 'Average CPU utilization over the last hour (%)', +}; + +const groupDefinitions: TableProps.GroupDefinition[] = [ + { id: 'identity', header: 'Identity' }, + { id: 'status', header: 'Status' }, +]; + +const groupedColumnDisplay: TableProps.ColumnDisplayProperties[] = [ + { + type: 'group', + id: 'identity', + visible: true, + children: [ + { id: 'name', visible: true }, + { id: 'type', visible: true }, + ], + }, + { + type: 'group', + id: 'status', + visible: true, + children: [ + { id: 'state', visible: true }, + { id: 'cpu', visible: true }, + ], + }, +]; + +const PAGE_SIZE = 4; + +export default function MultiColumnSortPage() { + const { urlParams, setUrlParams } = useAppContext< + 'multiSort' | 'selection' | 'grouped' | 'pagination' | 'longNames' | 'resizable' + >(); + + // `multiSort` defaults to on; the rest default to off. + const multiSortEnabled = urlParams.multiSort !== 'false' && urlParams.multiSort !== false; + const selectionEnabled = urlParams.selection === true || urlParams.selection === 'true'; + const groupedEnabled = urlParams.grouped === true || urlParams.grouped === 'true'; + const paginationEnabled = urlParams.pagination === true || urlParams.pagination === 'true'; + const longNamesEnabled = urlParams.longNames === true || urlParams.longNames === 'true'; + const resizableEnabled = urlParams.resizable === true || urlParams.resizable === 'true'; + + const [sortingColumns, setSortingColumns] = useState>>([ + { sortingColumn: { sortingField: 'state' }, isDescending: false }, + { sortingColumn: { sortingField: 'cpu' }, isDescending: true }, + ]); + // Single-column sort state, used when multi-column sort is toggled off. + const [sortingColumn, setSortingColumn] = useState>({ sortingField: 'state' }); + const [sortingDescending, setSortingDescending] = useState(false); + const [selectedItems, setSelectedItems] = useState>([]); + const [currentPageIndex, setCurrentPageIndex] = useState(1); + + const headers = longNamesEnabled ? longHeaders : shortHeaders; + const getSortLabel = + (label: string) => + ({ sorted, descending, disabled, sortIndex }: TableProps.LabelData): string => { + if (disabled) { + return `${label}, sorting disabled`; + } + if (!sorted) { + return `${label}, not sorted`; + } + const direction = descending ? 'descending' : 'ascending'; + return sortIndex ? `${label}, sorted ${direction}, sort position ${sortIndex}` : `${label}, sorted ${direction}`; + }; + const columnDefinitions: TableProps.ColumnDefinition[] = [ + { + id: 'name', + header: headers.name, + cell: item => item.name, + sortingField: 'name', + ariaLabel: getSortLabel(headers.name), + }, + { + id: 'type', + header: headers.type, + cell: item => item.type, + sortingField: 'type', + ariaLabel: getSortLabel(headers.type), + }, + { + id: 'state', + header: headers.state, + cell: item => item.state, + sortingField: 'state', + ariaLabel: getSortLabel(headers.state), + }, + { + id: 'cpu', + header: headers.cpu, + cell: item => `${item.cpu}%`, + sortingField: 'cpu', + ariaLabel: getSortLabel(headers.cpu), + }, + ]; + + const compareField = (a: Item, b: Item, field: keyof Item, descending: boolean) => { + const aVal = a[field]; + const bVal = b[field]; + const cmp = + typeof aVal === 'number' && typeof bVal === 'number' ? aVal - bVal : String(aVal).localeCompare(String(bVal)); + return descending ? -cmp : cmp; + }; + const sorted = multiSortEnabled + ? [...allItems].sort((a, b) => { + for (const entry of sortingColumns) { + const cmp = compareField(a, b, entry.sortingColumn.sortingField as keyof Item, !!entry.isDescending); + if (cmp !== 0) { + return cmp; + } + } + return 0; + }) + : [...allItems].sort((a, b) => compareField(a, b, sortingColumn.sortingField as keyof Item, sortingDescending)); + + const pagesCount = Math.ceil(sorted.length / PAGE_SIZE); + const pageItems = paginationEnabled + ? sorted.slice((currentPageIndex - 1) * PAGE_SIZE, currentPageIndex * PAGE_SIZE) + : sorted; + + return ( + + setUrlParams({ multiSort: detail.checked })}> + Multi-column sort + + setUrlParams({ selection: detail.checked })}> + Selection + + setUrlParams({ grouped: detail.checked })}> + Grouped columns + + setUrlParams({ pagination: detail.checked })}> + Pagination + + setUrlParams({ longNames: detail.checked })}> + Long column names + + setUrlParams({ resizable: detail.checked })}> + Resizable columns + + + } + > + setSelectedItems(detail.selectedItems)} + sortingColumn={multiSortEnabled ? undefined : sortingColumn} + sortingDescending={multiSortEnabled ? undefined : sortingDescending} + onSortingChange={ + multiSortEnabled + ? undefined + : ({ detail }) => { + setSortingColumn(detail.sortingColumn); + setSortingDescending(!!detail.isDescending); + } + } + multiColumnSort={ + multiSortEnabled + ? { sortingColumns, onChange: ({ detail }) => setSortingColumns(detail.sortingColumns) } + : undefined + } + pagination={ + paginationEnabled ? ( + setCurrentPageIndex(detail.currentPageIndex)} + /> + ) : undefined + } + ariaLabels={{ + tableLabel: 'Multi-column sort demo', + sortMenuTriggerLabel: 'Sort options', + selectionGroupLabel: 'Item selection', + allItemsSelectionLabel: () => 'Select all', + itemSelectionLabel: (_data, item) => `Select ${item.name}`, + }} + /> + + ); +} diff --git a/pages/table/multi-column-sort.permutations.page.tsx b/pages/table/multi-column-sort.permutations.page.tsx new file mode 100644 index 0000000000..9b9c41c01b --- /dev/null +++ b/pages/table/multi-column-sort.permutations.page.tsx @@ -0,0 +1,90 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; + +import Table, { TableProps } from '~components/table'; + +import createPermutations from '../utils/permutations'; +import PermutationsView from '../utils/permutations-view'; +import ScreenshotArea from '../utils/screenshot-area'; + +interface Item { + name: string; + type: string; + count: number; +} + +const items: Item[] = [ + { name: 'alpha', type: 't3.micro', count: 12 }, + { name: 'beta', type: 'm5.large', count: 3 }, +]; + +const columnDefinitions: TableProps.ColumnDefinition[] = [ + { id: 'name', header: 'Name', cell: item => item.name, sortingField: 'name' }, + { id: 'type', header: 'Type', cell: item => item.type, sortingField: 'type' }, + { id: 'count', header: 'Count', cell: item => item.count, sortingField: 'count' }, +]; + +const noop = () => {}; + +const i18nStrings: TableProps.I18nStrings = { + sortDropdown: { + sortAscending: 'Sort ascending', + sortDescending: 'Sort descending', + multiColumnSortGroup: 'Multi-column sort (Shift + Click)', + addToSortAscending: 'Add to sort (ascending)', + addToSortDescending: 'Add to sort (descending)', + removeFromSort: 'Remove from sort', + }, + clearSort: 'Clear sort', +}; + +const ariaLabels: TableProps.AriaLabels = { + tableLabel: 'Multi-column sort permutations', + sortMenuTriggerLabel: 'Sort options', + selectionGroupLabel: 'Item selection', + allItemsSelectionLabel: () => 'Select all', + itemSelectionLabel: (_data, item) => `Select ${item.name}`, +}; + +const permutations = createPermutations>([ + { + selectionType: [undefined, 'multi'], + multiColumnSort: [ + // No active sort: kebab present, no priority badge, aria-sort="none". + { sortingColumns: [], onChange: noop }, + // Single column: sorted icon, no priority badge (priority adds no info). + { sortingColumns: [{ sortingColumn: { sortingField: 'name' }, isDescending: false }], onChange: noop }, + // Two columns: primary ascending (badge 1, declares aria-sort) + secondary descending (badge 2, suppressed). + { + sortingColumns: [ + { sortingColumn: { sortingField: 'name' }, isDescending: false }, + { sortingColumn: { sortingField: 'type' }, isDescending: true }, + ], + onChange: noop, + }, + ], + items: [items], + columnDefinitions: [columnDefinitions], + }, +]); + +export default function () { + return ( + <> +

Table multi-column sort permutations

+ + ( +
+ )} + /> + + + ); +} diff --git a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap index a6b649ff84..1835a02243 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap @@ -28050,6 +28050,11 @@ in tables with data grouping. "optional": true, "type": "string", }, + { + "name": "sortMenuTriggerLabel", + "optional": true, + "type": "string", + }, { "inlineType": { "name": "(column: TableProps.ColumnDefinition) => string", @@ -28153,7 +28158,8 @@ To target individual cells use \`columnDefinitions.verticalAlign\`, that takes p Note that when the \`resizableColumns\` property is set to \`true\` this property is ignored. * \`ariaLabel\` (LabelData => string) - An optional function that's called to provide an \`aria-label\` for the cell header. It receives the current sorting state of this column, the direction it's sorted in, and an indication of - whether the sorting is disabled, as three Boolean values: \`sorted\`, \`descending\` and \`disabled\`. + whether the sorting is disabled, as three Boolean values: \`sorted\`, \`descending\` and \`disabled\`, + plus a number \`sortIndex\` if the column is part of a multi-column sort. We recommend that you use this for sortable columns to provide more meaningful labels based on the current sorting direction. * \`sortingField\` (string) - Enables default column sorting. The value is used in [collection hooks](/get-started/dev-guides/collection-hooks/) @@ -28442,6 +28448,122 @@ Each group definition contains the following: "optional": true, "type": "ReadonlyArray>", }, + { + "description": "An object containing all the localized strings required by the multi-column +sorting UI: + +* \`sortDropdown\` (object): Strings for the per-column sort dropdown menu. + * \`sortAscending\` (string): Label for the "Sort ascending" dropdown menu item. + * \`sortDescending\` (string): Label for the "Sort descending" dropdown menu item. + * \`multiColumnSortGroup\` (string): Label for the "Multi-column sort" dropdown menu group. + * \`addToSortAscending\` (string): Label for the "Add to sort (ascending)" dropdown menu item. + * \`addToSortDescending\` (string): Label for the "Add to sort (descending)" dropdown menu item. + * \`removeFromSort\` (string): Label for the "Remove from sort" dropdown menu item. +* \`clearSort\` (string): Label for the "Clear sort" button.", + "i18nTag": true, + "inlineType": { + "name": "TableProps.I18nStrings", + "properties": [ + { + "name": "clearSort", + "optional": true, + "type": "string", + }, + { + "name": "liveAnnouncementSortCleared", + "optional": true, + "type": "string", + }, + { + "inlineType": { + "name": "(data: { columnLabel: string; isDescending: boolean; }) => string", + "parameters": [ + { + "name": "data", + "type": "{ columnLabel: string; isDescending: boolean; }", + }, + ], + "returnType": "string", + "type": "function", + }, + "name": "liveAnnouncementSortColumn", + "optional": true, + "type": "((data: { columnLabel: string; isDescending: boolean; }) => string)", + }, + { + "inlineType": { + "name": "(data: { columns: string; }) => string", + "parameters": [ + { + "name": "data", + "type": "{ columns: string; }", + }, + ], + "returnType": "string", + "type": "function", + }, + "name": "liveAnnouncementSortOrder", + "optional": true, + "type": "((data: { columns: string; }) => string)", + }, + { + "inlineType": { + "name": "object", + "properties": [ + { + "name": "addToSortAscending", + "optional": true, + "type": "string", + }, + { + "name": "addToSortDescending", + "optional": true, + "type": "string", + }, + { + "name": "addToSortDisabledReason", + "optional": true, + "type": "string", + }, + { + "name": "multiColumnSortGroup", + "optional": true, + "type": "string", + }, + { + "name": "removeFromSort", + "optional": true, + "type": "string", + }, + { + "name": "removeFromSortDisabledReason", + "optional": true, + "type": "string", + }, + { + "name": "sortAscending", + "optional": true, + "type": "string", + }, + { + "name": "sortDescending", + "optional": true, + "type": "string", + }, + ], + "type": "object", + }, + "name": "sortDropdown", + "optional": true, + "type": "{ sortAscending?: string | undefined; sortDescending?: string | undefined; multiColumnSortGroup?: string | undefined; addToSortAscending?: string | undefined; addToSortDescending?: string | undefined; removeFromSort?: string | undefined; addToSortDisabledReason?: string | undefined; removeFromSortDisabledReason?: st...", + }, + ], + "type": "object", + }, + "name": "i18nStrings", + "optional": true, + "type": "TableProps.I18nStrings", + }, { "deprecatedTag": "The usage of the \`id\` attribute is reserved for internal use cases. For testing and other use cases, use [data attributes](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes). If you must @@ -28489,6 +28611,47 @@ In skeleton-loading mode this will be used as a label for screenreaders.", "optional": true, "type": "string", }, + { + "description": "Enables multi-column sorting on the table. The object contains: + +* \`sortingColumns\` (ReadonlyArray>) - The current multi-column + sort state. The first entry has the highest sort priority, and subsequent entries act as + tiebreakers. +* \`onChange\` (NonCancelableEventHandler>) - Called + when the user changes the sort state. + +Use either this property or \`sortingColumn\` / \`sortingDescending\` / \`onSortingChange\`, but not both.", + "inlineType": { + "name": "TableProps.MultiColumnSort", + "properties": [ + { + "inlineType": { + "name": "NonCancelableEventHandler>", + "parameters": [ + { + "name": "event", + "type": "NonCancelableCustomEvent", + }, + ], + "returnType": "void", + "type": "function", + }, + "name": "onChange", + "optional": false, + "type": "NonCancelableEventHandler>", + }, + { + "name": "sortingColumns", + "optional": false, + "type": "ReadonlyArray>", + }, + ], + "type": "object", + }, + "name": "multiColumnSort", + "optional": true, + "type": "TableProps.MultiColumnSort", + }, { "description": "Use this function to announce page changes to screen reader users. The function argument takes the following properties: @@ -43668,6 +43831,17 @@ Returns the current value of the input.", ], }, }, + { + "description": "Returns the "Clear sort" button rendered in the table header tools area. +The button is auto-rendered only when multi-column sorting is enabled and at +least one column is sorted. Returns \`null\` otherwise.", + "name": "findClearSort", + "parameters": [], + "returnType": { + "isNullable": true, + "name": "ButtonWrapper", + }, + }, { "name": "findCollectionPreferences", "parameters": [], @@ -43749,6 +43923,50 @@ For tables with column grouping this excludes group header cells.", ], }, }, + { + "description": "Returns the per-column sort menu (kebab dropdown) for the column header at the +given index. Only present on tables that opt in to multi-column sorting.", + "name": "findColumnSortMenu", + "parameters": [ + { + "description": "1-based index of the column.", + "flags": { + "isOptional": false, + }, + "name": "columnIndex", + "typeName": "number", + }, + ], + "returnType": { + "isNullable": true, + "name": "TableColumnSortMenuWrapper", + }, + }, + { + "description": "Returns the sort priority badge on the column header at the given index. +The badge is only rendered when two or more columns participate in a +multi-column sort. Returns \`null\` otherwise.", + "name": "findColumnSortPriorityBadge", + "parameters": [ + { + "description": "1-based index of the column.", + "flags": { + "isOptional": false, + }, + "name": "columnIndex", + "typeName": "number", + }, + ], + "returnType": { + "isNullable": true, + "name": "ElementWrapper", + "typeArguments": [ + { + "name": "HTMLElement", + }, + ], + }, + }, { "description": "Returns the column that is used for descending sorting.", "name": "findDescSortedColumn", @@ -44098,62 +44316,109 @@ Note: when used with collection-hooks the \`trackBy\` is set automatically from { "methods": [ { - "name": "findActiveLink", - "parameters": [], + "description": "Returns the "Add to sort (ascending)" menu item. + +Supported options: +* \`disabled\` (boolean) - Use it to find the disabled or non-disabled item.", + "name": "findAddToSortAscendingItem", + "parameters": [ + { + "defaultValue": "{}", + "flags": { + "isOptional": false, + }, + "name": "options", + "typeName": "{ disabled?: boolean | undefined; }", + }, + ], "returnType": { "isNullable": true, "name": "ElementWrapper", "typeArguments": [ { - "name": "HTMLAnchorElement", + "name": "HTMLElement", }, ], }, }, { - "name": "findHeader", - "parameters": [], + "description": "Returns the "Add to sort (descending)" menu item. + +Supported options: +* \`disabled\` (boolean) - Use it to find the disabled or non-disabled item.", + "name": "findAddToSortDescendingItem", + "parameters": [ + { + "defaultValue": "{}", + "flags": { + "isOptional": false, + }, + "name": "options", + "typeName": "{ disabled?: boolean | undefined; }", + }, + ], "returnType": { "isNullable": true, "name": "ElementWrapper", "typeArguments": [ { - "name": "HTMLAnchorElement", + "name": "HTMLElement", }, ], }, }, { - "name": "findHeaderLink", + "description": "Finds the disabled reason tooltip for a dropdown item. Returns null if no disabled item with \`disabledReason\` is highlighted.", + "inheritedFrom": { + "name": "ButtonDropdownWrapper.findDisabledReason", + }, + "name": "findDisabledReason", "parameters": [], "returnType": { "isNullable": true, "name": "ElementWrapper", "typeArguments": [ { - "name": "HTMLAnchorElement", + "name": "HTMLElement", }, ], }, }, { - "name": "findItemByIndex", + "description": "Finds an expandable category in the open dropdown by category id. Returns null if there is no open dropdown. + +This utility does not open the dropdown. To find dropdown items, call \`openDropdown()\` first.", + "inheritedFrom": { + "name": "ButtonDropdownWrapper.findExpandableCategoryById", + }, + "name": "findExpandableCategoryById", "parameters": [ { "flags": { "isOptional": false, }, - "name": "index", - "typeName": "number", + "name": "id", + "typeName": "string", }, ], "returnType": { "isNullable": true, - "name": "SideNavigationItemWrapper", + "name": "ElementWrapper", + "typeArguments": [ + { + "name": "HTMLElement", + }, + ], }, }, { - "name": "findItemsControl", + "description": "Finds the highlighted item in the open dropdown. Returns null if there is no open dropdown. + +This utility does not open the dropdown. To find dropdown items, call \`openDropdown()\` first.", + "inheritedFrom": { + "name": "ButtonDropdownWrapper.findHighlightedItem", + }, + "name": "findHighlightedItem", "parameters": [], "returnType": { "isNullable": true, @@ -44166,34 +44431,341 @@ Note: when used with collection-hooks the \`trackBy\` is set automatically from }, }, { - "name": "findLinkByHref", + "description": "Finds an item in the open dropdown by item id. Returns null if there is no open dropdown. + +This utility does not open the dropdown. To find dropdown items, call \`openDropdown()\` first. + +Supported options: +* \`disabled\` (boolean) - Use it to find the disabled or non-disabled item.", + "inheritedFrom": { + "name": "ButtonDropdownWrapper.findItemById", + }, + "name": "findItemById", "parameters": [ { "flags": { "isOptional": false, }, - "name": "href", + "name": "id", "typeName": "string", }, + { + "defaultValue": "{}", + "flags": { + "isOptional": false, + }, + "name": "options", + "typeName": "{ disabled?: boolean | undefined; }", + }, ], "returnType": { "isNullable": true, "name": "ElementWrapper", "typeArguments": [ { - "name": "HTMLAnchorElement", + "name": "HTMLElement", }, ], }, }, - ], - "name": "SideNavigationWrapper", - }, - { - "methods": [ { - "name": "findDivider", - "parameters": [], + "description": "Finds \`checked\` value of item in the open dropdown by item id. Returns null if there is no open dropdown or the item is not a checkbox item. + +This utility does not open the dropdown. To find dropdown items, call \`openDropdown()\` first.", + "inheritedFrom": { + "name": "ButtonDropdownWrapper.findItemCheckedById", + }, + "name": "findItemCheckedById", + "parameters": [ + { + "flags": { + "isOptional": false, + }, + "name": "id", + "typeName": "string", + }, + ], + "returnType": { + "isNullable": true, + "name": "string", + }, + }, + { + "description": "Finds all the items in the open dropdown. Returns empty array if there is no open dropdown. + +This utility does not open the dropdown. To find dropdown items, call \`openDropdown()\` first. + +Supported options: +* \`disabled\` (boolean) - Use it to find all disabled or non-disabled items.", + "inheritedFrom": { + "name": "ButtonDropdownWrapper.findItems", + }, + "name": "findItems", + "parameters": [ + { + "defaultValue": "{}", + "flags": { + "isOptional": false, + }, + "name": "options", + "typeName": "{ disabled?: boolean | undefined; }", + }, + ], + "returnType": { + "isNullable": false, + "name": "Array", + "typeArguments": [ + { + "name": "ElementWrapper", + }, + ], + }, + }, + { + "inheritedFrom": { + "name": "ButtonDropdownWrapper.findMainAction", + }, + "name": "findMainAction", + "parameters": [], + "returnType": { + "isNullable": true, + "name": "ButtonWrapper", + }, + }, + { + "inheritedFrom": { + "name": "ButtonDropdownWrapper.findNativeButton", + }, + "name": "findNativeButton", + "parameters": [], + "returnType": { + "isNullable": false, + "name": "ElementWrapper", + "typeArguments": [ + { + "name": "HTMLButtonElement", + }, + ], + }, + }, + { + "inheritedFrom": { + "name": "ButtonDropdownWrapper.findOpenDropdown", + }, + "name": "findOpenDropdown", + "parameters": [], + "returnType": { + "isNullable": true, + "name": "ElementWrapper", + "typeArguments": [ + { + "name": "HTMLElement", + }, + ], + }, + }, + { + "description": "Returns the "Remove from sort" menu item. + +Supported options: +* \`disabled\` (boolean) - Use it to find the disabled or non-disabled item.", + "name": "findRemoveFromSortItem", + "parameters": [ + { + "defaultValue": "{}", + "flags": { + "isOptional": false, + }, + "name": "options", + "typeName": "{ disabled?: boolean | undefined; }", + }, + ], + "returnType": { + "isNullable": true, + "name": "ElementWrapper", + "typeArguments": [ + { + "name": "HTMLElement", + }, + ], + }, + }, + { + "description": "Returns the "Sort ascending" menu item. + +Supported options: +* \`disabled\` (boolean) - Use it to find the disabled or non-disabled item.", + "name": "findSortAscendingItem", + "parameters": [ + { + "defaultValue": "{}", + "flags": { + "isOptional": false, + }, + "name": "options", + "typeName": "{ disabled?: boolean | undefined; }", + }, + ], + "returnType": { + "isNullable": true, + "name": "ElementWrapper", + "typeArguments": [ + { + "name": "HTMLElement", + }, + ], + }, + }, + { + "description": "Returns the "Sort descending" menu item. + +Supported options: +* \`disabled\` (boolean) - Use it to find the disabled or non-disabled item.", + "name": "findSortDescendingItem", + "parameters": [ + { + "defaultValue": "{}", + "flags": { + "isOptional": false, + }, + "name": "options", + "typeName": "{ disabled?: boolean | undefined; }", + }, + ], + "returnType": { + "isNullable": true, + "name": "ElementWrapper", + "typeArguments": [ + { + "name": "HTMLElement", + }, + ], + }, + }, + { + "inheritedFrom": { + "name": "ButtonDropdownWrapper.findTriggerButton", + }, + "name": "findTriggerButton", + "parameters": [], + "returnType": { + "isNullable": true, + "name": "ButtonWrapper", + }, + }, + { + "inheritedFrom": { + "name": "ButtonDropdownWrapper.openDropdown", + }, + "name": "openDropdown", + "parameters": [], + "returnType": { + "isNullable": false, + "name": "void", + }, + }, + ], + "name": "TableColumnSortMenuWrapper", + }, + { + "methods": [ + { + "name": "findActiveLink", + "parameters": [], + "returnType": { + "isNullable": true, + "name": "ElementWrapper", + "typeArguments": [ + { + "name": "HTMLAnchorElement", + }, + ], + }, + }, + { + "name": "findHeader", + "parameters": [], + "returnType": { + "isNullable": true, + "name": "ElementWrapper", + "typeArguments": [ + { + "name": "HTMLAnchorElement", + }, + ], + }, + }, + { + "name": "findHeaderLink", + "parameters": [], + "returnType": { + "isNullable": true, + "name": "ElementWrapper", + "typeArguments": [ + { + "name": "HTMLAnchorElement", + }, + ], + }, + }, + { + "name": "findItemByIndex", + "parameters": [ + { + "flags": { + "isOptional": false, + }, + "name": "index", + "typeName": "number", + }, + ], + "returnType": { + "isNullable": true, + "name": "SideNavigationItemWrapper", + }, + }, + { + "name": "findItemsControl", + "parameters": [], + "returnType": { + "isNullable": true, + "name": "ElementWrapper", + "typeArguments": [ + { + "name": "HTMLElement", + }, + ], + }, + }, + { + "name": "findLinkByHref", + "parameters": [ + { + "flags": { + "isOptional": false, + }, + "name": "href", + "typeName": "string", + }, + ], + "returnType": { + "isNullable": true, + "name": "ElementWrapper", + "typeArguments": [ + { + "name": "HTMLAnchorElement", + }, + ], + }, + }, + ], + "name": "SideNavigationWrapper", + }, + { + "methods": [ + { + "name": "findDivider", + "parameters": [], "returnType": { "isNullable": true, "name": "ElementWrapper", @@ -53151,6 +53723,17 @@ In this case, use findContentEditableElement() instead.", "name": "ElementWrapper", }, }, + { + "description": "Returns the "Clear sort" button rendered in the table header tools area. +The button is auto-rendered only when multi-column sorting is enabled and at +least one column is sorted. Returns \`null\` otherwise.", + "name": "findClearSort", + "parameters": [], + "returnType": { + "isNullable": false, + "name": "ButtonWrapper", + }, + }, { "name": "findCollectionPreferences", "parameters": [], @@ -53213,7 +53796,46 @@ For tables with column grouping this excludes group header cells.", "flags": { "isOptional": false, }, - "name": "colIndex", + "name": "colIndex", + "typeName": "number", + }, + ], + "returnType": { + "isNullable": false, + "name": "ElementWrapper", + }, + }, + { + "description": "Returns the per-column sort menu (kebab dropdown) for the column header at the +given index. Only present on tables that opt in to multi-column sorting.", + "name": "findColumnSortMenu", + "parameters": [ + { + "description": "1-based index of the column.", + "flags": { + "isOptional": false, + }, + "name": "columnIndex", + "typeName": "number", + }, + ], + "returnType": { + "isNullable": false, + "name": "TableColumnSortMenuWrapper", + }, + }, + { + "description": "Returns the sort priority badge on the column header at the given index. +The badge is only rendered when two or more columns participate in a +multi-column sort. Returns \`null\` otherwise.", + "name": "findColumnSortPriorityBadge", + "parameters": [ + { + "description": "1-based index of the column.", + "flags": { + "isOptional": false, + }, + "name": "columnIndex", "typeName": "number", }, ], @@ -53465,6 +54087,272 @@ Note: when used with collection-hooks the \`trackBy\` is set automatically from ], "name": "TableWrapper", }, + { + "methods": [ + { + "description": "Returns the "Add to sort (ascending)" menu item. + +Supported options: +* \`disabled\` (boolean) - Use it to find the disabled or non-disabled item.", + "name": "findAddToSortAscendingItem", + "parameters": [ + { + "defaultValue": "{}", + "flags": { + "isOptional": false, + }, + "name": "options", + "typeName": "{ disabled?: boolean | undefined; }", + }, + ], + "returnType": { + "isNullable": false, + "name": "ElementWrapper", + }, + }, + { + "description": "Returns the "Add to sort (descending)" menu item. + +Supported options: +* \`disabled\` (boolean) - Use it to find the disabled or non-disabled item.", + "name": "findAddToSortDescendingItem", + "parameters": [ + { + "defaultValue": "{}", + "flags": { + "isOptional": false, + }, + "name": "options", + "typeName": "{ disabled?: boolean | undefined; }", + }, + ], + "returnType": { + "isNullable": false, + "name": "ElementWrapper", + }, + }, + { + "description": "Finds the disabled reason tooltip for a dropdown item. Returns null if no disabled item with \`disabledReason\` is highlighted.", + "inheritedFrom": { + "name": "ButtonDropdownWrapper.findDisabledReason", + }, + "name": "findDisabledReason", + "parameters": [], + "returnType": { + "isNullable": false, + "name": "ElementWrapper", + }, + }, + { + "description": "Finds an expandable category in the open dropdown by category id. Returns null if there is no open dropdown. + +This utility does not open the dropdown. To find dropdown items, call \`openDropdown()\` first.", + "inheritedFrom": { + "name": "ButtonDropdownWrapper.findExpandableCategoryById", + }, + "name": "findExpandableCategoryById", + "parameters": [ + { + "flags": { + "isOptional": false, + }, + "name": "id", + "typeName": "string", + }, + ], + "returnType": { + "isNullable": false, + "name": "ElementWrapper", + }, + }, + { + "description": "Finds the highlighted item in the open dropdown. Returns null if there is no open dropdown. + +This utility does not open the dropdown. To find dropdown items, call \`openDropdown()\` first.", + "inheritedFrom": { + "name": "ButtonDropdownWrapper.findHighlightedItem", + }, + "name": "findHighlightedItem", + "parameters": [], + "returnType": { + "isNullable": false, + "name": "ElementWrapper", + }, + }, + { + "description": "Finds an item in the open dropdown by item id. Returns null if there is no open dropdown. + +This utility does not open the dropdown. To find dropdown items, call \`openDropdown()\` first. + +Supported options: +* \`disabled\` (boolean) - Use it to find the disabled or non-disabled item.", + "inheritedFrom": { + "name": "ButtonDropdownWrapper.findItemById", + }, + "name": "findItemById", + "parameters": [ + { + "flags": { + "isOptional": false, + }, + "name": "id", + "typeName": "string", + }, + { + "defaultValue": "{}", + "flags": { + "isOptional": false, + }, + "name": "options", + "typeName": "{ disabled?: boolean | undefined; }", + }, + ], + "returnType": { + "isNullable": false, + "name": "ElementWrapper", + }, + }, + { + "description": "Finds all the items in the open dropdown. Returns empty array if there is no open dropdown. + +This utility does not open the dropdown. To find dropdown items, call \`openDropdown()\` first. + +Supported options: +* \`disabled\` (boolean) - Use it to find all disabled or non-disabled items.", + "inheritedFrom": { + "name": "ButtonDropdownWrapper.findItems", + }, + "name": "findItems", + "parameters": [ + { + "defaultValue": "{}", + "flags": { + "isOptional": false, + }, + "name": "options", + "typeName": "{ disabled?: boolean | undefined; }", + }, + ], + "returnType": { + "isNullable": false, + "name": "MultiElementWrapper", + "typeArguments": [ + { + "name": "ElementWrapper", + }, + ], + }, + }, + { + "inheritedFrom": { + "name": "ButtonDropdownWrapper.findMainAction", + }, + "name": "findMainAction", + "parameters": [], + "returnType": { + "isNullable": false, + "name": "ButtonWrapper", + }, + }, + { + "inheritedFrom": { + "name": "ButtonDropdownWrapper.findNativeButton", + }, + "name": "findNativeButton", + "parameters": [], + "returnType": { + "isNullable": false, + "name": "ElementWrapper", + }, + }, + { + "inheritedFrom": { + "name": "ButtonDropdownWrapper.findOpenDropdown", + }, + "name": "findOpenDropdown", + "parameters": [], + "returnType": { + "isNullable": false, + "name": "ElementWrapper", + }, + }, + { + "description": "Returns the "Remove from sort" menu item. + +Supported options: +* \`disabled\` (boolean) - Use it to find the disabled or non-disabled item.", + "name": "findRemoveFromSortItem", + "parameters": [ + { + "defaultValue": "{}", + "flags": { + "isOptional": false, + }, + "name": "options", + "typeName": "{ disabled?: boolean | undefined; }", + }, + ], + "returnType": { + "isNullable": false, + "name": "ElementWrapper", + }, + }, + { + "description": "Returns the "Sort ascending" menu item. + +Supported options: +* \`disabled\` (boolean) - Use it to find the disabled or non-disabled item.", + "name": "findSortAscendingItem", + "parameters": [ + { + "defaultValue": "{}", + "flags": { + "isOptional": false, + }, + "name": "options", + "typeName": "{ disabled?: boolean | undefined; }", + }, + ], + "returnType": { + "isNullable": false, + "name": "ElementWrapper", + }, + }, + { + "description": "Returns the "Sort descending" menu item. + +Supported options: +* \`disabled\` (boolean) - Use it to find the disabled or non-disabled item.", + "name": "findSortDescendingItem", + "parameters": [ + { + "defaultValue": "{}", + "flags": { + "isOptional": false, + }, + "name": "options", + "typeName": "{ disabled?: boolean | undefined; }", + }, + ], + "returnType": { + "isNullable": false, + "name": "ElementWrapper", + }, + }, + { + "inheritedFrom": { + "name": "ButtonDropdownWrapper.findTriggerButton", + }, + "name": "findTriggerButton", + "parameters": [], + "returnType": { + "isNullable": false, + "name": "ButtonWrapper", + }, + }, + ], + "name": "TableColumnSortMenuWrapper", + }, { "methods": [ { diff --git a/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap b/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap index 4b1af42bc5..b0220aa79a 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap @@ -661,8 +661,10 @@ exports[`test-utils selectors 1`] = ` "awsui_root_wih1l", "awsui_row-selected_wih1l", "awsui_row_wih1l", + "awsui_sort-priority-badge_1spae", "awsui_table_wih1l", "awsui_thead-active_wih1l", + "awsui_tools-clear-sort_wih1l", "awsui_tools-filtering_wih1l", "awsui_tools-pagination_wih1l", "awsui_tools-preferences_wih1l", diff --git a/src/button-dropdown/__tests__/button-dropdown-compact-trigger.test.tsx b/src/button-dropdown/__tests__/button-dropdown-compact-trigger.test.tsx new file mode 100644 index 0000000000..125ed7fb91 --- /dev/null +++ b/src/button-dropdown/__tests__/button-dropdown-compact-trigger.test.tsx @@ -0,0 +1,27 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import * as React from 'react'; +import { render } from '@testing-library/react'; + +import InternalButtonDropdown from '../../../lib/components/button-dropdown/internal'; +import { InternalButtonDropdownProps } from '../../../lib/components/button-dropdown/internal-interfaces'; +import createWrapper from '../../../lib/components/test-utils/dom'; + +import styles from '../../../lib/components/button-dropdown/styles.css.js'; + +const items: InternalButtonDropdownProps['items'] = [{ id: 'one', text: 'One' }]; + +function renderDropdown(props: Partial) { + const { container } = render(); + return createWrapper(container).findButtonDropdown()!; +} + +test('applies the compact-trigger class to the trigger button when compactTrigger is set', () => { + const wrapper = renderDropdown({ compactTrigger: true }); + expect(wrapper.findNativeButton().getElement()).toHaveClass(styles['compact-trigger']); +}); + +test('does not apply the compact-trigger class by default', () => { + const wrapper = renderDropdown({}); + expect(wrapper.findNativeButton().getElement()).not.toHaveClass(styles['compact-trigger']); +}); diff --git a/src/button-dropdown/internal-interfaces.ts b/src/button-dropdown/internal-interfaces.ts index 9b75e87c59..840196deb0 100644 --- a/src/button-dropdown/internal-interfaces.ts +++ b/src/button-dropdown/internal-interfaces.ts @@ -141,4 +141,11 @@ export interface InternalButtonDropdownProps * Position of the button dropdown inside a list of elements, for example a button group */ position?: string; + + /** + * Renders the trigger button without vertical padding or borders, matching the compact + * `inline-icon` footprint. Use with `variant="icon"` to get the icon variant's neutral colour + * while keeping an inline footprint (used by the table multi-column sort menu). + */ + compactTrigger?: boolean; } diff --git a/src/button-dropdown/internal.tsx b/src/button-dropdown/internal.tsx index 51d997c21c..9d501ff529 100644 --- a/src/button-dropdown/internal.tsx +++ b/src/button-dropdown/internal.tsx @@ -77,6 +77,7 @@ const InternalButtonDropdown = React.forwardRef( filteringResultsText, noMatch, i18nStrings, + compactTrigger, ...props }: InternalButtonDropdownProps, ref: React.Ref @@ -361,6 +362,7 @@ const InternalButtonDropdown = React.forwardRef( className={clsx(baseTriggerProps.className, { [styles['full-width']]: canBeFullWidth, [styles.loading]: canBeFullWidth && !!loading, + [styles['compact-trigger']]: compactTrigger, })} badge={triggerHasBadge()} fullWidth={fullWidth} diff --git a/src/button-dropdown/styles.scss b/src/button-dropdown/styles.scss index ff801d17df..b938ab3fa3 100644 --- a/src/button-dropdown/styles.scss +++ b/src/button-dropdown/styles.scss @@ -65,6 +65,15 @@ $dropdown-trigger-icon-offset: 2px; } } +// Removes the trigger's vertical padding and borders so an `icon`-variant trigger matches the +// compact `inline-icon` footprint. The class is doubled to override the button variant styles +// (`.button.variant-icon`), which button-dropdown CSS is loaded after. +.compact-trigger.compact-trigger { + padding-block: 0; + border-block-width: 0; + border-inline-width: 0; +} + .split-trigger-wrapper { display: flex; diff --git a/src/i18n/messages-types.ts b/src/i18n/messages-types.ts index 63da31c470..af668ad823 100644 --- a/src/i18n/messages-types.ts +++ b/src/i18n/messages-types.ts @@ -536,6 +536,24 @@ export interface I18nFormatArgTypes { 'ariaLabels.collapseButtonLabel': never; 'columnDefinitions.editConfig.errorIconAriaLabel': never; 'columnDefinitions.editConfig.editIconAriaLabel': never; + 'i18nStrings.sortDropdown.sortAscending': never; + 'i18nStrings.sortDropdown.sortDescending': never; + 'i18nStrings.sortDropdown.multiColumnSortGroup': never; + 'i18nStrings.sortDropdown.addToSortAscending': never; + 'i18nStrings.sortDropdown.addToSortDescending': never; + 'i18nStrings.sortDropdown.removeFromSort': never; + 'i18nStrings.sortDropdown.addToSortDisabledReason': never; + 'i18nStrings.sortDropdown.removeFromSortDisabledReason': never; + 'i18nStrings.clearSort': never; + 'i18nStrings.liveAnnouncementSortColumn': { + columnLabel: string | number; + isDescending: string; + }; + 'i18nStrings.liveAnnouncementSortOrder': { + columns: string | number; + }; + 'i18nStrings.liveAnnouncementSortCleared': never; + 'ariaLabels.sortMenuTriggerLabel': never; }; tabs: { 'i18nStrings.scrollLeftAriaLabel': never; diff --git a/src/i18n/messages/all.en-GB.json b/src/i18n/messages/all.en-GB.json index dd45937e1b..a42ff8cf17 100644 --- a/src/i18n/messages/all.en-GB.json +++ b/src/i18n/messages/all.en-GB.json @@ -408,7 +408,15 @@ "ariaLabels.expandButtonLabel": "Expand", "ariaLabels.collapseButtonLabel": "Collapse", "columnDefinitions.editConfig.errorIconAriaLabel": "Error", - "columnDefinitions.editConfig.editIconAriaLabel": "editable" + "columnDefinitions.editConfig.editIconAriaLabel": "editable", + "i18nStrings.sortDropdown.sortAscending": "Sort ascending", + "i18nStrings.sortDropdown.sortDescending": "Sort descending", + "i18nStrings.sortDropdown.multiColumnSortGroup": "Multi-column sort (Shift + Click)", + "i18nStrings.sortDropdown.addToSortAscending": "Add to sort (ascending)", + "i18nStrings.sortDropdown.addToSortDescending": "Add to sort (descending)", + "i18nStrings.sortDropdown.removeFromSort": "Remove from sort", + "i18nStrings.clearSort": "Clear sort", + "ariaLabels.sortMenuTriggerLabel": "Sort options" }, "tabs": { "i18nStrings.scrollLeftAriaLabel": "Scroll left", @@ -495,4 +503,4 @@ "i18nStrings.nextButtonLoadingAnnouncement": "Loading next step", "i18nStrings.submitButtonLoadingAnnouncement": "Submitting form" } -} \ No newline at end of file +} diff --git a/src/i18n/messages/all.en.json b/src/i18n/messages/all.en.json index e075e74f0d..d92b2435df 100644 --- a/src/i18n/messages/all.en.json +++ b/src/i18n/messages/all.en.json @@ -409,7 +409,20 @@ "ariaLabels.expandButtonLabel": "Expand", "ariaLabels.collapseButtonLabel": "Collapse", "columnDefinitions.editConfig.errorIconAriaLabel": "Error", - "columnDefinitions.editConfig.editIconAriaLabel": "editable" + "columnDefinitions.editConfig.editIconAriaLabel": "editable", + "i18nStrings.sortDropdown.sortAscending": "Sort ascending", + "i18nStrings.sortDropdown.sortDescending": "Sort descending", + "i18nStrings.sortDropdown.multiColumnSortGroup": "Multi-column sort (Shift + Click)", + "i18nStrings.sortDropdown.addToSortAscending": "Add to sort (ascending)", + "i18nStrings.sortDropdown.addToSortDescending": "Add to sort (descending)", + "i18nStrings.sortDropdown.removeFromSort": "Remove from sort", + "i18nStrings.sortDropdown.addToSortDisabledReason": "This column is already sorted", + "i18nStrings.sortDropdown.removeFromSortDisabledReason": "This column is not sorted", + "i18nStrings.clearSort": "Clear sort", + "i18nStrings.liveAnnouncementSortColumn": "{isDescending, select, true {{columnLabel} descending} other {{columnLabel} ascending}}", + "i18nStrings.liveAnnouncementSortOrder": "Table sorted by {columns}", + "i18nStrings.liveAnnouncementSortCleared": "Sorting cleared", + "ariaLabels.sortMenuTriggerLabel": "Sort options" }, "tabs": { "i18nStrings.scrollLeftAriaLabel": "Scroll left", @@ -496,4 +509,4 @@ "i18nStrings.nextButtonLoadingAnnouncement": "Loading next step", "i18nStrings.submitButtonLoadingAnnouncement": "Submitting form" } -} \ No newline at end of file +} diff --git a/src/table/__tests__/multi-column-sort-behavior.test.tsx b/src/table/__tests__/multi-column-sort-behavior.test.tsx new file mode 100644 index 0000000000..c9401c5b5a --- /dev/null +++ b/src/table/__tests__/multi-column-sort-behavior.test.tsx @@ -0,0 +1,270 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import * as React from 'react'; +import { fireEvent, render } from '@testing-library/react'; + +import { ElementWrapper } from '@cloudscape-design/test-utils-core/dom'; +import { KeyCode } from '@cloudscape-design/test-utils-core/utils'; + +import Table from '../../../lib/components/table'; +import { TableProps } from '../../../lib/components/table/interfaces'; +import createWrapper, { TableWrapper } from '../../../lib/components/test-utils/dom'; + +interface Item { + a: string; + b: string; + c: string; +} + +const columnDefinitions: TableProps.ColumnDefinition[] = [ + { id: 'a', header: 'A', cell: i => i.a, sortingField: 'a' }, + { id: 'b', header: 'B', cell: i => i.b, sortingField: 'b' }, + { id: 'c', header: 'C', cell: i => i.c, sortingField: 'c' }, +]; + +const items: Item[] = [{ a: '1', b: '2', c: '3' }]; + +// 1-based column indices +const COL_A = 1; +const COL_B = 2; +const COL_C = 3; + +type Sort = ReadonlyArray>; + +function renderTable(sortingColumns: Sort, onChange = jest.fn(), extraColumns?: TableProps.ColumnDefinition[]) { + const { container } = render( +
+ ); + return { wrapper: createWrapper(container).findTable()!, onChange }; +} + +// Extracts the emitted sort state as a comparable [{ field, desc }] array. +function emitted(onChange: jest.Mock) { + const detail = onChange.mock.calls[onChange.mock.calls.length - 1][0] + .detail as TableProps.MultiColumnSortChangeDetail; + return detail.sortingColumns.map(s => ({ field: s.sortingColumn.sortingField, desc: !!s.isDescending })); +} + +function clickHeader(wrapper: TableWrapper, colIndex: number, shiftKey = false) { + fireEvent.click(wrapper.findColumnSortingArea(colIndex)!.getElement(), { shiftKey }); +} + +function getHeaderCell(wrapper: TableWrapper, colIndex: number) { + return wrapper.findColumnHeaders()[colIndex - 1]!.getElement(); +} + +describe('header click / keyboard', () => { + test('plain click on an unsorted column replaces the sort (ascending)', () => { + const { wrapper, onChange } = renderTable([]); + clickHeader(wrapper, COL_A); + expect(emitted(onChange)).toEqual([{ field: 'a', desc: false }]); + }); + + test('plain click on a column already in the sort toggles its direction', () => { + const { wrapper, onChange } = renderTable([{ sortingColumn: { sortingField: 'a' }, isDescending: false }]); + clickHeader(wrapper, COL_A); + expect(emitted(onChange)).toEqual([{ field: 'a', desc: true }]); + }); + + test('Shift+click appends a new column to the sort', () => { + const { wrapper, onChange } = renderTable([{ sortingColumn: { sortingField: 'a' }, isDescending: false }]); + clickHeader(wrapper, COL_B, true); + expect(emitted(onChange)).toEqual([ + { field: 'a', desc: false }, + { field: 'b', desc: false }, + ]); + }); + + test('Enter triggers the same behavior as a click', () => { + const { wrapper, onChange } = renderTable([]); + fireEvent.keyPress(wrapper.findColumnSortingArea(COL_A)!.getElement(), { keyCode: KeyCode.enter }); + expect(emitted(onChange)).toEqual([{ field: 'a', desc: false }]); + }); + + test('Shift+Enter appends like Shift+click', () => { + const { wrapper, onChange } = renderTable([{ sortingColumn: { sortingField: 'a' }, isDescending: false }]); + fireEvent.keyPress(wrapper.findColumnSortingArea(COL_B)!.getElement(), { keyCode: KeyCode.enter, shiftKey: true }); + expect(emitted(onChange)).toEqual([ + { field: 'a', desc: false }, + { field: 'b', desc: false }, + ]); + }); + + test('Shift+click on a column already in the sort toggles it in place (does not duplicate)', () => { + const { wrapper, onChange } = renderTable([{ sortingColumn: { sortingField: 'a' }, isDescending: false }]); + clickHeader(wrapper, COL_A, true); + expect(emitted(onChange)).toEqual([{ field: 'a', desc: true }]); + }); + + test('Shift+mousedown prevents default to avoid extending the text selection', () => { + const { wrapper } = renderTable([]); + // fireEvent returns false when the event's default action was prevented. + const notPreventedWithShift = fireEvent.mouseDown(wrapper.findColumnSortingArea(COL_A)!.getElement(), { + shiftKey: true, + }); + expect(notPreventedWithShift).toBe(false); + // A plain mousedown must not prevent default. + const notPreventedPlain = fireEvent.mouseDown(wrapper.findColumnSortingArea(COL_A)!.getElement()); + expect(notPreventedPlain).toBe(true); + }); +}); + +describe('sort menu dropdown actions', () => { + function openAndClick( + wrapper: TableWrapper, + colIndex: number, + click: (menu: ReturnType) => void + ) { + const menu = wrapper.findColumnSortMenu(colIndex)!; + menu.openDropdown(); + click(menu); + } + + test('"Add to sort (descending)" appends the column descending', () => { + const { wrapper, onChange } = renderTable([{ sortingColumn: { sortingField: 'a' }, isDescending: false }]); + openAndClick(wrapper, COL_B, menu => menu!.findAddToSortDescendingItem()!.click()); + expect(emitted(onChange)).toEqual([ + { field: 'a', desc: false }, + { field: 'b', desc: true }, + ]); + }); + + test('"Remove from sort" removes the column and keeps the rest', () => { + const { wrapper, onChange } = renderTable([ + { sortingColumn: { sortingField: 'a' }, isDescending: false }, + { sortingColumn: { sortingField: 'b' }, isDescending: true }, + ]); + openAndClick(wrapper, COL_A, menu => menu!.findRemoveFromSortItem()!.click()); + expect(emitted(onChange)).toEqual([{ field: 'b', desc: true }]); + }); + + test('"Sort descending" on a column not in the sort replaces the whole sort', () => { + const { wrapper, onChange } = renderTable([{ sortingColumn: { sortingField: 'b' }, isDescending: false }]); + openAndClick(wrapper, COL_A, menu => menu!.findSortDescendingItem()!.click()); + expect(emitted(onChange)).toEqual([{ field: 'a', desc: true }]); + }); + + test('"Sort ascending" on a column not in the sort replaces the whole sort', () => { + const { wrapper, onChange } = renderTable([{ sortingColumn: { sortingField: 'b' }, isDescending: true }]); + openAndClick(wrapper, COL_A, menu => menu!.findSortAscendingItem()!.click()); + expect(emitted(onChange)).toEqual([{ field: 'a', desc: false }]); + }); + + test('"Sort ascending" on a column already in the sort sets its direction and keeps the others', () => { + const { wrapper, onChange } = renderTable([ + { sortingColumn: { sortingField: 'a' }, isDescending: true }, + { sortingColumn: { sortingField: 'b' }, isDescending: true }, + ]); + openAndClick(wrapper, COL_A, menu => menu!.findSortAscendingItem()!.click()); + expect(emitted(onChange)).toEqual([ + { field: 'a', desc: false }, + { field: 'b', desc: true }, + ]); + }); + + test('"Add to sort (ascending)" appends the column ascending', () => { + const { wrapper, onChange } = renderTable([{ sortingColumn: { sortingField: 'a' }, isDescending: false }]); + openAndClick(wrapper, COL_B, menu => menu!.findAddToSortAscendingItem()!.click()); + expect(emitted(onChange)).toEqual([ + { field: 'a', desc: false }, + { field: 'b', desc: false }, + ]); + }); +}); + +describe('aria-sort', () => { + test('declares aria-sort only on the primary sorted column; secondaries are suppressed; unsorted are "none"', () => { + const { wrapper } = renderTable([ + { sortingColumn: { sortingField: 'a' }, isDescending: false }, + { sortingColumn: { sortingField: 'b' }, isDescending: true }, + ]); + expect(getHeaderCell(wrapper, COL_A).getAttribute('aria-sort')).toBe('ascending'); + // ARIA permits only one sorted column, so secondary sorted columns omit aria-sort entirely. + expect(getHeaderCell(wrapper, COL_B).getAttribute('aria-sort')).toBeNull(); + expect(getHeaderCell(wrapper, COL_C).getAttribute('aria-sort')).toBe('none'); + }); +}); + +describe('column ariaLabel sortIndex', () => { + test('receives the 1-based priority for sorted columns and undefined for unsorted', () => { + const labelColumns: TableProps.ColumnDefinition[] = [ + { id: 'a', header: 'A', cell: i => i.a, sortingField: 'a', ariaLabel: ({ sortIndex }) => `A idx=${sortIndex}` }, + { id: 'b', header: 'B', cell: i => i.b, sortingField: 'b', ariaLabel: ({ sortIndex }) => `B idx=${sortIndex}` }, + { id: 'c', header: 'C', cell: i => i.c, sortingField: 'c', ariaLabel: ({ sortIndex }) => `C idx=${sortIndex}` }, + ]; + const { wrapper } = renderTable( + [ + { sortingColumn: { sortingField: 'a' }, isDescending: false }, + { sortingColumn: { sortingField: 'b' }, isDescending: true }, + ], + jest.fn(), + labelColumns + ); + expect(wrapper.findColumnSortingArea(COL_A)!.getElement().getAttribute('aria-label')).toBe('A idx=1'); + expect(wrapper.findColumnSortingArea(COL_B)!.getElement().getAttribute('aria-label')).toBe('B idx=2'); + expect(wrapper.findColumnSortingArea(COL_C)!.getElement().getAttribute('aria-label')).toBe('C idx=undefined'); + }); +}); + +describe('sort menu gating', () => { + test('non-sortable columns do not render a sort menu', () => { + const mixed: TableProps.ColumnDefinition[] = [ + { id: 'a', header: 'A', cell: i => i.a, sortingField: 'a' }, + { id: 'b', header: 'B', cell: i => i.b }, // no sortingField -> not sortable + ]; + const { wrapper } = renderTable([], jest.fn(), mixed); + expect(wrapper.findColumnSortMenu(COL_A)).not.toBeNull(); + expect(wrapper.findColumnSortMenu(COL_B)).toBeNull(); + }); +}); + +describe('sort menu item states', () => { + function openMenu(wrapper: TableWrapper, colIndex: number) { + const menu = wrapper.findColumnSortMenu(colIndex)!; + menu.openDropdown(); + return menu; + } + + const ariaChecked = (item: ElementWrapper) => + item.find('[role="menuitemcheckbox"]')!.getElement().getAttribute('aria-checked'); + + test('marks the current direction as checked for a column sorted ascending', () => { + const { wrapper } = renderTable([{ sortingColumn: { sortingField: 'a' }, isDescending: false }]); + const menu = openMenu(wrapper, COL_A); + expect(ariaChecked(menu.findSortAscendingItem()!)).toBe('true'); + expect(ariaChecked(menu.findSortDescendingItem()!)).toBe('false'); + }); + + test('marks descending as checked for a column sorted descending', () => { + const { wrapper } = renderTable([{ sortingColumn: { sortingField: 'a' }, isDescending: true }]); + const menu = openMenu(wrapper, COL_A); + expect(ariaChecked(menu.findSortDescendingItem()!)).toBe('true'); + expect(ariaChecked(menu.findSortAscendingItem()!)).toBe('false'); + }); + + test('leaves both direction items unchecked for a column that is not sorted', () => { + const { wrapper } = renderTable([{ sortingColumn: { sortingField: 'b' }, isDescending: false }]); + const menu = openMenu(wrapper, COL_A); + expect(ariaChecked(menu.findSortAscendingItem()!)).toBe('false'); + expect(ariaChecked(menu.findSortDescendingItem()!)).toBe('false'); + }); + + test('disables the "Add to sort" items (and enables "Remove from sort") when the column is in the sort', () => { + const { wrapper } = renderTable([{ sortingColumn: { sortingField: 'a' }, isDescending: false }]); + const menu = openMenu(wrapper, COL_A); + expect(menu.findAddToSortAscendingItem({ disabled: true })).not.toBeNull(); + expect(menu.findAddToSortDescendingItem({ disabled: true })).not.toBeNull(); + expect(menu.findRemoveFromSortItem({ disabled: true })).toBeNull(); + }); + + test('disables "Remove from sort" (and enables "Add to sort") when the column is not in the sort', () => { + const { wrapper } = renderTable([{ sortingColumn: { sortingField: 'b' }, isDescending: false }]); + const menu = openMenu(wrapper, COL_A); + expect(menu.findRemoveFromSortItem({ disabled: true })).not.toBeNull(); + expect(menu.findAddToSortAscendingItem({ disabled: true })).toBeNull(); + }); +}); diff --git a/src/table/__tests__/multi-column-sort-clear.test.tsx b/src/table/__tests__/multi-column-sort-clear.test.tsx new file mode 100644 index 0000000000..55ba7c2814 --- /dev/null +++ b/src/table/__tests__/multi-column-sort-clear.test.tsx @@ -0,0 +1,121 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import * as React from 'react'; +import { render } from '@testing-library/react'; + +import Pagination from '../../../lib/components/pagination'; +import Table from '../../../lib/components/table'; +import { TableProps } from '../../../lib/components/table/interfaces'; +import createWrapper from '../../../lib/components/test-utils/dom'; + +import tableStyles from '../../../lib/components/table/styles.css.js'; + +interface Item { + id: string; + name: string; +} + +const columnDefinitions: TableProps.ColumnDefinition[] = [ + { id: 'name', header: 'Name', cell: item => item.name, sortingField: 'name' }, + { id: 'id', header: 'Id', cell: item => item.id, sortingField: 'id' }, +]; + +const items: Item[] = [ + { id: '1', name: 'alpha' }, + { id: '2', name: 'beta' }, +]; + +const activeSort: ReadonlyArray> = [ + { sortingColumn: { sortingField: 'name' }, isDescending: false }, +]; + +function renderTable(jsx: React.ReactElement) { + const { container } = render(jsx); + return { wrapper: createWrapper(container).findTable()!, container }; +} + +test('renders a Clear sort button when multiColumnSort is active on at least one column', () => { + const { wrapper } = renderTable( +
{} }} + /> + ); + // The label text itself comes from the i18n provider (no hardcoded fallback in the component), + // so here we only assert the button is rendered. Label text is covered by the i18nStrings test below. + expect(wrapper.findClearSort()).not.toBeNull(); +}); + +test('uses the i18nStrings.clearSort label when provided', () => { + const { wrapper } = renderTable( +
{} }} + i18nStrings={{ clearSort: 'Reset sorting' }} + /> + ); + expect(wrapper.findClearSort()!.getElement()).toHaveTextContent('Reset sorting'); +}); + +test('does not render the Clear sort button when no column is sorted', () => { + const { wrapper } = renderTable( +
{} }} + /> + ); + expect(wrapper.findClearSort()).toBeNull(); +}); + +test('does not render the Clear sort button for single-column (non-multi) sorting', () => { + const { wrapper } = renderTable( +
+ ); + expect(wrapper.findClearSort()).toBeNull(); +}); + +test('clicking Clear sort fires onChange with an empty sorting state', () => { + const onChange = jest.fn(); + const { wrapper } = renderTable( +
+ ); + wrapper.findClearSort()!.click(); + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange.mock.calls[0][0].detail).toEqual({ sortingColumns: [] }); +}); + +test('moves focus to the first sortable column header after clearing sort (avoids losing focus to the body)', () => { + const { wrapper } = renderTable( +
{} }} + /> + ); + wrapper.findClearSort()!.click(); + // Focus should land on the first sortable column's sort control, not fall back to document.body. + expect(document.activeElement).toHaveAttribute('data-focus-id', 'sorting-control-name'); +}); + +test('renders the Clear sort button before the pagination slot', () => { + const { container } = renderTable( +
{} }} + pagination={} + /> + ); + const clearSort = container.querySelector(`.${tableStyles['tools-clear-sort']}`)!; + const pagination = container.querySelector(`.${tableStyles['tools-pagination']}`)!; + expect(clearSort).not.toBeNull(); + expect(pagination).not.toBeNull(); + expect(clearSort.compareDocumentPosition(pagination) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); +}); diff --git a/src/table/__tests__/multi-column-sort-live-announcement.test.tsx b/src/table/__tests__/multi-column-sort-live-announcement.test.tsx new file mode 100644 index 0000000000..d03935bcd3 --- /dev/null +++ b/src/table/__tests__/multi-column-sort-live-announcement.test.tsx @@ -0,0 +1,64 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import * as React from 'react'; +import { render, waitFor } from '@testing-library/react'; + +import Table from '../../../lib/components/table'; +import { TableProps } from '../../../lib/components/table/interfaces'; + +interface Item { + id: string; + name: string; +} + +const items: Item[] = [ + { id: '1', name: 'alpha' }, + { id: '2', name: 'beta' }, +]; + +// ReactNode (non-string) headers, like a translation component, so the announcement can only produce +// these names by reading the rendered DOM text — not `columnDefinitions[].header`. +const columnDefinitions: TableProps.ColumnDefinition[] = [ + { id: 'name', header: Rendered name, cell: item => item.name, sortingField: 'name' }, + { id: 'id', header: Rendered id, cell: item => item.id, sortingField: 'id' }, +]; + +const i18nStrings: TableProps.I18nStrings = { + liveAnnouncementSortColumn: ({ columnLabel, isDescending }) => + `${columnLabel} ${isDescending ? 'descending' : 'ascending'}`, + liveAnnouncementSortOrder: ({ columns }) => `Table sorted by ${columns}`, + liveAnnouncementSortCleared: 'Sorting cleared', +}; + +function TableWithSort({ sortingColumns }: { sortingColumns: ReadonlyArray> }) { + return ( +
{} }} + i18nStrings={i18nStrings} + /> + ); +} + +const nameAsc: TableProps.SortingState = { sortingColumn: { sortingField: 'name' }, isDescending: false }; +const idDesc: TableProps.SortingState = { sortingColumn: { sortingField: 'id' }, isDescending: true }; + +test('does not announce the initial sort state on mount', () => { + const { container } = render(); + expect(container.textContent).not.toContain('Table sorted by'); +}); + +test('announces a sort change using the rendered (DOM) header text', async () => { + const { container, rerender } = render(); + rerender(); + await waitFor(() => + expect(container.textContent).toContain('Table sorted by Rendered name ascending, Rendered id descending') + ); +}); + +test('announces when sorting is cleared', async () => { + const { container, rerender } = render(); + rerender(); + await waitFor(() => expect(container.textContent).toContain('Sorting cleared')); +}); diff --git a/src/table/__tests__/multi-column-sort-test-utils.test.tsx b/src/table/__tests__/multi-column-sort-test-utils.test.tsx new file mode 100644 index 0000000000..146d949a3f --- /dev/null +++ b/src/table/__tests__/multi-column-sort-test-utils.test.tsx @@ -0,0 +1,119 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import * as React from 'react'; +import { render } from '@testing-library/react'; + +import Table from '../../../lib/components/table'; +import { TableProps } from '../../../lib/components/table/interfaces'; +import createWrapper from '../../../lib/components/test-utils/dom'; + +interface Item { + id: string; + name: string; + state: string; + cpu: number; +} + +const columnDefinitions: TableProps.ColumnDefinition[] = [ + { id: 'name', header: 'Name', cell: item => item.name, sortingField: 'name' }, + { id: 'type', header: 'Type', cell: item => item.id, sortingField: 'type' }, + { id: 'state', header: 'State', cell: item => item.state, sortingField: 'state' }, + { id: 'cpu', header: 'CPU', cell: item => item.cpu, sortingField: 'cpu' }, +]; + +const items: Item[] = [ + { id: '1', name: 'alpha', state: 'running', cpu: 12 }, + { id: '2', name: 'beta', state: 'stopped', cpu: 0 }, +]; + +// Column indices are 1-based: name=1, type=2, state=3, cpu=4. +const STATE_COL = 3; +const CPU_COL = 4; +const NAME_COL = 1; + +function renderTable(jsx: React.ReactElement) { + const { container } = render(jsx); + return createWrapper(container).findTable()!; +} + +function renderMultiSort(sortingColumns: ReadonlyArray>) { + return renderTable( +
{} }} + /> + ); +} + +// state (asc, priority 1) + cpu (desc, priority 2) +const twoColumnSort: ReadonlyArray> = [ + { sortingColumn: { sortingField: 'state' }, isDescending: false }, + { sortingColumn: { sortingField: 'cpu' }, isDescending: true }, +]; + +describe('findColumnSortMenu', () => { + test('returns a sort menu wrapper for a sortable column when multiColumnSort is set', () => { + const wrapper = renderMultiSort(twoColumnSort); + expect(wrapper.findColumnSortMenu(STATE_COL)).not.toBeNull(); + }); + + test('returns null when the table does not opt in to multi-column sorting', () => { + const wrapper = renderTable(
); + expect(wrapper.findColumnSortMenu(STATE_COL)).toBeNull(); + }); + + test('exposes a named finder for every sort menu item once the dropdown is open', () => { + const wrapper = renderMultiSort(twoColumnSort); + const menu = wrapper.findColumnSortMenu(NAME_COL)!; + menu.openDropdown(); + + expect(menu.findSortAscendingItem()).not.toBeNull(); + expect(menu.findSortDescendingItem()).not.toBeNull(); + expect(menu.findAddToSortAscendingItem()).not.toBeNull(); + expect(menu.findAddToSortDescendingItem()).not.toBeNull(); + expect(menu.findRemoveFromSortItem()).not.toBeNull(); + }); + + test('reflects disabled state for a column already in the sort', () => { + const wrapper = renderMultiSort(twoColumnSort); + const menu = wrapper.findColumnSortMenu(STATE_COL)!; + menu.openDropdown(); + + // Sort ascending/descending are always-enabled checkboxes; their checked state is asserted in the behavior test. + expect(menu.findSortAscendingItem({ disabled: false })).not.toBeNull(); + expect(menu.findSortDescendingItem({ disabled: false })).not.toBeNull(); + expect(menu.findAddToSortAscendingItem({ disabled: true })).not.toBeNull(); + expect(menu.findAddToSortDescendingItem({ disabled: true })).not.toBeNull(); + expect(menu.findRemoveFromSortItem({ disabled: false })).not.toBeNull(); + }); + + test('reflects disabled state for a column not in the sort', () => { + const wrapper = renderMultiSort(twoColumnSort); + const menu = wrapper.findColumnSortMenu(NAME_COL)!; + menu.openDropdown(); + + expect(menu.findSortAscendingItem({ disabled: false })).not.toBeNull(); + expect(menu.findSortDescendingItem({ disabled: false })).not.toBeNull(); + expect(menu.findAddToSortAscendingItem({ disabled: false })).not.toBeNull(); + expect(menu.findRemoveFromSortItem({ disabled: true })).not.toBeNull(); + }); +}); + +describe('findColumnSortPriorityBadge', () => { + test('shows the 1-based priority on each sorted column when 2+ columns are sorted', () => { + const wrapper = renderMultiSort(twoColumnSort); + expect(wrapper.findColumnSortPriorityBadge(STATE_COL)!.getElement()).toHaveTextContent('1'); + expect(wrapper.findColumnSortPriorityBadge(CPU_COL)!.getElement()).toHaveTextContent('2'); + }); + + test('returns null for a column that is not part of the sort', () => { + const wrapper = renderMultiSort(twoColumnSort); + expect(wrapper.findColumnSortPriorityBadge(NAME_COL)).toBeNull(); + }); + + test('returns null when only a single column is sorted (priority adds no information)', () => { + const wrapper = renderMultiSort([{ sortingColumn: { sortingField: 'state' }, isDescending: false }]); + expect(wrapper.findColumnSortPriorityBadge(STATE_COL)).toBeNull(); + }); +}); diff --git a/src/table/__tests__/multi-column-sort-utils.test.ts b/src/table/__tests__/multi-column-sort-utils.test.ts new file mode 100644 index 0000000000..e999564bbc --- /dev/null +++ b/src/table/__tests__/multi-column-sort-utils.test.ts @@ -0,0 +1,216 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { TableProps } from '../../../lib/components/table/interfaces'; +import { + appendSort, + buildSortLiveAnnouncement, + getSortIndex, + removeSort, + replaceSort, + setDirection, + toggleDirection, +} from '../../../lib/components/table/multi-column-sort/utils'; + +interface Item { + a: number; + b: number; +} + +const colA: TableProps.ColumnDefinition = { id: 'a', header: 'A', cell: i => i.a, sortingField: 'a' }; +const colB: TableProps.ColumnDefinition = { id: 'b', header: 'B', cell: i => i.b, sortingField: 'b' }; +const comparator = (x: Item, y: Item) => x.a - y.a; +const colComparator: TableProps.ColumnDefinition = { + id: 'c', + header: 'C', + cell: i => i.a, + sortingComparator: comparator, +}; + +type Sort = ReadonlyArray>; + +describe('getSortIndex', () => { + test('returns null when the column is not in the sort', () => { + expect(getSortIndex([], colA)).toBeNull(); + expect(getSortIndex([{ sortingColumn: colB, isDescending: false }], colA)).toBeNull(); + }); + + test('returns the 1-based priority index', () => { + const sort: Sort = [ + { sortingColumn: colB, isDescending: false }, + { sortingColumn: colA, isDescending: true }, + ]; + expect(getSortIndex(sort, colB)).toBe(1); + expect(getSortIndex(sort, colA)).toBe(2); + }); + + test('matches by sortingField even with a different column-definition object', () => { + const sort: Sort = [{ sortingColumn: { sortingField: 'a' }, isDescending: false }]; + expect(getSortIndex(sort, colA)).toBe(1); + }); + + test('matches by sortingComparator reference', () => { + const sort: Sort = [{ sortingColumn: { sortingComparator: comparator }, isDescending: false }]; + expect(getSortIndex(sort, colComparator)).toBe(1); + }); + + test('matches by object identity', () => { + const sort: Sort = [{ sortingColumn: colA, isDescending: false }]; + expect(getSortIndex(sort, colA)).toBe(1); + }); +}); + +describe('replaceSort', () => { + test('returns a single-element array with the given column and direction', () => { + expect(replaceSort(colA, true)).toEqual([{ sortingColumn: colA, isDescending: true }]); + expect(replaceSort(colB, false)).toEqual([{ sortingColumn: colB, isDescending: false }]); + }); +}); + +describe('appendSort', () => { + test('appends at the end, preserving existing entries and order', () => { + const current: Sort = [{ sortingColumn: colA, isDescending: false }]; + expect(appendSort(current, colB, true)).toEqual([ + { sortingColumn: colA, isDescending: false }, + { sortingColumn: colB, isDescending: true }, + ]); + }); + + test('does not mutate the input array', () => { + const current: Sort = [{ sortingColumn: colA, isDescending: false }]; + appendSort(current, colB, false); + expect(current).toHaveLength(1); + }); +}); + +describe('toggleDirection', () => { + test('flips only the matching column, keeping position and other entries', () => { + const current: Sort = [ + { sortingColumn: colA, isDescending: false }, + { sortingColumn: colB, isDescending: true }, + ]; + expect(toggleDirection(current, colA)).toEqual([ + { sortingColumn: colA, isDescending: true }, + { sortingColumn: colB, isDescending: true }, + ]); + }); + + test('is a no-op when the column is not present', () => { + const current: Sort = [{ sortingColumn: colB, isDescending: false }]; + expect(toggleDirection(current, colA)).toEqual(current); + }); +}); + +describe('setDirection', () => { + test('sets the matching column to the explicit direction', () => { + const current: Sort = [{ sortingColumn: colA, isDescending: false }]; + expect(setDirection(current, colA, true)).toEqual([{ sortingColumn: colA, isDescending: true }]); + }); + + test('is idempotent when already at the target direction', () => { + const current: Sort = [{ sortingColumn: colA, isDescending: true }]; + expect(setDirection(current, colA, true)).toEqual(current); + }); +}); + +describe('removeSort', () => { + test('removes the matching column and keeps the rest in order', () => { + const current: Sort = [ + { sortingColumn: colA, isDescending: false }, + { sortingColumn: colB, isDescending: true }, + ]; + expect(removeSort(current, colA)).toEqual([{ sortingColumn: colB, isDescending: true }]); + }); + + test('is a no-op when the column is not present', () => { + const current: Sort = [{ sortingColumn: colB, isDescending: false }]; + expect(removeSort(current, colA)).toEqual(current); + }); + + test('removes by sortingField match', () => { + const current: Sort = [{ sortingColumn: { sortingField: 'a' }, isDescending: false }]; + expect(removeSort(current, colA)).toEqual([]); + }); +}); + +describe('buildSortLiveAnnouncement', () => { + const renderSortColumn = ({ columnLabel, isDescending }: { columnLabel: string; isDescending: boolean }) => + `${columnLabel} ${isDescending ? 'desc' : 'asc'}`; + const renderSortOrder = ({ columns }: { columns: string }) => `sorted by ${columns}`; + const columnDefinitions = [colA, colB, colComparator]; + const base = { columnDefinitions, renderSortColumn, renderSortOrder, sortCleared: 'cleared' }; + + test('returns the cleared string when there is no active sort', () => { + expect(buildSortLiveAnnouncement({ ...base, sortingColumns: [] })).toBe('cleared'); + }); + + test('returns an empty string when there is no sort and no cleared string', () => { + expect(buildSortLiveAnnouncement({ ...base, sortCleared: undefined, sortingColumns: [] })).toBe(''); + }); + + test('returns an empty string when the render functions are missing', () => { + const sortingColumns: Sort = [{ sortingColumn: colA, isDescending: false }]; + expect(buildSortLiveAnnouncement({ columnDefinitions, sortingColumns, renderSortOrder })).toBe(''); + expect(buildSortLiveAnnouncement({ columnDefinitions, sortingColumns, renderSortColumn })).toBe(''); + }); + + test('does not throw when sortingColumns is omitted', () => { + expect(buildSortLiveAnnouncement({ ...base } as Parameters[0])).toBe('cleared'); + }); + + test('joins per-column fragments (default comma join) and wraps them', () => { + const sortingColumns: Sort = [ + { sortingColumn: colA, isDescending: false }, + { sortingColumn: colB, isDescending: true }, + ]; + expect(buildSortLiveAnnouncement({ ...base, sortingColumns })).toBe('sorted by A asc, B desc'); + }); + + test('uses the provided list formatter instead of the default comma join', () => { + const sortingColumns: Sort = [ + { sortingColumn: colA, isDescending: false }, + { sortingColumn: colB, isDescending: true }, + ]; + const formatList = (parts: readonly string[]) => parts.join(' | '); + expect(buildSortLiveAnnouncement({ ...base, sortingColumns, formatList })).toBe('sorted by A asc | B desc'); + }); + + test('prefers resolveColumnLabel over the column-definition label', () => { + const sortingColumns: Sort = [{ sortingColumn: colA, isDescending: false }]; + expect(buildSortLiveAnnouncement({ ...base, sortingColumns, resolveColumnLabel: () => 'Resolved' })).toBe( + 'sorted by Resolved asc' + ); + }); + + test('falls back to the column-definition label when resolveColumnLabel returns undefined', () => { + const sortingColumns: Sort = [{ sortingColumn: colA, isDescending: false }]; + expect(buildSortLiveAnnouncement({ ...base, sortingColumns, resolveColumnLabel: () => undefined })).toBe( + 'sorted by A asc' + ); + }); + + test('falls back to sortingField when the header is not a string', () => { + const nodeHeaderColumn: TableProps.ColumnDefinition = { + id: 'a', + header: 5 as unknown as TableProps.ColumnDefinition['header'], + cell: i => i.a, + sortingField: 'a', + }; + const sortingColumns: Sort = [{ sortingColumn: nodeHeaderColumn, isDescending: true }]; + expect(buildSortLiveAnnouncement({ ...base, columnDefinitions: [nodeHeaderColumn], sortingColumns })).toBe( + 'sorted by a desc' + ); + }); + + test('falls back to the column id for comparator columns with a non-string header', () => { + const nodeHeaderComparator: TableProps.ColumnDefinition = { + id: 'c', + header: 5 as unknown as TableProps.ColumnDefinition['header'], + cell: i => i.a, + sortingComparator: comparator, + }; + const sortingColumns: Sort = [{ sortingColumn: nodeHeaderComparator, isDescending: false }]; + expect(buildSortLiveAnnouncement({ ...base, columnDefinitions: [nodeHeaderComparator], sortingColumns })).toBe( + 'sorted by c asc' + ); + }); +}); diff --git a/src/table/__tests__/warnings.test.tsx b/src/table/__tests__/warnings.test.tsx index dcf095a61d..d3ae425404 100644 --- a/src/table/__tests__/warnings.test.tsx +++ b/src/table/__tests__/warnings.test.tsx @@ -114,3 +114,33 @@ describe('Sticky header validation', () => { expect(warnOnce).toHaveBeenCalledTimes(2); }); }); + +describe('Multi-column sort validation', () => { + const baseColumns = [ + { header: 'id', cell: () => 'id', sortingField: 'id' }, + { header: 'name', cell: () => 'name', sortingField: 'name' }, + ]; + + test('prints a warning when multiColumnSort is combined with single-column sorting props', () => { + renderTable( +
{} }} + sortingColumn={{ sortingField: 'name' }} + onSortingChange={() => {}} + /> + ); + expect(warnOnce).toHaveBeenCalledWith( + 'Table', + expect.stringMatching(/`multiColumnSort` prop is mutually exclusive/) + ); + }); + + test('does not print a warning when only multiColumnSort is provided', () => { + renderTable( +
{} }} /> + ); + expect(warnOnce).not.toHaveBeenCalled(); + }); +}); diff --git a/src/table/clear-sort.tsx b/src/table/clear-sort.tsx new file mode 100644 index 0000000000..4d3d7e4bb7 --- /dev/null +++ b/src/table/clear-sort.tsx @@ -0,0 +1,30 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; + +import InternalButton from '../button/internal'; +import { useInternalI18n } from '../i18n/context'; +import { fireNonCancelableEvent } from '../internal/events'; +import { TableProps } from './interfaces'; + +interface ClearSortButtonProps { + multiColumnSort: TableProps.MultiColumnSort; + i18nStrings?: TableProps.I18nStrings; + onClearSort?: () => void; +} + +export function ClearSortButton({ multiColumnSort, i18nStrings, onClearSort }: ClearSortButtonProps) { + const i18n = useInternalI18n('table'); + return ( + { + fireNonCancelableEvent(multiColumnSort.onChange, { sortingColumns: [] }); + onClearSort?.(); + }} + > + {i18n('i18nStrings.clearSort', i18nStrings?.clearSort) ?? ''} + + ); +} diff --git a/src/table/header-cell/index.tsx b/src/table/header-cell/index.tsx index e08d537b88..de47af6fda 100644 --- a/src/table/header-cell/index.tsx +++ b/src/table/header-cell/index.tsx @@ -9,11 +9,21 @@ import { getAnalyticsMetadataAttribute } from '@cloudscape-design/component-tool import { useInternalI18n } from '../../i18n/context'; import InternalIcon from '../../icon/internal'; +import { fireNonCancelableEvent } from '../../internal/events'; import { KeyCode } from '../../internal/keycode'; import { GeneratedAnalyticsMetadataTableSort } from '../analytics-metadata/interfaces'; import { TableProps } from '../interfaces'; +import { + appendSort, + getSortIndex, + removeSort, + replaceSort, + setDirection, + toggleDirection, +} from '../multi-column-sort/utils'; import { Divider, Resizer } from '../resizer'; import { BaseHeaderCellProps } from './common-props'; +import { SortMenu, SortMenuAction } from './sort-menu'; import { TableThElement } from './th-element'; import { getSortingIconName, getSortingStatus, isSorted } from './utils'; @@ -27,6 +37,9 @@ export interface TableHeaderCellProps extends BaseHeaderCellProps { sortingDisabled?: boolean; stuck?: boolean; onClick(detail: TableProps.SortingState): void; + multiColumnSort?: TableProps.MultiColumnSort; + i18nStrings?: TableProps.I18nStrings; + sortMenuTriggerLabel?: string; updateColumn: (columnId: PropertyKey, newWidth: number) => void; isEditable?: boolean; columnId: PropertyKey; @@ -73,17 +86,77 @@ export function TableHeaderCell({ isLastChildOfGroup, isLast, tableVariant, + multiColumnSort, + i18nStrings, + sortMenuTriggerLabel, }: TableHeaderCellProps) { const i18n = useInternalI18n('table'); const sortable = !!column.sortingComparator || !!column.sortingField; - const sorted = !!activeSortingColumn && isSorted(column, activeSortingColumn); - const sortingStatus = getSortingStatus(sortable, sorted, !!sortingDescending, !!sortingDisabled); const isGrouped = !!columnGroupId || (rowSpan ?? 1) > 1; - const handleClick = () => + + // Multi-column sort context — derived from `multiColumnSort.sortingColumns` when present. + const multiSortIndex = multiColumnSort ? getSortIndex(multiColumnSort.sortingColumns, column) : null; + const multiSortEntry = + multiSortIndex !== null && multiColumnSort ? multiColumnSort.sortingColumns[multiSortIndex - 1] : undefined; + const inMultiSort = multiSortIndex !== null; + const isPrimaryMultiSort = multiSortIndex === 1; + // Priority badges are only meaningful when 2+ columns are sorted; with a single column, the position number adds no information. + const showPriorityBadge = !!multiColumnSort && multiColumnSort.sortingColumns.length >= 2 && multiSortIndex !== null; + + // For multi-sort, derive sorted/descending from the matching entry; otherwise fall back to single-sort props. + const sorted = multiColumnSort ? inMultiSort : !!activeSortingColumn && isSorted(column, activeSortingColumn); + const descending = multiColumnSort ? !!multiSortEntry?.isDescending : !!sortingDescending; + const sortingStatus = getSortingStatus(sortable, sorted, descending, !!sortingDisabled); + // In multi-sort, only the primary column declares aria-sort (ARIA only allows one column sorted at a time). + const suppressAriaSort = !!multiColumnSort && inMultiSort && !isPrimaryMultiSort; + const handleClick = (shiftKey = false) => { + if (multiColumnSort) { + const current = multiColumnSort.sortingColumns; + let next: ReadonlyArray>; + if (shiftKey && !inMultiSort) { + next = appendSort(current, column, false); + } else if (inMultiSort) { + next = toggleDirection(current, column); + } else { + next = replaceSort(column, false); + } + fireNonCancelableEvent(multiColumnSort.onChange, { sortingColumns: next }); + return; + } onClick({ sortingColumn: column, isDescending: sorted ? !sortingDescending : false, }); + }; + + const handleSortMenuAction = (action: SortMenuAction) => { + // The sort menu is only rendered when `multiColumnSort` is set (see render below), so this guard + // exists purely to narrow the type for the accesses that follow and is unreachable at runtime. + /* istanbul ignore next */ + if (!multiColumnSort) { + return; + } + const current = multiColumnSort.sortingColumns; + let next: ReadonlyArray>; + switch (action) { + case 'sort-ascending': + next = inMultiSort ? setDirection(current, column, false) : replaceSort(column, false); + break; + case 'sort-descending': + next = inMultiSort ? setDirection(current, column, true) : replaceSort(column, true); + break; + case 'add-to-sort-ascending': + next = appendSort(current, column, false); + break; + case 'add-to-sort-descending': + next = appendSort(current, column, true); + break; + case 'remove-from-sort': + next = removeSort(current, column); + break; + } + fireNonCancelableEvent(multiColumnSort.onChange, { sortingColumns: next }); + }; // Elements with role="button" do not have the default behavior of
{} }} + /> + + ); + const { container, rerender } = render(renderWithProvider([])); + rerender(renderWithProvider([nameAsc, idDesc])); + await waitFor(() => { + expect(container.textContent).toContain('Rendered name ascending'); + expect(container.textContent).toContain('Rendered id descending'); + }); +}); + +test('falls back to the sorting field when the sorted column has no id', async () => { + const noIdColumns: TableProps.ColumnDefinition[] = [ + { header: Rendered name, cell: item => item.name, sortingField: 'name' }, + ]; + const renderNoId = (sortingColumns: ReadonlyArray>) => ( +
{} }} + i18nStrings={i18nStrings} + /> + ); + const { container, rerender } = render(renderNoId([])); + rerender(renderNoId([nameAsc])); + await waitFor(() => expect(container.textContent).toContain('Table sorted by name ascending')); +}); From 8cdb16c07148b882a540b6518f55403c4fecf400 Mon Sep 17 00:00:00 2001 From: Gethin Webster Date: Wed, 15 Jul 2026 08:09:01 +0000 Subject: [PATCH 6/8] refactor: flatten sort menu keys and split live-announcement into direction + priority strings Renames i18nStrings.sortDropdown.* to i18nStrings.sortDropdownX and replaces the single i18nStrings.liveAnnouncementSortColumn ICU string with ariaLabels.sortAscending/ sortDescending + ariaLabels.sortPriority, composed by ariaLabels.liveAnnouncementSortOrder. Matches the restructured AWS-UI-Components-I18n source (merged in CR-289393273). Includes best-guess translations for 12 locales for the renamed/new keys; real Totoro translations to follow. --- pages/table/multi-column-sort.page.tsx | 16 -- .../__snapshots__/documenter.test.ts.snap | 179 +++++++++--------- src/button-dropdown/internal-interfaces.ts | 6 + src/button-dropdown/internal.tsx | 2 + src/i18n/messages-types.ts | 29 +-- src/i18n/messages/all.ar.json | 24 +-- src/i18n/messages/all.de.json | 24 +-- src/i18n/messages/all.en-GB.json | 19 +- src/i18n/messages/all.en.json | 24 +-- src/i18n/messages/all.es.json | 24 +-- src/i18n/messages/all.fr.json | 24 +-- src/i18n/messages/all.id.json | 24 +-- src/i18n/messages/all.it.json | 24 +-- src/i18n/messages/all.ja.json | 24 +-- src/i18n/messages/all.ko.json | 24 +-- src/i18n/messages/all.pt-BR.json | 24 +-- src/i18n/messages/all.tr.json | 24 +-- src/i18n/messages/all.zh-CN.json | 24 +-- src/i18n/messages/all.zh-TW.json | 24 +-- .../multi-column-sort-behavior.test.tsx | 25 +++ ...lti-column-sort-live-announcement.test.tsx | 10 +- src/table/header-cell/index.tsx | 30 ++- src/table/header-cell/sort-menu.tsx | 30 +-- src/table/interfaces.tsx | 59 +++--- src/table/internal.tsx | 4 +- .../multi-column-sort/live-announcement.tsx | 34 ++-- src/table/thead.tsx | 8 +- 27 files changed, 420 insertions(+), 343 deletions(-) diff --git a/pages/table/multi-column-sort.page.tsx b/pages/table/multi-column-sort.page.tsx index c7af7f0891..6d9f813b85 100644 --- a/pages/table/multi-column-sort.page.tsx +++ b/pages/table/multi-column-sort.page.tsx @@ -92,46 +92,30 @@ export default function MultiColumnSortPage() { const [currentPageIndex, setCurrentPageIndex] = useState(1); const headers = longNamesEnabled ? longHeaders : shortHeaders; - const getSortLabel = - (label: string) => - ({ sorted, descending, disabled, sortIndex }: TableProps.LabelData): string => { - if (disabled) { - return `${label}, sorting disabled`; - } - if (!sorted) { - return `${label}, not sorted`; - } - const direction = descending ? 'descending' : 'ascending'; - return sortIndex ? `${label}, sorted ${direction}, sort position ${sortIndex}` : `${label}, sorted ${direction}`; - }; const columnDefinitions: TableProps.ColumnDefinition[] = [ { id: 'name', header: headers.name, cell: item => item.name, sortingField: 'name', - ariaLabel: getSortLabel(headers.name), }, { id: 'type', header: headers.type, cell: item => item.type, sortingField: 'type', - ariaLabel: getSortLabel(headers.type), }, { id: 'state', header: headers.state, cell: item => item.state, sortingField: 'state', - ariaLabel: getSortLabel(headers.state), }, { id: 'cpu', header: headers.cpu, cell: item => `${item.cpu}%`, sortingField: 'cpu', - ariaLabel: getSortLabel(headers.cpu), }, ]; diff --git a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap index 8e56c2f2ec..56c6af37fd 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap @@ -27906,7 +27906,13 @@ in tables with data grouping. * \`submittingEditText\` (EditableColumnDefinition) => string - Specifies a text that is announced to screen readers when a cell edit operation is submitted. * \`expandButtonLabel\` (Item) => string - Specifies an alternative text for row expand button. -* \`collapseButtonLabel\` (Item) => string - Specifies an alternative text for row collapse button.", +* \`collapseButtonLabel\` (Item) => string - Specifies an alternative text for row collapse button. +* \`sortMenuTriggerLabel\` (string) - Provides an alternative text for the column header sort menu trigger button when multi-column sort is enabled. +* \`sortAscending\` (string) - Screen reader word for ascending sort direction, used in sort announcements and a sorted column header's accessible text. +* \`sortDescending\` (string) - Screen reader word for descending sort direction, used in sort announcements and a sorted column header's accessible text. +* \`liveAnnouncementSortOrder\` (({ columns }) => string) - Formats the screen reader announcement for the current multi-column sort order. +* \`liveAnnouncementSortCleared\` (string) - Screen reader announcement made when the multi-column sort is cleared. +* \`sortPriority\` (({ priority }) => string) - Formats a sorted column's position in a multi-column sort, used in the sorted column header's accessible label.", "i18nTag": true, "inlineType": { "name": "TableProps.AriaLabels", @@ -28035,6 +28041,27 @@ in tables with data grouping. "optional": true, "type": "((data: TableProps.SelectionState, row: T) => string)", }, + { + "name": "liveAnnouncementSortCleared", + "optional": true, + "type": "string", + }, + { + "inlineType": { + "name": "(data: { columns: string; }) => string", + "parameters": [ + { + "name": "data", + "type": "{ columns: string; }", + }, + ], + "returnType": "string", + "type": "function", + }, + "name": "liveAnnouncementSortOrder", + "optional": true, + "type": "((data: { columns: string; }) => string)", + }, { "name": "resizerRoleDescription", "optional": true, @@ -28050,11 +28077,37 @@ in tables with data grouping. "optional": true, "type": "string", }, + { + "name": "sortAscending", + "optional": true, + "type": "string", + }, + { + "name": "sortDescending", + "optional": true, + "type": "string", + }, { "name": "sortMenuTriggerLabel", "optional": true, "type": "string", }, + { + "inlineType": { + "name": "(data: { priority: number; }) => string", + "parameters": [ + { + "name": "data", + "type": "{ priority: number; }", + }, + ], + "returnType": "string", + "type": "function", + }, + "name": "sortPriority", + "optional": true, + "type": "((data: { priority: number; }) => string)", + }, { "inlineType": { "name": "(column: TableProps.ColumnDefinition) => string", @@ -28449,18 +28502,18 @@ Each group definition contains the following: "type": "ReadonlyArray>", }, { - "description": "An object containing all the localized strings required by the multi-column -sorting UI: + "description": "Object containing the localized visible strings used by the Table component. -* \`sortDropdown\` (object): Strings for the per-column sort dropdown menu. - * \`sortAscending\` (string): Label for the "Sort ascending" dropdown menu item. - * \`sortDescending\` (string): Label for the "Sort descending" dropdown menu item. - * \`multiColumnSortGroup\` (string): Label for the "Multi-column sort" dropdown menu group. - * \`addToSortAscending\` (string): Label for the "Add to sort (ascending)" dropdown menu item. - * \`addToSortDescending\` (string): Label for the "Add to sort (descending)" dropdown menu item. - * \`removeFromSort\` (string): Label for the "Remove from sort" dropdown menu item. - * \`addToSortDisabledReason\` (string): Reason shown when the "Add to sort" menu items are disabled. - * \`removeFromSortDisabledReason\` (string): Reason shown when the "Remove from sort" menu item is disabled. +When multi-column sorting is enabled it provides the sort UI labels: + +* \`sortDropdownSortAscending\` (string): Label for the "Sort ascending" dropdown menu item. +* \`sortDropdownSortDescending\` (string): Label for the "Sort descending" dropdown menu item. +* \`sortDropdownMultiColumnSortGroup\` (string): Label for the multi-column sort dropdown menu group. +* \`sortDropdownAddToSortAscending\` (string): Label for the "Add to sort (ascending)" dropdown menu item. +* \`sortDropdownAddToSortDescending\` (string): Label for the "Add to sort (descending)" dropdown menu item. +* \`sortDropdownRemoveFromSort\` (string): Label for the "Remove from sort" dropdown menu item. +* \`sortDropdownAddToSortDisabledReason\` (string): Reason shown when the "Add to sort" menu items are disabled. +* \`sortDropdownRemoveFromSortDisabledReason\` (string): Reason shown when the "Remove from sort" menu item is disabled. * \`clearSort\` (string): Label for the "Clear sort" button.", "i18nTag": true, "inlineType": { @@ -28472,92 +28525,44 @@ sorting UI: "type": "string", }, { - "name": "liveAnnouncementSortCleared", + "name": "sortDropdownAddToSortAscending", "optional": true, "type": "string", }, { - "inlineType": { - "name": "(data: { columnLabel: string; isDescending: boolean; }) => string", - "parameters": [ - { - "name": "data", - "type": "{ columnLabel: string; isDescending: boolean; }", - }, - ], - "returnType": "string", - "type": "function", - }, - "name": "liveAnnouncementSortColumn", + "name": "sortDropdownAddToSortDescending", "optional": true, - "type": "((data: { columnLabel: string; isDescending: boolean; }) => string)", + "type": "string", }, { - "inlineType": { - "name": "(data: { columns: string; }) => string", - "parameters": [ - { - "name": "data", - "type": "{ columns: string; }", - }, - ], - "returnType": "string", - "type": "function", - }, - "name": "liveAnnouncementSortOrder", + "name": "sortDropdownAddToSortDisabledReason", "optional": true, - "type": "((data: { columns: string; }) => string)", + "type": "string", }, { - "inlineType": { - "name": "object", - "properties": [ - { - "name": "addToSortAscending", - "optional": true, - "type": "string", - }, - { - "name": "addToSortDescending", - "optional": true, - "type": "string", - }, - { - "name": "addToSortDisabledReason", - "optional": true, - "type": "string", - }, - { - "name": "multiColumnSortGroup", - "optional": true, - "type": "string", - }, - { - "name": "removeFromSort", - "optional": true, - "type": "string", - }, - { - "name": "removeFromSortDisabledReason", - "optional": true, - "type": "string", - }, - { - "name": "sortAscending", - "optional": true, - "type": "string", - }, - { - "name": "sortDescending", - "optional": true, - "type": "string", - }, - ], - "type": "object", - }, - "name": "sortDropdown", + "name": "sortDropdownMultiColumnSortGroup", + "optional": true, + "type": "string", + }, + { + "name": "sortDropdownRemoveFromSort", + "optional": true, + "type": "string", + }, + { + "name": "sortDropdownRemoveFromSortDisabledReason", "optional": true, - "type": "{ sortAscending?: string | undefined; sortDescending?: string | undefined; multiColumnSortGroup?: string | undefined; addToSortAscending?: string | undefined; addToSortDescending?: string | undefined; removeFromSort?: string | undefined; addToSortDisabledReason?: string | undefined; removeFromSortDisabledReason?: st...", + "type": "string", + }, + { + "name": "sortDropdownSortAscending", + "optional": true, + "type": "string", + }, + { + "name": "sortDropdownSortDescending", + "optional": true, + "type": "string", }, ], "type": "object", diff --git a/src/button-dropdown/internal-interfaces.ts b/src/button-dropdown/internal-interfaces.ts index 840196deb0..65dd6ba282 100644 --- a/src/button-dropdown/internal-interfaces.ts +++ b/src/button-dropdown/internal-interfaces.ts @@ -148,4 +148,10 @@ export interface InternalButtonDropdownProps * while keeping an inline footprint (used by the table multi-column sort menu). */ compactTrigger?: boolean; + + /** + * Adds `aria-describedby` to the trigger button, associating it with descriptive text elsewhere + * on the page (for example a table header cell that disambiguates otherwise-identical triggers). + */ + ariaDescribedby?: string; } diff --git a/src/button-dropdown/internal.tsx b/src/button-dropdown/internal.tsx index 9d501ff529..227dc01ecf 100644 --- a/src/button-dropdown/internal.tsx +++ b/src/button-dropdown/internal.tsx @@ -78,6 +78,7 @@ const InternalButtonDropdown = React.forwardRef( noMatch, i18nStrings, compactTrigger, + ariaDescribedby, ...props }: InternalButtonDropdownProps, ref: React.Ref @@ -216,6 +217,7 @@ const InternalButtonDropdown = React.forwardRef( }, ariaLabel, ariaExpanded: canBeOpened && isOpen, + ariaDescribedby, formAction: 'none', ariaHaspopup: hasFiltering ? 'dialog' : true, nativeButtonAttributes: nativeTriggerAttributes, diff --git a/src/i18n/messages-types.ts b/src/i18n/messages-types.ts index af668ad823..4b02069f50 100644 --- a/src/i18n/messages-types.ts +++ b/src/i18n/messages-types.ts @@ -536,23 +536,24 @@ export interface I18nFormatArgTypes { 'ariaLabels.collapseButtonLabel': never; 'columnDefinitions.editConfig.errorIconAriaLabel': never; 'columnDefinitions.editConfig.editIconAriaLabel': never; - 'i18nStrings.sortDropdown.sortAscending': never; - 'i18nStrings.sortDropdown.sortDescending': never; - 'i18nStrings.sortDropdown.multiColumnSortGroup': never; - 'i18nStrings.sortDropdown.addToSortAscending': never; - 'i18nStrings.sortDropdown.addToSortDescending': never; - 'i18nStrings.sortDropdown.removeFromSort': never; - 'i18nStrings.sortDropdown.addToSortDisabledReason': never; - 'i18nStrings.sortDropdown.removeFromSortDisabledReason': never; + 'i18nStrings.sortDropdownSortAscending': never; + 'i18nStrings.sortDropdownSortDescending': never; + 'i18nStrings.sortDropdownMultiColumnSortGroup': never; + 'i18nStrings.sortDropdownAddToSortAscending': never; + 'i18nStrings.sortDropdownAddToSortDescending': never; + 'i18nStrings.sortDropdownRemoveFromSort': never; + 'i18nStrings.sortDropdownAddToSortDisabledReason': never; + 'i18nStrings.sortDropdownRemoveFromSortDisabledReason': never; 'i18nStrings.clearSort': never; - 'i18nStrings.liveAnnouncementSortColumn': { - columnLabel: string | number; - isDescending: string; - }; - 'i18nStrings.liveAnnouncementSortOrder': { + 'ariaLabels.sortAscending': never; + 'ariaLabels.sortDescending': never; + 'ariaLabels.liveAnnouncementSortOrder': { columns: string | number; }; - 'i18nStrings.liveAnnouncementSortCleared': never; + 'ariaLabels.liveAnnouncementSortCleared': never; + 'ariaLabels.sortPriority': { + priority: string | number; + }; 'ariaLabels.sortMenuTriggerLabel': never; }; tabs: { diff --git a/src/i18n/messages/all.ar.json b/src/i18n/messages/all.ar.json index e3199d0e56..a227f53a87 100644 --- a/src/i18n/messages/all.ar.json +++ b/src/i18n/messages/all.ar.json @@ -409,19 +409,21 @@ "ariaLabels.collapseButtonLabel": "تحجيم", "columnDefinitions.editConfig.errorIconAriaLabel": "خطأ", "columnDefinitions.editConfig.editIconAriaLabel": "قابل للتعديل", - "i18nStrings.sortDropdown.sortAscending": "الترتيب تصاعديًّا", - "i18nStrings.sortDropdown.sortDescending": "الترتيب تنازليًّا", - "i18nStrings.sortDropdown.multiColumnSortGroup": "ترتيب متعدد الأعمدة (Shift + Click)", - "i18nStrings.sortDropdown.addToSortAscending": "إضافة إلى الترتيب (تصاعديًا)", - "i18nStrings.sortDropdown.addToSortDescending": "إضافة إلى الترتيب (تنازليًّا)", - "i18nStrings.sortDropdown.removeFromSort": "إزالة من الترتيب", + "i18nStrings.sortDropdownSortAscending": "الترتيب تصاعديًّا", + "i18nStrings.sortDropdownSortDescending": "الترتيب تنازليًّا", + "i18nStrings.sortDropdownMultiColumnSortGroup": "ترتيب متعدد الأعمدة (Shift + Click)", + "i18nStrings.sortDropdownAddToSortAscending": "إضافة إلى الترتيب (تصاعديًا)", + "i18nStrings.sortDropdownAddToSortDescending": "إضافة إلى الترتيب (تنازليًّا)", + "i18nStrings.sortDropdownRemoveFromSort": "إزالة من الترتيب", "i18nStrings.clearSort": "مسح الترتيب", "ariaLabels.sortMenuTriggerLabel": "خيارات الترتيب", - "i18nStrings.sortDropdown.addToSortDisabledReason": "تم بالفعل ترتيب هذا العمود", - "i18nStrings.sortDropdown.removeFromSortDisabledReason": "لم يتم ترتيب هذا العمود", - "i18nStrings.liveAnnouncementSortColumn": "{isDescending, select, true {{columnLabel} تنازلي} other {{columnLabel} تصاعدي}}", - "i18nStrings.liveAnnouncementSortOrder": "تم فرز الجدول حسب {columns}", - "i18nStrings.liveAnnouncementSortCleared": "تم إلغاء الفرز" + "i18nStrings.sortDropdownAddToSortDisabledReason": "تم بالفعل ترتيب هذا العمود", + "i18nStrings.sortDropdownRemoveFromSortDisabledReason": "لم يتم ترتيب هذا العمود", + "ariaLabels.sortAscending": "تصاعدي", + "ariaLabels.sortDescending": "تنازلي", + "ariaLabels.liveAnnouncementSortOrder": "تم فرز الجدول حسب {columns}", + "ariaLabels.liveAnnouncementSortCleared": "تم إلغاء الفرز", + "ariaLabels.sortPriority": "أولوية الفرز {priority}" }, "tabs": { "i18nStrings.scrollLeftAriaLabel": "التمرير إلى اليسار", diff --git a/src/i18n/messages/all.de.json b/src/i18n/messages/all.de.json index 970ddc82c5..448a6db68e 100644 --- a/src/i18n/messages/all.de.json +++ b/src/i18n/messages/all.de.json @@ -409,19 +409,21 @@ "ariaLabels.collapseButtonLabel": "Minimieren", "columnDefinitions.editConfig.errorIconAriaLabel": "Fehler", "columnDefinitions.editConfig.editIconAriaLabel": "bearbeitbar", - "i18nStrings.sortDropdown.sortAscending": "Aufsteigend sortieren", - "i18nStrings.sortDropdown.sortDescending": "Absteigend sortieren", - "i18nStrings.sortDropdown.multiColumnSortGroup": "Sortieren nach mehreren Spalten (Umschalttaste + Klick)", - "i18nStrings.sortDropdown.addToSortAscending": "Zur Sortierung hinzufügen (aufsteigend)", - "i18nStrings.sortDropdown.addToSortDescending": "Zur Sortierung hinzufügen (absteigend)", - "i18nStrings.sortDropdown.removeFromSort": "Aus Sortierung entfernen", + "i18nStrings.sortDropdownSortAscending": "Aufsteigend sortieren", + "i18nStrings.sortDropdownSortDescending": "Absteigend sortieren", + "i18nStrings.sortDropdownMultiColumnSortGroup": "Sortieren nach mehreren Spalten (Umschalttaste + Klick)", + "i18nStrings.sortDropdownAddToSortAscending": "Zur Sortierung hinzufügen (aufsteigend)", + "i18nStrings.sortDropdownAddToSortDescending": "Zur Sortierung hinzufügen (absteigend)", + "i18nStrings.sortDropdownRemoveFromSort": "Aus Sortierung entfernen", "i18nStrings.clearSort": "Sortierung löschen", "ariaLabels.sortMenuTriggerLabel": "Sortieroptionen", - "i18nStrings.sortDropdown.addToSortDisabledReason": "Diese Spalte ist bereits sortiert", - "i18nStrings.sortDropdown.removeFromSortDisabledReason": "Diese Spalte ist nicht sortiert", - "i18nStrings.liveAnnouncementSortColumn": "{isDescending, select, true {{columnLabel} absteigend} other {{columnLabel} aufsteigend}}", - "i18nStrings.liveAnnouncementSortOrder": "Tabelle sortiert nach {columns}", - "i18nStrings.liveAnnouncementSortCleared": "Sortierung gelöscht" + "i18nStrings.sortDropdownAddToSortDisabledReason": "Diese Spalte ist bereits sortiert", + "i18nStrings.sortDropdownRemoveFromSortDisabledReason": "Diese Spalte ist nicht sortiert", + "ariaLabels.sortAscending": "aufsteigend", + "ariaLabels.sortDescending": "absteigend", + "ariaLabels.liveAnnouncementSortOrder": "Tabelle sortiert nach {columns}", + "ariaLabels.liveAnnouncementSortCleared": "Sortierung gelöscht", + "ariaLabels.sortPriority": "Sortierpriorität {priority}" }, "tabs": { "i18nStrings.scrollLeftAriaLabel": "Nach links scrollen", diff --git a/src/i18n/messages/all.en-GB.json b/src/i18n/messages/all.en-GB.json index a42ff8cf17..2fb48aa74d 100644 --- a/src/i18n/messages/all.en-GB.json +++ b/src/i18n/messages/all.en-GB.json @@ -409,14 +409,19 @@ "ariaLabels.collapseButtonLabel": "Collapse", "columnDefinitions.editConfig.errorIconAriaLabel": "Error", "columnDefinitions.editConfig.editIconAriaLabel": "editable", - "i18nStrings.sortDropdown.sortAscending": "Sort ascending", - "i18nStrings.sortDropdown.sortDescending": "Sort descending", - "i18nStrings.sortDropdown.multiColumnSortGroup": "Multi-column sort (Shift + Click)", - "i18nStrings.sortDropdown.addToSortAscending": "Add to sort (ascending)", - "i18nStrings.sortDropdown.addToSortDescending": "Add to sort (descending)", - "i18nStrings.sortDropdown.removeFromSort": "Remove from sort", + "i18nStrings.sortDropdownSortAscending": "Sort ascending", + "i18nStrings.sortDropdownSortDescending": "Sort descending", + "i18nStrings.sortDropdownMultiColumnSortGroup": "Multi-column sort (Shift + Click)", + "i18nStrings.sortDropdownAddToSortAscending": "Add to sort (ascending)", + "i18nStrings.sortDropdownAddToSortDescending": "Add to sort (descending)", + "i18nStrings.sortDropdownRemoveFromSort": "Remove from sort", "i18nStrings.clearSort": "Clear sort", - "ariaLabels.sortMenuTriggerLabel": "Sort options" + "ariaLabels.sortMenuTriggerLabel": "Sort options", + "ariaLabels.sortPriority": "sort priority {priority}", + "ariaLabels.sortAscending": "ascending", + "ariaLabels.sortDescending": "descending", + "ariaLabels.liveAnnouncementSortOrder": "Table sorted by {columns}", + "ariaLabels.liveAnnouncementSortCleared": "Sorting cleared" }, "tabs": { "i18nStrings.scrollLeftAriaLabel": "Scroll left", diff --git a/src/i18n/messages/all.en.json b/src/i18n/messages/all.en.json index d92b2435df..b784c4609b 100644 --- a/src/i18n/messages/all.en.json +++ b/src/i18n/messages/all.en.json @@ -410,18 +410,20 @@ "ariaLabels.collapseButtonLabel": "Collapse", "columnDefinitions.editConfig.errorIconAriaLabel": "Error", "columnDefinitions.editConfig.editIconAriaLabel": "editable", - "i18nStrings.sortDropdown.sortAscending": "Sort ascending", - "i18nStrings.sortDropdown.sortDescending": "Sort descending", - "i18nStrings.sortDropdown.multiColumnSortGroup": "Multi-column sort (Shift + Click)", - "i18nStrings.sortDropdown.addToSortAscending": "Add to sort (ascending)", - "i18nStrings.sortDropdown.addToSortDescending": "Add to sort (descending)", - "i18nStrings.sortDropdown.removeFromSort": "Remove from sort", - "i18nStrings.sortDropdown.addToSortDisabledReason": "This column is already sorted", - "i18nStrings.sortDropdown.removeFromSortDisabledReason": "This column is not sorted", + "i18nStrings.sortDropdownSortAscending": "Sort ascending", + "i18nStrings.sortDropdownSortDescending": "Sort descending", + "i18nStrings.sortDropdownMultiColumnSortGroup": "Multi-column sort (Shift + Click)", + "i18nStrings.sortDropdownAddToSortAscending": "Add to sort (ascending)", + "i18nStrings.sortDropdownAddToSortDescending": "Add to sort (descending)", + "i18nStrings.sortDropdownRemoveFromSort": "Remove from sort", + "i18nStrings.sortDropdownAddToSortDisabledReason": "This column is already sorted", + "i18nStrings.sortDropdownRemoveFromSortDisabledReason": "This column is not sorted", "i18nStrings.clearSort": "Clear sort", - "i18nStrings.liveAnnouncementSortColumn": "{isDescending, select, true {{columnLabel} descending} other {{columnLabel} ascending}}", - "i18nStrings.liveAnnouncementSortOrder": "Table sorted by {columns}", - "i18nStrings.liveAnnouncementSortCleared": "Sorting cleared", + "ariaLabels.sortAscending": "ascending", + "ariaLabels.sortDescending": "descending", + "ariaLabels.liveAnnouncementSortOrder": "Table sorted by {columns}", + "ariaLabels.liveAnnouncementSortCleared": "Sorting cleared", + "ariaLabels.sortPriority": "sort priority {priority}", "ariaLabels.sortMenuTriggerLabel": "Sort options" }, "tabs": { diff --git a/src/i18n/messages/all.es.json b/src/i18n/messages/all.es.json index 6fd22941b0..0c7c5eae71 100644 --- a/src/i18n/messages/all.es.json +++ b/src/i18n/messages/all.es.json @@ -409,19 +409,21 @@ "ariaLabels.collapseButtonLabel": "Contraer", "columnDefinitions.editConfig.errorIconAriaLabel": "Error", "columnDefinitions.editConfig.editIconAriaLabel": "editable", - "i18nStrings.sortDropdown.sortAscending": "Ordenar de forma ascendente", - "i18nStrings.sortDropdown.sortDescending": "Ordenar de forma descendente", - "i18nStrings.sortDropdown.multiColumnSortGroup": "Ordenar por varias columnas (Mayús y clic)", - "i18nStrings.sortDropdown.addToSortAscending": "Agregar a la ordenación (ascendente)", - "i18nStrings.sortDropdown.addToSortDescending": "Agregar a la ordenación (descendente)", - "i18nStrings.sortDropdown.removeFromSort": "Eliminar de la ordenación", + "i18nStrings.sortDropdownSortAscending": "Ordenar de forma ascendente", + "i18nStrings.sortDropdownSortDescending": "Ordenar de forma descendente", + "i18nStrings.sortDropdownMultiColumnSortGroup": "Ordenar por varias columnas (Mayús y clic)", + "i18nStrings.sortDropdownAddToSortAscending": "Agregar a la ordenación (ascendente)", + "i18nStrings.sortDropdownAddToSortDescending": "Agregar a la ordenación (descendente)", + "i18nStrings.sortDropdownRemoveFromSort": "Eliminar de la ordenación", "i18nStrings.clearSort": "Borrar ordenación", "ariaLabels.sortMenuTriggerLabel": "Opciones de ordenación", - "i18nStrings.sortDropdown.addToSortDisabledReason": "Esta columna ya está ordenada", - "i18nStrings.sortDropdown.removeFromSortDisabledReason": "Esta columna no está ordenada", - "i18nStrings.liveAnnouncementSortColumn": "{isDescending, select, true {{columnLabel} en orden descendente} other {{columnLabel} en orden ascendente}}", - "i18nStrings.liveAnnouncementSortOrder": "Tabla ordenada por {columns}", - "i18nStrings.liveAnnouncementSortCleared": "Clasificación eliminada" + "i18nStrings.sortDropdownAddToSortDisabledReason": "Esta columna ya está ordenada", + "i18nStrings.sortDropdownRemoveFromSortDisabledReason": "Esta columna no está ordenada", + "ariaLabels.sortAscending": "en orden ascendente", + "ariaLabels.sortDescending": "en orden descendente", + "ariaLabels.liveAnnouncementSortOrder": "Tabla ordenada por {columns}", + "ariaLabels.liveAnnouncementSortCleared": "Clasificación eliminada", + "ariaLabels.sortPriority": "prioridad de ordenación {priority}" }, "tabs": { "i18nStrings.scrollLeftAriaLabel": "Desplácese hacia la izquierda", diff --git a/src/i18n/messages/all.fr.json b/src/i18n/messages/all.fr.json index 35dff89b01..9e364731a2 100644 --- a/src/i18n/messages/all.fr.json +++ b/src/i18n/messages/all.fr.json @@ -409,19 +409,21 @@ "ariaLabels.collapseButtonLabel": "Réduire", "columnDefinitions.editConfig.errorIconAriaLabel": "Erreur", "columnDefinitions.editConfig.editIconAriaLabel": "modifiable", - "i18nStrings.sortDropdown.sortAscending": "Trier par ordre croissant", - "i18nStrings.sortDropdown.sortDescending": "Trier par ordre décroissant", - "i18nStrings.sortDropdown.multiColumnSortGroup": "Tri sur plusieurs colonnes (Maj + clic)", - "i18nStrings.sortDropdown.addToSortAscending": "Ajouter au tri (par ordre croissant)", - "i18nStrings.sortDropdown.addToSortDescending": "Ajouter au tri (par ordre décroissant)", - "i18nStrings.sortDropdown.removeFromSort": "Supprimer du tri", + "i18nStrings.sortDropdownSortAscending": "Trier par ordre croissant", + "i18nStrings.sortDropdownSortDescending": "Trier par ordre décroissant", + "i18nStrings.sortDropdownMultiColumnSortGroup": "Tri sur plusieurs colonnes (Maj + clic)", + "i18nStrings.sortDropdownAddToSortAscending": "Ajouter au tri (par ordre croissant)", + "i18nStrings.sortDropdownAddToSortDescending": "Ajouter au tri (par ordre décroissant)", + "i18nStrings.sortDropdownRemoveFromSort": "Supprimer du tri", "i18nStrings.clearSort": "Effacer le tri", "ariaLabels.sortMenuTriggerLabel": "Options de tri", - "i18nStrings.sortDropdown.addToSortDisabledReason": "Cette colonne est déjà triée", - "i18nStrings.sortDropdown.removeFromSortDisabledReason": "Cette colonne n’est pas triée", - "i18nStrings.liveAnnouncementSortColumn": "{isDescending, select, true {{columnLabel} par ordre décroissant} other {{columnLabel} par ordre croissant}}", - "i18nStrings.liveAnnouncementSortOrder": "Tableau trié selon {columns}", - "i18nStrings.liveAnnouncementSortCleared": "Tri supprimé" + "i18nStrings.sortDropdownAddToSortDisabledReason": "Cette colonne est déjà triée", + "i18nStrings.sortDropdownRemoveFromSortDisabledReason": "Cette colonne n’est pas triée", + "ariaLabels.sortAscending": "par ordre croissant", + "ariaLabels.sortDescending": "par ordre décroissant", + "ariaLabels.liveAnnouncementSortOrder": "Tableau trié selon {columns}", + "ariaLabels.liveAnnouncementSortCleared": "Tri supprimé", + "ariaLabels.sortPriority": "priorité de tri {priority}" }, "tabs": { "i18nStrings.scrollLeftAriaLabel": "Faire défiler vers la gauche", diff --git a/src/i18n/messages/all.id.json b/src/i18n/messages/all.id.json index c66f185e69..8874626311 100644 --- a/src/i18n/messages/all.id.json +++ b/src/i18n/messages/all.id.json @@ -409,19 +409,21 @@ "ariaLabels.collapseButtonLabel": "Ciutkan", "columnDefinitions.editConfig.errorIconAriaLabel": "Kesalahan", "columnDefinitions.editConfig.editIconAriaLabel": "dapat diedit", - "i18nStrings.sortDropdown.sortAscending": "Urutkan naik", - "i18nStrings.sortDropdown.sortDescending": "Urutkan turun", - "i18nStrings.sortDropdown.multiColumnSortGroup": "Urutan multikolom (Shift + Klik)", - "i18nStrings.sortDropdown.addToSortAscending": "Tambahkan untuk mengurutkan (naik)", - "i18nStrings.sortDropdown.addToSortDescending": "Tambahkan untuk mengurutkan (turun)", - "i18nStrings.sortDropdown.removeFromSort": "Hapus dari urutan", + "i18nStrings.sortDropdownSortAscending": "Urutkan naik", + "i18nStrings.sortDropdownSortDescending": "Urutkan turun", + "i18nStrings.sortDropdownMultiColumnSortGroup": "Urutan multikolom (Shift + Klik)", + "i18nStrings.sortDropdownAddToSortAscending": "Tambahkan untuk mengurutkan (naik)", + "i18nStrings.sortDropdownAddToSortDescending": "Tambahkan untuk mengurutkan (turun)", + "i18nStrings.sortDropdownRemoveFromSort": "Hapus dari urutan", "i18nStrings.clearSort": "Hapus urutan", "ariaLabels.sortMenuTriggerLabel": "Opsi urutan", - "i18nStrings.sortDropdown.addToSortDisabledReason": "Kolom ini sudah diurutkan", - "i18nStrings.sortDropdown.removeFromSortDisabledReason": "Kolom ini tidak diurutkan", - "i18nStrings.liveAnnouncementSortColumn": "{isDescending, select, true {{columnLabel} turun} other {{columnLabel} naik}}", - "i18nStrings.liveAnnouncementSortOrder": "Tabel diurutkan berdasarkan {columns}", - "i18nStrings.liveAnnouncementSortCleared": "Pengurutan dibersihkan" + "i18nStrings.sortDropdownAddToSortDisabledReason": "Kolom ini sudah diurutkan", + "i18nStrings.sortDropdownRemoveFromSortDisabledReason": "Kolom ini tidak diurutkan", + "ariaLabels.sortAscending": "naik", + "ariaLabels.sortDescending": "turun", + "ariaLabels.liveAnnouncementSortOrder": "Tabel diurutkan berdasarkan {columns}", + "ariaLabels.liveAnnouncementSortCleared": "Pengurutan dibersihkan", + "ariaLabels.sortPriority": "prioritas pengurutan {priority}" }, "tabs": { "i18nStrings.scrollLeftAriaLabel": "Gulir ke kiri", diff --git a/src/i18n/messages/all.it.json b/src/i18n/messages/all.it.json index 9ef04c5fa3..1a0ee4c20e 100644 --- a/src/i18n/messages/all.it.json +++ b/src/i18n/messages/all.it.json @@ -409,19 +409,21 @@ "ariaLabels.collapseButtonLabel": "Comprimi", "columnDefinitions.editConfig.errorIconAriaLabel": "Errore", "columnDefinitions.editConfig.editIconAriaLabel": "modificabile", - "i18nStrings.sortDropdown.sortAscending": "Ordina in ordine crescente", - "i18nStrings.sortDropdown.sortDescending": "Ordina in ordine decrescente", - "i18nStrings.sortDropdown.multiColumnSortGroup": "Ordinamento a più colonne (MAIUSC + clic)", - "i18nStrings.sortDropdown.addToSortAscending": "Aggiungi all’ordinamento (crescente)", - "i18nStrings.sortDropdown.addToSortDescending": "Aggiungi all’ordinamento (decrescente)", - "i18nStrings.sortDropdown.removeFromSort": "Rimuovi dall’ordinamento", + "i18nStrings.sortDropdownSortAscending": "Ordina in ordine crescente", + "i18nStrings.sortDropdownSortDescending": "Ordina in ordine decrescente", + "i18nStrings.sortDropdownMultiColumnSortGroup": "Ordinamento a più colonne (MAIUSC + clic)", + "i18nStrings.sortDropdownAddToSortAscending": "Aggiungi all’ordinamento (crescente)", + "i18nStrings.sortDropdownAddToSortDescending": "Aggiungi all’ordinamento (decrescente)", + "i18nStrings.sortDropdownRemoveFromSort": "Rimuovi dall’ordinamento", "i18nStrings.clearSort": "Cancella l’ordinamento", "ariaLabels.sortMenuTriggerLabel": "Opzioni di ordinamento", - "i18nStrings.sortDropdown.addToSortDisabledReason": "Questa colonna è già ordinata", - "i18nStrings.sortDropdown.removeFromSortDisabledReason": "Questa colonna non è ordinata", - "i18nStrings.liveAnnouncementSortColumn": "{isDescending, select, true {{columnLabel} decrescente} other {{columnLabel} crescente}}", - "i18nStrings.liveAnnouncementSortOrder": "Tabella ordinata per {columns}", - "i18nStrings.liveAnnouncementSortCleared": "Ordinamento cancellato" + "i18nStrings.sortDropdownAddToSortDisabledReason": "Questa colonna è già ordinata", + "i18nStrings.sortDropdownRemoveFromSortDisabledReason": "Questa colonna non è ordinata", + "ariaLabels.sortAscending": "crescente", + "ariaLabels.sortDescending": "decrescente", + "ariaLabels.liveAnnouncementSortOrder": "Tabella ordinata per {columns}", + "ariaLabels.liveAnnouncementSortCleared": "Ordinamento cancellato", + "ariaLabels.sortPriority": "priorità di ordinamento {priority}" }, "tabs": { "i18nStrings.scrollLeftAriaLabel": "Scorri a sinistra", diff --git a/src/i18n/messages/all.ja.json b/src/i18n/messages/all.ja.json index 392517cea8..95bb338c8b 100644 --- a/src/i18n/messages/all.ja.json +++ b/src/i18n/messages/all.ja.json @@ -409,19 +409,21 @@ "ariaLabels.collapseButtonLabel": "折りたたむ", "columnDefinitions.editConfig.errorIconAriaLabel": "エラー", "columnDefinitions.editConfig.editIconAriaLabel": "編集可能", - "i18nStrings.sortDropdown.sortAscending": "昇順にソート", - "i18nStrings.sortDropdown.sortDescending": "降順にソート", - "i18nStrings.sortDropdown.multiColumnSortGroup": "複数列ソート (Shift + Click)", - "i18nStrings.sortDropdown.addToSortAscending": "ソートに追加 (昇順)", - "i18nStrings.sortDropdown.addToSortDescending": "ソートに追加 (降順)", - "i18nStrings.sortDropdown.removeFromSort": "ソートから削除", + "i18nStrings.sortDropdownSortAscending": "昇順にソート", + "i18nStrings.sortDropdownSortDescending": "降順にソート", + "i18nStrings.sortDropdownMultiColumnSortGroup": "複数列ソート (Shift + Click)", + "i18nStrings.sortDropdownAddToSortAscending": "ソートに追加 (昇順)", + "i18nStrings.sortDropdownAddToSortDescending": "ソートに追加 (降順)", + "i18nStrings.sortDropdownRemoveFromSort": "ソートから削除", "i18nStrings.clearSort": "ソートをクリア", "ariaLabels.sortMenuTriggerLabel": "ソートオプション", - "i18nStrings.sortDropdown.addToSortDisabledReason": "この列は既にソートされています", - "i18nStrings.sortDropdown.removeFromSortDisabledReason": "この列はソートされていません", - "i18nStrings.liveAnnouncementSortColumn": "{isDescending, select, true {{columnLabel} 降順} other {{columnLabel} 昇順}}", - "i18nStrings.liveAnnouncementSortOrder": "表は {columns}でソートされています", - "i18nStrings.liveAnnouncementSortCleared": "ソートがクリアされました" + "i18nStrings.sortDropdownAddToSortDisabledReason": "この列は既にソートされています", + "i18nStrings.sortDropdownRemoveFromSortDisabledReason": "この列はソートされていません", + "ariaLabels.sortAscending": "昇順", + "ariaLabels.sortDescending": "降順", + "ariaLabels.liveAnnouncementSortOrder": "表は {columns}でソートされています", + "ariaLabels.liveAnnouncementSortCleared": "ソートがクリアされました", + "ariaLabels.sortPriority": "並べ替えの優先順位 {priority}" }, "tabs": { "i18nStrings.scrollLeftAriaLabel": "左にスクロール", diff --git a/src/i18n/messages/all.ko.json b/src/i18n/messages/all.ko.json index ddb7405a44..e931d34e70 100644 --- a/src/i18n/messages/all.ko.json +++ b/src/i18n/messages/all.ko.json @@ -409,19 +409,21 @@ "ariaLabels.collapseButtonLabel": "축소", "columnDefinitions.editConfig.errorIconAriaLabel": "오류", "columnDefinitions.editConfig.editIconAriaLabel": "편집 가능", - "i18nStrings.sortDropdown.sortAscending": "오름차순 정렬", - "i18nStrings.sortDropdown.sortDescending": "내림차순 정렬", - "i18nStrings.sortDropdown.multiColumnSortGroup": "복수 열 정렬(Shift 키를 누른 채 클릭)", - "i18nStrings.sortDropdown.addToSortAscending": "정렬에 추가(오름차순)", - "i18nStrings.sortDropdown.addToSortDescending": "정렬에 추가 (내림차순)", - "i18nStrings.sortDropdown.removeFromSort": "정렬에서 제거", + "i18nStrings.sortDropdownSortAscending": "오름차순 정렬", + "i18nStrings.sortDropdownSortDescending": "내림차순 정렬", + "i18nStrings.sortDropdownMultiColumnSortGroup": "복수 열 정렬(Shift 키를 누른 채 클릭)", + "i18nStrings.sortDropdownAddToSortAscending": "정렬에 추가(오름차순)", + "i18nStrings.sortDropdownAddToSortDescending": "정렬에 추가 (내림차순)", + "i18nStrings.sortDropdownRemoveFromSort": "정렬에서 제거", "i18nStrings.clearSort": "정렬 지우기", "ariaLabels.sortMenuTriggerLabel": "정렬 옵션", - "i18nStrings.sortDropdown.addToSortDisabledReason": "이 열은 이미 정렬되었습니다.", - "i18nStrings.sortDropdown.removeFromSortDisabledReason": "이 열은 정렬되지 않았습니다.", - "i18nStrings.liveAnnouncementSortColumn": "{isDescending, select, true {{columnLabel} 내림차순} other {{columnLabel} 오름차순}}", - "i18nStrings.liveAnnouncementSortOrder": "{columns} 기준 테이블 정렬", - "i18nStrings.liveAnnouncementSortCleared": "정렬이 지워졌습니다." + "i18nStrings.sortDropdownAddToSortDisabledReason": "이 열은 이미 정렬되었습니다.", + "i18nStrings.sortDropdownRemoveFromSortDisabledReason": "이 열은 정렬되지 않았습니다.", + "ariaLabels.sortAscending": "오름차순", + "ariaLabels.sortDescending": "내림차순", + "ariaLabels.liveAnnouncementSortOrder": "{columns} 기준 테이블 정렬", + "ariaLabels.liveAnnouncementSortCleared": "정렬이 지워졌습니다.", + "ariaLabels.sortPriority": "정렬 우선순위 {priority}" }, "tabs": { "i18nStrings.scrollLeftAriaLabel": "왼쪽으로 스크롤", diff --git a/src/i18n/messages/all.pt-BR.json b/src/i18n/messages/all.pt-BR.json index 6fbf498f49..e26a34ed85 100644 --- a/src/i18n/messages/all.pt-BR.json +++ b/src/i18n/messages/all.pt-BR.json @@ -409,19 +409,21 @@ "ariaLabels.collapseButtonLabel": "Recolher", "columnDefinitions.editConfig.errorIconAriaLabel": "Erro", "columnDefinitions.editConfig.editIconAriaLabel": "editável", - "i18nStrings.sortDropdown.sortAscending": "Classificar em ordem crescente", - "i18nStrings.sortDropdown.sortDescending": "Classificar em ordem decrescente", - "i18nStrings.sortDropdown.multiColumnSortGroup": "Classificação em várias colunas (Shift + Click)", - "i18nStrings.sortDropdown.addToSortAscending": "Adicionar à classificação (ascendente)", - "i18nStrings.sortDropdown.addToSortDescending": "Adicionar à classificação (decrescente)", - "i18nStrings.sortDropdown.removeFromSort": "Remover da classificação", + "i18nStrings.sortDropdownSortAscending": "Classificar em ordem crescente", + "i18nStrings.sortDropdownSortDescending": "Classificar em ordem decrescente", + "i18nStrings.sortDropdownMultiColumnSortGroup": "Classificação em várias colunas (Shift + Click)", + "i18nStrings.sortDropdownAddToSortAscending": "Adicionar à classificação (ascendente)", + "i18nStrings.sortDropdownAddToSortDescending": "Adicionar à classificação (decrescente)", + "i18nStrings.sortDropdownRemoveFromSort": "Remover da classificação", "i18nStrings.clearSort": "Limpar classificação", "ariaLabels.sortMenuTriggerLabel": "Opções de classificação", - "i18nStrings.sortDropdown.addToSortDisabledReason": "Esta coluna já está classificada", - "i18nStrings.sortDropdown.removeFromSortDisabledReason": "Esta coluna não está classificada", - "i18nStrings.liveAnnouncementSortColumn": "{isDescending, select, true {{columnLabel} ordem decrescente} other {{columnLabel} ordem crescente}}", - "i18nStrings.liveAnnouncementSortOrder": "Tabela ordenada por {columns}", - "i18nStrings.liveAnnouncementSortCleared": "Classificação cancelada" + "i18nStrings.sortDropdownAddToSortDisabledReason": "Esta coluna já está classificada", + "i18nStrings.sortDropdownRemoveFromSortDisabledReason": "Esta coluna não está classificada", + "ariaLabels.sortAscending": "ordem crescente", + "ariaLabels.sortDescending": "ordem decrescente", + "ariaLabels.liveAnnouncementSortOrder": "Tabela ordenada por {columns}", + "ariaLabels.liveAnnouncementSortCleared": "Classificação cancelada", + "ariaLabels.sortPriority": "prioridade de classificação {priority}" }, "tabs": { "i18nStrings.scrollLeftAriaLabel": "Rolar para a esquerda", diff --git a/src/i18n/messages/all.tr.json b/src/i18n/messages/all.tr.json index 8f6c390b4a..6607f1890a 100644 --- a/src/i18n/messages/all.tr.json +++ b/src/i18n/messages/all.tr.json @@ -409,19 +409,21 @@ "ariaLabels.collapseButtonLabel": "Daralt", "columnDefinitions.editConfig.errorIconAriaLabel": "Hata", "columnDefinitions.editConfig.editIconAriaLabel": "düzenlenebilir", - "i18nStrings.sortDropdown.sortAscending": "Artan sıralama", - "i18nStrings.sortDropdown.sortDescending": "Azalan sıralama", - "i18nStrings.sortDropdown.multiColumnSortGroup": "Çok sütunlu sıralama (Shift + Tıklama)", - "i18nStrings.sortDropdown.addToSortAscending": "Sıralamaya ekle (artan)", - "i18nStrings.sortDropdown.addToSortDescending": "Sıralamaya ekle (azalan)", - "i18nStrings.sortDropdown.removeFromSort": "Sıralamadan kaldır", + "i18nStrings.sortDropdownSortAscending": "Artan sıralama", + "i18nStrings.sortDropdownSortDescending": "Azalan sıralama", + "i18nStrings.sortDropdownMultiColumnSortGroup": "Çok sütunlu sıralama (Shift + Tıklama)", + "i18nStrings.sortDropdownAddToSortAscending": "Sıralamaya ekle (artan)", + "i18nStrings.sortDropdownAddToSortDescending": "Sıralamaya ekle (azalan)", + "i18nStrings.sortDropdownRemoveFromSort": "Sıralamadan kaldır", "i18nStrings.clearSort": "Sıralamayı temizle", "ariaLabels.sortMenuTriggerLabel": "Sıralama seçenekleri", - "i18nStrings.sortDropdown.addToSortDisabledReason": "Bu sütun zaten sıralandı", - "i18nStrings.sortDropdown.removeFromSortDisabledReason": "Bu sütun sıralanmadı", - "i18nStrings.liveAnnouncementSortColumn": "{isDescending, select, true {{columnLabel} azalan} other {{columnLabel} artan}}", - "i18nStrings.liveAnnouncementSortOrder": "Tablo {columns} ölçütüne göre sıralandı", - "i18nStrings.liveAnnouncementSortCleared": "Sıralama temizlendi" + "i18nStrings.sortDropdownAddToSortDisabledReason": "Bu sütun zaten sıralandı", + "i18nStrings.sortDropdownRemoveFromSortDisabledReason": "Bu sütun sıralanmadı", + "ariaLabels.sortAscending": "artan", + "ariaLabels.sortDescending": "azalan", + "ariaLabels.liveAnnouncementSortOrder": "Tablo {columns} ölçütüne göre sıralandı", + "ariaLabels.liveAnnouncementSortCleared": "Sıralama temizlendi", + "ariaLabels.sortPriority": "sıralama önceliği {priority}" }, "tabs": { "i18nStrings.scrollLeftAriaLabel": "Sola kaydır", diff --git a/src/i18n/messages/all.zh-CN.json b/src/i18n/messages/all.zh-CN.json index 1aab4cd66b..8ca1cd5d67 100644 --- a/src/i18n/messages/all.zh-CN.json +++ b/src/i18n/messages/all.zh-CN.json @@ -409,19 +409,21 @@ "ariaLabels.collapseButtonLabel": "折叠", "columnDefinitions.editConfig.errorIconAriaLabel": "错误", "columnDefinitions.editConfig.editIconAriaLabel": "可编辑", - "i18nStrings.sortDropdown.sortAscending": "升序排序", - "i18nStrings.sortDropdown.sortDescending": "降序排序", - "i18nStrings.sortDropdown.multiColumnSortGroup": "多列排序(按住 Shift 键点击)", - "i18nStrings.sortDropdown.addToSortAscending": "添加到排序(升序)", - "i18nStrings.sortDropdown.addToSortDescending": "添加到排序(降序)", - "i18nStrings.sortDropdown.removeFromSort": "从排序中移除", + "i18nStrings.sortDropdownSortAscending": "升序排序", + "i18nStrings.sortDropdownSortDescending": "降序排序", + "i18nStrings.sortDropdownMultiColumnSortGroup": "多列排序(按住 Shift 键点击)", + "i18nStrings.sortDropdownAddToSortAscending": "添加到排序(升序)", + "i18nStrings.sortDropdownAddToSortDescending": "添加到排序(降序)", + "i18nStrings.sortDropdownRemoveFromSort": "从排序中移除", "i18nStrings.clearSort": "清除排序", "ariaLabels.sortMenuTriggerLabel": "排序选项", - "i18nStrings.sortDropdown.addToSortDisabledReason": "此列已排序", - "i18nStrings.sortDropdown.removeFromSortDisabledReason": "此列未排序", - "i18nStrings.liveAnnouncementSortColumn": "{isDescending, select, true {{columnLabel} 降序} other {{columnLabel} 升序}}", - "i18nStrings.liveAnnouncementSortOrder": "表格已按 {columns} 排序", - "i18nStrings.liveAnnouncementSortCleared": "排序已清除" + "i18nStrings.sortDropdownAddToSortDisabledReason": "此列已排序", + "i18nStrings.sortDropdownRemoveFromSortDisabledReason": "此列未排序", + "ariaLabels.sortAscending": "升序", + "ariaLabels.sortDescending": "降序", + "ariaLabels.liveAnnouncementSortOrder": "表格已按 {columns} 排序", + "ariaLabels.liveAnnouncementSortCleared": "排序已清除", + "ariaLabels.sortPriority": "排序优先级 {priority}" }, "tabs": { "i18nStrings.scrollLeftAriaLabel": "向左滚动", diff --git a/src/i18n/messages/all.zh-TW.json b/src/i18n/messages/all.zh-TW.json index a0ce58ca96..a7a30af491 100644 --- a/src/i18n/messages/all.zh-TW.json +++ b/src/i18n/messages/all.zh-TW.json @@ -409,19 +409,21 @@ "ariaLabels.collapseButtonLabel": "摺疊", "columnDefinitions.editConfig.errorIconAriaLabel": "錯誤", "columnDefinitions.editConfig.editIconAriaLabel": "可編輯", - "i18nStrings.sortDropdown.sortAscending": "遞增排序", - "i18nStrings.sortDropdown.sortDescending": "遞減排序", - "i18nStrings.sortDropdown.multiColumnSortGroup": "多欄排序 (Shift + 按一下)", - "i18nStrings.sortDropdown.addToSortAscending": "新增至排序 (遞增)", - "i18nStrings.sortDropdown.addToSortDescending": "新增至排序 (遞減)", - "i18nStrings.sortDropdown.removeFromSort": "從排序中移除", + "i18nStrings.sortDropdownSortAscending": "遞增排序", + "i18nStrings.sortDropdownSortDescending": "遞減排序", + "i18nStrings.sortDropdownMultiColumnSortGroup": "多欄排序 (Shift + 按一下)", + "i18nStrings.sortDropdownAddToSortAscending": "新增至排序 (遞增)", + "i18nStrings.sortDropdownAddToSortDescending": "新增至排序 (遞減)", + "i18nStrings.sortDropdownRemoveFromSort": "從排序中移除", "i18nStrings.clearSort": "清除排序", "ariaLabels.sortMenuTriggerLabel": "排序選項", - "i18nStrings.sortDropdown.addToSortDisabledReason": "此欄已排序", - "i18nStrings.sortDropdown.removeFromSortDisabledReason": "此欄未排序", - "i18nStrings.liveAnnouncementSortColumn": "{isDescending, select, true {{columnLabel} 遞減} other {{columnLabel} 遞增}}", - "i18nStrings.liveAnnouncementSortOrder": "資料表依 {columns} 排序", - "i18nStrings.liveAnnouncementSortCleared": "已清除排序" + "i18nStrings.sortDropdownAddToSortDisabledReason": "此欄已排序", + "i18nStrings.sortDropdownRemoveFromSortDisabledReason": "此欄未排序", + "ariaLabels.sortAscending": "遞增", + "ariaLabels.sortDescending": "遞減", + "ariaLabels.liveAnnouncementSortOrder": "資料表依 {columns} 排序", + "ariaLabels.liveAnnouncementSortCleared": "已清除排序", + "ariaLabels.sortPriority": "排序優先順序 {priority}" }, "tabs": { "i18nStrings.scrollLeftAriaLabel": "向左捲動", diff --git a/src/table/__tests__/multi-column-sort-behavior.test.tsx b/src/table/__tests__/multi-column-sort-behavior.test.tsx index c9401c5b5a..b26dc89864 100644 --- a/src/table/__tests__/multi-column-sort-behavior.test.tsx +++ b/src/table/__tests__/multi-column-sort-behavior.test.tsx @@ -268,3 +268,28 @@ describe('sort menu item states', () => { expect(menu.findAddToSortAscendingItem({ disabled: true })).toBeNull(); }); }); + +describe('sort menu accessibility', () => { + test('sort menu trigger is described by its own column header', () => { + const { container } = render( +
{} }} + /> + ); + const wrapper = createWrapper(container).findTable()!; + + const triggerA = wrapper.findColumnSortMenu(COL_A)!.getElement().querySelector('button')!; + const describedById = triggerA.getAttribute('aria-describedby'); + expect(describedById).toBeTruthy(); + + const describedEl = container.querySelector(`#${CSS.escape(describedById!)}`); + expect(getHeaderCell(wrapper, COL_A).contains(describedEl)).toBe(true); + expect(describedEl?.textContent).toContain('A'); + + // Each column's trigger is described by a different header, so the triggers are not ambiguous. + const triggerB = wrapper.findColumnSortMenu(COL_B)!.getElement().querySelector('button')!; + expect(triggerB.getAttribute('aria-describedby')).not.toBe(describedById); + }); +}); diff --git a/src/table/__tests__/multi-column-sort-live-announcement.test.tsx b/src/table/__tests__/multi-column-sort-live-announcement.test.tsx index fab6ed8d41..73667f795a 100644 --- a/src/table/__tests__/multi-column-sort-live-announcement.test.tsx +++ b/src/table/__tests__/multi-column-sort-live-announcement.test.tsx @@ -25,9 +25,9 @@ const columnDefinitions: TableProps.ColumnDefinition[] = [ { id: 'id', header: Rendered id, cell: item => item.id, sortingField: 'id' }, ]; -const i18nStrings: TableProps.I18nStrings = { - liveAnnouncementSortColumn: ({ columnLabel, isDescending }) => - `${columnLabel} ${isDescending ? 'descending' : 'ascending'}`, +const ariaLabels: TableProps.AriaLabels = { + sortAscending: 'ascending', + sortDescending: 'descending', liveAnnouncementSortOrder: ({ columns }) => `Table sorted by ${columns}`, liveAnnouncementSortCleared: 'Sorting cleared', }; @@ -38,7 +38,7 @@ function TableWithSort({ sortingColumns }: { sortingColumns: ReadonlyArray
{} }} - i18nStrings={i18nStrings} + ariaLabels={ariaLabels} /> ); } @@ -92,7 +92,7 @@ test('falls back to the sorting field when the sorted column has no id', async ( items={items} columnDefinitions={noIdColumns} multiColumnSort={{ sortingColumns, onChange: () => {} }} - i18nStrings={i18nStrings} + ariaLabels={ariaLabels} /> ); const { container, rerender } = render(renderNoId([])); diff --git a/src/table/header-cell/index.tsx b/src/table/header-cell/index.tsx index de47af6fda..893afc0800 100644 --- a/src/table/header-cell/index.tsx +++ b/src/table/header-cell/index.tsx @@ -9,6 +9,7 @@ import { getAnalyticsMetadataAttribute } from '@cloudscape-design/component-tool import { useInternalI18n } from '../../i18n/context'; import InternalIcon from '../../icon/internal'; +import ScreenreaderOnly from '../../internal/components/screenreader-only'; import { fireNonCancelableEvent } from '../../internal/events'; import { KeyCode } from '../../internal/keycode'; import { GeneratedAnalyticsMetadataTableSort } from '../analytics-metadata/interfaces'; @@ -39,7 +40,7 @@ export interface TableHeaderCellProps extends BaseHeaderCellProps { onClick(detail: TableProps.SortingState): void; multiColumnSort?: TableProps.MultiColumnSort; i18nStrings?: TableProps.I18nStrings; - sortMenuTriggerLabel?: string; + ariaLabels?: TableProps.AriaLabels; updateColumn: (columnId: PropertyKey, newWidth: number) => void; isEditable?: boolean; columnId: PropertyKey; @@ -88,7 +89,7 @@ export function TableHeaderCell({ tableVariant, multiColumnSort, i18nStrings, - sortMenuTriggerLabel, + ariaLabels, }: TableHeaderCellProps) { const i18n = useInternalI18n('table'); const sortable = !!column.sortingComparator || !!column.sortingField; @@ -109,6 +110,27 @@ export function TableHeaderCell({ const sortingStatus = getSortingStatus(sortable, sorted, descending, !!sortingDisabled); // In multi-sort, only the primary column declares aria-sort (ARIA only allows one column sorted at a time). const suppressAriaSort = !!multiColumnSort && inMultiSort && !isPrimaryMultiSort; + + // Screen-reader-only text appended to a sorted column header. The visible header text supplies the column name; + // this adds the sort direction (only when `aria-sort` is suppressed, i.e. secondary columns) and the sort priority. + const sortDirectionLabel = descending + ? i18n('ariaLabels.sortDescending', ariaLabels?.sortDescending) + : i18n('ariaLabels.sortAscending', ariaLabels?.sortAscending); + const sortPriorityLabel = i18n( + 'ariaLabels.sortPriority', + ariaLabels?.sortPriority, + format => + ({ priority }) => + format({ priority }) + ); + const sortAccessibleText = inMultiSort + ? [ + suppressAriaSort ? sortDirectionLabel : undefined, + showPriorityBadge ? sortPriorityLabel?.({ priority: multiSortIndex ?? 0 }) : undefined, + ] + .filter(Boolean) + .join(' ') + : ''; const handleClick = (shiftKey = false) => { if (multiColumnSort) { const current = multiColumnSort.sortingColumns; @@ -276,6 +298,7 @@ export function TableHeaderCell({ ) : null} + {sortAccessibleText && {sortAccessibleText}} {sortingStatus && ( {showPriorityBadge && ( @@ -294,7 +317,8 @@ export function TableHeaderCell({ isSortedAscending={inMultiSort && !descending} isSortedDescending={inMultiSort && descending} i18nStrings={i18nStrings} - ariaLabel={sortMenuTriggerLabel} + ariaLabel={ariaLabels?.sortMenuTriggerLabel} + ariaDescribedby={headerId} onAction={handleSortMenuAction} /> )} diff --git a/src/table/header-cell/sort-menu.tsx b/src/table/header-cell/sort-menu.tsx index b542b227d7..422b655604 100644 --- a/src/table/header-cell/sort-menu.tsx +++ b/src/table/header-cell/sort-menu.tsx @@ -23,6 +23,7 @@ export interface SortMenuProps { isSortedDescending: boolean; i18nStrings?: TableProps.I18nStrings; ariaLabel?: string; + ariaDescribedby?: string; onAction: (action: SortMenuAction) => void; } @@ -32,48 +33,54 @@ export function SortMenu({ isSortedDescending, i18nStrings, ariaLabel, + ariaDescribedby, onAction, }: SortMenuProps) { - const strings = i18nStrings?.sortDropdown; const i18n = useInternalI18n('table'); const items: ButtonDropdownProps.ItemOrGroup[] = [ { id: 'sort-ascending', itemType: 'checkbox', - text: i18n('i18nStrings.sortDropdown.sortAscending', strings?.sortAscending) ?? '', + text: i18n('i18nStrings.sortDropdownSortAscending', i18nStrings?.sortDropdownSortAscending) ?? '', checked: isSortedAscending, }, { id: 'sort-descending', itemType: 'checkbox', - text: i18n('i18nStrings.sortDropdown.sortDescending', strings?.sortDescending) ?? '', + text: i18n('i18nStrings.sortDropdownSortDescending', i18nStrings?.sortDropdownSortDescending) ?? '', checked: isSortedDescending, }, { id: 'multi-column-sort-group', itemType: 'group', - text: i18n('i18nStrings.sortDropdown.multiColumnSortGroup', strings?.multiColumnSortGroup) ?? '', + text: i18n('i18nStrings.sortDropdownMultiColumnSortGroup', i18nStrings?.sortDropdownMultiColumnSortGroup) ?? '', items: [ { id: 'add-to-sort-ascending', - text: i18n('i18nStrings.sortDropdown.addToSortAscending', strings?.addToSortAscending) ?? '', + text: i18n('i18nStrings.sortDropdownAddToSortAscending', i18nStrings?.sortDropdownAddToSortAscending) ?? '', disabled: inSort, - disabledReason: i18n('i18nStrings.sortDropdown.addToSortDisabledReason', strings?.addToSortDisabledReason), + disabledReason: i18n( + 'i18nStrings.sortDropdownAddToSortDisabledReason', + i18nStrings?.sortDropdownAddToSortDisabledReason + ), }, { id: 'add-to-sort-descending', - text: i18n('i18nStrings.sortDropdown.addToSortDescending', strings?.addToSortDescending) ?? '', + text: i18n('i18nStrings.sortDropdownAddToSortDescending', i18nStrings?.sortDropdownAddToSortDescending) ?? '', disabled: inSort, - disabledReason: i18n('i18nStrings.sortDropdown.addToSortDisabledReason', strings?.addToSortDisabledReason), + disabledReason: i18n( + 'i18nStrings.sortDropdownAddToSortDisabledReason', + i18nStrings?.sortDropdownAddToSortDisabledReason + ), }, { id: 'remove-from-sort', - text: i18n('i18nStrings.sortDropdown.removeFromSort', strings?.removeFromSort) ?? '', + text: i18n('i18nStrings.sortDropdownRemoveFromSort', i18nStrings?.sortDropdownRemoveFromSort) ?? '', disabled: !inSort, disabledReason: i18n( - 'i18nStrings.sortDropdown.removeFromSortDisabledReason', - strings?.removeFromSortDisabledReason + 'i18nStrings.sortDropdownRemoveFromSortDisabledReason', + i18nStrings?.sortDropdownRemoveFromSortDisabledReason ), }, ], @@ -88,6 +95,7 @@ export function SortMenu({ compactTrigger={true} expandToViewport={true} ariaLabel={i18n('ariaLabels.sortMenuTriggerLabel', ariaLabel) ?? ''} + ariaDescribedby={ariaDescribedby} onItemClick={({ detail }) => onAction(detail.id as SortMenuAction)} /> diff --git a/src/table/interfaces.tsx b/src/table/interfaces.tsx index 3e58dfc5a4..e1577e9e7d 100644 --- a/src/table/interfaces.tsx +++ b/src/table/interfaces.tsx @@ -226,6 +226,12 @@ export interface TableProps extends BaseComponentProps { * Specifies a text that is announced to screen readers when a cell edit operation is submitted. * * `expandButtonLabel` (Item) => string - Specifies an alternative text for row expand button. * * `collapseButtonLabel` (Item) => string - Specifies an alternative text for row collapse button. + * * `sortMenuTriggerLabel` (string) - Provides an alternative text for the column header sort menu trigger button when multi-column sort is enabled. + * * `sortAscending` (string) - Screen reader word for ascending sort direction, used in sort announcements and a sorted column header's accessible text. + * * `sortDescending` (string) - Screen reader word for descending sort direction, used in sort announcements and a sorted column header's accessible text. + * * `liveAnnouncementSortOrder` (({ columns }) => string) - Formats the screen reader announcement for the current multi-column sort order. + * * `liveAnnouncementSortCleared` (string) - Screen reader announcement made when the multi-column sort is cleared. + * * `sortPriority` (({ priority }) => string) - Formats a sorted column's position in a multi-column sort, used in the sorted column header's accessible label. * @i18n */ ariaLabels?: TableProps.AriaLabels; @@ -259,18 +265,18 @@ export interface TableProps extends BaseComponentProps { multiColumnSort?: TableProps.MultiColumnSort; /** - * An object containing all the localized strings required by the multi-column - * sorting UI: + * Object containing the localized visible strings used by the Table component. * - * * `sortDropdown` (object): Strings for the per-column sort dropdown menu. - * * `sortAscending` (string): Label for the "Sort ascending" dropdown menu item. - * * `sortDescending` (string): Label for the "Sort descending" dropdown menu item. - * * `multiColumnSortGroup` (string): Label for the "Multi-column sort" dropdown menu group. - * * `addToSortAscending` (string): Label for the "Add to sort (ascending)" dropdown menu item. - * * `addToSortDescending` (string): Label for the "Add to sort (descending)" dropdown menu item. - * * `removeFromSort` (string): Label for the "Remove from sort" dropdown menu item. - * * `addToSortDisabledReason` (string): Reason shown when the "Add to sort" menu items are disabled. - * * `removeFromSortDisabledReason` (string): Reason shown when the "Remove from sort" menu item is disabled. + * When multi-column sorting is enabled it provides the sort UI labels: + * + * * `sortDropdownSortAscending` (string): Label for the "Sort ascending" dropdown menu item. + * * `sortDropdownSortDescending` (string): Label for the "Sort descending" dropdown menu item. + * * `sortDropdownMultiColumnSortGroup` (string): Label for the multi-column sort dropdown menu group. + * * `sortDropdownAddToSortAscending` (string): Label for the "Add to sort (ascending)" dropdown menu item. + * * `sortDropdownAddToSortDescending` (string): Label for the "Add to sort (descending)" dropdown menu item. + * * `sortDropdownRemoveFromSort` (string): Label for the "Remove from sort" dropdown menu item. + * * `sortDropdownAddToSortDisabledReason` (string): Reason shown when the "Add to sort" menu items are disabled. + * * `sortDropdownRemoveFromSortDisabledReason` (string): Reason shown when the "Remove from sort" menu item is disabled. * * `clearSort` (string): Label for the "Clear sort" button. * * @i18n @@ -621,6 +627,11 @@ export namespace TableProps { expandButtonLabel?: (item: T) => string; collapseButtonLabel?: (item: T) => string; sortMenuTriggerLabel?: string; + sortAscending?: string; + sortDescending?: string; + liveAnnouncementSortOrder?: (data: { columns: string }) => string; + liveAnnouncementSortCleared?: string; + sortPriority?: (data: { priority: number }) => string; } export interface SortingState { isDescending?: boolean; @@ -638,25 +649,15 @@ export namespace TableProps { sortingColumns: ReadonlyArray>; } export interface I18nStrings { - sortDropdown?: { - sortAscending?: string; - sortDescending?: string; - multiColumnSortGroup?: string; - addToSortAscending?: string; - addToSortDescending?: string; - removeFromSort?: string; - addToSortDisabledReason?: string; - removeFromSortDisabledReason?: string; - }; + sortDropdownSortAscending?: string; + sortDropdownSortDescending?: string; + sortDropdownMultiColumnSortGroup?: string; + sortDropdownAddToSortAscending?: string; + sortDropdownAddToSortDescending?: string; + sortDropdownRemoveFromSort?: string; + sortDropdownAddToSortDisabledReason?: string; + sortDropdownRemoveFromSortDisabledReason?: string; clearSort?: string; - /** - * Live-region announcement fragments for multi-column sort changes, joined internally (charts-style). - * `liveAnnouncementSortColumn` formats a single sorted column; the results are joined and passed to - * `liveAnnouncementSortOrder`. `liveAnnouncementSortCleared` is announced when sorting is removed. - */ - liveAnnouncementSortColumn?: (data: { columnLabel: string; isDescending: boolean }) => string; - liveAnnouncementSortOrder?: (data: { columns: string }) => string; - liveAnnouncementSortCleared?: string; } export interface LabelData { sorted: boolean; diff --git a/src/table/internal.tsx b/src/table/internal.tsx index 292c399e7d..af7a4b17a2 100644 --- a/src/table/internal.tsx +++ b/src/table/internal.tsx @@ -431,7 +431,7 @@ const InternalTable = React.forwardRef( onSortingChange, multiColumnSort, i18nStrings, - sortMenuTriggerLabel: ariaLabels?.sortMenuTriggerLabel, + ariaLabels, onFocusMove: moveFocus, onResizeFinish(newWidth) { const widthsDetail = columnDefinitions.map( @@ -610,7 +610,7 @@ const InternalTable = React.forwardRef( )} diff --git a/src/table/multi-column-sort/live-announcement.tsx b/src/table/multi-column-sort/live-announcement.tsx index f65f4e2aff..18aba335b9 100644 --- a/src/table/multi-column-sort/live-announcement.tsx +++ b/src/table/multi-column-sort/live-announcement.tsx @@ -10,19 +10,10 @@ import { buildSortLiveAnnouncement } from './utils'; import analyticsSelectors from '../analytics-metadata/styles.css.js'; -// Adapt the i18n `format` function to the `i18nStrings.liveAnnouncementSortColumn` signature. The ICU -// `select` on `isDescending` requires a string, so the boolean is stringified. -const formatSortColumn: CustomHandler< - TableProps.I18nStrings['liveAnnouncementSortColumn'], - I18nFormatArgTypes['table']['i18nStrings.liveAnnouncementSortColumn'] -> = - format => - ({ columnLabel, isDescending }) => - format({ columnLabel, isDescending: `${isDescending}` }); - +// Adapt the i18n `format` function to the `ariaLabels.liveAnnouncementSortOrder` signature. const formatSortOrder: CustomHandler< - TableProps.I18nStrings['liveAnnouncementSortOrder'], - I18nFormatArgTypes['table']['i18nStrings.liveAnnouncementSortOrder'] + TableProps.AriaLabels['liveAnnouncementSortOrder'], + I18nFormatArgTypes['table']['ariaLabels.liveAnnouncementSortOrder'] > = format => ({ columns }) => @@ -49,7 +40,7 @@ function readColumnHeaderText( interface SortLiveAnnouncementProps { sortingColumns: ReadonlyArray>; columnDefinitions: ReadonlyArray>; - i18nStrings: TableProps.I18nStrings | undefined; + ariaLabels: TableProps.AriaLabels | undefined; containerRef: React.RefObject; } @@ -61,7 +52,7 @@ interface SortLiveAnnouncementProps { export function SortLiveAnnouncement({ sortingColumns, columnDefinitions, - i18nStrings, + ariaLabels, containerRef, }: SortLiveAnnouncementProps) { const i18n = useInternalI18n('table'); @@ -70,17 +61,16 @@ export function SortLiveAnnouncement({ // comma for Arabic). `type: 'unit'` keeps a neutral list without an "and" conjunction, which would // otherwise blur the sort priority order. const listFormatter = useMemo(() => new Intl.ListFormat(locale ?? undefined, { type: 'unit' }), [locale]); - const renderSortColumn = i18n( - 'i18nStrings.liveAnnouncementSortColumn', - i18nStrings?.liveAnnouncementSortColumn, - formatSortColumn - ); + const sortAscending = i18n('ariaLabels.sortAscending', ariaLabels?.sortAscending) ?? ''; + const sortDescending = i18n('ariaLabels.sortDescending', ariaLabels?.sortDescending) ?? ''; + const renderSortColumn = ({ columnLabel, isDescending }: { columnLabel: string; isDescending: boolean }) => + `${columnLabel} ${isDescending ? sortDescending : sortAscending}`.trim(); const renderSortOrder = i18n( - 'i18nStrings.liveAnnouncementSortOrder', - i18nStrings?.liveAnnouncementSortOrder, + 'ariaLabels.liveAnnouncementSortOrder', + ariaLabels?.liveAnnouncementSortOrder, formatSortOrder ); - const sortCleared = i18n('i18nStrings.liveAnnouncementSortCleared', i18nStrings?.liveAnnouncementSortCleared); + const sortCleared = i18n('ariaLabels.liveAnnouncementSortCleared', ariaLabels?.liveAnnouncementSortCleared); const [announcement, setAnnouncement] = useState(''); const isFirstRender = useRef(true); diff --git a/src/table/thead.tsx b/src/table/thead.tsx index 21bbf332e7..6f572d2cfa 100644 --- a/src/table/thead.tsx +++ b/src/table/thead.tsx @@ -32,7 +32,7 @@ export interface TheadProps { sortingDisabled: boolean | undefined; multiColumnSort?: TableProps.MultiColumnSort; i18nStrings?: TableProps.I18nStrings; - sortMenuTriggerLabel?: string; + ariaLabels?: TableProps.AriaLabels; variant: TableProps.Variant; tableVariant?: TableProps.Variant; wrapLines: boolean | undefined; @@ -71,7 +71,7 @@ const Thead = React.forwardRef( sortingDescending, multiColumnSort, i18nStrings, - sortMenuTriggerLabel, + ariaLabels, resizableColumns, variant, tableVariant, @@ -174,7 +174,7 @@ const Thead = React.forwardRef( sortingDisabled={sortingDisabled} multiColumnSort={multiColumnSort} i18nStrings={i18nStrings} - sortMenuTriggerLabel={sortMenuTriggerLabel} + ariaLabels={ariaLabels} wrapLines={wrapLines} colIndex={selectionType ? colIndex + 1 : colIndex} columnId={columnId} @@ -412,7 +412,7 @@ const Thead = React.forwardRef( sortingDisabled={sortingDisabled} multiColumnSort={multiColumnSort} i18nStrings={i18nStrings} - sortMenuTriggerLabel={sortMenuTriggerLabel} + ariaLabels={ariaLabels} wrapLines={wrapLines} colIndex={selectionType ? colIndex + 1 : colIndex} columnId={columnId} From edd6ed6e966f50406702a3639192591ee07246cd Mon Sep 17 00:00:00 2001 From: Gethin Webster Date: Wed, 15 Jul 2026 09:01:15 +0000 Subject: [PATCH 7/8] fix: Update multi-column sort permutations i18n --- .../multi-column-sort.permutations.page.tsx | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/pages/table/multi-column-sort.permutations.page.tsx b/pages/table/multi-column-sort.permutations.page.tsx index 9b9c41c01b..255395d361 100644 --- a/pages/table/multi-column-sort.permutations.page.tsx +++ b/pages/table/multi-column-sort.permutations.page.tsx @@ -28,20 +28,25 @@ const columnDefinitions: TableProps.ColumnDefinition[] = [ const noop = () => {}; const i18nStrings: TableProps.I18nStrings = { - sortDropdown: { - sortAscending: 'Sort ascending', - sortDescending: 'Sort descending', - multiColumnSortGroup: 'Multi-column sort (Shift + Click)', - addToSortAscending: 'Add to sort (ascending)', - addToSortDescending: 'Add to sort (descending)', - removeFromSort: 'Remove from sort', - }, + sortDropdownSortAscending: 'Sort ascending', + sortDropdownSortDescending: 'Sort descending', + sortDropdownMultiColumnSortGroup: 'Multi-column sort (Shift + Click)', + sortDropdownAddToSortAscending: 'Add to sort (ascending)', + sortDropdownAddToSortDescending: 'Add to sort (descending)', + sortDropdownRemoveFromSort: 'Remove from sort', + sortDropdownAddToSortDisabledReason: 'This column is already sorted', + sortDropdownRemoveFromSortDisabledReason: 'This column is not sorted', clearSort: 'Clear sort', }; const ariaLabels: TableProps.AriaLabels = { tableLabel: 'Multi-column sort permutations', sortMenuTriggerLabel: 'Sort options', + sortAscending: 'ascending', + sortDescending: 'descending', + liveAnnouncementSortOrder: ({ columns }) => `Table sorted by ${columns}`, + liveAnnouncementSortCleared: 'Sorting cleared', + sortPriority: ({ priority }) => `sort priority ${priority}`, selectionGroupLabel: 'Item selection', allItemsSelectionLabel: () => 'Select all', itemSelectionLabel: (_data, item) => `Select ${item.name}`, From 693c3d350fdc1a6840343ea311d0c91dce236522 Mon Sep 17 00:00:00 2001 From: Gethin Webster Date: Fri, 17 Jul 2026 08:31:18 +0000 Subject: [PATCH 8/8] chore: Apply Totoro Table translations --- src/i18n/messages/all.ar.json | 4 ++-- src/i18n/messages/all.de.json | 2 +- src/i18n/messages/all.en-GB.json | 2 ++ src/i18n/messages/all.es.json | 6 +++--- src/i18n/messages/all.fr.json | 4 ++-- src/i18n/messages/all.id.json | 6 +++--- src/i18n/messages/all.it.json | 4 ++-- src/i18n/messages/all.ja.json | 2 +- src/i18n/messages/all.ko.json | 2 +- src/i18n/messages/all.pt-BR.json | 2 +- src/i18n/messages/all.zh-TW.json | 4 ++-- 11 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/i18n/messages/all.ar.json b/src/i18n/messages/all.ar.json index a227f53a87..09080b3c7f 100644 --- a/src/i18n/messages/all.ar.json +++ b/src/i18n/messages/all.ar.json @@ -419,8 +419,8 @@ "ariaLabels.sortMenuTriggerLabel": "خيارات الترتيب", "i18nStrings.sortDropdownAddToSortDisabledReason": "تم بالفعل ترتيب هذا العمود", "i18nStrings.sortDropdownRemoveFromSortDisabledReason": "لم يتم ترتيب هذا العمود", - "ariaLabels.sortAscending": "تصاعدي", - "ariaLabels.sortDescending": "تنازلي", + "ariaLabels.sortAscending": "تصاعديًا", + "ariaLabels.sortDescending": "تنازليًا", "ariaLabels.liveAnnouncementSortOrder": "تم فرز الجدول حسب {columns}", "ariaLabels.liveAnnouncementSortCleared": "تم إلغاء الفرز", "ariaLabels.sortPriority": "أولوية الفرز {priority}" diff --git a/src/i18n/messages/all.de.json b/src/i18n/messages/all.de.json index 448a6db68e..726fecca6a 100644 --- a/src/i18n/messages/all.de.json +++ b/src/i18n/messages/all.de.json @@ -411,7 +411,7 @@ "columnDefinitions.editConfig.editIconAriaLabel": "bearbeitbar", "i18nStrings.sortDropdownSortAscending": "Aufsteigend sortieren", "i18nStrings.sortDropdownSortDescending": "Absteigend sortieren", - "i18nStrings.sortDropdownMultiColumnSortGroup": "Sortieren nach mehreren Spalten (Umschalttaste + Klick)", + "i18nStrings.sortDropdownMultiColumnSortGroup": "Sortieren nach mehreren Spalten (Umschalttaste + Klick)", "i18nStrings.sortDropdownAddToSortAscending": "Zur Sortierung hinzufügen (aufsteigend)", "i18nStrings.sortDropdownAddToSortDescending": "Zur Sortierung hinzufügen (absteigend)", "i18nStrings.sortDropdownRemoveFromSort": "Aus Sortierung entfernen", diff --git a/src/i18n/messages/all.en-GB.json b/src/i18n/messages/all.en-GB.json index 2fb48aa74d..8c34a1e40a 100644 --- a/src/i18n/messages/all.en-GB.json +++ b/src/i18n/messages/all.en-GB.json @@ -417,6 +417,8 @@ "i18nStrings.sortDropdownRemoveFromSort": "Remove from sort", "i18nStrings.clearSort": "Clear sort", "ariaLabels.sortMenuTriggerLabel": "Sort options", + "i18nStrings.sortDropdownAddToSortDisabledReason": "This column is already sorted", + "i18nStrings.sortDropdownRemoveFromSortDisabledReason": "This column is not sorted", "ariaLabels.sortPriority": "sort priority {priority}", "ariaLabels.sortAscending": "ascending", "ariaLabels.sortDescending": "descending", diff --git a/src/i18n/messages/all.es.json b/src/i18n/messages/all.es.json index 0c7c5eae71..db3fed3363 100644 --- a/src/i18n/messages/all.es.json +++ b/src/i18n/messages/all.es.json @@ -419,10 +419,10 @@ "ariaLabels.sortMenuTriggerLabel": "Opciones de ordenación", "i18nStrings.sortDropdownAddToSortDisabledReason": "Esta columna ya está ordenada", "i18nStrings.sortDropdownRemoveFromSortDisabledReason": "Esta columna no está ordenada", - "ariaLabels.sortAscending": "en orden ascendente", - "ariaLabels.sortDescending": "en orden descendente", + "ariaLabels.sortAscending": "ascendente", + "ariaLabels.sortDescending": "descendente", "ariaLabels.liveAnnouncementSortOrder": "Tabla ordenada por {columns}", - "ariaLabels.liveAnnouncementSortCleared": "Clasificación eliminada", + "ariaLabels.liveAnnouncementSortCleared": "Ordenación borrada", "ariaLabels.sortPriority": "prioridad de ordenación {priority}" }, "tabs": { diff --git a/src/i18n/messages/all.fr.json b/src/i18n/messages/all.fr.json index 9e364731a2..10a2455e39 100644 --- a/src/i18n/messages/all.fr.json +++ b/src/i18n/messages/all.fr.json @@ -419,8 +419,8 @@ "ariaLabels.sortMenuTriggerLabel": "Options de tri", "i18nStrings.sortDropdownAddToSortDisabledReason": "Cette colonne est déjà triée", "i18nStrings.sortDropdownRemoveFromSortDisabledReason": "Cette colonne n’est pas triée", - "ariaLabels.sortAscending": "par ordre croissant", - "ariaLabels.sortDescending": "par ordre décroissant", + "ariaLabels.sortAscending": "croissant", + "ariaLabels.sortDescending": "décroissant", "ariaLabels.liveAnnouncementSortOrder": "Tableau trié selon {columns}", "ariaLabels.liveAnnouncementSortCleared": "Tri supprimé", "ariaLabels.sortPriority": "priorité de tri {priority}" diff --git a/src/i18n/messages/all.id.json b/src/i18n/messages/all.id.json index 8874626311..3530575312 100644 --- a/src/i18n/messages/all.id.json +++ b/src/i18n/messages/all.id.json @@ -418,12 +418,12 @@ "i18nStrings.clearSort": "Hapus urutan", "ariaLabels.sortMenuTriggerLabel": "Opsi urutan", "i18nStrings.sortDropdownAddToSortDisabledReason": "Kolom ini sudah diurutkan", - "i18nStrings.sortDropdownRemoveFromSortDisabledReason": "Kolom ini tidak diurutkan", + "i18nStrings.sortDropdownRemoveFromSortDisabledReason": "Kolom ini belum diurutkan", "ariaLabels.sortAscending": "naik", "ariaLabels.sortDescending": "turun", "ariaLabels.liveAnnouncementSortOrder": "Tabel diurutkan berdasarkan {columns}", - "ariaLabels.liveAnnouncementSortCleared": "Pengurutan dibersihkan", - "ariaLabels.sortPriority": "prioritas pengurutan {priority}" + "ariaLabels.liveAnnouncementSortCleared": "Pengurutan telah dihapus", + "ariaLabels.sortPriority": "urutkan prioritas {priority}" }, "tabs": { "i18nStrings.scrollLeftAriaLabel": "Gulir ke kiri", diff --git a/src/i18n/messages/all.it.json b/src/i18n/messages/all.it.json index 1a0ee4c20e..cdd5462398 100644 --- a/src/i18n/messages/all.it.json +++ b/src/i18n/messages/all.it.json @@ -419,8 +419,8 @@ "ariaLabels.sortMenuTriggerLabel": "Opzioni di ordinamento", "i18nStrings.sortDropdownAddToSortDisabledReason": "Questa colonna è già ordinata", "i18nStrings.sortDropdownRemoveFromSortDisabledReason": "Questa colonna non è ordinata", - "ariaLabels.sortAscending": "crescente", - "ariaLabels.sortDescending": "decrescente", + "ariaLabels.sortAscending": "ascendente", + "ariaLabels.sortDescending": "discendente", "ariaLabels.liveAnnouncementSortOrder": "Tabella ordinata per {columns}", "ariaLabels.liveAnnouncementSortCleared": "Ordinamento cancellato", "ariaLabels.sortPriority": "priorità di ordinamento {priority}" diff --git a/src/i18n/messages/all.ja.json b/src/i18n/messages/all.ja.json index 95bb338c8b..062bb45068 100644 --- a/src/i18n/messages/all.ja.json +++ b/src/i18n/messages/all.ja.json @@ -423,7 +423,7 @@ "ariaLabels.sortDescending": "降順", "ariaLabels.liveAnnouncementSortOrder": "表は {columns}でソートされています", "ariaLabels.liveAnnouncementSortCleared": "ソートがクリアされました", - "ariaLabels.sortPriority": "並べ替えの優先順位 {priority}" + "ariaLabels.sortPriority": "ソートの優先順位 {priority}" }, "tabs": { "i18nStrings.scrollLeftAriaLabel": "左にスクロール", diff --git a/src/i18n/messages/all.ko.json b/src/i18n/messages/all.ko.json index e931d34e70..c0be14f1e1 100644 --- a/src/i18n/messages/all.ko.json +++ b/src/i18n/messages/all.ko.json @@ -423,7 +423,7 @@ "ariaLabels.sortDescending": "내림차순", "ariaLabels.liveAnnouncementSortOrder": "{columns} 기준 테이블 정렬", "ariaLabels.liveAnnouncementSortCleared": "정렬이 지워졌습니다.", - "ariaLabels.sortPriority": "정렬 우선순위 {priority}" + "ariaLabels.sortPriority": "정렬 우선 순위 {priority}" }, "tabs": { "i18nStrings.scrollLeftAriaLabel": "왼쪽으로 스크롤", diff --git a/src/i18n/messages/all.pt-BR.json b/src/i18n/messages/all.pt-BR.json index e26a34ed85..7ff120b894 100644 --- a/src/i18n/messages/all.pt-BR.json +++ b/src/i18n/messages/all.pt-BR.json @@ -421,7 +421,7 @@ "i18nStrings.sortDropdownRemoveFromSortDisabledReason": "Esta coluna não está classificada", "ariaLabels.sortAscending": "ordem crescente", "ariaLabels.sortDescending": "ordem decrescente", - "ariaLabels.liveAnnouncementSortOrder": "Tabela ordenada por {columns}", + "ariaLabels.liveAnnouncementSortOrder": "Tabela classificada por {columns}", "ariaLabels.liveAnnouncementSortCleared": "Classificação cancelada", "ariaLabels.sortPriority": "prioridade de classificação {priority}" }, diff --git a/src/i18n/messages/all.zh-TW.json b/src/i18n/messages/all.zh-TW.json index a7a30af491..974495651f 100644 --- a/src/i18n/messages/all.zh-TW.json +++ b/src/i18n/messages/all.zh-TW.json @@ -419,8 +419,8 @@ "ariaLabels.sortMenuTriggerLabel": "排序選項", "i18nStrings.sortDropdownAddToSortDisabledReason": "此欄已排序", "i18nStrings.sortDropdownRemoveFromSortDisabledReason": "此欄未排序", - "ariaLabels.sortAscending": "遞增", - "ariaLabels.sortDescending": "遞減", + "ariaLabels.sortAscending": "升序", + "ariaLabels.sortDescending": "降序", "ariaLabels.liveAnnouncementSortOrder": "資料表依 {columns} 排序", "ariaLabels.liveAnnouncementSortCleared": "已清除排序", "ariaLabels.sortPriority": "排序優先順序 {priority}"