@chiralkit/design-system 0.1.0-alpha.0
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/LICENSE +21 -0
- package/README.md +11 -0
- package/dist/chiralkit-ds/chiralkit-ds.css +1 -0
- package/dist/chiralkit-ds/chiralkit-ds.esm.js +1 -0
- package/dist/chiralkit-ds/index.esm.js +1 -0
- package/dist/chiralkit-ds/p-038fa008.entry.js +1 -0
- package/dist/chiralkit-ds/p-2fdc1012.entry.js +1 -0
- package/dist/chiralkit-ds/p-306ec33b.entry.js +1 -0
- package/dist/chiralkit-ds/p-3f704698.entry.js +1 -0
- package/dist/chiralkit-ds/p-41081d22.entry.js +1 -0
- package/dist/chiralkit-ds/p-7d7e5aac.entry.js +1 -0
- package/dist/chiralkit-ds/p-84b801f8.entry.js +1 -0
- package/dist/chiralkit-ds/p-a28c71b4.entry.js +1 -0
- package/dist/chiralkit-ds/p-be84312f.entry.js +1 -0
- package/dist/chiralkit-ds/p-c8015fe6.entry.js +1 -0
- package/dist/chiralkit-ds/p-d36a6bea.entry.js +1 -0
- package/dist/chiralkit-ds/p-e-glNjK6.js +2 -0
- package/dist/chiralkit-ds/p-f0f9ff61.entry.js +1 -0
- package/dist/cjs/chiralkit-ds.cjs.js +24 -0
- package/dist/cjs/ds-badge.cjs.entry.js +18 -0
- package/dist/cjs/ds-button.cjs.entry.js +26 -0
- package/dist/cjs/ds-card.cjs.entry.js +17 -0
- package/dist/cjs/ds-checkbox.cjs.entry.js +67 -0
- package/dist/cjs/ds-code-block.cjs.entry.js +39 -0
- package/dist/cjs/ds-dialog.cjs.entry.js +67 -0
- package/dist/cjs/ds-input.cjs.entry.js +50 -0
- package/dist/cjs/ds-loader.cjs.entry.js +26 -0
- package/dist/cjs/ds-select.cjs.entry.js +85 -0
- package/dist/cjs/ds-tabs.cjs.entry.js +138 -0
- package/dist/cjs/ds-textarea.cjs.entry.js +50 -0
- package/dist/cjs/ds-tooltip.cjs.entry.js +233 -0
- package/dist/cjs/index-mMvyq7U_.js +2018 -0
- package/dist/cjs/index.cjs.js +10 -0
- package/dist/cjs/loader.cjs.js +12 -0
- package/dist/collection/collection-manifest.json +24 -0
- package/dist/collection/components/ds-badge/ds-badge.css +36 -0
- package/dist/collection/components/ds-badge/ds-badge.js +93 -0
- package/dist/collection/components/ds-button/ds-button.css +66 -0
- package/dist/collection/components/ds-button/ds-button.js +197 -0
- package/dist/collection/components/ds-card/ds-card.css +12 -0
- package/dist/collection/components/ds-card/ds-card.js +68 -0
- package/dist/collection/components/ds-checkbox/ds-checkbox.css +22 -0
- package/dist/collection/components/ds-checkbox/ds-checkbox.js +339 -0
- package/dist/collection/components/ds-code-block/ds-code-block.css +65 -0
- package/dist/collection/components/ds-code-block/ds-code-block.js +162 -0
- package/dist/collection/components/ds-dialog/ds-dialog.css +30 -0
- package/dist/collection/components/ds-dialog/ds-dialog.js +313 -0
- package/dist/collection/components/ds-input/ds-input.css +38 -0
- package/dist/collection/components/ds-input/ds-input.js +353 -0
- package/dist/collection/components/ds-loader/ds-loader.css +94 -0
- package/dist/collection/components/ds-loader/ds-loader.js +149 -0
- package/dist/collection/components/ds-select/ds-select.css +69 -0
- package/dist/collection/components/ds-select/ds-select.js +235 -0
- package/dist/collection/components/ds-tabs/ds-tabs.css +59 -0
- package/dist/collection/components/ds-tabs/ds-tabs.js +248 -0
- package/dist/collection/components/ds-textarea/ds-textarea.css +41 -0
- package/dist/collection/components/ds-textarea/ds-textarea.js +342 -0
- package/dist/collection/components/ds-tooltip/ds-tooltip.css +31 -0
- package/dist/collection/components/ds-tooltip/ds-tooltip.js +318 -0
- package/dist/collection/components.js +2 -0
- package/dist/collection/index.js +1 -0
- package/dist/components/ds-badge.d.ts +11 -0
- package/dist/components/ds-badge.js +1 -0
- package/dist/components/ds-button.d.ts +11 -0
- package/dist/components/ds-button.js +1 -0
- package/dist/components/ds-card.d.ts +11 -0
- package/dist/components/ds-card.js +1 -0
- package/dist/components/ds-checkbox.d.ts +11 -0
- package/dist/components/ds-checkbox.js +1 -0
- package/dist/components/ds-code-block.d.ts +11 -0
- package/dist/components/ds-code-block.js +1 -0
- package/dist/components/ds-dialog.d.ts +11 -0
- package/dist/components/ds-dialog.js +1 -0
- package/dist/components/ds-input.d.ts +11 -0
- package/dist/components/ds-input.js +1 -0
- package/dist/components/ds-loader.d.ts +11 -0
- package/dist/components/ds-loader.js +1 -0
- package/dist/components/ds-select.d.ts +11 -0
- package/dist/components/ds-select.js +1 -0
- package/dist/components/ds-tabs.d.ts +11 -0
- package/dist/components/ds-tabs.js +1 -0
- package/dist/components/ds-textarea.d.ts +11 -0
- package/dist/components/ds-textarea.js +1 -0
- package/dist/components/ds-tooltip.d.ts +11 -0
- package/dist/components/ds-tooltip.js +1 -0
- package/dist/components/index.d.ts +35 -0
- package/dist/components/index.js +1 -0
- package/dist/components/p-DDUZow8F.js +1 -0
- package/dist/components/p-ZsYP0VBq.js +1 -0
- package/dist/components/p-k0ywxWpC.js +1 -0
- package/dist/esm/chiralkit-ds.js +20 -0
- package/dist/esm/ds-badge.entry.js +16 -0
- package/dist/esm/ds-button.entry.js +24 -0
- package/dist/esm/ds-card.entry.js +15 -0
- package/dist/esm/ds-checkbox.entry.js +65 -0
- package/dist/esm/ds-code-block.entry.js +37 -0
- package/dist/esm/ds-dialog.entry.js +65 -0
- package/dist/esm/ds-input.entry.js +48 -0
- package/dist/esm/ds-loader.entry.js +24 -0
- package/dist/esm/ds-select.entry.js +83 -0
- package/dist/esm/ds-tabs.entry.js +136 -0
- package/dist/esm/ds-textarea.entry.js +48 -0
- package/dist/esm/ds-tooltip.entry.js +231 -0
- package/dist/esm/index-e-glNjK6.js +2008 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/loader.js +10 -0
- package/dist/index.cjs.js +1 -0
- package/dist/index.js +1 -0
- package/dist/layouts/app-shell.css +84 -0
- package/dist/tests/a11y.js +55 -0
- package/dist/types/Users/alex/projects/MyGitHub/chiralkit/packages/design-system/.stencil/tests/a11y.d.ts +17 -0
- package/dist/types/components/ds-badge/ds-badge.d.ts +54 -0
- package/dist/types/components/ds-button/ds-button.d.ts +75 -0
- package/dist/types/components/ds-card/ds-card.d.ts +53 -0
- package/dist/types/components/ds-checkbox/ds-checkbox.d.ts +94 -0
- package/dist/types/components/ds-code-block/ds-code-block.d.ts +83 -0
- package/dist/types/components/ds-dialog/ds-dialog.d.ts +148 -0
- package/dist/types/components/ds-input/ds-input.d.ts +104 -0
- package/dist/types/components/ds-loader/ds-loader.d.ts +64 -0
- package/dist/types/components/ds-select/ds-select.d.ts +123 -0
- package/dist/types/components/ds-tabs/ds-tabs.d.ts +155 -0
- package/dist/types/components/ds-textarea/ds-textarea.d.ts +93 -0
- package/dist/types/components/ds-tooltip/ds-tooltip.d.ts +26 -0
- package/dist/types/components.d.ts +3068 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/stencil-public-runtime.d.ts +1861 -0
- package/docs.json +2112 -0
- package/loader/cdn.js +1 -0
- package/loader/index.cjs.js +1 -0
- package/loader/index.d.ts +24 -0
- package/loader/index.es2017.js +1 -0
- package/loader/index.js +2 -0
- package/package.json +78 -0
- package/react/components/ds-badge.tsx +25 -0
- package/react/components/ds-button.tsx +25 -0
- package/react/components/ds-card.tsx +25 -0
- package/react/components/ds-checkbox.tsx +25 -0
- package/react/components/ds-code-block.tsx +25 -0
- package/react/components/ds-dialog.tsx +31 -0
- package/react/components/ds-input.tsx +25 -0
- package/react/components/ds-loader.tsx +25 -0
- package/react/components/ds-select.tsx +25 -0
- package/react/components/ds-tabs.tsx +25 -0
- package/react/components/ds-textarea.tsx +25 -0
- package/react/components/ds-tooltip.tsx +25 -0
- package/react/components.ts +162 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { EventEmitter } from "../../stencil-public-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* ## Accessibility
|
|
4
|
+
*
|
|
5
|
+
* **Category:** state-machine
|
|
6
|
+
* **APG pattern:** https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/
|
|
7
|
+
*
|
|
8
|
+
* This component wraps the **native `<dialog>` element** rather than
|
|
9
|
+
* scratch-building the APG pattern on top of a `<div role="dialog">`.
|
|
10
|
+
* `<dialog>` opened via `showModal()` gives us — from the browser,
|
|
11
|
+
* verified by the platform, not by us:
|
|
12
|
+
*
|
|
13
|
+
* - Focus trap: Tab / Shift+Tab cycle only through focusable
|
|
14
|
+
* descendants (including slotted light-DOM descendants; the flattened
|
|
15
|
+
* accessibility tree is what the trap operates on).
|
|
16
|
+
* - Escape: closes the dialog and returns focus to the previously
|
|
17
|
+
* focused element (native, not implemented here).
|
|
18
|
+
* - Inert background: everything outside the dialog is inert while it
|
|
19
|
+
* is open — no mouse, no keyboard, not part of the a11y tree.
|
|
20
|
+
* - Return focus: on close, focus returns to whatever element had it
|
|
21
|
+
* before `showModal()` — native, not implemented here.
|
|
22
|
+
* - Initial focus: browser auto-focuses the first `autofocus` element
|
|
23
|
+
* inside, or the first focusable if none — native.
|
|
24
|
+
* - `role="dialog"` and `aria-modal="true"` semantics: implicit on
|
|
25
|
+
* `<dialog>`, no ARIA needed from us.
|
|
26
|
+
*
|
|
27
|
+
* This is the deliberate answer to the a11y spec §D warning about
|
|
28
|
+
* scratch-building Tier 2 components: the native element removes the
|
|
29
|
+
* "compiles, axe passes, screen-reader user gets trapped" failure
|
|
30
|
+
* mode by delegating the state machine to the platform.
|
|
31
|
+
*
|
|
32
|
+
* **Keyboard interactions (all handled by the native element unless
|
|
33
|
+
* noted otherwise):**
|
|
34
|
+
* - Tab: cycles focus within the dialog's focusable descendants
|
|
35
|
+
* (native focus trap).
|
|
36
|
+
* - Shift+Tab: reverse cycle (native).
|
|
37
|
+
* - Escape: closes the dialog and fires the `close` event (native).
|
|
38
|
+
* Consumers who need to prevent dismissal on Escape (destructive
|
|
39
|
+
* flows: e.g. an unsaved-changes guard) must call
|
|
40
|
+
* `event.preventDefault()` inside their own `keydown` listener on
|
|
41
|
+
* the dialog before it reaches the browser — that is an escape
|
|
42
|
+
* hatch, not a default we impose.
|
|
43
|
+
* - Enter / Space: no special dialog behavior; forwarded to
|
|
44
|
+
* whatever descendant has focus (a button will activate).
|
|
45
|
+
* - Home / End / Arrow keys: no special dialog behavior; forwarded
|
|
46
|
+
* to descendants.
|
|
47
|
+
* - Printable characters: forwarded to descendants (e.g. typing
|
|
48
|
+
* inside a slotted `<ds-input>`).
|
|
49
|
+
*
|
|
50
|
+
* **ARIA attributes and value triggers:**
|
|
51
|
+
* - Implicit `role="dialog"` on the native `<dialog>`.
|
|
52
|
+
* - Implicit `aria-modal="true"` when opened via `showModal()`
|
|
53
|
+
* (which is what this component always uses; `show()` — the
|
|
54
|
+
* non-modal form — is intentionally not exposed in v1).
|
|
55
|
+
* - `aria-label` set from the `label` prop onto the `<dialog>`.
|
|
56
|
+
* Consumers who prefer an in-content title can pass a
|
|
57
|
+
* `labelledBy` prop instead, whose value is the id of a heading
|
|
58
|
+
* element they render inside the slot — `aria-labelledby`
|
|
59
|
+
* resolves that id through the flattened tree.
|
|
60
|
+
* - `aria-describedby` from the `describedBy` prop, resolving a
|
|
61
|
+
* light-DOM id of a descriptive block (e.g. the dialog body copy
|
|
62
|
+
* when the label is short).
|
|
63
|
+
*
|
|
64
|
+
* **Focus lifecycle:**
|
|
65
|
+
* - Opening: `showModal()` moves focus to the first `autofocus`
|
|
66
|
+
* element inside, or the first focusable if none. Consumers who
|
|
67
|
+
* want a specific initial focus target should mark that element
|
|
68
|
+
* with `autofocus`.
|
|
69
|
+
* - While open: focus is trapped within the flattened tree of the
|
|
70
|
+
* `<dialog>`; Tab / Shift+Tab cycle through slotted descendants.
|
|
71
|
+
* Focus cannot escape into the inert background.
|
|
72
|
+
* - Closing: `close()` (either from Escape, from a form-submit with
|
|
73
|
+
* `method="dialog"`, from a light-dismiss backdrop click handler
|
|
74
|
+
* the consumer wires up, or from `open=false`) returns focus to
|
|
75
|
+
* the element that had it immediately before `showModal()` was
|
|
76
|
+
* called. The `dsClose` event fires after focus has been restored.
|
|
77
|
+
*
|
|
78
|
+
* **ARIA labelling contract (choose one):**
|
|
79
|
+
* - **`label`** (string) — sets `aria-label` on the dialog. Simplest
|
|
80
|
+
* for short titles.
|
|
81
|
+
* - **`labelledBy`** (id) — sets `aria-labelledby` to reference a
|
|
82
|
+
* heading rendered inside the slot. Preferred when the dialog has
|
|
83
|
+
* a visible title so the accessible name matches the visible name.
|
|
84
|
+
*
|
|
85
|
+
* If both are omitted, axe reports `dialog-name` — the dialog has no
|
|
86
|
+
* accessible name. The a11y test for this component asserts that at
|
|
87
|
+
* least one of the two is provided.
|
|
88
|
+
*
|
|
89
|
+
* **Motion:**
|
|
90
|
+
* - Animations: none applied by the component. Consumers who want an
|
|
91
|
+
* entrance transition can style `::backdrop` and `dialog[open]`
|
|
92
|
+
* with their own animation, gated on `@media
|
|
93
|
+
* (prefers-reduced-motion: no-preference)`.
|
|
94
|
+
* - Reduced-motion behavior: N/A — no animation to collapse. If a
|
|
95
|
+
* consumer adds one, they own the reduced-motion fallback.
|
|
96
|
+
*
|
|
97
|
+
* **Contrast:**
|
|
98
|
+
* - Text: inherits `color: inherit`; consumer theme owns contrast.
|
|
99
|
+
* - Non-text: subtle bordered surface via
|
|
100
|
+
* `color-mix(in oklch, currentColor N%, transparent)` matching
|
|
101
|
+
* `ds-card`. Backdrop is a semi-transparent black by default.
|
|
102
|
+
*
|
|
103
|
+
* **Screen reader announcement:**
|
|
104
|
+
* - Rendered state (closed): dialog is not in the a11y tree.
|
|
105
|
+
* - Opened: AT announces "dialog" plus the accessible name from
|
|
106
|
+
* `label` / `labelledBy`, then reads the content following focus
|
|
107
|
+
* (or the descriptive block referenced by `describedBy` first,
|
|
108
|
+
* depending on AT).
|
|
109
|
+
* - State change: on close, AT returns to the element that regained
|
|
110
|
+
* focus (the trigger); no `aria-live` announcement is added.
|
|
111
|
+
*
|
|
112
|
+
* **Consumer contract:**
|
|
113
|
+
* - Set `open` to true to show; set to false to close. The component
|
|
114
|
+
* syncs the DOM state via `showModal()` / `close()` in a Watch.
|
|
115
|
+
* - Listen to `dsClose` to know when the dialog was dismissed (by
|
|
116
|
+
* Escape, by `open=false`, or by a form submit with
|
|
117
|
+
* `method="dialog"`). Listen to `dsOpen` for the mirror event.
|
|
118
|
+
* - The default backdrop is opaque and NOT click-to-dismiss. Consumers
|
|
119
|
+
* who want light-dismiss can add a click listener on the dialog
|
|
120
|
+
* that closes when the click target is the dialog itself (not a
|
|
121
|
+
* descendant): `if (e.target === dialog) dialog.close()`.
|
|
122
|
+
* - Put focus targets (form fields, primary action button) inside
|
|
123
|
+
* the slot. Use `autofocus` on the element that should receive
|
|
124
|
+
* focus first.
|
|
125
|
+
*
|
|
126
|
+
* **Pending manual verification (see commit message TODO):**
|
|
127
|
+
* - VoiceOver on macOS: confirm the dialog announces its accessible
|
|
128
|
+
* name + "dialog" on open, focus lands on the correct element,
|
|
129
|
+
* Escape closes and focus returns to the trigger.
|
|
130
|
+
* - Chromium high-contrast / forced-colors: confirm the surface
|
|
131
|
+
* still reads as a bordered container with the native ::backdrop
|
|
132
|
+
* dim.
|
|
133
|
+
*/
|
|
134
|
+
export declare class DsDialog {
|
|
135
|
+
private dialogEl?;
|
|
136
|
+
open: boolean;
|
|
137
|
+
label?: string;
|
|
138
|
+
labelledBy?: string;
|
|
139
|
+
describedBy?: string;
|
|
140
|
+
dsOpen: EventEmitter<void>;
|
|
141
|
+
dsClose: EventEmitter<void>;
|
|
142
|
+
watchOpen(newValue: boolean): void;
|
|
143
|
+
componentDidLoad(): void;
|
|
144
|
+
disconnectedCallback(): void;
|
|
145
|
+
private syncOpen;
|
|
146
|
+
private onNativeClose;
|
|
147
|
+
render(): any;
|
|
148
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { EventEmitter } from "../../stencil-public-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* ## Accessibility
|
|
4
|
+
*
|
|
5
|
+
* **Category:** form-field
|
|
6
|
+
* **APG pattern:** N/A — native element. This wraps a native `<input>`;
|
|
7
|
+
* there is no WAI-ARIA APG pattern for a plain text input because the
|
|
8
|
+
* browser already implements it natively. Reference:
|
|
9
|
+
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input
|
|
10
|
+
*
|
|
11
|
+
* **Keyboard interactions:**
|
|
12
|
+
* - Tab: moves focus to the native `<input>` (shadow DOM does not
|
|
13
|
+
* interrupt tab order).
|
|
14
|
+
* - Printable characters: insert at the caret position (native).
|
|
15
|
+
* - Backspace / Delete: native editing.
|
|
16
|
+
* - Arrow keys: move the caret within the value (native); for
|
|
17
|
+
* `type="number"` Up/Down step the numeric value (native).
|
|
18
|
+
* - Enter: on `type="search"` may submit the enclosing form (native);
|
|
19
|
+
* no custom handling.
|
|
20
|
+
* - Escape: no native meaning for a text input.
|
|
21
|
+
*
|
|
22
|
+
* **ARIA:**
|
|
23
|
+
* - Roles used: none explicit — the native `<input>` supplies its
|
|
24
|
+
* implicit role (`textbox`, `searchbox`, etc. depending on `type`).
|
|
25
|
+
* - State attributes: `disabled` (native attribute, not `aria-disabled`),
|
|
26
|
+
* `aria-invalid` (from the `invalid` prop), `aria-required` (from the
|
|
27
|
+
* `required` prop), `aria-describedby` (from the `describedBy` prop,
|
|
28
|
+
* expected to reference a light-DOM node id — typically a hint or
|
|
29
|
+
* error message rendered by the consumer alongside the field).
|
|
30
|
+
* - Labeling strategy: `aria-label` set from the `label` prop onto the
|
|
31
|
+
* internal `<input>`. This is the **canonical, recommended** mode —
|
|
32
|
+
* explicit and independent of DOM structure. Consumers MUST pass
|
|
33
|
+
* `label` (or otherwise give the field an accessible name) — an
|
|
34
|
+
* `<input>` with no accessible name is a WCAG failure (axe rule
|
|
35
|
+
* `label`). Wrapping the host in a light-DOM `<label>` (e.g.
|
|
36
|
+
* `<label>Email <ds-input></ds-input></label>`) also produces an
|
|
37
|
+
* accessible name in Chromium via flattened-tree resolution — the
|
|
38
|
+
* same secondary mode documented on `ds-select`, verified in this
|
|
39
|
+
* component's e2e tests. Setting `aria-label` directly on the
|
|
40
|
+
* `<ds-input>` host does NOT work: shadow-DOM attribute forwarding
|
|
41
|
+
* would be required — use the `label` prop instead.
|
|
42
|
+
*
|
|
43
|
+
* **Focus:**
|
|
44
|
+
* - On mount: nothing auto-focuses.
|
|
45
|
+
* - On value change: focus remains on the native `<input>`.
|
|
46
|
+
* - Focus indicator: the browser default `:focus-visible` ring is kept
|
|
47
|
+
* on purpose — a themed focus ring using `--ds-color-focus` is a
|
|
48
|
+
* future decision once the DS ships a shared focus utility. Not
|
|
49
|
+
* overridden by `ds-input.css`.
|
|
50
|
+
*
|
|
51
|
+
* **Motion:**
|
|
52
|
+
* - Animations: none.
|
|
53
|
+
* - Reduced-motion behavior: N/A — no animation to collapse.
|
|
54
|
+
*
|
|
55
|
+
* **Contrast:**
|
|
56
|
+
* - Text: inherits `color: inherit` / `currentColor`; ratio depends on
|
|
57
|
+
* the consumer's theme, not verified in isolation here.
|
|
58
|
+
* - Border: `color-mix(in oklch, currentColor 22%, transparent)`,
|
|
59
|
+
* matching `ds-select` — inherits whatever contrast the host page
|
|
60
|
+
* sets. WCAG 1.4.11 (3:1 non-text) is the consumer's responsibility
|
|
61
|
+
* in themes that override the surface.
|
|
62
|
+
*
|
|
63
|
+
* **Screen reader announcement:**
|
|
64
|
+
* - Rendered state: announces the `label` prop text as the accessible
|
|
65
|
+
* name, followed by the browser/AT's role announcement for the
|
|
66
|
+
* input type ("edit text", "search text", "password", "phone
|
|
67
|
+
* number", "email"…). When `required=true` the announcement
|
|
68
|
+
* includes "required"; when `invalid=true` it includes "invalid".
|
|
69
|
+
* - State change: on value edit, AT typically echoes the typed
|
|
70
|
+
* character (native input behavior); no `aria-live` added, per the
|
|
71
|
+
* spec's guidance against live-region overuse.
|
|
72
|
+
*
|
|
73
|
+
* **Consumer contract:**
|
|
74
|
+
* - `placeholder` is a hint, not a label. It disappears on focus in
|
|
75
|
+
* most browsers and is not reliably announced by every AT — always
|
|
76
|
+
* pair with `label`.
|
|
77
|
+
* - For `type="password"` in a real form, pair with a light-DOM
|
|
78
|
+
* `<input type="hidden">` or a form-associated ancestor that gives
|
|
79
|
+
* the browser's password manager an `autocomplete` hint. This
|
|
80
|
+
* component intentionally does not expose `autocomplete` in v1;
|
|
81
|
+
* consumers that need it can render a native `<input>` for that
|
|
82
|
+
* field until the prop is added.
|
|
83
|
+
* - When `invalid=true`, pair with a `describedBy` id that points at
|
|
84
|
+
* a visible error message; `aria-invalid` alone gives the AT the
|
|
85
|
+
* status but not the reason.
|
|
86
|
+
*/
|
|
87
|
+
export declare class DsInput {
|
|
88
|
+
private inputEl?;
|
|
89
|
+
value?: string;
|
|
90
|
+
name?: string;
|
|
91
|
+
type: "text" | "email" | "password" | "search" | "tel" | "url" | "number";
|
|
92
|
+
placeholder?: string;
|
|
93
|
+
disabled: boolean;
|
|
94
|
+
required: boolean;
|
|
95
|
+
invalid: boolean;
|
|
96
|
+
describedBy?: string;
|
|
97
|
+
label?: string;
|
|
98
|
+
size: "md" | "lg";
|
|
99
|
+
dsInput: EventEmitter<string>;
|
|
100
|
+
watchValue(newValue?: string): void;
|
|
101
|
+
componentDidLoad(): void;
|
|
102
|
+
private onInput;
|
|
103
|
+
render(): any;
|
|
104
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ## Accessibility
|
|
3
|
+
*
|
|
4
|
+
* **Category:** static-display (indeterminate progress indicator)
|
|
5
|
+
* **APG pattern:** N/A — the loader is a passive presence indicator, not a
|
|
6
|
+
* `progressbar` (no measurable value). Uses `role="status"` per WAI-ARIA
|
|
7
|
+
* live-region semantics for indeterminate loading states.
|
|
8
|
+
*
|
|
9
|
+
* **Keyboard interactions:**
|
|
10
|
+
* - Tab: N/A — not focusable.
|
|
11
|
+
* - Enter: N/A.
|
|
12
|
+
* - Escape: N/A.
|
|
13
|
+
* - Arrow keys: N/A.
|
|
14
|
+
* - Printable characters: N/A.
|
|
15
|
+
*
|
|
16
|
+
* **ARIA:**
|
|
17
|
+
* - Roles used: `role="status"` on the host — an implicit polite live
|
|
18
|
+
* region ARIA lists as valid for "advisory information" including
|
|
19
|
+
* in-progress operations. The inner `<svg>` is decorative and carries
|
|
20
|
+
* `aria-hidden="true"` + `focusable="false"` so it never becomes a
|
|
21
|
+
* parallel target for screen readers.
|
|
22
|
+
* - State attributes: none. The loader has no state changes to announce
|
|
23
|
+
* beyond its own presence; the polite live region on mount is the
|
|
24
|
+
* announcement.
|
|
25
|
+
* - Labeling strategy: `aria-label` from the `label` prop (default
|
|
26
|
+
* "Loading"). Consumers pass a more specific string when the context
|
|
27
|
+
* warrants ("Loading ceremonies", "Signing in", …).
|
|
28
|
+
*
|
|
29
|
+
* **Focus:**
|
|
30
|
+
* - On mount: nothing auto-focuses.
|
|
31
|
+
* - On activate/open: N/A — not interactive.
|
|
32
|
+
* - On close/deactivate: N/A.
|
|
33
|
+
* - Focus indicator: N/A — the host is not focusable.
|
|
34
|
+
*
|
|
35
|
+
* **Motion:**
|
|
36
|
+
* - Animations: two mirrored halves of the logo path oscillate
|
|
37
|
+
* symmetrically around the visual center, meeting at the ring midpoint
|
|
38
|
+
* and separating again. Cycle 1.4s by default (`speed="normal"`);
|
|
39
|
+
* `speed="slow"` = 2.2s, `speed="fast"` = 0.9s.
|
|
40
|
+
* - Reduced-motion behavior: `@media (prefers-reduced-motion: reduce)`
|
|
41
|
+
* collapses both halves to a static rendering with a slow opacity
|
|
42
|
+
* pulse (2s ease-in-out). No rotation, no lateral movement.
|
|
43
|
+
*
|
|
44
|
+
* **Contrast:**
|
|
45
|
+
* - Text: N/A — the visible surface is the SVG stroke, not text.
|
|
46
|
+
* - Non-text: stroke inherits `currentColor` from the host. Consumers are
|
|
47
|
+
* responsible for confirming the stroke against its background meets
|
|
48
|
+
* the ≥ 3:1 non-text contrast threshold (WCAG 1.4.11) where the loader
|
|
49
|
+
* conveys state (e.g. distinguishing "still loading" from "idle").
|
|
50
|
+
*
|
|
51
|
+
* **Screen reader announcement:**
|
|
52
|
+
* - Rendered state: the polite live region announces the label
|
|
53
|
+
* (e.g. "Loading") when the loader mounts.
|
|
54
|
+
* - State change: N/A — the loader itself has no state that changes.
|
|
55
|
+
* Consumers unmount the loader when the awaited work completes; that
|
|
56
|
+
* removal is silent, which is the intended behavior (the arriving
|
|
57
|
+
* content is the announcement).
|
|
58
|
+
*/
|
|
59
|
+
export declare class DsLoader {
|
|
60
|
+
size: "sm" | "md" | "lg";
|
|
61
|
+
label: string;
|
|
62
|
+
speed: "slow" | "normal" | "fast";
|
|
63
|
+
render(): any;
|
|
64
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { EventEmitter } from "../../stencil-public-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* ## Accessibility
|
|
4
|
+
*
|
|
5
|
+
* **Category:** form-field
|
|
6
|
+
* **APG pattern:** N/A — native element. This wraps a native `<select>`;
|
|
7
|
+
* there is no WAI-ARIA APG pattern for it because the browser already
|
|
8
|
+
* implements the full listbox/combobox behavior natively. Reference:
|
|
9
|
+
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select
|
|
10
|
+
*
|
|
11
|
+
* **Keyboard interactions:**
|
|
12
|
+
* - Tab: moves focus to the native `<select>` (shadow DOM does not
|
|
13
|
+
* interrupt tab order).
|
|
14
|
+
* - Enter: opens the native options popup on most platforms (browser/OS
|
|
15
|
+
* dependent, identical to any bare `<select>`).
|
|
16
|
+
* - Escape: closes the native options popup without changing the value
|
|
17
|
+
* (native behavior, not implemented by us).
|
|
18
|
+
* - Arrow keys: Up/Down move the selected option when closed; Up/Down/Home/End
|
|
19
|
+
* navigate options when the native popup is open (native behavior).
|
|
20
|
+
* - Printable characters: native typeahead jumps to the option starting
|
|
21
|
+
* with the typed character(s) (native behavior).
|
|
22
|
+
*
|
|
23
|
+
* **ARIA:**
|
|
24
|
+
* - Roles used: none explicit — the native `<select>` element supplies the
|
|
25
|
+
* implicit `listbox`/`combobox`-equivalent role itself.
|
|
26
|
+
* - State attributes: `disabled` (native attribute, not `aria-disabled`).
|
|
27
|
+
* - Labeling strategy: `aria-label`, set from the `label` prop onto the
|
|
28
|
+
* internal `<select>`. This is the **canonical, recommended** mode —
|
|
29
|
+
* explicit and independent of DOM structure. Consumers MUST pass `label`
|
|
30
|
+
* (or otherwise give the field an accessible name) — a `<select>` with no
|
|
31
|
+
* accessible name is a WCAG failure (axe rule `select-name`).
|
|
32
|
+
* Wrapping the host in a light-DOM `<label>` (e.g.
|
|
33
|
+
* `<label>Fruit<ds-select>...</ds-select></label>`) is verified (via
|
|
34
|
+
* axe-core/Chrome in this component's e2e tests) to also resolve an
|
|
35
|
+
* accessible name for the shadow-nested `<select>` — Chromium computes it
|
|
36
|
+
* from the flattened/composed tree even though `ds-select` is not a
|
|
37
|
+
* "labelable" element per the strict HTML spec text (it is not a
|
|
38
|
+
* form-associated custom element). Treat this as a secondary, best-effort
|
|
39
|
+
* mode rather than the primary contract, since it depends on
|
|
40
|
+
* browser/AT-specific accessible-name resolution rather than an explicit
|
|
41
|
+
* attribute — prefer the `label` prop when in doubt.
|
|
42
|
+
* Setting `aria-label` directly on the `<ds-select>` host does NOT work:
|
|
43
|
+
* the host's own attributes are not forwarded into its shadow tree, so
|
|
44
|
+
* the internal `<select>` still has no accessible name (confirmed by an
|
|
45
|
+
* `select-name` axe violation) — use the `label` prop instead.
|
|
46
|
+
* Options themselves are **prop-based, not slotted**: a `<slot />` nested
|
|
47
|
+
* inside a shadow-DOM `<select>` does not project light-DOM `<option>`
|
|
48
|
+
* elements into the native option list in Chromium (`select.options` stays
|
|
49
|
+
* empty even though the flattened a11y tree looks correct — the bug this
|
|
50
|
+
* prop API replaces). Pass `options` as a JSON string (SSR-friendly
|
|
51
|
+
* attribute) or as an array (client-side property); the component renders
|
|
52
|
+
* real `<option>` children inside its own shadow `<select>` from that data.
|
|
53
|
+
*
|
|
54
|
+
* **Focus:**
|
|
55
|
+
* - On mount: nothing auto-focuses.
|
|
56
|
+
* - On activate/open: focus stays on the native `<select>`; the browser
|
|
57
|
+
* manages the options popup itself.
|
|
58
|
+
* - On close/deactivate: focus remains on the native `<select>`.
|
|
59
|
+
* - Focus indicator: none added by this component — the native `<select>`
|
|
60
|
+
* keeps the browser's default `:focus-visible` ring, which is not
|
|
61
|
+
* overridden by `ds-select.css`.
|
|
62
|
+
*
|
|
63
|
+
* **Motion:**
|
|
64
|
+
* - Animations: none.
|
|
65
|
+
* - Reduced-motion behavior: N/A — no animation to collapse.
|
|
66
|
+
*
|
|
67
|
+
* **Contrast:**
|
|
68
|
+
* - Text: inherits `color: inherit` / `currentColor`; ratio depends on the
|
|
69
|
+
* consumer's theme, not verified in isolation here (no default text
|
|
70
|
+
* color is imposed by this component).
|
|
71
|
+
* - Non-text (border, caret icon): both use `currentColor`, so they match
|
|
72
|
+
* the text color 1:1 and inherit whatever contrast the host page sets;
|
|
73
|
+
* the caret is decorative-only (no accessible name), applied via
|
|
74
|
+
* `:host::after` mask, `pointer-events: none`.
|
|
75
|
+
*
|
|
76
|
+
* **Screen reader announcement:**
|
|
77
|
+
* - Rendered state: announces the `label` prop text as the accessible
|
|
78
|
+
* name, followed by "combo box" / "pop up button" (announcement wording
|
|
79
|
+
* is browser/AT dependent, standard for any native `<select>`), and the
|
|
80
|
+
* currently selected option's text — sourced from the `label` field of
|
|
81
|
+
* each entry in the `options` prop.
|
|
82
|
+
* - State change: on selection change, AT announces the newly selected
|
|
83
|
+
* option text (native `<select>` behavior; no custom `aria-live` added,
|
|
84
|
+
* per spec guidance to avoid live-region overuse).
|
|
85
|
+
*
|
|
86
|
+
* **Consumer contract:** `value` must correspond to a `value` in `options`.
|
|
87
|
+
* If `value` is set to a string not present in `options`, the native
|
|
88
|
+
* `<select>` will silently set `selectedIndex = -1` and no `dsChange` will
|
|
89
|
+
* fire — the component does not fabricate a fallback selection.
|
|
90
|
+
*/
|
|
91
|
+
export type DsSelectOption = {
|
|
92
|
+
value: string;
|
|
93
|
+
label: string;
|
|
94
|
+
};
|
|
95
|
+
export declare class DsSelect {
|
|
96
|
+
private selectEl?;
|
|
97
|
+
value?: string;
|
|
98
|
+
name?: string;
|
|
99
|
+
disabled: boolean;
|
|
100
|
+
size: "md" | "lg";
|
|
101
|
+
label?: string;
|
|
102
|
+
/**
|
|
103
|
+
* Options rendered inside the internal shadow `<select>`. Accepts either:
|
|
104
|
+
* - a JSON string (SSR-friendly — server-rendered HTML can only set
|
|
105
|
+
* string attributes, and Stencil does not JSON-parse array attributes),
|
|
106
|
+
* e.g. `options='[{"value":"en","label":"EN"}]'`
|
|
107
|
+
* - an array property set from client-side JS,
|
|
108
|
+
* e.g. `el.options = [{ value: 'en', label: 'EN' }]`
|
|
109
|
+
*
|
|
110
|
+
* Malformed JSON fails safe to an empty list rather than throwing, so the
|
|
111
|
+
* component still renders a visible (if empty) select.
|
|
112
|
+
*/
|
|
113
|
+
options: string | ReadonlyArray<DsSelectOption>;
|
|
114
|
+
dsChange: EventEmitter<string>;
|
|
115
|
+
watchValue(newValue?: string): void;
|
|
116
|
+
watchOptions(): void;
|
|
117
|
+
componentDidLoad(): void;
|
|
118
|
+
componentDidUpdate(): void;
|
|
119
|
+
private syncValue;
|
|
120
|
+
private parseOptions;
|
|
121
|
+
private onChange;
|
|
122
|
+
render(): any;
|
|
123
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { EventEmitter } from "../../stencil-public-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* ## Accessibility
|
|
4
|
+
*
|
|
5
|
+
* **Category:** state-machine (Tier 2)
|
|
6
|
+
* **APG pattern:** https://www.w3.org/WAI/ARIA/apg/patterns/tabs/
|
|
7
|
+
*
|
|
8
|
+
* This component is a scratch build of the WAI-ARIA Tabs pattern —
|
|
9
|
+
* there is no native <tabs> element to wrap. Per the a11y spec §D,
|
|
10
|
+
* scratch-building Tier 2 requires citation + full keyboard/ARIA
|
|
11
|
+
* enumeration + focus lifecycle + manual VoiceOver evidence
|
|
12
|
+
* (pending — see commit message TODO).
|
|
13
|
+
*
|
|
14
|
+
* **Scope choice — tablist only, panels owned by the consumer.** The
|
|
15
|
+
* component renders `<button role="tab">` elements based on the
|
|
16
|
+
* `items` prop and manages roving tabindex + selection. It does NOT
|
|
17
|
+
* render tabpanels: the consumer renders their own light-DOM
|
|
18
|
+
* `<div role="tabpanel" aria-labelledby={tabId} hidden={id!==active}>`
|
|
19
|
+
* and toggles visibility based on the `dsChange` event. This keeps
|
|
20
|
+
* the state machine narrow (no reparenting, no slot ambiguity) and
|
|
21
|
+
* matches how ds-select delivers content — declare data, get behavior.
|
|
22
|
+
*
|
|
23
|
+
* **shadow: false intentional.** The tab buttons must have `id`
|
|
24
|
+
* attributes that the consumer's tabpanels reference via
|
|
25
|
+
* `aria-labelledby`. Shadow-DOM ids are not reachable from light-DOM
|
|
26
|
+
* ARIA references. Rendering the tab buttons directly in light DOM
|
|
27
|
+
* makes the ARIA association work correctly, at the cost of style
|
|
28
|
+
* encapsulation. The visual footprint is small (a bordered button
|
|
29
|
+
* strip + selected state); consumers can override with their own CSS.
|
|
30
|
+
*
|
|
31
|
+
* **Activation model — automatic.** APG allows either automatic
|
|
32
|
+
* (focus = select) or manual (focus, then Enter/Space to select).
|
|
33
|
+
* We use automatic because panels are cheap to swap (they are the
|
|
34
|
+
* consumer's DOM, already rendered). Consumers with expensive panels
|
|
35
|
+
* can defer the swap by listening to `dsChange` and rendering lazily.
|
|
36
|
+
*
|
|
37
|
+
* **Keyboard interactions (full APG enumeration):**
|
|
38
|
+
* - Tab: from outside the tablist → focus lands on the currently
|
|
39
|
+
* selected tab (the only one with tabindex=0). From the selected
|
|
40
|
+
* tab → focus moves out of the tablist to the next focusable
|
|
41
|
+
* element in the document (typically the active tabpanel via its
|
|
42
|
+
* own tabindex=0).
|
|
43
|
+
* - Shift+Tab: reverse of Tab.
|
|
44
|
+
* - Arrow Right (horizontal) / Arrow Down (vertical): move focus to
|
|
45
|
+
* the next enabled tab; automatic activation fires dsChange with
|
|
46
|
+
* the new id. Wraps at the last tab back to the first.
|
|
47
|
+
* - Arrow Left (horizontal) / Arrow Up (vertical): move focus to the
|
|
48
|
+
* previous enabled tab; wraps at the first tab to the last.
|
|
49
|
+
* - Home: focus and activate the first enabled tab.
|
|
50
|
+
* - End: focus and activate the last enabled tab.
|
|
51
|
+
* - Enter / Space: no-op under automatic activation (the tab is
|
|
52
|
+
* already selected). We do not preventDefault so consumers can
|
|
53
|
+
* still form-submit if the tab is inside a form for some reason.
|
|
54
|
+
* - Printable characters: no APG-required behavior (typeahead is
|
|
55
|
+
* optional and out of scope for v1).
|
|
56
|
+
*
|
|
57
|
+
* Wrap-around and skip-disabled behavior: arrow keys skip disabled
|
|
58
|
+
* tabs; Home/End land on the first/last *enabled* tab. If every tab
|
|
59
|
+
* is disabled, focus does not move.
|
|
60
|
+
*
|
|
61
|
+
* **ARIA attributes and value triggers:**
|
|
62
|
+
* - Tablist container: `role="tablist"`, `aria-orientation` set from
|
|
63
|
+
* the `orientation` prop ("horizontal" is the ARIA default; we
|
|
64
|
+
* emit "vertical" explicitly when the prop is "vertical" so AT
|
|
65
|
+
* knows which arrow-key axis is meaningful).
|
|
66
|
+
* - Each tab button: `role="tab"`, `id={item.id}` (the consumer's
|
|
67
|
+
* tabpanel references this via `aria-labelledby`),
|
|
68
|
+
* `aria-selected="true"` on the active tab and `"false"` on the
|
|
69
|
+
* others (toggled every dsChange), `aria-controls={item.controls
|
|
70
|
+
* || item.id}` (the id of the tabpanel this tab governs — defaults
|
|
71
|
+
* to the tab id when the consumer uses the same id for both),
|
|
72
|
+
* `tabindex="0"` on the active tab and `-1` on the others (roving
|
|
73
|
+
* tabindex), `disabled` (native) when the item declares
|
|
74
|
+
* `disabled: true`.
|
|
75
|
+
*
|
|
76
|
+
* **Focus lifecycle:**
|
|
77
|
+
* - On mount: no auto-focus. The tablist is not focused by default;
|
|
78
|
+
* Tab from the previous focusable in the document reaches the
|
|
79
|
+
* active tab.
|
|
80
|
+
* - While focus is inside the tablist: arrow keys, Home, and End
|
|
81
|
+
* move focus (and, under automatic activation, selection) among
|
|
82
|
+
* the tabs. Focus is bounded to the tabs; there is no focus trap,
|
|
83
|
+
* Tab / Shift+Tab still leave the tablist.
|
|
84
|
+
* - When the active tab changes via arrow / Home / End: the newly
|
|
85
|
+
* selected tab's `<button>` receives programmatic focus (via
|
|
86
|
+
* `.focus()`) after Stencil re-renders so the visible focus ring
|
|
87
|
+
* tracks the selected state.
|
|
88
|
+
* - On dsChange from a mouse click: focus follows the click to the
|
|
89
|
+
* clicked tab, matching native button behavior.
|
|
90
|
+
*
|
|
91
|
+
* **Motion:**
|
|
92
|
+
* - Animations: none applied by the component. Consumers who want
|
|
93
|
+
* a selection-indicator slide can style the tabs themselves,
|
|
94
|
+
* gated on `prefers-reduced-motion`.
|
|
95
|
+
*
|
|
96
|
+
* **Contrast:**
|
|
97
|
+
* - Text: inherits `color: inherit`.
|
|
98
|
+
* - Selected-tab visual: an underline drawn with `currentColor`
|
|
99
|
+
* (matches the text and inherits whatever contrast the theme
|
|
100
|
+
* provides). WCAG 1.4.11 (3:1 non-text) is the consumer theme's
|
|
101
|
+
* responsibility.
|
|
102
|
+
*
|
|
103
|
+
* **Screen reader announcement:**
|
|
104
|
+
* - Rendered state: AT announces "tab list, N items" (or the
|
|
105
|
+
* equivalent per AT), then each tab as "Label, tab, X of N,
|
|
106
|
+
* selected" or "not selected".
|
|
107
|
+
* - State change: on arrow-key move under automatic activation, AT
|
|
108
|
+
* announces the newly focused tab plus its selection status.
|
|
109
|
+
*
|
|
110
|
+
* **Consumer contract:**
|
|
111
|
+
* - `items` is either a JSON string (SSR-friendly attribute) or an
|
|
112
|
+
* array (client-side property), matching ds-select's dual API.
|
|
113
|
+
* Each item: `{ id, label, controls?, disabled? }`. Malformed
|
|
114
|
+
* JSON falls back to an empty list (visible-but-empty tablist).
|
|
115
|
+
* - `active` is the id of the currently selected tab. If `active`
|
|
116
|
+
* is not in `items` at mount, the first enabled tab is selected
|
|
117
|
+
* and dsChange fires once to reconcile.
|
|
118
|
+
* - The consumer's `<div role="tabpanel" aria-labelledby={tabId}
|
|
119
|
+
* id={item.controls || item.id} tabindex="0" hidden={id !== active}>`
|
|
120
|
+
* is not rendered by this component — it is the consumer's DOM.
|
|
121
|
+
*
|
|
122
|
+
* **Pending manual verification (see commit message TODO):**
|
|
123
|
+
* - VoiceOver on macOS: confirm "tab list" grouping, tab-of-N
|
|
124
|
+
* position, selected state on selection change, and that arrow
|
|
125
|
+
* navigation announces correctly.
|
|
126
|
+
* - Chromium high-contrast / forced-colors: confirm the selected
|
|
127
|
+
* underline remains visible.
|
|
128
|
+
*/
|
|
129
|
+
export type DsTabItem = {
|
|
130
|
+
id: string;
|
|
131
|
+
label: string;
|
|
132
|
+
/**
|
|
133
|
+
* Id of the tabpanel this tab controls. Defaults to `id`.
|
|
134
|
+
*/
|
|
135
|
+
controls?: string;
|
|
136
|
+
disabled?: boolean;
|
|
137
|
+
};
|
|
138
|
+
export declare class DsTabs {
|
|
139
|
+
private tabRefs;
|
|
140
|
+
private pendingFocusId?;
|
|
141
|
+
items: string | ReadonlyArray<DsTabItem>;
|
|
142
|
+
active?: string;
|
|
143
|
+
orientation: "horizontal" | "vertical";
|
|
144
|
+
label?: string;
|
|
145
|
+
dsChange: EventEmitter<string>;
|
|
146
|
+
watchItems(): void;
|
|
147
|
+
componentWillLoad(): void;
|
|
148
|
+
componentDidRender(): void;
|
|
149
|
+
private parseItems;
|
|
150
|
+
private enabledItems;
|
|
151
|
+
private selectAndFocus;
|
|
152
|
+
private onClick;
|
|
153
|
+
private onKeyDown;
|
|
154
|
+
render(): any;
|
|
155
|
+
}
|