@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,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,5 @@
1
+ export interface MaskableElement extends HTMLElement {
2
+ value: string;
3
+ selectionStart: number | null;
4
+ setSelectionRange(start: number, end: number): void;
5
+ }
@@ -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
+ });