Skip to content

Commit

Permalink
feat: Autolabel table and cards with header
Browse files Browse the repository at this point in the history
  • Loading branch information
Al-Dani committed Jul 25, 2023
1 parent f94e679 commit 8e2d096
Show file tree
Hide file tree
Showing 8 changed files with 56 additions and 14 deletions.
11 changes: 10 additions & 1 deletion src/cards/__tests__/cards.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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(<Cards<Item> 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(
<Cards<Item> cardDefinition={{}} items={defaultItems} header={<Header>Cards header</Header>} />
).wrapper;
const cardsOrderedList = getCard(0).getElement().parentElement;
expect(cardsOrderedList).toHaveAccessibleName('Cards header');
});

it('allows label to be overridden', () => {
wrapper = renderCards(
<Cards<Item>
Expand Down
12 changes: 7 additions & 5 deletions src/cards/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 };

Expand Down Expand Up @@ -63,9 +64,9 @@ const Cards = React.forwardRef(function <T = any>(
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;

Check failure on line 69 in src/cards/index.tsx

View workflow job for this annotation

GitHub Actions / build / build

'isLabelledByHeader' is assigned a value but never used

const [columns, measureRef] = useContainerQuery<number>(
({ contentBoxWidth }) => getCardsPerRow(contentBoxWidth, cardsPerRow),
Expand Down Expand Up @@ -139,7 +140,9 @@ const Cards = React.forwardRef(function <T = any>(
styles[`header-variant-${computedVariant}`]
)}
>
<ToolsHeader header={header} filter={filter} pagination={pagination} preferences={preferences} />
<ScrollbarLabelContext.Provider value={headingId}>
<ToolsHeader header={header} filter={filter} pagination={pagination} preferences={preferences} />
</ScrollbarLabelContext.Provider>
</div>
)
}
Expand All @@ -150,7 +153,6 @@ const Cards = React.forwardRef(function <T = any>(
__stickyHeader={stickyHeader}
__stickyOffset={stickyHeaderVerticalOffset}
__headerRef={headerRef}
__headerId={cardsHeaderId}
__darkHeader={computedVariant === 'full-page'}
__disableFooterDivider={true}
>
Expand Down
4 changes: 0 additions & 4 deletions src/container/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ export interface InternalContainerProps extends Omit<ContainerProps, 'variant'>,
__disableFooterPaddings?: boolean;
__hiddenContent?: boolean;
__headerRef?: React.RefObject<HTMLDivElement>;
__headerId?: string;
__darkHeader?: boolean;
__disableStickyMobile?: boolean;
/**
Expand Down Expand Up @@ -51,7 +50,6 @@ export default function InternalContainer({
__disableFooterPaddings = false,
__hiddenContent = false,
__headerRef,
__headerId,
__darkHeader = false,
__disableStickyMobile = true,
...restProps
Expand All @@ -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
Expand Down Expand Up @@ -139,7 +136,6 @@ export default function InternalContainer({
[styles['with-hidden-content']]: !children || __hiddenContent,
[styles['header-with-media']]: hasMedia,
})}
{...headerIdProp}
{...stickyStyles}
ref={headerMergedRef}
>
Expand Down
5 changes: 4 additions & 1 deletion src/header/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions src/internal/context/scrollbar-label-context.ts
Original file line number Diff line number Diff line change
@@ -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<string | undefined>(undefined);
16 changes: 16 additions & 0 deletions src/table/__tests__/a11y.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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: <Header counter="(10)">Labelled table</Header>,
});
expect(wrapper.find('[role=table]')!.getElement()).toHaveAccessibleName('Labelled table');
});

test('aria-label has priority over auto-labelling', () => {
const wrapper = renderTableWrapper({
header: <Header>Labelled table</Header>,
ariaLabels: { itemSelectionLabel: () => '', selectionGroupLabel: '', tableLabel },
});
expect(wrapper.find('[role=table]')!.getElement()).toHaveAccessibleName(tableLabel);
});

describe('rows', () => {
test('sets aria-rowcount on table', () => {
const wrapper = renderTableWrapper({
Expand Down
10 changes: 9 additions & 1 deletion src/table/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -253,7 +258,9 @@ const InternalTable = React.forwardRef(
ref={toolsHeaderWrapper}
className={clsx(styles['header-controls'], styles[`variant-${computedVariant}`])}
>
<ToolsHeader header={header} filter={filter} pagination={pagination} preferences={preferences} />
<ScrollbarLabelContext.Provider value={headingId}>
<ToolsHeader header={header} filter={filter} pagination={pagination} preferences={preferences} />
</ScrollbarLabelContext.Provider>
</div>
</div>
)}
Expand Down Expand Up @@ -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}
>
<Thead
Expand Down
7 changes: 5 additions & 2 deletions src/table/tools-header.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import clsx from 'clsx';
import React from 'react';
import React, { useContext } from 'react';
import { useContainerBreakpoints } from '../internal/hooks/container-queries';
import styles from './styles.css.js';
import { ScrollbarLabelContext } from '../internal/context/scrollbar-label-context';

interface ToolsHeaderProps {
header: React.ReactNode;
Expand All @@ -14,11 +15,13 @@ interface ToolsHeaderProps {

export default function ToolsHeader({ header, filter, pagination, preferences }: ToolsHeaderProps) {
const [breakpoint, ref] = useContainerBreakpoints(['xs']);
const headerId = useContext(ScrollbarLabelContext);
const isHeaderString = typeof header === 'string';
const isSmall = breakpoint === 'default';
const hasTools = filter || pagination || preferences;
return (
<>
{header}
{isHeaderString ? <span id={headerId}>{header}</span> : header}
{hasTools && (
<div ref={ref} className={clsx(styles.tools, isSmall && styles['tools-small'])}>
{filter && <div className={styles['tools-filtering']}>{filter}</div>}
Expand Down

0 comments on commit 8e2d096

Please sign in to comment.