@sb1/indeks-web 0.7.0 → 0.9.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/CHANGELOG.md +12 -0
- package/dist/npm/index.d.ts +13 -0
- package/dist/npm/index.js +207 -1
- package/dist/npm/lib/components/radio-group/IxRadioGroup.d.ts +77 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @sb1/indeks-web
|
|
2
2
|
|
|
3
|
+
## 0.9.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- f0707b4: Legg til `RadioGroup` og `RadioButton` med full støtte for tilstander, orientering og a11y.
|
|
8
|
+
|
|
9
|
+
- `@sb1/indeks-css`: `radio-group.css` skrevet om — alle tilstander (default, hover, focus, selected, error, readOnly, disabled), vertikal/horisontal orientering, touch-mål 44×44 px, `prefers-reduced-motion`.
|
|
10
|
+
- `@sb1/indeks-web`: ny `<ix-radio-group>` web component (ARIA-lim) som setter `role="radiogroup"`, `aria-labelledby`, `aria-describedby`, `aria-live="polite"` og synkroniserer `aria-invalid`, `name`, `disabled`, `required` og `aria-required` på riktig nivå. Blokkerer tastatur-endringer i `readonly`-tilstand.
|
|
11
|
+
- `@sb1/indeks-react`: `RadioGroup` skrevet om til compound-mønster med `RadioButton`-barn. Støtter kontrollert/ukontrollert bruk, `legend`, `description`, `errorMessage`, `orientation`, `hideLegend`, `disabled`, `readOnly`, `required`. Erstatter den gamle flat-array-API-en (breaking endring i komponentens API, men ikke i typenavn).
|
|
12
|
+
|
|
13
|
+
## 0.8.0
|
|
14
|
+
|
|
3
15
|
## 0.7.0
|
|
4
16
|
|
|
5
17
|
## 0.6.0
|
package/dist/npm/index.d.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { IxField } from './lib/components/field/IxField.js';
|
|
2
2
|
import { IxIcon, IconName } from './lib/components/icon/IxIcon.js';
|
|
3
|
+
import { IxRadioGroup } from './lib/components/radio-group/IxRadioGroup.js';
|
|
3
4
|
export { IxField };
|
|
4
5
|
export { IxIcon };
|
|
6
|
+
export { IxRadioGroup };
|
|
5
7
|
export { ICON_NAMES } from './lib/components/icon/IxIcon.js';
|
|
6
8
|
export type { IconName, IconValue } from './lib/components/icon/IxIcon.js';
|
|
7
9
|
declare global {
|
|
8
10
|
interface HTMLElementTagNameMap {
|
|
9
11
|
'ix-field': IxField;
|
|
10
12
|
'ix-icon': IxIcon;
|
|
13
|
+
'ix-radio-group': IxRadioGroup;
|
|
11
14
|
}
|
|
12
15
|
namespace JSX {
|
|
13
16
|
interface IntrinsicElements {
|
|
@@ -16,6 +19,16 @@ declare global {
|
|
|
16
19
|
class?: string;
|
|
17
20
|
children?: unknown;
|
|
18
21
|
};
|
|
22
|
+
'ix-radio-group': {
|
|
23
|
+
name?: string;
|
|
24
|
+
disabled?: boolean | '';
|
|
25
|
+
readonly?: boolean | '';
|
|
26
|
+
required?: boolean | '';
|
|
27
|
+
'data-orientation'?: 'vertical' | 'horizontal';
|
|
28
|
+
'data-state'?: 'error' | 'readonly' | 'disabled';
|
|
29
|
+
class?: string;
|
|
30
|
+
children?: unknown;
|
|
31
|
+
};
|
|
19
32
|
'ix-icon': {
|
|
20
33
|
name?: IconName;
|
|
21
34
|
materialdesignname?: string;
|
package/dist/npm/index.js
CHANGED
|
@@ -281,6 +281,211 @@ var IxIcon = class extends HTMLElement {
|
|
|
281
281
|
}
|
|
282
282
|
};
|
|
283
283
|
//#endregion
|
|
284
|
+
//#region lib/components/radio-group/IxRadioGroup.ts
|
|
285
|
+
/**
|
|
286
|
+
* IxRadioGroup — ARIA-lim for radioknapp-grupper.
|
|
287
|
+
*
|
|
288
|
+
* ## Designfilosofi
|
|
289
|
+
*
|
|
290
|
+
* Komponenten erstatter ingen native HTML-elementer. Forfatteren skriver
|
|
291
|
+
* native <input type="radio">, <label> og hjelpetekster inni <ix-radio-group>.
|
|
292
|
+
* Komponenten sin eneste jobb er å sette opp ARIA-koblingene mellom dem.
|
|
293
|
+
*
|
|
294
|
+
* ## Hva komponenten gjør
|
|
295
|
+
*
|
|
296
|
+
* 1. Setter role="radiogroup" på host slik at skjermlesere annonserer gruppen
|
|
297
|
+
* med riktig rolle og leser legend-teksten som gruppens tilgjengelige navn.
|
|
298
|
+
*
|
|
299
|
+
* 2. Kobler legend via aria-labelledby — genererer ID på [data-field="legend"]
|
|
300
|
+
* om nødvendig og peker host til den.
|
|
301
|
+
*
|
|
302
|
+
* 3. Setter aria-describedby på host med IDer til description og error
|
|
303
|
+
* (description alltid først). Hopper over elementer som mangler.
|
|
304
|
+
*
|
|
305
|
+
* 4. Genererer stabile IDer på hvert input og setter htmlFor på matchende label
|
|
306
|
+
* om label ikke allerede har det. Respekterer alltid eksisterende IDer.
|
|
307
|
+
*
|
|
308
|
+
* 5. Synkroniserer name-attributt på alle inputs — name er mekanismen
|
|
309
|
+
* nettleseren bruker for mutual exclusivity (ett valg av gangen) og
|
|
310
|
+
* ArrowKey-navigasjon mellom alternativene. Alle inputs i gruppen må ha
|
|
311
|
+
* samme name. Prioritet: name på host > name på input > auto-generert.
|
|
312
|
+
*
|
|
313
|
+
* 6. Setter aria-live="polite" på [data-field="error"] og observerer
|
|
314
|
+
* textContent-endringer: ikke-tomt innhold → aria-invalid="true" på host;
|
|
315
|
+
* tomt → aria-invalid fjernes. aria-invalid settes kun på host (ikke per
|
|
316
|
+
* input) — det er gruppens validitet som telles, og en duplisert flagg
|
|
317
|
+
* per input gir ingen merverdi for skjermlesere.
|
|
318
|
+
*
|
|
319
|
+
* 7. Propagerer disabled/readonly til alle inputs via attributeChangedCallback.
|
|
320
|
+
* Per-knapp disabled bevares — settes med en WeakMap ved første _wire().
|
|
321
|
+
*
|
|
322
|
+
* 8. Lytter på childList på host slik at nye <input type="radio">-elementer
|
|
323
|
+
* som legges til etter mount automatisk wires (id, name, htmlFor, disabled).
|
|
324
|
+
*
|
|
325
|
+
* ## Ingen Shadow DOM
|
|
326
|
+
*
|
|
327
|
+
* Komponenten bruker light DOM slik at CSS fra @sb1/indeks-css når alle
|
|
328
|
+
* delelementer direkte.
|
|
329
|
+
*
|
|
330
|
+
* @example
|
|
331
|
+
* <ix-radio-group>
|
|
332
|
+
* <span data-field="legend">Velg kundetype</span>
|
|
333
|
+
* <span data-field="description">Beskrivelse</span>
|
|
334
|
+
* <div class="ix-radio-button">
|
|
335
|
+
* <input type="radio" value="privat" />
|
|
336
|
+
* <label>Privat</label>
|
|
337
|
+
* </div>
|
|
338
|
+
* <div class="ix-radio-button">
|
|
339
|
+
* <input type="radio" value="bedrift" />
|
|
340
|
+
* <label>Bedrift</label>
|
|
341
|
+
* </div>
|
|
342
|
+
* <span data-field="error"></span>
|
|
343
|
+
* </ix-radio-group>
|
|
344
|
+
*/
|
|
345
|
+
var radioGroupCounter = 0;
|
|
346
|
+
var radioInputCounter = 0;
|
|
347
|
+
var IxRadioGroup = class extends HTMLElement {
|
|
348
|
+
constructor(..._args) {
|
|
349
|
+
super(..._args);
|
|
350
|
+
_defineProperty(this, "_errorObserver", null);
|
|
351
|
+
_defineProperty(this, "_childObserver", null);
|
|
352
|
+
_defineProperty(this, "_keydownListener", null);
|
|
353
|
+
_defineProperty(this, "_instanceId", 0);
|
|
354
|
+
_defineProperty(this, "_ownDisabled", /* @__PURE__ */ new WeakMap());
|
|
355
|
+
}
|
|
356
|
+
static get observedAttributes() {
|
|
357
|
+
return [
|
|
358
|
+
"disabled",
|
|
359
|
+
"readonly",
|
|
360
|
+
"required",
|
|
361
|
+
"name"
|
|
362
|
+
];
|
|
363
|
+
}
|
|
364
|
+
connectedCallback() {
|
|
365
|
+
this._wire();
|
|
366
|
+
}
|
|
367
|
+
disconnectedCallback() {
|
|
368
|
+
this._errorObserver?.disconnect();
|
|
369
|
+
this._errorObserver = null;
|
|
370
|
+
this._childObserver?.disconnect();
|
|
371
|
+
this._childObserver = null;
|
|
372
|
+
if (this._keydownListener) this.removeEventListener("keydown", this._keydownListener);
|
|
373
|
+
this._keydownListener = null;
|
|
374
|
+
}
|
|
375
|
+
attributeChangedCallback(attr, _oldValue, _newValue) {
|
|
376
|
+
if (!this.isConnected) return;
|
|
377
|
+
if (attr === "disabled" || attr === "readonly") this._syncHostStateToInputs();
|
|
378
|
+
if (attr === "required") this._syncRequired();
|
|
379
|
+
if (attr === "name") this._wireInputs();
|
|
380
|
+
}
|
|
381
|
+
_wire() {
|
|
382
|
+
this.setAttribute("role", "radiogroup");
|
|
383
|
+
this._instanceId = ++radioGroupCounter;
|
|
384
|
+
const legend = this.querySelector("[data-field=\"legend\"]");
|
|
385
|
+
if (legend) {
|
|
386
|
+
if (!legend.id) legend.id = `ix-radio-group-legend-${this._instanceId}`;
|
|
387
|
+
this.setAttribute("aria-labelledby", legend.id);
|
|
388
|
+
}
|
|
389
|
+
const description = this.querySelector("[data-field=\"description\"]");
|
|
390
|
+
const error = this.querySelector("[data-field=\"error\"]");
|
|
391
|
+
const describedBy = [];
|
|
392
|
+
if (description) {
|
|
393
|
+
if (!description.id) description.id = `ix-radio-group-desc-${this._instanceId}`;
|
|
394
|
+
describedBy.push(description.id);
|
|
395
|
+
}
|
|
396
|
+
if (error) {
|
|
397
|
+
if (!error.id) error.id = `ix-radio-group-error-${this._instanceId}`;
|
|
398
|
+
describedBy.push(error.id);
|
|
399
|
+
error.setAttribute("aria-live", "polite");
|
|
400
|
+
this._errorObserver = new MutationObserver(() => {
|
|
401
|
+
this._syncInvalid(error);
|
|
402
|
+
});
|
|
403
|
+
this._errorObserver.observe(error, {
|
|
404
|
+
childList: true,
|
|
405
|
+
characterData: true,
|
|
406
|
+
subtree: true
|
|
407
|
+
});
|
|
408
|
+
this._syncInvalid(error);
|
|
409
|
+
}
|
|
410
|
+
if (describedBy.length > 0) this.setAttribute("aria-describedby", describedBy.join(" "));
|
|
411
|
+
this._wireInputs();
|
|
412
|
+
this._syncRequired();
|
|
413
|
+
this._keydownListener = (e) => {
|
|
414
|
+
if (this.hasAttribute("readonly") && [
|
|
415
|
+
"ArrowUp",
|
|
416
|
+
"ArrowDown",
|
|
417
|
+
"ArrowLeft",
|
|
418
|
+
"ArrowRight",
|
|
419
|
+
" "
|
|
420
|
+
].includes(e.key)) e.preventDefault();
|
|
421
|
+
};
|
|
422
|
+
this.addEventListener("keydown", this._keydownListener);
|
|
423
|
+
this._childObserver = new MutationObserver((mutations) => {
|
|
424
|
+
const hasInputChange = mutations.some((m) => Array.from(m.addedNodes).some((n) => this._containsRadioInput(n)) || Array.from(m.removedNodes).some((n) => this._containsRadioInput(n)));
|
|
425
|
+
const hasNameRemoval = mutations.some((m) => m.type === "attributes" && m.attributeName === "name" && m.target instanceof HTMLInputElement && m.target.type === "radio" && !m.target.name);
|
|
426
|
+
if (hasInputChange || hasNameRemoval) {
|
|
427
|
+
this._wireInputs();
|
|
428
|
+
this._syncRequired();
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
this._childObserver.observe(this, {
|
|
432
|
+
childList: true,
|
|
433
|
+
subtree: true,
|
|
434
|
+
attributes: true,
|
|
435
|
+
attributeFilter: ["name"]
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
_containsRadioInput(node) {
|
|
439
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return false;
|
|
440
|
+
const el = node;
|
|
441
|
+
if (el.matches?.("input[type=\"radio\"]")) return true;
|
|
442
|
+
return !!el.querySelector?.("input[type=\"radio\"]");
|
|
443
|
+
}
|
|
444
|
+
_wireInputs() {
|
|
445
|
+
const inputs = Array.from(this.querySelectorAll("input[type=\"radio\"]"));
|
|
446
|
+
if (inputs.length === 0) return;
|
|
447
|
+
for (const input of inputs) {
|
|
448
|
+
if (!input.id) input.id = `ix-radio-${++radioInputCounter}`;
|
|
449
|
+
const wrapper = input.closest(".ix-radio-button") ?? input.parentElement;
|
|
450
|
+
if (wrapper) {
|
|
451
|
+
const label = wrapper.querySelector("label");
|
|
452
|
+
if (label && !label.htmlFor) label.htmlFor = input.id;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
const hostName = this.getAttribute("name");
|
|
456
|
+
const existingName = inputs.find((i) => i.name)?.name;
|
|
457
|
+
const groupName = hostName || existingName || `ix-radio-group-name-${this._instanceId}`;
|
|
458
|
+
for (const input of inputs) if (hostName || !input.name) input.name = groupName;
|
|
459
|
+
this._syncHostStateToInputs();
|
|
460
|
+
}
|
|
461
|
+
_syncInvalid(error) {
|
|
462
|
+
if (!!error.textContent?.trim()) this.setAttribute("aria-invalid", "true");
|
|
463
|
+
else this.removeAttribute("aria-invalid");
|
|
464
|
+
}
|
|
465
|
+
_syncHostStateToInputs() {
|
|
466
|
+
const inputs = Array.from(this.querySelectorAll("input[type=\"radio\"]"));
|
|
467
|
+
const isDisabled = this.hasAttribute("disabled");
|
|
468
|
+
for (const input of inputs) if (isDisabled) {
|
|
469
|
+
if (!this._ownDisabled.has(input)) this._ownDisabled.set(input, input.disabled);
|
|
470
|
+
input.disabled = true;
|
|
471
|
+
} else if (this._ownDisabled.has(input)) {
|
|
472
|
+
input.disabled = this._ownDisabled.get(input);
|
|
473
|
+
this._ownDisabled.delete(input);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
_syncRequired() {
|
|
477
|
+
const inputs = Array.from(this.querySelectorAll("input[type=\"radio\"]"));
|
|
478
|
+
if (this.hasAttribute("required")) {
|
|
479
|
+
this.setAttribute("aria-required", "true");
|
|
480
|
+
if (inputs[0]) inputs[0].required = true;
|
|
481
|
+
for (const input of inputs.slice(1)) input.required = false;
|
|
482
|
+
} else {
|
|
483
|
+
this.removeAttribute("aria-required");
|
|
484
|
+
for (const input of inputs) input.required = false;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
//#endregion
|
|
284
489
|
//#region lib/tooltip/tooltip.ts
|
|
285
490
|
var TOOLTIP_ID = "ix-tooltip-singleton";
|
|
286
491
|
var ATTR = "data-tooltip";
|
|
@@ -402,5 +607,6 @@ document.addEventListener("keydown", handleKeydown);
|
|
|
402
607
|
//#region index.ts
|
|
403
608
|
customElements.define("ix-field", IxField);
|
|
404
609
|
customElements.define("ix-icon", IxIcon);
|
|
610
|
+
customElements.define("ix-radio-group", IxRadioGroup);
|
|
405
611
|
//#endregion
|
|
406
|
-
export { ICON_NAMES, IxField, IxIcon };
|
|
612
|
+
export { ICON_NAMES, IxField, IxIcon, IxRadioGroup };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IxRadioGroup — ARIA-lim for radioknapp-grupper.
|
|
3
|
+
*
|
|
4
|
+
* ## Designfilosofi
|
|
5
|
+
*
|
|
6
|
+
* Komponenten erstatter ingen native HTML-elementer. Forfatteren skriver
|
|
7
|
+
* native <input type="radio">, <label> og hjelpetekster inni <ix-radio-group>.
|
|
8
|
+
* Komponenten sin eneste jobb er å sette opp ARIA-koblingene mellom dem.
|
|
9
|
+
*
|
|
10
|
+
* ## Hva komponenten gjør
|
|
11
|
+
*
|
|
12
|
+
* 1. Setter role="radiogroup" på host slik at skjermlesere annonserer gruppen
|
|
13
|
+
* med riktig rolle og leser legend-teksten som gruppens tilgjengelige navn.
|
|
14
|
+
*
|
|
15
|
+
* 2. Kobler legend via aria-labelledby — genererer ID på [data-field="legend"]
|
|
16
|
+
* om nødvendig og peker host til den.
|
|
17
|
+
*
|
|
18
|
+
* 3. Setter aria-describedby på host med IDer til description og error
|
|
19
|
+
* (description alltid først). Hopper over elementer som mangler.
|
|
20
|
+
*
|
|
21
|
+
* 4. Genererer stabile IDer på hvert input og setter htmlFor på matchende label
|
|
22
|
+
* om label ikke allerede har det. Respekterer alltid eksisterende IDer.
|
|
23
|
+
*
|
|
24
|
+
* 5. Synkroniserer name-attributt på alle inputs — name er mekanismen
|
|
25
|
+
* nettleseren bruker for mutual exclusivity (ett valg av gangen) og
|
|
26
|
+
* ArrowKey-navigasjon mellom alternativene. Alle inputs i gruppen må ha
|
|
27
|
+
* samme name. Prioritet: name på host > name på input > auto-generert.
|
|
28
|
+
*
|
|
29
|
+
* 6. Setter aria-live="polite" på [data-field="error"] og observerer
|
|
30
|
+
* textContent-endringer: ikke-tomt innhold → aria-invalid="true" på host;
|
|
31
|
+
* tomt → aria-invalid fjernes. aria-invalid settes kun på host (ikke per
|
|
32
|
+
* input) — det er gruppens validitet som telles, og en duplisert flagg
|
|
33
|
+
* per input gir ingen merverdi for skjermlesere.
|
|
34
|
+
*
|
|
35
|
+
* 7. Propagerer disabled/readonly til alle inputs via attributeChangedCallback.
|
|
36
|
+
* Per-knapp disabled bevares — settes med en WeakMap ved første _wire().
|
|
37
|
+
*
|
|
38
|
+
* 8. Lytter på childList på host slik at nye <input type="radio">-elementer
|
|
39
|
+
* som legges til etter mount automatisk wires (id, name, htmlFor, disabled).
|
|
40
|
+
*
|
|
41
|
+
* ## Ingen Shadow DOM
|
|
42
|
+
*
|
|
43
|
+
* Komponenten bruker light DOM slik at CSS fra @sb1/indeks-css når alle
|
|
44
|
+
* delelementer direkte.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* <ix-radio-group>
|
|
48
|
+
* <span data-field="legend">Velg kundetype</span>
|
|
49
|
+
* <span data-field="description">Beskrivelse</span>
|
|
50
|
+
* <div class="ix-radio-button">
|
|
51
|
+
* <input type="radio" value="privat" />
|
|
52
|
+
* <label>Privat</label>
|
|
53
|
+
* </div>
|
|
54
|
+
* <div class="ix-radio-button">
|
|
55
|
+
* <input type="radio" value="bedrift" />
|
|
56
|
+
* <label>Bedrift</label>
|
|
57
|
+
* </div>
|
|
58
|
+
* <span data-field="error"></span>
|
|
59
|
+
* </ix-radio-group>
|
|
60
|
+
*/
|
|
61
|
+
export declare class IxRadioGroup extends HTMLElement {
|
|
62
|
+
private _errorObserver;
|
|
63
|
+
private _childObserver;
|
|
64
|
+
private _keydownListener;
|
|
65
|
+
private _instanceId;
|
|
66
|
+
private _ownDisabled;
|
|
67
|
+
static get observedAttributes(): string[];
|
|
68
|
+
connectedCallback(): void;
|
|
69
|
+
disconnectedCallback(): void;
|
|
70
|
+
attributeChangedCallback(attr: string, _oldValue: string | null, _newValue: string | null): void;
|
|
71
|
+
private _wire;
|
|
72
|
+
private _containsRadioInput;
|
|
73
|
+
private _wireInputs;
|
|
74
|
+
private _syncInvalid;
|
|
75
|
+
private _syncHostStateToInputs;
|
|
76
|
+
private _syncRequired;
|
|
77
|
+
}
|