From 8e2d0961be4524dd0bdc325fcc1ea50dce8e0d8c Mon Sep 17 00:00:00 2001 From: Aleksandra Danilina Date: Mon, 24 Jul 2023 17:03:38 +0200 Subject: [PATCH] feat: Autolabel table and cards with header --- src/cards/__tests__/cards.test.tsx | 11 ++++++++++- src/cards/index.tsx | 12 +++++++----- src/container/internal.tsx | 4 ---- src/header/internal.tsx | 5 ++++- src/internal/context/scrollbar-label-context.ts | 5 +++++ src/table/__tests__/a11y.test.tsx | 16 ++++++++++++++++ src/table/internal.tsx | 10 +++++++++- src/table/tools-header.tsx | 7 +++++-- 8 files changed, 56 insertions(+), 14 deletions(-) create mode 100644 src/internal/context/scrollbar-label-context.ts diff --git a/src/cards/__tests__/cards.test.tsx b/src/cards/__tests__/cards.test.tsx index 8d4746ae23..b7e1d94c37 100644 --- a/src/cards/__tests__/cards.test.tsx +++ b/src/cards/__tests__/cards.test.tsx @@ -3,6 +3,7 @@ import * as React from 'react'; import { render } from '@testing-library/react'; import Cards, { CardsProps } from '../../../lib/components/cards'; +import Header from '../../../lib/components/header'; import { CardsWrapper, PaginationWrapper } from '../../../lib/components/test-utils/dom'; import { useMobile } from '../../../lib/components/internal/hooks/use-mobile'; import liveRegionStyles from '../../../lib/components/internal/components/live-region/styles.css.js'; @@ -181,12 +182,20 @@ describe('Cards', () => { expect(wrapper.findHeader()?.getElement()).toHaveTextContent('abcedefg'); }); - it('maintains logical relationship between header and cards', () => { + it('maintains logical relationship between header and cards when header is a string', () => { wrapper = renderCards( cardDefinition={{}} items={defaultItems} header="abcedefg" />).wrapper; const cardsOrderedList = getCard(0).getElement().parentElement; expect(cardsOrderedList).toHaveAccessibleName('abcedefg'); }); + it('maintains logical relationship between header and cards when header is a component', () => { + wrapper = renderCards( + cardDefinition={{}} items={defaultItems} header={
Cards header
} /> + ).wrapper; + const cardsOrderedList = getCard(0).getElement().parentElement; + expect(cardsOrderedList).toHaveAccessibleName('Cards header'); + }); + it('allows label to be overridden', () => { wrapper = renderCards( diff --git a/src/cards/index.tsx b/src/cards/index.tsx index 09720a0388..39971bafbb 100644 --- a/src/cards/index.tsx +++ b/src/cards/index.tsx @@ -25,6 +25,7 @@ import { supportsStickyPosition } from '../internal/utils/dom'; import { useInternalI18n } from '../internal/i18n/context'; import { useContainerQuery } from '@cloudscape-design/component-toolkit'; import { AnalyticsFunnelSubStep } from '../internal/analytics/components/analytics-funnel'; +import { ScrollbarLabelContext } from '../internal/context/scrollbar-label-context'; export { CardsProps }; @@ -63,9 +64,9 @@ const Cards = React.forwardRef(function ( const isMobile = useMobile(); const computedVariant = isRefresh ? variant : 'container'; - const instanceUniqueId = useUniqueId('cards'); - const cardsId = baseProps?.id || instanceUniqueId; - const cardsHeaderId = header ? `${cardsId}-header` : undefined; + + const headingId = useUniqueId('cards-heading'); + const isLabelledByHeader = !ariaLabels?.cardsLabel && !!header; const [columns, measureRef] = useContainerQuery( ({ contentBoxWidth }) => getCardsPerRow(contentBoxWidth, cardsPerRow), @@ -139,7 +140,9 @@ const Cards = React.forwardRef(function ( styles[`header-variant-${computedVariant}`] )} > - + + + ) } @@ -150,7 +153,6 @@ const Cards = React.forwardRef(function ( __stickyHeader={stickyHeader} __stickyOffset={stickyHeaderVerticalOffset} __headerRef={headerRef} - __headerId={cardsHeaderId} __darkHeader={computedVariant === 'full-page'} __disableFooterDivider={true} > diff --git a/src/container/internal.tsx b/src/container/internal.tsx index 3c483b65a9..0a2b6bd63c 100644 --- a/src/container/internal.tsx +++ b/src/container/internal.tsx @@ -22,7 +22,6 @@ export interface InternalContainerProps extends Omit, __disableFooterPaddings?: boolean; __hiddenContent?: boolean; __headerRef?: React.RefObject; - __headerId?: string; __darkHeader?: boolean; __disableStickyMobile?: boolean; /** @@ -51,7 +50,6 @@ export default function InternalContainer({ __disableFooterPaddings = false, __hiddenContent = false, __headerRef, - __headerId, __darkHeader = false, __disableStickyMobile = true, ...restProps @@ -77,7 +75,6 @@ export default function InternalContainer({ const mergedRef = useMergeRefs(rootRef, subStepRef, __internalRootRef); const headerMergedRef = useMergeRefs(headerRef, overlapElement, __headerRef); - const headerIdProp = __headerId ? { id: __headerId } : {}; /** * The visual refresh AppLayout component needs to know if a child component @@ -139,7 +136,6 @@ export default function InternalContainer({ [styles['with-hidden-content']]: !children || __hiddenContent, [styles['header-with-media']]: hasMedia, })} - {...headerIdProp} {...stickyStyles} ref={headerMergedRef} > diff --git a/src/header/internal.tsx b/src/header/internal.tsx index 300db08ec4..8d0de52131 100644 --- a/src/header/internal.tsx +++ b/src/header/internal.tsx @@ -11,6 +11,7 @@ import styles from './styles.css.js'; import { SomeRequired } from '../internal/types'; import { useMobile } from '../internal/hooks/use-mobile'; import { InfoLinkLabelContext } from '../internal/context/info-link-label-context'; +import { ScrollbarLabelContext } from '../internal/context/scrollbar-label-context'; import { useUniqueId } from '../internal/hooks/use-unique-id'; import { DATA_ATTR_FUNNEL_KEY, FUNNEL_KEY_SUBSTEP_NAME } from '../internal/analytics/selectors'; @@ -35,7 +36,9 @@ export default function InternalHeader({ const { isStuck } = useContext(StickyHeaderContext); const baseProps = getBaseProps(restProps); const isRefresh = useVisualRefresh(); - const headingId = useUniqueId('heading'); + const wrapperHeadingId = useContext(ScrollbarLabelContext); + const uniqueHeadingId = useUniqueId('heading'); + const headingId = wrapperHeadingId ? wrapperHeadingId : uniqueHeadingId; // If is mobile there is no need to have the dynamic variant because it's scrolled out of view const dynamicVariant = !isMobile && isStuck ? 'h2' : 'h1'; const variantOverride = variant === 'awsui-h1-sticky' ? (isRefresh ? dynamicVariant : 'h2') : variant; diff --git a/src/internal/context/scrollbar-label-context.ts b/src/internal/context/scrollbar-label-context.ts new file mode 100644 index 0000000000..12886ef203 --- /dev/null +++ b/src/internal/context/scrollbar-label-context.ts @@ -0,0 +1,5 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { createContext } from 'react'; + +export const ScrollbarLabelContext = createContext(undefined); diff --git a/src/table/__tests__/a11y.test.tsx b/src/table/__tests__/a11y.test.tsx index d432985337..9a82cfa5c2 100644 --- a/src/table/__tests__/a11y.test.tsx +++ b/src/table/__tests__/a11y.test.tsx @@ -4,6 +4,7 @@ import React from 'react'; import createWrapper from '../../../lib/components/test-utils/dom'; import Table, { TableProps } from '../../../lib/components/table'; +import Header from '../../../lib/components/header'; import { render } from '@testing-library/react'; import liveRegionStyles from '../../../lib/components/internal/components/live-region/styles.css.js'; @@ -47,6 +48,21 @@ describe('labels', () => { expect(wrapper.find('[role=table]')!.getElement().getAttribute('aria-label')).toEqual(tableLabel); }); + test('automatically labels table with header if provided', () => { + const wrapper = renderTableWrapper({ + header:
Labelled table
, + }); + expect(wrapper.find('[role=table]')!.getElement()).toHaveAccessibleName('Labelled table'); + }); + + test('aria-label has priority over auto-labelling', () => { + const wrapper = renderTableWrapper({ + header:
Labelled table
, + ariaLabels: { itemSelectionLabel: () => '', selectionGroupLabel: '', tableLabel }, + }); + expect(wrapper.find('[role=table]')!.getElement()).toHaveAccessibleName(tableLabel); + }); + describe('rows', () => { test('sets aria-rowcount on table', () => { const wrapper = renderTableWrapper({ diff --git a/src/table/internal.tsx b/src/table/internal.tsx index ef04ea3d87..b80928708f 100644 --- a/src/table/internal.tsx +++ b/src/table/internal.tsx @@ -36,6 +36,8 @@ import { StickyScrollbar } from './sticky-scrollbar'; import { checkColumnWidths } from './column-widths-utils'; import { useMobile } from '../internal/hooks/use-mobile'; import { useContainerQuery } from '@cloudscape-design/component-toolkit'; +import { ScrollbarLabelContext } from '../internal/context/scrollbar-label-context'; +import { useUniqueId } from '../internal/hooks/use-unique-id'; const SELECTION_COLUMN_WIDTH = 54; const selectionColumnId = Symbol('selection-column-id'); @@ -158,6 +160,9 @@ const InternalTable = React.forwardRef( const hasFooterPagination = isMobile && variant === 'full-page' && !!pagination; const hasFooter = !!footer || hasFooterPagination; + const headingId = useUniqueId('table-heading'); + const isLabelledByHeader = !ariaLabels?.tableLabel && !!header; + const visibleColumnWidthsWithSelection: ColumnWidthDefinition[] = []; const visibleColumnIdsWithSelection: PropertyKey[] = []; if (hasSelection) { @@ -253,7 +258,9 @@ const InternalTable = React.forwardRef( ref={toolsHeaderWrapper} className={clsx(styles['header-controls'], styles[`variant-${computedVariant}`])} > - + + + )} @@ -319,6 +326,7 @@ const InternalTable = React.forwardRef( // If we state explicitly, they get it always correctly even with low number of rows. role="table" aria-label={ariaLabels?.tableLabel} + aria-labelledby={isLabelledByHeader ? headingId : undefined} aria-rowcount={totalItemsCount ? totalItemsCount + 1 : -1} > - {header} + {isHeaderString ? {header} : header} {hasTools && (
{filter &&
{filter}
}