@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,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,11 +1,14 @@
1
- import { inject, injectable, injected, Injector, Provider } from "@joist/di";
2
- import { attr, css, element, html, ready } from "@joist/element";
1
+ import { injectable } from "@joist/di";
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
- @injectable()
8
+ @injectable({
9
+ name: "usa-config-ctx",
10
+ provideSelfAs: [USAConfig],
11
+ })
9
12
  @element({
10
13
  tagName: "usa-config",
11
14
  shadowDom: [
@@ -22,25 +25,4 @@ export class USAConfigElement extends HTMLElement {
22
25
  name: "icon-path",
23
26
  })
24
27
  accessor iconPath = "/assets/usa-icons/";
25
-
26
- #injector = inject(Injector);
27
-
28
- @injected()
29
- onInjected() {
30
- const { providers } = this.#injector();
31
- const config = this;
32
-
33
- const usaConfig: Provider<USAConfig> = {
34
- provide: USAConfig,
35
- factory() {
36
- return {
37
- get iconPath() {
38
- return config.iconPath;
39
- },
40
- };
41
- },
42
- };
43
-
44
- providers.push(usaConfig);
45
- }
46
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,46 +106,63 @@ 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
  })
80
120
  @observe()
81
121
  accessor value = "";
82
122
 
83
- get selectionStart() {
84
- const { selectionStart } = this.#input();
123
+ @observe()
124
+ accessor selectionStart: number | null = null;
85
125
 
86
- return selectionStart;
87
- }
126
+ @observe()
127
+ accessor selectionEnd: number | null = null;
88
128
 
89
129
  #internals = this.attachInternals();
90
130
  #input = query("input");
91
131
 
92
- setSelectionRange(start: number, end: number) {
93
- const input = this.#input();
94
-
95
- input.setSelectionRange(start, end);
96
- }
97
-
98
132
  @ready()
99
133
  onReady() {
100
134
  const input = this.#input();
101
135
  input.autofocus = this.autofocus;
102
136
  }
103
137
 
138
+ connectedCallback() {
139
+ this.#syncFormState();
140
+ }
141
+
104
142
  @effect()
105
- onChange() {
143
+ onChange(changes: Changes<this>) {
106
144
  const input = this.#input();
145
+
107
146
  input.value = this.value;
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();
108
157
  }
109
158
 
110
159
  @listen("input")
111
160
  onInputChange() {
112
161
  const input = this.#input();
113
162
 
114
- this.#internals.setFormValue(input.value);
115
-
116
163
  this.value = input.value;
164
+ this.selectionStart = input.selectionStart;
165
+ this.selectionEnd = input.selectionEnd;
117
166
  }
118
167
 
119
168
  attributeChangedCallback(attr: string) {
@@ -131,11 +180,18 @@ export class USATextInputElement
131
180
  case "name":
132
181
  input.name = this.name;
133
182
  break;
183
+ }
184
+ }
134
185
 
135
- case "value":
136
- input.value = this.value;
137
- this.#internals.setFormValue(this.value);
138
- 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({});
139
195
  }
140
196
  }
141
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" foo="test">
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