@noctuatech/uswds 0.0.22 → 0.0.24

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 (162) 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 +4 -8
  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 +69 -12
  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 +7 -9
  28. package/src/lib/input-mask/input-mask.test.ts +17 -13
  29. package/src/lib/modal/modal-close/modal-close.test.ts +2 -2
  30. package/src/lib/modal/modal-heading/modal-heading.test.ts +2 -2
  31. package/src/lib/modal/modal.test.ts +2 -2
  32. package/src/lib/radio/context.ts +9 -0
  33. package/src/lib/radio/radio-option/radio-option.element.ts +45 -26
  34. package/src/lib/radio/radio-option/radio-option.test.ts +1 -1
  35. package/src/lib/radio/radio.element.ts +42 -31
  36. package/src/lib/radio/radio.stories.ts +2 -2
  37. package/src/lib/radio/radio.test.ts +16 -16
  38. package/src/lib/select/context.ts +10 -0
  39. package/src/lib/select/select-option/select-option.element.ts +26 -4
  40. package/src/lib/select/select.element.ts +37 -18
  41. package/src/lib/select/select.stories.ts +1 -1
  42. package/src/lib/select/select.test.ts +32 -13
  43. package/src/lib/services/icon.service.ts +11 -3
  44. package/src/lib/side-nav/side-nav.element.ts +1 -1
  45. package/src/lib/side-nav/side-nav.test.ts +6 -6
  46. package/src/lib/step-indicator/step/step.element.ts +27 -29
  47. package/src/lib/step-indicator/step-indicator.element.ts +1 -1
  48. package/src/lib/step-indicator/step-indicator.stories.ts +9 -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 +4 -10
  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 +6 -2
  97. package/target/lib/input/input.element.js +73 -10
  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 +6 -7
  106. package/target/lib/input-mask/input-mask.element.js.map +1 -1
  107. package/target/lib/input-mask/input-mask.test.js +12 -8
  108. package/target/lib/input-mask/input-mask.test.js.map +1 -1
  109. package/target/lib/modal/modal-close/modal-close.test.js +1 -1
  110. package/target/lib/modal/modal-close/modal-close.test.js.map +1 -1
  111. package/target/lib/modal/modal-heading/modal-heading.test.js +1 -1
  112. package/target/lib/modal/modal-heading/modal-heading.test.js.map +1 -1
  113. package/target/lib/modal/modal.test.js +1 -1
  114. package/target/lib/modal/modal.test.js.map +1 -1
  115. package/target/lib/radio/context.d.ts +7 -0
  116. package/target/lib/radio/context.js +3 -0
  117. package/target/lib/radio/context.js.map +1 -0
  118. package/target/lib/radio/radio-option/radio-option.element.d.ts +1 -5
  119. package/target/lib/radio/radio-option/radio-option.element.js +45 -35
  120. package/target/lib/radio/radio-option/radio-option.element.js.map +1 -1
  121. package/target/lib/radio/radio.element.d.ts +5 -5
  122. package/target/lib/radio/radio.element.js +39 -29
  123. package/target/lib/radio/radio.element.js.map +1 -1
  124. package/target/lib/radio/radio.stories.js.map +1 -1
  125. package/target/lib/radio/radio.test.js +9 -13
  126. package/target/lib/radio/radio.test.js.map +1 -1
  127. package/target/lib/select/context.d.ts +6 -0
  128. package/target/lib/select/context.js +3 -0
  129. package/target/lib/select/context.js.map +1 -0
  130. package/target/lib/select/select-option/select-option.element.d.ts +2 -1
  131. package/target/lib/select/select-option/select-option.element.js +20 -3
  132. package/target/lib/select/select-option/select-option.element.js.map +1 -1
  133. package/target/lib/select/select.element.d.ts +5 -2
  134. package/target/lib/select/select.element.js +33 -18
  135. package/target/lib/select/select.element.js.map +1 -1
  136. package/target/lib/select/select.stories.js +1 -1
  137. package/target/lib/select/select.test.js +24 -8
  138. package/target/lib/select/select.test.js.map +1 -1
  139. package/target/lib/services/icon.service.js +7 -1
  140. package/target/lib/services/icon.service.js.map +1 -1
  141. package/target/lib/side-nav/side-nav.element.js +1 -1
  142. package/target/lib/side-nav/side-nav.element.js.map +1 -1
  143. package/target/lib/side-nav/side-nav.test.js +1 -1
  144. package/target/lib/side-nav/side-nav.test.js.map +1 -1
  145. package/target/lib/step-indicator/step/step.element.js +27 -29
  146. package/target/lib/step-indicator/step/step.element.js.map +1 -1
  147. package/target/lib/step-indicator/step-indicator.element.js +1 -1
  148. package/target/lib/step-indicator/step-indicator.stories.d.ts +1 -1
  149. package/target/lib/step-indicator/step-indicator.stories.js +9 -6
  150. package/target/lib/step-indicator/step-indicator.stories.js.map +1 -1
  151. package/target/lib/step-indicator/step-indicator.test.js +1 -1
  152. package/target/lib/step-indicator/step-indicator.test.js.map +1 -1
  153. package/target/lib/summary-box/summary-box.element.js +0 -7
  154. package/target/lib/summary-box/summary-box.element.js.map +1 -1
  155. package/target/lib/summary-box/summary-box.stories.js +1 -1
  156. package/target/lib/summary-box/summary-box.stories.js.map +1 -1
  157. package/target/lib/summary-box/summary-box.test.js +1 -1
  158. package/target/lib/summary-box/summary-box.test.js.map +1 -1
  159. package/target/lib/tag/tag.element.js +0 -7
  160. package/target/lib/tag/tag.element.js.map +1 -1
  161. package/target/lib/tag/tag.test.js +1 -1
  162. package/target/lib/tag/tag.test.js.map +1 -1
