Skip to content

Commit 9aa301f

Browse files
feat(table): the type cell can provide a disclosure on extra-long types
- scrollable container for overflow tables - styling of certain columns - a11y screenreader enhancements
1 parent 697d122 commit 9aa301f

7 files changed

Lines changed: 634 additions & 44 deletions

File tree

‎blocks/table/table.css‎

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
--table-border-width: 1px;
55
--table-head-row-padding-block: 7px;
66
--table-body-row-padding-block: 11px;
7+
--table-scroll-hint-width: var(--s2-spacing-700);
8+
--table-scroll-hint-color: var(--se-body-background-color);
79

10+
position: relative;
811
container-type: inline-size;
912
container-name: table-wrapper;
1013
margin-block: var(--s2-spacing-500) var(--s2-spacing-800);
@@ -71,6 +74,25 @@
7174

7275
.table-cell-value {
7376
grid-column: 2;
77+
min-inline-size: 0;
78+
}
79+
80+
.table-type-cell {
81+
se-disclosure {
82+
/* Bound intrinsic sizing without fixing the other table columns. */
83+
inline-size: 100%;
84+
max-inline-size: 28rem;
85+
}
86+
87+
> .table-cell-value:not(:has(se-disclosure)) {
88+
background-color: rgb(from var(--s2-gray-1000) r g b / 8%);
89+
font-family: monospace;
90+
padding: 2px;
91+
border-radius: 3px;
92+
font-size: var(--s2-component-s-medium-font-size);
93+
color: var(--s2-gray-800);
94+
inline-size: fit-content;
95+
}
7496
}
7597

7698
tbody td svg {
@@ -94,9 +116,40 @@
94116
position: relative;
95117
}
96118

