Merge pull request #4551 from Yokozuna59/refactor-accessibility

refactor accessibility
This commit is contained in:
Sidharth Vinod
2023-07-03 15:07:21 +00:00
committed by GitHub
6 changed files with 111 additions and 177 deletions

View File

@@ -96,7 +96,7 @@ mermaid.initialize(config);
#### Defined in #### Defined in
[mermaidAPI.ts:663](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L663) [mermaidAPI.ts:667](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L667)
## Functions ## Functions

View File

@@ -1,27 +1,24 @@
import { MockedD3 } from './tests/MockedD3.js'; import { MockedD3 } from './tests/MockedD3.js';
import { setA11yDiagramInfo, addSVGa11yTitleDescription } from './accessibility.js'; import { setA11yDiagramInfo, addSVGa11yTitleDescription } from './accessibility.js';
import { D3Element } from './mermaidAPI.js'; import type { D3Element } from './mermaidAPI.js';
describe('accessibility', () => { describe('accessibility', () => {
const fauxSvgNode = new MockedD3(); const fauxSvgNode: MockedD3 = new MockedD3();
describe('setA11yDiagramInfo', () => { describe('setA11yDiagramInfo', () => {
it('sets the svg element role to "graphics-document document"', () => { it('should set svg element role to "graphics-document document"', () => {
// @ts-ignore Required to easily handle the d3 select types
const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode); const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
setA11yDiagramInfo(fauxSvgNode, 'flowchart'); setA11yDiagramInfo(fauxSvgNode, 'flowchart');
expect(svgAttrSpy).toHaveBeenCalledWith('role', 'graphics-document document'); expect(svgAttrSpy).toHaveBeenCalledWith('role', 'graphics-document document');
}); });
it('sets the aria-roledescription to the diagram type', () => { it('should set aria-roledescription to the diagram type', () => {
// @ts-ignore Required to easily handle the d3 select types
const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode); const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
setA11yDiagramInfo(fauxSvgNode, 'flowchart'); setA11yDiagramInfo(fauxSvgNode, 'flowchart');
expect(svgAttrSpy).toHaveBeenCalledWith('aria-roledescription', 'flowchart'); expect(svgAttrSpy).toHaveBeenCalledWith('aria-roledescription', 'flowchart');
}); });
it('does not set the aria-roledescription if the diagram type is empty', () => { it('should not set aria-roledescription if the diagram type is empty', () => {
// @ts-ignore Required to easily handle the d3 select types
const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode); const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
setA11yDiagramInfo(fauxSvgNode, ''); setA11yDiagramInfo(fauxSvgNode, '');
expect(svgAttrSpy).toHaveBeenCalledTimes(1); expect(svgAttrSpy).toHaveBeenCalledTimes(1);
@@ -32,8 +29,8 @@ describe('accessibility', () => {
describe('addSVGa11yTitleDescription', () => { describe('addSVGa11yTitleDescription', () => {
const givenId = 'theBaseId'; const givenId = 'theBaseId';
describe('with the given svg d3 object:', () => { describe('with svg d3 object', () => {
it('does nothing if there is no insert defined', () => { it('should do nothing if there is no insert defined', () => {
const noInsertSvg = { const noInsertSvg = {
attr: vi.fn(), attr: vi.fn(),
}; };
@@ -42,26 +39,25 @@ describe('accessibility', () => {
expect(noInsertAttrSpy).not.toHaveBeenCalled(); expect(noInsertAttrSpy).not.toHaveBeenCalled();
}); });
// ---------------- // convenience functions to DRY up the spec
// Convenience functions to DRY up the spec
function expectAriaLabelledByIsTitleId( function expectAriaLabelledByItTitleId(
svgD3Node: D3Element, svgD3Node: D3Element,
title: string | null | undefined, title: string | undefined,
desc: string | null | undefined, desc: string | undefined,
givenId: string givenId: string
) { ): void {
const svgAttrSpy = vi.spyOn(svgD3Node, 'attr').mockReturnValue(svgD3Node); const svgAttrSpy = vi.spyOn(svgD3Node, 'attr').mockReturnValue(svgD3Node);
addSVGa11yTitleDescription(svgD3Node, title, desc, givenId); addSVGa11yTitleDescription(svgD3Node, title, desc, givenId);
expect(svgAttrSpy).toHaveBeenCalledWith('aria-labelledby', `chart-title-${givenId}`); expect(svgAttrSpy).toHaveBeenCalledWith('aria-labelledby', `chart-title-${givenId}`);
} }
function expectAriaDescribedByIsDescId( function expectAriaDescribedByItDescId(
svgD3Node: D3Element, svgD3Node: D3Element,
title: string | null | undefined, title: string | undefined,
desc: string | null | undefined, desc: string | undefined,
givenId: string givenId: string
) { ): void {
const svgAttrSpy = vi.spyOn(svgD3Node, 'attr').mockReturnValue(svgD3Node); const svgAttrSpy = vi.spyOn(svgD3Node, 'attr').mockReturnValue(svgD3Node);
addSVGa11yTitleDescription(svgD3Node, title, desc, givenId); addSVGa11yTitleDescription(svgD3Node, title, desc, givenId);
expect(svgAttrSpy).toHaveBeenCalledWith('aria-describedby', `chart-desc-${givenId}`); expect(svgAttrSpy).toHaveBeenCalledWith('aria-describedby', `chart-desc-${givenId}`);
@@ -69,154 +65,148 @@ describe('accessibility', () => {
function a11yTitleTagInserted( function a11yTitleTagInserted(
svgD3Node: D3Element, svgD3Node: D3Element,
title: string | null | undefined, title: string | undefined,
desc: string | null | undefined, desc: string | undefined,
givenId: string, givenId: string,
callNumber: number callNumber: number
) { ): void {
a11yTagInserted(svgD3Node, title, desc, givenId, callNumber, 'title', title); a11yTagInserted(svgD3Node, title, desc, givenId, callNumber, 'title', title);
} }
function a11yDescTagInserted( function a11yDescTagInserted(
svgD3Node: D3Element, svgD3Node: D3Element,
title: string | null | undefined, title: string | undefined,
desc: string | null | undefined, desc: string | undefined,
givenId: string, givenId: string,
callNumber: number callNumber: number
) { ): void {
a11yTagInserted(svgD3Node, title, desc, givenId, callNumber, 'desc', desc); a11yTagInserted(svgD3Node, title, desc, givenId, callNumber, 'desc', desc);
} }
function a11yTagInserted( function a11yTagInserted(
svgD3Node: D3Element, _svgD3Node: D3Element,
title: string | null | undefined, title: string | undefined,
desc: string | null | undefined, desc: string | undefined,
givenId: string, givenId: string,
callNumber: number, callNumber: number,
expectedPrefix: string, expectedPrefix: string,
expectedText: string | null | undefined expectedText: string | undefined
) { ): void {
const fauxInsertedD3 = new MockedD3(); const fauxInsertedD3: MockedD3 = new MockedD3();
const svgInsertSpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxInsertedD3); const svginsertpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxInsertedD3);
// @ts-ignore Required to easily handle the d3 select types
const titleAttrSpy = vi.spyOn(fauxInsertedD3, 'attr').mockReturnValue(fauxInsertedD3); const titleAttrSpy = vi.spyOn(fauxInsertedD3, 'attr').mockReturnValue(fauxInsertedD3);
const titleTextSpy = vi.spyOn(fauxInsertedD3, 'text'); const titleTextSpy = vi.spyOn(fauxInsertedD3, 'text');
addSVGa11yTitleDescription(fauxSvgNode, title, desc, givenId); addSVGa11yTitleDescription(fauxSvgNode, title, desc, givenId);
expect(svgInsertSpy).toHaveBeenCalledWith(expectedPrefix, ':first-child'); expect(svginsertpy).toHaveBeenCalledWith(expectedPrefix, ':first-child');
expect(titleAttrSpy).toHaveBeenCalledWith('id', `chart-${expectedPrefix}-${givenId}`); expect(titleAttrSpy).toHaveBeenCalledWith('id', `chart-${expectedPrefix}-${givenId}`);
expect(titleTextSpy).toHaveBeenNthCalledWith(callNumber, expectedText); expect(titleTextSpy).toHaveBeenNthCalledWith(callNumber, expectedText);
} }
// ----------------
describe('given an a11y title', () => { describe('with a11y title', () => {
const a11yTitle = 'a11y title'; const a11yTitle = 'a11y title';
describe('given an a11y description', () => { describe('with a11y description', () => {
const a11yDesc = 'a11y description'; const a11yDesc = 'a11y description';
it('sets aria-labelledby to the title id inserted as a child', () => { it('shold set aria-labelledby to the title id inserted as a child', () => {
expectAriaLabelledByIsTitleId(fauxSvgNode, a11yTitle, a11yDesc, givenId); expectAriaLabelledByItTitleId(fauxSvgNode, a11yTitle, a11yDesc, givenId);
}); });
it('sets aria-describedby to the description id inserted as a child', () => { it('should set aria-describedby to the description id inserted as a child', () => {
expectAriaDescribedByIsDescId(fauxSvgNode, a11yTitle, a11yDesc, givenId); expectAriaDescribedByItDescId(fauxSvgNode, a11yTitle, a11yDesc, givenId);
}); });
it('inserts a title tag as the first child with the text set to the accTitle given', () => { it('should insert title tag as the first child with the text set to the accTitle given', () => {
a11yTitleTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 2); a11yTitleTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 2);
}); });
it('inserts a desc tag as the 2nd child with the text set to accDescription given', () => { it('should insert desc tag as the 2nd child with the text set to accDescription given', () => {
a11yDescTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 1); a11yDescTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 1);
}); });
}); });
describe(`no a11y description`, () => { describe(`without a11y description`, () => {
const a11yDesc = undefined; const a11yDesc = undefined;
it('sets aria-labelledby to the title id inserted as a child', () => { it('should set aria-labelledby to the title id inserted as a child', () => {
expectAriaLabelledByIsTitleId(fauxSvgNode, a11yTitle, a11yDesc, givenId); expectAriaLabelledByItTitleId(fauxSvgNode, a11yTitle, a11yDesc, givenId);
}); });
it('no aria-describedby is set', () => { it('should not set aria-describedby', () => {
// @ts-ignore Required to easily handle the d3 select types
const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode); const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId); addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
expect(svgAttrSpy).not.toHaveBeenCalledWith('aria-describedby', expect.anything()); expect(svgAttrSpy).not.toHaveBeenCalledWith('aria-describedby', expect.anything());
}); });
it('inserts a title tag as the first child with the text set to the accTitle given', () => { it('should insert title tag as the first child with the text set to the accTitle given', () => {
a11yTitleTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 1); a11yTitleTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 1);
}); });
it('no description tag is inserted', () => { it('should not insert description tag', () => {
const fauxTitle = new MockedD3(); const fauxTitle: MockedD3 = new MockedD3();
const svgInsertSpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxTitle); const svginsertpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxTitle);
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId); addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
expect(svgInsertSpy).not.toHaveBeenCalledWith('desc', ':first-child'); expect(svginsertpy).not.toHaveBeenCalledWith('desc', ':first-child');
}); });
}); });
}); });
describe('no a11y title', () => { describe('without a11y title', () => {
const a11yTitle = undefined; const a11yTitle = undefined;
describe('given an a11y description', () => { describe('with a11y description', () => {
const a11yDesc = 'a11y description'; const a11yDesc = 'a11y description';
it('no aria-labelledby is set', () => { it('should not set aria-labelledby', () => {
// @ts-ignore Required to easily handle the d3 select types
const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode); const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId); addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
expect(svgAttrSpy).not.toHaveBeenCalledWith('aria-labelledby', expect.anything()); expect(svgAttrSpy).not.toHaveBeenCalledWith('aria-labelledby', expect.anything());
}); });
it('no title tag inserted', () => { it('should not insert title tag', () => {
const fauxTitle = new MockedD3(); const fauxTitle: MockedD3 = new MockedD3();
const svgInsertSpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxTitle); const svginsertpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxTitle);
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId); addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
expect(svgInsertSpy).not.toHaveBeenCalledWith('title', ':first-child'); expect(svginsertpy).not.toHaveBeenCalledWith('title', ':first-child');
}); });
it('sets aria-describedby to the description id inserted as a child', () => { it('should set aria-describedby to the description id inserted as a child', () => {
expectAriaDescribedByIsDescId(fauxSvgNode, a11yTitle, a11yDesc, givenId); expectAriaDescribedByItDescId(fauxSvgNode, a11yTitle, a11yDesc, givenId);
}); });
it('inserts a desc tag as the 2nd child with the text set to accDescription given', () => { it('should insert desc tag as the 2nd child with the text set to accDescription given', () => {
a11yDescTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 1); a11yDescTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 1);
}); });
}); });
describe('no a11y description', () => { describe('without a11y description', () => {
const a11yDesc = undefined; const a11yDesc = undefined;
it('no aria-labelledby is set', () => { it('should not set aria-labelledby', () => {
// @ts-ignore Required to easily handle the d3 select types
const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode); const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId); addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
expect(svgAttrSpy).not.toHaveBeenCalledWith('aria-labelledby', expect.anything()); expect(svgAttrSpy).not.toHaveBeenCalledWith('aria-labelledby', expect.anything());
}); });
it('no aria-describedby is set', () => { it('should not set aria-describedby', () => {
// @ts-ignore Required to easily handle the d3 select types
const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode); const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId); addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
expect(svgAttrSpy).not.toHaveBeenCalledWith('aria-describedby', expect.anything()); expect(svgAttrSpy).not.toHaveBeenCalledWith('aria-describedby', expect.anything());
}); });
it('no title tag inserted', () => { it('should not insert title tag', () => {
const fauxTitle = new MockedD3(); const fauxTitle: MockedD3 = new MockedD3();
const svgInsertSpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxTitle); const svginsertpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxTitle);
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId); addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
expect(svgInsertSpy).not.toHaveBeenCalledWith('title', ':first-child'); expect(svginsertpy).not.toHaveBeenCalledWith('title', ':first-child');
}); });
it('no description tag inserted', () => { it('should not insert description tag', () => {
const fauxDesc = new MockedD3(); const fauxDesc: MockedD3 = new MockedD3();
const svgInsertSpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxDesc); const svginsertpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxDesc);
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId); addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
expect(svgInsertSpy).not.toHaveBeenCalledWith('desc', ':first-child'); expect(svginsertpy).not.toHaveBeenCalledWith('desc', ':first-child');
}); });
}); });
}); });

