Skip to content

Commit 8f11660

Browse files
authored
Merge pull request #4569 from grommet/add-accessibility-section-anchor-accordion
Add accessibility section anchor accordion
2 parents f9e27b5 + fb8bdd9 commit 8f11660

File tree

10 files changed

+403
-37
lines changed

10 files changed

+403
-37
lines changed

aries-site/src/components/content/SubsectionText.js

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react';
22
import styled from 'styled-components';
33
import PropTypes from 'prop-types';
4-
import { Box, Paragraph } from 'grommet';
4+
import { Box, Tag, Paragraph } from 'grommet';
55

66
import { TEXT_SIZE } from '../../layouts';
77
import { HighlightPhrase } from './HighlightPhrase';
@@ -21,17 +21,36 @@ const StyledBox = styled(Box)`
2121
}
2222
`;
2323

24-
export const SubsectionText = ({ children, level, size, ...rest }) => (
25-
<StyledBox width="large" margin={{ bottom: 'medium' }}>
24+
export const SubsectionText = ({
25+
children,
26+
level,
27+
size,
28+
accessibility,
29+
...rest
30+
}) => (
31+
<StyledBox gap="xsmall" width="large" margin={{ bottom: 'medium' }}>
2632
<Paragraph size={size || TEXT_SIZE[level]} fill margin="none" {...rest}>
2733
<HighlightPhrase size={size || TEXT_SIZE[level]}>
2834
{children}
2935
</HighlightPhrase>
3036
</Paragraph>
37+
{accessibility && (
38+
<Tag
39+
alignSelf="start"
40+
border={{ color: 'brand' }}
41+
value={accessibility}
42+
onClick={() =>
43+
document
44+
.getElementById('wcag-compliance')
45+
.scrollIntoView({ behavior: 'auto' })
46+
}
47+
/>
48+
)}
3149
</StyledBox>
3250
);
3351

3452
SubsectionText.propTypes = {
53+
accessibility: PropTypes.string,
3554
children: PropTypes.oneOfType([
3655
PropTypes.string,
3756
PropTypes.array,

aries-site/src/components/seo/Meta.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ export const Meta = ({
1717
const segment = 'corporate';
1818
const lifecycle = 'support';
1919
const pageContent = 'products';
20-
const csp = `default-src 'self' 'unsafe-eval';
20+
const csp = `default-src 'self' 'unsafe-eval';
2121
style-src 'self' *.hpe.com/hfws-static/5/css/ 'unsafe-inline';
22-
connect-src 'self' *.githubusercontent.com/grommet/hpe-design-system/ https://api.github.com/repos/grommet/hpe-design-system/pulls https://api.github.com/repos/grommet/hpe-design-system/commits https://www.google-analytics.com https://www.github.com/grommet/ https://eyes.applitools.com *.hpe.com/hpe/api/ https://iad1.qualtrics.com/API/v3/surveys/ https://api.spacexdata.com/;
22+
connect-src 'self' *.githubusercontent.com/grommet/hpe-design-system/ https://api.github.com/repos/grommet/hpe-design-system/pulls https://api.github.com/repos/grommet/hpe-design-system/commits https://www.google-analytics.com https://www.github.com/grommet/ https://eyes.applitools.com *.hpe.com/hpe/api/ https://iad1.qualtrics.com/API/v3/surveys/ https://api.spacexdata.com/ https://raw.githubusercontent.com/w3c/wcag/refs/heads/main/guidelines/wcag.json;
2323
media-src 'self' https://d3hq6blov2iije.cloudfront.net/media/;
2424
img-src 'self' data: https://www.google-analytics.com https://images.unsplash.com/ http://s.gravatar.com/avatar/ *.hpe.com/hfws-static/5/ https://d3hq6blov2iije.cloudfront.net/images/textures/ https://d3hq6blov2iije.cloudfront.net/images/gradients/ https://d3hq6blov2iije.cloudfront.net/images/hpe-greenlake/;
2525
script-src 'self' *.hpe.com https://www.google-analytics.com/analytics.js https://www.googletagmanager.com/gtag/js https://netlify-cdp-loader.netlify.app/netlify.js ${

aries-site/src/data/structures/components.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { TagPreview } from '../../examples/cardPreviews/tag';
3030
export const components = [
3131
{
3232
name: 'Anchor',
33+
accessibility: 'Passed WCAG 2.2 AAA',
3334
category: 'Controls',
3435
description:
3536
'Hyperlinks used with text-based navigation, such as inline text.',
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[
2+
{
3+
"anchor": [
4+
{ "rule": "1.4.1", "status": "passed" },
5+
{ "rule": "1.4.6", "status": "passed" },
6+
{ "rule": "1.4.3", "status": "passed" },
7+
{ "rule": "2.1.1", "status": "passed" },
8+
{ "rule": "2.1.2", "status": "passed" },
9+
{ "rule": "2.1.3", "status": "passed" },
10+
{ "rule": "2.4.4", "status": "conditional" },
11+
{ "rule": "2.4.7", "status": "passed" },
12+
{ "rule": "2.4.9", "status": "conditional" },
13+
{ "rule": "3.2.1", "status": "passed" },
14+
{ "rule": "4.1.2", "status": "passed" }
15+
]
16+
}
17+
]
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/* eslint-disable react/prop-types */
2+
import React, { useState, useMemo, useEffect } from 'react';
3+
import { WCAGRuleDetail, WCAGRuleSummary } from '.';
4+
import componentData from '../../data/wcag/components.json';
5+
6+
// title prop is the name of the component that will be passed in mdx file
7+
// version will be used so if we need to update the version of WCAG we can do it
8+
// easily component by component.
9+
export const AccessibilitySection = ({ title, version }) => {
10+
const [data, setData] = useState();
11+
12+
const componentInfo = useMemo(() => {
13+
if (!title || !componentData) {
14+
return [];
15+
}
16+
17+
const component = componentData.find(item => item[title.toLowerCase()]);
18+
return component ? component[title.toLowerCase()] : [];
19+
}, [title]);
20+
21+
useEffect(() => {
22+
fetch(
23+
'https://raw.githubusercontent.com/w3c/wcag/refs/heads/main/guidelines/wcag.json',
24+
)
25+
.then(response => {
26+
if (!response.ok) {
27+
throw new Error('Error fetching data');
28+
}
29+
return response.json();
30+
})
31+
.then(fetchedData => {
32+
setData(fetchedData);
33+
})
34+
.catch(error => {
35+
console.error('Error:', error);
36+
});
37+
}, []);
38+
39+
// create a map of each of the successCriteria ->
40+
// num to make it way easier and faster over time.
41+
// This will be used to
42+
// look up the success criteria by rule number
43+
const successCriteriaMap = useMemo(() => {
44+
if (!data) return new Map();
45+
46+
const map = new Map();
47+
data.principles?.forEach(principle => {
48+
principle.guidelines?.forEach(guideline => {
49+
guideline.successcriteria?.forEach(criterion => {
50+
map.set(criterion.num, criterion);
51+
});
52+
});
53+
});
54+
return map;
55+
}, [data]);
56+
57+
// Compare the component info with the success criteria
58+
// and return the status of each rule.
59+
const comparisons = useMemo(() => {
60+
return componentInfo.map(rule => {
61+
const ruleNum = rule.rule;
62+
const successCriterion = successCriteriaMap.get(ruleNum);
63+
64+
if (successCriterion) {
65+
const extractedData = {
66+
id: successCriterion.id.split(':')[1],
67+
num: successCriterion.num,
68+
level: successCriterion.level,
69+
handle: successCriterion.handle,
70+
title: successCriterion.title,
71+
};
72+
73+
return {
74+
...extractedData,
75+
status: rule.status,
76+
};
77+
}
78+
79+
return {
80+
rule: ruleNum,
81+
message: `Success criterion with num ${ruleNum} not found`,
82+
};
83+
});
84+
}, [componentInfo, successCriteriaMap]);
85+
86+
const statusData = comparisons.map(item => item.status);
87+
88+
return (
89+
<>
90+
<WCAGRuleSummary statuses={statusData} />
91+
<WCAGRuleDetail version={version || '2.2'} rules={comparisons} />
92+
</>
93+
);
94+
};

aries-site/src/layouts/content/DocsPageHeader.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@ export const DocsPageHeader = ({ title, topic, render }) => {
2727
</Link>
2828
)
2929
}
30-
subtitle={<SubsectionText>{page.description}</SubsectionText>}
30+
subtitle={
31+
<SubsectionText accessibility={page.accessibility}>
32+
{page.description}
33+
</SubsectionText>
34+
}
3135
margin={{ bottom: 'small' }}
3236
>
3337
{page.status && <Status status={page.status} />}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/* eslint-disable react/prop-types */
2+
import {
3+
Accordion,
4+
AccordionPanel,
5+
Anchor,
6+
Box,
7+
Heading,
8+
Paragraph,
9+
Tag,
10+
Text,
11+
} from 'grommet';
12+
import {
13+
StatusGoodSmall,
14+
StatusCriticalSmall,
15+
StatusWarningSmall,
16+
CircleInformation,
17+
ShareRounded,
18+
} from 'grommet-icons';
19+
import { TextEmphasis } from 'aries-core';
20+
21+
const getStatusIcon = status => {
22+
switch (status) {
23+
case 'failed':
24+
return <StatusCriticalSmall alt="" color="status-critical" />;
25+
case 'conditional':
26+
return <CircleInformation alt="" />;
27+
case 'passed with exceptions':
28+
return <StatusWarningSmall alt="" color="status-warning" />;
29+
default:
30+
return <StatusGoodSmall alt="" color="status-ok" />;
31+
}
32+
};
33+
34+
const WCAGAccessibilityCardView = ({
35+
level,
36+
link,
37+
ruleDescription,
38+
ruleName,
39+
ruleNumber,
40+
status,
41+
version,
42+
}) => (
43+
<Box
44+
pad={{ vertical: 'small', horizontal: 'medium' }}
45+
background="background-front"
46+
round="small"
47+
justify="between"
48+
direction="row"
49+
gap="small"
50+
>
51+
<Box flex gap="small">
52+
<Paragraph margin="none">
53+
<TextEmphasis>{ruleName}.</TextEmphasis> {ruleDescription}
54+
</Paragraph>
55+
<Box alignSelf="start" direction="row" align="center" gap="small">
56+
<Tag size="small" value={`WCAG ${version} ${level}`} />
57+
<Anchor
58+
target="_blank"
59+
size="small"
60+
href={link}
61+
label={`${ruleNumber} ${ruleName}`}
62+
/>
63+
</Box>
64+
</Box>
65+
<Box alignSelf="start" align="center" direction="row" gap="xsmall">
66+
{getStatusIcon(status)}
67+
<Text>{status.charAt(0).toUpperCase() + status.slice(1)}</Text>
68+
</Box>
69+
</Box>
70+
);
71+
72+
export const WCAGRuleDetail = ({ rules, version }) => {
73+
// Group rules by accessibility principle
74+
const groupRulesByAccessibilityPrinciple = (ruleList = []) => {
75+
const principleMapping = {
76+
1: 'Perceivable',
77+
2: 'Operable',
78+
3: 'Understandable',
79+
4: 'Robust',
80+
};
81+
82+
return ruleList.reduce((grouped, rule) => {
83+
const principleName = rule.num
84+
? principleMapping[parseInt(rule.num.split('.')[0], 10)]
85+
: undefined;
86+
return {
87+
...grouped,
88+
[principleName]: [...(grouped[principleName] || []), rule],
89+
};
90+
}, {});
91+
};
92+
93+
const groupedRules = groupRulesByAccessibilityPrinciple(rules);
94+
95+
return (
96+
<Box pad={{ vertical: 'medium' }} gap="medium">
97+
<Box gap="xxsmall" direction="row">
98+
<Text>Grouped by</Text>
99+
<Anchor
100+
label="accessibility principles"
101+
href="https://www.w3.org/WAI/WCAG22/Understanding/intro#understanding-the-four-principles-of-accessibility"
102+
target="_blank"
103+
icon={<ShareRounded />}
104+
reverse
105+
rel="noopener noreferrer"
106+
/>
107+
</Box>
108+
<Box width="large">
109+
{Object.keys(groupedRules).map(group => (
110+
<Accordion key={group} multiple>
111+
<AccordionPanel
112+
label={
113+
<Box gap="small" align="center" direction="row">
114+
{getStatusIcon(
115+
groupedRules[group].reduce(
116+
(worst, item) =>
117+
item.status < worst.status ? item : worst,
118+
{ status: 'passed' },
119+
).status,
120+
)}
121+
<Heading margin={{ vertical: 'small' }} level={4}>
122+
{group}
123+
</Heading>
124+
</Box>
125+
}
126+
>
127+
<Box pad={{ vertical: 'small' }} gap="small">
128+
{groupedRules[group].map(item => (
129+
<WCAGAccessibilityCardView
130+
key={item.num}
131+
status={item.status}
132+
level={item.level}
133+
link={`https://www.w3.org/TR/WCAG22/#${item.id}`}
134+
ruleNumber={item.num}
135+
version={version}
136+
ruleName={item.handle}
137+
ruleDescription={item.title}
138+
/>
139+
))}
140+
</Box>
141+
</AccordionPanel>
142+
</Accordion>
143+
))}
144+
</Box>
145+
</Box>
146+
);
147+
};

0 commit comments

Comments
 (0)