@noctuatech/uswds 0.0.36 → 0.1.2

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 (143) hide show
  1. package/assets/flags/alabama.svg +5 -0
  2. package/assets/flags/alaska.svg +14 -0
  3. package/assets/flags/arizona.svg +7 -0
  4. package/assets/flags/arkansas.svg +15 -0
  5. package/assets/flags/california.svg +8 -0
  6. package/assets/flags/colorado.svg +8 -0
  7. package/assets/flags/connecticut.svg +5 -0
  8. package/assets/flags/delaware.svg +7 -0
  9. package/assets/flags/florida.svg +5 -0
  10. package/assets/flags/georgia.svg +5 -0
  11. package/assets/flags/hawaii.svg +19 -0
  12. package/assets/flags/idaho.svg +5 -0
  13. package/assets/flags/illinois.svg +5 -0
  14. package/assets/flags/indiana.svg +15 -0
  15. package/assets/flags/iowa.svg +13 -0
  16. package/assets/flags/kansas.svg +5 -0
  17. package/assets/flags/kentucky.svg +16 -0
  18. package/assets/flags/louisiana.svg +14 -0
  19. package/assets/flags/maine.svg +12 -0
  20. package/assets/flags/maryland.svg +14 -0
  21. package/assets/flags/massachusetts.svg +15 -0
  22. package/assets/flags/michigan.svg +5 -0
  23. package/assets/flags/minnesota.svg +15 -0
  24. package/assets/flags/mississippi.svg +10 -0
  25. package/assets/flags/missouri.svg +16 -0
  26. package/assets/flags/montana.svg +11 -0
  27. package/assets/flags/nebraska.svg +13 -0
  28. package/assets/flags/nevada.svg +14 -0
  29. package/assets/flags/new_hampshire.svg +13 -0
  30. package/assets/flags/new_jersey.svg +13 -0
  31. package/assets/flags/new_mexico.svg +7 -0
  32. package/assets/flags/new_york.svg +13 -0
  33. package/assets/flags/north_carolina.svg +5 -0
  34. package/assets/flags/north_dakota.svg +13 -0
  35. package/assets/flags/ohio.svg +16 -0
  36. package/assets/flags/oklahoma.svg +12 -0
  37. package/assets/flags/oregon.svg +13 -0
  38. package/assets/flags/pennsylvania.svg +15 -0
  39. package/assets/flags/rhode_island.svg +13 -0
  40. package/assets/flags/south_carolina.svg +11 -0
  41. package/assets/flags/south_dakota.svg +15 -0
  42. package/assets/flags/tennessee.svg +13 -0
  43. package/assets/flags/texas.svg +8 -0
  44. package/assets/flags/utah.svg +9 -0
  45. package/assets/flags/vermont.svg +13 -0
  46. package/assets/flags/virginia.svg +5 -0
  47. package/assets/flags/washington.svg +5 -0
  48. package/assets/flags/west_virginia.svg +24 -0
  49. package/assets/flags/wisconsin.svg +5 -0
  50. package/assets/flags/wyoming.svg +5 -0
  51. package/package.json +13 -5
  52. package/src/lib/accordion/accordion.test.ts +12 -10
  53. package/src/lib/button/button.stories.ts +1 -1
  54. package/src/lib/card/card.stories.ts +0 -1
  55. package/src/lib/checkbox/checkbox.element.ts +1 -17
  56. package/src/lib/checkbox/checkbox.stories.ts +0 -27
  57. package/src/lib/combo-box/combo-box-option/combo-box-option.element.ts +68 -0
  58. package/src/lib/combo-box/combo-box.element.ts +222 -0
  59. package/src/lib/combo-box/combo-box.stories.ts +236 -0
  60. package/src/lib/combo-box/combo-box.test.ts +150 -0
  61. package/src/lib/combo-box/context.ts +10 -0
  62. package/src/lib/define.ts +3 -0
  63. package/src/lib/input/input.element.ts +4 -0
  64. package/src/lib/input/input.test.ts +2 -4
  65. package/src/lib/radio/radio-option/radio-option.element.ts +2 -6
  66. package/src/lib/radio/radio.element.ts +1 -1
  67. package/src/lib/radio/radio.stories.ts +0 -1
  68. package/src/lib/range-slider/range-slider.element.ts +111 -0
  69. package/src/lib/range-slider/range-slider.stories.ts +24 -0
  70. package/src/lib/range-slider/range-slider.test.ts +52 -0
  71. package/src/lib/select/select.stories.ts +1 -1
  72. package/src/lib/select/select.test.ts +2 -4
  73. package/src/lib/side-nav/side-nav.stories.ts +1 -1
  74. package/src/lib/summary-box/summary-box.stories.ts +1 -1
  75. package/src/lib/textarea/textarea.test.ts +2 -4
  76. package/src/lib.ts +3 -0
  77. package/target/lib/accordion/accordion.test.js +12 -10
  78. package/target/lib/accordion/accordion.test.js.map +1 -1
  79. package/target/lib/button/button.stories.d.ts +1 -1
  80. package/target/lib/button/button.stories.js +1 -1
  81. package/target/lib/button/button.stories.js.map +1 -1
  82. package/target/lib/card/card.stories.js.map +1 -1
  83. package/target/lib/checkbox/checkbox.element.d.ts +0 -1
  84. package/target/lib/checkbox/checkbox.element.js +1 -15
  85. package/target/lib/checkbox/checkbox.element.js.map +1 -1
  86. package/target/lib/checkbox/checkbox.stories.d.ts +0 -1
  87. package/target/lib/checkbox/checkbox.stories.js +0 -24
  88. package/target/lib/checkbox/checkbox.stories.js.map +1 -1
  89. package/target/lib/combo-box/combo-box-option/combo-box-option.element.d.ts +12 -0
  90. package/target/lib/combo-box/combo-box-option/combo-box-option.element.js +72 -0
  91. package/target/lib/combo-box/combo-box-option/combo-box-option.element.js.map +1 -0
  92. package/target/lib/combo-box/combo-box.element.d.ts +22 -0
  93. package/target/lib/combo-box/combo-box.element.js +209 -0
  94. package/target/lib/combo-box/combo-box.element.js.map +1 -0
  95. package/target/lib/combo-box/combo-box.stories.d.ts +12 -0
  96. package/target/lib/combo-box/combo-box.stories.js +229 -0
  97. package/target/lib/combo-box/combo-box.stories.js.map +1 -0
  98. package/target/lib/combo-box/combo-box.test.d.ts +3 -0
  99. package/target/lib/combo-box/combo-box.test.js +88 -0
  100. package/target/lib/combo-box/combo-box.test.js.map +1 -0
  101. package/target/lib/combo-box/context.d.ts +6 -0
  102. package/target/lib/combo-box/context.js +3 -0
  103. package/target/lib/combo-box/context.js.map +1 -0
  104. package/target/lib/define.d.ts +3 -0
  105. package/target/lib/define.js +3 -0
  106. package/target/lib/define.js.map +1 -1
  107. package/target/lib/input/input.element.d.ts +1 -0
  108. package/target/lib/input/input.element.js +3 -0
  109. package/target/lib/input/input.element.js.map +1 -1
  110. package/target/lib/input/input.test.js +2 -3
  111. package/target/lib/input/input.test.js.map +1 -1
  112. package/target/lib/radio/radio-option/radio-option.element.js +2 -5
  113. package/target/lib/radio/radio-option/radio-option.element.js.map +1 -1
  114. package/target/lib/radio/radio.element.js.map +1 -1
  115. package/target/lib/radio/radio.stories.js.map +1 -1
  116. package/target/lib/range-slider/range-slider.element.d.ts +16 -0
  117. package/target/lib/range-slider/range-slider.element.js +146 -0
  118. package/target/lib/range-slider/range-slider.element.js.map +1 -0
  119. package/target/lib/range-slider/range-slider.stories.d.ts +12 -0
  120. package/target/lib/range-slider/range-slider.stories.js +17 -0
  121. package/target/lib/range-slider/range-slider.stories.js.map +1 -0
  122. package/target/lib/range-slider/range-slider.test.d.ts +1 -0
  123. package/target/lib/range-slider/range-slider.test.js +39 -0
  124. package/target/lib/range-slider/range-slider.test.js.map +1 -0
  125. package/target/lib/select/select.stories.d.ts +1 -1
  126. package/target/lib/select/select.stories.js +1 -1
  127. package/target/lib/select/select.stories.js.map +1 -1
  128. package/target/lib/select/select.test.js +2 -3
  129. package/target/lib/select/select.test.js.map +1 -1
  130. package/target/lib/side-nav/side-nav.stories.d.ts +1 -1
  131. package/target/lib/side-nav/side-nav.stories.js +1 -1
  132. package/target/lib/side-nav/side-nav.stories.js.map +1 -1
  133. package/target/lib/summary-box/summary-box.stories.d.ts +1 -1
  134. package/target/lib/summary-box/summary-box.stories.js +1 -1
  135. package/target/lib/summary-box/summary-box.stories.js.map +1 -1
  136. package/target/lib/textarea/textarea.test.js +2 -3
  137. package/target/lib/textarea/textarea.test.js.map +1 -1
  138. package/target/lib.d.ts +3 -0
  139. package/target/lib.js +3 -0
  140. package/target/lib.js.map +1 -1
  141. package/target/lib/form/validation.d.ts +0 -2
  142. package/target/lib/form/validation.js +0 -27
  143. package/target/lib/form/validation.js.map +0 -1
