diff --git a/packages/mermaid/src/accessibility.spec.ts b/packages/mermaid/src/accessibility.spec.ts index 29a2f125d..7336284fe 100644 --- a/packages/mermaid/src/accessibility.spec.ts +++ b/packages/mermaid/src/accessibility.spec.ts @@ -1,73 +1,172 @@ -// Spec/tests for accessibility - +import { MockedD3 } from './tests/MockedD3'; import { setA11yDiagramInfo, addSVGa11yTitleDescription } from './accessibility'; -import { MockedD3 } from './tests/MockedD3'; +describe('accessibility', () => { + const fauxSvgNode = new MockedD3(); -const fauxSvgNode = new MockedD3(); - -const MockedDiagramDb = { - getAccTitle: vi.fn().mockReturnValue('the title'), - getAccDescription: vi.fn().mockReturnValue('the description'), -}; - -describe('setA11yDiagramInfo', () => { - it('sets the aria-roledescription to the diagram type', () => { - // @ts-ignore Required to easily handle the d3 select types - const svg_attr_spy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode); - setA11yDiagramInfo(fauxSvgNode, 'flowchart'); - expect(svg_attr_spy).toHaveBeenCalledWith('aria-roledescription', 'flowchart'); - }); -}); - -describe('addSVGa11yTitleDescription', () => { - const testDiagramDb = MockedDiagramDb; - const givenId = 'theBaseId'; - - describe('with the given svg d3 object:', () => { - it('does nothing if there is no insert defined', () => { - const noInsertSvg = { - attr: vi.fn(), - }; - const noInsert_attr_spy = vi.spyOn(noInsertSvg, 'attr').mockReturnValue(noInsertSvg); - addSVGa11yTitleDescription(testDiagramDb, noInsertSvg, givenId); - expect(noInsert_attr_spy).not.toHaveBeenCalled(); - }); - - it('sets aria-labelledby to the title id and the description id inserted as children', () => { - // @ts-ignore Required to easily handle the d3 select types + describe('setA11yDiagramInfo', () => { + it('sets the aria-roledescription to the diagram type', () => { + // @ts-ignore Required to easily handle the d3 select types const svg_attr_spy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode); - addSVGa11yTitleDescription(testDiagramDb, fauxSvgNode, givenId); - expect(svg_attr_spy).toHaveBeenCalledWith( - 'aria-labelledby', - `chart-title-${givenId} chart-desc-${givenId}` - ); + setA11yDiagramInfo(fauxSvgNode, 'flowchart'); + expect(svg_attr_spy).toHaveBeenCalledWith('aria-roledescription', 'flowchart'); }); - it('inserts a title tag as the first child with the text set to the accTitle returned by the diagram db', () => { - const faux_title = new MockedD3(); - const svg_insert_spy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(faux_title); + it('does nothing if the diagram type is empty', () => { // @ts-ignore Required to easily handle the d3 select types - const title_attr_spy = vi.spyOn(faux_title, 'attr').mockReturnValue(faux_title); - const title_text_spy = vi.spyOn(faux_title, 'text'); - - addSVGa11yTitleDescription(testDiagramDb, fauxSvgNode, givenId); - expect(svg_insert_spy).toHaveBeenCalledWith('title', ':first-child'); - expect(title_attr_spy).toHaveBeenCalledWith('id', `chart-title-` + givenId); - expect(title_text_spy).toHaveBeenNthCalledWith(2, 'the title'); + const svg_attr_spy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode); + setA11yDiagramInfo(fauxSvgNode, ''); + expect(svg_attr_spy).not.toHaveBeenCalled(); }); + }); - it('inserts a desc tag as the 2nd child with the text set to accDescription returned by the diagram db', () => { - const faux_desc = new MockedD3(); - const svg_insert_spy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(faux_desc); - // @ts-ignore Required to easily handle the d3 select types - const desc_attr_spy = vi.spyOn(faux_desc, 'attr').mockReturnValue(faux_desc); - const desc_text_spy = vi.spyOn(faux_desc, 'text'); + describe('addSVGa11yTitleDescription', () => { + const givenId = 'theBaseId'; - addSVGa11yTitleDescription(testDiagramDb, fauxSvgNode, givenId); - expect(svg_insert_spy).toHaveBeenCalledWith('desc', ':first-child'); - expect(desc_attr_spy).toHaveBeenCalledWith('id', `chart-desc-` + givenId); - expect(desc_text_spy).toHaveBeenNthCalledWith(1, 'the description'); + describe('with the given svg d3 object:', () => { + it('does nothing if there is no insert defined', () => { + const noInsertSvg = { + attr: vi.fn(), + }; + const noInsert_attr_spy = vi.spyOn(noInsertSvg, 'attr').mockReturnValue(noInsertSvg); + addSVGa11yTitleDescription(noInsertSvg, 'some title', 'some desc', givenId); + expect(noInsert_attr_spy).not.toHaveBeenCalled(); + }); + + describe('given an a11y title', () => { + const a11yTitle = 'a11y title'; + + describe('given an a11y description', () => { + const a11yDesc = 'a11y description'; + + it('sets aria-labelledby to the title id and the description id inserted as children', () => { + // @ts-ignore Required to easily handle the d3 select types + const svg_attr_spy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode); + addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId); + expect(svg_attr_spy).toHaveBeenCalledWith( + 'aria-labelledby', + `chart-title-${givenId} chart-desc-${givenId}` + ); + }); + + it('inserts a title tag as the first child with the text set to the accTitle given', () => { + const faux_title = new MockedD3(); + const svg_insert_spy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(faux_title); + // @ts-ignore Required to easily handle the d3 select types + const title_attr_spy = vi.spyOn(faux_title, 'attr').mockReturnValue(faux_title); + const title_text_spy = vi.spyOn(faux_title, 'text'); + + addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId); + expect(svg_insert_spy).toHaveBeenCalledWith('desc', ':first-child'); + expect(title_attr_spy).toHaveBeenCalledWith('id', `chart-desc-` + givenId); + expect(title_text_spy).toHaveBeenNthCalledWith(1, 'a11y description'); + }); + + it('inserts a desc tag as the 2nd child with the text set to accDescription given', () => { + const faux_desc = new MockedD3(); + const svg_insert_spy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(faux_desc); + // @ts-ignore Required to easily handle the d3 select types + const desc_attr_spy = vi.spyOn(faux_desc, 'attr').mockReturnValue(faux_desc); + const desc_text_spy = vi.spyOn(faux_desc, 'text'); + + addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId); + expect(svg_insert_spy).toHaveBeenCalledWith('desc', ':first-child'); + expect(desc_attr_spy).toHaveBeenCalledWith('id', `chart-desc-` + givenId); + expect(desc_text_spy).toHaveBeenNthCalledWith(1, 'a11y description'); + }); + }); + + describe(`no a11y description`, () => { + const a11yDesc = undefined; + + it('sets aria-labelledby to the title id inserted as a child', () => { + // @ts-ignore Required to easily handle the d3 select types + const svg_attr_spy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode); + addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId); + expect(svg_attr_spy).toHaveBeenCalledWith('aria-labelledby', `chart-title-${givenId}`); + }); + + it('inserts a title tag as the first child with the text set to the accTitle given', () => { + const faux_title = new MockedD3(); + const svg_insert_spy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(faux_title); + // @ts-ignore Required to easily handle the d3 select types + const title_attr_spy = vi.spyOn(faux_title, 'attr').mockReturnValue(faux_title); + const title_text_spy = vi.spyOn(faux_title, 'text'); + + addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId); + expect(svg_insert_spy).toHaveBeenCalledWith('title', ':first-child'); + expect(title_attr_spy).toHaveBeenCalledWith('id', `chart-title-` + givenId); + expect(title_text_spy).toHaveBeenNthCalledWith(1, 'a11y title'); + }); + + it('no description tag is inserted', () => { + const faux_title = new MockedD3(); + const svg_insert_spy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(faux_title); + addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId); + expect(svg_insert_spy).not.toHaveBeenCalledWith('desc', ':first-child'); + }); + }); + }); + + describe('no a11y title', () => { + const a11yTitle = undefined; + + describe('given an a11y description', () => { + const a11yDesc = 'a11y description'; + + it('no title tag inserted', () => { + const faux_title = new MockedD3(); + const svg_insert_spy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(faux_title); + addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId); + expect(svg_insert_spy).not.toHaveBeenCalledWith('title', ':first-child'); + }); + + it('sets aria-labelledby to the description id inserted as a child', () => { + // @ts-ignore Required to easily handle the d3 select types + const svg_attr_spy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode); + addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId); + expect(svg_attr_spy).toHaveBeenCalledWith('aria-labelledby', `chart-desc-${givenId}`); + }); + + it('inserts a desc tag as a child with the text set to accDescription given', () => { + const faux_desc = new MockedD3(); + const svg_insert_spy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(faux_desc); + // @ts-ignore Required to easily handle the d3 select types + const desc_attr_spy = vi.spyOn(faux_desc, 'attr').mockReturnValue(faux_desc); + const desc_text_spy = vi.spyOn(faux_desc, 'text'); + + addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId); + expect(svg_insert_spy).toHaveBeenCalledWith('desc', ':first-child'); + expect(desc_attr_spy).toHaveBeenCalledWith('id', `chart-desc-` + givenId); + expect(desc_text_spy).toHaveBeenNthCalledWith(1, 'a11y description'); + }); + }); + + describe('no a11y description', () => { + const a11yDesc = undefined; + + it('no title tag inserted', () => { + const faux_title = new MockedD3(); + const svg_insert_spy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(faux_title); + addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId); + expect(svg_insert_spy).not.toHaveBeenCalledWith('title', ':first-child'); + }); + + it('no description tag inserted', () => { + const faux_desc = new MockedD3(); + const svg_insert_spy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(faux_desc); + addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId); + expect(svg_insert_spy).not.toHaveBeenCalledWith('desc', ':first-child'); + }); + + it('no aria-labelledby is set', () => { + // @ts-ignore Required to easily handle the d3 select types + const svg_attr_spy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode); + addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId); + expect(svg_attr_spy).not.toHaveBeenCalled(); + }); + }); + }); }); }); }); diff --git a/packages/mermaid/src/accessibility.ts b/packages/mermaid/src/accessibility.ts index 246a88f66..eff9a4edc 100644 --- a/packages/mermaid/src/accessibility.ts +++ b/packages/mermaid/src/accessibility.ts @@ -3,43 +3,54 @@ * */ -// This is just a convenience alias to make it clear the type is a d3 object. (It's easier to make it 'any' instead of the comple typing set in d3) +import { isEmpty, compact } from 'lodash'; + +// This is just a convenience alias to make it clear the type is a d3 object. (It's easier to make it 'any' instead of the complete typing set in d3) type D3object = any; /** - * Set the accessibility (a11y) information for the svg d3 object using the given diagram type - * Note that the svg element role _should_ be mapped to a 'graphics-document' by default. Thus we don't set it here, but can set it in the future if needed. + * Add aria-roledescription to the svg element to the diagramType + * * @param svg - d3 object that contains the SVG HTML element * @param diagramType - diagram name for to the aria-roledescription */ -export function setA11yDiagramInfo(svg: D3object, diagramType: string) { - svg.attr('aria-roledescription', diagramType); +export function setA11yDiagramInfo(svg: D3object, diagramType: string | null | undefined) { + if (!isEmpty(diagramType)) { + svg.attr('aria-roledescription', diagramType); + } } - /** - * This method will add a basic title and description element to a chart. The yy parser will need to - * respond to getAccTitle and getAccDescription, - * where the accessible title is the title element on the chart. + * Add an accessible title and/or description element to a chart. + * The title is usually not displayed and the description is never displayed. * - * Note that the accessible title is generally _not_ displayed - * and the accessible description is never displayed. + * The following charts display their title as a visual and accessibility element: gantt * - * - * The following charts display their title as a visual and accessibility element: gantt. TODO fix this - * - * @param diagramDb - the 'db' object/module for a diagram. Must respond to getAccTitle() and getAccDescription() - * @param svg - the d3 object that represents the svg element - * @param baseId - the id to use as the base for the title and description + * @param svg - d3 node to insert the a11y title and desc info + * @param a11yTitle - a11y title. null and undefined are meaningful: means to skip it + * @param a11yDesc - a11y description. null and undefined are meaningful: means to skip it + * @param baseId - id used to construct the a11y title and description id */ -export function addSVGa11yTitleDescription(diagramDb: any, svg: D3object, baseId: string) { +export function addSVGa11yTitleDescription( + svg: D3object, + a11yTitle: string | null | undefined, + a11yDesc: string | null | undefined, + baseId: string +) { if (typeof svg.insert === 'undefined') { return; } - const titleId = 'chart-title-' + baseId; - const descId = 'chart-desc-' + baseId; - - svg.attr('aria-labelledby', titleId + ' ' + descId); - svg.insert('desc', ':first-child').attr('id', descId).text(diagramDb.getAccDescription()); - svg.insert('title', ':first-child').attr('id', titleId).text(diagramDb.getAccTitle()); + const titleId = a11yTitle ? 'chart-title-' + baseId : null; + const descId = a11yDesc ? 'chart-desc-' + baseId : null; + if (a11yTitle || a11yDesc) { + svg.attr('aria-labelledby', compact([titleId, descId]).join(' ')); + if (a11yDesc) { + svg.insert('desc', ':first-child').attr('id', descId).text(a11yDesc); + } + if (a11yTitle) { + svg.insert('title', ':first-child').attr('id', titleId).text(a11yTitle); + } + } else { + return; + } }