@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,138 @@
|
|
|
1
|
+
import { attr, css, element, html, listen, query } from "@joist/element";
|
|
2
|
+
import { effect, observe } from "@joist/observable";
|
|
3
|
+
|
|
4
|
+
import { MaskableElement } from "../input-mask/maskable.element.js";
|
|
5
|
+
|
|
6
|
+
declare global {
|
|
7
|
+
interface HTMLElementTagNameMap {
|
|
8
|
+
"usa-input": USATextInputElement;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@element({
|
|
13
|
+
tagName: "usa-input",
|
|
14
|
+
shadowDom: [
|
|
15
|
+
css`
|
|
16
|
+
* {
|
|
17
|
+
box-sizing: border-box;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
:host {
|
|
21
|
+
font-family:
|
|
22
|
+
Source Sans Pro Web,
|
|
23
|
+
Helvetica Neue,
|
|
24
|
+
Helvetica,
|
|
25
|
+
Roboto,
|
|
26
|
+
Arial,
|
|
27
|
+
sans-serif;
|
|
28
|
+
font-size: 1.06rem;
|
|
29
|
+
line-height: 1.3;
|
|
30
|
+
display: block;
|
|
31
|
+
font-weight: 400;
|
|
32
|
+
max-width: 30rem;
|
|
33
|
+
margin-bottom: 1.5rem;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
input {
|
|
37
|
+
border-width: 1px;
|
|
38
|
+
border-color: #5c5c5c;
|
|
39
|
+
border-style: solid;
|
|
40
|
+
border-radius: 0;
|
|
41
|
+
color: #1b1b1b;
|
|
42
|
+
display: block;
|
|
43
|
+
height: 2.5rem;
|
|
44
|
+
line-height: 1.3;
|
|
45
|
+
font-size: 1.06rem;
|
|
46
|
+
margin-top: 0.5rem;
|
|
47
|
+
padding: 0.5rem;
|
|
48
|
+
width: 100%;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
input:not(:disabled):focus {
|
|
52
|
+
outline: 0.25rem solid #2491ff;
|
|
53
|
+
outline-offset: 0;
|
|
54
|
+
}
|
|
55
|
+
`,
|
|
56
|
+
html`
|
|
57
|
+
<label>
|
|
58
|
+
<slot></slot>
|
|
59
|
+
|
|
60
|
+
<input />
|
|
61
|
+
</label>
|
|
62
|
+
`,
|
|
63
|
+
],
|
|
64
|
+
})
|
|
65
|
+
export class USATextInputElement
|
|
66
|
+
extends HTMLElement
|
|
67
|
+
implements MaskableElement
|
|
68
|
+
{
|
|
69
|
+
static formAssociated = true;
|
|
70
|
+
|
|
71
|
+
@attr()
|
|
72
|
+
accessor name = "";
|
|
73
|
+
|
|
74
|
+
@attr()
|
|
75
|
+
accessor autocomplete: AutoFill = "on";
|
|
76
|
+
|
|
77
|
+
@attr()
|
|
78
|
+
accessor placeholder = "";
|
|
79
|
+
|
|
80
|
+
@attr({
|
|
81
|
+
reflect: false,
|
|
82
|
+
})
|
|
83
|
+
@observe()
|
|
84
|
+
accessor value = "";
|
|
85
|
+
|
|
86
|
+
get selectionStart() {
|
|
87
|
+
const { selectionStart } = this.#input();
|
|
88
|
+
|
|
89
|
+
return selectionStart;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
#internals = this.attachInternals();
|
|
93
|
+
#input = query("input");
|
|
94
|
+
|
|
95
|
+
setSelectionRange(start: number, end: number) {
|
|
96
|
+
const input = this.#input();
|
|
97
|
+
|
|
98
|
+
input.setSelectionRange(start, end);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@effect()
|
|
102
|
+
onChange() {
|
|
103
|
+
const input = this.#input();
|
|
104
|
+
input.value = this.value;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@listen("input")
|
|
108
|
+
onInputChange() {
|
|
109
|
+
const input = this.#input();
|
|
110
|
+
|
|
111
|
+
this.#internals.setFormValue(input.value);
|
|
112
|
+
|
|
113
|
+
this.value = input.value;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
attributeChangedCallback(attr: string) {
|
|
117
|
+
const input = this.#input();
|
|
118
|
+
|
|
119
|
+
switch (attr) {
|
|
120
|
+
case "autocomplete":
|
|
121
|
+
input.autocomplete = this.autocomplete;
|
|
122
|
+
break;
|
|
123
|
+
|
|
124
|
+
case "placeholder":
|
|
125
|
+
input.placeholder = this.placeholder;
|
|
126
|
+
break;
|
|
127
|
+
|
|
128
|
+
case "name":
|
|
129
|
+
input.name = this.name;
|
|
130
|
+
break;
|
|
131
|
+
|
|
132
|
+
case "value":
|
|
133
|
+
input.value = this.value;
|
|
134
|
+
this.#internals.setFormValue(this.value);
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/web-components";
|
|
2
|
+
import { html } from "lit";
|
|
3
|
+
|
|
4
|
+
import type { USATextInputElement } from "./input.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-input",
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
render() {
|
|
11
|
+
return html`
|
|
12
|
+
<form>
|
|
13
|
+
<usa-input name="fname" value="Danny" autocomplete="off" foo="test">
|
|
14
|
+
First name
|
|
15
|
+
</usa-input>
|
|
16
|
+
|
|
17
|
+
<usa-button type="submit">Submit</usa-button>
|
|
18
|
+
</form>
|
|
19
|
+
`;
|
|
20
|
+
},
|
|
21
|
+
argTypes: {},
|
|
22
|
+
args: {},
|
|
23
|
+
} satisfies Meta<USATextInputElement>;
|
|
24
|
+
|
|
25
|
+
export default meta;
|
|
26
|
+
|
|
27
|
+
type Story = StoryObj<USATextInputElement>;
|
|
28
|
+
|
|
29
|
+
// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
|
|
30
|
+
export const Primary: Story = {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import "./input.element.js";
|
|
2
|
+
|
|
3
|
+
import { fixture, html, assert } from "@open-wc/testing";
|
|
4
|
+
import { fireEvent } from "@noctuatech-uswds/testing";
|
|
5
|
+
|
|
6
|
+
describe("usa-input", () => {
|
|
7
|
+
it("should be accessible", async () => {
|
|
8
|
+
const form = await fixture<HTMLFormElement>(html`
|
|
9
|
+
<usa-input name="fname" value="Foo">Hello World</usa-input>
|
|
10
|
+
`);
|
|
11
|
+
|
|
12
|
+
return assert.isAccessible(form);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("should submit form with default values", async () => {
|
|
16
|
+
const form = await fixture<HTMLFormElement>(html`
|
|
17
|
+
<form>
|
|
18
|
+
<usa-input name="fname" value="Foo">Hello World</usa-input>
|
|
19
|
+
|
|
20
|
+
<button>Submit</button>
|
|
21
|
+
</form>
|
|
22
|
+
`);
|
|
23
|
+
|
|
24
|
+
const value = new FormData(form);
|
|
25
|
+
|
|
26
|
+
assert.equal(value.get("fname"), "Foo");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should update form value as input value changed", async () => {
|
|
30
|
+
const form = await fixture<HTMLFormElement>(html`
|
|
31
|
+
<form>
|
|
32
|
+
<usa-input name="fname">Hello World</usa-input>
|
|
33
|
+
|
|
34
|
+
<button>Submit</button>
|
|
35
|
+
</form>
|
|
36
|
+
`);
|
|
37
|
+
|
|
38
|
+
const input = form.querySelector("usa-input")!;
|
|
39
|
+
const nativeInput = input.shadowRoot!.querySelector("input")!;
|
|
40
|
+
nativeInput.value = "Bar";
|
|
41
|
+
|
|
42
|
+
await fireEvent.input(nativeInput, { bubbles: true });
|
|
43
|
+
|
|
44
|
+
const value = new FormData(form);
|
|
45
|
+
|
|
46
|
+
assert.equal(value.get("fname"), "Bar");
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export enum PatternChar {
|
|
2
|
+
Any = "*",
|
|
3
|
+
Number = "9",
|
|
4
|
+
Letter = "A",
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const PATTERN_CHARS = Object.values(PatternChar);
|
|
8
|
+
|
|
9
|
+
export const REG_EXPS = {
|
|
10
|
+
Letters: /^[a-z]/i,
|
|
11
|
+
Numbers: /^[0-9]/i,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export interface FormattedResult {
|
|
15
|
+
raw: string;
|
|
16
|
+
formatted: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function format(value: string, pattern: string): FormattedResult {
|
|
20
|
+
const raw = value.replace(/[^a-z0-9]/gi, ""); // remove all special chars
|
|
21
|
+
const chars = raw.split("");
|
|
22
|
+
|
|
23
|
+
let count = 0;
|
|
24
|
+
let formatted = "";
|
|
25
|
+
|
|
26
|
+
for (var i = 0; i < pattern.length; i++) {
|
|
27
|
+
const patternChar = pattern[i];
|
|
28
|
+
const char = chars[count];
|
|
29
|
+
|
|
30
|
+
if (char && patternChar) {
|
|
31
|
+
if (patternChar === PatternChar.Any) {
|
|
32
|
+
// Any letter or number
|
|
33
|
+
formatted += char;
|
|
34
|
+
count++;
|
|
35
|
+
} else if (patternChar === PatternChar.Number) {
|
|
36
|
+
// Numbers only
|
|
37
|
+
if (/^[0-9]/i.test(char)) {
|
|
38
|
+
formatted += char;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
count++;
|
|
42
|
+
} else if (patternChar === PatternChar.Letter) {
|
|
43
|
+
// Letters only
|
|
44
|
+
if (/^[a-z]/i.test(char)) {
|
|
45
|
+
formatted += char;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
count++;
|
|
49
|
+
} else {
|
|
50
|
+
formatted += patternChar;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { raw, formatted };
|
|
56
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { attr, css, element, html, listen } from "@joist/element";
|
|
2
|
+
|
|
3
|
+
import { MaskableElement } from "./maskable.element.js";
|
|
4
|
+
import { format, PATTERN_CHARS, PatternChar, REG_EXPS } from "./format.js";
|
|
5
|
+
|
|
6
|
+
declare global {
|
|
7
|
+
interface HTMLElementTagNameMap {
|
|
8
|
+
"usa-input-mask": USAInputMaskElement;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@element({
|
|
13
|
+
tagName: "usa-input-mask",
|
|
14
|
+
shadowDom: [
|
|
15
|
+
css`
|
|
16
|
+
:host {
|
|
17
|
+
display: contents;
|
|
18
|
+
}
|
|
19
|
+
`,
|
|
20
|
+
html`<slot></slot>`,
|
|
21
|
+
],
|
|
22
|
+
})
|
|
23
|
+
export class USAInputMaskElement extends HTMLElement {
|
|
24
|
+
@attr()
|
|
25
|
+
accessor mask = "";
|
|
26
|
+
|
|
27
|
+
connectedCallback() {
|
|
28
|
+
for (let input of this.querySelectorAll<MaskableElement>("[mask]")) {
|
|
29
|
+
const { formatted } = format(input.value, this.#getMaskFor(input));
|
|
30
|
+
|
|
31
|
+
input.value = formatted;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@listen("input")
|
|
36
|
+
async onInput(e: Event) {
|
|
37
|
+
const input = e.target as MaskableElement;
|
|
38
|
+
const selectionStart = input.selectionStart || 0;
|
|
39
|
+
const prev = input.value;
|
|
40
|
+
const mask = this.#getMaskFor(input);
|
|
41
|
+
|
|
42
|
+
const { formatted } = format(input.value, mask);
|
|
43
|
+
|
|
44
|
+
input.value = formatted;
|
|
45
|
+
|
|
46
|
+
const offset = input.value.length - prev.length;
|
|
47
|
+
const maskChar = mask[selectionStart - 1] as PatternChar | undefined;
|
|
48
|
+
|
|
49
|
+
// This is a hack to make sure that changes are propagated appropriately
|
|
50
|
+
await Promise.resolve();
|
|
51
|
+
|
|
52
|
+
// check if the current value is not a space for characters and has an offset greater then 0
|
|
53
|
+
if (maskChar && !PATTERN_CHARS.includes(maskChar) && offset > 0) {
|
|
54
|
+
input.setSelectionRange(selectionStart + offset, selectionStart + offset);
|
|
55
|
+
} else {
|
|
56
|
+
input.setSelectionRange(selectionStart, selectionStart);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (prev !== input.value) {
|
|
60
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@listen("keydown")
|
|
65
|
+
onKeyDown(e: KeyboardEvent) {
|
|
66
|
+
const input = e.target as MaskableElement;
|
|
67
|
+
const mask = this.#getMaskFor(input);
|
|
68
|
+
const patternChar = mask[input.selectionStart || 0];
|
|
69
|
+
|
|
70
|
+
if (e.key.length === 1 && /^[a-z0-9]/i.test(e.key)) {
|
|
71
|
+
// check that the key is a single character and that it is a letter or number
|
|
72
|
+
|
|
73
|
+
if (input.value.length >= mask.length) {
|
|
74
|
+
// prevent default once value is the same as the mask length
|
|
75
|
+
e.preventDefault();
|
|
76
|
+
} else if (patternChar === PatternChar.Number) {
|
|
77
|
+
if (!REG_EXPS.Numbers.test(e.key)) {
|
|
78
|
+
// if pattern char specifies number and is not
|
|
79
|
+
e.preventDefault();
|
|
80
|
+
}
|
|
81
|
+
} else if (patternChar === PatternChar.Letter) {
|
|
82
|
+
if (!REG_EXPS.Letters.test(e.key)) {
|
|
83
|
+
// if pattern char specifies letter and is not
|
|
84
|
+
e.preventDefault();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#getMaskFor(input: MaskableElement) {
|
|
91
|
+
return this.mask || input.getAttribute("mask") || "";
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/web-components";
|
|
2
|
+
import { html } from "lit";
|
|
3
|
+
|
|
4
|
+
import type { USAInputMaskElement } from "./input-mask.element.js";
|
|
5
|
+
|
|
6
|
+
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories
|
|
7
|
+
const meta = {
|
|
8
|
+
title: "input-mask",
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
render(args) {
|
|
11
|
+
return html`
|
|
12
|
+
<usa-input-mask>
|
|
13
|
+
<usa-input
|
|
14
|
+
name="phone"
|
|
15
|
+
placeholder=${args.mask}
|
|
16
|
+
autocomplete="off"
|
|
17
|
+
mask=${args.mask}
|
|
18
|
+
value="3042616138"
|
|
19
|
+
>
|
|
20
|
+
Phone:
|
|
21
|
+
</usa-input>
|
|
22
|
+
</usa-input-mask>
|
|
23
|
+
`;
|
|
24
|
+
},
|
|
25
|
+
argTypes: {},
|
|
26
|
+
args: {
|
|
27
|
+
mask: "(999) 999-9999",
|
|
28
|
+
},
|
|
29
|
+
} satisfies Meta<USAInputMaskElement>;
|
|
30
|
+
|
|
31
|
+
export default meta;
|
|
32
|
+
|
|
33
|
+
type Story = StoryObj<USAInputMaskElement>;
|
|
34
|
+
|
|
35
|
+
// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
|
|
36
|
+
export const Primary: Story = {
|
|
37
|
+
args: {},
|
|
38
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import "./input-mask.element.js";
|
|
2
|
+
import "../input/input.element.js";
|
|
3
|
+
|
|
4
|
+
import { assert, fixture, html } from "@open-wc/testing";
|
|
5
|
+
|
|
6
|
+
import { format } from "./format.js";
|
|
7
|
+
import { USAInputMaskElement } from "./input-mask.element.js";
|
|
8
|
+
|
|
9
|
+
describe("format", () => {
|
|
10
|
+
it("should retrn the correct raw value", () => {
|
|
11
|
+
assert.deepEqual(format("(123) 456 7890", "(***) ***-****"), {
|
|
12
|
+
raw: "1234567890",
|
|
13
|
+
formatted: "(123) 456-7890",
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("should return a formatted phone number (***) ***-****", () => {
|
|
18
|
+
assert.deepEqual(format("1234567890", "(***) ***-****"), {
|
|
19
|
+
raw: "1234567890",
|
|
20
|
+
formatted: "(123) 456-7890",
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("should return a formatted phone number ***-***-****", () => {
|
|
25
|
+
assert.deepEqual(format("1234567890", "***-***-****"), {
|
|
26
|
+
raw: "1234567890",
|
|
27
|
+
formatted: "123-456-7890",
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("should only allow numbers", () => {
|
|
32
|
+
assert.deepEqual(format("304213abcd", "999-999-9999"), {
|
|
33
|
+
raw: "304213abcd",
|
|
34
|
+
formatted: "304-213-",
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should only allow a mix of letters and numbers", () => {
|
|
39
|
+
assert.deepEqual(format("C94749", "A-99999"), {
|
|
40
|
+
raw: "C94749",
|
|
41
|
+
formatted: "C-94749",
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("usa-input-mask", () => {
|
|
47
|
+
it("should format default value", async () => {
|
|
48
|
+
const el = await fixture<USAInputMaskElement>(html`
|
|
49
|
+
<usa-input-mask mask="(999) 999-9999">
|
|
50
|
+
<input name="phone" value="1234567890" mask />
|
|
51
|
+
</usa-input-mask>
|
|
52
|
+
`);
|
|
53
|
+
|
|
54
|
+
const input = el.querySelector("input")!;
|
|
55
|
+
|
|
56
|
+
assert.equal(input.value, "(123) 456-7890");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should update value when on input event", async () => {
|
|
60
|
+
const el = await fixture<USAInputMaskElement>(html`
|
|
61
|
+
<usa-input-mask>
|
|
62
|
+
<input name="phone" mask="(999) 999-9999" />
|
|
63
|
+
</usa-input-mask>
|
|
64
|
+
`);
|
|
65
|
+
|
|
66
|
+
const input = el.querySelector("input")!;
|
|
67
|
+
|
|
68
|
+
input.value = "8888888888";
|
|
69
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
70
|
+
|
|
71
|
+
assert.equal(input.value, "(888) 888-8888");
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("usa-input-mask with usa-input", () => {
|
|
76
|
+
it("should format default value", async () => {
|
|
77
|
+
const el = await fixture<USAInputMaskElement>(html`
|
|
78
|
+
<usa-input-mask mask="(999) 999-9999">
|
|
79
|
+
<usa-input name="phone" value="1234567890" id="TEST" mask></usa-input>
|
|
80
|
+
</usa-input-mask>
|
|
81
|
+
`);
|
|
82
|
+
|
|
83
|
+
const input = el.querySelector("usa-input")!;
|
|
84
|
+
|
|
85
|
+
assert.equal(input.value, "(123) 456-7890");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("should update value when on input event", async () => {
|
|
89
|
+
const el = await fixture<USAInputMaskElement>(html`
|
|
90
|
+
<usa-input-mask>
|
|
91
|
+
<usa-input
|
|
92
|
+
name="phone"
|
|
93
|
+
value="1234567890"
|
|
94
|
+
mask="(999) 999-9999"
|
|
95
|
+
></usa-input>
|
|
96
|
+
</usa-input-mask>
|
|
97
|
+
`);
|
|
98
|
+
|
|
99
|
+
const input = el.querySelector("usa-input")!;
|
|
100
|
+
|
|
101
|
+
input.value = "8888888888";
|
|
102
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
103
|
+
|
|
104
|
+
assert.equal(input.value, "(888) 888-8888");
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { attr, css, element, html, query } from "@joist/element";
|
|
2
|
+
|
|
3
|
+
declare global {
|
|
4
|
+
interface HTMLElementTagNameMap {
|
|
5
|
+
"usa-link": USALinkElement;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
@element({
|
|
10
|
+
tagName: "usa-link",
|
|
11
|
+
shadowDom: [
|
|
12
|
+
css`
|
|
13
|
+
:host {
|
|
14
|
+
display: inline;
|
|
15
|
+
color: #005ea2;
|
|
16
|
+
text-decoration: underline;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
a {
|
|
20
|
+
color: inherit;
|
|
21
|
+
}
|
|
22
|
+
`,
|
|
23
|
+
html`
|
|
24
|
+
<a>
|
|
25
|
+
<slot></slot>
|
|
26
|
+
</a>
|
|
27
|
+
`,
|
|
28
|
+
],
|
|
29
|
+
})
|
|
30
|
+
export class USALinkElement extends HTMLElement {
|
|
31
|
+
@attr()
|
|
32
|
+
accessor href = "";
|
|
33
|
+
|
|
34
|
+
@attr()
|
|
35
|
+
accessor target: "_blank" | "_parent" | "_self" | "_top" | "" = "";
|
|
36
|
+
|
|
37
|
+
@attr()
|
|
38
|
+
accessor title = "";
|
|
39
|
+
|
|
40
|
+
@attr()
|
|
41
|
+
accessor disabled = false;
|
|
42
|
+
|
|
43
|
+
#anchor = query("a");
|
|
44
|
+
|
|
45
|
+
attributeChangedCallback(attr: string) {
|
|
46
|
+
const anchor = this.#anchor();
|
|
47
|
+
|
|
48
|
+
switch (attr) {
|
|
49
|
+
case "href":
|
|
50
|
+
anchor.href = this.href;
|
|
51
|
+
break;
|
|
52
|
+
|
|
53
|
+
case "target":
|
|
54
|
+
anchor.target = this.target;
|
|
55
|
+
break;
|
|
56
|
+
|
|
57
|
+
case "title":
|
|
58
|
+
anchor.target = this.title;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/web-components";
|
|
2
|
+
import { html } from "lit";
|
|
3
|
+
|
|
4
|
+
import type { USALinkElement } from "./link.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-link",
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
render(args) {
|
|
11
|
+
return html`<usa-link href="${args.href}">Hello World</usa-link>`;
|
|
12
|
+
},
|
|
13
|
+
argTypes: {
|
|
14
|
+
href: {
|
|
15
|
+
type: "string",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
args: {},
|
|
19
|
+
} satisfies Meta<USALinkElement>;
|
|
20
|
+
|
|
21
|
+
export default meta;
|
|
22
|
+
|
|
23
|
+
type Story = StoryObj<USALinkElement>;
|
|
24
|
+
|
|
25
|
+
// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
|
|
26
|
+
export const Primary: Story = {
|
|
27
|
+
args: {
|
|
28
|
+
href: "www.google.com",
|
|
29
|
+
},
|
|
30
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { attr, css, element, html } from "@joist/element";
|
|
2
|
+
|
|
3
|
+
import { USARadioElement } from "./radio.element.js";
|
|
4
|
+
|
|
5
|
+
declare global {
|
|
6
|
+
interface HTMLElementTagNameMap {
|
|
7
|
+
"usa-radio-option": USARadioElement;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
@element({
|
|
12
|
+
tagName: "usa-radio-option",
|
|
13
|
+
shadowDom: [
|
|
14
|
+
css`
|
|
15
|
+
:host {
|
|
16
|
+
display: inline-flex;
|
|
17
|
+
flex-direction: column;
|
|
18
|
+
}
|
|
19
|
+
`,
|
|
20
|
+
html`<slot></slot>`,
|
|
21
|
+
],
|
|
22
|
+
})
|
|
23
|
+
export class USARadioOptionElement extends HTMLElement {
|
|
24
|
+
@attr()
|
|
25
|
+
accessor value = "";
|
|
26
|
+
|
|
27
|
+
#parent: USARadioElement | null = null;
|
|
28
|
+
|
|
29
|
+
attributeChangedCallback() {
|
|
30
|
+
this.slot = this.value;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
connectedCallback() {
|
|
34
|
+
if (this.parentElement instanceof USARadioElement) {
|
|
35
|
+
this.#parent = this.parentElement;
|
|
36
|
+
|
|
37
|
+
this.parentElement.onOptionAdded(this);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
disconnectedCallback() {
|
|
42
|
+
if (this.#parent) {
|
|
43
|
+
this.#parent.onOptionRemoved(this);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import "./radio.element.js";
|
|
2
|
+
import "./radio-option.element.js";
|
|
3
|
+
|
|
4
|
+
import { assert, fixture, html } from "@open-wc/testing";
|
|
5
|
+
|
|
6
|
+
import { USARadioOptionElement } from "./radio-option.element.js";
|
|
7
|
+
|
|
8
|
+
describe("usa-radio-option", () => {
|
|
9
|
+
it("should map value to slot", async () => {
|
|
10
|
+
const radio = await fixture<USARadioOptionElement>(html`
|
|
11
|
+
<usa-radio>
|
|
12
|
+
<usa-radio-option value="first">First</usa-radio-option>
|
|
13
|
+
</usa-radio>
|
|
14
|
+
`);
|
|
15
|
+
|
|
16
|
+
const option = radio.querySelectorAll("usa-radio-option");
|
|
17
|
+
|
|
18
|
+
assert.equal(option[0].value, option[0].slot);
|
|
19
|
+
});
|
|
20
|
+
});
|