@@ -0,0 +1,24 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg width="24" height="16" viewBox="0 0 24 16" fill="none" xmlns="http://www.w3.org/2000/svg">
3
+ <rect width="24" height="16" fill="#FFFFFF"/>
4
+ <rect x="0" y="0" width="24" height="16" fill="#002868"/>
5
+
6
+ <!-- State seal circle -->
7
+ <circle cx="12" cy="8" r="6" fill="#FFFFFF"/>
8
+ <circle cx="12" cy="8" r="5" fill="#002868"/>
9
+
10
+ <!-- Rhododendron flowers -->
11
+ <circle cx="8" cy="6" r="1" fill="#FFFFFF"/>
12
+ <circle cx="16" cy="6" r="1" fill="#FFFFFF"/>
13
+ <circle cx="8" cy="10" r="1" fill="#FFFFFF"/>
14
+ <circle cx="16" cy="10" r="1" fill="#FFFFFF"/>
15
+ <circle cx="12" cy="8" r="1" fill="#FFFFFF"/>
16
+
17
+ <!-- Crossed rifles -->
18
+ <path d="M10 6L14 10" stroke="#FFFFFF" stroke-width="1"/>
19
+ <path d="M14 6L10 10" stroke="#FFFFFF" stroke-width="1"/>
20
+
21
+ <!-- Mining tools -->
22
+ <path d="M8 8L16 8" stroke="#FFFFFF" stroke-width="1"/>
23
+ <path d="M12 6L12 10" stroke="#FFFFFF" stroke-width="1"/>
24
+ </svg>
@@ -0,0 +1,5 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg width="24" height="16" viewBox="0 0 24 16" fill="none" xmlns="http://www.w3.org/2000/svg">
3
+ <rect width="24" height="16" fill="#002868"/>
4
+ <path d="M12 4L16 8L12 12L8 8L12 4Z" fill="#FFD700"/>
5
+ </svg>
@@ -0,0 +1,5 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg width="24" height="16" viewBox="0 0 24 16" fill="none" xmlns="http://www.w3.org/2000/svg">
3
+ <rect width="24" height="16" fill="#002868"/>
4
+ <path d="M12 4L16 8L12 12L8 8L12 4Z" fill="#FFD700"/>
5
+ </svg>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noctuatech/uswds",
3
- "version": "0.0.36",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "workspaces": ["packages/**"],
6
6
  "main": "./target/lib.js",
