diff --git a/__mocks__/c4Renderer.js b/__mocks__/c4Renderer.js new file mode 100644 index 000000000..576d5d863 --- /dev/null +++ b/__mocks__/c4Renderer.js @@ -0,0 +1,21 @@ +/** + * Mocked C4Context diagram renderer + */ + +import { vi } from 'vitest'; + +export const drawPersonOrSystemArray = vi.fn(); +export const drawBoundary = vi.fn(); + +export const setConf = vi.fn(); + +export const draw = vi.fn().mockImplementation(() => { + return ''; +}); + +export default { + drawPersonOrSystemArray, + drawBoundary, + setConf, + draw, +}; diff --git a/__mocks__/classRenderer-v2.js b/__mocks__/classRenderer-v2.js new file mode 100644 index 000000000..1ad95806f --- /dev/null +++ b/__mocks__/classRenderer-v2.js @@ -0,0 +1,16 @@ +/** + * Mocked class diagram v2 renderer + */ + +import { vi } from 'vitest'; + +export const setConf = vi.fn(); + +export const draw = vi.fn().mockImplementation(() => { + return ''; +}); + +export default { + setConf, + draw, +}; diff --git a/__mocks__/classRenderer.js b/__mocks__/classRenderer.js new file mode 100644 index 000000000..1c20de4b1 --- /dev/null +++ b/__mocks__/classRenderer.js @@ -0,0 +1,13 @@ +/** + * Mocked class diagram renderer + */ + +import { vi } from 'vitest'; + +export const draw = vi.fn().mockImplementation(() => { + return ''; +}); + +export default { + draw, +}; diff --git a/__mocks__/d3.ts b/__mocks__/d3.ts index f90d93557..af35020c5 100644 --- a/__mocks__/d3.ts +++ b/__mocks__/d3.ts @@ -1,79 +1,14 @@ // @ts-nocheck TODO: Fix TS -import { vi } from 'vitest'; - -const NewD3 = function () { - /** - * - */ - function returnThis() { - return this; - } - return { - append: function () { - return NewD3(); - }, - lower: returnThis, - attr: returnThis, - style: returnThis, - text: returnThis, - 0: { - 0: { - getBBox: function () { - return { - height: 10, - width: 20, - }; - }, - }, - }, - }; -}; +import { MockedD3 } from '../packages/mermaid/src/tests/MockedD3'; export const select = function () { - return new NewD3(); + return new MockedD3(); }; export const selectAll = function () { - return new NewD3(); + return new MockedD3(); }; export const curveBasis = 'basis'; export const curveLinear = 'linear'; export const curveCardinal = 'cardinal'; - -export const MockD3 = (name, parent) => { - const children = []; - const elem = { - get __children() { - return children; - }, - get __name() { - return name; - }, - get __parent() { - return parent; - }, - node() { - return { - getBBox() { - return { - x: 5, - y: 10, - height: 15, - width: 20, - }; - }, - }; - }, - }; - elem.append = (name) => { - const mockElem = MockD3(name, elem); - children.push(mockElem); - return mockElem; - }; - elem.lower = vi.fn(() => elem); - elem.attr = vi.fn(() => elem); - elem.text = vi.fn(() => elem); - elem.style = vi.fn(() => elem); - return elem; -}; diff --git a/__mocks__/erRenderer.js b/__mocks__/erRenderer.js new file mode 100644 index 000000000..845d641f7 --- /dev/null +++ b/__mocks__/erRenderer.js @@ -0,0 +1,16 @@ +/** + * Mocked er diagram renderer + */ + +import { vi } from 'vitest'; + +export const setConf = vi.fn(); + +export const draw = vi.fn().mockImplementation(() => { + return ''; +}); + +export default { + setConf, + draw, +}; diff --git a/__mocks__/flowRenderer-v2.js b/__mocks__/flowRenderer-v2.js new file mode 100644 index 000000000..89cc86031 --- /dev/null +++ b/__mocks__/flowRenderer-v2.js @@ -0,0 +1,24 @@ +/** + * Mocked flow (flowchart) diagram v2 renderer + */ + +import { vi } from 'vitest'; + +export const setConf = vi.fn(); +export const addVertices = vi.fn(); +export const addEdges = vi.fn(); +export const getClasses = vi.fn().mockImplementation(() => { + return {}; +}); + +export const draw = vi.fn().mockImplementation(() => { + return ''; +}); + +export default { + setConf, + addVertices, + addEdges, + getClasses, + draw, +}; diff --git a/__mocks__/ganttRenderer.js b/__mocks__/ganttRenderer.js new file mode 100644 index 000000000..957249832 --- /dev/null +++ b/__mocks__/ganttRenderer.js @@ -0,0 +1,16 @@ +/** + * Mocked gantt diagram renderer + */ + +import { vi } from 'vitest'; + +export const setConf = vi.fn(); + +export const draw = vi.fn().mockImplementation(() => { + return ''; +}); + +export default { + setConf, + draw, +}; diff --git a/__mocks__/gitGraphRenderer.js b/__mocks__/gitGraphRenderer.js new file mode 100644 index 000000000..1daa82ca4 --- /dev/null +++ b/__mocks__/gitGraphRenderer.js @@ -0,0 +1,13 @@ +/** + * Mocked git (graph) diagram renderer + */ + +import { vi } from 'vitest'; + +export const draw = vi.fn().mockImplementation(() => { + return ''; +}); + +export default { + draw, +}; diff --git a/__mocks__/journeyRenderer.js b/__mocks__/journeyRenderer.js new file mode 100644 index 000000000..2bc77c0b1 --- /dev/null +++ b/__mocks__/journeyRenderer.js @@ -0,0 +1,15 @@ +/** + * Mocked pie (picChart) diagram renderer + */ + +import { vi } from 'vitest'; +export const setConf = vi.fn(); + +export const draw = vi.fn().mockImplementation(() => { + return ''; +}); + +export default { + setConf, + draw, +}; diff --git a/__mocks__/pieRenderer.js b/__mocks__/pieRenderer.js new file mode 100644 index 000000000..317c69901 --- /dev/null +++ b/__mocks__/pieRenderer.js @@ -0,0 +1,13 @@ +/** + * Mocked pie (picChart) diagram renderer + */ + +import { vi } from 'vitest'; + +export const draw = vi.fn().mockImplementation(() => { + return ''; +}); + +export default { + draw, +}; diff --git a/__mocks__/requirementRenderer.js b/__mocks__/requirementRenderer.js new file mode 100644 index 000000000..48d8997ac --- /dev/null +++ b/__mocks__/requirementRenderer.js @@ -0,0 +1,13 @@ +/** + * Mocked requirement diagram renderer + */ + +import { vi } from 'vitest'; + +export const draw = vi.fn().mockImplementation(() => { + return ''; +}); + +export default { + draw, +}; diff --git a/__mocks__/sequenceRenderer.js b/__mocks__/sequenceRenderer.js new file mode 100644 index 000000000..11080c6bb --- /dev/null +++ b/__mocks__/sequenceRenderer.js @@ -0,0 +1,23 @@ +/** + * Mocked sequence diagram renderer + */ + +import { vi } from 'vitest'; + +export const bounds = vi.fn(); +export const drawActors = vi.fn(); +export const drawActorsPopup = vi.fn(); + +export const setConf = vi.fn(); + +export const draw = vi.fn().mockImplementation(() => { + return ''; +}); + +export default { + bounds, + drawActors, + drawActorsPopup, + setConf, + draw, +}; diff --git a/__mocks__/stateRenderer-v2.js b/__mocks__/stateRenderer-v2.js new file mode 100644 index 000000000..a2d103b50 --- /dev/null +++ b/__mocks__/stateRenderer-v2.js @@ -0,0 +1,22 @@ +/** + * Mocked state diagram v2 renderer + */ + +import { vi } from 'vitest'; + +export const setConf = vi.fn(); +export const getClasses = vi.fn().mockImplementation(() => { + return {}; +}); +export const stateDomId = vi.fn().mockImplementation(() => { + return 'mocked-stateDiagram-stateDomId'; +}); +export const draw = vi.fn().mockImplementation(() => { + return ''; +}); + +export default { + setConf, + getClasses, + draw, +}; diff --git a/cSpell.json b/cSpell.json index 9a1c49a6b..45e7fe99a 100644 --- a/cSpell.json +++ b/cSpell.json @@ -70,6 +70,7 @@ "rect", "rects", "redmine", + "roledescription", "sandboxed", "setupgraphviewbox", "shiki", diff --git a/cypress/integration/rendering/gantt.spec.js b/cypress/integration/rendering/gantt.spec.js index 325cca065..c0156eee3 100644 --- a/cypress/integration/rendering/gantt.spec.js +++ b/cypress/integration/rendering/gantt.spec.js @@ -310,38 +310,6 @@ describe('Gantt diagram', () => { ); }); - it('should render accessibility tags', function () { - const expectedTitle = 'Gantt Diagram'; - const expectedAccDescription = 'Tasks for Q4'; - renderGraph( - ` - gantt - accTitle: ${expectedTitle} - accDescr: ${expectedAccDescription} - dateFormat YYYY-MM-DD - section Section - A task :a1, 2014-01-01, 30d - `, - {} - ); - cy.get('svg').should((svg) => { - const el = svg.get(0); - const children = [...el.children]; - - const titleEl = children.find(function (node) { - return node.tagName === 'title'; - }); - const descriptionEl = children.find(function (node) { - return node.tagName === 'desc'; - }); - - expect(titleEl).to.exist; - expect(titleEl.textContent).to.equal(expectedTitle); - expect(descriptionEl).to.exist; - expect(descriptionEl.textContent).to.equal(expectedAccDescription); - }); - }); - it('should render a gantt diagram with tick is 15 minutes', () => { imgSnapshotTest( ` diff --git a/cypress/integration/rendering/requirement.spec.js b/cypress/integration/rendering/requirement.spec.js index 8a8d188ff..0bf9014bf 100644 --- a/cypress/integration/rendering/requirement.spec.js +++ b/cypress/integration/rendering/requirement.spec.js @@ -46,69 +46,4 @@ describe('Requirement diagram', () => { ); cy.get('svg'); }); - - it('should render accessibility tags', function () { - const expectedTitle = 'Gantt Diagram'; - const expectedAccDescription = 'Tasks for Q4'; - renderGraph( - ` - requirementDiagram - accTitle: ${expectedTitle} - accDescr: ${expectedAccDescription} - - requirement test_req { - id: 1 - text: the test text. - risk: high - verifymethod: test - } - - functionalRequirement test_req2 { - id: 1.1 - text: the second test text. - risk: low - verifymethod: inspection - } - - performanceRequirement test_req3 { - id: 1.2 - text: the third test text. - risk: medium - verifymethod: demonstration - } - - element test_entity { - type: simulation - } - - element test_entity2 { - type: word doc - docRef: reqs/test_entity - } - - - test_entity - satisfies -> test_req2 - test_req - traces -> test_req2 - test_req - contains -> test_req3 - test_req <- copies - test_entity2 - `, - {} - ); - cy.get('svg').should((svg) => { - const el = svg.get(0); - const children = [...el.children]; - - const titleEl = children.find(function (node) { - return node.tagName === 'title'; - }); - const descriptionEl = children.find(function (node) { - return node.tagName === 'desc'; - }); - - expect(titleEl).to.exist; - expect(titleEl.textContent).to.equal(expectedTitle); - expect(descriptionEl).to.exist; - expect(descriptionEl.textContent).to.equal(expectedAccDescription); - }); - }); }); diff --git a/cypress/integration/rendering/stateDiagram-v2.spec.js b/cypress/integration/rendering/stateDiagram-v2.spec.js index 0eca01873..047e240fc 100644 --- a/cypress/integration/rendering/stateDiagram-v2.spec.js +++ b/cypress/integration/rendering/stateDiagram-v2.spec.js @@ -328,7 +328,7 @@ describe('State diagram', () => { } ); }); - it('v2 it should be possibel to use a choice', () => { + it('v2 it should be possible to use a choice', () => { imgSnapshotTest( ` stateDiagram-v2 diff --git a/demos/state.html b/demos/state.html index 3d070f379..7aaa7516a 100644 --- a/demos/state.html +++ b/demos/state.html @@ -124,8 +124,8 @@ title: Very simple diagram
     stateDiagram-v2
       accTitle: very very simple state
