@noctuatech/uswds 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +25 -0
  3. package/package.json +89 -0
  4. package/src/lib/alert/alert-types.ts +33 -0
  5. package/src/lib/alert/alert.element.ts +105 -0
  6. package/src/lib/alert/alert.stories.ts +63 -0
  7. package/src/lib/alert/alert.test.ts +23 -0
  8. package/src/lib/button/button.element.ts +224 -0
  9. package/src/lib/button/button.stories.ts +34 -0
  10. package/src/lib/button/button.test.ts +17 -0
  11. package/src/lib/checkbox/checkbox.element.ts +166 -0
  12. package/src/lib/checkbox/checkbox.stories.ts +57 -0
  13. package/src/lib/checkbox/checkbox.test.ts +47 -0
  14. package/src/lib/config/config.element.ts +31 -0
  15. package/src/lib/config/config.test.ts +15 -0
  16. package/src/lib/define.ts +14 -0
  17. package/src/lib/description/description.element.ts +22 -0
  18. package/src/lib/description/description.test.ts +15 -0
  19. package/src/lib/file-input/file-input-preview.element.ts +121 -0
  20. package/src/lib/file-input/file-input-preview.test.ts +95 -0
  21. package/src/lib/file-input/file-input.element.ts +140 -0
  22. package/src/lib/file-input/file-input.stories.ts +46 -0
  23. package/src/lib/file-input/file-input.test.ts +47 -0
  24. package/src/lib/icon/icon-types.ts +263 -0
  25. package/src/lib/icon/icon.element.ts +65 -0
  26. package/src/lib/icon/icon.stories.ts +50 -0
  27. package/src/lib/input/input.element.ts +138 -0
  28. package/src/lib/input/input.stories.ts +30 -0
  29. package/src/lib/input/input.test.ts +48 -0
  30. package/src/lib/input-mask/format.ts +56 -0
  31. package/src/lib/input-mask/input-mask.element.ts +93 -0
  32. package/src/lib/input-mask/input-mask.stories.ts +38 -0
  33. package/src/lib/input-mask/input-mask.test.ts +106 -0
  34. package/src/lib/input-mask/maskable.element.ts +5 -0
  35. package/src/lib/link/link.element.ts +62 -0
  36. package/src/lib/link/link.stories.ts +30 -0
  37. package/src/lib/radio/radio-option.element.ts +46 -0
  38. package/src/lib/radio/radio-option.test.ts +20 -0
  39. package/src/lib/radio/radio.element.ts +152 -0
  40. package/src/lib/radio/radio.stories.ts +47 -0
  41. package/src/lib/radio/radio.test.ts +174 -0
  42. package/src/lib/select/select-option.element.ts +40 -0
  43. package/src/lib/select/select.element.ts +121 -0
  44. package/src/lib/select/select.stories.ts +33 -0
  45. package/src/lib/select/select.test.ts +113 -0
  46. package/src/lib/tag/tag.element.ts +46 -0
  47. package/src/lib/tag/tag.stories.ts +31 -0
  48. package/src/lib/tag/tag.test.ts +15 -0
  49. package/src/lib.ts +13 -0
  50. package/target/lib/alert/alert-types.d.ts +7 -0
  51. package/target/lib/alert/alert-types.js +25 -0
  52. package/target/lib/alert/alert-types.js.map +1 -0
  53. package/target/lib/alert/alert.element.d.ts +11 -0
  54. package/target/lib/alert/alert.element.js +124 -0
  55. package/target/lib/alert/alert.element.js.map +1 -0
  56. package/target/lib/alert/alert.stories.d.ts +11 -0
  57. package/target/lib/alert/alert.stories.js +56 -0
  58. package/target/lib/alert/alert.stories.js.map +1 -0
  59. package/target/lib/alert/alert.test.d.ts +1 -0
  60. package/target/lib/alert/alert.test.js +20 -0
  61. package/target/lib/alert/alert.test.js.map +1 -0
  62. package/target/lib/button/button.element.d.ts +17 -0
  63. package/target/lib/button/button.element.js +259 -0
  64. package/target/lib/button/button.element.js.map +1 -0
  65. package/target/lib/button/button.stories.d.ts +12 -0
  66. package/target/lib/button/button.stories.js +25 -0
  67. package/target/lib/button/button.stories.js.map +1 -0
  68. package/target/lib/button/button.test.d.ts +1 -0
  69. package/target/lib/button/button.test.js +14 -0
  70. package/target/lib/button/button.test.js.map +1 -0
  71. package/target/lib/checkbox/checkbox.element.d.ts +16 -0
  72. package/target/lib/checkbox/checkbox.element.js +205 -0
  73. package/target/lib/checkbox/checkbox.element.js.map +1 -0
  74. package/target/lib/checkbox/checkbox.stories.d.ts +31 -0
  75. package/target/lib/checkbox/checkbox.stories.js +46 -0
  76. package/target/lib/checkbox/checkbox.stories.js.map +1 -0
  77. package/target/lib/checkbox/checkbox.test.d.ts +1 -0
  78. package/target/lib/checkbox/checkbox.test.js +38 -0
  79. package/target/lib/checkbox/checkbox.test.js.map +1 -0
  80. package/target/lib/config/config.element.d.ts +8 -0
  81. package/target/lib/config/config.element.js +57 -0
  82. package/target/lib/config/config.element.js.map +1 -0
  83. package/target/lib/config/config.test.d.ts +1 -0
  84. package/target/lib/config/config.test.js +11 -0
  85. package/target/lib/config/config.test.js.map +1 -0
  86. package/target/lib/define.d.ts +14 -0
  87. package/target/lib/define.js +15 -0
  88. package/target/lib/define.js.map +1 -0
  89. package/target/lib/description/description.element.d.ts +7 -0
  90. package/target/lib/description/description.element.js +34 -0
  91. package/target/lib/description/description.element.js.map +1 -0
  92. package/target/lib/description/description.test.d.ts +1 -0
  93. package/target/lib/description/description.test.js +11 -0
  94. package/target/lib/description/description.test.js.map +1 -0
  95. package/target/lib/file-input/file-input-preview.element.d.ts +11 -0
  96. package/target/lib/file-input/file-input-preview.element.js +136 -0
  97. package/target/lib/file-input/file-input-preview.element.js.map +1 -0
  98. package/target/lib/file-input/file-input-preview.test.d.ts +2 -0
  99. package/target/lib/file-input/file-input-preview.test.js +67 -0
  100. package/target/lib/file-input/file-input-preview.test.js.map +1 -0
  101. package/target/lib/file-input/file-input.element.d.ts +18 -0
  102. package/target/lib/file-input/file-input.element.js +180 -0
  103. package/target/lib/file-input/file-input.element.js.map +1 -0
  104. package/target/lib/file-input/file-input.stories.d.ts +12 -0
  105. package/target/lib/file-input/file-input.stories.js +36 -0
  106. package/target/lib/file-input/file-input.stories.js.map +1 -0
  107. package/target/lib/file-input/file-input.test.d.ts +1 -0
  108. package/target/lib/file-input/file-input.test.js +37 -0
  109. package/target/lib/file-input/file-input.test.js.map +1 -0
  110. package/target/lib/icon/icon-types.d.ts +2 -0
  111. package/target/lib/icon/icon-types.js +262 -0
  112. package/target/lib/icon/icon-types.js.map +1 -0
  113. package/target/lib/icon/icon.element.d.ts +12 -0
  114. package/target/lib/icon/icon.element.js +84 -0
  115. package/target/lib/icon/icon.element.js.map +1 -0
  116. package/target/lib/icon/icon.stories.d.ts +12 -0
  117. package/target/lib/icon/icon.stories.js +39 -0
  118. package/target/lib/icon/icon.stories.js.map +1 -0
  119. package/target/lib/input/input.element.d.ts +19 -0
  120. package/target/lib/input/input.element.js +166 -0
  121. package/target/lib/input/input.element.js.map +1 -0
  122. package/target/lib/input/input.stories.d.ts +12 -0
  123. package/target/lib/input/input.stories.js +23 -0
  124. package/target/lib/input/input.stories.js.map +1 -0
  125. package/target/lib/input/input.test.d.ts +1 -0
  126. package/target/lib/input/input.test.js +38 -0
  127. package/target/lib/input/input.test.js.map +1 -0
  128. package/target/lib/input-mask/format.d.ts +15 -0
  129. package/target/lib/input-mask/format.js +47 -0
  130. package/target/lib/input-mask/format.js.map +1 -0
  131. package/target/lib/input-mask/input-mask.element.d.ts +12 -0
  132. package/target/lib/input-mask/input-mask.element.js +111 -0
  133. package/target/lib/input-mask/input-mask.element.js.map +1 -0
  134. package/target/lib/input-mask/input-mask.stories.d.ts +14 -0
  135. package/target/lib/input-mask/input-mask.stories.js +31 -0
  136. package/target/lib/input-mask/input-mask.stories.js.map +1 -0
  137. package/target/lib/input-mask/input-mask.test.d.ts +2 -0
  138. package/target/lib/input-mask/input-mask.test.js +85 -0
  139. package/target/lib/input-mask/input-mask.test.js.map +1 -0
  140. package/target/lib/input-mask/maskable.element.d.ts +5 -0
  141. package/target/lib/input-mask/maskable.element.js +2 -0
  142. package/target/lib/input-mask/maskable.element.js.map +1 -0
  143. package/target/lib/link/link.element.d.ts +13 -0
  144. package/target/lib/link/link.element.js +98 -0
  145. package/target/lib/link/link.element.js.map +1 -0
  146. package/target/lib/link/link.stories.d.ts +16 -0
  147. package/target/lib/link/link.stories.js +23 -0
  148. package/target/lib/link/link.stories.js.map +1 -0
  149. package/target/lib/radio/radio-option.element.d.ts +13 -0
  150. package/target/lib/radio/radio-option.element.js +63 -0
  151. package/target/lib/radio/radio-option.element.js.map +1 -0
  152. package/target/lib/radio/radio-option.test.d.ts +2 -0
  153. package/target/lib/radio/radio-option.test.js +15 -0
  154. package/target/lib/radio/radio-option.test.js.map +1 -0
  155. package/target/lib/radio/radio.element.d.ts +18 -0
  156. package/target/lib/radio/radio.element.js +177 -0
  157. package/target/lib/radio/radio.element.js.map +1 -0
  158. package/target/lib/radio/radio.stories.d.ts +12 -0
  159. package/target/lib/radio/radio.stories.js +40 -0
  160. package/target/lib/radio/radio.stories.js.map +1 -0
  161. package/target/lib/radio/radio.test.d.ts +2 -0
  162. package/target/lib/radio/radio.test.js +147 -0
  163. package/target/lib/radio/radio.test.js.map +1 -0
  164. package/target/lib/select/select-option.element.d.ts +11 -0
  165. package/target/lib/select/select-option.element.js +58 -0
  166. package/target/lib/select/select-option.element.js.map +1 -0
  167. package/target/lib/select/select.element.d.ts +16 -0
  168. package/target/lib/select/select.element.js +144 -0
  169. package/target/lib/select/select.element.js.map +1 -0
  170. package/target/lib/select/select.stories.d.ts +12 -0
  171. package/target/lib/select/select.stories.js +26 -0
  172. package/target/lib/select/select.stories.js.map +1 -0
  173. package/target/lib/select/select.test.d.ts +2 -0
  174. package/target/lib/select/select.test.js +89 -0
  175. package/target/lib/select/select.test.js.map +1 -0
  176. package/target/lib/tag/tag.element.d.ts +10 -0
  177. package/target/lib/tag/tag.element.js +66 -0
  178. package/target/lib/tag/tag.element.js.map +1 -0
  179. package/target/lib/tag/tag.stories.d.ts +19 -0
  180. package/target/lib/tag/tag.stories.js +25 -0
  181. package/target/lib/tag/tag.stories.js.map +1 -0
  182. package/target/lib/tag/tag.test.d.ts +1 -0
  183. package/target/lib/tag/tag.test.js +11 -0
  184. package/target/lib/tag/tag.test.js.map +1 -0
  185. package/target/lib.d.ts +13 -0
  186. package/target/lib.js +14 -0
  187. package/target/lib.js.map +1 -0