@@ -26,13 +26,13 @@
26
26
  },
27
27
  "test": {
28
28
  "command": "wtr",
29
- "dependencies": ["tsc"]
29
+ "dependencies": ["tsc", "build_testing_library"]
30
30
  },
31
31
  "build": {
32
32
  "dependencies": ["build_storybook", "build_website"]
33
33
  },
34
34
  "preview": {
35
- "command": "eleventy --serve --incremental --quiet",
35
+ "command": "eleventy --serve --incremental",
36
36
  "service": true,
37
37
  "dependencies": [
38
38
  {
@@ -48,14 +48,19 @@
48
48
  "command": "eleventy --pathprefix=uswds",
49
49
  "dependencies": ["tsc", "copy_icons"]
50
50
  },
51
+ "build_testing_library": {
52
+ "command": "./scripts/build_testing_library.sh",
53
+ "files": ["node_modules/@testing-library/**"],
54
+ "output": ["testing-library/**"]
55
+ },
51
56
  "tsc": {
52
57
  "command": "tsc --build --pretty",
53
58
  "clean": "if-file-deleted",
54
59
  "files": ["src/**", "tsconfig.json"],
55
- "dependencies": ["./packages/testing:build"]
60
+ "output": ["target/**"]
56
61
  },
57
62
  "copy_icons": {
58
- "command": "cp -a node_modules/@uswds/uswds/dist/img/usa-icons/. assets/usa-icons/",
63
+ "command": "./scripts/copy_usa_icons.sh",
59
64
  "files": ["node_modules/@uswds/uswds/dist/img/usa-icons/**"],
60
65
  "output": ["assets/usa-icon/**"]
61
66
  }
@@ -76,9 +81,12 @@
76
81
  "@storybook/addon-essentials": "^8.6.0",
77
82
  "@storybook/web-components": "^8.6.0",
78
83
  "@storybook/web-components-vite": "^8.6.0",
84
+ "@testing-library/dom": "^10.4.0",
85
+ "@testing-library/user-event": "^14.6.1",
79
86
  "@types/mocha": "^10.0.7",
80
87
  "@types/node": "^22.0.0",
81
88
  "@uswds/uswds": "^3.10.0",
89
+ "@web/dev-server-import-maps": "^0.2.1",
82
90
  "@web/test-runner": "^0.20.0",
83
91
  "husky": "^9.0.11",
84
92
  "js-beautify": "^1.15.1",
@@ -1,6 +1,8 @@
1
1
  import "./accordion.element.js";
2
2
 
3
3
  import { assert, fixture, html } from "@open-wc/testing";
4
+ import { screen } from "@testing-library/dom";
5
+ import { userEvent } from "@testing-library/user-event";
4
6
 
5
7
  import type { USAAccordionElement } from "./accordion.element.js";
6
8
 
@@ -36,10 +38,10 @@ describe("usa-accordion", () => {
36
38
  </usa-accordion>
37
39
  `);
38
40
 
39
- const heading = accordion.querySelector("h4");
41
+ const heading = await screen.findByRole("heading");
40
42
  const content = accordion.querySelector<HTMLDivElement>(".content");
41
43
 
42
- heading?.click();
44
+ await userEvent.click(heading);
43
45
 
44
46
  assert.isTrue(content?.checkVisibility());
45
47
  });
@@ -58,12 +60,12 @@ describe("usa-accordion", () => {
58
60
  </usa-accordion>
59
61
  `);
60
62
 
61
- const heading = accordion.querySelector("h4");
63
+ const heading = await screen.findByRole("heading");
62
64
  const content = accordion.querySelector<HTMLDivElement>(".content");
63
65
 
64
66
  assert.isFalse(content?.checkVisibility());
65
67
 
66
- heading?.click();
68
+ await userEvent.click(heading);
67
69
 
68
70
  assert.isTrue(content.checkVisibility());
69
71
  });
@@ -91,21 +93,21 @@ describe("usa-accordion", () => {
91
93
  const headings = el.querySelectorAll("h4");
92
94
  const content = Array.from(el.querySelectorAll<HTMLDivElement>(".content"));
93
95
 
94
- headings[0].click();
96
+ await userEvent.click(headings[0]);
95
97
 
96
98
  assert.deepEqual(
97
99
  content.map((el) => el.checkVisibility()),
98
100
  [true, false, false],
99
101
  );
100
102
 
101
- headings[1].click();
103
+ await userEvent.click(headings[1]);
102
104
 
103
105
  assert.deepEqual(
104
106
  content.map((el) => el.checkVisibility()),
105
107
  [false, true, false],
106
108
  );
107
109
 
108
- headings[2].click();
110
+ await userEvent.click(headings[2]);
109
111
 
110
112
  assert.deepEqual(
111
113
  content.map((el) => el.checkVisibility()),
@@ -136,21 +138,21 @@ describe("usa-accordion", () => {
136
138
  const headings = el.querySelectorAll("h4");
137
139
  const content = Array.from(el.querySelectorAll<HTMLDivElement>(".content"));
138
140
 
139
- headings[0].click();
141
+ await userEvent.click(headings[0]);
140
142
 
141
143
  assert.deepEqual(
142
144
  content.map((el) => el.checkVisibility()),
143
145
  [true, false, false],
144
146
  );
145
147
 
146
- headings[1].click();
148
+ await userEvent.click(headings[1]);
147
149
 
148
150
  assert.deepEqual(
149
151
  content.map((el) => el.checkVisibility()),
150
152
  [false, true, false],
151
153
  );
152
154
 
153
- headings[2].click();
155
+ await userEvent.click(headings[2]);
154
156
 
155
157
  assert.deepEqual(
156
158
  content.map((el) => el.checkVisibility()),
@@ -7,7 +7,7 @@ import { BUTTON_VARIANTS, type USAButtonElement } from "./button.element.js";
7
7
  const meta = {
8
8
  title: "usa-button",
9
9
  tags: ["autodocs"],
10
- render(args) {
10
+ render() {
11
11
  return html`
12
12
  <div style="display: inline-flex; flex-direction: column; gap: 1rem">
13
13
  ${BUTTON_VARIANTS.map(
@@ -7,7 +7,6 @@ import type { USACardElement } from "./card.element.js";
7
7
  const meta = {
8
8
  title: "usa-card",
9
9
  tags: ["autodocs"],
10
-
11
10
  argTypes: {},
12
11
  args: {},
13
12
  } satisfies Meta<USACardElement>;
@@ -150,17 +150,6 @@ export class USACheckboxElement extends HTMLElement {
150
150
 
151
151
  #internals = this.attachInternals();
152
152
 
153
- connectedCallback() {
154
- this.#checkbox({
155
- checked: this.checked,
156
- name: this.name,
157
- disabled: this.disabled,
158
- required: this.required,
159
- });
160
-
161
- this.#syncFormState();
162
- }
163
-
164
153
  attributeChangedCallback() {
165
154
  this.#checkbox({
166
155
  checked: this.checked,
@@ -184,12 +173,7 @@ export class USACheckboxElement extends HTMLElement {
184
173
  const checkbox = this.#checkbox();
185
174
 
186
175
  this.#internals.setValidity({});
187
-
188
- if (checkbox.checked) {
189
- this.#internals.setFormValue(this.value);
190
- } else {
191
- this.#internals.setFormValue(null);
192
- }
176
+ this.#internals.setFormValue(checkbox.checked ? this.value : null);
193
177
 
194
178
  if (checkbox.validationMessage) {
195
179
  this.#internals.setValidity(
@@ -1,39 +1,12 @@
1
1
  import type { Meta, StoryObj } from "@storybook/web-components";
2
2
  import { html } from "lit";
3
3
 
4
- import { ifDefined } from "lit/directives/if-defined.js";
5
- import { when } from "lit/directives/when.js";
6
-
7
4
  import type { USACheckboxElement } from "./checkbox.element.js";
8
5
 
9
6
  // More on how to set up stories at: https://storybook.js.org/docs/writing-stories
10
7
  const meta = {
11
8
  title: "usa-checkbox",
12
9
  tags: ["autodocs"],
13
- render() {
14
- return html`
15
- <usa-checkbox-group>
16
- <legend class="usa-legend">Select any historical figure</legend>
17
-
18
- <usa-checkbox name="historical-figure" value="sojurner-truth" tiled>
19
- Sojourner Truth
20
- <usa-description>This is optional text that can be used to describe the label in more detail.</usa-description>
21
- </usa-checkbox>
22
-
23
- <usa-checkbox name="historical-figure" value="frederick-douglass" tiled>
24
- Frederick Douglass
25
- </usa-checkbox>
26
-
27
- <usa-checkbox name="historical-figure" value="booker-t-washington" tiled>
28
- Booker T. Washington
29
- </usa-checkbox>
30
-
31
- <usa-checkbox name="historical-figure" value="gw-carver" tiled disabled>
32
- George Washington Carver
33
- </usa-checkbox>
34
- </usa-checkbox-group>
35
- `;
36
- },
37
10
  argTypes: {},
38
11
  args: {},
39
12
  } satisfies Meta<USACheckboxElement & { description: string }>;
@@ -0,0 +1,68 @@
1
+ import { inject, injectable } from "@joist/di";
2
+ import { attr, css, element, html, query } from "@joist/element";
3
+
4
+ import { COMBO_BOX_CTX } from "../context.js";
5
+
6
+ declare global {
7
+ interface HTMLElementTagNameMap {
8
+ "usa-combo-box-option": USAComboBoxOptionElement;
9
+ }
10
+ }
11
+
12
+ const template = document.createElement("template");
13
+
14
+ template.innerHTML = /*html*/ `
15
+ <li tabindex="-1" role="option">
16
+ <slot></slot>
17
+ </li>
18
+ `;
19
+
20
+ @injectable({
21
+ name: "usa-combo-box-option-ctx",
22
+ })
23
+ @element({
24
+ tagName: "usa-combo-box-option",
25
+ shadowDom: [
26
+ css`
27
+ :host {
28
+ display: flex;
29
+ align-items: center;
30
+ gap: 0.5rem;
31
+ padding: 0.5rem;
32
+ }
33
+ `,
34
+ html`<slot></slot>`,
35
+ ],
36
+ })
37
+ export class USAComboBoxOptionElement extends HTMLElement {
38
+ @attr()
39
+ accessor value = "";
40
+
41
+ #listItem = template.content.cloneNode(true) as HTMLLIElement;
42
+ #li = query("li", this.#listItem);
43
+ #slot = query("slot", this.#listItem);
44
+ #ctx = inject(COMBO_BOX_CTX);
45
+
46
+ attributeChangedCallback() {
47
+ const value = this.value.split(" ").join("-").toLocaleLowerCase();
48
+
49
+ this.#li().dataset.value = this.value;
50
+ this.#slot().name = value;
51
+
52
+ this.slot = value;
53
+ }
54
+
55
+ connectedCallback() {
56
+ const ctx = this.#ctx();
57
+
58
+ ctx.addOption(this.#li());
59
+ }
60
+
61
+ disconnectedCallback() {
62
+ const ctx = this.#ctx();
63
+
64
+ ctx.removeOption(this.#li());
65
+
66
+ this.#li().remove();
67
+ }
68
+ }
@@ -0,0 +1,222 @@
1
+ import { injectable } from "@joist/di";
2
+ import { css, element, html, listen, query } from "@joist/element";
3
+
4
+ import { COMBO_BOX_CTX, type ComboBoxContainer } from "./context.js";
5
+
6
+ declare global {
7
+ interface HTMLElementTagNameMap {
8
+ "usa-combo-box": USAComboBoxElement;
9
+ }
10
+ }
11
+
12
+ @injectable({
13
+ name: "usa-combo-box-ctx",
14
+ provideSelfAs: [COMBO_BOX_CTX],
15
+ })
16
+ @element({
17
+ tagName: "usa-combo-box",
18
+ shadowDom: [
19
+ css`
20
+ :host {
21
+ --usa-combo-max-height: 12.5em;
22
+
23
+ display: block;
24
+ max-width: 30rem;
25
+ position: relative;
26
+ }
27
+
28
+ ul {
29
+ padding: 0;
30
+ position: absolute;
31
+ bottom: 0;
32
+ left: 0;
33
+ right: 0;
34
+ transform: translateY(100%);
35
+ margin: 0;
36
+ border: 1px solid rgb(92, 92, 92);
37
+ max-height: var(--usa-combo-max-height);
38
+ overflow-y: scroll;
39
+ overflow-x: visible;
40
+ z-index: 1001;
41
+ }
42
+
43
+ ul:empty {
44
+ border: none;
45
+ }
46
+
47
+ ul li {
48
+ background: #ffff;
49
+ list-style: none;
50
+ border-bottom: 1px solid #e6e6e6;
51
+ cursor: pointer;
52
+ display: block;
53
+ }
54
+
55
+ ul li:hover {
56
+ background-color: #f0f0f0;
57
+ }
58
+
59
+ li:focus {
60
+ outline: 0.25rem solid #2491ff;
61
+ outline-offset: -0.25rem;
62
+ }
63
+ `,
64
+ html`
65
+ <slot name="input"></slot>
66
+ <ul tabindex="-1" role="listbox"></ul>
67
+ `,
68
+ ],
69
+ })
70
+ export class USAComboBoxElement
71
+ extends HTMLElement
72
+ implements ComboBoxContainer
73
+ {
74
+ list = query("ul");
75
+ input = query<HTMLInputElement>('[slot="input"]', this);
76
+ currentItemEl: Element | null = null;
77
+ #allListItems = new Set<HTMLLIElement>();
78
+
79
+ listItems() {
80
+ return this.list().querySelectorAll("li");
81
+ }
82
+
83
+ addOption(el: HTMLLIElement) {
84
+ this.#allListItems.add(el);
85
+ }
86
+
87
+ removeOption(el: HTMLLIElement) {
88
+ this.#allListItems.delete(el);
89
+ }
90
+
91
+ @listen("focus", (host) => host.input())
92
+ onFocusIn() {
93
+ this.currentItemEl = null;
94
+
95
+ const list = this.list();
96
+
97
+ const fragment = document.createDocumentFragment();
98
+
99
+ for (const item of this.#allListItems) {
100
+ fragment.append(item);
101
+ }
102
+
103
+ list.replaceChildren(fragment);
104
+ }
105
+
106
+ @listen("input", (host) => host)
107
+ async onInput() {
108
+ const input = this.input();
109
+ const list = this.list();
110
+
111
+ this.currentItemEl = null;
112
+
113
+ const fragment = document.createDocumentFragment();
114
+
115
+ for (const item of this.#allListItems) {
116
+ if (
117
+ item.dataset.value?.toLowerCase().startsWith(input.value.toLowerCase())
118
+ ) {
119
+ fragment.append(item);
120
+ }
121
+ }
122
+
123
+ list.replaceChildren(fragment);
124
+ }
125
+
126
+ @listen("focusout")
127
+ onFocusOut() {
128
+ setTimeout(() => {
129
+ // This needs to be in a timeout so that it runs as part of the next loop.
130
+ // the active element will not be set until after all of the focus and blur events are done
131
+ if (!this.contains(document.activeElement)) {
132
+ this.list({ innerHTML: "" });
133
+ this.currentItemEl = null;
134
+ }
135
+ }, 0);
136
+ }
137
+
138
+ @listen("keydown")
139
+ onArrowDown(e: KeyboardEvent): void {
140
+ if (e.key.toUpperCase() !== "ARROWDOWN") {
141
+ return;
142
+ }
143
+
144
+ e.preventDefault();
145
+
146
+ if (this.currentItemEl === null) {
147
+ // if there is no current item, set the first item as the current item
148
+ const list = this.list();
149
+
150
+ this.currentItemEl = list.firstElementChild;
151
+ } else if (this.currentItemEl.nextSibling) {
152
+ // if there is a current item, set the next item as the current item
153
+ this.currentItemEl = this.currentItemEl.nextElementSibling;
154
+ }
155
+
156
+ if (this.currentItemEl instanceof HTMLElement) {
157
+ this.currentItemEl.focus();
158
+ }
159
+ }
160
+
161
+ @listen("keydown")
162
+ onArrowUp(e: KeyboardEvent): void {
163
+ if (e.key.toUpperCase() !== "ARROWUP") {
164
+ return;
165
+ }
166
+
167
+ e.preventDefault();
168
+
169
+ if (this.currentItemEl?.previousElementSibling) {
170
+ this.currentItemEl = this.currentItemEl.previousElementSibling;
171
+
172
+ if (this.currentItemEl instanceof HTMLElement) {
173
+ this.currentItemEl.focus();
174
+ }
175
+ } else {
176
+ this.input().focus();
177
+ this.currentItemEl = null;
178
+ }
179
+ }
180
+
181
+ @listen("keydown")
182
+ onEnter(e: KeyboardEvent): void {
183
+ if (e.key.toUpperCase() !== "ENTER") {
184
+ return;
185
+ }
186
+
187
+ e.preventDefault();
188
+
189
+ const target = e.target as HTMLElement;
190
+
191
+ this.currentItemEl = null;
192
+
193
+ const value = target.dataset.value || "";
194
+
195
+ this.input({
196
+ value,
197
+ selectionStart: value.length,
198
+ selectionEnd: value.length,
199
+ }).focus();
200
+
201
+ this.list({ innerHTML: "" });
202
+ }
203
+
204
+ @listen("click")
205
+ onClick(e: MouseEvent) {
206
+ if (e.target instanceof HTMLElement) {
207
+ const value = e.target.getAttribute("value");
208
+
209
+ if (value) {
210
+ this.input({
211
+ value,
212
+ selectionStart: value.length,
213
+ selectionEnd: value.length,
214
+ }).focus();
215
+
216
+ this.list({ innerHTML: "" });
217
+
218
+ this.currentItemEl = null;
219
+ }
220
+ }
221
+ }
222
+ }