@@ -1,11 +1,11 @@
1
1
  import "./button.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 { BUTTON_VARIANTS, USAButtonElement } from "./button.element.js";
5
+ import { BUTTON_VARIANTS, type USAButtonElement } from "./button.element.js";
6
6
 
7
7
  describe("usa-button", () => {
8
- for (let variant of BUTTON_VARIANTS) {
8
+ for (const variant of BUTTON_VARIANTS) {
9
9
  it("should be accessible", async () => {
10
10
  const button = await fixture<USAButtonElement>(html`
11
11
  <usa-button variant=${variant}>Hello World</usa-button>
@@ -5,9 +5,9 @@ import "./card-group/card-group.element.js";
5
5
  import "./card-header/card-header.element.js";
6
6
  import "./card-media/card-media.element.js";
7
7
 
8
- import { fixture, html, assert } from "@open-wc/testing";
8
+ import { assert, fixture, html } from "@open-wc/testing";
9
9
 
10
- import { USACardElement } from "./card.element.js";
10
+ import type { USACardElement } from "./card.element.js";
11
11
 
12
12
  describe("usa-card", () => {
13
13
  it("should be accessible", async () => {
@@ -16,14 +16,8 @@ declare global {
16
16
 
17
17
  :host {
18
18
  display: inline-block;
19
- font-family:
20
- Source Sans Pro Web,
21
- Helvetica Neue,
22
- Helvetica,
23
- Roboto,
24
- Arial,
25
- sans-serif;
26
19
  max-width: 30rem;
20
+ position: relative;
27
21
  }
28
22
 
29
23
  :host([tiled]) label {
@@ -71,11 +65,7 @@ declare global {
71
65
  }
72
66
 
73
67
  input {
74
- height: 0;
75
- width: 0;
76
68
  position: absolute;
77
- left: -999em;
78
- right: auto;
79
69
  }
80
70
 
81
71
  input:focus + .checkbox {
@@ -107,7 +97,7 @@ declare global {
107
97
  `,
108
98
  html`
109
99
  <label>
110
- <input type="checkbox" />
100
+ <input type="checkbox" tabindex="0"/>
111
101
 
112
102
  <div class="checkbox"></div>
113
103
 
@@ -130,23 +120,25 @@ export class USACheckboxElement extends HTMLElement {
130
120
  @attr()
131
121
  accessor value = "";
132
122
 
123
+ @attr()
124
+ accessor required = false;
125
+
133
126
  @attr({
134
127
  observed: false,
135
128
  })
136
129
  accessor tiled = false;
137
130
 
138
131
  #checkbox = query("input");
132
+
139
133
  #internals = this.attachInternals();
140
134
 
141
135
  connectedCallback() {
142
136
  const checkbox = this.#checkbox();
143
137
 
144
- if (this.checked) {
145
- this.#internals.setFormValue(this.value);
146
- }
147
-
148
138
  checkbox.checked = this.checked;
149
139
  checkbox.name = this.name;
140
+
141
+ this.#syncFormState();
150
142
  }
151
143
 
152
144
  attributeChangedCallback() {
@@ -154,16 +146,35 @@ export class USACheckboxElement extends HTMLElement {
154
146
 
155
147
  checkbox.checked = this.checked;
156
148
  checkbox.name = this.name;
149
+
150
+ this.#syncFormState();
157
151
  }
158
152
 
159
153
  @listen("change", "input[type=checkbox]")
160
154
  onCheckboxChange() {
161
155
  const checkbox = this.#checkbox();
156
+ this.checked = checkbox.checked;
157
+
158
+ this.#syncFormState();
159
+ }
160
+
161
+ #syncFormState() {
162
+ const checkbox = this.#checkbox();
162
163
 
163
164
  if (checkbox.checked) {
164
165
  this.#internals.setFormValue(this.value);
165
166
  } else {
166
167
  this.#internals.setFormValue(null);
167
168
  }
169
+
170
+ if (this.required && !checkbox.checked) {
171
+ this.#internals.setValidity(
172
+ { valueMissing: true },
173
+ "Please check this box if you want to proceed",
174
+ this.#checkbox(),
175
+ );
176
+ } else {
177
+ this.#internals.setValidity({});
178
+ }
168
179
  }
169
180
  }
@@ -21,7 +21,7 @@ const meta = {
21
21
  Hello World
22
22
  ${when(
23
23
  args.description,
24
- () => html`<usa-description>${args.description}</usa-description>`
24
+ () => html`<usa-description>${args.description}</usa-description>`,
25
25
  )}
26
26
  </usa-checkbox>
27
27
  `;
@@ -1,6 +1,6 @@
1
1
  import "./checkbox.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
5
  describe("usa-checkbox", () => {
6
6
  it("should be accessible", async () => {
@@ -36,12 +36,29 @@ describe("usa-checkbox", () => {
36
36
  </form>
37
37
  `);
38
38
 
39
- const checkbox = form.querySelector("usa-checkbox")!;
40
- const nativeInput = checkbox.shadowRoot!.querySelector("input")!;
41
- nativeInput.click();
39
+ const checkbox = form.querySelector("usa-checkbox");
40
+ const nativeInput = checkbox?.shadowRoot?.querySelector("input");
41
+
42
+ if (nativeInput) {
43
+ nativeInput.click();
44
+ }
42
45
 
43
46
  const value = new FormData(form);
44
47
 
45
48
  assert.equal(value.get("enabled"), "test");
46
49
  });
50
+
51
+ it("should not submit when not valid", async () => {
52
+ const form = await fixture<HTMLFormElement>(html`
53
+ <form>
54
+ <usa-checkbox name="enabled" value="test" required>
55
+ Hello World
56
+ </usa-checkbox>
57
+
58
+ <button>Submit</button>
59
+ </form>
60
+ `);
61
+
62
+ assert.equal(form.checkValidity(), false);
63
+ });
47
64
  });
@@ -1,12 +1,13 @@
1
- import { created, injectable, Injector } from "@joist/di";
1
+ import { injectable } from "@joist/di";
2
2
  import { attr, css, element, html } from "@joist/element";
3
3
 
4
4
  export class USAConfig {
5
- iconPath: string = "";
5
+ iconPath = "";
6
6
  }
7
7
 
8
8
  @injectable({
9
- name: "USAConfigElement",
9
+ name: "usa-config-ctx",
10
+ provideSelfAs: [USAConfig],
10
11
  })
11
12
  @element({
12
13
  tagName: "usa-config",
@@ -24,9 +25,4 @@ export class USAConfigElement extends HTMLElement {
24
25
  name: "icon-path",
25
26
  })
26
27
  accessor iconPath = "/assets/usa-icons/";
27
-
28
- @created()
29
- onInjectorCreated({ providers }: Injector) {
30
- providers.set(USAConfig, { factory: () => this });
31
- }
32
28
  }
@@ -1,8 +1,8 @@
1
1
  import "./config.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 { USAConfigElement } from "./config.element.js";
5
+ import type { USAConfigElement } from "./config.element.js";
6
6
 
7
7
  describe("usa-config", () => {
8
8
  it("should be accessible", async () => {
@@ -1,8 +1,8 @@
1
1
  import "./description.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 { USADescriptionElement } from "./description.element.js";
5
+ import type { USADescriptionElement } from "./description.element.js";
6
6
 
7
7
  describe("usa-description", () => {
8
8
  it("should be accessible", async () => {
@@ -83,16 +83,24 @@ export class USAFileInputPreviewElement extends HTMLElement {
83
83
  this.onChange();
84
84
  }
85
85
 
86
+ get shadow() {
87
+ if (!this.shadowRoot) {
88
+ throw new Error("no shadow root");
89
+ }
90
+
91
+ return this.shadowRoot;
92
+ }
93
+
86
94
  @effect()
87
95
  onChange() {
88
96
  const template = this.#template();
89
97
 
90
- if (this.files && this.files.length) {
98
+ if (this.files?.length) {
91
99
  this.hidden = false;
92
100
 
93
- let names = new Set<string>();
101
+ const names = new Set<string>();
94
102
 
95
- for (let file of this.files) {
103
+ for (const file of this.files) {
96
104
  names.add(file.name);
97
105
 
98
106
  if (!this.#items.has(file.name)) {
@@ -102,16 +110,19 @@ export class USAFileInputPreviewElement extends HTMLElement {
102
110
  item.id = file.name;
103
111
  item.append(document.createTextNode(file.name));
104
112
 
105
- const img = item.querySelector("img")!;
106
- img.src = URL.createObjectURL(file);
113
+ const img = item.querySelector("img");
114
+
115
+ if (img) {
116
+ img.src = URL.createObjectURL(file);
117
+ }
107
118
 
108
- this.shadowRoot!.append(item);
119
+ this.shadow.append(item);
109
120
 
110
121
  this.#items.set(file.name, item);
111
122
  }
112
123
  }
113
124
 
114
- for (let [name, item] of this.#items) {
125
+ for (const [name, item] of this.#items) {
115
126
  if (!names.has(name)) {
116
127
  item.remove();
117
128
  this.#items.delete(name);
@@ -3,7 +3,7 @@ import "../../link/link.element.js";
3
3
 
4
4
  import { assert, fixture, html } from "@open-wc/testing";
5
5
 
6
- import { USAFileInputPreviewElement } from "./file-input-preview.element.js";
6
+ import type { USAFileInputPreviewElement } from "./file-input-preview.element.js";
7
7
 
8
8
  describe("usa-file-input-preview", () => {
9
9
  it("should be accessible with no files", async () => {
@@ -42,7 +42,7 @@ describe("usa-file-input-preview", () => {
42
42
  `);
43
43
 
44
44
  const previewItems = Array.from(
45
- shadowRoot!.querySelectorAll(".preview-item")
45
+ shadowRoot?.querySelectorAll(".preview-item") ?? [],
46
46
  ).map((item) => item.textContent?.trim());
47
47
 
48
48
  assert.deepEqual(previewItems, ["image1.jpg", "image2.jpg"]);
@@ -68,7 +68,7 @@ describe("usa-file-input-preview", () => {
68
68
  await Promise.resolve();
69
69
 
70
70
  const previewItems = Array.from(
71
- fileInputPreview.shadowRoot!.querySelectorAll(".preview-item")
71
+ fileInputPreview.shadowRoot?.querySelectorAll(".preview-item") ?? [],
72
72
  ).map((item) => item.textContent?.trim());
73
73
 
74
74
  assert.deepEqual(previewItems, ["image1.jpg"]);
@@ -86,7 +86,7 @@ describe("usa-file-input-preview", () => {
86
86
  `);
87
87
 
88
88
  const [first, second] = Array.from(
89
- shadowRoot!.querySelectorAll<HTMLImageElement>(".preview-item img")
89
+ shadowRoot?.querySelectorAll<HTMLImageElement>(".preview-item img") ?? [],
90
90
  ).map((item) => item.src.substring(29));
91
91
 
92
92
  // we are just testing that the two hashes are in fact different
@@ -74,7 +74,7 @@ declare global {
74
74
  <slot class="label"></slot>
75
75
 
76
76
  <div class="container">
77
- <input type="file" />
77
+ <input type="file" tabindex="0"/>
78
78
 
79
79
  <div class="box">
80
80
  <slot name="description">
@@ -137,10 +137,10 @@ export class USAFileInputElement extends HTMLElement {
137
137
 
138
138
  const formData = new FormData();
139
139
 
140
- if (input.files && input.files.length) {
140
+ if (input.files?.length) {
141
141
  box.style.display = "none";
142
142
 
143
- for (let file of input.files) {
143
+ for (const file of input.files) {
144
144
  formData.append(this.name, file);
145
145
  }
146
146
  } else {
@@ -1,8 +1,8 @@
1
1
  import "./file-input.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 { USAFileInputElement } from "./file-input.element.js";
5
+ import type { USAFileInputElement } from "./file-input.element.js";
6
6
 
7
7
  describe("usa-file-input", () => {
8
8
  it("should be accessible", async () => {
@@ -1,8 +1,8 @@
1
- import { attr, css, element } from "@joist/element";
2
1
  import { inject, injectable, injected } from "@joist/di";
2
+ import { attr, css, element } from "@joist/element";
3
3
 
4
- import { USAIcon } from "./icon-types.js";
5
4
  import { IconService } from "../services/icon.service.js";
5
+ import type { USAIcon } from "./icon-types.js";
6
6
 
7
7
  declare global {
8
8
  interface HTMLElementTagNameMap {
@@ -39,10 +39,6 @@ export class USAIconElement extends HTMLElement {
39
39
  #icon = inject(IconService);
40
40
  #injected = false;
41
41
 
42
- get #shadow() {
43
- return this.shadowRoot!;
44
- }
45
-
46
42
  @injected()
47
43
  onInjected() {
48
44
  this.#injected = true;
@@ -58,6 +54,8 @@ export class USAIconElement extends HTMLElement {
58
54
  async #updateIcon() {
59
55
  const icon = this.#icon();
60
56
 
61
- this.#shadow.append(await icon.getIcon(this.icon));
57
+ if (this.shadowRoot) {
58
+ this.shadowRoot.append(await icon.getIcon(this.icon));
59
+ }
62
60
  }
63
61
  }
@@ -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 { USAIconElement } from "./icon.element.js";
5
4
  import { ICON_TYPES } from "./icon-types.js";
5
+ import type { USAIconElement } from "./icon.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 = {
@@ -24,7 +24,7 @@ export const AllIcons: Story = {
24
24
  async function copyIcon(icon: string) {
25
25
  try {
26
26
  await navigator.clipboard.writeText(
27
- `<usa-icon icon=${icon}></usa-icon>`
27
+ `<usa-icon icon=${icon}></usa-icon>`,
28
28
  );
29
29
  alert(`copied markup for ${icon}`);
30
30
  } catch (err) {
@@ -43,7 +43,7 @@ export const AllIcons: Story = {
43
43
 
44
44
  ${icon}
45
45
  </button>
46
- `
46
+ `,
47
47
  )}`}
48
48
  </div>`;
49
49
  },
@@ -1,7 +1,7 @@
1
1
  import { attr, css, element, html, listen, query, ready } from "@joist/element";
2
- import { effect, observe } from "@joist/observable";
2
+ import { type Changes, effect, observe } from "@joist/observable";
3
3
 
4
- import { MaskableElement } from "../input-mask/maskable.element.js";
4
+ import type { MaskableElement } from "../input-mask/maskable.element.js";
5
5
 
6
6
  declare global {
7
7
  interface HTMLElementTagNameMap {
@@ -28,6 +28,7 @@ declare global {
28
28
  font-weight: 400;
29
29
  max-width: 30rem;
30
30
  margin-bottom: 1.5rem;
31
+ position: relative;
31
32
  }
32
33
 
33
34
  input {
@@ -49,12 +50,43 @@ declare global {
49
50
  outline: 0.25rem solid #2491ff;
50
51
  outline-offset: 0;
51
52
  }
53
+
54
+ slot[name="detail"]::slotted(*) {
55
+ color: #757575;
56
+ }
57
+
58
+ slot[name="detail"]::slotted(usa-icon) {
59
+ width: 1.5rem;
60
+ height: 1.5rem;
61
+ }
62
+
63
+ slot[name="detail"] {
64
+ display: block;
65
+ position: absolute;
66
+ bottom: 0.21rem;
67
+ left: 0.5rem;
68
+ }
69
+
70
+ :host([detail="pfx"]) input {
71
+ padding-left: 2.5rem;
72
+ }
73
+
74
+ :host([detail="sfx"]) input {
75
+ padding-right: 2.5rem;
76
+ }
77
+
78
+ :host([detail="sfx"]) slot[name="detail"] {
79
+ right: 0.5rem;
80
+ left: auto;
81
+ }
52
82
  `,
53
83
  html`
54
84
  <label>
85
+ <slot name="detail"></slot>
86
+
55
87
  <slot></slot>
56
88
 
57
- <input />
89
+ <input tabindex="0" />
58
90
  </label>
59
91
  `,
60
92
  ],
@@ -74,6 +106,14 @@ export class USATextInputElement
74
106
  @attr()
75
107
  accessor placeholder = "";
76
108
 
109
+ @attr()
110
+ accessor required = false;
111
+
112
+ @attr({
113
+ observed: false,
114
+ })
115
+ accessor detail: "pfx" | "sfx" | "" = "";
116
+
77
117
  @attr({
78
118
  reflect: false,
79
119
  })
@@ -95,13 +135,25 @@ export class USATextInputElement
95
135
  input.autofocus = this.autofocus;
96
136
  }
97
137
 
138
+ connectedCallback() {
139
+ this.#syncFormState();
140
+ }
141
+
98
142
  @effect()
99
- onChange() {
143
+ onChange(changes: Changes<this>) {
100
144
  const input = this.#input();
101
145
 
102
146
  input.value = this.value;
103
- input.selectionStart = this.selectionStart;
104
- input.selectionEnd = this.selectionEnd;
147
+
148
+ if (changes.has("selectionStart")) {
149
+ input.selectionStart = this.selectionStart;
150
+ }
151
+
152
+ if (changes.has("selectionEnd")) {
153
+ input.selectionEnd = this.selectionEnd;
154
+ }
155
+
156
+ this.#syncFormState();
105
157
  }
106
158
 
107
159
  @listen("input")
@@ -111,8 +163,6 @@ export class USATextInputElement
111
163
  this.value = input.value;
112
164
  this.selectionStart = input.selectionStart;
113
165
  this.selectionEnd = input.selectionEnd;
114
-
115
- this.#internals.setFormValue(input.value);
116
166
  }
117
167
 
118
168
  attributeChangedCallback(attr: string) {
@@ -130,11 +180,18 @@ export class USATextInputElement
130
180
  case "name":
131
181
  input.name = this.name;
132
182
  break;
183
+ }
184
+ }
133
185
 
134
- case "value":
135
- input.value = this.value;
136
- this.#internals.setFormValue(this.value);
137
- break;
186
+ #syncFormState() {
187
+ const input = this.#input();
188
+
189
+ this.#internals.setFormValue(input.value);
190
+
191
+ if (this.required && !input.value) {
192
+ this.#internals.setValidity({ valueMissing: true }, "Required", input);
193
+ } else {
194
+ this.#internals.setValidity({});
138
195
  }
139
196
  }
140
197
  }
@@ -10,7 +10,9 @@ const meta = {
10
10
  render() {
11
11
  return html`
12
12
  <form>
13
- <usa-input name="fname" value="Danny" autocomplete="off">
13
+ <usa-input name="fname" value="Danny" autocomplete="off" detail="pfx">
14
+ <usa-icon icon="credit_card" slot="detail"></usa-icon>
15
+
14
16
  First name
15
17
  </usa-input>
16
18
 
@@ -1,7 +1,7 @@
1
1
  import "./input.element.js";
2
2
 
3
- import { fixture, html, assert } from "@open-wc/testing";
4
3
  import { fireEvent } from "@noctuatech-uswds/testing";
4
+ import { assert, fixture, html } from "@open-wc/testing";
5
5
 
6
6
  describe("usa-input", () => {
7
7
  it("should be accessible", async () => {
@@ -35,14 +35,29 @@ describe("usa-input", () => {
35
35
  </form>
36
36
  `);
37
37
 
38
- const input = form.querySelector("usa-input")!;
39
- const nativeInput = input.shadowRoot!.querySelector("input")!;
40
- nativeInput.value = "Bar";
38
+ const input = form.querySelector("usa-input");
39
+ const nativeInput = input?.shadowRoot?.querySelector("input");
41
40
 
42
- await fireEvent.input(nativeInput, { bubbles: true });
41
+ if (nativeInput) {
42
+ nativeInput.value = "Bar";
43
+
44
+ await fireEvent.input(nativeInput, { bubbles: true });
45
+ }
43
46
 
44
47
  const value = new FormData(form);
45
48
 
46
49
  assert.equal(value.get("fname"), "Bar");
47
50
  });
51
+
52
+ it("should not submit when not valid", async () => {
53
+ const form = await fixture<HTMLFormElement>(html`
54
+ <form>
55
+ <usa-input name="fname" required>Hello World</usa-input>
56
+
57
+ <button>Submit</button>
58
+ </form>
59
+ `);
60
+
61
+ assert.equal(form.checkValidity(), false);
62
+ });
48
63
  });
@@ -23,7 +23,7 @@ export function format(value: string, pattern: string): FormattedResult {
23
23
  let count = 0;
24
24
  let formatted = "";
25
25
 
26
- for (var i = 0; i < pattern.length; i++) {
26
+ for (let i = 0; i < pattern.length; i++) {
27
27
  const patternChar = pattern[i];
28
28
  const char = chars[count];
29
29
 
@@ -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;
@@ -54,10 +56,6 @@ export class USAInputMaskElement extends HTMLElement {
54
56
  input.selectionStart = selectionStart;
55
57
  input.selectionEnd = selectionStart;
56
58
  }
57
-
58
- if (prev !== input.value) {
59
- input.dispatchEvent(new Event("input", { bubbles: true }));
60
- }
61
59
  }
62
60
 
63
61
  @listen("keydown")