diff --git a/src/components/StructuredNavigation/NavUtils/List.test.js b/src/components/StructuredNavigation/NavUtils/List.test.js index 645bc24e..8ea740e2 100644 --- a/src/components/StructuredNavigation/NavUtils/List.test.js +++ b/src/components/StructuredNavigation/NavUtils/List.test.js @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, diff --git a/src/components/StructuredNavigation/NavUtils/ListItem.js b/src/components/StructuredNavigation/NavUtils/ListItem.js index 5345cdf7..0999acc5 100644 --- a/src/components/StructuredNavigation/NavUtils/ListItem.js +++ b/src/components/StructuredNavigation/NavUtils/ListItem.js @@ -1,9 +1,9 @@ -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'; @@ -11,6 +11,8 @@ 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 @@ -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, @@ -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, }); @@ -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 ( @@ -109,9 +124,11 @@ const ListItem = ({
{isClickable ? ( <> - {isEmpty && } {`${itemIndex}.`} @@ -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, @@ -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 }; diff --git a/src/components/StructuredNavigation/NavUtils/ListItem.test.js b/src/components/StructuredNavigation/NavUtils/ListItem.test.js index bce7c80e..d9fa25c9 100644 --- a/src/components/StructuredNavigation/NavUtils/ListItem.test.js +++ b/src/components/StructuredNavigation/NavUtils/ListItem.test.js @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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'); @@ -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'); }); diff --git a/src/components/StructuredNavigation/NavUtils/SectionHeading.js b/src/components/StructuredNavigation/NavUtils/SectionHeading.js index c5387a01..86b7805c 100644 --- a/src/components/StructuredNavigation/NavUtils/SectionHeading.js +++ b/src/components/StructuredNavigation/NavUtils/SectionHeading.js @@ -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'; @@ -50,7 +50,9 @@ const SectionHeading = ({ itemId, liRef: sectionRef, sectionRef, + structureContainerRef, isCanvas: true, + isEmpty: false, canvasDuration: duration, setSectionIsCollapsed }); @@ -92,6 +94,12 @@ const SectionHeading = ({ ); }; + 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 (