@adia-ai/web-components 0.8.44 → 0.8.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/components/card/card.css +12 -23
- package/components/card/card.yaml +11 -0
- package/components/chart/chart.a2ui.json +16 -1
- package/components/chart/chart.class.js +611 -41
- package/components/chart/chart.css +174 -0
- package/components/chart/chart.d.ts +5 -1
- package/components/chart/chart.yaml +45 -1
- package/components/context-menu/context-menu.a2ui.json +8 -3
- package/components/context-menu/context-menu.class.js +46 -5
- package/components/context-menu/context-menu.d.ts +6 -3
- package/components/context-menu/context-menu.examples.md +2 -2
- package/components/context-menu/context-menu.yaml +22 -5
- package/components/field/field.css +24 -2
- package/components/index.js +1 -0
- package/components/input/input.css +7 -0
- package/components/nav/nav.a2ui.json +2 -2
- package/components/nav/nav.css +1 -1
- package/components/nav/nav.d.ts +1 -1
- package/components/nav/nav.yaml +14 -3
- package/components/nav-group/nav-group.css +37 -3
- package/components/pagination/pagination.class.js +52 -22
- package/components/search/search.class.js +39 -5
- package/components/select/select.a2ui.json +5 -0
- package/components/select/select.class.js +20 -0
- package/components/select/select.css +25 -0
- package/components/select/select.d.ts +2 -0
- package/components/select/select.yaml +12 -0
- package/components/table/cell-types.js +9 -0
- package/components/table/table.a2ui.json +19 -4
- package/components/table/table.class.js +410 -45
- package/components/table/table.css +7 -4
- package/components/table/table.d.ts +9 -3
- package/components/table/table.yaml +115 -9
- package/components/table-footer/table-footer.a2ui.json +150 -0
- package/components/table-footer/table-footer.class.js +391 -0
- package/components/table-footer/table-footer.css +64 -0
- package/components/table-footer/table-footer.d.ts +39 -0
- package/components/table-footer/table-footer.examples.md +46 -0
- package/components/table-footer/table-footer.js +17 -0
- package/components/table-footer/table-footer.yaml +219 -0
- package/components/table-toolbar/table-toolbar.a2ui.json +15 -0
- package/components/table-toolbar/table-toolbar.class.js +61 -17
- package/components/table-toolbar/table-toolbar.css +34 -0
- package/components/table-toolbar/table-toolbar.d.ts +10 -0
- package/components/table-toolbar/table-toolbar.yaml +58 -15
- package/core/data-stream.js +37 -2
- package/core/index.d.ts +1 -0
- package/core/index.js +1 -0
- package/core/provider.d.ts +9 -13
- package/core/provider.js +9 -113
- package/core/store.d.ts +46 -0
- package/core/store.js +89 -0
- package/custom-elements.json +192 -8
- package/dist/host.min.css +1 -1
- package/dist/host.sheet.js +1 -1
- package/dist/theme-provider.min.js +1 -1
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +96 -96
- package/dist/web-components.sheet.js +1 -1
- package/package.json +1 -1
- package/patterns/chart-in-card/chart-in-card.examples.html +36 -9
- package/patterns/new-enrollments/new-enrollments.examples.html +140 -0
- package/patterns/new-enrollments/new-enrollments.html +54 -0
- package/patterns/table-in-card/table-in-card.examples.html +168 -0
- package/patterns/table-in-card/table-in-card.examples.js +139 -0
- package/patterns/table-in-card/table-in-card.html +86 -0
- package/styles/api/sizing.css +46 -0
- package/styles/components.css +1 -0
package/components/nav/nav.yaml
CHANGED
|
@@ -38,7 +38,7 @@ props:
|
|
|
38
38
|
heading:
|
|
39
39
|
type: string
|
|
40
40
|
default: ''
|
|
41
|
-
description: "Optional kicker label. Section variant renders it via ::before; primary uses it as aria-label only."
|
|
41
|
+
description: "Optional kicker label. Section variant renders it via ::before; primary uses it as aria-label only. For a VISIBLE kicker inside a primary-variant rail — or more than one kicker per <nav-ui> — hand-place <span data-nav-label> in the default slot instead; see slots below."
|
|
42
42
|
multiExpand:
|
|
43
43
|
type: boolean
|
|
44
44
|
default: false
|
|
@@ -62,7 +62,7 @@ events:
|
|
|
62
62
|
|
|
63
63
|
slots:
|
|
64
64
|
default:
|
|
65
|
-
description: "Primary slot — accepts <nav-group-ui> + <nav-item-ui> children, plus <hr data-nav-divider> for hand-placed dividers."
|
|
65
|
+
description: "Primary slot — accepts <nav-group-ui> + <nav-item-ui> children, plus <hr data-nav-divider> for hand-placed dividers and <span data-nav-label> for hand-placed group-label kickers (titled runs of items/groups that aren't wrapped in a <nav-group-ui>). <span data-nav-label> renders with the same uppercase/tracking/muted kicker treatment as the [heading] ::before kicker, and is hidden alongside dividers whenever the primary-variant rail collapses ([collapsed] or ≤96px container width)."
|
|
66
66
|
|
|
67
67
|
states:
|
|
68
68
|
- name: idle
|
|
@@ -79,7 +79,18 @@ a2ui:
|
|
|
79
79
|
wrapped in <section-ui> for app sidebars; inside an
|
|
80
80
|
<aside data-subnav> with variant="section" for section / subnav
|
|
81
81
|
rails; standalone on docs / auth pages. Children: <nav-group-ui>,
|
|
82
|
-
<nav-item-ui>, optional <hr data-nav-divider
|
|
82
|
+
<nav-item-ui>, optional <hr data-nav-divider>, optional
|
|
83
|
+
<span data-nav-label> (hand-placed titled section header).
|
|
84
|
+
- >-
|
|
85
|
+
Titled nav-item groups: hand-place <span data-nav-label>Text</span>
|
|
86
|
+
directly in the default slot, before a run of <nav-item-ui> /
|
|
87
|
+
<nav-group-ui> children, to render a section-header kicker — the
|
|
88
|
+
same uppercase/tracking/muted treatment as the [heading] ::before
|
|
89
|
+
kicker on variant="section", but usable anywhere in the child list
|
|
90
|
+
(not limited to one kicker per <nav-ui>) and always visually
|
|
91
|
+
rendered, unlike [heading] which is aria-only on variant="primary".
|
|
92
|
+
Hidden automatically whenever the primary-variant rail collapses
|
|
93
|
+
([collapsed] or ≤96px container width), same as <hr data-nav-divider>.
|
|
83
94
|
- >-
|
|
84
95
|
Variants: variant="primary" (default) — app sidebar; ResizeObserver
|
|
85
96
|
collapses to icon-only ≤96px; collapsible groups open as a popover
|
|
@@ -17,13 +17,31 @@
|
|
|
17
17
|
selected — bg container · icon + text ON-SURFACE
|
|
18
18
|
The selected icon no longer takes primary (supersedes gh#522's
|
|
19
19
|
glyph ruling); Figma's neutral/surfaceBright was considered for the
|
|
20
|
-
selected fill and RULED OUT — container stays (gh#795's mirror).
|
|
21
|
-
|
|
20
|
+
selected fill and RULED OUT — container stays (gh#795's mirror).
|
|
21
|
+
gh#968's ladder above is about ROUTE selection (hover/selected);
|
|
22
|
+
it says nothing about the group's own open/closed disclosure state,
|
|
23
|
+
which --nav-group-fg left pinned unconditionally to one role
|
|
24
|
+
(neutral-high) regardless of [open]. gh#1740 (operator ruling,
|
|
25
|
+
2026-08-19) supersedes that unconditional pin with a second,
|
|
26
|
+
orthogonal axis — the header text and caret glyph now also shift
|
|
27
|
+
role by [open], the same way they already shift by [selected]/
|
|
28
|
+
hover; the two ladders compose (a selected-route header still wins
|
|
29
|
+
its --nav-group-fg-selected treatment regardless of open state —
|
|
30
|
+
see the :scope[open] rule below, kept lower-precedence than
|
|
31
|
+
:scope[selected] by source order). */
|
|
32
|
+
--nav-group-fg: var(--md-sys-color-neutral);
|
|
33
|
+
--nav-group-fg-active: var(--md-sys-color-neutral-high);
|
|
22
34
|
--nav-group-fg-hover: var(--a-fg-strong);
|
|
23
35
|
--nav-group-fg-muted: var(--a-fg-muted);
|
|
24
36
|
--nav-group-fg-selected: var(--a-fg-strong);
|
|
25
37
|
--nav-group-icon-fg: var(--md-sys-color-neutral);
|
|
26
38
|
--nav-group-icon-fg-selected: var(--a-fg-strong);
|
|
39
|
+
/* Caret role ladder (gh#1740) — idle/closed: NEUTRAL-LOW (one step
|
|
40
|
+
quieter than the muted badge/kicker role); open: NEUTRAL, matching
|
|
41
|
+
the icon's own idle role so the caret doesn't outshine the icon
|
|
42
|
+
once the group is expanded. */
|
|
43
|
+
--nav-group-caret-fg: var(--md-sys-color-neutral-low);
|
|
44
|
+
--nav-group-caret-fg-active: var(--md-sys-color-neutral);
|
|
27
45
|
--nav-group-bg-hover: var(--a-bg-muted);
|
|
28
46
|
--nav-group-bg-selected: var(--a-bg-selected); /* mirrors --nav-item-bg-selected (gh#795) */
|
|
29
47
|
--nav-group-icon-size: calc(var(--nav-group-row-height) - var(--a-space-2));
|
|
@@ -71,6 +89,20 @@
|
|
|
71
89
|
background: var(--nav-group-indent-rail-bg, var(--md-sys-color-neutral-outline-variant));
|
|
72
90
|
pointer-events: none;
|
|
73
91
|
}
|
|
92
|
+
/* Disclosure-state ladder (gh#1740, operator ruling 2026-08-19): the
|
|
93
|
+
header text and caret glyph step UP a role when the group is open —
|
|
94
|
+
idle/closed reads NEUTRAL, expanded reads NEUTRAL-HIGH. Placed BEFORE
|
|
95
|
+
the [selected]/[data-selected-within] block below so an equal-
|
|
96
|
+
specificity (0,3,0) tie always resolves to the route-selection color:
|
|
97
|
+
a selected-route group keeps its existing --nav-group-fg-selected
|
|
98
|
+
treatment regardless of its own open/closed state — this rule only
|
|
99
|
+
ever wins when the group is open AND not selected. */
|
|
100
|
+
:scope[open] > [slot="header"] {
|
|
101
|
+
color: var(--nav-group-fg-active);
|
|
102
|
+
}
|
|
103
|
+
:scope[open] > [slot="header"] [slot="caret"] {
|
|
104
|
+
color: var(--nav-group-caret-fg-active);
|
|
105
|
+
}
|
|
74
106
|
/* Selected-look header treatment keys on [selected] — the CSS half of
|
|
75
107
|
gh issue 459 (PR 460 corrected the icon weight): the group CONTAINING
|
|
76
108
|
the selected route reads strong; a merely-expanded group does not.
|
|
@@ -174,7 +206,9 @@
|
|
|
174
206
|
|
|
175
207
|
[slot="header"] [slot="caret"] {
|
|
176
208
|
/*--a-icon-size: 0.5rem;*/
|
|
177
|
-
|
|
209
|
+
/* idle/closed caret = NEUTRAL-LOW (gh#1740 role ladder); steps up to
|
|
210
|
+
--nav-group-caret-fg-active via the :scope[open] rule above. */
|
|
211
|
+
color: var(--nav-group-caret-fg);
|
|
178
212
|
transition:
|
|
179
213
|
transform var(--nav-duration-fast) var(--nav-easing),
|
|
180
214
|
color var(--nav-duration-fast) var(--nav-easing);
|
|
@@ -24,6 +24,32 @@
|
|
|
24
24
|
import { UIElement } from '../../core/element.js';
|
|
25
25
|
import { KEY_MAP } from '../../core/template.js';
|
|
26
26
|
|
|
27
|
+
// ── SSR idempotent-write guard (gh#1755, follow-up to table-ui's gh#1678) ──
|
|
28
|
+
//
|
|
29
|
+
// Local copies of table.class.js's own setAttrIfChanged()/removeAttrIfPresent()
|
|
30
|
+
// — module-local there too (not exported), so a shared-utility extraction for
|
|
31
|
+
// two call sites would be a premature abstraction; these mirror that shape.
|
|
32
|
+
//
|
|
33
|
+
/**
|
|
34
|
+
* Idempotent attribute set — skip the DOM write entirely when the value
|
|
35
|
+
* already matches. `setAttribute` mutates (and queues a MutationObserver
|
|
36
|
+
* record) even when the new value is byte-identical to the old one, so a
|
|
37
|
+
* per-item attribute this render path sets on EVERY call (`aria-current`,
|
|
38
|
+
* `variant`, `disabled`, `tabindex`, `text`, `aria-label`, `data-value`,
|
|
39
|
+
* `size`) must compare-before-write to let a byte-identical adopted SSR
|
|
40
|
+
* `<nav>` (gh#1687) survive re-render with zero mutations — bare
|
|
41
|
+
* `setAttribute` cannot give that guarantee even when the value never
|
|
42
|
+
* actually changes.
|
|
43
|
+
*/
|
|
44
|
+
function setAttrIfChanged(el, name, value) {
|
|
45
|
+
if (el.getAttribute(name) !== value) el.setAttribute(name, value);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Idempotent attribute removal — same rationale as setAttrIfChanged. */
|
|
49
|
+
function removeAttrIfPresent(el, name) {
|
|
50
|
+
if (el.hasAttribute(name)) el.removeAttribute(name);
|
|
51
|
+
}
|
|
52
|
+
|
|
27
53
|
export class UIPagination extends UIElement {
|
|
28
54
|
static properties = {
|
|
29
55
|
page: { type: Number, default: 1, reflect: true },
|
|
@@ -251,21 +277,21 @@ export class UIPagination extends UIElement {
|
|
|
251
277
|
}
|
|
252
278
|
|
|
253
279
|
const btn = document.createElement('button-ui');
|
|
254
|
-
btn
|
|
280
|
+
setAttrIfChanged(btn, 'size', this.size);
|
|
255
281
|
|
|
256
282
|
if (item.type === 'prev') {
|
|
257
283
|
btn.setAttribute('data-prev', '');
|
|
258
|
-
btn
|
|
259
|
-
btn
|
|
284
|
+
setAttrIfChanged(btn, 'icon', 'caret-left');
|
|
285
|
+
setAttrIfChanged(btn, 'aria-label', 'Previous page');
|
|
260
286
|
} else if (item.type === 'next') {
|
|
261
287
|
btn.setAttribute('data-next', '');
|
|
262
|
-
btn
|
|
263
|
-
btn
|
|
288
|
+
setAttrIfChanged(btn, 'icon', 'caret-right');
|
|
289
|
+
setAttrIfChanged(btn, 'aria-label', 'Next page');
|
|
264
290
|
} else {
|
|
265
291
|
btn.setAttribute('data-page', '');
|
|
266
|
-
btn
|
|
267
|
-
btn
|
|
268
|
-
btn
|
|
292
|
+
setAttrIfChanged(btn, 'data-value', String(item.value));
|
|
293
|
+
setAttrIfChanged(btn, 'text', String(item.value));
|
|
294
|
+
setAttrIfChanged(btn, 'aria-label', `Page ${item.value}`);
|
|
269
295
|
}
|
|
270
296
|
|
|
271
297
|
this.#updateItem(btn, item, page);
|
|
@@ -276,32 +302,36 @@ export class UIPagination extends UIElement {
|
|
|
276
302
|
if (item.type === 'ellipsis') return;
|
|
277
303
|
|
|
278
304
|
// Keep size in sync \u2014 the host's [size] may change between renders.
|
|
279
|
-
|
|
305
|
+
// gh#1755 \u2014 compare-before-write: this runs on EVERY render pass
|
|
306
|
+
// (including one triggered by an unrelated reactive dependency), so a
|
|
307
|
+
// bare setAttribute() would re-mutate an already-correct adopted
|
|
308
|
+
// gh#1687 SSR `<nav>` child even when nothing here actually changed.
|
|
309
|
+
setAttrIfChanged(el, 'size', this.size);
|
|
280
310
|
|
|
281
311
|
if (item.type === 'prev') {
|
|
282
|
-
el
|
|
283
|
-
if (page <= 1) { el
|
|
284
|
-
else { el
|
|
312
|
+
setAttrIfChanged(el, 'variant', this.#restVariant());
|
|
313
|
+
if (page <= 1) { setAttrIfChanged(el, 'disabled', ''); setAttrIfChanged(el, 'tabindex', '-1'); }
|
|
314
|
+
else { removeAttrIfPresent(el, 'disabled'); setAttrIfChanged(el, 'tabindex', '0'); }
|
|
285
315
|
} else if (item.type === 'next') {
|
|
286
|
-
el
|
|
287
|
-
if (page >= this.total) { el
|
|
288
|
-
else { el
|
|
316
|
+
setAttrIfChanged(el, 'variant', this.#restVariant());
|
|
317
|
+
if (page >= this.total) { setAttrIfChanged(el, 'disabled', ''); setAttrIfChanged(el, 'tabindex', '-1'); }
|
|
318
|
+
else { removeAttrIfPresent(el, 'disabled'); setAttrIfChanged(el, 'tabindex', '0'); }
|
|
289
319
|
} else {
|
|
290
|
-
el
|
|
291
|
-
el
|
|
292
|
-
el
|
|
320
|
+
setAttrIfChanged(el, 'data-value', String(item.value));
|
|
321
|
+
setAttrIfChanged(el, 'text', String(item.value));
|
|
322
|
+
setAttrIfChanged(el, 'aria-label', `Page ${item.value}`);
|
|
293
323
|
if (item.value === page) {
|
|
294
324
|
// Active page reads as `variant="primary"` so the filled-accent
|
|
295
325
|
// state comes from button-ui's primary surface matrix (the
|
|
296
326
|
// canonical token chain) \u2014 not a pagination-tier re-impl.
|
|
297
|
-
el
|
|
327
|
+
setAttrIfChanged(el, 'variant', 'primary');
|
|
298
328
|
// aria-current="page" alone carries the active-page state — no
|
|
299
329
|
// private data-active mirror (gh#1332 Category A; admin-roster's
|
|
300
330
|
// aria-checked precedent from PR #1438).
|
|
301
|
-
el
|
|
331
|
+
setAttrIfChanged(el, 'aria-current', 'page');
|
|
302
332
|
} else {
|
|
303
|
-
el
|
|
304
|
-
el
|
|
333
|
+
setAttrIfChanged(el, 'variant', this.#restVariant());
|
|
334
|
+
removeAttrIfPresent(el, 'aria-current');
|
|
305
335
|
}
|
|
306
336
|
}
|
|
307
337
|
}
|
|
@@ -22,6 +22,30 @@
|
|
|
22
22
|
|
|
23
23
|
import { UIFormElement } from '../../core/form.js';
|
|
24
24
|
|
|
25
|
+
// ── SSR idempotent-write guard (gh#1755, follow-up to table-ui's gh#1678) ──
|
|
26
|
+
//
|
|
27
|
+
// Local copies of table.class.js's own setAttrIfChanged()/removeAttrIfPresent()
|
|
28
|
+
// — module-local there too (not exported), so a shared-utility extraction for
|
|
29
|
+
// two call sites would be a premature abstraction; these mirror that shape.
|
|
30
|
+
//
|
|
31
|
+
/**
|
|
32
|
+
* Idempotent attribute set — skip the DOM write entirely when the value
|
|
33
|
+
* already matches. `setAttribute` mutates (and queues a MutationObserver
|
|
34
|
+
* record) even when the new value is byte-identical to the old one, so a
|
|
35
|
+
* host/child attribute this render path sets on EVERY call (`placeholder`,
|
|
36
|
+
* `value`) must compare-before-write to let a byte-identical SSR-rendered
|
|
37
|
+
* `<input-ui>` survive upgrade with zero mutations — bare `setAttribute`
|
|
38
|
+
* cannot give that guarantee even when the value never actually changes.
|
|
39
|
+
*/
|
|
40
|
+
function setAttrIfChanged(el, name, value) {
|
|
41
|
+
if (el.getAttribute(name) !== value) el.setAttribute(name, value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Idempotent attribute removal — same rationale as setAttrIfChanged. */
|
|
45
|
+
function removeAttrIfPresent(el, name) {
|
|
46
|
+
if (el.hasAttribute(name)) el.removeAttribute(name);
|
|
47
|
+
}
|
|
48
|
+
|
|
25
49
|
export class UISearch extends UIFormElement {
|
|
26
50
|
static properties = {
|
|
27
51
|
...UIFormElement.properties,
|
|
@@ -37,7 +61,10 @@ export class UISearch extends UIFormElement {
|
|
|
37
61
|
|
|
38
62
|
connected() {
|
|
39
63
|
super.connected();
|
|
40
|
-
this
|
|
64
|
+
// gh#1755 — a byte-identical SSR-rendered host already carries this
|
|
65
|
+
// attribute; an unconditional setAttribute() here would still queue a
|
|
66
|
+
// mutation record even though the value never actually changes.
|
|
67
|
+
setAttrIfChanged(this, 'role', 'search');
|
|
41
68
|
|
|
42
69
|
if (!this.querySelector('input-ui')) {
|
|
43
70
|
const size = this.getAttribute('size');
|
|
@@ -69,12 +96,19 @@ export class UISearch extends UIFormElement {
|
|
|
69
96
|
render() {
|
|
70
97
|
if (!this.#inputEl) return;
|
|
71
98
|
|
|
72
|
-
this
|
|
73
|
-
|
|
74
|
-
|
|
99
|
+
// gh#1755 — this render effect re-runs on ANY of this element's
|
|
100
|
+
// reactive properties changing (element.js's connectedCallback effect
|
|
101
|
+
// reads every signal unconditionally), not only placeholder/disabled/
|
|
102
|
+
// value themselves — so a bare setAttribute() here would re-mutate an
|
|
103
|
+
// already-correct adopted `<input-ui>` (or this host's own `value`
|
|
104
|
+
// attribute) on every unrelated re-render, defeating SSR zero-mutation
|
|
105
|
+
// upgrade even when nothing this pass actually cares about changed.
|
|
106
|
+
setAttrIfChanged(this.#inputEl, 'placeholder', this.placeholder);
|
|
107
|
+
if (this.disabled) setAttrIfChanged(this.#inputEl, 'disabled', '');
|
|
108
|
+
else removeAttrIfPresent(this.#inputEl, 'disabled');
|
|
75
109
|
|
|
76
110
|
// Reflect value for CSS (clear button visibility)
|
|
77
|
-
this
|
|
111
|
+
setAttrIfChanged(this, 'value', this.value || '');
|
|
78
112
|
}
|
|
79
113
|
|
|
80
114
|
#onInput = () => {
|
|
@@ -66,6 +66,11 @@
|
|
|
66
66
|
"type": "string",
|
|
67
67
|
"default": ""
|
|
68
68
|
},
|
|
69
|
+
"label-hidden": {
|
|
70
|
+
"description": "When true, [label] still sets the accessible name (aria-label) but the visible `::before` text is suppressed via the canonical sr-only technique (gh#1748, mirrors check-ui's [label-hidden], gh#1010). Use when a sibling/ancestor composition already conveys the same name visually (e.g. a scope/view-switcher select in a table-toolbar-ui [slot=\"scope\"], where the toolbar's own title already names the view) and a second visible \"Scope\"-style label would paint it twice.",
|
|
71
|
+
"type": "boolean",
|
|
72
|
+
"default": false
|
|
73
|
+
},
|
|
69
74
|
"mark": {
|
|
70
75
|
"description": "Renders the Adia brand mark (`<adia-mark-ui>`) as the leading visual — takes precedence over avatar and icon. Token-driven (light/dark handled internally), so it fits a scheme-switching workspace/app switcher where a static logo URL can't invert. Per-option `mark` on an `<option>`/options-array entry works the same way, scoped to that row.",
|
|
71
76
|
"type": "boolean",
|
|
@@ -47,6 +47,11 @@ export class UISelect extends UIFormElement {
|
|
|
47
47
|
size: { type: String, default: 'md', reflect: true },
|
|
48
48
|
open: { type: Boolean, default: false, reflect: true },
|
|
49
49
|
label: { type: String, default: '', reflect: true },
|
|
50
|
+
// gh#1748: keeps [label] as the accessible name (bridged to an explicit
|
|
51
|
+
// aria-label in #syncAccessibleName below) while suppressing the VISIBLE
|
|
52
|
+
// `::before` text (select.css) — for compositions that already show the
|
|
53
|
+
// name elsewhere. Mirrors check-ui's [label-hidden] (gh#1010).
|
|
54
|
+
labelHidden: { type: Boolean, default: false, reflect: true, attribute: 'label-hidden' },
|
|
50
55
|
icon: { type: String, default: '', reflect: true },
|
|
51
56
|
avatar: { type: String, default: '', reflect: true },
|
|
52
57
|
mark: { type: Boolean, default: false, reflect: true },
|
|
@@ -361,6 +366,21 @@ export class UISelect extends UIFormElement {
|
|
|
361
366
|
*/
|
|
362
367
|
#syncAccessibleName() {
|
|
363
368
|
const DEFAULT_PLACEHOLDER = 'Select...'; // matches static properties.placeholder.default
|
|
369
|
+
// gh#1748: [label-hidden] suppresses the visible `::before` text
|
|
370
|
+
// (select.css's `attr(label)` content) but [label] must still name the
|
|
371
|
+
// control — bridge it through the SAME tracked-ownership pipeline used
|
|
372
|
+
// below for the placeholder-only case, so a consumer's own directly-set
|
|
373
|
+
// aria-label is never clobbered (gh#1646 guarantee extends for free) and
|
|
374
|
+
// toggling [label-hidden] back off cleanly releases the bridged value
|
|
375
|
+
// (falls through to the visible-label path, which sets no aria-label —
|
|
376
|
+
// the generated `::before` content supplies the name instead, unchanged
|
|
377
|
+
// from today). Mirrors check-ui's [label-hidden] (gh#1010), which
|
|
378
|
+
// instead sets aria-label unconditionally since check-ui never relies
|
|
379
|
+
// on generated-content naming.
|
|
380
|
+
if (this.labelHidden && this.label) {
|
|
381
|
+
this.#lastAutoAriaLabel = syncAutoAriaLabel(this, this.#lastAutoAriaLabel, this.label);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
364
384
|
const labeledElsewhere = !!this.label || this.hasAttribute('aria-labelledby');
|
|
365
385
|
const namable = !labeledElsewhere
|
|
366
386
|
&& this.placeholder && this.placeholder !== DEFAULT_PLACEHOLDER;
|
|
@@ -98,6 +98,31 @@
|
|
|
98
98
|
:scope:not([label])::before,
|
|
99
99
|
:scope[label=""]::before { display: none; }
|
|
100
100
|
|
|
101
|
+
/* Visually-hidden label (gh#1748) — [label-hidden] keeps the a11y name
|
|
102
|
+
(aria-label, bridged from [label] in select.class.js's
|
|
103
|
+
#syncAccessibleName()) but drops the visible text, for compositions
|
|
104
|
+
that already show the name elsewhere (e.g. a table-toolbar-ui
|
|
105
|
+
[slot="scope"] select whose scope/view name is redundant chrome next
|
|
106
|
+
to the toolbar's own title). Same canonical sr-only recipe as
|
|
107
|
+
<visually-hidden-ui> (visually-hidden.css) and check-ui's own
|
|
108
|
+
[label-hidden] (gh#1010) — clip-path does the real hiding, the rest is
|
|
109
|
+
belt-and-suspenders for cross-UA. More specific than the bare
|
|
110
|
+
`::before` rule above, so it wins whenever both [label] and
|
|
111
|
+
[label-hidden] are set. */
|
|
112
|
+
:scope[label][label-hidden]::before {
|
|
113
|
+
content: attr(label);
|
|
114
|
+
position: absolute !important;
|
|
115
|
+
width: 1px !important;
|
|
116
|
+
height: 1px !important;
|
|
117
|
+
padding: 0 !important;
|
|
118
|
+
margin: -1px !important;
|
|
119
|
+
overflow: hidden !important;
|
|
120
|
+
clip: rect(0, 0, 0, 0) !important;
|
|
121
|
+
clip-path: inset(100%) !important;
|
|
122
|
+
white-space: nowrap !important;
|
|
123
|
+
border: 0 !important;
|
|
124
|
+
}
|
|
125
|
+
|
|
101
126
|
/* Trigger */
|
|
102
127
|
[slot="trigger"] {
|
|
103
128
|
display: flex;
|
|
@@ -45,6 +45,8 @@ export class UISelect extends UIFormElement {
|
|
|
45
45
|
/** Open/closed reflected attribute — toggled by trigger click / keyboard. */
|
|
46
46
|
open: boolean;
|
|
47
47
|
label: string;
|
|
48
|
+
/** When true, [label] still sets the accessible name but the visible text is sr-only hidden (gh#1748, mirrors check-ui's [label-hidden], gh#1010). */
|
|
49
|
+
labelHidden: boolean;
|
|
48
50
|
/** Leading icon name (Phosphor registry). */
|
|
49
51
|
icon: string;
|
|
50
52
|
/** Leading avatar URL or name. */
|
|
@@ -104,6 +104,18 @@ props:
|
|
|
104
104
|
description: Label text above the trigger
|
|
105
105
|
type: string
|
|
106
106
|
default: ""
|
|
107
|
+
label-hidden:
|
|
108
|
+
description: >-
|
|
109
|
+
When true, [label] still sets the accessible name (aria-label) but the
|
|
110
|
+
visible `::before` text is suppressed via the canonical sr-only
|
|
111
|
+
technique (gh#1748, mirrors check-ui's [label-hidden], gh#1010). Use
|
|
112
|
+
when a sibling/ancestor composition already conveys the same name
|
|
113
|
+
visually (e.g. a scope/view-switcher select in a table-toolbar-ui
|
|
114
|
+
[slot="scope"], where the toolbar's own title already names the
|
|
115
|
+
view) and a second visible "Scope"-style label would paint it twice.
|
|
116
|
+
type: boolean
|
|
117
|
+
default: false
|
|
118
|
+
reflect: true
|
|
107
119
|
hint:
|
|
108
120
|
description: |-
|
|
109
121
|
§184 (v0.5.5, FEEDBACK-08 §7): small caption rendered beneath the select. Sets `aria-describedby` on the host so screen readers announce it as a description (distinct from `aria-label`, which comes from `label`). Does not conflict with the in-component `label`.
|
|
@@ -312,6 +312,15 @@ registerCellType('progress', {
|
|
|
312
312
|
|
|
313
313
|
// 13. actions
|
|
314
314
|
registerCellType('actions', {
|
|
315
|
+
// gh#1678 CodeRabbit follow-up: this render() calls addEventListener()
|
|
316
|
+
// directly on a plain node it creates — unlike every other cell type
|
|
317
|
+
// above (which only set attributes on custom elements, safe to adopt
|
|
318
|
+
// structurally), a discarded 'actions' cell loses a real listener with
|
|
319
|
+
// no way for isEqualNode() to see the loss. `attachesListeners: true`
|
|
320
|
+
// tells table.class.js's adoptOrDiffChildren() (via RENDERER_OWNED) to
|
|
321
|
+
// always replace this cell rather than ever adopt it on structural
|
|
322
|
+
// equality alone. See ssr-compatibility's references/guard-patterns.md §4.1.
|
|
323
|
+
attachesListeners: true,
|
|
315
324
|
render(value, row, cell, meta) {
|
|
316
325
|
const actions = meta?.actions || [];
|
|
317
326
|
let wrapper = cell.querySelector('row-ui');
|
|
@@ -34,6 +34,11 @@
|
|
|
34
34
|
"type": "boolean",
|
|
35
35
|
"default": false
|
|
36
36
|
},
|
|
37
|
+
"filteredCount": {
|
|
38
|
+
"description": "Read-only. The row count AFTER search + column filters apply but BEFORE pagination slices it (gh#1807, ADR-0080, REQ-W-006) — a plain JS getter, no setter, never a reflected attribute. table-footer-ui's client-mode range-total derivation reads this directly; a raw `.data.length` read (the toolbar's own `count` fallback precedent) would double-count a filtered-out row. In server mode ([range-total] set, gh#1754, ADR-0082), filteredCount stays page-scoped — the loaded page's rows after local filters, NOT the server total across all pages; [range-total] is the server total, filteredCount never is (REQ-D-006).",
|
|
39
|
+
"type": "number",
|
|
40
|
+
"readOnly": true
|
|
41
|
+
},
|
|
37
42
|
"frameless": {
|
|
38
43
|
"description": "Drop the table's outer 1px perimeter and nothing else (gh#511) — row dividers are untouched (the perimeter is split onto its own --table-perimeter token, defaulting to --table-border). For card-composed tables: the card owns the frame, so the table's square perimeter otherwise renders a doubled edge (flush bleed) or a stray inner box (with content above). Standalone tables keep their chrome by default. Theming path without the attribute: set --table-perimeter to transparent per instance. Note: `<card-ui><section bleed>` already drops a direct-child table-ui's chrome automatically (card.css, gh#796) — `frameless`/`raw` on the instance still applies and is honored, but is no longer required just to avoid the doubled-edge look in that composition.",
|
|
39
44
|
"type": "boolean",
|
|
@@ -44,8 +49,18 @@
|
|
|
44
49
|
"type": "boolean",
|
|
45
50
|
"default": false
|
|
46
51
|
},
|
|
52
|
+
"no-pager": {
|
|
53
|
+
"description": "Hide the internal pagination bar while leaving [paginate] slicing, page state, the `page` event, and the `footer-page` command listener all intact (gh#1807, ADR-0080). The anti-doubled-pager mechanism for a table-footer-ui composition, client- or server-mode alike (gh#1754, ADR-0082): without it, a bound footer's own composed pager and the table's own internal one would both render for any non-empty paginated table. Explicit author intent — never suppressed automatically just because a footer happens to be bound.",
|
|
54
|
+
"type": "boolean",
|
|
55
|
+
"default": false
|
|
56
|
+
},
|
|
47
57
|
"paginate": {
|
|
48
|
-
"description": "Rows per page. 0 = show all rows without pagination. When > 0, renders
|
|
58
|
+
"description": "Rows per page. 0 = show all rows without pagination. When > 0, renders the internal pagination bar below the table — UNLESS [no-pager] is also set (gh#1807, ADR-0080), which keeps [paginate]'s slicing/page-state/`page`-event/`footer-page`-listener behavior but suppresses only the bar itself (the anti-doubled-pager path for a table-footer-ui composition). When [range-total] is also set (gh#1754, ADR-0082), [paginate] becomes purely presentational — it supplies only the page-size for pager math; no local slicing runs.",
|
|
59
|
+
"type": "number",
|
|
60
|
+
"default": 0
|
|
61
|
+
},
|
|
62
|
+
"range-total": {
|
|
63
|
+
"description": "Total row count across ALL server pages (gh#1754, ADR-0082) — the table-authoritative server-mode data contract. Presence-gated: the attribute's PRESENCE, not its value, is the mode switch (checked via hasAttribute, never the coerced value alone — the same discipline table-footer-ui's own [range-total] ships). Absent (the default) means client mode: today's behavior exactly, [paginate] slices `.data` as always. Present with [paginate] > 0 means server mode: no local slicing (`.data` IS the current page and renders whole, after local search/sort/filter), the internal pager's page count becomes `max(1, ceil(range-total / paginate))` (one calc site, consolidated), and the internal page-reset sites (`data` set, `setFilter`, `clearFilters`) are suppressed so a fetch write-back never fights the pager back to page 0 — page state then moves only via pager interaction, the `footer-page` command, or `setState()`. The existing `page` event (0-based, unchanged) becomes the fetch trigger: the consumer listens for it, fetches that server page, and writes `.data` back. Explicit `range-total=\"0\"` is server-confirmed empty (pager hidden), never conflated with absent. With [paginate] absent/0, [range-total] is inert for this table's own rendering but stays readable by a bound table-footer-ui (REQ-W-006 branch 2) for its count-only label — see the footer-authoritative shape below, which remains lawful and is unaffected by this attribute. Named identically to table-toolbar-ui's and table-footer-ui's own [range-total] (rows, never `pagination-ui[total]`'s pages — the ADR-0063 B5 collision rule); a third instance of one name for one concept.",
|
|
49
64
|
"type": "number",
|
|
50
65
|
"default": 0
|
|
51
66
|
},
|
|
@@ -55,7 +70,7 @@
|
|
|
55
70
|
"default": false
|
|
56
71
|
},
|
|
57
72
|
"search": {
|
|
58
|
-
"description": "Global search/filter string. Filters visible rows across all columns using case-insensitive substring matching.
|
|
73
|
+
"description": "Global search/filter string. Filters visible rows across all columns using case-insensitive substring matching. Does NOT reset the current page (verified against source at the gh#1754/ADR-0082 build — unlike setFilter()/clearFilters(), no internal page reset ever ran for a search change; this description previously claimed \"resets to page 1 on change,\" which the source never did). Aligning the doc to observed behavior here, not a behavior change — a page reset on search, if wanted, is a separate client-mode ticket.",
|
|
59
74
|
"type": "string",
|
|
60
75
|
"default": ""
|
|
61
76
|
},
|
|
@@ -141,10 +156,10 @@
|
|
|
141
156
|
}
|
|
142
157
|
},
|
|
143
158
|
"page": {
|
|
144
|
-
"description": "Fired when the
|
|
159
|
+
"description": "Fired when the table's own internal page state changes — an internal pager click, or a `footer-page` command applied (REQ-W-002). This is a RESYNC notification a bound `<table-footer-ui>` listens to for its own `page` attribute (REQ-W-004) — it is NOT the public server-mode fetch trigger a consumer should listen to; use `<table-footer-ui>`'s own `page-change` event for that (1-based, gh#1754/ADR-0082 REQ-W-003). Listening to both risks a duplicate fetch or an index-base mismatch.",
|
|
145
160
|
"detail": {
|
|
146
161
|
"page": {
|
|
147
|
-
"description": "New active page index (1-based).",
|
|
162
|
+
"description": "New active page index (0-based — gh#1808; the `footer-page` command and the `page` attribute on <table-footer-ui> are 1-based, this event's own detail is not).",
|
|
148
163
|
"type": "number"
|
|
149
164
|
}
|
|
150
165
|
}
|