mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-09-30 04:39:40 +02:00
add error checking (empty diagramType, title, desc) to a11y methods
This commit is contained in:

parent
03a11e103e
commit
4d7496b8dd
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user