@noctuatech/uswds 0.0.21 → 0.0.23

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.
Files changed (161) hide show
  1. package/assets/uswds.min.js +1 -1
  2. package/package.json +20 -50
  3. package/src/lib/accordion/accordion.test.ts +16 -16
  4. package/src/lib/alert/alert-types.ts +1 -1
  5. package/src/lib/alert/alert.element.ts +5 -1
  6. package/src/lib/alert/alert.test.ts +2 -2
  7. package/src/lib/button/button.element.ts +1 -8
  8. package/src/lib/button/button.stories.ts +3 -4
  9. package/src/lib/button/button.test.ts +3 -3
  10. package/src/lib/card/card.test.ts +2 -2
  11. package/src/lib/checkbox/checkbox.element.ts +27 -16
  12. package/src/lib/checkbox/checkbox.stories.ts +1 -1
  13. package/src/lib/checkbox/checkbox.test.ts +21 -4
  14. package/src/lib/config/config.element.ts +7 -25
  15. package/src/lib/config/config.test.ts +2 -2
  16. package/src/lib/description/description.test.ts +2 -2
  17. package/src/lib/file-input/file-input-preview/file-input-preview.element.ts +18 -7
  18. package/src/lib/file-input/file-input-preview/file-input-preview.test.ts +4 -4
  19. package/src/lib/file-input/file-input.element.ts +3 -3
  20. package/src/lib/file-input/file-input.test.ts +2 -2
  21. package/src/lib/icon/icon.element.ts +5 -7
  22. package/src/lib/icon/icon.stories.ts +3 -3
  23. package/src/lib/input/input.element.ts +76 -20
  24. package/src/lib/input/input.stories.ts +3 -1
  25. package/src/lib/input/input.test.ts +20 -5
  26. package/src/lib/input-mask/format.ts +1 -1
  27. package/src/lib/input-mask/input-mask.element.ts +11 -14
  28. package/src/lib/input-mask/input-mask.stories.ts +0 -1
  29. package/src/lib/input-mask/input-mask.test.ts +17 -13
  30. package/src/lib/input-mask/maskable.element.ts +1 -1
  31. package/src/lib/modal/modal-close/modal-close.test.ts +2 -2
  32. package/src/lib/modal/modal-heading/modal-heading.test.ts +2 -2
  33. package/src/lib/modal/modal.test.ts +2 -2
  34. package/src/lib/radio/context.ts +9 -0
  35. package/src/lib/radio/radio-option/radio-option.element.ts +45 -26
  36. package/src/lib/radio/radio-option/radio-option.test.ts +1 -1
  37. package/src/lib/radio/radio.element.ts +42 -31
  38. package/src/lib/radio/radio.stories.ts +2 -2
  39. package/src/lib/radio/radio.test.ts +16 -16
  40. package/src/lib/select/context.ts +10 -0
  41. package/src/lib/select/select-option/select-option.element.ts +26 -4
  42. package/src/lib/select/select.element.ts +37 -18
  43. package/src/lib/select/select.stories.ts +1 -1
  44. package/src/lib/select/select.test.ts +32 -13
  45. package/src/lib/services/icon.service.test.ts +30 -22
  46. package/src/lib/services/icon.service.ts +11 -3
  47. package/src/lib/side-nav/side-nav.element.ts +1 -1
  48. package/src/lib/side-nav/side-nav.test.ts +6 -6
  49. package/src/lib/step-indicator/step-indicator.test.ts +2 -2
  50. package/src/lib/summary-box/summary-box.element.ts +0 -7
  51. package/src/lib/summary-box/summary-box.stories.ts +1 -1
  52. package/src/lib/summary-box/summary-box.test.ts +2 -2
  53. package/src/lib/tag/tag.element.ts +0 -7
  54. package/src/lib/tag/tag.test.ts +2 -2
  55. package/target/lib/accordion/accordion.test.js +5 -5
  56. package/target/lib/accordion/accordion.test.js.map +1 -1
  57. package/target/lib/alert/alert-types.d.ts +1 -1
  58. package/target/lib/alert/alert.element.d.ts +1 -1
  59. package/target/lib/alert/alert.element.js +4 -0
  60. package/target/lib/alert/alert.element.js.map +1 -1
  61. package/target/lib/alert/alert.test.js +2 -2
  62. package/target/lib/alert/alert.test.js.map +1 -1
  63. package/target/lib/button/button.element.js +1 -8
  64. package/target/lib/button/button.element.js.map +1 -1
  65. package/target/lib/button/button.stories.js +1 -4
  66. package/target/lib/button/button.stories.js.map +1 -1
  67. package/target/lib/button/button.test.js +2 -2
  68. package/target/lib/button/button.test.js.map +1 -1
  69. package/target/lib/card/card.test.js +1 -1
  70. package/target/lib/card/card.test.js.map +1 -1
  71. package/target/lib/checkbox/checkbox.element.d.ts +1 -0
  72. package/target/lib/checkbox/checkbox.element.js +24 -16
  73. package/target/lib/checkbox/checkbox.element.js.map +1 -1
  74. package/target/lib/checkbox/checkbox.test.js +17 -3
  75. package/target/lib/checkbox/checkbox.test.js.map +1 -1
  76. package/target/lib/config/config.element.d.ts +0 -2
  77. package/target/lib/config/config.element.js +9 -22
  78. package/target/lib/config/config.element.js.map +1 -1
  79. package/target/lib/config/config.test.js +1 -1
  80. package/target/lib/config/config.test.js.map +1 -1
  81. package/target/lib/description/description.test.js +1 -1
  82. package/target/lib/description/description.test.js.map +1 -1
  83. package/target/lib/file-input/file-input-preview/file-input-preview.element.d.ts +1 -0
  84. package/target/lib/file-input/file-input-preview/file-input-preview.element.js +14 -6
  85. package/target/lib/file-input/file-input-preview/file-input-preview.element.js.map +1 -1
  86. package/target/lib/file-input/file-input-preview/file-input-preview.test.js +3 -3
  87. package/target/lib/file-input/file-input-preview/file-input-preview.test.js.map +1 -1
  88. package/target/lib/file-input/file-input.element.js +3 -3
  89. package/target/lib/file-input/file-input.element.js.map +1 -1
  90. package/target/lib/file-input/file-input.test.js +1 -1
  91. package/target/lib/file-input/file-input.test.js.map +1 -1
  92. package/target/lib/icon/icon.element.d.ts +1 -1
  93. package/target/lib/icon/icon.element.js +4 -5
  94. package/target/lib/icon/icon.element.js.map +1 -1
  95. package/target/lib/icon/icon.stories.js.map +1 -1
  96. package/target/lib/input/input.element.d.ts +8 -4
  97. package/target/lib/input/input.element.js +92 -17
  98. package/target/lib/input/input.element.js.map +1 -1
  99. package/target/lib/input/input.stories.js +3 -1
  100. package/target/lib/input/input.stories.js.map +1 -1
  101. package/target/lib/input/input.test.js +16 -4
  102. package/target/lib/input/input.test.js.map +1 -1
  103. package/target/lib/input-mask/format.js +1 -1
  104. package/target/lib/input-mask/input-mask.element.d.ts +1 -1
  105. package/target/lib/input-mask/input-mask.element.js +10 -11
  106. package/target/lib/input-mask/input-mask.element.js.map +1 -1
  107. package/target/lib/input-mask/input-mask.stories.js +0 -1
  108. package/target/lib/input-mask/input-mask.stories.js.map +1 -1
  109. package/target/lib/input-mask/input-mask.test.js +12 -8
  110. package/target/lib/input-mask/input-mask.test.js.map +1 -1
  111. package/target/lib/input-mask/maskable.element.d.ts +1 -1
  112. package/target/lib/modal/modal-close/modal-close.test.js +1 -1
  113. package/target/lib/modal/modal-close/modal-close.test.js.map +1 -1
  114. package/target/lib/modal/modal-heading/modal-heading.test.js +1 -1
  115. package/target/lib/modal/modal-heading/modal-heading.test.js.map +1 -1
  116. package/target/lib/modal/modal.test.js +1 -1
  117. package/target/lib/modal/modal.test.js.map +1 -1
  118. package/target/lib/radio/context.d.ts +7 -0
  119. package/target/lib/radio/context.js +3 -0
  120. package/target/lib/radio/context.js.map +1 -0
  121. package/target/lib/radio/radio-option/radio-option.element.d.ts +1 -5
  122. package/target/lib/radio/radio-option/radio-option.element.js +45 -35
  123. package/target/lib/radio/radio-option/radio-option.element.js.map +1 -1
  124. package/target/lib/radio/radio.element.d.ts +5 -5
  125. package/target/lib/radio/radio.element.js +39 -29
  126. package/target/lib/radio/radio.element.js.map +1 -1
  127. package/target/lib/radio/radio.stories.js.map +1 -1
  128. package/target/lib/radio/radio.test.js +9 -13
  129. package/target/lib/radio/radio.test.js.map +1 -1
  130. package/target/lib/select/context.d.ts +6 -0
  131. package/target/lib/select/context.js +3 -0
  132. package/target/lib/select/context.js.map +1 -0
  133. package/target/lib/select/select-option/select-option.element.d.ts +2 -1
  134. package/target/lib/select/select-option/select-option.element.js +20 -3
  135. package/target/lib/select/select-option/select-option.element.js.map +1 -1
  136. package/target/lib/select/select.element.d.ts +5 -2
  137. package/target/lib/select/select.element.js +33 -18
  138. package/target/lib/select/select.element.js.map +1 -1
  139. package/target/lib/select/select.stories.js +1 -1
  140. package/target/lib/select/select.test.js +24 -8
  141. package/target/lib/select/select.test.js.map +1 -1
  142. package/target/lib/services/icon.service.js +7 -1
  143. package/target/lib/services/icon.service.js.map +1 -1
  144. package/target/lib/services/icon.service.test.js +30 -22
  145. package/target/lib/services/icon.service.test.js.map +1 -1
  146. package/target/lib/side-nav/side-nav.element.js +1 -1
  147. package/target/lib/side-nav/side-nav.element.js.map +1 -1
  148. package/target/lib/side-nav/side-nav.test.js +1 -1
  149. package/target/lib/side-nav/side-nav.test.js.map +1 -1
  150. package/target/lib/step-indicator/step-indicator.test.js +1 -1
  151. package/target/lib/step-indicator/step-indicator.test.js.map +1 -1
  152. package/target/lib/summary-box/summary-box.element.js +0 -7
  153. package/target/lib/summary-box/summary-box.element.js.map +1 -1
  154. package/target/lib/summary-box/summary-box.stories.js +1 -1
  155. package/target/lib/summary-box/summary-box.stories.js.map +1 -1
  156. package/target/lib/summary-box/summary-box.test.js +1 -1
  157. package/target/lib/summary-box/summary-box.test.js.map +1 -1
  158. package/target/lib/tag/tag.element.js +0 -7
  159. package/target/lib/tag/tag.element.js.map +1 -1
  160. package/target/lib/tag/tag.test.js +1 -1
  161. package/target/lib/tag/tag.test.js.map +1 -1
