Skip to content

Commit

Permalink
Use aria-live for player updates, aria-label & aria-role='button' for…
Browse files Browse the repository at this point in the history
… links in structured nav
  • Loading branch information
Dananji committed Feb 25, 2025
1 parent 3ca22c3 commit d588afc
Show file tree
Hide file tree
Showing 10 changed files with 158 additions and 19 deletions.
6 changes: 6 additions & 0 deletions src/components/StructuredNavigation/NavUtils/List.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ describe('List component', () => {
rangeId: 'https://example.com/manifest/lunchroom_manners/range/1',
isTitle: true,
isCanvas: true,
canvasIndex: 1,
itemIndex: 1,
isClickable: false,
isEmpty: false,
Expand All @@ -27,6 +28,7 @@ describe('List component', () => {
rangeId: 'https://example.com/manifest/lunchroom_manners/range/1-1',
isTitle: true,
isCanvas: false,
canvasIndex: 1,
itemIndex: undefined,
isClickable: false,
isEmpty: false,
Expand All @@ -39,6 +41,7 @@ describe('List component', () => {
rangeId: 'https://example.com/manifest/lunchroom_manners/range/1-1-1',
isTitle: false,
isCanvas: false,
canvasIndex: 1,
itemIndex: 1,
isClickable: true,
isEmpty: false,
Expand All @@ -52,6 +55,7 @@ describe('List component', () => {
rangeId: 'https://example.com/manifest/lunchroom_manners/range/1-1-2',
isTitle: false,
isCanvas: false,
canvasIndex: 1,
itemIndex: 2,
isClickable: true,
isEmpty: false,
Expand Down Expand Up @@ -109,6 +113,7 @@ describe('List component', () => {
rangeId: 'https://example.com/manifest/lunchroom_manners/range/1',
isTitle: true,
isCanvas: true,
canvasIndex: 1,
itemIndex: 1,
isClickable: false,
isEmpty: false,
Expand All @@ -121,6 +126,7 @@ describe('List component', () => {
rangeId: 'https://example.com/manifest/lunchroom_manners/range/1-1',
isTitle: true,
isCanvas: false,
canvasIndex: 1,
itemIndex: undefined,
isClickable: false,
isEmpty: false,
Expand Down
34 changes: 26 additions & 8 deletions src/components/StructuredNavigation/NavUtils/ListItem.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import React, { Fragment, useEffect, useRef } from 'react';
import React, { Fragment, useEffect, useMemo, useRef } from 'react';
import cx from 'classnames';
import List from './List';
import SectionHeading from './SectionHeading';
import PropTypes from 'prop-types';
import { autoScroll } from '@Services/utility-helpers';
import { autoScroll, CANVAS_MESSAGE_TIMEOUT } from '@Services/utility-helpers';
import { LockedSVGIcon } from '@Services/svg-icons';
import { useActiveStructure } from '@Services/ramp-hooks';

/**
* Build leaf-level nodes in the structures in Manifest. These nodes can be
* either timespans (with media fragment) or titles (w/o media fragment).
* @param {Object} props
* @param {Number} props.canvasDuration duration of the Canvas associated with the item
* @param {Number} props.canvasIndex index of the Canvas associated with the item
* @param {Number} props.duration duration of the item
* @param {String} props.id media fragemnt of the item
* @param {Boolean} props.isTitle flag to indicate item w/o mediafragment
Expand All @@ -23,11 +25,12 @@ import { useActiveStructure } from '@Services/ramp-hooks';
* @param {Array} props.items list of children for the item
* @param {Number} props.itemIndex index of the item within the section/canvas
* @param {String} props.rangeId unique id of the item
* @param {Number} props.canvasDuration duration of the Canvas associated with the item
* @param {Object} props.sectionRef React ref of the section element associated with the item
* @param {Object} props.structureContainerRef React ref of the structure container
*/
const ListItem = ({
canvasDuration,
canvasIndex,
duration,
id,
isTitle,
Expand All @@ -41,15 +44,15 @@ const ListItem = ({
items,
itemIndex,
rangeId,
canvasDuration,
sectionRef,
structureContainerRef
}) => {
const liRef = useRef(null);

const { handleClick, isActiveLi, currentNavItem, isPlaylist } = useActiveStructure({
itemId: id, liRef, sectionRef,
const { handleClick, isActiveLi, currentNavItem, isPlaylist, screenReaderTime } = useActiveStructure({
itemId: id, liRef, sectionRef, structureContainerRef,
isCanvas,
isEmpty,
canvasDuration,
});

Expand Down Expand Up @@ -81,6 +84,18 @@ const ListItem = ({
}
}, [currentNavItem]);

// Build aria-label based on the structure item and context
const ariaLabel = useMemo(() => {
if (isPlaylist) {
return isEmpty
? `Restricted playlist item labelled ${label} starts a ${CANVAS_MESSAGE_TIMEOUT / 1000}
second timer to auto-advance to next playlist item`
: `Playlist item labelled ${label} starting at ${screenReaderTime}`;
} else {
return `Structure item labelled ${label} starting at ${screenReaderTime} in Canvas ${canvasIndex}`;
}
}, [screenReaderTime, isPlaylist]);

const renderListItem = () => {
return (
<Fragment key={rangeId}>
Expand Down Expand Up @@ -109,9 +124,11 @@ const ListItem = ({
<div className="tracker"></div>
{isClickable ? (
<>
<a role='link'
<a
role='button'
className='ramp--structured-nav__item-link'
href={homepage && homepage != '' ? homepage : id}
aria-label={ariaLabel}
onClick={handleClick}>
{isEmpty && <LockedSVGIcon />}
{`${itemIndex}.`}
Expand Down Expand Up @@ -155,6 +172,8 @@ const ListItem = ({
};

ListItem.propTypes = {
canvasDuration: PropTypes.number.isRequired,
canvasIndex: PropTypes.number.isRequired,
duration: PropTypes.string.isRequired,
id: PropTypes.string,
isTitle: PropTypes.bool.isRequired,
Expand All @@ -167,7 +186,6 @@ ListItem.propTypes = {
items: PropTypes.array.isRequired,
itemIndex: PropTypes.number,
rangeId: PropTypes.string.isRequired,
canvasDuration: PropTypes.number.isRequired,
sectionRef: PropTypes.object.isRequired,
structureContainerRef: PropTypes.object.isRequired
};
Expand Down
17 changes: 16 additions & 1 deletion src/components/StructuredNavigation/NavUtils/ListItem.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ import {
withManifestProvider,
withPlayerProvider,
} from '../../../services/testing-helpers';
import * as utils from '@Services/utility-helpers';

describe('ListItem component', () => {
const sectionRef = { current: '' };
const initialManifestState = { structures: { isCollapsed: false }, canvasIndex: 0 };
const structureContainerRef = { current: { scrollTop: 0 } };
const structureContainerRef = { current: { scrollTop: 0, querySelector: jest.fn() } };
const autoScrollMock = jest.spyOn(utils, 'autoScroll').mockImplementationOnce(jest.fn());

const playlistItem =
{
canvasIndex: 1,
Expand All @@ -36,6 +39,7 @@ describe('ListItem component', () => {
rangeId: 'https://example.com/manifest/lunchroom_manners/range/1-1',
isTitle: true,
isCanvas: false,
canvasIndex: 1,
itemIndex: undefined,
isClickable: false,
isEmpty: false,
Expand All @@ -48,6 +52,7 @@ describe('ListItem component', () => {
rangeId: 'https://example.com/manifest/lunchroom_manners/range/1-1-1',
isTitle: false,
isCanvas: false,
canvasIndex: 1,
itemIndex: 1,
isClickable: true,
isEmpty: false,
Expand All @@ -63,6 +68,7 @@ describe('ListItem component', () => {
rangeId: 'https://example.com/manifest/lunchroom_manners/range/1-1-2',
isTitle: false,
isCanvas: false,
canvasIndex: 1,
itemIndex: 2,
isClickable: true,
isEmpty: false,
Expand All @@ -85,6 +91,7 @@ describe('ListItem component', () => {
rangeId: 'https://example.com/manifest/lunchroom_manners/range/1-1',
isTitle: false,
isCanvas: false,
canvasIndex: 1,
itemIndex: 1,
isClickable: true,
isEmpty: false,
Expand Down Expand Up @@ -117,6 +124,7 @@ describe('ListItem component', () => {
rangeId: 'https://example.com/manifest/lunchroom_manners/range/1-1',
isTitle: true,
isCanvas: false,
canvasIndex: 1,
itemIndex: undefined,
isClickable: false,
isEmpty: false,
Expand All @@ -129,6 +137,7 @@ describe('ListItem component', () => {
rangeId: 'https://example.com/manifest/lunchroom_manners/range/1-1-1',
isTitle: false,
isCanvas: false,
canvasIndex: 1,
itemIndex: 1,
isClickable: true,
isEmpty: false,
Expand All @@ -144,6 +153,7 @@ describe('ListItem component', () => {
rangeId: 'https://example.com/manifest/lunchroom_manners/range/1-1-3',
isTitle: false,
isCanvas: false,
canvasIndex: 1,
itemIndex: 2,
isClickable: true,
isEmpty: false,
Expand All @@ -159,6 +169,7 @@ describe('ListItem component', () => {
rangeId: 'https://example.com/manifest/lunchroom_manners/range/1-2',
isTitle: true,
isCanvas: false,
canvasIndex: 1,
itemIndex: undefined,
isClickable: false,
isEmpty: false,
Expand All @@ -171,6 +182,7 @@ describe('ListItem component', () => {
rangeId: 'https://example.com/manifest/lunchroom_manners/range/1-2-1',
isTitle: false,
isCanvas: false,
canvasIndex: 1,
itemIndex: 3,
isClickable: true,
isEmpty: false,
Expand All @@ -186,6 +198,7 @@ describe('ListItem component', () => {
rangeId: 'https://example.com/manifest/lunchroom_manners/range/1-2-2',
isTitle: false,
isCanvas: false,
canvasIndex: 1,
itemIndex: 4,
isClickable: true,
isEmpty: false,
Expand Down Expand Up @@ -247,6 +260,7 @@ describe('ListItem component', () => {
fireEvent.click(listItem.children[1]);

waitFor(() => {
expect(autoScrollMock).toHaveBeenCalledTimes(1);
expect(listItem.isClicked).toBeTruthy();
expect(listItem).toHaveClass('active');
expect(listItem.className).toEqual('ramp--structured-nav__list-item active');
Expand All @@ -264,6 +278,7 @@ describe('ListItem component', () => {
fireEvent.click(listItem2.children[1]);

waitFor(() => {
expect(autoScrollMock).toHaveBeenCalledTimes(2);
expect(listItem2).toHaveClass('active');
expect(listItem1).not.toHaveClass('active');
});
Expand Down
14 changes: 12 additions & 2 deletions src/components/StructuredNavigation/NavUtils/SectionHeading.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import cx from 'classnames';
import PropTypes from 'prop-types';
import { autoScroll } from '@Services/utility-helpers';
Expand Down Expand Up @@ -50,7 +50,9 @@ const SectionHeading = ({
itemId,
liRef: sectionRef,
sectionRef,
structureContainerRef,
isCanvas: true,
isEmpty: false,
canvasDuration: duration,
setSectionIsCollapsed
});
Expand Down Expand Up @@ -92,6 +94,12 @@ const SectionHeading = ({
</button>);
};

const ariaLabel = useMemo(() => {
return itemId != undefined
? `Load media for Canvas ${itemIndex} labelled ${label} into the player`
: isRoot ? `Table of content for ${label}` : `Section for Canvas ${itemIndex} labelled ${label}`;
}, [itemId]);

return (
<div className={cx(
'ramp--structured-nav__section',
Expand All @@ -103,7 +111,9 @@ const SectionHeading = ({
<div className='ramp--structured-nav__section-head-buttons'>
<button
data-testid={itemId == undefined ? 'listitem-section-span' : 'listitem-section-button'}
ref={sectionRef} onClick={handleClick}
ref={sectionRef}
onClick={itemId != undefined ? handleClick : null}
aria-label={ariaLabel}
className={cx(
'ramp--structured-nav__section-title',
!itemId && 'not-clickable'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ describe('SectionHeading component', () => {
isEmpty: false,
isRoot: false,
isTitle: false,
canvasIndex: 1,
itemIndex: 1,
items: [],
label: "Title",
Expand Down
10 changes: 6 additions & 4 deletions src/components/StructuredNavigation/StructuredNavigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -215,10 +215,11 @@ const StructuredNavigation = ({ showAllSectionsButton = false, sectionsHeading =
)}>
{showAllSectionsButton && !playlist.isPlaylist &&
<div className='ramp--structured-nav__sections'>
<span className={cx(
'ramp--structured-nav__sections-text',
hasRootRangeRef.current && 'hidden' // hide 'Sections' text when a root Range exists
)}>{sectionsHeading}</span>
<span
className={cx(
'ramp--structured-nav__sections-text',
hasRootRangeRef.current && 'hidden' // hide 'Sections' text when a root Range exists
)}>{sectionsHeading}</span>
{hasCollapsibleStructRef.current && <CollapseExpandButton numberOfSections={structureItemsRef.current?.length} />}
</div>
}
Expand All @@ -238,6 +239,7 @@ const StructuredNavigation = ({ showAllSectionsButton = false, sectionsHeading =
onMouseLeave={() => handleMouseOver(false)}
onMouseOver={() => handleMouseOver(true)}
>
<div aria-live="assertive" className="ramp--structured-nav__sr-only" />
{structureItemsRef.current?.length > 0 ? (
structureItemsRef.current.map((item, index) => (
/* For playlist views omit the accordion style display of
Expand Down
5 changes: 5 additions & 0 deletions src/components/StructuredNavigation/StructuredNavigation.scss
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@
}
}

.ramp--structured-nav__sr-only {
position: absolute;
left: -9999px;
}

.ramp--structured-nav__content {
margin-top: 0;
overflow-y: auto;
Expand Down
Loading

0 comments on commit d588afc

Please sign in to comment.