View File

@@ -1,13 +1,11 @@
/** /**
* Accessibility (a11y) functions, types, helpers * Accessibility (a11y) functions, types, helpers.
*
* @see https://www.w3.org/WAI/ * @see https://www.w3.org/WAI/
* @see https://www.w3.org/TR/wai-aria-1.1/ * @see https://www.w3.org/TR/wai-aria-1.1/
* @see https://www.w3.org/TR/svg-aam-1.0/ * @see https://www.w3.org/TR/svg-aam-1.0/
*
*/ */
import { D3Element } from './mermaidAPI.js'; import type { D3Element } from './mermaidAPI.js';
import isEmpty from 'lodash-es/isEmpty.js';
/** /**
* SVG element role: * SVG element role:
@@ -21,50 +19,47 @@ import isEmpty from 'lodash-es/isEmpty.js';
const SVG_ROLE = 'graphics-document document'; const SVG_ROLE = 'graphics-document document';
/** /**
* Add role and aria-roledescription to the svg element * Add role and aria-roledescription to the svg element.
* *
* @param svg - d3 object that contains the SVG HTML element * @param svg - d3 object that contains the SVG HTML element
* @param diagramType - diagram name for to the aria-roledescription * @param diagramType - diagram name for to the aria-roledescription
*/ */
export function setA11yDiagramInfo(svg: D3Element, diagramType: string | null | undefined) { export function setA11yDiagramInfo(svg: D3Element, diagramType: string) {
svg.attr('role', SVG_ROLE); svg.attr('role', SVG_ROLE);
if (!isEmpty(diagramType)) { if (diagramType !== '') {
svg.attr('aria-roledescription', diagramType); svg.attr('aria-roledescription', diagramType);
} }
} }
/** /**
* Add an accessible title and/or description element to a chart. * Add an accessible title and/or description element to a chart.
* The title is usually not displayed and the description is never displayed. * The title is usually not displayed and the 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.
* *
* @param svg - d3 node to insert the a11y title and desc info * @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 a11yTitle - a11y title. undefined or empty strings mean to skip them
* @param a11yDesc - a11y description. null and undefined are meaningful: means to skip it * @param a11yDesc - a11y description. undefined or empty strings mean to skip them
* @param baseId - id used to construct the a11y title and description id * @param baseId - id used to construct the a11y title and description id
*/ */
export function addSVGa11yTitleDescription( export function addSVGa11yTitleDescription(
svg: D3Element, svg: D3Element,
a11yTitle: string | null | undefined, a11yTitle: string | undefined,
a11yDesc: string | null | undefined, a11yDesc: string | undefined,
baseId: string baseId: string
) { ): void {
if (svg.insert === undefined) { if (svg.insert === undefined) {
return; return;
} }
if (a11yTitle || a11yDesc) {
if (a11yDesc) { if (a11yDesc) {
const descId = 'chart-desc-' + baseId; const descId = `chart-desc-${baseId}`;
svg.attr('aria-describedby', descId); svg.attr('aria-describedby', descId);
svg.insert('desc', ':first-child').attr('id', descId).text(a11yDesc); svg.insert('desc', ':first-child').attr('id', descId).text(a11yDesc);
} }
if (a11yTitle) { if (a11yTitle) {
const titleId = 'chart-title-' + baseId; const titleId = `chart-title-${baseId}`;
svg.attr('aria-labelledby', titleId); svg.attr('aria-labelledby', titleId);
svg.insert('title', ':first-child').attr('id', titleId).text(a11yTitle); svg.insert('title', ':first-child').attr('id', titleId).text(a11yTitle);
} }
} else {
return;
}
} }

