@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.
- package/LICENSE +21 -0
- package/README.md +25 -0
- package/package.json +89 -0
- package/src/lib/alert/alert-types.ts +33 -0
- package/src/lib/alert/alert.element.ts +105 -0
- package/src/lib/alert/alert.stories.ts +63 -0
- package/src/lib/alert/alert.test.ts +23 -0
- package/src/lib/button/button.element.ts +224 -0
- package/src/lib/button/button.stories.ts +34 -0
- package/src/lib/button/button.test.ts +17 -0
- package/src/lib/checkbox/checkbox.element.ts +166 -0
- package/src/lib/checkbox/checkbox.stories.ts +57 -0
- package/src/lib/checkbox/checkbox.test.ts +47 -0
- package/src/lib/config/config.element.ts +31 -0
- package/src/lib/config/config.test.ts +15 -0
- package/src/lib/define.ts +14 -0
- package/src/lib/description/description.element.ts +22 -0
- package/src/lib/description/description.test.ts +15 -0
- package/src/lib/file-input/file-input-preview.element.ts +121 -0
- package/src/lib/file-input/file-input-preview.test.ts +95 -0
- package/src/lib/file-input/file-input.element.ts +140 -0
- package/src/lib/file-input/file-input.stories.ts +46 -0
- package/src/lib/file-input/file-input.test.ts +47 -0
- package/src/lib/icon/icon-types.ts +263 -0
- package/src/lib/icon/icon.element.ts +65 -0
- package/src/lib/icon/icon.stories.ts +50 -0
- package/src/lib/input/input.element.ts +138 -0
- package/src/lib/input/input.stories.ts +30 -0
- package/src/lib/input/input.test.ts +48 -0
- package/src/lib/input-mask/format.ts +56 -0
- package/src/lib/input-mask/input-mask.element.ts +93 -0
- package/src/lib/input-mask/input-mask.stories.ts +38 -0
- package/src/lib/input-mask/input-mask.test.ts +106 -0
- package/src/lib/input-mask/maskable.element.ts +5 -0
- package/src/lib/link/link.element.ts +62 -0
- package/src/lib/link/link.stories.ts +30 -0
- package/src/lib/radio/radio-option.element.ts +46 -0
- package/src/lib/radio/radio-option.test.ts +20 -0
- package/src/lib/radio/radio.element.ts +152 -0
- package/src/lib/radio/radio.stories.ts +47 -0
- package/src/lib/radio/radio.test.ts +174 -0
- package/src/lib/select/select-option.element.ts +40 -0
- package/src/lib/select/select.element.ts +121 -0
- package/src/lib/select/select.stories.ts +33 -0
- package/src/lib/select/select.test.ts +113 -0
- package/src/lib/tag/tag.element.ts +46 -0
- package/src/lib/tag/tag.stories.ts +31 -0
- package/src/lib/tag/tag.test.ts +15 -0
- package/src/lib.ts +13 -0
- package/target/lib/alert/alert-types.d.ts +7 -0
- package/target/lib/alert/alert-types.js +25 -0
- package/target/lib/alert/alert-types.js.map +1 -0
- package/target/lib/alert/alert.element.d.ts +11 -0
- package/target/lib/alert/alert.element.js +124 -0
- package/target/lib/alert/alert.element.js.map +1 -0
- package/target/lib/alert/alert.stories.d.ts +11 -0
- package/target/lib/alert/alert.stories.js +56 -0
- package/target/lib/alert/alert.stories.js.map +1 -0
- package/target/lib/alert/alert.test.d.ts +1 -0
- package/target/lib/alert/alert.test.js +20 -0
- package/target/lib/alert/alert.test.js.map +1 -0
- package/target/lib/button/button.element.d.ts +17 -0
- package/target/lib/button/button.element.js +259 -0
- package/target/lib/button/button.element.js.map +1 -0
- package/target/lib/button/button.stories.d.ts +12 -0
- package/target/lib/button/button.stories.js +25 -0
- package/target/lib/button/button.stories.js.map +1 -0
- package/target/lib/button/button.test.d.ts +1 -0
- package/target/lib/button/button.test.js +14 -0
- package/target/lib/button/button.test.js.map +1 -0
- package/target/lib/checkbox/checkbox.element.d.ts +16 -0
- package/target/lib/checkbox/checkbox.element.js +205 -0
- package/target/lib/checkbox/checkbox.element.js.map +1 -0
- package/target/lib/checkbox/checkbox.stories.d.ts +31 -0
- package/target/lib/checkbox/checkbox.stories.js +46 -0
- package/target/lib/checkbox/checkbox.stories.js.map +1 -0
- package/target/lib/checkbox/checkbox.test.d.ts +1 -0
- package/target/lib/checkbox/checkbox.test.js +38 -0
- package/target/lib/checkbox/checkbox.test.js.map +1 -0
- package/target/lib/config/config.element.d.ts +8 -0
- package/target/lib/config/config.element.js +57 -0
- package/target/lib/config/config.element.js.map +1 -0
- package/target/lib/config/config.test.d.ts +1 -0
- package/target/lib/config/config.test.js +11 -0
- package/target/lib/config/config.test.js.map +1 -0
- package/target/lib/define.d.ts +14 -0
- package/target/lib/define.js +15 -0
- package/target/lib/define.js.map +1 -0
- package/target/lib/description/description.element.d.ts +7 -0
- package/target/lib/description/description.element.js +34 -0
- package/target/lib/description/description.element.js.map +1 -0
- package/target/lib/description/description.test.d.ts +1 -0
- package/target/lib/description/description.test.js +11 -0
- package/target/lib/description/description.test.js.map +1 -0
- package/target/lib/file-input/file-input-preview.element.d.ts +11 -0
- package/target/lib/file-input/file-input-preview.element.js +136 -0
- package/target/lib/file-input/file-input-preview.element.js.map +1 -0
- package/target/lib/file-input/file-input-preview.test.d.ts +2 -0
- package/target/lib/file-input/file-input-preview.test.js +67 -0
- package/target/lib/file-input/file-input-preview.test.js.map +1 -0
- package/target/lib/file-input/file-input.element.d.ts +18 -0
- package/target/lib/file-input/file-input.element.js +180 -0
- package/target/lib/file-input/file-input.element.js.map +1 -0
- package/target/lib/file-input/file-input.stories.d.ts +12 -0
- package/target/lib/file-input/file-input.stories.js +36 -0
- package/target/lib/file-input/file-input.stories.js.map +1 -0
- package/target/lib/file-input/file-input.test.d.ts +1 -0
- package/target/lib/file-input/file-input.test.js +37 -0
- package/target/lib/file-input/file-input.test.js.map +1 -0
- package/target/lib/icon/icon-types.d.ts +2 -0
- package/target/lib/icon/icon-types.js +262 -0
- package/target/lib/icon/icon-types.js.map +1 -0
- package/target/lib/icon/icon.element.d.ts +12 -0
- package/target/lib/icon/icon.element.js +84 -0
- package/target/lib/icon/icon.element.js.map +1 -0
- package/target/lib/icon/icon.stories.d.ts +12 -0
- package/target/lib/icon/icon.stories.js +39 -0
- package/target/lib/icon/icon.stories.js.map +1 -0
- package/target/lib/input/input.element.d.ts +19 -0
- package/target/lib/input/input.element.js +166 -0
- package/target/lib/input/input.element.js.map +1 -0
- package/target/lib/input/input.stories.d.ts +12 -0
- package/target/lib/input/input.stories.js +23 -0
- package/target/lib/input/input.stories.js.map +1 -0
- package/target/lib/input/input.test.d.ts +1 -0
- package/target/lib/input/input.test.js +38 -0
- package/target/lib/input/input.test.js.map +1 -0
- package/target/lib/input-mask/format.d.ts +15 -0
- package/target/lib/input-mask/format.js +47 -0
- package/target/lib/input-mask/format.js.map +1 -0
- package/target/lib/input-mask/input-mask.element.d.ts +12 -0
- package/target/lib/input-mask/input-mask.element.js +111 -0
- package/target/lib/input-mask/input-mask.element.js.map +1 -0
- package/target/lib/input-mask/input-mask.stories.d.ts +14 -0
- package/target/lib/input-mask/input-mask.stories.js +31 -0
- package/target/lib/input-mask/input-mask.stories.js.map +1 -0
- package/target/lib/input-mask/input-mask.test.d.ts +2 -0
- package/target/lib/input-mask/input-mask.test.js +85 -0
- package/target/lib/input-mask/input-mask.test.js.map +1 -0
- package/target/lib/input-mask/maskable.element.d.ts +5 -0
- package/target/lib/input-mask/maskable.element.js +2 -0
- package/target/lib/input-mask/maskable.element.js.map +1 -0
- package/target/lib/link/link.element.d.ts +13 -0
- package/target/lib/link/link.element.js +98 -0
- package/target/lib/link/link.element.js.map +1 -0
- package/target/lib/link/link.stories.d.ts +16 -0
- package/target/lib/link/link.stories.js +23 -0
- package/target/lib/link/link.stories.js.map +1 -0
- package/target/lib/radio/radio-option.element.d.ts +13 -0
- package/target/lib/radio/radio-option.element.js +63 -0
- package/target/lib/radio/radio-option.element.js.map +1 -0
- package/target/lib/radio/radio-option.test.d.ts +2 -0
- package/target/lib/radio/radio-option.test.js +15 -0
- package/target/lib/radio/radio-option.test.js.map +1 -0
- package/target/lib/radio/radio.element.d.ts +18 -0
- package/target/lib/radio/radio.element.js +177 -0
- package/target/lib/radio/radio.element.js.map +1 -0
- package/target/lib/radio/radio.stories.d.ts +12 -0
- package/target/lib/radio/radio.stories.js +40 -0
- package/target/lib/radio/radio.stories.js.map +1 -0
- package/target/lib/radio/radio.test.d.ts +2 -0
- package/target/lib/radio/radio.test.js +147 -0
- package/target/lib/radio/radio.test.js.map +1 -0
- package/target/lib/select/select-option.element.d.ts +11 -0
- package/target/lib/select/select-option.element.js +58 -0
- package/target/lib/select/select-option.element.js.map +1 -0
- package/target/lib/select/select.element.d.ts +16 -0
- package/target/lib/select/select.element.js +144 -0
- package/target/lib/select/select.element.js.map +1 -0
- package/target/lib/select/select.stories.d.ts +12 -0
- package/target/lib/select/select.stories.js +26 -0
- package/target/lib/select/select.stories.js.map +1 -0
- package/target/lib/select/select.test.d.ts +2 -0
- package/target/lib/select/select.test.js +89 -0
- package/target/lib/select/select.test.js.map +1 -0
- package/target/lib/tag/tag.element.d.ts +10 -0
- package/target/lib/tag/tag.element.js +66 -0
- package/target/lib/tag/tag.element.js.map +1 -0
- package/target/lib/tag/tag.stories.d.ts +19 -0
- package/target/lib/tag/tag.stories.js +25 -0
- package/target/lib/tag/tag.stories.js.map +1 -0
- package/target/lib/tag/tag.test.d.ts +1 -0
- package/target/lib/tag/tag.test.js +11 -0
- package/target/lib/tag/tag.test.js.map +1 -0
- package/target/lib.d.ts +13 -0
- package/target/lib.js +14 -0
- package/target/lib.js.map +1 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { attr, css, element, html, listen, query } from "@joist/element";
|
|
2
|
+
|
|
3
|
+
import type { USARadioOptionElement } from "./radio-option.element.js";
|
|
4
|
+
|
|
5
|
+
declare global {
|
|
6
|
+
interface HTMLElementTagNameMap {
|
|
7
|
+
"usa-radio": USARadioElement;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
@element({
|
|
12
|
+
tagName: "usa-radio",
|
|
13
|
+
shadowDom: [
|
|
14
|
+
css`
|
|
15
|
+
:host {
|
|
16
|
+
display: block;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.radios {
|
|
20
|
+
display: flex;
|
|
21
|
+
flex-direction: column;
|
|
22
|
+
gap: 1rem;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
label {
|
|
26
|
+
display: flex;
|
|
27
|
+
cursor: pointer;
|
|
28
|
+
gap: 0.5rem;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
input {
|
|
32
|
+
position: absolute;
|
|
33
|
+
left: -999em;
|
|
34
|
+
right: auto;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
label::before {
|
|
38
|
+
content: " ";
|
|
39
|
+
display: block;
|
|
40
|
+
left: 0;
|
|
41
|
+
height: 1.25rem;
|
|
42
|
+
border-radius: 99rem;
|
|
43
|
+
width: 1.25rem;
|
|
44
|
+
background: #fff;
|
|
45
|
+
box-shadow: 0 0 0 2px #1b1b1b;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
label:has(input:checked)::before {
|
|
49
|
+
background-color: #005ea2;
|
|
50
|
+
box-shadow:
|
|
51
|
+
0 0 0 2px #005ea2,
|
|
52
|
+
inset 0 0 0 2px #fff;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
label:has(input:focus)::before {
|
|
56
|
+
outline: 0.25rem solid #2491ff;
|
|
57
|
+
outline-offset: 0.25rem;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
:host([tiled]) .radios {
|
|
61
|
+
gap: 0.5rem;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
:host([tiled]) label {
|
|
65
|
+
background-color: #fff;
|
|
66
|
+
border: 2px solid #c9c9c9;
|
|
67
|
+
color: #1b1b1b;
|
|
68
|
+
border-radius: 0.25rem;
|
|
69
|
+
padding: 0.75rem 1rem 0.75rem 0.75rem;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
:host([tiled]) label:has(input:checked) {
|
|
73
|
+
background-color: rgba(0, 94, 162, 0.1);
|
|
74
|
+
border-color: #005ea2;
|
|
75
|
+
}
|
|
76
|
+
`,
|
|
77
|
+
html`
|
|
78
|
+
<slot></slot>
|
|
79
|
+
|
|
80
|
+
<div class="radios"></div>
|
|
81
|
+
`,
|
|
82
|
+
],
|
|
83
|
+
})
|
|
84
|
+
export class USARadioElement extends HTMLElement {
|
|
85
|
+
static formAssociated = true;
|
|
86
|
+
|
|
87
|
+
@attr()
|
|
88
|
+
accessor value = "";
|
|
89
|
+
|
|
90
|
+
@attr()
|
|
91
|
+
accessor name = "";
|
|
92
|
+
|
|
93
|
+
@attr()
|
|
94
|
+
accessor tiled = false;
|
|
95
|
+
|
|
96
|
+
#radios = query(".radios");
|
|
97
|
+
#internals = this.attachInternals();
|
|
98
|
+
|
|
99
|
+
@listen("change")
|
|
100
|
+
onChange(e: Event) {
|
|
101
|
+
if (e.target instanceof HTMLInputElement) {
|
|
102
|
+
if (e.target.checked) {
|
|
103
|
+
this.value = e.target.value;
|
|
104
|
+
this.#internals.setFormValue(e.target.value);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
connectedCallback() {
|
|
110
|
+
if (this.value) {
|
|
111
|
+
this.#internals.setFormValue(this.value);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
attributeChangedCallback() {
|
|
116
|
+
const radios = this.#radios();
|
|
117
|
+
|
|
118
|
+
for (let radio of radios.querySelectorAll("input")) {
|
|
119
|
+
radio.checked = radio.value === this.value;
|
|
120
|
+
radio.name = this.name;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
onOptionAdded(el: USARadioOptionElement) {
|
|
125
|
+
const radios = this.#radios();
|
|
126
|
+
|
|
127
|
+
const radioLabel = document.createElement("label");
|
|
128
|
+
radioLabel.id = el.value;
|
|
129
|
+
|
|
130
|
+
const input = document.createElement("input");
|
|
131
|
+
input.type = "radio";
|
|
132
|
+
input.name = this.name;
|
|
133
|
+
input.value = el.value;
|
|
134
|
+
input.checked = this.value === el.value;
|
|
135
|
+
|
|
136
|
+
const slot = document.createElement("slot");
|
|
137
|
+
slot.name = el.value;
|
|
138
|
+
|
|
139
|
+
radioLabel.append(input, slot);
|
|
140
|
+
|
|
141
|
+
radios.append(radioLabel);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
onOptionRemoved(el: USARadioOptionElement) {
|
|
145
|
+
const radios = this.#radios();
|
|
146
|
+
const option = radios.querySelector(`#${el.value}`);
|
|
147
|
+
|
|
148
|
+
if (option) {
|
|
149
|
+
option.remove();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/web-components";
|
|
2
|
+
import { html } from "lit";
|
|
3
|
+
|
|
4
|
+
import type { USARadioElement } from "./radio.element.js";
|
|
5
|
+
|
|
6
|
+
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories
|
|
7
|
+
const meta = {
|
|
8
|
+
title: "usa-radio",
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
render(args) {
|
|
11
|
+
return html`
|
|
12
|
+
<usa-radio name="historical-figures" value="frederick-douglass" tiled>
|
|
13
|
+
<usa-radio-option value="sojourner-truth">
|
|
14
|
+
Sojourner Truth
|
|
15
|
+
</usa-radio-option>
|
|
16
|
+
|
|
17
|
+
<usa-radio-option value="frederick-douglass">
|
|
18
|
+
Frederick Douglass
|
|
19
|
+
|
|
20
|
+
<usa-description>
|
|
21
|
+
This is optional text that can be used to describe the label in more
|
|
22
|
+
detail.
|
|
23
|
+
</usa-description>
|
|
24
|
+
</usa-radio-option>
|
|
25
|
+
|
|
26
|
+
<usa-radio-option value="booker-t-washington">
|
|
27
|
+
Booker T. Washington
|
|
28
|
+
</usa-radio-option>
|
|
29
|
+
|
|
30
|
+
<usa-radio-option value="george-washington-carver">
|
|
31
|
+
George Washington Carver
|
|
32
|
+
</usa-radio-option>
|
|
33
|
+
</usa-radio>
|
|
34
|
+
`;
|
|
35
|
+
},
|
|
36
|
+
argTypes: {},
|
|
37
|
+
args: {},
|
|
38
|
+
} satisfies Meta<USARadioElement>;
|
|
39
|
+
|
|
40
|
+
export default meta;
|
|
41
|
+
|
|
42
|
+
type Story = StoryObj<USARadioElement>;
|
|
43
|
+
|
|
44
|
+
// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
|
|
45
|
+
export const Primary: Story = {
|
|
46
|
+
args: {},
|
|
47
|
+
};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import "./radio.element.js";
|
|
2
|
+
import "./radio-option.element.js";
|
|
3
|
+
|
|
4
|
+
import { fixture, html, assert } from "@open-wc/testing";
|
|
5
|
+
|
|
6
|
+
import { USARadioElement } from "./radio.element.js";
|
|
7
|
+
|
|
8
|
+
describe("usa-radio", () => {
|
|
9
|
+
it("should be accessible", async () => {
|
|
10
|
+
const radio = await fixture<USARadioElement>(html`
|
|
11
|
+
<usa-radio name="historical-figures" value="frederick-douglass" tiled>
|
|
12
|
+
<usa-radio-option value="sojourner-truth">
|
|
13
|
+
Sojourner Truth
|
|
14
|
+
</usa-radio-option>
|
|
15
|
+
|
|
16
|
+
<usa-radio-option value="frederick-douglass">
|
|
17
|
+
Frederick Douglass
|
|
18
|
+
</usa-radio-option>
|
|
19
|
+
|
|
20
|
+
<usa-radio-option value="booker-t-washington">
|
|
21
|
+
Booker T. Washington
|
|
22
|
+
</usa-radio-option>
|
|
23
|
+
|
|
24
|
+
<usa-radio-option value="george-washington-carver">
|
|
25
|
+
George Washington Carver
|
|
26
|
+
</usa-radio-option>
|
|
27
|
+
</usa-radio>
|
|
28
|
+
`);
|
|
29
|
+
|
|
30
|
+
return assert.isAccessible(radio);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should create local inputs for each option", async () => {
|
|
34
|
+
const form = await fixture<HTMLFormElement>(html`
|
|
35
|
+
<form>
|
|
36
|
+
<usa-radio name="historical-figures" value="frederick-douglass">
|
|
37
|
+
<usa-radio-option value="sojourner-truth">
|
|
38
|
+
Sojourner Truth
|
|
39
|
+
</usa-radio-option>
|
|
40
|
+
|
|
41
|
+
<usa-radio-option value="frederick-douglass">
|
|
42
|
+
Frederick Douglass
|
|
43
|
+
</usa-radio-option>
|
|
44
|
+
|
|
45
|
+
<usa-radio-option value="booker-t-washington">
|
|
46
|
+
Booker T. Washington
|
|
47
|
+
</usa-radio-option>
|
|
48
|
+
|
|
49
|
+
<usa-radio-option value="george-washington-carver">
|
|
50
|
+
George Washington Carver
|
|
51
|
+
</usa-radio-option>
|
|
52
|
+
</usa-radio>
|
|
53
|
+
</form>
|
|
54
|
+
`);
|
|
55
|
+
|
|
56
|
+
const nativeInputs = form
|
|
57
|
+
.querySelector("usa-radio")!
|
|
58
|
+
.shadowRoot!.querySelectorAll("input");
|
|
59
|
+
|
|
60
|
+
assert.deepEqual(
|
|
61
|
+
Array.from(nativeInputs).map((input) => input.value),
|
|
62
|
+
[
|
|
63
|
+
"sojourner-truth",
|
|
64
|
+
"frederick-douglass",
|
|
65
|
+
"booker-t-washington",
|
|
66
|
+
"george-washington-carver",
|
|
67
|
+
]
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("should remove inputs when options are removed", async () => {
|
|
72
|
+
const form = await fixture<HTMLFormElement>(html`
|
|
73
|
+
<form>
|
|
74
|
+
<usa-radio name="historical-figures" value="frederick-douglass">
|
|
75
|
+
<usa-radio-option value="sojourner-truth">
|
|
76
|
+
Sojourner Truth
|
|
77
|
+
</usa-radio-option>
|
|
78
|
+
|
|
79
|
+
<usa-radio-option value="frederick-douglass">
|
|
80
|
+
Frederick Douglass
|
|
81
|
+
</usa-radio-option>
|
|
82
|
+
|
|
83
|
+
<usa-radio-option value="booker-t-washington">
|
|
84
|
+
Booker T. Washington
|
|
85
|
+
</usa-radio-option>
|
|
86
|
+
|
|
87
|
+
<usa-radio-option value="george-washington-carver">
|
|
88
|
+
George Washington Carver
|
|
89
|
+
</usa-radio-option>
|
|
90
|
+
</usa-radio>
|
|
91
|
+
</form>
|
|
92
|
+
`);
|
|
93
|
+
|
|
94
|
+
const options = form.querySelectorAll("usa-radio-option");
|
|
95
|
+
|
|
96
|
+
options[2].remove();
|
|
97
|
+
|
|
98
|
+
const nativeInputs = form
|
|
99
|
+
.querySelector("usa-radio")!
|
|
100
|
+
.shadowRoot!.querySelectorAll("input");
|
|
101
|
+
|
|
102
|
+
assert.deepEqual(
|
|
103
|
+
Array.from(nativeInputs).map((input) => input.value),
|
|
104
|
+
["sojourner-truth", "frederick-douglass", "george-washington-carver"]
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("should submit form with default values", async () => {
|
|
109
|
+
const form = await fixture<HTMLFormElement>(html`
|
|
110
|
+
<form>
|
|
111
|
+
<usa-radio name="historical-figures" value="frederick-douglass">
|
|
112
|
+
<usa-radio-option value="sojourner-truth">
|
|
113
|
+
Sojourner Truth
|
|
114
|
+
</usa-radio-option>
|
|
115
|
+
|
|
116
|
+
<usa-radio-option value="frederick-douglass">
|
|
117
|
+
Frederick Douglass
|
|
118
|
+
</usa-radio-option>
|
|
119
|
+
|
|
120
|
+
<usa-radio-option value="booker-t-washington">
|
|
121
|
+
Booker T. Washington
|
|
122
|
+
</usa-radio-option>
|
|
123
|
+
|
|
124
|
+
<usa-radio-option value="george-washington-carver">
|
|
125
|
+
George Washington Carver
|
|
126
|
+
</usa-radio-option>
|
|
127
|
+
</usa-radio>
|
|
128
|
+
</form>
|
|
129
|
+
`);
|
|
130
|
+
|
|
131
|
+
const value = new FormData(form);
|
|
132
|
+
|
|
133
|
+
const nativeInputs = form
|
|
134
|
+
.querySelector("usa-radio")!
|
|
135
|
+
.shadowRoot!.querySelectorAll("input");
|
|
136
|
+
|
|
137
|
+
assert.equal(nativeInputs[1].checked, true);
|
|
138
|
+
assert.equal(value.get("historical-figures"), "frederick-douglass");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("should submit form with changed value", async () => {
|
|
142
|
+
const form = await fixture<HTMLFormElement>(html`
|
|
143
|
+
<form>
|
|
144
|
+
<usa-radio name="historical-figures" value="frederick-douglass">
|
|
145
|
+
<usa-radio-option value="sojourner-truth">
|
|
146
|
+
Sojourner Truth
|
|
147
|
+
</usa-radio-option>
|
|
148
|
+
|
|
149
|
+
<usa-radio-option value="frederick-douglass">
|
|
150
|
+
Frederick Douglass
|
|
151
|
+
</usa-radio-option>
|
|
152
|
+
|
|
153
|
+
<usa-radio-option value="booker-t-washington">
|
|
154
|
+
Booker T. Washington
|
|
155
|
+
</usa-radio-option>
|
|
156
|
+
|
|
157
|
+
<usa-radio-option value="george-washington-carver">
|
|
158
|
+
George Washington Carver
|
|
159
|
+
</usa-radio-option>
|
|
160
|
+
</usa-radio>
|
|
161
|
+
</form>
|
|
162
|
+
`);
|
|
163
|
+
|
|
164
|
+
const nativeInputs = form
|
|
165
|
+
.querySelector("usa-radio")!
|
|
166
|
+
.shadowRoot!.querySelectorAll("input");
|
|
167
|
+
|
|
168
|
+
nativeInputs[3].click();
|
|
169
|
+
|
|
170
|
+
const value = new FormData(form);
|
|
171
|
+
|
|
172
|
+
assert.equal(value.get("historical-figures"), "george-washington-carver");
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { attr, css, element } from "@joist/element";
|
|
2
|
+
|
|
3
|
+
import { USASelectElement } from "./select.element.js";
|
|
4
|
+
|
|
5
|
+
declare global {
|
|
6
|
+
interface HTMLElementTagNameMap {
|
|
7
|
+
"usa-select-option": USASelecOptionElement;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
@element({
|
|
12
|
+
tagName: "usa-select-option",
|
|
13
|
+
shadowDom: [
|
|
14
|
+
css`
|
|
15
|
+
:host {
|
|
16
|
+
display: none;
|
|
17
|
+
}
|
|
18
|
+
`,
|
|
19
|
+
],
|
|
20
|
+
})
|
|
21
|
+
export class USASelecOptionElement extends HTMLElement {
|
|
22
|
+
@attr()
|
|
23
|
+
accessor value = "";
|
|
24
|
+
|
|
25
|
+
#parent: USASelectElement | null = null;
|
|
26
|
+
|
|
27
|
+
connectedCallback() {
|
|
28
|
+
if (this.parentElement instanceof USASelectElement) {
|
|
29
|
+
this.#parent = this.parentElement;
|
|
30
|
+
|
|
31
|
+
this.parentElement.onOptionAdded(this);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
disconnectedCallback() {
|
|
36
|
+
if (this.#parent) {
|
|
37
|
+
this.#parent.onOptionRemoved(this);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { attr, css, element, html, listen, query } from "@joist/element";
|
|
2
|
+
|
|
3
|
+
import type { USASelecOptionElement } from "./select-option.element.js";
|
|
4
|
+
|
|
5
|
+
declare global {
|
|
6
|
+
interface HTMLElementTagNameMap {
|
|
7
|
+
"usa-select": USASelectElement;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
@element({
|
|
12
|
+
tagName: "usa-select",
|
|
13
|
+
shadowDom: [
|
|
14
|
+
css`
|
|
15
|
+
:host {
|
|
16
|
+
display: block;
|
|
17
|
+
font-family:
|
|
18
|
+
Source Sans Pro Web,
|
|
19
|
+
Helvetica Neue,
|
|
20
|
+
Helvetica,
|
|
21
|
+
Roboto,
|
|
22
|
+
Arial,
|
|
23
|
+
sans-serif;
|
|
24
|
+
line-height: 1.3;
|
|
25
|
+
position: relative;
|
|
26
|
+
width: 100%;
|
|
27
|
+
max-width: 30rem;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
select {
|
|
31
|
+
font-size: 1.06rem;
|
|
32
|
+
appearance: none;
|
|
33
|
+
border-width: 1px;
|
|
34
|
+
border-color: #5c5c5c;
|
|
35
|
+
border-style: solid;
|
|
36
|
+
border-radius: 0;
|
|
37
|
+
color: #1b1b1b;
|
|
38
|
+
display: block;
|
|
39
|
+
height: 2.5rem;
|
|
40
|
+
margin-top: 0.5rem;
|
|
41
|
+
padding: 0.5rem;
|
|
42
|
+
width: 100%;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
select:not(:disabled):focus {
|
|
46
|
+
outline: 0.25rem solid #2491ff;
|
|
47
|
+
outline-offset: 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
usa-icon {
|
|
51
|
+
position: absolute;
|
|
52
|
+
right: 0.5rem;
|
|
53
|
+
bottom: 12%;
|
|
54
|
+
height: 1.5rem;
|
|
55
|
+
width: 1.5rem;
|
|
56
|
+
}
|
|
57
|
+
`,
|
|
58
|
+
html`
|
|
59
|
+
<usa-icon icon="unfold_more"></usa-icon>
|
|
60
|
+
|
|
61
|
+
<label>
|
|
62
|
+
<div class="label">
|
|
63
|
+
<slot></slot>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<select></select>
|
|
67
|
+
</label>
|
|
68
|
+
`,
|
|
69
|
+
],
|
|
70
|
+
})
|
|
71
|
+
export class USASelectElement extends HTMLElement {
|
|
72
|
+
static formAssociated = true;
|
|
73
|
+
|
|
74
|
+
@attr()
|
|
75
|
+
accessor value = "";
|
|
76
|
+
|
|
77
|
+
@attr()
|
|
78
|
+
accessor name = "";
|
|
79
|
+
|
|
80
|
+
#select = query("select");
|
|
81
|
+
#internals = this.attachInternals();
|
|
82
|
+
|
|
83
|
+
connectedCallback() {
|
|
84
|
+
const select = this.#select();
|
|
85
|
+
select.value = this.value;
|
|
86
|
+
select.name = this.name;
|
|
87
|
+
|
|
88
|
+
this.#internals.setFormValue(this.value);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@listen("change")
|
|
92
|
+
onSelectChange() {
|
|
93
|
+
const select = this.#select();
|
|
94
|
+
|
|
95
|
+
this.#internals.setFormValue(select.value);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
onOptionAdded(el: USASelecOptionElement) {
|
|
99
|
+
const option = document.createElement("option");
|
|
100
|
+
option.value = el.value;
|
|
101
|
+
option.innerHTML = el.innerHTML;
|
|
102
|
+
option.id = this.#createId(el.value);
|
|
103
|
+
|
|
104
|
+
const select = this.#select();
|
|
105
|
+
|
|
106
|
+
select.append(option);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
onOptionRemoved(el: USASelecOptionElement) {
|
|
110
|
+
const select = this.#select();
|
|
111
|
+
const option = select.querySelector(`#${this.#createId(el.value)}`);
|
|
112
|
+
|
|
113
|
+
if (option) {
|
|
114
|
+
option.remove();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
#createId(val: string) {
|
|
119
|
+
return val.split(" ").join("-").toLowerCase();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/web-components";
|
|
2
|
+
import { html } from "lit";
|
|
3
|
+
|
|
4
|
+
import type { USASelectElement } from "./select.element.js";
|
|
5
|
+
|
|
6
|
+
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories
|
|
7
|
+
const meta = {
|
|
8
|
+
title: "usa-select",
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
render(args) {
|
|
11
|
+
return html`
|
|
12
|
+
<usa-select>
|
|
13
|
+
Hello World
|
|
14
|
+
|
|
15
|
+
<usa-select-option value="first">first</usa-select-option>
|
|
16
|
+
<usa-select-option value="second">second</usa-select-option>
|
|
17
|
+
<usa-select-option value="third">third</usa-select-option>
|
|
18
|
+
<usa-select-option value="fourth">fourth</usa-select-option>
|
|
19
|
+
</usa-select>
|
|
20
|
+
`;
|
|
21
|
+
},
|
|
22
|
+
argTypes: {},
|
|
23
|
+
args: {},
|
|
24
|
+
} satisfies Meta<USASelectElement>;
|
|
25
|
+
|
|
26
|
+
export default meta;
|
|
27
|
+
|
|
28
|
+
type Story = StoryObj<USASelectElement>;
|
|
29
|
+
|
|
30
|
+
// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
|
|
31
|
+
export const Primary: Story = {
|
|
32
|
+
args: {},
|
|
33
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import "./select.element.js";
|
|
2
|
+
import "./select-option.element.js";
|
|
3
|
+
|
|
4
|
+
import { fixture, html, assert } from "@open-wc/testing";
|
|
5
|
+
import { fireEvent } from "@noctuatech-uswds/testing";
|
|
6
|
+
|
|
7
|
+
describe("usa-select", () => {
|
|
8
|
+
it("should be accessible", async () => {
|
|
9
|
+
const el = await fixture<HTMLFormElement>(html`
|
|
10
|
+
<usa-select name="example">
|
|
11
|
+
Hello World
|
|
12
|
+
|
|
13
|
+
<usa-select-option value="first">First</usa-select-option>
|
|
14
|
+
<usa-select-option value="second">Second</usa-select-option>
|
|
15
|
+
<usa-select-option value="third">Third</usa-select-option>
|
|
16
|
+
</usa-select>
|
|
17
|
+
`);
|
|
18
|
+
|
|
19
|
+
return assert.isAccessible(el);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should create local select options", async () => {
|
|
23
|
+
const form = await fixture<HTMLFormElement>(html`
|
|
24
|
+
<form>
|
|
25
|
+
<usa-select name="example">
|
|
26
|
+
Hello World
|
|
27
|
+
|
|
28
|
+
<usa-select-option value="first">First</usa-select-option>
|
|
29
|
+
<usa-select-option value="second">Second</usa-select-option>
|
|
30
|
+
<usa-select-option value="third">Third</usa-select-option>
|
|
31
|
+
</usa-select>
|
|
32
|
+
</form>
|
|
33
|
+
`);
|
|
34
|
+
|
|
35
|
+
const nativeInputs = form
|
|
36
|
+
.querySelector("usa-select")!
|
|
37
|
+
.shadowRoot!.querySelectorAll("option");
|
|
38
|
+
|
|
39
|
+
assert.deepEqual(
|
|
40
|
+
Array.from(nativeInputs).map((input) => input.value),
|
|
41
|
+
["first", "second", "third"]
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("should remove select options when options are removed", async () => {
|
|
46
|
+
const form = await fixture<HTMLFormElement>(html`
|
|
47
|
+
<form>
|
|
48
|
+
<usa-select name="example">
|
|
49
|
+
Hello World
|
|
50
|
+
|
|
51
|
+
<usa-select-option value="first">First</usa-select-option>
|
|
52
|
+
<usa-select-option value="second">Second</usa-select-option>
|
|
53
|
+
<usa-select-option value="third">Third</usa-select-option>
|
|
54
|
+
</usa-select>
|
|
55
|
+
</form>
|
|
56
|
+
`);
|
|
57
|
+
|
|
58
|
+
const options = form.querySelectorAll("usa-select-option");
|
|
59
|
+
|
|
60
|
+
options[1].remove();
|
|
61
|
+
|
|
62
|
+
const nativeInputs = form
|
|
63
|
+
.querySelector("usa-select")!
|
|
64
|
+
.shadowRoot!.querySelectorAll("option");
|
|
65
|
+
|
|
66
|
+
assert.deepEqual(
|
|
67
|
+
Array.from(nativeInputs).map((input) => input.value),
|
|
68
|
+
["first", "third"]
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should submit form with default values", async () => {
|
|
73
|
+
const form = await fixture<HTMLFormElement>(html`
|
|
74
|
+
<form>
|
|
75
|
+
<usa-select name="example" value="second">
|
|
76
|
+
Hello World
|
|
77
|
+
|
|
78
|
+
<usa-select-option value="first">First</usa-select-option>
|
|
79
|
+
<usa-select-option value="second">Second</usa-select-option>
|
|
80
|
+
<usa-select-option value="third">Third</usa-select-option>
|
|
81
|
+
</usa-select>
|
|
82
|
+
</form>
|
|
83
|
+
`);
|
|
84
|
+
|
|
85
|
+
const value = new FormData(form);
|
|
86
|
+
|
|
87
|
+
assert.equal(value.get("example"), "second");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("should update form value as select value changed", async () => {
|
|
91
|
+
const form = await fixture<HTMLFormElement>(html`
|
|
92
|
+
<form>
|
|
93
|
+
<usa-select name="example" value="second">
|
|
94
|
+
Hello World
|
|
95
|
+
|
|
96
|
+
<usa-select-option value="first">First</usa-select-option>
|
|
97
|
+
<usa-select-option value="second">Second</usa-select-option>
|
|
98
|
+
<usa-select-option value="third">Third</usa-select-option>
|
|
99
|
+
</usa-select>
|
|
100
|
+
</form>
|
|
101
|
+
`);
|
|
102
|
+
|
|
103
|
+
const select = form.querySelector("usa-select")!;
|
|
104
|
+
const nativeSelect = select.shadowRoot!.querySelector("select")!;
|
|
105
|
+
nativeSelect.value = "third";
|
|
106
|
+
|
|
107
|
+
await fireEvent.change(nativeSelect, { bubbles: true });
|
|
108
|
+
|
|
109
|
+
const value = new FormData(form);
|
|
110
|
+
|
|
111
|
+
assert.equal(value.get("example"), "third");
|
|
112
|
+
});
|
|
113
|
+
});
|