97-
@container table-wrapper (width >= 650px) {
119+
.table-scroll {
98120
overflow-x: auto;
99121

122+
/* Leave room for focus rings on links at the first and last rows. */
123+
padding-block: var(--s2-spacing-100);
124+
margin-block: calc(var(--s2-spacing-100) * -1);
125+
}
126+
127+
/* Keep the hint outside the scroller so it stays at the visible edge. */
128+
&::after {
129+
content: '';
130+
position: absolute;
131+
inset-block: 0;
132+
inset-inline-end: 0;
133+
inline-size: var(--table-scroll-hint-width);
134+
background: linear-gradient(to right, transparent, var(--table-scroll-hint-color));
135+
opacity: 0;
136+
pointer-events: none;
137+
transition: opacity 0.2s ease;
138+
}
139+
140+
&:dir(rtl)::after {
141+
background: linear-gradient(to left, transparent, var(--table-scroll-hint-color));
142+
}
143+
144+
&[data-scroll-hint]::after {
145+
opacity: 1;
146+
}
147+
148+
@container table-wrapper (width >= 650px) {
149+
.table-type-cell:has(se-disclosure) {
150+
min-inline-size: 16rem;
151+
}
152+
100153
table {
101154
border-collapse: separate;
102155
border-spacing: 0;
@@ -212,3 +265,9 @@
212265
background-color: transparent;
213266
}
214267
}
268+
269+
@media (prefers-reduced-motion: reduce) {
270+
.table::after {
271+
transition: none;
272+
}
273+
}

‎blocks/table/table.js‎

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { getConfig } from '../../scripts/ak.js';
22
import { getComponentProps } from '../../scripts/utils/extraction-status.js';
3+
import '../../deps/se/se.js';
34

45
const config = getConfig();
56

@@ -40,6 +41,7 @@ const EXCLUDED_SOURCES = new Set([
4041
// `kind`/`values`/`optional` back the playground's controls; this table shows the
4142
// human-readable `type` instead.
4243
const EXCLUDED_COLUMNS = new Set(['status', 'since', 'kind', 'values', 'optional']);
44+
const TYPE_DISCLOSURE_THRESHOLD = 500;
4345

4446
/**
4547
* The column-header text repeated inside a body cell. Only the narrow stacked layout
@@ -62,6 +64,30 @@ const buildCellValue = () => {
6264
return value;
6365
};
6466

67+
const buildDescription = (text) => {
68+
const content = document.createDocumentFragment();
69+
const tokens = /(?<!`)(`+)(?!`)([\s\S]*?)(?<!`)\1(?!`)|(?<!!)\[([^\]]+)\]\((https?:\/\/(?:[^()\s]|\([^()\s]*\))+)\)/gi;
70+
let offset = 0;
71+
for (const match of text.matchAll(tokens)) {
72+
const [markdown, backticks, codeText, label, href] = match;
73+
let node;
74+
if (backticks) {
75+
node = document.createElement('code');
76+
node.textContent = codeText;
77+
} else if (label.trim() && URL.canParse(href)) {
78+
node = document.createElement('a');
79+
node.href = href;
80+
node.append(buildDescription(label));
81+
}
82+
if (node) {
83+
content.append(text.slice(offset, match.index), node);
84+
offset = match.index + markdown.length;
85+
}
86+
}
87+
content.append(text.slice(offset));
88+
return content;
89+
};
90+
6591
const buildTableElement = (headerCells, dataCells) => {
6692
const tableHead = document.createElement('thead');
6793
tableHead.classList.add('header-row');
@@ -152,14 +178,68 @@ const buildDataTable = async (href) => {
152178
const dataCells = rows.map((props) => properties.map((key) => {
153179
const tableCell = document.createElement('td');
154180
const value = buildCellValue();
155-
value.textContent = props[key] || '-';
181+
const resolvedValue = props[key] || '-';
182+
if (key === 'property'
183+
|| key === 'attribute'
184+
|| key === 'type'
185+
|| (key === 'default' && resolvedValue !== '-')) {
186+
tableCell.classList.add('table-type-cell');
187+
}
188+
if (key === 'type' && typeof props[key] === 'string' && props[key].length > TYPE_DISCLOSURE_THRESHOLD) {
189+
const disclosure = document.createElement('se-disclosure');
190+
disclosure.setAttribute('preview', '');
191+
disclosure.setAttribute('label', `Expand to view more about ${props.attribute || props.property || 'this property'}`);
192+
disclosure.textContent = resolvedValue;
193+
value.append(disclosure);
194+
} else if (key === 'description' && typeof resolvedValue === 'string') {
195+
value.append(buildDescription(resolvedValue));
196+
} else {
197+
value.textContent = resolvedValue;
198+
}
156199
tableCell.append(value);
157200
return tableCell;
158201
}));
159202

160203
return buildTableElement(headerCells, dataCells);
161204
};
162205

206+
const buildScroller = (table, labelIds) => {
207+
const scroller = document.createElement('div');
208+
scroller.className = 'table-scroll';
209+
scroller.tabIndex = 0;
210+
if (labelIds.length) {
211+
scroller.role = 'region';
212+
scroller.setAttribute('aria-labelledby', labelIds.join(' '));
213+
}
214+
scroller.append(table);
215+
return scroller;
216+
};
217+
218+
const watchScrollHint = (el, scroller, instructions) => {
219+
const update = () => {
220+
const overflow = scroller.scrollWidth - scroller.clientWidth;
221+
const remaining = overflow - Math.abs(scroller.scrollLeft);
222+
el.toggleAttribute('data-scroll-hint', remaining > 1);
223+
const descriptionId = overflow > 1 ? instructions.id : null;
224+
if (scroller.getAttribute('aria-describedby') !== descriptionId) {
225+
instructions.hidden = !descriptionId;
226+
if (descriptionId) {
227+
scroller.setAttribute('aria-describedby', descriptionId);
228+
} else {
229+
scroller.removeAttribute('aria-describedby');
230+
}
231+
}
232+
};
233+
update();
234+
// Measure again when styles, content, or the surrounding layout change.
235+
const observer = new ResizeObserver(update);
236+
observer.observe(scroller);
237+
observer.observe(scroller.querySelector('table'));
238+
scroller.addEventListener('scroll', update, { passive: true });
239+
window.addEventListener('resize', update, { passive: true });
240+
document.fonts?.ready.then(update);
241+
};
242+
163243
export default async function init(el) {
164244
const dataHref = el.querySelector('a[href$=".json"]')?.href;
165245

@@ -187,5 +267,14 @@ export default async function init(el) {
187267
return heading.id;
188268
});
189269
if (table && labelIds.length) { table.setAttribute('aria-labelledby', labelIds.join(' ')); }
190-
el.tabIndex = 0;
270+
if (table) {
271+
const scroller = buildScroller(table, labelIds);
272+
const instructions = document.createElement('span');
273+
instructions.id = `table-scroll-instructions-${Math.random().toString(36).slice(2)}`;
274+
instructions.className = 'visually-hidden';
275+
instructions.textContent = 'Scrollable table. Use the left and right arrow keys to view more.';
276+
instructions.hidden = true;
277+
el.replaceChildren(scroller, instructions);
278+
watchScrollHint(el, scroller, instructions);
279+
}
191280
}

‎test/a11y/accessibility.homepage.spec.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { test, expect } from './axe-test.js';
33

44
// TODO: these are current skipped right now because aem.page (the proxy) is gated
55
test.describe('homepage', () => {
6-
test.skip('should not have any automatically detectable accessibility issues', async ({ page }) => {
6+
test('should not have any automatically detectable accessibility issues', async ({ page }) => {
77
await page.goto('/');
88
await page.waitForLoadState('networkidle');
99

@@ -12,7 +12,7 @@ test.describe('homepage', () => {
1212
expect(accessibilityScanResults.violations).toEqual([]);
1313
});
1414

15-
test.skip('should not have any automatically detectable WCAG 2.2 A or AA violations', async ({ page, makeAxeBuilder }) => {
15+
test('should not have any automatically detectable WCAG 2.2 A or AA violations', async ({ page, makeAxeBuilder }) => {
1616
await page.goto('/');
1717
await page.waitForLoadState('networkidle');
1818

0 commit comments

Comments
 (0)