@@ -1,7 +1,7 @@
1
1
  import { attr, css, element, html, listen } from "@joist/element";
2
2
 
3
- import { MaskableElement } from "./maskable.element.js";
4
- import { format, PATTERN_CHARS, PatternChar, REG_EXPS } from "./format.js";
3
+ import { PATTERN_CHARS, PatternChar, REG_EXPS, format } from "./format.js";
4
+ import type { MaskableElement } from "./maskable.element.js";
5
5
 
6
6
  declare global {
7
7
  interface HTMLElementTagNameMap {
@@ -25,15 +25,17 @@ export class USAInputMaskElement extends HTMLElement {
25
25
  accessor mask = "";
26
26
 
27
27
  connectedCallback() {
28
- for (let input of this.querySelectorAll<MaskableElement>("[mask]")) {
28
+ for (const input of this.querySelectorAll<MaskableElement>("[mask]")) {
29
29
  const { formatted } = format(input.value, this.#getMaskFor(input));
30
30
 
31
- input.value = formatted;
31
+ if (formatted) {
32
+ input.value = formatted;
33
+ }
32
34
  }
33
35
  }
34
36
 
35
37
  @listen("input")
36
- async onInput(e: Event) {
38
+ onInput(e: Event) {
37
39
  const input = e.target as MaskableElement;
38
40
  const selectionStart = input.selectionStart || 0;
39
41
  const prev = input.value;
@@ -46,18 +48,13 @@ export class USAInputMaskElement extends HTMLElement {
46
48
  const offset = input.value.length - prev.length;
47
49
  const maskChar = mask[selectionStart - 1] as PatternChar | undefined;
48
50
 
49
- // This is a hack to make sure that changes are propagated appropriately
50
- await Promise.resolve();
51
-
52
51
  // check if the current value is not a space for characters and has an offset greater then 0
53
52
  if (maskChar && !PATTERN_CHARS.includes(maskChar) && offset > 0) {
54
- input.setSelectionRange(selectionStart + offset, selectionStart + offset);
53
+ input.selectionStart = selectionStart + offset;
54
+ input.selectionEnd = selectionStart + offset;
55
55
  } else {
56
- input.setSelectionRange(selectionStart, selectionStart);
57
- }
58
-
59
- if (prev !== input.value) {
60
- input.dispatchEvent(new Event("input", { bubbles: true }));
56
+ input.selectionStart = selectionStart;
57
+ input.selectionEnd = selectionStart;
61
58
  }
62
59
  }
63
60
 
@@ -15,7 +15,6 @@ const meta = {
15
15
  placeholder=${args.mask}
16
16
  autocomplete="off"
17
17
  mask=${args.mask}
18
- value="3042616138"
19
18
  >
20
19
  Phone:
21
20
  </usa-input>
@@ -4,7 +4,7 @@ import "../input/input.element.js";
4
4
  import { assert, fixture, html } from "@open-wc/testing";
5
5
 
6
6
  import { format } from "./format.js";
7
- import { USAInputMaskElement } from "./input-mask.element.js";
7
+ import type { USAInputMaskElement } from "./input-mask.element.js";
8
8
 
9
9
  describe("format", () => {
10
10
  it("should retrn the correct raw value", () => {
@@ -51,9 +51,9 @@ describe("usa-input-mask", () => {
51
51
  </usa-input-mask>
52
52
  `);
53
53
 
54
- const input = el.querySelector("input")!;
54
+ const input = el.querySelector("input");
55
55
 
56
- assert.equal(input.value, "(123) 456-7890");
56
+ assert.equal(input?.value, "(123) 456-7890");
57
57
  });
58
58
 
59
59
  it("should update value when on input event", async () => {
@@ -63,12 +63,14 @@ describe("usa-input-mask", () => {
63
63
  </usa-input-mask>
64
64
  `);
65
65
 
66
- const input = el.querySelector("input")!;
66
+ const input = el.querySelector("input");
67
67
 
68
- input.value = "8888888888";
69
- input.dispatchEvent(new Event("input", { bubbles: true }));
68
+ if (input) {
69
+ input.value = "8888888888";
70
+ input.dispatchEvent(new Event("input", { bubbles: true }));
71
+ }
70
72
 
71
- assert.equal(input.value, "(888) 888-8888");
73
+ assert.equal(input?.value, "(888) 888-8888");
72
74
  });
73
75
  });
74
76
 
@@ -80,9 +82,9 @@ describe("usa-input-mask with usa-input", () => {
80
82
  </usa-input-mask>
81
83
  `);
82
84
 
83
- const input = el.querySelector("usa-input")!;
85
+ const input = el.querySelector("usa-input");
84
86
 
85
- assert.equal(input.value, "(123) 456-7890");
87
+ assert.equal(input?.value, "(123) 456-7890");
86
88
  });
87
89
 
88
90
  it("should update value when on input event", async () => {
@@ -96,11 +98,13 @@ describe("usa-input-mask with usa-input", () => {
96
98
  </usa-input-mask>
97
99
  `);
98
100
 
99
- const input = el.querySelector("usa-input")!;
101
+ const input = el.querySelector("usa-input");
100
102
 
101
- input.value = "8888888888";
102
- input.dispatchEvent(new Event("input", { bubbles: true }));
103
+ if (input) {
104
+ input.value = "8888888888";
105
+ input.dispatchEvent(new Event("input", { bubbles: true }));
106
+ }
103
107
 
104
- assert.equal(input.value, "(888) 888-8888");
108
+ assert.equal(input?.value, "(888) 888-8888");
105
109
  });
106
110
  });
@@ -1,5 +1,5 @@
1
1
  export interface MaskableElement extends HTMLElement {
2
2
  value: string;
3
3
  selectionStart: number | null;
4
- setSelectionRange(start: number, end: number): void;
4
+ selectionEnd: number | null;
5
5
  }
@@ -1,8 +1,8 @@
1
1
  import "./modal-close.element.js";
2
2
 
3
- import { fixture, html, assert } from "@open-wc/testing";
3
+ import { assert, fixture, html } from "@open-wc/testing";
4
4
 
5
- import { USAModalCloseElement } from "./modal-close.element.js";
5
+ import type { USAModalCloseElement } from "./modal-close.element.js";
6
6
 
7
7
  describe("usa-modal-close", () => {
8
8
  it("should be accessible", async () => {
@@ -1,8 +1,8 @@
1
1
  import "./modal-heading.element.js";
2
2
 
3
- import { fixture, html, assert } from "@open-wc/testing";
3
+ import { assert, fixture, html } from "@open-wc/testing";
4
4
 
5
- import { USAModalHeadingElement } from "./modal-heading.element.js";
5
+ import type { USAModalHeadingElement } from "./modal-heading.element.js";
6
6
 
7
7
  describe("usa-modal-heading", () => {
8
8
  it("should be accessible", async () => {
@@ -2,9 +2,9 @@ import "./modal.element.js";
2
2
  import "./modal-heading/modal-heading.element.js";
3
3
  import "./modal-close/modal-close.element.js";
4
4
 
5
- import { fixture, html, assert } from "@open-wc/testing";
5
+ import { assert, fixture, html } from "@open-wc/testing";
6
6
 
7
- import { USAModalElement } from "./modal.element.js";
7
+ import type { USAModalElement } from "./modal.element.js";
8
8
 
9
9
  describe("usa-modal", () => {
10
10
  it("should be accessible", async () => {
@@ -0,0 +1,9 @@
1
+ import { StaticToken } from "@joist/di";
2
+
3
+ export interface RadioContainer extends Node {
4
+ name: string;
5
+ value: string;
6
+ addRadioOption(el: HTMLElement): void;
7
+ }
8
+
9
+ export const RADIO_CTX = new StaticToken<RadioContainer>("RADIO_CTX");
@@ -1,4 +1,7 @@
1
- import { attr, css, element, html } from "@joist/element";
1
+ import { attr, css, element, html, query } from "@joist/element";
2
+
3
+ import { inject, injectable, injected } from "@joist/di";
4
+ import { RADIO_CTX } from "../context.js";
2
5
 
3
6
  declare global {
4
7
  interface HTMLElementTagNameMap {
@@ -6,6 +9,9 @@ declare global {
6
9
  }
7
10
  }
8
11
 
12
+ @injectable({
13
+ name: "usa-radio-option-ctx",
14
+ })
9
15
  @element({
10
16
  tagName: "usa-radio-option",
11
17
  shadowDom: [
@@ -16,49 +22,62 @@ declare global {
16
22
  margin-top: 0.05rem;
17
23
  }
18
24
  `,
19
- html`<slot></slot>`,
25
+ html`
26
+ <!-- This label will be moved to the shadow dom of its parent -->
27
+ <label>
28
+ <input type="radio" tabindex="0" />
29
+ <slot name="reserved"></slot>
30
+ </label>
31
+
32
+ <slot></slot>
33
+ `,
20
34
  ],
21
35
  })
22
36
  export class USARadioOptionElement extends HTMLElement {
23
37
  @attr()
24
38
  accessor value = "";
25
39
 
26
- @attr()
27
- accessor name = "";
28
-
29
- @attr()
30
- accessor checked = false;
40
+ #label = query("label");
41
+ #input = query("input");
42
+ #slot = query("slot");
43
+ #radio = inject(RADIO_CTX);
31
44
 
32
- readonly radio = document.createElement("label");
45
+ #observer = new MutationObserver(() => {
46
+ const input = this.#input();
47
+ const radio = this.#radio();
33
48
 
34
- readonly #input = document.createElement("input");
35
- readonly #slotEl = document.createElement("slot");
49
+ input.name = radio.name;
50
+ input.checked = radio.value === this.value;
51
+ });
36
52
 
37
- constructor() {
38
- super();
53
+ attributeChangedCallback() {
54
+ const input = this.#input();
55
+ const slot = this.#slot();
39
56
 
40
- this.#input.type = "radio";
57
+ this.slot = this.value;
41
58
 
42
- this.radio.append(this.#input, this.#slotEl);
59
+ slot.name = this.value;
60
+ input.value = this.value;
43
61
  }
44
62
 
45
- attributeChangedCallback() {
46
- this.slot = this.value;
63
+ @injected()
64
+ onInjected() {
65
+ const input = this.#input();
66
+ const radio = this.#radio();
47
67
 
48
- this.#input.name = this.name;
49
- this.#input.value = this.value;
50
- this.#input.checked = this.checked;
68
+ radio.addRadioOption(this.#label());
51
69
 
52
- this.#slotEl.name = this.value;
53
- }
70
+ input.name = radio.name;
71
+ input.checked = radio.value === this.value;
54
72
 
55
- connectedCallback() {
56
- this.dispatchEvent(
57
- new Event("usa::radio::option::added", { bubbles: true })
58
- );
73
+ this.#observer.observe(radio, {
74
+ attributeFilter: ["value", "name"],
75
+ });
59
76
  }
60
77
 
61
78
  disconnectedCallback() {
62
- this.radio.remove();
79
+ this.#label().remove();
80
+
81
+ this.#observer.disconnect();
63
82
  }
64
83
  }
@@ -3,7 +3,7 @@ import "./radio-option.element.js";
3
3
 
4
4
  import { assert, fixture, html } from "@open-wc/testing";
5
5
 
6
- import { USARadioOptionElement } from "./radio-option.element.js";
6
+ import type { USARadioOptionElement } from "./radio-option.element.js";
7
7
 
8
8
  describe("usa-radio-option", () => {
9
9
  it("should map value to slot", async () => {
@@ -1,6 +1,7 @@
1
- import { attr, css, element, html, listen } from "@joist/element";
1
+ import { injectable } from "@joist/di";
2
+ import { attr, css, element, html, listen, query } from "@joist/element";
2
3
 
3
- import type { USARadioOptionElement } from "./radio-option/radio-option.element.js";
4
+ import { RADIO_CTX, type RadioContainer } from "./context.js";
4
5
 
5
6
  declare global {
6
7
  interface HTMLElementTagNameMap {
@@ -8,6 +9,10 @@ declare global {
8
9
  }
9
10
  }
10
11
 
12
+ @injectable({
13
+ name: "usa-radio-ctx",
14
+ provideSelfAs: [RADIO_CTX],
15
+ })
11
16
  @element({
12
17
  tagName: "usa-radio",
13
18
  shadowDom: [
@@ -24,12 +29,11 @@ declare global {
24
29
  display: flex;
25
30
  cursor: pointer;
26
31
  gap: 0.5rem;
32
+ position: relative;
27
33
  }
28
34
 
29
35
  input {
30
36
  position: absolute;
31
- left: -999em;
32
- right: auto;
33
37
  }
34
38
 
35
39
  label::before {
@@ -42,6 +46,8 @@ declare global {
42
46
  background: #fff;
43
47
  box-shadow: 0 0 0 2px #1b1b1b;
44
48
  flex: 0 0 1.25rem;
49
+ position: relative;
50
+ z-index: 1000;
45
51
  }
46
52
 
47
53
  label:has(input:checked)::before {
@@ -77,10 +83,14 @@ declare global {
77
83
  display: flex;
78
84
  }
79
85
  `,
80
- html`<slot id="main"></slot>`,
86
+ html`
87
+ <slot name="legend" id="legend" tabindex="-1"></slot>
88
+
89
+ <slot></slot>
90
+ `,
81
91
  ],
82
92
  })
83
- export class USARadioElement extends HTMLElement {
93
+ export class USARadioElement extends HTMLElement implements RadioContainer {
84
94
  static formAssociated = true;
85
95
 
86
96
  @attr()
@@ -89,49 +99,50 @@ export class USARadioElement extends HTMLElement {
89
99
  @attr()
90
100
  accessor name = "";
91
101
 
102
+ @attr()
103
+ accessor required = false;
104
+
92
105
  @attr({
93
106
  observed: false,
94
107
  })
95
108
  accessor tiled = false;
96
109
 
97
- get shadow() {
98
- return this.shadowRoot!;
110
+ #internals = this.attachInternals();
111
+ #legend = query("#legend");
112
+
113
+ connectedCallback() {
114
+ this.#syncFormState();
99
115
  }
100
116
 
101
- #internals = this.attachInternals();
117
+ addRadioOption(el: HTMLElement) {
118
+ this.shadowRoot?.append(el);
119
+
120
+ this.#syncFormState();
121
+ }
102
122
 
103
123
  @listen("change")
104
124
  onChange(e: Event) {
105
125
  if (e.target instanceof HTMLInputElement) {
106
126
  if (e.target.checked) {
107
127
  this.value = e.target.value;
108
- this.#internals.setFormValue(e.target.value);
109
- }
110
- }
111
- }
112
-
113
- connectedCallback() {
114
- if (this.value) {
115
- this.#internals.setFormValue(this.value);
116
- }
117
- }
118
128
 
119
- attributeChangedCallback() {
120
- for (let radio of this.shadow.querySelectorAll("usa-radio-option")) {
121
- radio.checked = radio.value === this.value;
122
- radio.name = this.name;
129
+ this.#syncFormState();
130
+ }
123
131
  }
124
132
  }
125
133
 
126
- @listen("usa::radio::option::added", (el) => el)
127
- onOptionAdded(e: Event) {
128
- e.stopPropagation();
134
+ #syncFormState() {
135
+ this.#internals.setFormValue(this.value);
136
+ this.#internals.setValidity({});
129
137
 
130
- const target = e.target as USARadioOptionElement;
138
+ if (this.required && !this.value) {
139
+ const input = this.shadowRoot?.querySelector("input");
131
140
 
132
- target.checked = target.value === this.value;
133
- target.name = this.name;
134
-
135
- this.shadow.append(target.radio);
141
+ this.#internals.setValidity(
142
+ { valueMissing: true },
143
+ "Please select an option if you want to proceed",
144
+ input ?? this.#legend(),
145
+ );
146
+ }
136
147
  }
137
148
  }
@@ -1,8 +1,8 @@
1
1
  import type { Meta, StoryObj } from "@storybook/web-components";
2
2
  import { html } from "lit";
3
3
 
4
- import type { USARadioElement } from "./radio.element.js";
5
4
  import { when } from "lit/directives/when.js";
5
+ import type { USARadioElement } from "./radio.element.js";
6
6
 
7
7
  // More on how to set up stories at: https://storybook.js.org/docs/writing-stories
8
8
  const meta = {
@@ -25,7 +25,7 @@ const meta = {
25
25
  Frederick Douglass
26
26
  ${when(
27
27
  args.description,
28
- () => html`<usa-description>${args.description}</usa-description>`
28
+ () => html`<usa-description>${args.description}</usa-description>`,
29
29
  )}
30
30
  </usa-radio-option>
31
31
 
@@ -1,9 +1,9 @@
1
1
  import "./radio.element.js";
2
2
  import "./radio-option/radio-option.element.js";
3
3
 
4
- import { fixture, html, assert } from "@open-wc/testing";
4
+ import { assert, fixture, html } from "@open-wc/testing";
5
5
 
6
- import { USARadioElement } from "./radio.element.js";
6
+ import type { USARadioElement } from "./radio.element.js";
7
7
 
8
8
  describe("usa-radio", () => {
9
9
  it("should be accessible", async () => {
@@ -53,9 +53,9 @@ describe("usa-radio", () => {
53
53
  </form>
54
54
  `);
55
55
 
56
- const nativeInputs = form
57
- .querySelector("usa-radio")!
58
- .shadowRoot!.querySelectorAll("input");
56
+ const nativeInputs =
57
+ form.querySelector("usa-radio")?.shadowRoot?.querySelectorAll("input") ??
58
+ [];
59
59
 
60
60
  assert.deepEqual(
61
61
  Array.from(nativeInputs).map((input) => input.value),
@@ -64,7 +64,7 @@ describe("usa-radio", () => {
64
64
  "frederick-douglass",
65
65
  "booker-t-washington",
66
66
  "george-washington-carver",
67
- ]
67
+ ],
68
68
  );
69
69
  });
70
70
 
@@ -95,13 +95,13 @@ describe("usa-radio", () => {
95
95
 
96
96
  options[2].remove();
97
97
 
98
- const nativeInputs = form
99
- .querySelector("usa-radio")!
100
- .shadowRoot!.querySelectorAll("input");
98
+ const nativeInputs =
99
+ form.querySelector("usa-radio")?.shadowRoot?.querySelectorAll("input") ??
100
+ [];
101
101
 
102
102
  assert.deepEqual(
103
103
  Array.from(nativeInputs).map((input) => input.value),
104
- ["sojourner-truth", "frederick-douglass", "george-washington-carver"]
104
+ ["sojourner-truth", "frederick-douglass", "george-washington-carver"],
105
105
  );
106
106
  });
107
107
 
@@ -130,9 +130,9 @@ describe("usa-radio", () => {
130
130
 
131
131
  const value = new FormData(form);
132
132
 
133
- const nativeInputs = form
134
- .querySelector("usa-radio")!
135
- .shadowRoot!.querySelectorAll("input");
133
+ const nativeInputs =
134
+ form.querySelector("usa-radio")?.shadowRoot?.querySelectorAll("input") ??
135
+ [];
136
136
 
137
137
  assert.equal(nativeInputs[1].checked, true);
138
138
  assert.equal(value.get("historical-figures"), "frederick-douglass");
@@ -161,9 +161,9 @@ describe("usa-radio", () => {
161
161
  </form>
162
162
  `);
163
163
 
164
- const nativeInputs = form
165
- .querySelector("usa-radio")!
166
- .shadowRoot!.querySelectorAll("input");
164
+ const nativeInputs =
165
+ form.querySelector("usa-radio")?.shadowRoot?.querySelectorAll("input") ??
166
+ [];
167
167
 
168
168
  nativeInputs[3].click();
169
169
 
@@ -0,0 +1,10 @@
1
+ import { StaticToken } from "@joist/di";
2
+
3
+ export interface SelectContainer extends Node {
4
+ value: string;
5
+ addSelectOption: (el: HTMLOptionElement) => void;
6
+ }
7
+
8
+ export const SELECT_CONTEXT = new StaticToken<SelectContainer>(
9
+ "SELECT_CONTEXT",
10
+ );
@@ -1,5 +1,8 @@
1
1
  import { attr, css, element, html, listen } from "@joist/element";
2
2
 
3
+ import { inject, injectable, injected } from "@joist/di";
4
+ import { SELECT_CONTEXT } from "../context.js";
5
+
3
6
  declare global {
4
7
  interface HTMLElementTagNameMap {
5
8
  "usa-select-option": USASelecOptionElement;
@@ -17,20 +20,37 @@ declare global {
17
20
  html`<slot></slot>`,
18
21
  ],
19
22
  })
23
+ @injectable()
20
24
  export class USASelecOptionElement extends HTMLElement {
21
25
  @attr()
22
26
  accessor value = "";
23
27
 
24
28
  readonly option = document.createElement("option");
25
29
 
30
+ #select = inject(SELECT_CONTEXT);
31
+
32
+ #observer = new MutationObserver(() => {
33
+ const { value } = this.#select();
34
+
35
+ this.option.selected = value === this.value;
36
+ });
37
+
26
38
  attributeChangedCallback() {
27
39
  this.option.value = this.value;
28
40
  }
29
41
 
30
- connectedCallback() {
31
- this.dispatchEvent(
32
- new Event("usa::select::option::added", { bubbles: true })
33
- );
42
+ @injected()
43
+ onInjected() {
44
+ const select = this.#select();
45
+
46
+ this.option.selected = select.value === this.value;
47
+
48
+ select.addSelectOption(this.option);
49
+
50
+ this.#observer.observe(select, {
51
+ attributes: true,
52
+ attributeFilter: ["value"],
53
+ });
34
54
  }
35
55
 
36
56
  @listen("slotchange")
@@ -39,6 +59,8 @@ export class USASelecOptionElement extends HTMLElement {
39
59
  }
40
60
 
41
61
  disconnectedCallback() {
62
+ this.#observer.disconnect();
63
+
42
64
  this.option.remove();
43
65
  }
44
66
  }