-    accDescr: This is a state diagram showing one state
-    State1
+      accDescr: This is a state diagram showing one state
+      State1
     

diff --git a/docs/community/newDiagram.md b/docs/community/newDiagram.md index da86f9838..288af42cd 100644 --- a/docs/community/newDiagram.md +++ b/docs/community/newDiagram.md @@ -14,8 +14,8 @@ This would be to define a jison grammar for the new diagram type. That should st For instance: -- the flowchart starts with the keyword graph. -- the sequence diagram starts with the keyword sequenceDiagram +- the flowchart starts with the keyword _graph_ +- the sequence diagram starts with the keyword _sequenceDiagram_ #### Store data found during parsing @@ -61,6 +61,11 @@ Place the renderer in the diagram folder. ### Step 3: Detection of the new diagram type The second thing to do is to add the capability to detect the new new diagram to type to the detectType in utils.js. The detection should return a key for the new diagram type. +[This key will be used to as the aria roledescription](#aria-roledescription), so it should be a word that clearly describes the diagram type. +For example, if your new diagram use a UML deployment diagram, a good key would be "UMLDeploymentDiagram" because assistive technologies such as a screen reader +would voice that as "U-M-L Deployment diagram." Another good key would be "deploymentDiagram" because that would be voiced as "Deployment Diagram." A bad key would be "deployment" because that would not sufficiently describe the diagram. + +Note that the diagram type key does not have to be the same as the diagram keyword chosen for the [grammar](#grammar), but it is helpful if they are the same. ### Step 4: The final piece - triggering the rendering @@ -168,17 +173,23 @@ It is probably a good idea to keep the handling similar to this in your new diag ## Accessibility -The syntax for adding title and description looks like this: +Mermaid automatically adds the following accessibility information for the diagram SVG HTML element: - accTitle: The title - accDescr: The description +- aria-roledescription +- accessible title +- accessible description - accDescr { - Syntax for a description text - written on multiple lines. - } +### aria-roledescription -In a similar way to the directives the jison syntax are quite similar between the diagrams. +The aria-roledescription is automatically set to [the diagram type](#step-3--detection-of-the-new-diagram-type) and inserted into the SVG element. + +See [the definition of aria-roledescription](https://www.w3.org/TR/wai-aria-1.1/#aria-roledescription) in [the Accessible Rich Internet Applications W3 standard.](https://www.w3.org/WAI/standards-guidelines/aria/) + +### accessible title and description + +The syntax for accessible titles and descriptions is described in [the Accessibility documenation section.](../config/accessibility.md) + +In a similar way to the directives, the jison syntax are quite similar between the diagrams. ```jison @@ -214,18 +225,7 @@ The functions for setting title and description are provided by a common module. clear as commonClear, } from '../../commonDb'; -For rendering the accessibility tags you have again an existing function you can use. - -**In the renderer:** - -```js -import addSVGAccessibilityFields from '../../accessibility'; - -/* ... */ - -// Adds title and description to the flow chart -addSVGAccessibilityFields(parser.yy, svg, id); -``` +The accessibility title and description are inserted into the SVG element in the `render` function in mermaidAPI. ## Theming diff --git a/docs/config/accessibility.md b/docs/config/accessibility.md index 8fa4aa3ac..e5b96670e 100644 --- a/docs/config/accessibility.md +++ b/docs/config/accessibility.md @@ -10,118 +10,169 @@ Now with Mermaid library in much wider use, we have started to work towards more accessible features, based on the feedback from the community. -To begin with, we have added a new feature to Mermaid library, which is to support accessibility options, **Accessibility Title** and **Accessibility Description**. +Adding accessibility means that the rich information communicated by visual diagrams can be made available to those using assistive technologies (and of course to search engines). +[Read more about Accessible Rich Internet Applications and the W3 standards.](https://www.w3.org/WAI/standards-guidelines/aria/) -This support for accessibility options is available for all the diagrams/chart types. Also, we have tired to keep the same format for the accessibility options, so that it is easy to understand and maintain. +Mermaid will automatically insert the [aria-roledescription](#aria-roledescription) and, if provided in the diagram text by the diagram author, the [accessible title and description.](#accessible-title-and-description) -## Defining Accessibility Options +### aria-roledescription -### Single line accessibility values +The [aria-roledescription](https://www.w3.org/TR/wai-aria-1.1/#aria-roledescription) for the SVG HTML element is set to the diagram type key. (Note this may be slightly different than the keyword used for the diagram in the diagram text.) -The diagram authors can now add the accessibility options in the diagram definition, using the `accTitle` and `accDescr` keywords, where each keyword is followed by `:` and the string value for title and description like: +For example: The diagram type key for a state diagram is "stateDiagram". Here (a part of) the HTML of the SVG tag that shows the automatically inserted aria-roledscription set to "stateDiagram". _(Note that some of the SVG attributes and the SVG contents are omitted for clarity.):_ -- `accTitle: "Your Accessibility Title"` or -- `accDescr: "Your Accessibility Description"` +```html + +``` -**When these two options are defined, they will add a corresponding `` and `<desc>` tag in the SVG.** +### Accessible Title and Description -Let us take a look at the following example with a flowchart diagram: +Support for accessible titles and descriptions is available for all diagrams/chart types. We have tried to keep the same keywords and format for all diagrams so that it is easy to understand and maintain. + +The accessible title and description will add `<title>` and `<desc>` elements within the SVG element and the [aria-labelledby](https://www.w3.org/TR/wai-aria/#aria-labelledby) and [aria-describedby](https://www.w3.org/TR/wai-aria/#aria-describedby) attributes in the SVG tag. + +Here is HTML that is generated, showing that the SVG element is labelled by the accessible title (id = `chart-title-mermaid-1668725057758`) +and described by the accessible description (id = `chart-desc-mermaid-1668725057758` ); +and the accessible title element (text = "This is the accessible title") +and the accessible description element (text = "This is an accessible description"). + +_(Note that some of the SVG attributes and the SVG contents are omitted for clarity.)_ + +```html +<svg + aria-labelledby="chart-title-mermaid-1668725057758" + aria-describedby="chart-desc-mermaid-1668725057758" + xmlns="http://www.w3.org/2000/svg" + width="100%" + id="mermaid-1668725057758" +> + <title id="chart-title-mermaid-1668725057758">This is the accessible title + This is an accessible description + +``` + +Details for the syntax follow. + +#### accessible title + +The **accessible title** is specified with the **accTitle** _keyword_, followed by a colon (`:`), and the string value for the title. +The string value ends at the end of the line. (It can only be a single line.) + +Ex: `accTitle: This is a single line title` + +See [the accTitle and accDescr usage examples](#acctitle-and-accdescr-usage-examples) for how this can be used in a diagram and the resulting HTML generated. + +#### accessible description + +An accessible description can be 1 line long (a single line) or many lines long. + +The **single line accessible description** is specified with the **accDescr** _keyword_, followed by a colon (`:`), followed by the string value for the description. + +Ex: `accDescr: This is a single line description.` + +A **multiple line accessible description** _does not have a colon (`:`) after the accDescr keyword_ and is surrounded by curly brackets (`{}`). + +Ex: + + accDescr { The official Bob's Burgers corporate processes that are used + for making very, very big decisions. + This is actually a very simple flow: see the big decision and then make the big decision.} + +See [the accTitle and accDescr usage examples](#acctitle-and-accdescr-usage-examples) for how this can be used in a diagram and the resulting HTML generated. + +#### accTitle and accDescr Usage Examples + +- Flowchart with the accessible title "Big Decisions" and the single-line accessible description "Bob's Burgers process for making big decisions" ```mermaid-example - graph LR - accTitle: Big decisions - accDescr: Flow chart of the decision making process - A[Hard] -->|Text| B(Round) - B --> C{Decision} - C -->|One| D[Result 1] - + graph LR + accTitle: Big Decisions + accDescr: Bob's Burgers process for making big decisions + A[Identify Big Descision] --> B{Make Big Decision} + B --> D[Be done] ``` ```mermaid - graph LR - accTitle: Big decisions - accDescr: Flow chart of the decision making process - A[Hard] -->|Text| B(Round) - B --> C{Decision} - C -->|One| D[Result 1] - + graph LR + accTitle: Big Decisions + accDescr: Bob's Burgers process for making big decisions + A[Identify Big Descision] --> B{Make Big Decision} + B --> D[Be done] ``` -See in the code snippet above, the `accTitle` and `accDescr` are defined in the diagram definition. They result in the following tags in SVG code: +Here is the HTML generated for the SVG element: _(Note that some of the SVG attributes and the SVG contents are omitted for clarity.):_ -![Accessibility options rendered inside SVG](img/accessibility-div-example.png) +```html + + Big decisions + Bob's Burgers process for making big decisions + +``` -### Multi-line Accessibility title/description - -You can also define the accessibility options in a multi-line format, where the keyword is followed by opening curly bracket `{` and then multiple lines, followed by a closing `}`. - -`accTitle: My single line title value` (**_single line format_**) - -vs - -`accDescr: { My multi-line description of the diagram }` (**_multi-line format_**) - -Let us look at it in the following example, with same flowchart: +- Flowchart with the accessible title "Bob's Burger's Making Big Decisions" and the multiple line accessible description "The official Bob's Burgers corporate processes that are used + for making very, very big decisions. + This is actually a very simple flow: identify the big decision and then make the big decision." ```mermaid-example - graph LR - accTitle: Big decisions - + graph LR + accTitle: Bob's Burger's Making Big Decisions accDescr { - My multi-line description - of the diagram - } - - A[Hard] -->|Text| B(Round) - B --> C{Decision} - C -->|One| D[Result 1] - + The official Bob's Burgers corporate processes that are used + for making very, very big decisions. + This is actually a very simple flow: identify the big decision and then make the big decision. + } + A[Identify Big Descision] --> B{Make Big Decision} + B --> D[Be done] ``` ```mermaid - graph LR - accTitle: Big decisions - + graph LR + accTitle: Bob's Burger's Making Big Decisions accDescr { - My multi-line description - of the diagram - } - - A[Hard] -->|Text| B(Round) - B --> C{Decision} - C -->|One| D[Result 1] - + The official Bob's Burgers corporate processes that are used + for making very, very big decisions. + This is actually a very simple flow: identify the big decision and then make the big decision. + } + A[Identify Big Descision] --> B{Make Big Decision} + B --> D[Be done] ``` -See in the code snippet above, the `accTitle` and `accDescr` are defined in the diagram definition. They result in the following tags in SVG code: +Here is the HTML generated for the SVG element: _(Note that some of the SVG attributes and the SVG contents are omitted for clarity.):_ -![Accessibility options rendered inside SVG](img/accessibility-div-example-2.png) - -### Sample Code Snippet for other diagram types - -#### Sequence Diagram - -```mermaid-example - sequenceDiagram - accTitle: My Sequence Diagram - accDescr: My Sequence Diagram Description - - Alice->>John: Hello John, how are you? - John-->>Alice: Great! - Alice-)John: See you later! +```html + + Big decisions + + The official Bob's Burgers corporate processes that are used for making very, very big + decisions. This is actually a very simple flow: identify the big decision and then make the big + decision. + + ``` -```mermaid - sequenceDiagram - accTitle: My Sequence Diagram - accDescr: My Sequence Diagram Description +#### Sample Code Snippets for other diagram types - Alice->>John: Hello John, how are you? - John-->>Alice: Great! - Alice-)John: See you later! -``` - -#### Class Diagram +##### Class Diagram ```mermaid-example classDiagram @@ -139,27 +190,7 @@ See in the code snippet above, the `accTitle` and `accDescr` are defined in the Vehicle <|-- Car ``` -#### State Diagram - -```mermaid-example - stateDiagram - accTitle: My State Diagram - accDescr: My State Diagram Description - - s1 --> s2 - -``` - -```mermaid - stateDiagram - accTitle: My State Diagram - accDescr: My State Diagram Description - - s1 --> s2 - -``` - -#### Entity Relationship Diagram +##### Entity Relationship Diagram ```mermaid-example erDiagram @@ -183,41 +214,7 @@ See in the code snippet above, the `accTitle` and `accDescr` are defined in the ``` -#### User Journey Diagram - -```mermaid-example - journey - accTitle: My User Journey Diagram - accDescr: My User Journey Diagram Description - - title My working day - section Go to work - Make tea: 5: Me - Go upstairs: 3: Me - Do work: 1: Me, Cat - section Go home - Go downstairs: 5: Me - Sit down: 5: Me - -``` - -```mermaid - journey - accTitle: My User Journey Diagram - accDescr: My User Journey Diagram Description - - title My working day - section Go to work - Make tea: 5: Me - Go upstairs: 3: Me - Do work: 1: Me, Cat - section Go home - Go downstairs: 5: Me - Sit down: 5: Me - -``` - -#### Gantt Chart +##### Gantt Chart ```mermaid-example gantt @@ -251,7 +248,45 @@ See in the code snippet above, the `accTitle` and `accDescr` are defined in the ``` -#### Pie Chart +##### Gitgraph + +```mermaid-example + gitGraph + accTitle: My Gitgraph Accessibility Title + accDescr: My Gitgraph Accessibility Description + + commit + commit + branch develop + checkout develop + commit + commit + checkout main + merge develop + commit + commit + +``` + +```mermaid + gitGraph + accTitle: My Gitgraph Accessibility Title + accDescr: My Gitgraph Accessibility Description + + commit + commit + branch develop + checkout develop + commit + commit + checkout main + merge develop + commit + commit + +``` + +##### Pie Chart ```mermaid-example pie @@ -279,7 +314,7 @@ See in the code snippet above, the `accTitle` and `accDescr` are defined in the ``` -#### Requirement Diagram +##### Requirement Diagram ```mermaid-example requirementDiagram @@ -321,40 +356,78 @@ See in the code snippet above, the `accTitle` and `accDescr` are defined in the ``` -#### Gitgraph +##### Sequence Diagram ```mermaid-example - gitGraph - accTitle: My Gitgraph Accessibility Title - accDescr: My Gitgraph Accessibility Description + sequenceDiagram + accTitle: My Sequence Diagram + accDescr: My Sequence Diagram Description - commit - commit - branch develop - checkout develop - commit - commit - checkout main - merge develop - commit - commit + Alice->>John: Hello John, how are you? + John-->>Alice: Great! + Alice-)John: See you later! +``` + +```mermaid + sequenceDiagram + accTitle: My Sequence Diagram + accDescr: My Sequence Diagram Description + + Alice->>John: Hello John, how are you? + John-->>Alice: Great! + Alice-)John: See you later! +``` + +##### State Diagram + +```mermaid-example + stateDiagram + accTitle: My State Diagram + accDescr: My State Diagram Description + + s1 --> s2 ``` ```mermaid - gitGraph - accTitle: My Gitgraph Accessibility Title - accDescr: My Gitgraph Accessibility Description + stateDiagram + accTitle: My State Diagram + accDescr: My State Diagram Description - commit - commit - branch develop - checkout develop - commit - commit - checkout main - merge develop - commit - commit + s1 --> s2 + +``` + +##### User Journey Diagram + +```mermaid-example + journey + accTitle: My User Journey Diagram + accDescr: My User Journey Diagram Description + + title My working day + section Go to work + Make tea: 5: Me + Go upstairs: 3: Me + Do work: 1: Me, Cat + section Go home + Go downstairs: 5: Me + Sit down: 5: Me + +``` + +```mermaid + journey + accTitle: My User Journey Diagram + accDescr: My User Journey Diagram Description + + title My working day + section Go to work + Make tea: 5: Me + Go upstairs: 3: Me + Do work: 1: Me, Cat + section Go home + Go downstairs: 5: Me + Sit down: 5: Me ``` diff --git a/docs/config/setup/modules/mermaidAPI.md b/docs/config/setup/modules/mermaidAPI.md index d24369c3c..1b840dcd3 100644 --- a/docs/config/setup/modules/mermaidAPI.md +++ b/docs/config/setup/modules/mermaidAPI.md @@ -12,6 +12,16 @@ Renames and re-exports [mermaidAPI](mermaidAPI.md#mermaidapi) +## Type Aliases + +### D3Element + +Ƭ **D3Element**: `any` + +#### Defined in + +[mermaidAPI.ts:72](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L72) + ## Variables ### mermaidAPI @@ -80,7 +90,7 @@ mermaid.initialize(config); #### Defined in -[mermaidAPI.ts:939](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L939) +[mermaidAPI.ts:968](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L968) ## Functions @@ -111,7 +121,7 @@ Return the last node appended #### Defined in -[mermaidAPI.ts:284](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L284) +[mermaidAPI.ts:285](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L285) --- @@ -137,7 +147,7 @@ the cleaned up svgCode #### Defined in -[mermaidAPI.ts:235](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L235) +[mermaidAPI.ts:236](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L236) --- @@ -163,7 +173,7 @@ the string with all the user styles #### Defined in -[mermaidAPI.ts:164](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L164) +[mermaidAPI.ts:165](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L165) --- @@ -186,7 +196,7 @@ the string with all the user styles #### Defined in -[mermaidAPI.ts:212](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L212) +[mermaidAPI.ts:213](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L213) --- @@ -213,7 +223,7 @@ with an enclosing block that has each of the cssClasses followed by !important; #### Defined in -[mermaidAPI.ts:148](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L148) +[mermaidAPI.ts:149](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L149) --- @@ -233,7 +243,7 @@ with an enclosing block that has each of the cssClasses followed by !important; #### Defined in -[mermaidAPI.ts:128](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L128) +[mermaidAPI.ts:129](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L129) --- @@ -253,7 +263,7 @@ with an enclosing block that has each of the cssClasses followed by !important; #### Defined in -[mermaidAPI.ts:99](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L99) +[mermaidAPI.ts:100](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L100) --- @@ -279,7 +289,7 @@ Put the svgCode into an iFrame. Return the iFrame code #### Defined in -[mermaidAPI.ts:263](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L263) +[mermaidAPI.ts:264](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L264) --- @@ -305,4 +315,4 @@ Remove any existing elements from the given document #### Defined in -[mermaidAPI.ts:335](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L335) +[mermaidAPI.ts:336](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L336) diff --git a/packages/mermaid/src/accessibility.js b/packages/mermaid/src/accessibility.js deleted file mode 100644 index 4d4837fff..000000000 --- a/packages/mermaid/src/accessibility.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * 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 title is the title element on the chart, - * which is generally not displayed and the accDescription is the description element on the chart, - * which is never displayed. - * - * The following charts display their title as a visual and accessibility element: gantt - * - * @param yy_parser - * @param svg - * @param id - */ -export default function addSVGAccessibilityFields(yy_parser, svg, id) { - if (svg.insert === undefined) { - return; - } - - let title_string = yy_parser.getAccTitle(); - let description = yy_parser.getAccDescription(); - svg.attr('role', 'img').attr('aria-labelledby', 'chart-title-' + id + ' chart-desc-' + id); - svg - .insert('desc', ':first-child') - .attr('id', 'chart-desc-' + id) - .text(description); - svg - .insert('title', ':first-child') - .attr('id', 'chart-title-' + id) - .text(title_string); -} diff --git a/packages/mermaid/src/accessibility.spec.ts b/packages/mermaid/src/accessibility.spec.ts new file mode 100644 index 000000000..c633d0e15 --- /dev/null +++ b/packages/mermaid/src/accessibility.spec.ts @@ -0,0 +1,219 @@ +import { MockedD3 } from './tests/MockedD3'; +import { setA11yDiagramInfo, addSVGa11yTitleDescription } from './accessibility'; +import { D3Element } from './mermaidAPI'; + +describe('accessibility', () => { + const fauxSvgNode = new MockedD3(); + + describe('setA11yDiagramInfo', () => { + it('sets the aria-roledescription to the diagram type', () => { + // @ts-ignore Required to easily handle the d3 select types + const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode); + setA11yDiagramInfo(fauxSvgNode, 'flowchart'); + expect(svgAttrSpy).toHaveBeenCalledWith('aria-roledescription', 'flowchart'); + }); + + it('does nothing if the diagram type is empty', () => { + // @ts-ignore Required to easily handle the d3 select types + const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode); + setA11yDiagramInfo(fauxSvgNode, ''); + expect(svgAttrSpy).not.toHaveBeenCalled(); + }); + }); + + describe('addSVGa11yTitleDescription', () => { + 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 noInsertAttrSpy = vi.spyOn(noInsertSvg, 'attr').mockReturnValue(noInsertSvg); + addSVGa11yTitleDescription(noInsertSvg, 'some title', 'some desc', givenId); + expect(noInsertAttrSpy).not.toHaveBeenCalled(); + }); + + // ---------------- + // Convenience functions to DRY up the spec + + function expectAriaLabelledByIsTitleId( + svgD3Node: D3Element, + title: string | null | undefined, + desc: string | null | undefined, + givenId: string + ) { + // @ts-ignore Required to easily handle the d3 select types + const svgAttrSpy = vi.spyOn(svgD3Node, 'attr').mockReturnValue(svgD3Node); + addSVGa11yTitleDescription(svgD3Node, title, desc, givenId); + expect(svgAttrSpy).toHaveBeenCalledWith('aria-labelledby', `chart-title-${givenId}`); + } + + function expectAriaDescribedByIsDescId( + svgD3Node: D3Element, + title: string | null | undefined, + desc: string | null | undefined, + givenId: string + ) { + // @ts-ignore Required to easily handle the d3 select types + const svgAttrSpy = vi.spyOn(svgD3Node, 'attr').mockReturnValue(svgD3Node); + addSVGa11yTitleDescription(svgD3Node, title, desc, givenId); + expect(svgAttrSpy).toHaveBeenCalledWith('aria-describedby', `chart-desc-${givenId}`); + } + + function a11yTitleTagInserted( + svgD3Node: D3Element, + title: string | null | undefined, + desc: string | null | undefined, + givenId: string, + callNumber: number + ) { + a11yTagInserted(svgD3Node, title, desc, givenId, callNumber, 'title', title); + } + + function a11yDescTagInserted( + svgD3Node: D3Element, + title: string | null | undefined, + desc: string | null | undefined, + givenId: string, + callNumber: number + ) { + a11yTagInserted(svgD3Node, title, desc, givenId, callNumber, 'desc', desc); + } + + function a11yTagInserted( + svgD3Node: D3Element, + title: string | null | undefined, + desc: string | null | undefined, + givenId: string, + callNumber: number, + expectedPrefix: string, + expectedText: string | null | undefined + ) { + const fauxInsertedD3 = new MockedD3(); + const svgInsertSpy = 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 titleTextSpy = vi.spyOn(fauxInsertedD3, 'text'); + + addSVGa11yTitleDescription(fauxSvgNode, title, desc, givenId); + expect(svgInsertSpy).toHaveBeenCalledWith(expectedPrefix, ':first-child'); + expect(titleAttrSpy).toHaveBeenCalledWith('id', `chart-${expectedPrefix}-${givenId}`); + expect(titleTextSpy).toHaveBeenNthCalledWith(callNumber, expectedText); + } + // ---------------- + + 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 inserted as a child', () => { + expectAriaLabelledByIsTitleId(fauxSvgNode, a11yTitle, a11yDesc, givenId); + }); + + it('sets aria-describedby to the description id inserted as a child', () => { + expectAriaDescribedByIsDescId(fauxSvgNode, a11yTitle, a11yDesc, givenId); + }); + + it('inserts a title tag as the first child with the text set to the accTitle given', () => { + a11yTitleTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 2); + }); + + it('inserts a desc tag as the 2nd child with the text set to accDescription given', () => { + a11yDescTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 1); + }); + }); + + describe(`no a11y description`, () => { + const a11yDesc = undefined; + + it('sets aria-labelledby to the title id inserted as a child', () => { + expectAriaLabelledByIsTitleId(fauxSvgNode, a11yTitle, a11yDesc, givenId); + }); + + it('no aria-describedby is set', () => { + // @ts-ignore Required to easily handle the d3 select types + const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode); + addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId); + 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', () => { + a11yTitleTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 1); + }); + + it('no description tag is inserted', () => { + const fauxTitle = new MockedD3(); + const svgInsertSpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxTitle); + addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId); + expect(svgInsertSpy).not.toHaveBeenCalledWith('desc', ':first-child'); + }); + }); + }); + + describe('no a11y title', () => { + const a11yTitle = undefined; + + describe('given an a11y description', () => { + const a11yDesc = 'a11y description'; + + it('no aria-labelledby is set', () => { + // @ts-ignore Required to easily handle the d3 select types + const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode); + addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId); + expect(svgAttrSpy).not.toHaveBeenCalledWith('aria-labelledby', expect.anything()); + }); + + it('no title tag inserted', () => { + const fauxTitle = new MockedD3(); + const svgInsertSpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxTitle); + addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId); + expect(svgInsertSpy).not.toHaveBeenCalledWith('title', ':first-child'); + }); + + it('sets aria-describedby to the description id inserted as a child', () => { + expectAriaDescribedByIsDescId(fauxSvgNode, a11yTitle, a11yDesc, givenId); + }); + + it('inserts a desc tag as the 2nd child with the text set to accDescription given', () => { + a11yDescTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 1); + }); + }); + + describe('no a11y description', () => { + const a11yDesc = undefined; + + it('no aria-labelledby is set', () => { + // @ts-ignore Required to easily handle the d3 select types + const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode); + addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId); + expect(svgAttrSpy).not.toHaveBeenCalledWith('aria-labelledby', expect.anything()); + }); + + it('no aria-describedby is set', () => { + // @ts-ignore Required to easily handle the d3 select types + const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode); + addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId); + expect(svgAttrSpy).not.toHaveBeenCalledWith('aria-describedby', expect.anything()); + }); + + it('no title tag inserted', () => { + const fauxTitle = new MockedD3(); + const svgInsertSpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxTitle); + addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId); + expect(svgInsertSpy).not.toHaveBeenCalledWith('title', ':first-child'); + }); + + it('no description tag inserted', () => { + const fauxDesc = new MockedD3(); + const svgInsertSpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxDesc); + addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId); + expect(svgInsertSpy).not.toHaveBeenCalledWith('desc', ':first-child'); + }); + }); + }); + }); + }); +}); diff --git a/packages/mermaid/src/accessibility.ts b/packages/mermaid/src/accessibility.ts new file mode 100644 index 000000000..b9e088e0b --- /dev/null +++ b/packages/mermaid/src/accessibility.ts @@ -0,0 +1,55 @@ +/** + * Accessibility (a11y) functions, types, helpers + * + */ +import { D3Element } from './mermaidAPI'; + +import isEmpty from 'lodash-es/isEmpty'; + +/** + * 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: D3Element, diagramType: string | null | undefined) { + if (!isEmpty(diagramType)) { + svg.attr('aria-roledescription', diagramType); + } +} +/** + * Add an accessible title and/or description element to a chart. + * 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 + * + * @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( + svg: D3Element, + a11yTitle: string | null | undefined, + a11yDesc: string | null | undefined, + baseId: string +) { + if (svg.insert === undefined) { + return; + } + + if (a11yTitle || a11yDesc) { + if (a11yDesc) { + const descId = 'chart-desc-' + baseId; + svg.attr('aria-describedby', descId); + svg.insert('desc', ':first-child').attr('id', descId).text(a11yDesc); + } + if (a11yTitle) { + const titleId = 'chart-title-' + baseId; + svg.attr('aria-labelledby', titleId); + svg.insert('title', ':first-child').attr('id', titleId).text(a11yTitle); + } + } else { + return; + } +} diff --git a/packages/mermaid/src/diagram-api/types.ts b/packages/mermaid/src/diagram-api/types.ts index 23810d133..3449782e2 100644 --- a/packages/mermaid/src/diagram-api/types.ts +++ b/packages/mermaid/src/diagram-api/types.ts @@ -14,6 +14,8 @@ export interface InjectUtils { export interface DiagramDb { clear?: () => void; setDiagramTitle?: (title: string) => void; + getAccTitle?: () => string; + getAccDescription?: () => string; } export interface DiagramDefinition { diff --git a/packages/mermaid/src/diagrams/c4/c4Renderer.js b/packages/mermaid/src/diagrams/c4/c4Renderer.js index 6490a8e19..a51fe0b6a 100644 --- a/packages/mermaid/src/diagrams/c4/c4Renderer.js +++ b/packages/mermaid/src/diagrams/c4/c4Renderer.js @@ -8,7 +8,6 @@ import * as configApi from '../../config'; import assignWithDepth from '../../assignWithDepth'; import { wrapLabel, calculateTextWidth, calculateTextHeight } from '../../utils'; import { configureSvgSize } from '../../setupGraphViewbox'; -import addSVGAccessibilityFields from '../../accessibility'; let globalBoundaryMaxX = 0, globalBoundaryMaxY = 0; @@ -675,7 +674,6 @@ export const draw = function (_text, id, _version, diagObj) { (height + extraVertForTitle) ); - addSVGAccessibilityFields(parser.yy, diagram, id); log.debug(`models:`, box); }; diff --git a/packages/mermaid/src/diagrams/class/classRenderer-v2.js b/packages/mermaid/src/diagrams/class/classRenderer-v2.js index c4e7e0291..3fecfcb9f 100644 --- a/packages/mermaid/src/diagrams/class/classRenderer-v2.js +++ b/packages/mermaid/src/diagrams/class/classRenderer-v2.js @@ -8,7 +8,6 @@ import { curveLinear } from 'd3'; import { interpolateToCurve, getStylesFromArray } from '../../utils'; import { setupGraphViewbox } from '../../setupGraphViewbox'; import common from '../common/common'; -import addSVGAccessibilityFields from '../../accessibility'; const sanitizeText = (txt) => common.sanitizeText(txt, getConfig()); @@ -451,7 +450,6 @@ export const draw = function (text, id, _version, diagObj) { } } - addSVGAccessibilityFields(diagObj.db, svg, id); // If node has a link, wrap it in an anchor SVG object. // const keys = Object.keys(classes); // keys.forEach(function(key) { diff --git a/packages/mermaid/src/diagrams/class/classRenderer.js b/packages/mermaid/src/diagrams/class/classRenderer.js index c500a73a7..80a7f26e4 100644 --- a/packages/mermaid/src/diagrams/class/classRenderer.js +++ b/packages/mermaid/src/diagrams/class/classRenderer.js @@ -5,7 +5,6 @@ import { log } from '../../logger'; import svgDraw from './svgDraw'; import { configureSvgSize } from '../../setupGraphViewbox'; import { getConfig } from '../../config'; -import addSVGAccessibilityFields from '../../accessibility'; let idCache = {}; const padding = 20; @@ -272,7 +271,6 @@ export const draw = function (text, id, _version, diagObj) { const vBox = `${svgBounds.x - padding} ${svgBounds.y - padding} ${width} ${height}`; log.debug(`viewBox ${vBox}`); diagram.attr('viewBox', vBox); - addSVGAccessibilityFields(diagObj.db, diagram, id); }; export default { diff --git a/packages/mermaid/src/diagrams/er/erRenderer.js b/packages/mermaid/src/diagrams/er/erRenderer.js index 101beebb9..ebf338ae1 100644 --- a/packages/mermaid/src/diagrams/er/erRenderer.js +++ b/packages/mermaid/src/diagrams/er/erRenderer.js @@ -6,7 +6,6 @@ import { log } from '../../logger'; import utils from '../../utils'; import erMarkers from './erMarkers'; import { configureSvgSize } from '../../setupGraphViewbox'; -import addSVGAccessibilityFields from '../../accessibility'; import { parseGenericTypes } from '../common/common'; import { v4 as uuid4 } from 'uuid'; @@ -642,8 +641,6 @@ export const draw = function (text, id, _version, diagObj) { configureSvgSize(svg, height, width, conf.useMaxWidth); svg.attr('viewBox', `${svgBounds.x - padding} ${svgBounds.y - padding} ${width} ${height}`); - - addSVGAccessibilityFields(diagObj.db, svg, id); }; // draw /** diff --git a/packages/mermaid/src/diagrams/er/parser/erDiagram.spec.js b/packages/mermaid/src/diagrams/er/parser/erDiagram.spec.js index eb738fe4b..43bc13e6d 100644 --- a/packages/mermaid/src/diagrams/er/parser/erDiagram.spec.js +++ b/packages/mermaid/src/diagrams/er/parser/erDiagram.spec.js @@ -323,34 +323,34 @@ describe('when parsing ER diagram it...', function () { expect(Object.keys(erDb.getEntities()).length).toBe(1); }); - it('should allow for a accessibility title and description (accDescr)', function () { + describe('accessible title and description', () => { const teacherRole = 'is teacher of'; const line1 = `TEACHER }o--o{ STUDENT : "${teacherRole}"`; - erDiagram.parser.parse( - `erDiagram + it('should allow for a accessibility title and description (accDescr)', function () { + erDiagram.parser.parse( + `erDiagram accTitle: graph title accDescr: this graph is about stuff ${line1}` - ); - expect(erDb.getAccTitle()).toBe('graph title'); - expect(erDb.getAccDescription()).toBe('this graph is about stuff'); - }); + ); + expect(erDb.getAccTitle()).toBe('graph title'); + expect(erDb.getAccDescription()).toBe('this graph is about stuff'); + }); - it('should allow for a accessibility title and multi line description (accDescr)', function () { - const teacherRole = 'is teacher of'; - const line1 = `TEACHER }o--o{ STUDENT : "${teacherRole}"`; - - erDiagram.parser.parse( - `erDiagram + it('parses a multi line description (accDescr)', function () { + erDiagram.parser.parse( + `erDiagram accTitle: graph title - accDescr { - this graph is about stuff - }\n + accDescr { this graph is + about + stuff + }\n ${line1}` - ); - expect(erDb.getAccTitle()).toBe('graph title'); - expect(erDb.getAccDescription()).toBe('this graph is about stuff'); + ); + expect(erDb.getAccTitle()).toEqual('graph title'); + expect(erDb.getAccDescription()).toEqual('this graph is\nabout\nstuff'); + }); }); it('should allow more than one relationship between the same two entities', function () { diff --git a/packages/mermaid/src/diagrams/flowchart/flowRenderer-v2.js b/packages/mermaid/src/diagrams/flowchart/flowRenderer-v2.js index be3fffa0c..3e46aaaa5 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowRenderer-v2.js +++ b/packages/mermaid/src/diagrams/flowchart/flowRenderer-v2.js @@ -11,7 +11,6 @@ import { log } from '../../logger'; import common, { evaluate } from '../common/common'; import { interpolateToCurve, getStylesFromArray } from '../../utils'; import { setupGraphViewbox } from '../../setupGraphViewbox'; -import addSVGAccessibilityFields from '../../accessibility'; const conf = {}; export const setConf = function (cnf) { @@ -431,9 +430,6 @@ export const draw = function (text, id, _version, diagObj) { // Set up an SVG group so that we can translate the final graph. const svg = root.select(`[id="${id}"]`); - // Adds title and description to the flow chart - addSVGAccessibilityFields(diagObj.db, svg, id); - // Run the renderer. This is what draws the final graph. const element = root.select('#' + id + ' g'); render(element, g, ['point', 'circle', 'cross'], 'flowchart', id); diff --git a/packages/mermaid/src/diagrams/flowchart/flowRenderer.js b/packages/mermaid/src/diagrams/flowchart/flowRenderer.js index 4b3232189..6cbc65532 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowRenderer.js +++ b/packages/mermaid/src/diagrams/flowchart/flowRenderer.js @@ -9,7 +9,6 @@ import common, { evaluate } from '../common/common'; import { interpolateToCurve, getStylesFromArray } from '../../utils'; import { setupGraphViewbox } from '../../setupGraphViewbox'; import flowChartShapes from './flowChartShapes'; -import addSVGAccessibilityFields from '../../accessibility'; const conf = {}; export const setConf = function (cnf) { @@ -417,9 +416,6 @@ export const draw = function (text, id, _version, diagObj) { // Set up an SVG group so that we can translate the final graph. const svg = root.select(`[id="${id}"]`); - // Adds title and description to the flow chart - addSVGAccessibilityFields(diagObj.db, svg, id); - // Run the renderer. This is what draws the final graph. const element = root.select('#' + id + ' g'); render(element, g); diff --git a/packages/mermaid/src/diagrams/gantt/ganttRenderer.js b/packages/mermaid/src/diagrams/gantt/ganttRenderer.js index ab2407ecd..faec35a86 100644 --- a/packages/mermaid/src/diagrams/gantt/ganttRenderer.js +++ b/packages/mermaid/src/diagrams/gantt/ganttRenderer.js @@ -19,7 +19,6 @@ import { import common from '../common/common'; import { getConfig } from '../../config'; import { configureSvgSize } from '../../setupGraphViewbox'; -import addSVGAccessibilityFields from '../../accessibility'; export const setConf = function () { log.debug('Something is calling, setConf, remove the call'); @@ -116,8 +115,6 @@ export const draw = function (text, id, version, diagObj) { .attr('y', conf.titleTopMargin) .attr('class', 'titleText'); - addSVGAccessibilityFields(diagObj.db, svg, id); - /** * @param tasks * @param pageWidth diff --git a/packages/mermaid/src/diagrams/git/gitGraphRenderer.js b/packages/mermaid/src/diagrams/git/gitGraphRenderer.js index 6874363ad..787eb2490 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphRenderer.js +++ b/packages/mermaid/src/diagrams/git/gitGraphRenderer.js @@ -2,7 +2,6 @@ import { select } from 'd3'; import { getConfig, setupGraphViewbox } from '../../diagram-api/diagramAPI'; import { log } from '../../logger'; import utils from '../../utils'; -import addSVGAccessibilityFields from '../../accessibility'; let allCommitsDict = {}; @@ -506,9 +505,6 @@ export const draw = function (txt, id, ver, diagObj) { const diagram = select(`[id="${id}"]`); - // Adds title and description to the flow chart - addSVGAccessibilityFields(diagObj.db, diagram, id); - drawCommits(diagram, allCommitsDict, false); if (gitGraphConfig.showBranches) { drawBranches(diagram, branches); diff --git a/packages/mermaid/src/diagrams/pie/pieRenderer.js b/packages/mermaid/src/diagrams/pie/pieRenderer.js index c5d86ad65..83f301207 100644 --- a/packages/mermaid/src/diagrams/pie/pieRenderer.js +++ b/packages/mermaid/src/diagrams/pie/pieRenderer.js @@ -3,7 +3,6 @@ import { select, scaleOrdinal, pie as d3pie, arc } from 'd3'; import { log } from '../../logger'; import { configureSvgSize } from '../../setupGraphViewbox'; import * as configApi from '../../config'; -import addSVGAccessibilityFields from '../../accessibility'; let conf = configApi.getConfig(); @@ -53,7 +52,6 @@ export const draw = (txt, id, _version, diagObj) => { const diagram = root.select('#' + id); configureSvgSize(diagram, height, width, conf.pie.useMaxWidth); - addSVGAccessibilityFields(diagObj.db, diagram, id); // Set viewBox elem.setAttribute('viewBox', '0 0 ' + width + ' ' + height); diff --git a/packages/mermaid/src/diagrams/requirement/requirementRenderer.js b/packages/mermaid/src/diagrams/requirement/requirementRenderer.js index a0019f46b..9fd746bd1 100644 --- a/packages/mermaid/src/diagrams/requirement/requirementRenderer.js +++ b/packages/mermaid/src/diagrams/requirement/requirementRenderer.js @@ -6,7 +6,6 @@ import { configureSvgSize } from '../../setupGraphViewbox'; import common from '../common/common'; import markers from './requirementMarkers'; import { getConfig } from '../../config'; -import addSVGAccessibilityFields from '../../accessibility'; let conf = {}; let relCnt = 0; @@ -363,8 +362,6 @@ export const draw = (text, id, _version, diagObj) => { configureSvgSize(svg, height, width, conf.useMaxWidth); svg.attr('viewBox', `${svgBounds.x - padding} ${svgBounds.y - padding} ${width} ${height}`); - // Adds title and description to the requirements diagram - addSVGAccessibilityFields(diagObj.db, svg, id); }; export default { diff --git a/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js b/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js index 9422a5f37..6395940b0 100644 --- a/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js +++ b/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js @@ -1,8 +1,62 @@ +import { vi } from 'vitest'; + import * as configApi from '../../config'; import mermaidAPI from '../../mermaidAPI'; import Diagram from '../../Diagram'; import { addDiagrams } from '../../diagram-api/diagram-orchestration'; + +/** + * Sequence diagrams require their own very special version of a mocked d3 module + * diagrams/sequence/svgDraw uses statements like this with d3 nodes: (note the [0][0]) + * + * // in drawText(...) + * textHeight += (textElem._groups || textElem)[0][0].getBBox().height; + */ +vi.mock('d3', () => { + const NewD3 = function () { + function returnThis() { + return this; + } + return { + append: function () { + return NewD3(); + }, + lower: returnThis, + attr: returnThis, + style: returnThis, + text: returnThis, + // [0][0] (below) is required by drawText() in packages/mermaid/src/diagrams/sequence/svgDraw.js + 0: { + 0: { + getBBox: function () { + return { + height: 10, + width: 20, + }; + }, + }, + }, + }; + }; + + return { + select: function () { + return new NewD3(); + }, + + selectAll: function () { + return new NewD3(); + }, + + curveBasis: 'basis', + curveLinear: 'linear', + curveCardinal: 'cardinal', + }; +}); +// ------------------------------- + addDiagrams(); + /** * @param conf * @param key diff --git a/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts b/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts index 738b86540..1f6164b92 100644 --- a/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts +++ b/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts @@ -9,7 +9,6 @@ import * as configApi from '../../config'; import assignWithDepth from '../../assignWithDepth'; import utils from '../../utils'; import { configureSvgSize } from '../../setupGraphViewbox'; -import addSVGAccessibilityFields from '../../accessibility'; import Diagram from '../../Diagram'; let conf = {}; @@ -904,7 +903,6 @@ export const draw = function (_text: string, id: string, _version: string, diagO (height + extraVertForTitle) ); - addSVGAccessibilityFields(diagObj.db, diagram, id); log.debug(`models:`, bounds.models); }; diff --git a/packages/mermaid/src/diagrams/sequence/svgDraw.spec.js b/packages/mermaid/src/diagrams/sequence/svgDraw.spec.js index 580dafe89..8e5f5f32b 100644 --- a/packages/mermaid/src/diagrams/sequence/svgDraw.spec.js +++ b/packages/mermaid/src/diagrams/sequence/svgDraw.spec.js @@ -1,5 +1,31 @@ +import { vi } from 'vitest'; import svgDraw from './svgDraw'; -import { MockD3 } from 'd3'; + +// This is the only place that uses this mock +export const MockD3 = (name, parent) => { + const children = []; + const elem = { + get __children() { + return children; + }, + get __name() { + return name; + }, + get __parent() { + return parent; + }, + }; + elem.append = (name) => { + const mockElem = MockD3(name, elem); + children.push(mockElem); + return mockElem; + }; + elem.lower = vi.fn(() => elem); + elem.attr = vi.fn(() => elem); + elem.text = vi.fn(() => elem); + elem.style = vi.fn(() => elem); + return elem; +}; describe('svgDraw', function () { describe('drawRect', function () { diff --git a/packages/mermaid/src/diagrams/state/stateRenderer-v2.js b/packages/mermaid/src/diagrams/state/stateRenderer-v2.js index 78e38726e..fa2470d6b 100644 --- a/packages/mermaid/src/diagrams/state/stateRenderer-v2.js +++ b/packages/mermaid/src/diagrams/state/stateRenderer-v2.js @@ -6,7 +6,7 @@ import { log } from '../../logger'; import { configureSvgSize } from '../../setupGraphViewbox'; import common from '../common/common'; import utils from '../../utils'; -import addSVGAccessibilityFields from '../../accessibility'; + import { DEFAULT_DIAGRAM_DIRECTION, DEFAULT_NESTED_DOC_DIR, @@ -470,7 +470,6 @@ export const draw = function (text, id, _version, diag) { label.insertBefore(rect, label.firstChild); // } } - addSVGAccessibilityFields(diag.db, svg, id); }; export default { diff --git a/packages/mermaid/src/diagrams/state/stateRenderer.js b/packages/mermaid/src/diagrams/state/stateRenderer.js index 4eeede12e..8d410fdd9 100644 --- a/packages/mermaid/src/diagrams/state/stateRenderer.js +++ b/packages/mermaid/src/diagrams/state/stateRenderer.js @@ -6,7 +6,6 @@ import common from '../common/common'; import { drawState, addTitleAndBox, drawEdge } from './shapes'; import { getConfig } from '../../config'; import { configureSvgSize } from '../../setupGraphViewbox'; -import addSVGAccessibilityFields from '../../accessibility'; // TODO Move conf object to main conf in mermaidAPI let conf; @@ -97,7 +96,6 @@ export const draw = function (text, id, _version, diagObj) { 'viewBox', `${bounds.x - conf.padding} ${bounds.y - conf.padding} ` + width + ' ' + height ); - addSVGAccessibilityFields(diagObj.db, diagram, id); }; const getLabelWidth = (text) => { return text ? text.length * conf.fontSizeFactor : 1; diff --git a/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts b/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts index b22192101..df46fc9c6 100644 --- a/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts +++ b/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts @@ -3,7 +3,6 @@ import { select } from 'd3'; import svgDraw from './svgDraw'; import { getConfig } from '../../config'; import { configureSvgSize } from '../../setupGraphViewbox'; -import addSVGAccessibilityFields from '../../accessibility'; export const setConf = function (cnf) { const keys = Object.keys(cnf); @@ -121,8 +120,6 @@ export const draw = function (text, id, version, diagObj) { diagram.attr('viewBox', `${box.startx} -25 ${width} ${height + extraVertForTitle}`); diagram.attr('preserveAspectRatio', 'xMinYMin meet'); diagram.attr('height', height + extraVertForTitle + 25); - - addSVGAccessibilityFields(diagObj.db, diagram, id); }; export const bounds = { diff --git a/packages/mermaid/src/docs/community/newDiagram.md b/packages/mermaid/src/docs/community/newDiagram.md index 74026b3ff..57a454671 100644 --- a/packages/mermaid/src/docs/community/newDiagram.md +++ b/packages/mermaid/src/docs/community/newDiagram.md @@ -8,8 +8,8 @@ This would be to define a jison grammar for the new diagram type. That should st For instance: -- the flowchart starts with the keyword graph. -- the sequence diagram starts with the keyword sequenceDiagram +- the flowchart starts with the keyword _graph_ +- the sequence diagram starts with the keyword _sequenceDiagram_ #### Store data found during parsing @@ -56,6 +56,11 @@ Place the renderer in the diagram folder. ### Step 3: Detection of the new diagram type The second thing to do is to add the capability to detect the new new diagram to type to the detectType in utils.js. The detection should return a key for the new diagram type. +[This key will be used to as the aria roledescription](#aria-roledescription), so it should be a word that clearly describes the diagram type. +For example, if your new diagram use a UML deployment diagram, a good key would be "UMLDeploymentDiagram" because assistive technologies such as a screen reader +would voice that as "U-M-L Deployment diagram." Another good key would be "deploymentDiagram" because that would be voiced as "Deployment Diagram." A bad key would be "deployment" because that would not sufficiently describe the diagram. + +Note that the diagram type key does not have to be the same as the diagram keyword chosen for the [grammar](#grammar), but it is helpful if they are the same. ### Step 4: The final piece - triggering the rendering @@ -163,19 +168,23 @@ It is probably a good idea to keep the handling similar to this in your new diag ## Accessibility -The syntax for adding title and description looks like this: +Mermaid automatically adds the following accessibility information for the diagram SVG HTML element: -``` -accTitle: The title -accDescr: The description +- aria-roledescription +- accessible title +- accessible description -accDescr { - Syntax for a description text - written on multiple lines. -} -``` +### aria-roledescription -In a similar way to the directives the jison syntax are quite similar between the diagrams. +The aria-roledescription is automatically set to [the diagram type](#step-3--detection-of-the-new-diagram-type) and inserted into the SVG element. + +See [the definition of aria-roledescription](https://www.w3.org/TR/wai-aria-1.1/#aria-roledescription) in [the Accessible Rich Internet Applications W3 standard.](https://www.w3.org/WAI/standards-guidelines/aria/) + +### accessible title and description + +The syntax for accessible titles and descriptions is described in [the Accessibility documenation section.](../config/accessibility.md) + +In a similar way to the directives, the jison syntax are quite similar between the diagrams. ```jison @@ -213,18 +222,7 @@ import { } from '../../commonDb'; ``` -For rendering the accessibility tags you have again an existing function you can use. - -**In the renderer:** - -```js -import addSVGAccessibilityFields from '../../accessibility'; - -/* ... */ - -// Adds title and description to the flow chart -addSVGAccessibilityFields(parser.yy, svg, id); -``` +The accessibility title and description are inserted into the SVG element in the `render` function in mermaidAPI. ## Theming diff --git a/packages/mermaid/src/docs/config/accessibility.md b/packages/mermaid/src/docs/config/accessibility.md index ade20a839..e7947adec 100644 --- a/packages/mermaid/src/docs/config/accessibility.md +++ b/packages/mermaid/src/docs/config/accessibility.md @@ -4,83 +4,151 @@ Now with Mermaid library in much wider use, we have started to work towards more accessible features, based on the feedback from the community. -To begin with, we have added a new feature to Mermaid library, which is to support accessibility options, **Accessibility Title** and **Accessibility Description**. +Adding accessibility means that the rich information communicated by visual diagrams can be made available to those using assistive technologies (and of course to search engines). +[Read more about Accessible Rich Internet Applications and the W3 standards.](https://www.w3.org/WAI/standards-guidelines/aria/) -This support for accessibility options is available for all the diagrams/chart types. Also, we have tired to keep the same format for the accessibility options, so that it is easy to understand and maintain. +Mermaid will automatically insert the [aria-roledescription](#aria-roledescription) and, if provided in the diagram text by the diagram author, the [accessible title and description.](#accessible-title-and-description) -## Defining Accessibility Options +### aria-roledescription -### Single line accessibility values +The [aria-roledescription](https://www.w3.org/TR/wai-aria-1.1/#aria-roledescription) for the SVG HTML element is set to the diagram type key. (Note this may be slightly different than the keyword used for the diagram in the diagram text.) -The diagram authors can now add the accessibility options in the diagram definition, using the `accTitle` and `accDescr` keywords, where each keyword is followed by `:` and the string value for title and description like: - -- `accTitle: "Your Accessibility Title"` or -- `accDescr: "Your Accessibility Description"` - -**When these two options are defined, they will add a corresponding `` and `<desc>` tag in the SVG.** - -Let us take a look at the following example with a flowchart diagram: - -```mermaid-example - graph LR - accTitle: Big decisions - accDescr: Flow chart of the decision making process - A[Hard] -->|Text| B(Round) - B --> C{Decision} - C -->|One| D[Result 1] +For example: The diagram type key for a state diagram is "stateDiagram". Here (a part of) the HTML of the SVG tag that shows the automatically inserted aria-roledscription set to "stateDiagram". _(Note that some of the SVG attributes and the SVG contents are omitted for clarity.):_ +```html +<svg + aria-roledescription="stateDiagram" + class="statediagram" + xmlns="http://www.w3.org/2000/svg" + width="100%" + id="mermaid-1668720491568" +></svg> ``` -See in the code snippet above, the `accTitle` and `accDescr` are defined in the diagram definition. They result in the following tags in SVG code: +### Accessible Title and Description -![Accessibility options rendered inside SVG](img/accessibility-div-example.png) +Support for accessible titles and descriptions is available for all diagrams/chart types. We have tried to keep the same keywords and format for all diagrams so that it is easy to understand and maintain. -### Multi-line Accessibility title/description +The accessible title and description will add `<title>` and `<desc>` elements within the SVG element and the [aria-labelledby](https://www.w3.org/TR/wai-aria/#aria-labelledby) and [aria-describedby](https://www.w3.org/TR/wai-aria/#aria-describedby) attributes in the SVG tag. -You can also define the accessibility options in a multi-line format, where the keyword is followed by opening curly bracket `{` and then multiple lines, followed by a closing `}`. +Here is HTML that is generated, showing that the SVG element is labelled by the accessible title (id = `chart-title-mermaid-1668725057758`) +and described by the accessible description (id = `chart-desc-mermaid-1668725057758` ); +and the accessible title element (text = "This is the accessible title") +and the accessible description element (text = "This is an accessible description"). -`accTitle: My single line title value` (**_single line format_**) +_(Note that some of the SVG attributes and the SVG contents are omitted for clarity.)_ -vs +```html +<svg + aria-labelledby="chart-title-mermaid-1668725057758" + aria-describedby="chart-desc-mermaid-1668725057758" + xmlns="http://www.w3.org/2000/svg" + width="100%" + id="mermaid-1668725057758" +> + <title id="chart-title-mermaid-1668725057758">This is the accessible title + This is an accessible description + +``` -`accDescr: { My multi-line description of the diagram }` (**_multi-line format_**) +Details for the syntax follow. -Let us look at it in the following example, with same flowchart: +#### accessible title + +The **accessible title** is specified with the **accTitle** _keyword_, followed by a colon (`:`), and the string value for the title. +The string value ends at the end of the line. (It can only be a single line.) + +Ex: `accTitle: This is a single line title` + +See [the accTitle and accDescr usage examples](#acctitle-and-accdescr-usage-examples) for how this can be used in a diagram and the resulting HTML generated. + +#### accessible description + +An accessible description can be 1 line long (a single line) or many lines long. + +The **single line accessible description** is specified with the **accDescr** _keyword_, followed by a colon (`:`), followed by the string value for the description. + +Ex: `accDescr: This is a single line description.` + +A **multiple line accessible description** _does not have a colon (`:`) after the accDescr keyword_ and is surrounded by curly brackets (`{}`). + +Ex: + +``` +accDescr { The official Bob's Burgers corporate processes that are used + for making very, very big decisions. + This is actually a very simple flow: see the big decision and then make the big decision.} +``` + +See [the accTitle and accDescr usage examples](#acctitle-and-accdescr-usage-examples) for how this can be used in a diagram and the resulting HTML generated. + +#### accTitle and accDescr Usage Examples + +- Flowchart with the accessible title "Big Decisions" and the single-line accessible description "Bob's Burgers process for making big decisions" ```mermaid-example - graph LR - accTitle: Big decisions + graph LR + accTitle: Big Decisions + accDescr: Bob's Burgers process for making big decisions + A[Identify Big Descision] --> B{Make Big Decision} + B --> D[Be done] +``` +Here is the HTML generated for the SVG element: _(Note that some of the SVG attributes and the SVG contents are omitted for clarity.):_ + +```html + + Big decisions + Bob's Burgers process for making big decisions + +``` + +- Flowchart with the accessible title "Bob's Burger's Making Big Decisions" and the multiple line accessible description "The official Bob's Burgers corporate processes that are used + for making very, very big decisions. + This is actually a very simple flow: identify the big decision and then make the big decision." + +```mermaid-example + graph LR + accTitle: Bob's Burger's Making Big Decisions accDescr { - My multi-line description - of the diagram - } - - A[Hard] -->|Text| B(Round) - B --> C{Decision} - C -->|One| D[Result 1] - + The official Bob's Burgers corporate processes that are used + for making very, very big decisions. + This is actually a very simple flow: identify the big decision and then make the big decision. + } + A[Identify Big Descision] --> B{Make Big Decision} + B --> D[Be done] ``` -See in the code snippet above, the `accTitle` and `accDescr` are defined in the diagram definition. They result in the following tags in SVG code: +Here is the HTML generated for the SVG element: _(Note that some of the SVG attributes and the SVG contents are omitted for clarity.):_ -![Accessibility options rendered inside SVG](img/accessibility-div-example-2.png) - -### Sample Code Snippet for other diagram types - -#### Sequence Diagram - -```mermaid-example - sequenceDiagram - accTitle: My Sequence Diagram - accDescr: My Sequence Diagram Description - - Alice->>John: Hello John, how are you? - John-->>Alice: Great! - Alice-)John: See you later! +```html + + Big decisions + + The official Bob's Burgers corporate processes that are used for making very, very big + decisions. This is actually a very simple flow: identify the big decision and then make the big + decision. + + ``` -#### Class Diagram +#### Sample Code Snippets for other diagram types + +##### Class Diagram ```mermaid-example classDiagram @@ -90,18 +158,7 @@ See in the code snippet above, the `accTitle` and `accDescr` are defined in the Vehicle <|-- Car ``` -#### State Diagram - -```mermaid-example - stateDiagram - accTitle: My State Diagram - accDescr: My State Diagram Description - - s1 --> s2 - -``` - -#### Entity Relationship Diagram +##### Entity Relationship Diagram ```mermaid-example erDiagram @@ -114,25 +171,7 @@ See in the code snippet above, the `accTitle` and `accDescr` are defined in the ``` -#### User Journey Diagram - -```mermaid-example - journey - accTitle: My User Journey Diagram - accDescr: My User Journey Diagram Description - - title My working day - section Go to work - Make tea: 5: Me - Go upstairs: 3: Me - Do work: 1: Me, Cat - section Go home - Go downstairs: 5: Me - Sit down: 5: Me - -``` - -#### Gantt Chart +##### Gantt Chart ```mermaid-example gantt @@ -150,7 +189,27 @@ See in the code snippet above, the `accTitle` and `accDescr` are defined in the ``` -#### Pie Chart +##### Gitgraph + +```mermaid-example + gitGraph + accTitle: My Gitgraph Accessibility Title + accDescr: My Gitgraph Accessibility Description + + commit + commit + branch develop + checkout develop + commit + commit + checkout main + merge develop + commit + commit + +``` + +##### Pie Chart ```mermaid-example pie @@ -165,7 +224,7 @@ See in the code snippet above, the `accTitle` and `accDescr` are defined in the ``` -#### Requirement Diagram +##### Requirement Diagram ```mermaid-example requirementDiagram @@ -187,22 +246,43 @@ See in the code snippet above, the `accTitle` and `accDescr` are defined in the ``` -#### Gitgraph +##### Sequence Diagram ```mermaid-example - gitGraph - accTitle: My Gitgraph Accessibility Title - accDescr: My Gitgraph Accessibility Description + sequenceDiagram + accTitle: My Sequence Diagram + accDescr: My Sequence Diagram Description - commit - commit - branch develop - checkout develop - commit - commit - checkout main - merge develop - commit - commit + Alice->>John: Hello John, how are you? + John-->>Alice: Great! + Alice-)John: See you later! +``` + +##### State Diagram + +```mermaid-example + stateDiagram + accTitle: My State Diagram + accDescr: My State Diagram Description + + s1 --> s2 + +``` + +##### User Journey Diagram + +```mermaid-example + journey + accTitle: My User Journey Diagram + accDescr: My User Journey Diagram Description + + title My working day + section Go to work + Make tea: 5: Me + Go upstairs: 3: Me + Do work: 1: Me, Cat + section Go home + Go downstairs: 5: Me + Sit down: 5: Me ``` diff --git a/packages/mermaid/src/mermaidAPI.spec.ts b/packages/mermaid/src/mermaidAPI.spec.ts index 55d46ae7c..f9bad66d7 100644 --- a/packages/mermaid/src/mermaidAPI.spec.ts +++ b/packages/mermaid/src/mermaidAPI.spec.ts @@ -1,6 +1,38 @@ 'use strict'; import { vi } from 'vitest'; +// ------------------------------------- +// Mocks and mocking + +import { MockedD3 } from './tests/MockedD3'; + +// Note: If running this directly from within an IDE, the mocks directory must be at packages/mermaid/mocks +vi.mock('d3'); +vi.mock('dagre-d3'); + +// mermaidAPI.spec.ts: +import * as accessibility from './accessibility'; // Import it this way so we can use spyOn(accessibility,...) +vi.mock('./accessibility', () => ({ + setA11yDiagramInfo: vi.fn(), + addSVGa11yTitleDescription: vi.fn(), +})); + +// Mock the renderers specifically so we can test render(). Need to mock draw() for each renderer +vi.mock('./diagrams/c4/c4Renderer'); +vi.mock('./diagrams/class/classRenderer'); +vi.mock('./diagrams/class/classRenderer-v2'); +vi.mock('./diagrams/er/erRenderer'); +vi.mock('./diagrams/flowchart/flowRenderer-v2'); +vi.mock('./diagrams/git/gitGraphRenderer'); +vi.mock('./diagrams/gantt/ganttRenderer'); +vi.mock('./diagrams/user-journey/journeyRenderer'); +vi.mock('./diagrams/pie/pieRenderer'); +vi.mock('./diagrams/requirement/requirementRenderer'); +vi.mock('./diagrams/sequence/sequenceRenderer'); +vi.mock('./diagrams/state/stateRenderer-v2'); + +// ------------------------------------- + import mermaid from './mermaid'; import { MermaidConfig } from './config.type'; @@ -37,7 +69,10 @@ vi.mock('stylis', () => { }); import { compile, serialize } from 'stylis'; -import { MockedD3 } from './tests/MockedD3'; +/** + * @see https://vitest.dev/guide/mocking.html Mock part of a module + * To investigate how to mock just some methods from a module - call the actual implementation and then mock others, e.g. so they can be spied on + */ // ------------------------------------------------------------------------------------- @@ -335,7 +370,8 @@ describe('mermaidAPI', function () { const htmlElements = ['> *', 'span']; it('creates CSS styles for every style and textStyle in every classDef', () => { - // @todo TODO Can't figure out how to spy on the cssImportantStyles method. That would be a much better approach than manually checking the result + // @todo TODO Can't figure out how to spy on the cssImportantStyles method. + // That would be a much better approach than manually checking the result const styles = createCssStyles(mocked_config, graphType, classDefs); htmlElements.forEach((htmlElement) => { @@ -373,7 +409,7 @@ describe('mermaidAPI', function () { const htmlElements = ['rect', 'polygon', 'ellipse', 'circle']; it('creates CSS styles for every style and textStyle in every classDef', () => { - // @todo TODO Can't figure out how to spy on the cssImportantStyles method. That would be a much better approach than manually checking the result + // TODO Can't figure out how to spy on the cssImportantStyles method. That would be a much better approach than manually checking the result. const styles = createCssStyles(mocked_config_no_htmlLabels, graphType, classDefs); htmlElements.forEach((htmlElement) => { @@ -510,7 +546,7 @@ describe('mermaidAPI', function () { expect(config.testLiteral).toBe(true); }); - it('copies a an object into the configuration', function () { + it('copies an object into the configuration', function () { const orgConfig: any = mermaidAPI.getConfig(); expect(orgConfig.testObject).toBe(undefined); @@ -616,6 +652,7 @@ describe('mermaidAPI', function () { expect(mermaidAPI.defaultConfig['logLevel']).toBe(5); }); }); + describe('dompurify config', function () { it('allows dompurify config to be set', function () { mermaidAPI.initialize({ dompurifyConfig: { ADD_ATTR: ['onclick'] } }); @@ -623,6 +660,7 @@ describe('mermaidAPI', function () { expect(mermaidAPI!.getConfig()!.dompurifyConfig!.ADD_ATTR).toEqual(['onclick']); }); }); + describe('parse', function () { mermaid.parseError = undefined; // ensure it parseError undefined it('throws for an invalid definition (with no mermaid.parseError() defined)', function () { @@ -646,4 +684,106 @@ describe('mermaidAPI', function () { expect(mermaidAPI.parse('graph TD;A--x|text including URL space|B;')).toEqual(true); }); }); + + describe('render', () => { + // 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', () => { + const a11yDiagramInfo_spy = vi.spyOn(accessibility, 'setA11yDiagramInfo'); + const a11yTitleDesc_spy = vi.spyOn(accessibility, 'addSVGa11yTitleDescription'); + mermaidAPI.render(id, diagramText); + expect(a11yDiagramInfo_spy).toHaveBeenCalledWith( + expect.anything(), + expectedDiagramType + ); + expect(a11yTitleDesc_spy).toHaveBeenCalled(); + }); + }); + }); + }); + }); + + describe('renderAsync', () => { + // 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 a11yTitleDesc_spy = vi.spyOn(accessibility, 'addSVGa11yTitleDescription'); + await mermaidAPI.renderAsync(id, diagramText); + expect(a11yDiagramInfo_spy).toHaveBeenCalledWith( + expect.anything(), + expectedDiagramType + ); + expect(a11yTitleDesc_spy).toHaveBeenCalled(); + }); + }); + }); + }); + }); }); diff --git a/packages/mermaid/src/mermaidAPI.ts b/packages/mermaid/src/mermaidAPI.ts index 5687a1807..a77aed96d 100644 --- a/packages/mermaid/src/mermaidAPI.ts +++ b/packages/mermaid/src/mermaidAPI.ts @@ -30,6 +30,7 @@ import DOMPurify from 'dompurify'; import { MermaidConfig } from './config.type'; import { evaluate } from './diagrams/common/common'; import isEmpty from 'lodash-es/isEmpty'; +import { setA11yDiagramInfo, addSVGa11yTitleDescription } from './accessibility'; // diagram names that support classDef statements const CLASSDEF_DIAGRAMS = ['graph', 'flowchart', 'flowchart-v2', 'stateDiagram', 'stateDiagram-v2']; @@ -68,7 +69,7 @@ interface DiagramStyleClassDef { // This makes it clear that we're working with a d3 selected element of some kind, even though it's hard to specify the exact type. // @ts-ignore Could replicate the type definition in d3. This also makes it possible to use the untyped info from the js diagram files. -type D3Element = any; +export type D3Element = any; // ---------------------------------------------------------------------------- @@ -371,7 +372,7 @@ export const removeExistingElements = ( * @param id - The id for the SVG element (the element to be rendered) * @param text - The text for the graph definition * @param cb - Callback which is called after rendering is finished with the svg code as in param. - * @param container - HTML element where the svg will be inserted. (Is usually element with the .mermaid class) + * @param svgContainingElement - HTML element where the svg will be inserted. (Is usually element with the .mermaid class) * If no svgContainingElement is provided then the SVG element will be appended to the body. * Selector to element in which a div with the graph temporarily will be * inserted. If one is provided a hidden div will be inserted in the body of the page instead. The @@ -479,12 +480,13 @@ const render = function ( parseEncounteredException = error; } - // Get the temporary div element containing the svg + // Get the temporary div element containing the svg (the parent HTML Element) const element = root.select(enclosingDivID_selector).node(); const graphType = diag.type; // ------------------------------------------------------------------------------- // Create and insert the styles (user styles, theme styles, config styles) + // These are dealing with HTML Elements, not d3 nodes. // Insert an element into svg. This is where we put the styles const svg = element.firstChild; @@ -501,6 +503,7 @@ const render = function ( idSelector ); + // svg is a HTML element (not a d3 node) const style1 = document.createElement('style'); style1.innerHTML = rules; svg.insertBefore(style1, firstChild); @@ -514,6 +517,12 @@ const render = function ( throw e; } + // This is the d3 node for the svg element + const svgNode = root.select(`${enclosingDivID_selector} svg`); + const a11yTitle = diag.db.getAccTitle?.(); + const a11yDescr = diag.db.getAccDescription?.(); + addA11yInfo(graphType, svgNode, a11yTitle, a11yDescr); + // ------------------------------------------------------------------------------- // Clean up SVG code root.select(`[id="${id}"]`).selectAll('foreignobject > *').attr('xmlns', XMLNS_XHTML_STD); @@ -710,6 +719,12 @@ const renderAsync = async function ( throw e; } + // This is the d3 node for the svg element + const svgNode = root.select(`${enclosingDivID_selector} svg`); + const a11yTitle = diag.db.getAccTitle?.(); + const a11yDescr = diag.db.getAccDescription?.(); + addA11yInfo(graphType, svgNode, a11yTitle, a11yDescr); + // ------------------------------------------------------------------------------- // Clean up SVG code root.select(`[id="${id}"]`).selectAll('foreignobject > *').attr('xmlns', XMLNS_XHTML_STD); @@ -755,7 +770,7 @@ const renderAsync = async function ( attachFunctions(); // ------------------------------------------------------------------------------- - // Remove the temporary element if appropriate + // Remove the temporary HTML element if appropriate const tmpElementSelector = isSandboxed ? iFrameID_selector : enclosingDivID_selector; const node = select(tmpElementSelector).node(); if (node && 'remove' in node) { @@ -874,6 +889,20 @@ function initialize(options: MermaidConfig = {}) { addDiagrams(); } +/** + * Add accessibility (a11y) information to the diagram. + * + */ +function addA11yInfo( + graphType: string, + svgNode: D3Element, + a11yTitle: string | undefined, + a11yDescr: string | undefined +) { + setA11yDiagramInfo(svgNode, graphType); + addSVGa11yTitleDescription(svgNode, a11yTitle, a11yDescr, svgNode.attr('id')); +} + /** * ## mermaidAPI configuration defaults * diff --git a/packages/mermaid/src/tests/MockedD3.ts b/packages/mermaid/src/tests/MockedD3.ts index 9cf01ddad..284b21b08 100644 --- a/packages/mermaid/src/tests/MockedD3.ts +++ b/packages/mermaid/src/tests/MockedD3.ts @@ -1,12 +1,18 @@ /** * 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. + * + * 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(). */ + export class MockedD3 { public attribs = new Map(); public id: string | undefined = ''; _children: MockedD3[] = []; + _containingHTMLdoc = new Document(); + constructor(givenId = 'mock-id') { this.id = givenId; } @@ -29,6 +35,11 @@ export class MockedD3 { return new MockedD3(cleanId); }); + // This has the same implementation as select(). (It calls it.) + selectAll = vi.fn().mockImplementation(({ select_str = '' }): MockedD3 => { + return this.select(select_str); + }); + append = vi .fn() .mockImplementation(function (this: MockedD3, type: string, id = '' + '-appended'): MockedD3 { @@ -87,9 +98,18 @@ export class MockedD3 { this.attribs.set('text', attrValue); return this; } - // NOTE: Arbitrarily returns an empty object. The return value could be something different with a mockReturnValue() or mockImplementation() - public node = vi.fn().mockReturnValue({}); + // NOTE: Returns a HTML Element with tag 'svg' that has _another_ 'svg' element child. + // This allows different tests to succeed -- some need a top level 'svg' and some need a 'svg' element to be the firstChild + // Real implementation returns an HTML Element + public node = vi.fn().mockImplementation(() => { + const topElem = this._containingHTMLdoc.createElement('svg'); + const elem_svgChild = this._containingHTMLdoc.createElement('svg'); // another svg element + topElem.appendChild(elem_svgChild); + return topElem; + }); + + // TODO Is this correct? shouldn't it return a list of HTML Elements? nodes = vi.fn().mockImplementation(function (this: MockedD3): MockedD3[] { return this._children; }); diff --git a/packages/mermaid/src/utils.spec.js b/packages/mermaid/src/utils.spec.js index 54262f10e..e983d21c8 100644 --- a/packages/mermaid/src/utils.spec.js +++ b/packages/mermaid/src/utils.spec.js @@ -4,7 +4,8 @@ import assignWithDepth from './assignWithDepth'; import { detectType } from './diagram-api/detectType'; import { addDiagrams } from './diagram-api/diagram-orchestration'; import memoize from 'lodash-es/memoize'; -import { MockD3 } from 'd3'; +import { MockedD3 } from './tests/MockedD3'; + addDiagrams(); describe('when assignWithDepth: should merge objects within objects', function () { @@ -352,21 +353,52 @@ describe('when initializing the id generator', function () { }); describe('when inserting titles', function () { - it('should do nothing when title is empty', function () { - const svg = MockD3('svg'); - utils.insertTitle(svg, 'testClass', 0, ''); - expect(svg.__children.length).toBe(0); + const svg = new MockedD3('svg'); + const mockedElement = { + getBBox: vi.fn().mockReturnValue({ x: 10, y: 11, width: 100, height: 200 }), + }; + const fauxTitle = new MockedD3('title'); + + beforeEach(() => { + svg.node = vi.fn().mockReturnValue(mockedElement); }); - it('should insert title centered', function () { - const svg = MockD3('svg'); + it('does nothing if the title is empty', function () { + const svgAppendSpy = vi.spyOn(svg, 'append'); + utils.insertTitle(svg, 'testClass', 0, ''); + expect(svgAppendSpy).not.toHaveBeenCalled(); + }); + + it('appends the title as a text item with the given title text', function () { + const svgAppendSpy = vi.spyOn(svg, 'append').mockReturnValue(fauxTitle); + const titleTextSpy = vi.spyOn(fauxTitle, 'text'); + utils.insertTitle(svg, 'testClass', 5, 'test title'); - expect(svg.__children.length).toBe(1); - const text = svg.__children[0]; - expect(text.__name).toBe('text'); - expect(text.text).toHaveBeenCalledWith('test title'); - expect(text.attr).toHaveBeenCalledWith('x', 15); - expect(text.attr).toHaveBeenCalledWith('y', -5); - expect(text.attr).toHaveBeenCalledWith('class', 'testClass'); + expect(svgAppendSpy).toHaveBeenCalled(); + expect(titleTextSpy).toHaveBeenCalledWith('test title'); + }); + + it('x value is the bounds x position + half of the bounds width', () => { + vi.spyOn(svg, 'append').mockReturnValue(fauxTitle); + const titleAttrSpy = vi.spyOn(fauxTitle, 'attr'); + + utils.insertTitle(svg, 'testClass', 5, 'test title'); + expect(titleAttrSpy).toHaveBeenCalledWith('x', 10 + 100 / 2); + }); + + it('y value is the negative of given title top margin', () => { + vi.spyOn(svg, 'append').mockReturnValue(fauxTitle); + const titleAttrSpy = vi.spyOn(fauxTitle, 'attr'); + + utils.insertTitle(svg, 'testClass', 5, 'test title'); + expect(titleAttrSpy).toHaveBeenCalledWith('y', -5); + }); + + it('class is the given css class', () => { + vi.spyOn(svg, 'append').mockReturnValue(fauxTitle); + const titleAttrSpy = vi.spyOn(fauxTitle, 'attr'); + + utils.insertTitle(svg, 'testClass', 5, 'test title'); + expect(titleAttrSpy).toHaveBeenCalledWith('class', 'testClass'); }); }); diff --git a/packages/mermaid/src/utils.ts b/packages/mermaid/src/utils.ts index 16566c3b1..767fdaa7d 100644 --- a/packages/mermaid/src/utils.ts +++ b/packages/mermaid/src/utils.ts @@ -913,7 +913,7 @@ export function getErrorMessage(error: unknown): string { } /** - * Appends element with the given title, centered. + * Appends element with the given title and css class. * * @param parent - d3 svg object to append title to * @param cssClass - CSS class for the element containing the title