diff --git a/packages/react-core/src/components/Page/PageBreadcrumb.tsx b/packages/react-core/src/components/Page/PageBreadcrumb.tsx index 7452ecacabd..5fb31e2ec7f 100644 --- a/packages/react-core/src/components/Page/PageBreadcrumb.tsx +++ b/packages/react-core/src/components/Page/PageBreadcrumb.tsx @@ -28,6 +28,8 @@ export interface PageBreadcrumbProps extends React.HTMLProps { hasShadowBottom?: boolean; /** Flag indicating if the PageBreadcrumb has a scrolling overflow */ hasOverflowScroll?: boolean; + /** Adds an accessible name to the breadcrumb section. Required when the hasOverflowScroll prop is set to true. */ + 'aria-label'?: string; } export const PageBreadcrumb = ({ @@ -39,10 +41,18 @@ export const PageBreadcrumb = ({ hasShadowTop = false, hasShadowBottom = false, hasOverflowScroll = false, + 'aria-label': ariaLabel, ...props }: PageBreadcrumbProps) => { const { height, getVerticalBreakpoint } = React.useContext(PageContext); + React.useEffect(() => { + if (hasOverflowScroll && !ariaLabel) { + /* eslint-disable no-console */ + console.warn('PageBreadcrumb: An accessible aria-label is required when hasOverflowScroll is set to true.'); + } + }, [hasOverflowScroll, ariaLabel]); + return (
{isWidthLimited &&
{children}
} diff --git a/packages/react-core/src/components/Page/PageGroup.tsx b/packages/react-core/src/components/Page/PageGroup.tsx index 5583bffd70e..afff972989a 100644 --- a/packages/react-core/src/components/Page/PageGroup.tsx +++ b/packages/react-core/src/components/Page/PageGroup.tsx @@ -25,6 +25,8 @@ export interface PageGroupProps extends React.HTMLProps { hasShadowBottom?: boolean; /** Flag indicating if the PageGroup has a scrolling overflow */ hasOverflowScroll?: boolean; + /** Adds an accessible name to the page group. Required when the hasOverflowScroll prop is set to true. */ + 'aria-label'?: string; } export const PageGroup = ({ @@ -35,10 +37,18 @@ export const PageGroup = ({ hasShadowTop = false, hasShadowBottom = false, hasOverflowScroll = false, + 'aria-label': ariaLabel, ...props }: PageGroupProps) => { const { height, getVerticalBreakpoint } = React.useContext(PageContext); + React.useEffect(() => { + if (hasOverflowScroll && !ariaLabel) { + /* eslint-disable no-console */ + console.warn('PageGroup: An accessible aria-label is required when hasOverflowScroll is set to true.'); + } + }, [hasOverflowScroll, ariaLabel]); + return (
{children}
diff --git a/packages/react-core/src/components/Page/PageNavigation.tsx b/packages/react-core/src/components/Page/PageNavigation.tsx index e911a2adc99..5acedb329fc 100644 --- a/packages/react-core/src/components/Page/PageNavigation.tsx +++ b/packages/react-core/src/components/Page/PageNavigation.tsx @@ -28,6 +28,8 @@ export interface PageNavigationProps extends React.HTMLProps { hasShadowBottom?: boolean; /** Flag indicating if the PageNavigation has a scrolling overflow */ hasOverflowScroll?: boolean; + /** Adds an accessible name to the page navigation. Required when the hasOverflowScroll prop is set to true. */ + 'aria-label'?: string; } export const PageNavigation = ({ @@ -39,10 +41,18 @@ export const PageNavigation = ({ hasShadowTop = false, hasShadowBottom = false, hasOverflowScroll = false, + 'aria-label': ariaLabel, ...props }: PageNavigationProps) => { const { height, getVerticalBreakpoint } = React.useContext(PageContext); + React.useEffect(() => { + if (hasOverflowScroll && !ariaLabel) { + /* eslint-disable no-console */ + console.warn('PageNavigation: An accessible aria-label is required when hasOverflowScroll is set to true.'); + } + }, [hasOverflowScroll, ariaLabel]); + return (
{isWidthLimited &&
{children}
} diff --git a/packages/react-core/src/components/Page/PageSection.tsx b/packages/react-core/src/components/Page/PageSection.tsx index e320f7c854f..0ab2db8333d 100644 --- a/packages/react-core/src/components/Page/PageSection.tsx +++ b/packages/react-core/src/components/Page/PageSection.tsx @@ -61,6 +61,10 @@ export interface PageSectionProps extends React.HTMLProps { hasShadowBottom?: boolean; /** Flag indicating if the PageSection has a scrolling overflow */ hasOverflowScroll?: boolean; + /** Adds an accessible name to the page section. Required when the hasOverflowScroll prop is set to true. + * This prop should also be passed in if a heading is not being used to describe the content of the page section. + */ + 'aria-label'?: string; } const variantType = { @@ -93,10 +97,18 @@ export const PageSection: React.FunctionComponent = ({ hasShadowTop = false, hasShadowBottom = false, hasOverflowScroll = false, + 'aria-label': ariaLabel, ...props }: PageSectionProps) => { const { height, getVerticalBreakpoint } = React.useContext(PageContext); + React.useEffect(() => { + if (hasOverflowScroll && !ariaLabel) { + /* eslint-disable no-console */ + console.warn('PageSection: An accessible aria-label is required when hasOverflowScroll is set to true.'); + } + }, [hasOverflowScroll, ariaLabel]); + return (
= ({ className )} {...(hasOverflowScroll && { tabIndex: 0 })} + aria-label={ariaLabel} > {isWidthLimited &&
{children}
} {!isWidthLimited && children} diff --git a/packages/react-core/src/components/Page/__tests__/PageBreadcrumb.test.tsx b/packages/react-core/src/components/Page/__tests__/PageBreadcrumb.test.tsx index f6d95f59396..913838f7473 100644 --- a/packages/react-core/src/components/Page/__tests__/PageBreadcrumb.test.tsx +++ b/packages/react-core/src/components/Page/__tests__/PageBreadcrumb.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import { PageBreadcrumb } from '../PageBreadcrumb'; describe('page breadcrumb', () => { @@ -31,4 +31,44 @@ describe('page breadcrumb', () => { const { asFragment } = render(test); expect(asFragment()).toMatchSnapshot(); }); + + test('Renders without an aria-label by default', () => { + render(test); + + expect(screen.getByText('test')).not.toHaveAccessibleName('Test label'); + }); + + test('Renders with the passed aria-label applied', () => { + render(test); + + expect(screen.getByText('test')).toHaveAccessibleName('Test label'); + }); + + test('Does not log a warning in the console by default', () => { + const consoleWarning = jest.spyOn(console, 'warn').mockImplementation(); + + render(test); + + expect(consoleWarning).not.toHaveBeenCalled(); + }); + + test('Does not log a warning in the console when an aria-label is included with hasOverflowScroll', () => { + const consoleWarning = jest.spyOn(console, 'warn').mockImplementation(); + + render( + + test + + ); + + expect(consoleWarning).not.toHaveBeenCalled(); + }); + + test('Logs a warning in the console when an aria-label is not included with hasOverflowScroll', () => { + const consoleWarning = jest.spyOn(console, 'warn').mockImplementation(); + + render(test); + + expect(consoleWarning).toHaveBeenCalled(); + }); }); diff --git a/packages/react-core/src/components/Page/__tests__/PageGroup.test.tsx b/packages/react-core/src/components/Page/__tests__/PageGroup.test.tsx index 2468a6ece07..f4bb73fd370 100644 --- a/packages/react-core/src/components/Page/__tests__/PageGroup.test.tsx +++ b/packages/react-core/src/components/Page/__tests__/PageGroup.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import { PageGroup } from '../PageGroup'; describe('page group', () => { @@ -27,4 +27,44 @@ describe('page group', () => { const { asFragment } = render(test); expect(asFragment()).toMatchSnapshot(); }); + + test('Renders without an aria-label by default', () => { + render(test); + + expect(screen.getByText('test')).not.toHaveAccessibleName('Test label'); + }); + + test('Renders with the passed aria-label applied', () => { + render(test); + + expect(screen.getByText('test')).toHaveAccessibleName('Test label'); + }); + + test('Does not log a warning in the console by default', () => { + const consoleWarning = jest.spyOn(console, 'warn').mockImplementation(); + + render(test); + + expect(consoleWarning).not.toHaveBeenCalled(); + }); + + test('Does not log a warning in the console when an aria-label is included with hasOverflowScroll', () => { + const consoleWarning = jest.spyOn(console, 'warn').mockImplementation(); + + render( + + test + + ); + + expect(consoleWarning).not.toHaveBeenCalled(); + }); + + test('Logs a warning in the console when an aria-label is not included with hasOverflowScroll', () => { + const consoleWarning = jest.spyOn(console, 'warn').mockImplementation(); + + render(test); + + expect(consoleWarning).toHaveBeenCalled(); + }); }); diff --git a/packages/react-core/src/components/Page/__tests__/PageNavigation.test.tsx b/packages/react-core/src/components/Page/__tests__/PageNavigation.test.tsx index ecfab3e6826..80e3c6556d3 100644 --- a/packages/react-core/src/components/Page/__tests__/PageNavigation.test.tsx +++ b/packages/react-core/src/components/Page/__tests__/PageNavigation.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import { PageNavigation } from '../PageNavigation'; describe('page navigation', () => { @@ -31,4 +31,44 @@ describe('page navigation', () => { const { asFragment } = render(test); expect(asFragment()).toMatchSnapshot(); }); + + test('Renders without an aria-label by default', () => { + render(test); + + expect(screen.getByText('test')).not.toHaveAccessibleName('Test label'); + }); + + test('Renders with the passed aria-label applied', () => { + render(test); + + expect(screen.getByText('test')).toHaveAccessibleName('Test label'); + }); + + test('Does not log a warning in the console by default', () => { + const consoleWarning = jest.spyOn(console, 'warn').mockImplementation(); + + render(test); + + expect(consoleWarning).not.toHaveBeenCalled(); + }); + + test('Does not log a warning in the console when an aria-label is included with hasOverflowScroll', () => { + const consoleWarning = jest.spyOn(console, 'warn').mockImplementation(); + + render( + + test + + ); + + expect(consoleWarning).not.toHaveBeenCalled(); + }); + + test('Logs a warning in the console when an aria-label is not included with hasOverflowScroll', () => { + const consoleWarning = jest.spyOn(console, 'warn').mockImplementation(); + + render(test); + + expect(consoleWarning).toHaveBeenCalled(); + }); }); diff --git a/packages/react-core/src/components/Page/__tests__/PageSection.test.tsx b/packages/react-core/src/components/Page/__tests__/PageSection.test.tsx index 9ad5e11fcc1..f5d2c6cce84 100644 --- a/packages/react-core/src/components/Page/__tests__/PageSection.test.tsx +++ b/packages/react-core/src/components/Page/__tests__/PageSection.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import { PageSection, PageSectionTypes } from '../PageSection'; jest.mock('../Page'); @@ -88,3 +88,43 @@ test('Verify page section overflow scroll', () => { const { asFragment } = render(test); expect(asFragment()).toMatchSnapshot(); }); + +test('Renders without an aria-label by default', () => { + render(test); + + expect(screen.getByText('test')).not.toHaveAccessibleName('Test label'); +}); + +test('Renders with the passed aria-label applied', () => { + render(test); + + expect(screen.getByText('test')).toHaveAccessibleName('Test label'); +}); + +test('Does not log a warning in the console by default', () => { + const consoleWarning = jest.spyOn(console, 'warn').mockImplementation(); + + render(test); + + expect(consoleWarning).not.toHaveBeenCalled(); +}); + +test('Does not log a warning in the console when an aria-label is included with hasOverflowScroll', () => { + const consoleWarning = jest.spyOn(console, 'warn').mockImplementation(); + + render( + + test + + ); + + expect(consoleWarning).not.toHaveBeenCalled(); +}); + +test('Logs a warning in the console when an aria-label is not included with hasOverflowScroll', () => { + const consoleWarning = jest.spyOn(console, 'warn').mockImplementation(); + + render(test); + + expect(consoleWarning).toHaveBeenCalled(); +}); diff --git a/packages/react-core/src/demos/JumpLinks.md b/packages/react-core/src/demos/JumpLinks.md index f8b07fd5ba5..01804f6ee81 100644 --- a/packages/react-core/src/demos/JumpLinks.md +++ b/packages/react-core/src/demos/JumpLinks.md @@ -10,6 +10,7 @@ import DashboardWrapper from './examples/DashboardWrapper'; JumpLinks has a scrollspy built-in to make your implementation easier. When implementing JumpLinks be sure to: 1. Find the correct `scrollableSelector` for your page via [Firefox's debugging scrollable overflow](https://developer.mozilla.org/en-US/docs/Tools/Page_Inspector/How_to/Debug_Scrollable_Overflow) or by adding `hasOverflowScroll` to a [PageSection](/components/page#pagesection) or [PageGroup](/components/page#pagegroup). + - If you add `hasOverflowScroll` to a Page sub-component you should also add a relevant aria-label to that component as well. 2. Provide `href`s to your JumpLinksItems which match the `id` of elements you want to spy on. If you wish to scroll to a different item than you're linking to use the `node` prop. ### Scrollspy with subsections diff --git a/packages/react-core/src/demos/examples/BackToTop/BackToTopNameDemo.tsx b/packages/react-core/src/demos/examples/BackToTop/BackToTopNameDemo.tsx index 30de7d00033..2e0f84543a9 100644 --- a/packages/react-core/src/demos/examples/BackToTop/BackToTopNameDemo.tsx +++ b/packages/react-core/src/demos/examples/BackToTop/BackToTopNameDemo.tsx @@ -34,7 +34,12 @@ export const Name = () => { - + {Array.apply(0, Array(60)).map((_x: any, i: number) => (