@memberjunction/ng-ui-components 6.2.0-edge.0 → 6.2.0-edge.1
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/README.md +35 -9
- package/dist/lib/a11y/named-control.base.d.ts +91 -0
- package/dist/lib/a11y/named-control.base.d.ts.map +1 -0
- package/dist/lib/a11y/named-control.base.js +121 -0
- package/dist/lib/a11y/named-control.base.js.map +1 -0
- package/dist/lib/a11y/unnamed-control-guard.d.ts +52 -0
- package/dist/lib/a11y/unnamed-control-guard.d.ts.map +1 -0
- package/dist/lib/a11y/unnamed-control-guard.js +94 -0
- package/dist/lib/a11y/unnamed-control-guard.js.map +1 -0
- package/dist/lib/accordion/accordion.component.d.ts +16 -4
- package/dist/lib/accordion/accordion.component.d.ts.map +1 -1
- package/dist/lib/accordion/accordion.component.js +36 -12
- package/dist/lib/accordion/accordion.component.js.map +1 -1
- package/dist/lib/applied-filters/applied-filters.component.js.map +1 -1
- package/dist/lib/button/button.directive.d.ts +34 -6
- package/dist/lib/button/button.directive.d.ts.map +1 -1
- package/dist/lib/button/button.directive.js +85 -25
- package/dist/lib/button/button.directive.js.map +1 -1
- package/dist/lib/card-grid/card-grid.component.d.ts +3 -1
- package/dist/lib/card-grid/card-grid.component.d.ts.map +1 -1
- package/dist/lib/card-grid/card-grid.component.js +13 -9
- package/dist/lib/card-grid/card-grid.component.js.map +1 -1
- package/dist/lib/clickable/clickable.directive.d.ts +11 -3
- package/dist/lib/clickable/clickable.directive.d.ts.map +1 -1
- package/dist/lib/clickable/clickable.directive.js +25 -5
- package/dist/lib/clickable/clickable.directive.js.map +1 -1
- package/dist/lib/combobox/combobox.component.d.ts +44 -3
- package/dist/lib/combobox/combobox.component.d.ts.map +1 -1
- package/dist/lib/combobox/combobox.component.js +135 -51
- package/dist/lib/combobox/combobox.component.js.map +1 -1
- package/dist/lib/confirm-dialog/confirm-dialog.component.d.ts +6 -0
- package/dist/lib/confirm-dialog/confirm-dialog.component.d.ts.map +1 -1
- package/dist/lib/confirm-dialog/confirm-dialog.component.js +16 -4
- package/dist/lib/confirm-dialog/confirm-dialog.component.js.map +1 -1
- package/dist/lib/datepicker/datepicker.component.d.ts +25 -2
- package/dist/lib/datepicker/datepicker.component.d.ts.map +1 -1
- package/dist/lib/datepicker/datepicker.component.js +98 -42
- package/dist/lib/datepicker/datepicker.component.js.map +1 -1
- package/dist/lib/dialog/dialog.component.d.ts +4 -0
- package/dist/lib/dialog/dialog.component.d.ts.map +1 -1
- package/dist/lib/dialog/dialog.component.js +12 -4
- package/dist/lib/dialog/dialog.component.js.map +1 -1
- package/dist/lib/dialog/dialog.service.js.map +1 -1
- package/dist/lib/dropdown/dropdown.component.d.ts +19 -44
- package/dist/lib/dropdown/dropdown.component.d.ts.map +1 -1
- package/dist/lib/dropdown/dropdown.component.js +37 -76
- package/dist/lib/dropdown/dropdown.component.js.map +1 -1
- package/dist/lib/dropdown/dropdown.scss +0 -16
- package/dist/lib/filter-panel/filter-panel.component.d.ts +8 -0
- package/dist/lib/filter-panel/filter-panel.component.d.ts.map +1 -1
- package/dist/lib/filter-panel/filter-panel.component.js +25 -9
- package/dist/lib/filter-panel/filter-panel.component.js.map +1 -1
- package/dist/lib/numeric-input/numeric-input.component.d.ts +15 -1
- package/dist/lib/numeric-input/numeric-input.component.d.ts.map +1 -1
- package/dist/lib/numeric-input/numeric-input.component.js +36 -9
- package/dist/lib/numeric-input/numeric-input.component.js.map +1 -1
- package/dist/lib/page-search/page-search.component.d.ts +32 -3
- package/dist/lib/page-search/page-search.component.d.ts.map +1 -1
- package/dist/lib/page-search/page-search.component.js +68 -12
- package/dist/lib/page-search/page-search.component.js.map +1 -1
- package/dist/lib/switch/switch.component.d.ts +22 -1
- package/dist/lib/switch/switch.component.d.ts.map +1 -1
- package/dist/lib/switch/switch.component.js +50 -16
- package/dist/lib/switch/switch.component.js.map +1 -1
- package/dist/lib/tabs/tab-chrome-guard.d.ts +2 -0
- package/dist/lib/tabs/tab-chrome-guard.d.ts.map +1 -1
- package/dist/lib/tabs/tab-chrome-guard.js +5 -1
- package/dist/lib/tabs/tab-chrome-guard.js.map +1 -1
- package/dist/lib/window/window.component.d.ts +10 -0
- package/dist/lib/window/window.component.d.ts.map +1 -1
- package/dist/lib/window/window.component.js +27 -7
- package/dist/lib/window/window.component.js.map +1 -1
- package/dist/lib/workspace-tabs/workspace-tab-strip.component.d.ts +10 -0
- package/dist/lib/workspace-tabs/workspace-tab-strip.component.d.ts.map +1 -1
- package/dist/lib/workspace-tabs/workspace-tab-strip.component.js +27 -7
- package/dist/lib/workspace-tabs/workspace-tab-strip.component.js.map +1 -1
- package/dist/public-api.d.ts +2 -0
- package/dist/public-api.d.ts.map +1 -1
- package/dist/public-api.js +2 -0
- package/dist/public-api.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -53,6 +53,25 @@ All colors resolve through `--mj-*` semantic tokens, so light/dark theming requi
|
|
|
53
53
|
|
|
54
54
|
`mj-combobox`, `mj-dropdown`, `mj-datepicker`, `mj-numeric-input`, and `mj-switch` all implement `ControlValueAccessor` — they work with `[(ngModel)]` and reactive forms, including `setDisabledState`.
|
|
55
55
|
|
|
56
|
+
## Accessible names — one contract across every control
|
|
57
|
+
|
|
58
|
+
`mj-combobox`, `mj-dropdown`, `mj-datepicker`, `mj-numeric-input`, `mj-page-search` and `mj-switch` share four optional inputs, from `MJNamedControlBase`. **Name every one of them**: without a name a control announces as its role and state alone — "combobox, collapsed", "switch, on" — which fails WCAG 2.1 4.1.2 (Name, Role, Value).
|
|
59
|
+
|
|
60
|
+
| input | for |
|
|
61
|
+
|---|---|
|
|
62
|
+
| **`AriaLabelledBy`** | the id of a **visible** label — the preferred wiring when one exists |
|
|
63
|
+
| `AriaLabel` | when no visible label exists |
|
|
64
|
+
| `AriaDescribedBy` | hint and error text |
|
|
65
|
+
| `InputId` | an id on the focusable element, so other markup can reference it |
|
|
66
|
+
|
|
67
|
+
Three things hold across all six:
|
|
68
|
+
|
|
69
|
+
- **Absent beats empty** — an unset input renders no attribute at all. `aria-label=""` is worse than nothing, because an explicitly empty name overrides every other naming source.
|
|
70
|
+
- **The name reaches the whole control** — the popup listbox, the calendar grid, the filter box, the toggle and clear buttons all take their name from the same source, so a form with six filterable dropdowns does not present six identical "Filter options" boxes. Under `AriaLabelledBy` this works through an `aria-labelledby` id list, the only mechanism that can compose a fixed word with a visible label's own text.
|
|
71
|
+
- **`InputId` is a `<label for>` target wherever the focusable element is labelable** — the four input-backed controls (`mj-combobox`, `mj-datepicker`, `mj-numeric-input`, `mj-page-search`) and `mj-switch`, whose `<button>` is labelable too and gains click-to-focus from it. The exception is **`mj-dropdown`**, whose trigger is a `div[role=combobox]`: `label[for]` does not associate with it, so the visible-label path there is `AriaLabelledBy`.
|
|
72
|
+
|
|
73
|
+
**A control that renders with no accessible name warns in dev mode** (`WarnIfUnnamed`, alongside the existing `mjButton` and `mjClickable` guards) and is silent in production. It fires when the name would be genuinely empty — including an `aria-labelledby` whose ids resolve to nothing, which names the control no better than no attribute at all. A `placeholder` does **not** count as a name for a form control, since it disappears the moment the user types; `mj-page-search` is the one control that opts into counting it, because its placeholder is the caller's statement of what the box searches.
|
|
74
|
+
|
|
56
75
|
---
|
|
57
76
|
|
|
58
77
|
# Component catalog
|
|
@@ -69,17 +88,19 @@ Attribute directive that styles a **native** button or anchor. Variants: `primar
|
|
|
69
88
|
```
|
|
70
89
|
|
|
71
90
|
### `mj-combobox` — MJComboboxComponent
|
|
72
|
-
Editable text-input combobox (CDK overlay): type-to-filter, keyboard nav, optional custom values (`[AllowCustom]`). Inputs: `Data`, `TextField`, `ValueField`, `ValuePrimitive`, `Filterable` (default true), `Placeholder`, `Disabled
|
|
91
|
+
Editable text-input combobox (CDK overlay): type-to-filter, keyboard nav, optional custom values (`[AllowCustom]`). Inputs: `Data`, `TextField`, `ValueField`, `ValuePrimitive`, `Filterable` (default true), `Placeholder`, `Disabled`, plus the four naming inputs above. Outputs: `ValueChange`, `FilterChange`. Custom item template via `<ng-template #mjComboboxItem>`.
|
|
92
|
+
|
|
93
|
+
`role="combobox"` and its state attributes (`aria-expanded`, `aria-controls`, `aria-activedescendant`) sit on the **input** — the element that takes focus — not on the wrapper. The toggle and clear buttons take their names from the combobox's own name.
|
|
73
94
|
|
|
74
95
|
```html
|
|
75
|
-
<mj-combobox [Data]="categories" TextField="text" ValueField="value"
|
|
96
|
+
<mj-combobox AriaLabel="Category" [Data]="categories" TextField="text" ValueField="value"
|
|
76
97
|
[(ngModel)]="selected" [ValuePrimitive]="true" [AllowCustom]="true" />
|
|
77
98
|
```
|
|
78
99
|
|
|
79
100
|
### `mj-dropdown` — MJDropdownComponent
|
|
80
101
|
Non-editable select (CDK overlay) with optional in-panel filter (`[Filterable]`, default false) and `DefaultItem` clear option. Same `Data`/`TextField`/`ValueField`/`ValuePrimitive` contract as combobox. Custom item template via `<ng-template #mjDropdownItem>`.
|
|
81
102
|
|
|
82
|
-
**Give every dropdown an accessible name
|
|
103
|
+
**Give every dropdown an accessible name** — see [Accessible names](#accessible-names--one-contract-across-every-control). The trigger is a `div[role=combobox]`, so `<label for>` neither names nor focuses it: the visible-label path here is `AriaLabelledBy`, not `InputId`. The name is applied to the popup listbox as well as the trigger, and a filterable panel's filter box takes its name from the same source.
|
|
83
104
|
|
|
84
105
|
```html
|
|
85
106
|
<!-- A visible label already on screen (preferred) -->
|
|
@@ -93,24 +114,24 @@ Non-editable select (CDK overlay) with optional in-panel filter (`[Filterable]`,
|
|
|
93
114
|
```
|
|
94
115
|
|
|
95
116
|
### `mj-datepicker` — MJDatepickerComponent
|
|
96
|
-
Text input + calendar popup with `Min`/`Max` range disabling and typed-input parsing. Accepts `Date | string | null` through forms; emits `ValueChange: Date | null`. (Display format is currently fixed at `MM/dd/yyyy`.)
|
|
117
|
+
Text input + calendar popup with `Min`/`Max` range disabling and typed-input parsing. Accepts `Date | string | null` through forms; emits `ValueChange: Date | null`. (Display format is currently fixed at `MM/dd/yyyy`.) The toggle button and the calendar grid take their names from the field's own name, so two pickers on one form do not present two grids both called "Calendar".
|
|
97
118
|
|
|
98
119
|
```html
|
|
99
|
-
<mj-datepicker [(ngModel)]="dueDate" [Min]="minDate" [Max]="maxDate" Placeholder="Select a date" />
|
|
120
|
+
<mj-datepicker AriaLabel="Due date" [(ngModel)]="dueDate" [Min]="minDate" [Max]="maxDate" Placeholder="Select a date" />
|
|
100
121
|
```
|
|
101
122
|
|
|
102
123
|
### `mj-numeric-input` — MJNumericInputComponent
|
|
103
124
|
Native number input that clamps to `Min`/`Max`, rounds to `Decimals`, and formats on blur. Value flows through forms only (no separate output).
|
|
104
125
|
|
|
105
126
|
```html
|
|
106
|
-
<mj-numeric-input [(ngModel)]="quantity" [Min]="0" [Max]="100" [Decimals]="2" />
|
|
127
|
+
<mj-numeric-input AriaLabel="Quantity" [(ngModel)]="quantity" [Min]="0" [Max]="100" [Decimals]="2" />
|
|
107
128
|
```
|
|
108
129
|
|
|
109
130
|
### `mj-switch` — MJSwitchComponent
|
|
110
|
-
Boolean toggle with `role="switch"`, optional `OnLabel`/`OffLabel`.
|
|
131
|
+
Boolean toggle with `role="switch"`, optional `OnLabel`/`OffLabel`. Name it: `OnLabel`/`OffLabel` render inside the button, so an unnamed switch is named after its own **state** — "Off, switch, off" — and `AriaLabel`/`AriaLabelledBy` is what replaces that with its purpose.
|
|
111
132
|
|
|
112
133
|
```html
|
|
113
|
-
<mj-switch [(ngModel)]="isEnabled" OnLabel="On" OffLabel="Off" />
|
|
134
|
+
<mj-switch AriaLabel="Email notifications" [(ngModel)]="isEnabled" OnLabel="On" OffLabel="Off" />
|
|
114
135
|
```
|
|
115
136
|
|
|
116
137
|
### `[mjClickable]` — MJClickableDirective
|
|
@@ -213,7 +234,12 @@ The card is a **query container** (`container-type: size`): size content inside
|
|
|
213
234
|
Delayed (~450ms still-pointer), non-interactive, token-themed hover tooltip that by default shows only when the host is truncated (`scrollWidth > clientWidth`); `mjTipAlways` forces it for hosts whose truncation can't be measured (a native `<select>`). Used by the workspace tab strip for full labels on capped tabs.
|
|
214
235
|
|
|
215
236
|
### `mj-page-search` — MJPageSearchComponent
|
|
216
|
-
Compact toolbar search input. `Placeholder`, `Value`, `Icon
|
|
237
|
+
Compact toolbar search input. `Placeholder`, `Value`, `Icon`, plus the four naming inputs above; `(ValueChange)` on input. The placeholder is the name of last resort — it is what the name computation falls back to, and it disappears the moment the user types — so pass `AriaLabel` for a name that survives having text in the box.
|
|
238
|
+
|
|
239
|
+
```html
|
|
240
|
+
<mj-page-search AriaLabel="Search templates" Placeholder="Search templates..."
|
|
241
|
+
[Value]="q" (ValueChange)="q = $event" />
|
|
242
|
+
```
|
|
217
243
|
|
|
218
244
|
### `mj-slide-panel` — MjSlidePanelComponent
|
|
219
245
|
Chrome-only slide-in panel from the right (resizable) or centered dialog (`Mode: 'slide' | 'dialog'`). Inputs include `Title`, `Visible`, `Resizable`, `MinWidthPx`, `MaxWidthRatio`, `WidthPx`, and a `CanClose` guard callback. Outputs: `Closed`, `WidthChanged`. Dialog-mode body projects into `[dialog-content]`.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import * as i0 from "@angular/core";
|
|
2
|
+
/**
|
|
3
|
+
* The accessible-name inputs shared by every control in this package that a caller has to be able
|
|
4
|
+
* to NAME: `mj-dropdown`, `mj-combobox`, `mj-switch`, `mj-numeric-input`, `mj-datepicker` and
|
|
5
|
+
* `mj-page-search`.
|
|
6
|
+
*
|
|
7
|
+
* A control with no accessible name announces as its role and state alone — "combobox, collapsed",
|
|
8
|
+
* "switch, on", "edit blank" — which fails WCAG 2.1 4.1.2 (Name, Role, Value). `mj-dropdown` got
|
|
9
|
+
* these four inputs first (#3860); this base is what keeps each sibling from inventing its own
|
|
10
|
+
* spelling of the same idea (#4116).
|
|
11
|
+
*
|
|
12
|
+
* **Absent beats empty.** Subclasses bind every one of these through `X || null`, never as a bare
|
|
13
|
+
* binding. `aria-label=""` is worse than no attribute at all: an explicitly empty name OVERRIDES
|
|
14
|
+
* every other naming source in the accessible-name computation.
|
|
15
|
+
*
|
|
16
|
+
* Subclasses apply the four to the element that actually **receives focus** — naming a
|
|
17
|
+
* non-focusable wrapper announces nothing — and repeat the name on any popup they own, so both
|
|
18
|
+
* halves announce the same thing.
|
|
19
|
+
*/
|
|
20
|
+
export declare abstract class MJNamedControlBase {
|
|
21
|
+
/**
|
|
22
|
+
* Accessible name for the control, for when no visible label exists. Prefer
|
|
23
|
+
* {@link AriaLabelledBy} when one does — an `aria-label` duplicates that label's text and drifts
|
|
24
|
+
* from it on rename. `aria-labelledby` beats `aria-label` where both are present.
|
|
25
|
+
*/
|
|
26
|
+
AriaLabel: string;
|
|
27
|
+
/**
|
|
28
|
+
* The id of a VISIBLE label element that names this control — the preferred wiring whenever a
|
|
29
|
+
* label already exists on screen. It is also the only path that can name a control's SECONDARY
|
|
30
|
+
* pieces (a filter box, a toggle, a calendar) from that same label: see
|
|
31
|
+
* {@link SecondaryLabelledBy}.
|
|
32
|
+
*/
|
|
33
|
+
AriaLabelledBy: string;
|
|
34
|
+
/**
|
|
35
|
+
* `id` for the control's focusable element, so other markup can REFERENCE it — `aria-controls`,
|
|
36
|
+
* hint text, test hooks.
|
|
37
|
+
*
|
|
38
|
+
* Whether it doubles as a `<label for>` target depends on the control, because `label[for]`
|
|
39
|
+
* associates only with LABELABLE elements: it works on the input-backed controls (`mj-combobox`,
|
|
40
|
+
* `mj-numeric-input`, `mj-datepicker`, `mj-page-search`) and on `mj-switch`, whose `<button>` is
|
|
41
|
+
* labelable too. It does NOT work on `mj-dropdown`, whose trigger is a `div[role=combobox]` —
|
|
42
|
+
* wiring `label[for]` there produces markup that looks correct, passes review, and leaves the
|
|
43
|
+
* control named only in the browsers that guess. Each control's own docs say which it is.
|
|
44
|
+
*/
|
|
45
|
+
InputId: string;
|
|
46
|
+
/** `aria-describedby` passthrough for hint and error text — the same shape of gap as the name. */
|
|
47
|
+
AriaDescribedBy: string;
|
|
48
|
+
/**
|
|
49
|
+
* Counter behind every id a control generates for itself, matching the `static nextId` mechanism
|
|
50
|
+
* `dialog`, `window` and `accordion` already use in this package. One counter across all named
|
|
51
|
+
* controls: the ids only have to be unique in the document, not dense per component.
|
|
52
|
+
*/
|
|
53
|
+
private static nextId;
|
|
54
|
+
/** This instance's id suffix, shared by every id the control generates. */
|
|
55
|
+
readonly NamedControlId: number;
|
|
56
|
+
/**
|
|
57
|
+
* Id of a hidden span holding one fixed WORD of a secondary control's name — "Filter", "Clear",
|
|
58
|
+
* "Open calendar for". Composed with the host's visible label by {@link SecondaryLabelledBy}.
|
|
59
|
+
*/
|
|
60
|
+
SecondaryWordId(slug: string): string;
|
|
61
|
+
/**
|
|
62
|
+
* `aria-labelledby` for a secondary control when a VISIBLE label names the host: the hidden word
|
|
63
|
+
* plus that label's own text, composed by the accessibility tree from an id LIST. Empty when
|
|
64
|
+
* there is no such label, in which case {@link SecondaryLabel} supplies a string instead.
|
|
65
|
+
*
|
|
66
|
+
* An id list is the only mechanism that can work here — the component never sees the label's
|
|
67
|
+
* text, only its id. Without it, every filterable dropdown on a form named this way announces an
|
|
68
|
+
* identical "Filter options", and a form with six of them has six indistinguishable filter boxes.
|
|
69
|
+
*/
|
|
70
|
+
SecondaryLabelledBy(slug: string): string;
|
|
71
|
+
/**
|
|
72
|
+
* `aria-label` for a secondary control in the no-visible-label case: `word` followed by the
|
|
73
|
+
* host's own name, or `fallback` when the host has no name either.
|
|
74
|
+
*
|
|
75
|
+
* The already-starts-with-`word` guard is not cosmetic: this repo's house habit is
|
|
76
|
+
* `AriaLabel="Filter roles"`, and an unconditional prefix announces that box as
|
|
77
|
+
* "Filter Filter roles".
|
|
78
|
+
*
|
|
79
|
+
* The match ends on a WORD BOUNDARY, not on a space: `AriaLabel="Filter: roles"` already begins
|
|
80
|
+
* with the word and must not be prefixed either, while `"Filters"` is a different word and must
|
|
81
|
+
* be. A space-only test gets the punctuated case wrong in the doubling direction.
|
|
82
|
+
*/
|
|
83
|
+
SecondaryLabel(word: string, fallback: string): string;
|
|
84
|
+
/** True when `name` starts with `word` and the next character (if any) ends that word. */
|
|
85
|
+
private beginsWithWord;
|
|
86
|
+
/** `\w` as a regular expression means it: ASCII letters, digits and underscore. Input is lowercased. */
|
|
87
|
+
private static isWordCharacter;
|
|
88
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<MJNamedControlBase, never>;
|
|
89
|
+
static ɵdir: i0.ɵɵDirectiveDeclaration<MJNamedControlBase, never, never, { "AriaLabel": { "alias": "AriaLabel"; "required": false; }; "AriaLabelledBy": { "alias": "AriaLabelledBy"; "required": false; }; "InputId": { "alias": "InputId"; "required": false; }; "AriaDescribedBy": { "alias": "AriaDescribedBy"; "required": false; }; }, {}, never, never, true, never>;
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=named-control.base.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"named-control.base.d.ts","sourceRoot":"","sources":["../../../src/lib/a11y/named-control.base.ts"],"names":[],"mappings":";AAEA;;;;;;;;;;;;;;;;;GAiBG;AACH,8BACsB,kBAAkB;IACtC;;;;OAIG;IACM,SAAS,SAAM;IAExB;;;;;OAKG;IACM,cAAc,SAAM;IAE7B;;;;;;;;;;OAUG;IACM,OAAO,SAAM;IAEtB,kGAAkG;IACzF,eAAe,SAAM;IAE9B;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,MAAM,CAAK;IAE1B,2EAA2E;IAC3E,QAAQ,CAAC,cAAc,SAA+B;IAEtD;;;OAGG;IACH,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAIrC;;;;;;;;OAQG;IACH,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAIzC;;;;;;;;;;;OAWG;IACH,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM;IAQtD,0FAA0F;IAC1F,OAAO,CAAC,cAAc;IAQtB,wGAAwG;IACxG,OAAO,CAAC,MAAM,CAAC,eAAe;yCA7FV,kBAAkB;2CAAlB,kBAAkB;CAgGvC"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { Directive, Input } from '@angular/core';
|
|
2
|
+
import * as i0 from "@angular/core";
|
|
3
|
+
/**
|
|
4
|
+
* The accessible-name inputs shared by every control in this package that a caller has to be able
|
|
5
|
+
* to NAME: `mj-dropdown`, `mj-combobox`, `mj-switch`, `mj-numeric-input`, `mj-datepicker` and
|
|
6
|
+
* `mj-page-search`.
|
|
7
|
+
*
|
|
8
|
+
* A control with no accessible name announces as its role and state alone — "combobox, collapsed",
|
|
9
|
+
* "switch, on", "edit blank" — which fails WCAG 2.1 4.1.2 (Name, Role, Value). `mj-dropdown` got
|
|
10
|
+
* these four inputs first (#3860); this base is what keeps each sibling from inventing its own
|
|
11
|
+
* spelling of the same idea (#4116).
|
|
12
|
+
*
|
|
13
|
+
* **Absent beats empty.** Subclasses bind every one of these through `X || null`, never as a bare
|
|
14
|
+
* binding. `aria-label=""` is worse than no attribute at all: an explicitly empty name OVERRIDES
|
|
15
|
+
* every other naming source in the accessible-name computation.
|
|
16
|
+
*
|
|
17
|
+
* Subclasses apply the four to the element that actually **receives focus** — naming a
|
|
18
|
+
* non-focusable wrapper announces nothing — and repeat the name on any popup they own, so both
|
|
19
|
+
* halves announce the same thing.
|
|
20
|
+
*/
|
|
21
|
+
export class MJNamedControlBase {
|
|
22
|
+
/**
|
|
23
|
+
* Accessible name for the control, for when no visible label exists. Prefer
|
|
24
|
+
* {@link AriaLabelledBy} when one does — an `aria-label` duplicates that label's text and drifts
|
|
25
|
+
* from it on rename. `aria-labelledby` beats `aria-label` where both are present.
|
|
26
|
+
*/
|
|
27
|
+
AriaLabel = '';
|
|
28
|
+
/**
|
|
29
|
+
* The id of a VISIBLE label element that names this control — the preferred wiring whenever a
|
|
30
|
+
* label already exists on screen. It is also the only path that can name a control's SECONDARY
|
|
31
|
+
* pieces (a filter box, a toggle, a calendar) from that same label: see
|
|
32
|
+
* {@link SecondaryLabelledBy}.
|
|
33
|
+
*/
|
|
34
|
+
AriaLabelledBy = '';
|
|
35
|
+
/**
|
|
36
|
+
* `id` for the control's focusable element, so other markup can REFERENCE it — `aria-controls`,
|
|
37
|
+
* hint text, test hooks.
|
|
38
|
+
*
|
|
39
|
+
* Whether it doubles as a `<label for>` target depends on the control, because `label[for]`
|
|
40
|
+
* associates only with LABELABLE elements: it works on the input-backed controls (`mj-combobox`,
|
|
41
|
+
* `mj-numeric-input`, `mj-datepicker`, `mj-page-search`) and on `mj-switch`, whose `<button>` is
|
|
42
|
+
* labelable too. It does NOT work on `mj-dropdown`, whose trigger is a `div[role=combobox]` —
|
|
43
|
+
* wiring `label[for]` there produces markup that looks correct, passes review, and leaves the
|
|
44
|
+
* control named only in the browsers that guess. Each control's own docs say which it is.
|
|
45
|
+
*/
|
|
46
|
+
InputId = '';
|
|
47
|
+
/** `aria-describedby` passthrough for hint and error text — the same shape of gap as the name. */
|
|
48
|
+
AriaDescribedBy = '';
|
|
49
|
+
/**
|
|
50
|
+
* Counter behind every id a control generates for itself, matching the `static nextId` mechanism
|
|
51
|
+
* `dialog`, `window` and `accordion` already use in this package. One counter across all named
|
|
52
|
+
* controls: the ids only have to be unique in the document, not dense per component.
|
|
53
|
+
*/
|
|
54
|
+
static nextId = 0;
|
|
55
|
+
/** This instance's id suffix, shared by every id the control generates. */
|
|
56
|
+
NamedControlId = MJNamedControlBase.nextId++;
|
|
57
|
+
/**
|
|
58
|
+
* Id of a hidden span holding one fixed WORD of a secondary control's name — "Filter", "Clear",
|
|
59
|
+
* "Open calendar for". Composed with the host's visible label by {@link SecondaryLabelledBy}.
|
|
60
|
+
*/
|
|
61
|
+
SecondaryWordId(slug) {
|
|
62
|
+
return `mj-a11y-${slug}-${this.NamedControlId}`;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* `aria-labelledby` for a secondary control when a VISIBLE label names the host: the hidden word
|
|
66
|
+
* plus that label's own text, composed by the accessibility tree from an id LIST. Empty when
|
|
67
|
+
* there is no such label, in which case {@link SecondaryLabel} supplies a string instead.
|
|
68
|
+
*
|
|
69
|
+
* An id list is the only mechanism that can work here — the component never sees the label's
|
|
70
|
+
* text, only its id. Without it, every filterable dropdown on a form named this way announces an
|
|
71
|
+
* identical "Filter options", and a form with six of them has six indistinguishable filter boxes.
|
|
72
|
+
*/
|
|
73
|
+
SecondaryLabelledBy(slug) {
|
|
74
|
+
return this.AriaLabelledBy ? `${this.SecondaryWordId(slug)} ${this.AriaLabelledBy}` : '';
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* `aria-label` for a secondary control in the no-visible-label case: `word` followed by the
|
|
78
|
+
* host's own name, or `fallback` when the host has no name either.
|
|
79
|
+
*
|
|
80
|
+
* The already-starts-with-`word` guard is not cosmetic: this repo's house habit is
|
|
81
|
+
* `AriaLabel="Filter roles"`, and an unconditional prefix announces that box as
|
|
82
|
+
* "Filter Filter roles".
|
|
83
|
+
*
|
|
84
|
+
* The match ends on a WORD BOUNDARY, not on a space: `AriaLabel="Filter: roles"` already begins
|
|
85
|
+
* with the word and must not be prefixed either, while `"Filters"` is a different word and must
|
|
86
|
+
* be. A space-only test gets the punctuated case wrong in the doubling direction.
|
|
87
|
+
*/
|
|
88
|
+
SecondaryLabel(word, fallback) {
|
|
89
|
+
const name = this.AriaLabel.trim();
|
|
90
|
+
if (!name) {
|
|
91
|
+
return fallback;
|
|
92
|
+
}
|
|
93
|
+
return this.beginsWithWord(name.toLowerCase(), word.toLowerCase()) ? name : `${word} ${name}`;
|
|
94
|
+
}
|
|
95
|
+
/** True when `name` starts with `word` and the next character (if any) ends that word. */
|
|
96
|
+
beginsWithWord(name, word) {
|
|
97
|
+
if (!name.startsWith(word)) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
const next = name.charAt(word.length);
|
|
101
|
+
return next === '' || !MJNamedControlBase.isWordCharacter(next);
|
|
102
|
+
}
|
|
103
|
+
/** `\w` as a regular expression means it: ASCII letters, digits and underscore. Input is lowercased. */
|
|
104
|
+
static isWordCharacter(character) {
|
|
105
|
+
return (character >= 'a' && character <= 'z') || (character >= '0' && character <= '9') || character === '_';
|
|
106
|
+
}
|
|
107
|
+
static ɵfac = function MJNamedControlBase_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || MJNamedControlBase)(); };
|
|
108
|
+
static ɵdir = /*@__PURE__*/ i0.ɵɵdefineDirective({ type: MJNamedControlBase, inputs: { AriaLabel: "AriaLabel", AriaLabelledBy: "AriaLabelledBy", InputId: "InputId", AriaDescribedBy: "AriaDescribedBy" } });
|
|
109
|
+
}
|
|
110
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(MJNamedControlBase, [{
|
|
111
|
+
type: Directive
|
|
112
|
+
}], null, { AriaLabel: [{
|
|
113
|
+
type: Input
|
|
114
|
+
}], AriaLabelledBy: [{
|
|
115
|
+
type: Input
|
|
116
|
+
}], InputId: [{
|
|
117
|
+
type: Input
|
|
118
|
+
}], AriaDescribedBy: [{
|
|
119
|
+
type: Input
|
|
120
|
+
}] }); })();
|
|
121
|
+
//# sourceMappingURL=named-control.base.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"named-control.base.js","sourceRoot":"","sources":["../../../src/lib/a11y/named-control.base.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;;AAEjD;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,OAAgB,kBAAkB;IACtC;;;;OAIG;IACM,SAAS,GAAG,EAAE,CAAC;IAExB;;;;;OAKG;IACM,cAAc,GAAG,EAAE,CAAC;IAE7B;;;;;;;;;;OAUG;IACM,OAAO,GAAG,EAAE,CAAC;IAEtB,kGAAkG;IACzF,eAAe,GAAG,EAAE,CAAC;IAE9B;;;;OAIG;IACK,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC;IAE1B,2EAA2E;IAClE,cAAc,GAAG,kBAAkB,CAAC,MAAM,EAAE,CAAC;IAEtD;;;OAGG;IACH,eAAe,CAAC,IAAY;QAC1B,OAAO,WAAW,IAAI,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;IAClD,CAAC;IAED;;;;;;;;OAQG;IACH,mBAAmB,CAAC,IAAY;QAC9B,OAAO,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAC3F,CAAC;IAED;;;;;;;;;;;OAWG;IACH,cAAc,CAAC,IAAY,EAAE,QAAgB;QAC3C,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC;QACnC,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,OAAO,QAAQ,CAAC;QAClB,CAAC;QACD,OAAO,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,IAAI,EAAE,CAAC;IAChG,CAAC;IAED,0FAA0F;IAClF,cAAc,CAAC,IAAY,EAAE,IAAY;QAC/C,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;YAC3B,OAAO,KAAK,CAAC;QACf,CAAC;QACD,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACtC,OAAO,IAAI,KAAK,EAAE,IAAI,CAAC,kBAAkB,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;IAClE,CAAC;IAED,wGAAwG;IAChG,MAAM,CAAC,eAAe,CAAC,SAAiB;QAC9C,OAAO,CAAC,SAAS,IAAI,GAAG,IAAI,SAAS,IAAI,GAAG,CAAC,IAAI,CAAC,SAAS,IAAI,GAAG,IAAI,SAAS,IAAI,GAAG,CAAC,IAAI,SAAS,KAAK,GAAG,CAAC;IAC/G,CAAC;4GA/FmB,kBAAkB;6DAAlB,kBAAkB;;iFAAlB,kBAAkB;cADvC,SAAS;;kBAOP,KAAK;;kBAQL,KAAK;;kBAaL,KAAK;;kBAGL,KAAK","sourcesContent":["import { Directive, Input } from '@angular/core';\n\n/**\n * The accessible-name inputs shared by every control in this package that a caller has to be able\n * to NAME: `mj-dropdown`, `mj-combobox`, `mj-switch`, `mj-numeric-input`, `mj-datepicker` and\n * `mj-page-search`.\n *\n * A control with no accessible name announces as its role and state alone — \"combobox, collapsed\",\n * \"switch, on\", \"edit blank\" — which fails WCAG 2.1 4.1.2 (Name, Role, Value). `mj-dropdown` got\n * these four inputs first (#3860); this base is what keeps each sibling from inventing its own\n * spelling of the same idea (#4116).\n *\n * **Absent beats empty.** Subclasses bind every one of these through `X || null`, never as a bare\n * binding. `aria-label=\"\"` is worse than no attribute at all: an explicitly empty name OVERRIDES\n * every other naming source in the accessible-name computation.\n *\n * Subclasses apply the four to the element that actually **receives focus** — naming a\n * non-focusable wrapper announces nothing — and repeat the name on any popup they own, so both\n * halves announce the same thing.\n */\n@Directive()\nexport abstract class MJNamedControlBase {\n /**\n * Accessible name for the control, for when no visible label exists. Prefer\n * {@link AriaLabelledBy} when one does — an `aria-label` duplicates that label's text and drifts\n * from it on rename. `aria-labelledby` beats `aria-label` where both are present.\n */\n @Input() AriaLabel = '';\n\n /**\n * The id of a VISIBLE label element that names this control — the preferred wiring whenever a\n * label already exists on screen. It is also the only path that can name a control's SECONDARY\n * pieces (a filter box, a toggle, a calendar) from that same label: see\n * {@link SecondaryLabelledBy}.\n */\n @Input() AriaLabelledBy = '';\n\n /**\n * `id` for the control's focusable element, so other markup can REFERENCE it — `aria-controls`,\n * hint text, test hooks.\n *\n * Whether it doubles as a `<label for>` target depends on the control, because `label[for]`\n * associates only with LABELABLE elements: it works on the input-backed controls (`mj-combobox`,\n * `mj-numeric-input`, `mj-datepicker`, `mj-page-search`) and on `mj-switch`, whose `<button>` is\n * labelable too. It does NOT work on `mj-dropdown`, whose trigger is a `div[role=combobox]` —\n * wiring `label[for]` there produces markup that looks correct, passes review, and leaves the\n * control named only in the browsers that guess. Each control's own docs say which it is.\n */\n @Input() InputId = '';\n\n /** `aria-describedby` passthrough for hint and error text — the same shape of gap as the name. */\n @Input() AriaDescribedBy = '';\n\n /**\n * Counter behind every id a control generates for itself, matching the `static nextId` mechanism\n * `dialog`, `window` and `accordion` already use in this package. One counter across all named\n * controls: the ids only have to be unique in the document, not dense per component.\n */\n private static nextId = 0;\n\n /** This instance's id suffix, shared by every id the control generates. */\n readonly NamedControlId = MJNamedControlBase.nextId++;\n\n /**\n * Id of a hidden span holding one fixed WORD of a secondary control's name — \"Filter\", \"Clear\",\n * \"Open calendar for\". Composed with the host's visible label by {@link SecondaryLabelledBy}.\n */\n SecondaryWordId(slug: string): string {\n return `mj-a11y-${slug}-${this.NamedControlId}`;\n }\n\n /**\n * `aria-labelledby` for a secondary control when a VISIBLE label names the host: the hidden word\n * plus that label's own text, composed by the accessibility tree from an id LIST. Empty when\n * there is no such label, in which case {@link SecondaryLabel} supplies a string instead.\n *\n * An id list is the only mechanism that can work here — the component never sees the label's\n * text, only its id. Without it, every filterable dropdown on a form named this way announces an\n * identical \"Filter options\", and a form with six of them has six indistinguishable filter boxes.\n */\n SecondaryLabelledBy(slug: string): string {\n return this.AriaLabelledBy ? `${this.SecondaryWordId(slug)} ${this.AriaLabelledBy}` : '';\n }\n\n /**\n * `aria-label` for a secondary control in the no-visible-label case: `word` followed by the\n * host's own name, or `fallback` when the host has no name either.\n *\n * The already-starts-with-`word` guard is not cosmetic: this repo's house habit is\n * `AriaLabel=\"Filter roles\"`, and an unconditional prefix announces that box as\n * \"Filter Filter roles\".\n *\n * The match ends on a WORD BOUNDARY, not on a space: `AriaLabel=\"Filter: roles\"` already begins\n * with the word and must not be prefixed either, while `\"Filters\"` is a different word and must\n * be. A space-only test gets the punctuated case wrong in the doubling direction.\n */\n SecondaryLabel(word: string, fallback: string): string {\n const name = this.AriaLabel.trim();\n if (!name) {\n return fallback;\n }\n return this.beginsWithWord(name.toLowerCase(), word.toLowerCase()) ? name : `${word} ${name}`;\n }\n\n /** True when `name` starts with `word` and the next character (if any) ends that word. */\n private beginsWithWord(name: string, word: string): boolean {\n if (!name.startsWith(word)) {\n return false;\n }\n const next = name.charAt(word.length);\n return next === '' || !MJNamedControlBase.isWordCharacter(next);\n }\n\n /** `\\w` as a regular expression means it: ASCII letters, digits and underscore. Input is lowercased. */\n private static isWordCharacter(character: string): boolean {\n return (character >= 'a' && character <= 'z') || (character >= '0' && character <= '9') || character === '_';\n }\n}\n"]}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for {@link WarnIfUnnamed}.
|
|
3
|
+
*/
|
|
4
|
+
export type UnnamedControlGuardOptions = {
|
|
5
|
+
/**
|
|
6
|
+
* Whether a non-empty `placeholder` counts as this control's name.
|
|
7
|
+
*
|
|
8
|
+
* **False for every form control**, and deliberately so: a placeholder disappears the moment the
|
|
9
|
+
* user types, and counting it would silence the guard for most of the controls it exists to catch
|
|
10
|
+
* — nearly every `mj-combobox` and `mj-datepicker` call site in this repo already passes a
|
|
11
|
+
* `Placeholder`, so a new one could still ship unnamed without a word of warning.
|
|
12
|
+
*
|
|
13
|
+
* True only for `mj-page-search`, a toolbar widget whose placeholder ("Search templates…") IS the
|
|
14
|
+
* caller's statement of what the box searches, and whose default placeholder is never absent.
|
|
15
|
+
*/
|
|
16
|
+
PlaceholderIsName?: boolean;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Dev-mode guard: warn when one of this package's named controls renders with **no accessible name
|
|
20
|
+
* at all**.
|
|
21
|
+
*
|
|
22
|
+
* This is the recorded answer to the prevention question raised in #3860 and resolved in #4116: an
|
|
23
|
+
* unnamed control is silent, which is why the defect survived as long as it did. Without a feedback
|
|
24
|
+
* loop, every dropdown or combobox added tomorrow ships unnamed and silent again. It follows the two
|
|
25
|
+
* guards already shipping in this package — `mjButton`'s icon-only warning and `mjClickable`'s —
|
|
26
|
+
* rather than inventing a third shape: dev-mode only, once per rendered control, never throwing, and
|
|
27
|
+
* always logging the offending element so it is one click to find.
|
|
28
|
+
*
|
|
29
|
+
* **What counts as a name here** is what the accessible-name computation would actually resolve on
|
|
30
|
+
* the element: an `aria-labelledby` whose ids resolve to real elements, `aria-label`, an associated
|
|
31
|
+
* `<label for>`, `title`, and — only where {@link UnnamedControlGuardOptions.PlaceholderIsName} says
|
|
32
|
+
* so — a `placeholder`. A dangling `aria-labelledby` does NOT count: an idref pointing at an element
|
|
33
|
+
* that was renamed or is not rendered yet computes to an empty name, and that silent half-wiring is
|
|
34
|
+
* exactly the failure this guard is for.
|
|
35
|
+
*
|
|
36
|
+
* It does not judge name *quality* beyond that, because a guard that fires on a merely-imperfect
|
|
37
|
+
* name gets muted and then the empty ones go unnoticed too.
|
|
38
|
+
*
|
|
39
|
+
* Text content is **not** counted. None of these controls take their name from their contents: a
|
|
40
|
+
* `div[role=combobox]`'s text is the selected value and a `role=switch`'s is its state, so counting
|
|
41
|
+
* it would make the guard fall silent for exactly the controls it exists to catch.
|
|
42
|
+
*
|
|
43
|
+
* **Known limitation**, shared with the two existing guards: it runs once, after the view
|
|
44
|
+
* initializes, so a name bound to data that arrives later (`[AriaLabel]="record?.Name"`), or one
|
|
45
|
+
* pointing at a label that a later change detection renders, warns spuriously on that first pass.
|
|
46
|
+
*
|
|
47
|
+
* @param element the control's focusable element — the one the name has to land on
|
|
48
|
+
* @param selector the control's tag, for the message prefix (`mj-combobox`)
|
|
49
|
+
* @param options per-control naming rules; see {@link UnnamedControlGuardOptions}
|
|
50
|
+
*/
|
|
51
|
+
export declare function WarnIfUnnamed(element: HTMLElement | null | undefined, selector: string, options?: UnnamedControlGuardOptions): void;
|
|
52
|
+
//# sourceMappingURL=unnamed-control-guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unnamed-control-guard.d.ts","sourceRoot":"","sources":["../../../src/lib/a11y/unnamed-control-guard.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,MAAM,0BAA0B,GAAG;IACvC;;;;;;;;;;OAUG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS,EACvC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,0BAA+B,GACvC,IAAI,CAiBN"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { isDevMode } from '@angular/core';
|
|
2
|
+
/**
|
|
3
|
+
* Dev-mode guard: warn when one of this package's named controls renders with **no accessible name
|
|
4
|
+
* at all**.
|
|
5
|
+
*
|
|
6
|
+
* This is the recorded answer to the prevention question raised in #3860 and resolved in #4116: an
|
|
7
|
+
* unnamed control is silent, which is why the defect survived as long as it did. Without a feedback
|
|
8
|
+
* loop, every dropdown or combobox added tomorrow ships unnamed and silent again. It follows the two
|
|
9
|
+
* guards already shipping in this package — `mjButton`'s icon-only warning and `mjClickable`'s —
|
|
10
|
+
* rather than inventing a third shape: dev-mode only, once per rendered control, never throwing, and
|
|
11
|
+
* always logging the offending element so it is one click to find.
|
|
12
|
+
*
|
|
13
|
+
* **What counts as a name here** is what the accessible-name computation would actually resolve on
|
|
14
|
+
* the element: an `aria-labelledby` whose ids resolve to real elements, `aria-label`, an associated
|
|
15
|
+
* `<label for>`, `title`, and — only where {@link UnnamedControlGuardOptions.PlaceholderIsName} says
|
|
16
|
+
* so — a `placeholder`. A dangling `aria-labelledby` does NOT count: an idref pointing at an element
|
|
17
|
+
* that was renamed or is not rendered yet computes to an empty name, and that silent half-wiring is
|
|
18
|
+
* exactly the failure this guard is for.
|
|
19
|
+
*
|
|
20
|
+
* It does not judge name *quality* beyond that, because a guard that fires on a merely-imperfect
|
|
21
|
+
* name gets muted and then the empty ones go unnoticed too.
|
|
22
|
+
*
|
|
23
|
+
* Text content is **not** counted. None of these controls take their name from their contents: a
|
|
24
|
+
* `div[role=combobox]`'s text is the selected value and a `role=switch`'s is its state, so counting
|
|
25
|
+
* it would make the guard fall silent for exactly the controls it exists to catch.
|
|
26
|
+
*
|
|
27
|
+
* **Known limitation**, shared with the two existing guards: it runs once, after the view
|
|
28
|
+
* initializes, so a name bound to data that arrives later (`[AriaLabel]="record?.Name"`), or one
|
|
29
|
+
* pointing at a label that a later change detection renders, warns spuriously on that first pass.
|
|
30
|
+
*
|
|
31
|
+
* @param element the control's focusable element — the one the name has to land on
|
|
32
|
+
* @param selector the control's tag, for the message prefix (`mj-combobox`)
|
|
33
|
+
* @param options per-control naming rules; see {@link UnnamedControlGuardOptions}
|
|
34
|
+
*/
|
|
35
|
+
export function WarnIfUnnamed(element, selector, options = {}) {
|
|
36
|
+
if (!isDevMode() || !element) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
if (hasAccessibleName(element, options)) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
console.warn(`[${selector}] has no accessible name — it announces as its role and state alone, which fails ` +
|
|
44
|
+
'WCAG 2.1 4.1.2. Pass AriaLabelledBy="<id of a visible label>", or AriaLabel="…" when there ' +
|
|
45
|
+
'is no visible label.', element);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Never let diagnostics break the control.
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** True when the accessible-name computation would resolve any name for this element. */
|
|
52
|
+
function hasAccessibleName(element, options) {
|
|
53
|
+
const attributes = options.PlaceholderIsName ? ['aria-label', 'title', 'placeholder'] : ['aria-label', 'title'];
|
|
54
|
+
const named = attributes.some((attribute) => element.getAttribute(attribute)?.trim());
|
|
55
|
+
return named || hasResolvedLabelledBy(element) || hasAssociatedLabel(element);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* True when at least one id in `aria-labelledby` resolves to an element in the document.
|
|
59
|
+
*
|
|
60
|
+
* The resolution check is the point: `aria-labelledby` pointing at nothing computes to an EMPTY
|
|
61
|
+
* name, so treating the attribute's mere presence as proof of a name would wave through the
|
|
62
|
+
* renamed-label case — markup that looks correct and leaves the control unnamed.
|
|
63
|
+
*/
|
|
64
|
+
function hasResolvedLabelledBy(element) {
|
|
65
|
+
const ids = element.getAttribute('aria-labelledby')?.trim();
|
|
66
|
+
if (!ids) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
const doc = element.ownerDocument;
|
|
70
|
+
return ids.split(/\s+/).some((id) => !!doc?.getElementById(id));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* True when a `<label>` names this element through the DOM's own association.
|
|
74
|
+
*
|
|
75
|
+
* Read off the element's `labels` collection rather than by scanning for a `label[for]` that
|
|
76
|
+
* mentions its id, because `label[for]` associates only with LABELABLE elements — button, input,
|
|
77
|
+
* select, textarea and friends. `mj-dropdown`'s trigger is a `div[role=combobox]`, so a caller who
|
|
78
|
+
* writes `<label for="role-dd">Role</label><mj-dropdown InputId="role-dd">` gets markup that looks
|
|
79
|
+
* correct and an association the HTML standard does not make. (Blink names that div anyway; Gecko
|
|
80
|
+
* and WebKit do not, so the name a user hears depends on their browser — which is a defect to warn
|
|
81
|
+
* about, not a name to accept.) Matching on the id alone would wave that straight through, and it
|
|
82
|
+
* is precisely the mistake `InputId`'s own documentation warns callers away from.
|
|
83
|
+
*
|
|
84
|
+
* `'labels' in element` is the honest discriminator: the property exists on labelable elements and
|
|
85
|
+
* is absent on a div, so nothing has to be cast to a type the element may not be.
|
|
86
|
+
*/
|
|
87
|
+
function hasAssociatedLabel(element) {
|
|
88
|
+
if (!('labels' in element)) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
const { labels } = element;
|
|
92
|
+
return !!labels && labels.length > 0;
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=unnamed-control-guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unnamed-control-guard.js","sourceRoot":"","sources":["../../../src/lib/a11y/unnamed-control-guard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAoB1C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,UAAU,aAAa,CAC3B,OAAuC,EACvC,QAAgB,EAChB,UAAsC,EAAE;IAExC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC;QAC7B,OAAO;IACT,CAAC;IACD,IAAI,CAAC;QACH,IAAI,iBAAiB,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC;YACxC,OAAO;QACT,CAAC;QACD,OAAO,CAAC,IAAI,CACV,IAAI,QAAQ,mFAAmF;YAC7F,6FAA6F;YAC7F,sBAAsB,EACxB,OAAO,CACR,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,2CAA2C;IAC7C,CAAC;AACH,CAAC;AAED,yFAAyF;AACzF,SAAS,iBAAiB,CAAC,OAAoB,EAAE,OAAmC;IAClF,MAAM,UAAU,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,YAAY,EAAE,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;IAChH,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,OAAO,CAAC,YAAY,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC;IACtF,OAAO,KAAK,IAAI,qBAAqB,CAAC,OAAO,CAAC,IAAI,kBAAkB,CAAC,OAAO,CAAC,CAAC;AAChF,CAAC;AAED;;;;;;GAMG;AACH,SAAS,qBAAqB,CAAC,OAAoB;IACjD,MAAM,GAAG,GAAG,OAAO,CAAC,YAAY,CAAC,iBAAiB,CAAC,EAAE,IAAI,EAAE,CAAC;IAC5D,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,GAAG,GAAG,OAAO,CAAC,aAAa,CAAC;IAClC,OAAO,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,EAAE,cAAc,CAAC,EAAE,CAAC,CAAC,CAAC;AAClE,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,SAAS,kBAAkB,CAAC,OAAoB;IAC9C,IAAI,CAAC,CAAC,QAAQ,IAAI,OAAO,CAAC,EAAE,CAAC;QAC3B,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,EAAE,MAAM,EAAE,GAAG,OAAiF,CAAC;IACrG,OAAO,CAAC,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC;AACvC,CAAC","sourcesContent":["import { isDevMode } from '@angular/core';\n\n/**\n * Options for {@link WarnIfUnnamed}.\n */\nexport type UnnamedControlGuardOptions = {\n /**\n * Whether a non-empty `placeholder` counts as this control's name.\n *\n * **False for every form control**, and deliberately so: a placeholder disappears the moment the\n * user types, and counting it would silence the guard for most of the controls it exists to catch\n * — nearly every `mj-combobox` and `mj-datepicker` call site in this repo already passes a\n * `Placeholder`, so a new one could still ship unnamed without a word of warning.\n *\n * True only for `mj-page-search`, a toolbar widget whose placeholder (\"Search templates…\") IS the\n * caller's statement of what the box searches, and whose default placeholder is never absent.\n */\n PlaceholderIsName?: boolean;\n};\n\n/**\n * Dev-mode guard: warn when one of this package's named controls renders with **no accessible name\n * at all**.\n *\n * This is the recorded answer to the prevention question raised in #3860 and resolved in #4116: an\n * unnamed control is silent, which is why the defect survived as long as it did. Without a feedback\n * loop, every dropdown or combobox added tomorrow ships unnamed and silent again. It follows the two\n * guards already shipping in this package — `mjButton`'s icon-only warning and `mjClickable`'s —\n * rather than inventing a third shape: dev-mode only, once per rendered control, never throwing, and\n * always logging the offending element so it is one click to find.\n *\n * **What counts as a name here** is what the accessible-name computation would actually resolve on\n * the element: an `aria-labelledby` whose ids resolve to real elements, `aria-label`, an associated\n * `<label for>`, `title`, and — only where {@link UnnamedControlGuardOptions.PlaceholderIsName} says\n * so — a `placeholder`. A dangling `aria-labelledby` does NOT count: an idref pointing at an element\n * that was renamed or is not rendered yet computes to an empty name, and that silent half-wiring is\n * exactly the failure this guard is for.\n *\n * It does not judge name *quality* beyond that, because a guard that fires on a merely-imperfect\n * name gets muted and then the empty ones go unnoticed too.\n *\n * Text content is **not** counted. None of these controls take their name from their contents: a\n * `div[role=combobox]`'s text is the selected value and a `role=switch`'s is its state, so counting\n * it would make the guard fall silent for exactly the controls it exists to catch.\n *\n * **Known limitation**, shared with the two existing guards: it runs once, after the view\n * initializes, so a name bound to data that arrives later (`[AriaLabel]=\"record?.Name\"`), or one\n * pointing at a label that a later change detection renders, warns spuriously on that first pass.\n *\n * @param element the control's focusable element — the one the name has to land on\n * @param selector the control's tag, for the message prefix (`mj-combobox`)\n * @param options per-control naming rules; see {@link UnnamedControlGuardOptions}\n */\nexport function WarnIfUnnamed(\n element: HTMLElement | null | undefined,\n selector: string,\n options: UnnamedControlGuardOptions = {}\n): void {\n if (!isDevMode() || !element) {\n return;\n }\n try {\n if (hasAccessibleName(element, options)) {\n return;\n }\n console.warn(\n `[${selector}] has no accessible name — it announces as its role and state alone, which fails ` +\n 'WCAG 2.1 4.1.2. Pass AriaLabelledBy=\"<id of a visible label>\", or AriaLabel=\"…\" when there ' +\n 'is no visible label.',\n element\n );\n } catch {\n // Never let diagnostics break the control.\n }\n}\n\n/** True when the accessible-name computation would resolve any name for this element. */\nfunction hasAccessibleName(element: HTMLElement, options: UnnamedControlGuardOptions): boolean {\n const attributes = options.PlaceholderIsName ? ['aria-label', 'title', 'placeholder'] : ['aria-label', 'title'];\n const named = attributes.some((attribute) => element.getAttribute(attribute)?.trim());\n return named || hasResolvedLabelledBy(element) || hasAssociatedLabel(element);\n}\n\n/**\n * True when at least one id in `aria-labelledby` resolves to an element in the document.\n *\n * The resolution check is the point: `aria-labelledby` pointing at nothing computes to an EMPTY\n * name, so treating the attribute's mere presence as proof of a name would wave through the\n * renamed-label case — markup that looks correct and leaves the control unnamed.\n */\nfunction hasResolvedLabelledBy(element: HTMLElement): boolean {\n const ids = element.getAttribute('aria-labelledby')?.trim();\n if (!ids) {\n return false;\n }\n const doc = element.ownerDocument;\n return ids.split(/\\s+/).some((id) => !!doc?.getElementById(id));\n}\n\n/**\n * True when a `<label>` names this element through the DOM's own association.\n *\n * Read off the element's `labels` collection rather than by scanning for a `label[for]` that\n * mentions its id, because `label[for]` associates only with LABELABLE elements — button, input,\n * select, textarea and friends. `mj-dropdown`'s trigger is a `div[role=combobox]`, so a caller who\n * writes `<label for=\"role-dd\">Role</label><mj-dropdown InputId=\"role-dd\">` gets markup that looks\n * correct and an association the HTML standard does not make. (Blink names that div anyway; Gecko\n * and WebKit do not, so the name a user hears depends on their browser — which is a defect to warn\n * about, not a name to accept.) Matching on the id alone would wave that straight through, and it\n * is precisely the mistake `InputId`'s own documentation warns callers away from.\n *\n * `'labels' in element` is the honest discriminator: the property exists on labelable elements and\n * is absent on a div, so nothing has to be cast to a type the element may not be.\n */\nfunction hasAssociatedLabel(element: HTMLElement): boolean {\n if (!('labels' in element)) {\n return false;\n }\n const { labels } = element as HTMLElement & { readonly labels: NodeListOf<HTMLLabelElement> | null };\n return !!labels && labels.length > 0;\n}\n"]}
|
|
@@ -150,9 +150,21 @@ export declare class MJAccordionPanelComponent {
|
|
|
150
150
|
*/
|
|
151
151
|
Fill: boolean;
|
|
152
152
|
ExpandedChange: EventEmitter<boolean>;
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
153
|
+
TitleTemplate: MJAccordionTitleDirective | null;
|
|
154
|
+
/** @deprecated Use {@link TitleTemplate}. */
|
|
155
|
+
get titleTemplate(): MJAccordionTitleDirective | null;
|
|
156
|
+
/** @deprecated Use {@link TitleTemplate}. */
|
|
157
|
+
set titleTemplate(value: MJAccordionTitleDirective | null);
|
|
158
|
+
ActionsTemplate: MJAccordionActionsDirective | null;
|
|
159
|
+
/** @deprecated Use {@link ActionsTemplate}. */
|
|
160
|
+
get actionsTemplate(): MJAccordionActionsDirective | null;
|
|
161
|
+
/** @deprecated Use {@link ActionsTemplate}. */
|
|
162
|
+
set actionsTemplate(value: MJAccordionActionsDirective | null);
|
|
163
|
+
BodyTemplate: MJAccordionBodyDirective | null;
|
|
164
|
+
/** @deprecated Use {@link BodyTemplate}. */
|
|
165
|
+
get bodyTemplate(): MJAccordionBodyDirective | null;
|
|
166
|
+
/** @deprecated Use {@link BodyTemplate}. */
|
|
167
|
+
set bodyTemplate(value: MJAccordionBodyDirective | null);
|
|
156
168
|
readonly hostClass = true;
|
|
157
169
|
/** Host claims flex:1 of its parent ONLY while a Fill panel is expanded —
|
|
158
170
|
* so a collapsed Fill panel sits at natural (header) height like any other. */
|
|
@@ -166,7 +178,7 @@ export declare class MJAccordionPanelComponent {
|
|
|
166
178
|
readonly BodyId: string;
|
|
167
179
|
Toggle(): void;
|
|
168
180
|
static ɵfac: i0.ɵɵFactoryDeclaration<MJAccordionPanelComponent, never>;
|
|
169
|
-
static ɵcmp: i0.ɵɵComponentDeclaration<MJAccordionPanelComponent, "mj-accordion-panel", never, { "Title": { "alias": "Title"; "required": false; }; "Expanded": { "alias": "Expanded"; "required": false; }; "Disabled": { "alias": "Disabled"; "required": false; }; "Variant": { "alias": "Variant"; "required": false; }; "TitleIconMuted": { "alias": "TitleIconMuted"; "required": false; }; "Size": { "alias": "Size"; "required": false; }; "Bare": { "alias": "Bare"; "required": false; }; "FlushBody": { "alias": "FlushBody"; "required": false; }; "Fill": { "alias": "Fill"; "required": false; }; }, { "ExpandedChange": "ExpandedChange"; }, ["
|
|
181
|
+
static ɵcmp: i0.ɵɵComponentDeclaration<MJAccordionPanelComponent, "mj-accordion-panel", never, { "Title": { "alias": "Title"; "required": false; }; "Expanded": { "alias": "Expanded"; "required": false; }; "Disabled": { "alias": "Disabled"; "required": false; }; "Variant": { "alias": "Variant"; "required": false; }; "TitleIconMuted": { "alias": "TitleIconMuted"; "required": false; }; "Size": { "alias": "Size"; "required": false; }; "Bare": { "alias": "Bare"; "required": false; }; "FlushBody": { "alias": "FlushBody"; "required": false; }; "Fill": { "alias": "Fill"; "required": false; }; }, { "ExpandedChange": "ExpandedChange"; }, ["TitleTemplate", "ActionsTemplate", "BodyTemplate"], ["*"], true, never>;
|
|
170
182
|
}
|
|
171
183
|
/**
|
|
172
184
|
* Convenience module bundling the accordion panel + all of its slot directives.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"accordion.component.d.ts","sourceRoot":"","sources":["../../../src/lib/accordion/accordion.component.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4B,YAAY,EAA6B,WAAW,EAAuB,MAAM,eAAe,CAAC;;AAOpI;;;;;;;;;;;;;;GAcG;AACH,qBAIa,yBAAyB;IACjB,WAAW,EAAE,WAAW,CAAC,OAAO,CAAC;gBAAjC,WAAW,EAAE,WAAW,CAAC,OAAO,CAAC;yCADzC,yBAAyB;2CAAzB,yBAAyB;CAErC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAIa,2BAA2B;IACnB,WAAW,EAAE,WAAW,CAAC,OAAO,CAAC;gBAAjC,WAAW,EAAE,WAAW,CAAC,OAAO,CAAC;yCADzC,2BAA2B;2CAA3B,2BAA2B;CAEvC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAIa,wBAAwB;IAChB,WAAW,EAAE,WAAW,CAAC,OAAO,CAAC;gBAAjC,WAAW,EAAE,WAAW,CAAC,OAAO,CAAC;yCADzC,wBAAwB;2CAAxB,wBAAwB;CAEpC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAwDa,yBAAyB;IAC3B,KAAK,SAAM;IACpB,OAAO,CAAC,SAAS,CAAS;IAC1B;;yDAEqD;IACrD,OAAO,CAAC,aAAa,CAAS;IAC9B,IACI,QAAQ,CAAC,KAAK,EAAE,OAAO,EAG1B;IACD,IAAI,QAAQ,IAAI,OAAO,CAA2B;IAClD,SAAS,KAAK,eAAe,IAAI,OAAO,CAA+B;IAC9D,QAAQ,UAAS;IAC1B;;;;;;OAMG;IACM,OAAO,EAAE,SAAS,GAAG,SAAS,GAAG,WAAW,CAAa;IAClE;;;;OAIG;IACM,cAAc,UAAS;IAChC;;;OAGG;IACM,IAAI,EAAE,IAAI,GAAG,IAAI,CAAQ;IAClC;;;;OAIG;IACM,IAAI,UAAS;IACtB;;;OAGG;IACM,SAAS,UAAS;IAC3B;;;;;;;;;;;;;OAaG;IACM,IAAI,UAAS;IACZ,cAAc,wBAA+B;IACd,aAAa,EAAE,yBAAyB,GAAG,IAAI,CAAQ;
|
|
1
|
+
{"version":3,"file":"accordion.component.d.ts","sourceRoot":"","sources":["../../../src/lib/accordion/accordion.component.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4B,YAAY,EAA6B,WAAW,EAAuB,MAAM,eAAe,CAAC;;AAOpI;;;;;;;;;;;;;;GAcG;AACH,qBAIa,yBAAyB;IACjB,WAAW,EAAE,WAAW,CAAC,OAAO,CAAC;gBAAjC,WAAW,EAAE,WAAW,CAAC,OAAO,CAAC;yCADzC,yBAAyB;2CAAzB,yBAAyB;CAErC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAIa,2BAA2B;IACnB,WAAW,EAAE,WAAW,CAAC,OAAO,CAAC;gBAAjC,WAAW,EAAE,WAAW,CAAC,OAAO,CAAC;yCADzC,2BAA2B;2CAA3B,2BAA2B;CAEvC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAIa,wBAAwB;IAChB,WAAW,EAAE,WAAW,CAAC,OAAO,CAAC;gBAAjC,WAAW,EAAE,WAAW,CAAC,OAAO,CAAC;yCADzC,wBAAwB;2CAAxB,wBAAwB;CAEpC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAwDa,yBAAyB;IAC3B,KAAK,SAAM;IACpB,OAAO,CAAC,SAAS,CAAS;IAC1B;;yDAEqD;IACrD,OAAO,CAAC,aAAa,CAAS;IAC9B,IACI,QAAQ,CAAC,KAAK,EAAE,OAAO,EAG1B;IACD,IAAI,QAAQ,IAAI,OAAO,CAA2B;IAClD,SAAS,KAAK,eAAe,IAAI,OAAO,CAA+B;IAC9D,QAAQ,UAAS;IAC1B;;;;;;OAMG;IACM,OAAO,EAAE,SAAS,GAAG,SAAS,GAAG,WAAW,CAAa;IAClE;;;;OAIG;IACM,cAAc,UAAS;IAChC;;;OAGG;IACM,IAAI,EAAE,IAAI,GAAG,IAAI,CAAQ;IAClC;;;;OAIG;IACM,IAAI,UAAS;IACtB;;;OAGG;IACM,SAAS,UAAS;IAC3B;;;;;;;;;;;;;OAaG;IACM,IAAI,UAAS;IACZ,cAAc,wBAA+B;IACd,aAAa,EAAE,yBAAyB,GAAG,IAAI,CAAQ;IAEhG,6CAA6C;IAC7C,IAAI,aAAa,IAAI,yBAAyB,GAAG,IAAI,CAEpD;IACD,6CAA6C;IAC7C,IAAI,aAAa,CAAC,KAAK,EAAE,yBAAyB,GAAG,IAAI,EAExD;IAC0C,eAAe,EAAE,2BAA2B,GAAG,IAAI,CAAQ;IAEtG,+CAA+C;IAC/C,IAAI,eAAe,IAAI,2BAA2B,GAAG,IAAI,CAExD;IACD,+CAA+C;IAC/C,IAAI,eAAe,CAAC,KAAK,EAAE,2BAA2B,GAAG,IAAI,EAE5D;IACuC,YAAY,EAAE,wBAAwB,GAAG,IAAI,CAAQ;IAE7F,4CAA4C;IAC5C,IAAI,YAAY,IAAI,wBAAwB,GAAG,IAAI,CAElD;IACD,4CAA4C;IAC5C,IAAI,YAAY,CAAC,KAAK,EAAE,wBAAwB,GAAG,IAAI,EAEtD;IAC6C,QAAQ,CAAC,SAAS,QAAQ;IACxE;oFACgF;IAChF,IAAmD,UAAU,IAAI,OAAO,CAEvE;IAED;;;yCAGqC;IACrC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAsB;IAC3C,QAAQ,CAAC,QAAQ,SAAsC;IACvD,QAAQ,CAAC,MAAM,SAAoC;IAEnD,MAAM,IAAI,IAAI;yCA1GH,yBAAyB;2CAAzB,yBAAyB;CA+GrC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,qBAca,iBAAiB;yCAAjB,iBAAiB;0CAAjB,iBAAiB,iBAhJjB,yBAAyB,SAxIzB,yBAAyB,SA0BzB,2BAA2B,SA2B3B,wBAAwB,WAmFxB,yBAAyB,SAxIzB,yBAAyB,SA0BzB,2BAA2B,SA2B3B,wBAAwB;0CAmOxB,iBAAiB;CAAG"}
|