View File

@@ -733,59 +733,7 @@ describe('mermaidAPI', () => {
const diagramText = `${diagramType}\n accTitle: ${a11yTitle}\n accDescr: ${a11yDescr}\n`; const diagramText = `${diagramType}\n accTitle: ${a11yTitle}\n accDescr: ${a11yDescr}\n`;
const expectedDiagramType = testedDiagram.expectedType; const expectedDiagramType = testedDiagram.expectedType;
it('aria-roledscription is set to the diagram type, addSVGa11yTitleDescription is called', async () => { it('should set aria-roledscription to the diagram type AND should call addSVGa11yTitleDescription', async () => {
const a11yDiagramInfo_spy = vi.spyOn(accessibility, 'setA11yDiagramInfo');
const a11yTitleDesc_spy = vi.spyOn(accessibility, 'addSVGa11yTitleDescription');
await mermaidAPI.render(id, diagramText);
expect(a11yDiagramInfo_spy).toHaveBeenCalledWith(
expect.anything(),
expectedDiagramType
);
expect(a11yTitleDesc_spy).toHaveBeenCalled();
});
});
});
});
});
describe('render', () => {
// Be sure to add async before each test (anonymous) method
// These are more like integration tests right now because nothing is mocked.
// But it is faster that a cypress test and there's no real reason to actually evaluate an image pixel by pixel.
// render(id, text, cb?, svgContainingElement?)
// Test all diagram types. Note that old flowchart 'graph' type will invoke the flowRenderer-v2. (See the flowchart v2 detector.)
// We have to have both the specific textDiagramType and the expected type name because the expected type may be slightly different than was is put in the diagram text (ex: in -v2 diagrams)
const diagramTypesAndExpectations = [
{ textDiagramType: 'C4Context', expectedType: 'c4' },
{ textDiagramType: 'classDiagram', expectedType: 'classDiagram' },
{ textDiagramType: 'classDiagram-v2', expectedType: 'classDiagram' },
{ textDiagramType: 'erDiagram', expectedType: 'er' },
{ textDiagramType: 'graph', expectedType: 'flowchart-v2' },
{ textDiagramType: 'flowchart', expectedType: 'flowchart-v2' },
{ textDiagramType: 'gitGraph', expectedType: 'gitGraph' },
{ textDiagramType: 'gantt', expectedType: 'gantt' },
{ textDiagramType: 'journey', expectedType: 'journey' },
{ textDiagramType: 'pie', expectedType: 'pie' },
{ textDiagramType: 'requirementDiagram', expectedType: 'requirement' },
{ textDiagramType: 'sequenceDiagram', expectedType: 'sequence' },
{ textDiagramType: 'stateDiagram-v2', expectedType: 'stateDiagram' },
];
describe('accessibility', () => {
const id = 'mermaid-fauxId';
const a11yTitle = 'a11y title';
const a11yDescr = 'a11y description';
diagramTypesAndExpectations.forEach((testedDiagram) => {
describe(`${testedDiagram.textDiagramType}`, () => {
const diagramType = testedDiagram.textDiagramType;
const diagramText = `${diagramType}\n accTitle: ${a11yTitle}\n accDescr: ${a11yDescr}\n`;
const expectedDiagramType = testedDiagram.expectedType;
it('aria-roledscription is set to the diagram type, addSVGa11yTitleDescription is called', async () => {
const a11yDiagramInfo_spy = vi.spyOn(accessibility, 'setA11yDiagramInfo'); const a11yDiagramInfo_spy = vi.spyOn(accessibility, 'setA11yDiagramInfo');
const a11yTitleDesc_spy = vi.spyOn(accessibility, 'addSVGa11yTitleDescription'); const a11yTitleDesc_spy = vi.spyOn(accessibility, 'addSVGa11yTitleDescription');
await mermaidAPI.render(id, diagramText); await mermaidAPI.render(id, diagramText);

View File

@@ -478,7 +478,7 @@ const render = async function (
// Get the temporary div element containing the svg // Get the temporary div element containing the svg
const element = root.select(enclosingDivID_selector).node(); const element = root.select(enclosingDivID_selector).node();
const graphType = diag.type; const diagramType = diag.type;
// ------------------------------------------------------------------------------- // -------------------------------------------------------------------------------
// Create and insert the styles (user styles, theme styles, config styles) // Create and insert the styles (user styles, theme styles, config styles)
@@ -486,11 +486,11 @@ const render = async function (
// Insert an element into svg. This is where we put the styles // Insert an element into svg. This is where we put the styles
const svg = element.firstChild; const svg = element.firstChild;
const firstChild = svg.firstChild; const firstChild = svg.firstChild;
const diagramClassDefs = CLASSDEF_DIAGRAMS.includes(graphType) const diagramClassDefs = CLASSDEF_DIAGRAMS.includes(diagramType)
? diag.renderer.getClasses(text, diag) ? diag.renderer.getClasses(text, diag)
: {}; : {};
const rules = createUserStyles(config, graphType, diagramClassDefs, idSelector); const rules = createUserStyles(config, diagramType, diagramClassDefs, idSelector);
const style1 = document.createElement('style'); const style1 = document.createElement('style');
style1.innerHTML = rules; style1.innerHTML = rules;
@@ -507,9 +507,9 @@ const render = async function (
// This is the d3 node for the svg element // This is the d3 node for the svg element
const svgNode = root.select(`${enclosingDivID_selector} svg`); const svgNode = root.select(`${enclosingDivID_selector} svg`);
const a11yTitle = diag.db.getAccTitle?.(); const a11yTitle: string | undefined = diag.db.getAccTitle?.();
const a11yDescr = diag.db.getAccDescription?.(); const a11yDescr: string | undefined = diag.db.getAccDescription?.();
addA11yInfo(graphType, svgNode, a11yTitle, a11yDescr); addA11yInfo(diagramType, svgNode, a11yTitle, a11yDescr);
// ------------------------------------------------------------------------------- // -------------------------------------------------------------------------------
// Clean up SVG code // Clean up SVG code
@@ -586,14 +586,18 @@ function initialize(options: MermaidConfig = {}) {
/** /**
* Add accessibility (a11y) information to the diagram. * Add accessibility (a11y) information to the diagram.
* *
* @param diagramType - diagram type
* @param svgNode - d3 node to insert the a11y title and desc info
* @param a11yTitle - a11y title
* @param a11yDescr - a11y description
*/ */
function addA11yInfo( function addA11yInfo(
graphType: string, diagramType: string,
svgNode: D3Element, svgNode: D3Element,
a11yTitle: string | undefined, a11yTitle?: string,
a11yDescr: string | undefined a11yDescr?: string
) { ): void {
setA11yDiagramInfo(svgNode, graphType); setA11yDiagramInfo(svgNode, diagramType);
addSVGa11yTitleDescription(svgNode, a11yTitle, a11yDescr, svgNode.attr('id')); addSVGa11yTitleDescription(svgNode, a11yTitle, a11yDescr, svgNode.attr('id'));
} }

View File

@@ -1,5 +1,3 @@
import type {} from '@vitest/spy/dist/index.js';
/** /**
* This is a mocked/stubbed version of the d3 Selection type. Each of the main functions are all * This is a mocked/stubbed version of the d3 Selection type. Each of the main functions are all
* mocked (via vi.fn()) so you can track if they have been called, etc. * mocked (via vi.fn()) so you can track if they have been called, etc.
@@ -7,9 +5,8 @@ import type {} from '@vitest/spy/dist/index.js';
* Note that node() returns a HTML Element with tag 'svg'. It is an empty element (no innerHTML, no children, etc). * Note that node() returns a HTML Element with tag 'svg'. It is an empty element (no innerHTML, no children, etc).
* This potentially allows testing of mermaidAPI render(). * This potentially allows testing of mermaidAPI render().
*/ */
export class MockedD3 { export class MockedD3 {
public attribs = new Map<string, string | null>(); public attribs = new Map<string, string>();
public id: string | undefined = ''; public id: string | undefined = '';
_children: MockedD3[] = []; _children: MockedD3[] = [];
@@ -72,9 +69,9 @@ export class MockedD3 {
return newMock; return newMock;
}; };
attr(attrName: string): null | undefined | string | number; attr(attrName: string): undefined | string;
// attr(attrName: string, attrValue: string): MockedD3; attr(attrName: string, attrValue: string): MockedD3;
attr(attrName: string, attrValue?: string): null | undefined | string | number | MockedD3 { attr(attrName: string, attrValue?: string): undefined | string | MockedD3 {
if (arguments.length === 1) { if (arguments.length === 1) {
return this.attribs.get(attrName); return this.attribs.get(attrName);
} else { } else {