@@ -0,0 +1,166 @@
1
+ import { attr, css, element, html, listen, query } from "@joist/element";
2
+
3
+ declare global {
4
+ interface HTMLElementTagNameMap {
5
+ "usa-checkbox": USACheckboxElement;
6
+ }
7
+ }
8
+
9
+ @element({
10
+ tagName: "usa-checkbox",
11
+ shadowDom: [
12
+ css`
13
+ * {
14
+ box-sizing: border-box;
15
+ }
16
+
17
+ :host {
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
+ }
27
+
28
+ :host([tiled]) label {
29
+ background-color: #fff;
30
+ border: 2px solid #c9c9c9;
31
+ border-radius: 0.25rem;
32
+ color: #1b1b1b;
33
+ padding: 0.75rem 1rem 0.75rem 0.75rem;
34
+ }
35
+
36
+ label {
37
+ display: inline-flex;
38
+ cursor: pointer;
39
+ font-size: 1.06rem;
40
+ line-height: 1.3;
41
+ flex-wrap: wrap;
42
+ }
43
+
44
+ .checkbox {
45
+ background: #fff;
46
+ box-shadow: 0 0 0 2px #1b1b1b;
47
+ display: flex;
48
+ align-items: center;
49
+ justify-content: center;
50
+ height: 1.25rem;
51
+ width: 1.25rem;
52
+ border-radius: 2px;
53
+ position: relative;
54
+ margin-right: 0.75rem;
55
+ }
56
+
57
+ input:checked + .checkbox {
58
+ background-color: #005ea2;
59
+ box-shadow: 0 0 0 2px #005ea2;
60
+ }
61
+
62
+ input:checked + .checkbox::after {
63
+ content: " ";
64
+ display: block;
65
+ height: 1rem;
66
+ width: 0.5rem;
67
+ border-right: 4px solid rgb(255, 255, 255);
68
+ border-bottom: 4px solid rgb(255, 255, 255);
69
+ transform: rotate(45deg) scale(0.65);
70
+ }
71
+
72
+ input {
73
+ height: 0;
74
+ width: 0;
75
+ position: absolute;
76
+ left: -999em;
77
+ right: auto;
78
+ }
79
+
80
+ input:focus + .checkbox {
81
+ outline: 0.25rem solid #2491ff;
82
+ outline-offset: 0.25rem;
83
+ }
84
+
85
+ .description {
86
+ display: block;
87
+ font-size: 0.93rem;
88
+ margin-top: 0.5rem;
89
+ }
90
+
91
+ .break {
92
+ flex-basis: 100%;
93
+ height: 0;
94
+ }
95
+
96
+ .spacer {
97
+ height: 1.25rem;
98
+ width: 1.25rem;
99
+ margin-right: 0.75rem;
100
+ }
101
+
102
+ :host([tiled]) label:has(input:checked) {
103
+ background-color: rgba(0, 94, 162, 0.1);
104
+ border-color: #005ea2;
105
+ }
106
+ `,
107
+ html`
108
+ <label>
109
+ <input type="checkbox" />
110
+
111
+ <div class="checkbox"></div>
112
+
113
+ <div class="title">
114
+ <slot></slot>
115
+ </div>
116
+ </label>
117
+ `,
118
+ ],
119
+ })
120
+ export class USACheckboxElement extends HTMLElement {
121
+ static formAssociated = true;
122
+
123
+ @attr()
124
+ accessor checked = false;
125
+
126
+ @attr()
127
+ accessor name = "";
128
+
129
+ @attr()
130
+ accessor value = "";
131
+
132
+ @attr()
133
+ accessor tiled = false;
134
+
135
+ #checkbox = query("input");
136
+ #internals = this.attachInternals();
137
+
138
+ connectedCallback() {
139
+ const checkbox = this.#checkbox();
140
+
141
+ if (this.checked) {
142
+ this.#internals.setFormValue(this.value);
143
+ }
144
+
145
+ checkbox.checked = this.checked;
146
+ checkbox.name = this.name;
147
+ }
148
+
149
+ attributeChangedCallback() {
150
+ const checkbox = this.#checkbox();
151
+
152
+ checkbox.checked = this.checked;
153
+ checkbox.name = this.name;
154
+ }
155
+
156
+ @listen("change", "input[type=checkbox]")
157
+ onCheckboxChange() {
158
+ const checkbox = this.#checkbox();
159
+
160
+ if (checkbox.checked) {
161
+ this.#internals.setFormValue(this.value);
162
+ } else {
163
+ this.#internals.setFormValue(null);
164
+ }
165
+ }
166
+ }
@@ -0,0 +1,57 @@
1
+ import type { Meta, StoryObj } from "@storybook/web-components";
2
+ import { html } from "lit";
3
+
4
+ import { ifDefined } from "lit/directives/if-defined.js";
5
+ import { when } from "lit/directives/when.js";
6
+
7
+ import type { USACheckboxElement } from "./checkbox.element.js";
8
+
9
+ // More on how to set up stories at: https://storybook.js.org/docs/writing-stories
10
+ const meta = {
11
+ title: "usa-checkbox",
12
+ tags: ["autodocs"],
13
+ render(args) {
14
+ return html`
15
+ <usa-checkbox
16
+ name=${args.name}
17
+ value=${ifDefined(args.value)}
18
+ checked=${ifDefined(args.checked)}
19
+ ?tiled=${args.tiled}
20
+ >
21
+ Hello World
22
+ ${when(
23
+ args.description,
24
+ () => html`<usa-description>${args.description}</usa-description>`
25
+ )}
26
+ </usa-checkbox>
27
+ `;
28
+ },
29
+ argTypes: {
30
+ name: {
31
+ control: "text",
32
+ },
33
+ value: {
34
+ control: "text",
35
+ },
36
+ description: {
37
+ control: "text",
38
+ },
39
+ tiled: {
40
+ control: "boolean",
41
+ },
42
+ },
43
+ args: {
44
+ name: "toc",
45
+ value: "agree",
46
+ tiled: false,
47
+ },
48
+ } satisfies Meta<USACheckboxElement & { description: string }>;
49
+
50
+ export default meta;
51
+
52
+ type Story = StoryObj<USACheckboxElement>;
53
+
54
+ // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
55
+ export const Primary: Story = {
56
+ args: {},
57
+ };
@@ -0,0 +1,47 @@
1
+ import "./checkbox.element.js";
2
+
3
+ import { fixture, html, assert } from "@open-wc/testing";
4
+
5
+ describe("usa-checkbox", () => {
6
+ it("should be accessible", async () => {
7
+ const form = await fixture<HTMLFormElement>(html`
8
+ <usa-checkbox name="fname" value="Foo">Hello World</usa-checkbox>
9
+ `);
10
+
11
+ return assert.isAccessible(form);
12
+ });
13
+
14
+ it("should submit form with default values", async () => {
15
+ const form = await fixture<HTMLFormElement>(html`
16
+ <form>
17
+ <usa-checkbox name="enabled" value="test" checked>
18
+ Hello World
19
+ </usa-checkbox>
20
+
21
+ <button>Submit</button>
22
+ </form>
23
+ `);
24
+
25
+ const value = new FormData(form);
26
+
27
+ assert.equal(value.get("enabled"), "test");
28
+ });
29
+
30
+ it("should update form value as input value changed", async () => {
31
+ const form = await fixture<HTMLFormElement>(html`
32
+ <form>
33
+ <usa-checkbox name="enabled" value="test">Hello World</usa-checkbox>
34
+
35
+ <button>Submit</button>
36
+ </form>
37
+ `);
38
+
39
+ const checkbox = form.querySelector("usa-checkbox")!;
40
+ const nativeInput = checkbox.shadowRoot!.querySelector("input")!;
41
+ nativeInput.click();
42
+
43
+ const value = new FormData(form);
44
+
45
+ assert.equal(value.get("enabled"), "test");
46
+ });
47
+ });
@@ -0,0 +1,31 @@
1
+ import { inject, injectable, Injector } from "@joist/di";
2
+ import { attr, css, element, html } from "@joist/element";
3
+
4
+ export class USAConfig {
5
+ spriteSheet = "";
6
+ }
7
+
8
+ @element({
9
+ tagName: "usa-config",
10
+ shadowDom: [
11
+ css`
12
+ :host {
13
+ display: contents;
14
+ }
15
+ `,
16
+ html`<slot></slot>`,
17
+ ],
18
+ })
19
+ @injectable()
20
+ export class USAConfigElement extends HTMLElement implements USAConfig {
21
+ @attr()
22
+ accessor spriteSheet = "/assets/img/sprite.svg";
23
+
24
+ #injector = inject(Injector);
25
+
26
+ connectedCallback() {
27
+ const { providers } = this.#injector();
28
+
29
+ providers.push({ provide: USAConfig, factory: () => this });
30
+ }
31
+ }
@@ -0,0 +1,15 @@
1
+ import "./config.element.js";
2
+
3
+ import { fixture, html, assert } from "@open-wc/testing";
4
+
5
+ import { USAConfigElement } from "./config.element.js";
6
+
7
+ describe("usa-config", () => {
8
+ it("should be accessible", async () => {
9
+ const config = await fixture<USAConfigElement>(html`
10
+ <usa-config>Hello World</usa-config>
11
+ `);
12
+
13
+ return assert.isAccessible(config);
14
+ });
15
+ });
@@ -0,0 +1,14 @@
1
+ import "./config/config.element.js";
2
+ import "./alert/alert.element.js";
3
+ import "./button/button.element.js";
4
+ import "./checkbox/checkbox.element.js";
5
+ import "./description/description.element.js";
6
+ import "./file-input/file-input-preview.element.js";
7
+ import "./icon/icon.element.js";
8
+ import "./input/input.element.js";
9
+ import "./input-mask/input-mask.element.js";
10
+ import "./link/link.element.js";
11
+ import "./radio/radio.element.js";
12
+ import "./radio/radio-option.element.js";
13
+ import "./select/select-option.element.js";
14
+ import "./tag/tag.element.js";
@@ -0,0 +1,22 @@
1
+ import { css, element, html } from "@joist/element";
2
+
3
+ declare global {
4
+ interface HTMLElementTagNameMap {
5
+ "usa-description": USADescriptionElement;
6
+ }
7
+ }
8
+
9
+ @element({
10
+ tagName: "usa-description",
11
+ shadowDom: [
12
+ css`
13
+ :host {
14
+ display: block;
15
+ font-size: 0.93rem;
16
+ margin-top: 0.5rem;
17
+ }
18
+ `,
19
+ html` <slot></slot> `,
20
+ ],
21
+ })
22
+ export class USADescriptionElement extends HTMLElement {}
@@ -0,0 +1,15 @@
1
+ import "./description.element.js";
2
+
3
+ import { fixture, html, assert } from "@open-wc/testing";
4
+
5
+ import { USADescriptionElement } from "./description.element.js";
6
+
7
+ describe("usa-description", () => {
8
+ it("should be accessible", async () => {
9
+ const description = await fixture<USADescriptionElement>(html`
10
+ <usa-description>Hello World</usa-description>
11
+ `);
12
+
13
+ return assert.isAccessible(description);
14
+ });
15
+ });
@@ -0,0 +1,121 @@
1
+ import { css, element, html, query } from "@joist/element";
2
+ import { effect, observe } from "@joist/observable";
3
+
4
+ declare global {
5
+ interface HTMLElementTagNameMap {
6
+ "usa-file-input-preview": USAFileInputPreviewElement;
7
+ }
8
+ }
9
+
10
+ @element({
11
+ tagName: "usa-file-input-preview",
12
+ shadowDom: [
13
+ css`
14
+ * {
15
+ box-sizing: border-box;
16
+ }
17
+
18
+ :host {
19
+ display: block;
20
+ font-size: 0.87rem;
21
+ pointer-events: none;
22
+ position: relative;
23
+ text-align: left;
24
+ word-wrap: anywhere;
25
+ z-index: 3;
26
+ }
27
+
28
+ :host([hidden]) {
29
+ display: none;
30
+ }
31
+
32
+ img {
33
+ border: 0;
34
+ display: block;
35
+ height: 2.5rem;
36
+ margin-right: 0.5rem;
37
+ -o-object-fit: contain;
38
+ object-fit: contain;
39
+ width: 2.5rem;
40
+ }
41
+
42
+ .preview-heading {
43
+ align-items: center;
44
+ background: #d9e8f6;
45
+ display: flex;
46
+ pointer-events: none;
47
+ position: relative;
48
+ z-index: 3;
49
+ font-weight: 700;
50
+ justify-content: space-between;
51
+ padding: 0.5rem;
52
+ text-align: left;
53
+ }
54
+
55
+ .preview-item {
56
+ align-items: center;
57
+ background: #d9e8f6;
58
+ display: flex;
59
+ padding: 0.5rem;
60
+ width: 100%;
61
+ margin-top: 1px;
62
+ }
63
+ `,
64
+ html`<slot class="preview-heading"></slot>`,
65
+ ],
66
+ })
67
+ export class USAFileInputPreviewElement extends HTMLElement {
68
+ @observe()
69
+ accessor files: FileList | null = null;
70
+
71
+ #items = new Map<string, HTMLElement>();
72
+
73
+ connectedCallback() {
74
+ this.onChange();
75
+ }
76
+
77
+ @effect()
78
+ onChange() {
79
+ if (this.files) {
80
+ this.hidden = false;
81
+
82
+ let names = new Set<string>();
83
+
84
+ for (let file of this.files) {
85
+ names.add(file.name);
86
+
87
+ if (!this.#items.has(file.name)) {
88
+ const item = document.createElement("div");
89
+ item.id = file.name;
90
+ item.className = "preview-item";
91
+
92
+ const img = createImagePreview(file);
93
+
94
+ item.append(img, document.createTextNode(file.name));
95
+
96
+ this.shadowRoot!.append(item);
97
+ this.#items.set(file.name, item);
98
+ }
99
+ }
100
+
101
+ for (let [name, item] of this.#items) {
102
+ if (!names.has(name)) {
103
+ item.remove();
104
+ this.#items.delete(name);
105
+ }
106
+ }
107
+ } else {
108
+ this.hidden = true;
109
+ }
110
+ }
111
+ }
112
+
113
+ function createImagePreview(file: File) {
114
+ const img = new Image();
115
+ img.height = 40;
116
+ img.width = 40;
117
+ img.src = URL.createObjectURL(file);
118
+ img.ariaHidden = "true";
119
+
120
+ return img;
121
+ }
@@ -0,0 +1,95 @@
1
+ import "./file-input-preview.element.js";
2
+ import "../link/link.element.js";
3
+
4
+ import { assert, fixture, html } from "@open-wc/testing";
5
+
6
+ import { USAFileInputPreviewElement } from "./file-input-preview.element.js";
7
+
8
+ describe("usa-file-input-preview", () => {
9
+ it("should be accessible with no files", async () => {
10
+ const fileInputPreview = await fixture<USAFileInputPreviewElement>(html`
11
+ <usa-file-input-preview>
12
+ Selected file <usa-link>Change file</usa-link>
13
+ </usa-file-input-preview>
14
+ `);
15
+
16
+ return assert.isAccessible(fileInputPreview);
17
+ });
18
+
19
+ it("should be accessible with files", async () => {
20
+ const data = new DataTransfer();
21
+ data.items.add(new File([], "image1.jpg"));
22
+ data.items.add(new File([], "image2.jpg"));
23
+
24
+ const fileInputPreview = await fixture<USAFileInputPreviewElement>(html`
25
+ <usa-file-input-preview .files=${data.files}>
26
+ Selected file <usa-link>Change file</usa-link>
27
+ </usa-file-input-preview>
28
+ `);
29
+
30
+ return assert.isAccessible(fileInputPreview);
31
+ });
32
+
33
+ it("should display file preview for images", async () => {
34
+ const data = new DataTransfer();
35
+ data.items.add(new File([], "image1.jpg"));
36
+ data.items.add(new File([], "image2.jpg"));
37
+
38
+ const { shadowRoot } = await fixture<USAFileInputPreviewElement>(html`
39
+ <usa-file-input-preview .files=${data.files}>
40
+ Selected file <usa-link>Change file</usa-link>
41
+ </usa-file-input-preview>
42
+ `);
43
+
44
+ const previewItems = Array.from(
45
+ shadowRoot!.querySelectorAll(".preview-item")
46
+ ).map((item) => item.textContent);
47
+
48
+ assert.deepEqual(previewItems, ["image1.jpg", "image2.jpg"]);
49
+ });
50
+
51
+ it("should display fewer previews when the number of items decreases", async () => {
52
+ const data = new DataTransfer();
53
+ data.items.add(new File([], "image1.jpg"));
54
+ data.items.add(new File([], "image2.jpg"));
55
+
56
+ const fileInputPreview = await fixture<USAFileInputPreviewElement>(html`
57
+ <usa-file-input-preview .files=${data.files}>
58
+ Selected file <usa-link>Change file</usa-link>
59
+ </usa-file-input-preview>
60
+ `);
61
+
62
+ const data2 = new DataTransfer();
63
+ data2.items.add(new File([], "image1.jpg"));
64
+
65
+ fileInputPreview.files = data2.files;
66
+
67
+ // need this to wait for effects to resolve
68
+ await Promise.resolve();
69
+
70
+ const previewItems = Array.from(
71
+ fileInputPreview.shadowRoot!.querySelectorAll(".preview-item")
72
+ ).map((item) => item.textContent);
73
+
74
+ assert.deepEqual(previewItems, ["image1.jpg"]);
75
+ });
76
+
77
+ it("should create preview images for each file", async () => {
78
+ const data = new DataTransfer();
79
+ data.items.add(new File(["Image1"], "image1.jpg"));
80
+ data.items.add(new File(["Image2"], "image2.jpg"));
81
+
82
+ const { shadowRoot } = await fixture<USAFileInputPreviewElement>(html`
83
+ <usa-file-input-preview .files=${data.files}>
84
+ Selected file <usa-link>Change file</usa-link>
85
+ </usa-file-input-preview>
86
+ `);
87
+
88
+ const [first, second] = Array.from(
89
+ shadowRoot!.querySelectorAll<HTMLImageElement>(".preview-item img")
90
+ ).map((item) => item.src.substring(29));
91
+
92
+ // we are just testing that the two hashes are in fact different
93
+ assert.isFalse(first === second);
94
+ });
95
+ });