@cfpb/cfpb-design-system 4.0.4 → 4.2.0

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 (114) hide show
  1. package/CHANGELOG.md +60 -1
  2. package/dist/base/index.css +1 -1
  3. package/dist/base/index.css.map +2 -2
  4. package/dist/base/index.js +1 -1
  5. package/dist/base/index.js.map +1 -1
  6. package/dist/components/cfpb-buttons/index.css +1 -1
  7. package/dist/components/cfpb-buttons/index.css.map +2 -2
  8. package/dist/components/cfpb-buttons/index.js +1 -1
  9. package/dist/components/cfpb-buttons/index.js.map +1 -1
  10. package/dist/components/cfpb-expandables/index.css +1 -1
  11. package/dist/components/cfpb-expandables/index.css.map +2 -2
  12. package/dist/components/cfpb-expandables/index.js +1 -1
  13. package/dist/components/cfpb-expandables/index.js.map +1 -1
  14. package/dist/components/cfpb-forms/index.css +1 -1
  15. package/dist/components/cfpb-forms/index.css.map +2 -2
  16. package/dist/components/cfpb-forms/index.js +1 -1
  17. package/dist/components/cfpb-forms/index.js.map +1 -1
  18. package/dist/components/cfpb-icons/index.css +1 -1
  19. package/dist/components/cfpb-icons/index.css.map +2 -2
  20. package/dist/components/cfpb-icons/index.js +1 -1
  21. package/dist/components/cfpb-icons/index.js.map +1 -1
  22. package/dist/components/cfpb-layout/index.css +1 -1
  23. package/dist/components/cfpb-layout/index.css.map +2 -2
  24. package/dist/components/cfpb-layout/index.js +1 -1
  25. package/dist/components/cfpb-layout/index.js.map +1 -1
  26. package/dist/components/cfpb-notifications/index.css +1 -1
  27. package/dist/components/cfpb-notifications/index.css.map +2 -2
  28. package/dist/components/cfpb-notifications/index.js +1 -1
  29. package/dist/components/cfpb-notifications/index.js.map +1 -1
  30. package/dist/components/cfpb-pagination/index.css +1 -1
  31. package/dist/components/cfpb-pagination/index.css.map +2 -2
  32. package/dist/components/cfpb-pagination/index.js +1 -1
  33. package/dist/components/cfpb-pagination/index.js.map +1 -1
  34. package/dist/components/cfpb-tables/index.css +1 -1
  35. package/dist/components/cfpb-tables/index.css.map +2 -2
  36. package/dist/components/cfpb-tables/index.js +1 -1
  37. package/dist/components/cfpb-tables/index.js.map +1 -1
  38. package/dist/components/cfpb-tooltips/index.css +1 -1
  39. package/dist/components/cfpb-tooltips/index.css.map +2 -2
  40. package/dist/components/cfpb-tooltips/index.js +1 -1
  41. package/dist/components/cfpb-tooltips/index.js.map +1 -1
  42. package/dist/components/cfpb-typography/index.css +1 -1
  43. package/dist/components/cfpb-typography/index.css.map +2 -2
  44. package/dist/components/cfpb-typography/index.js +1 -1
  45. package/dist/components/cfpb-typography/index.js.map +1 -1
  46. package/dist/elements/cfpb-button/index.js +21 -4
  47. package/dist/elements/cfpb-button/index.js.map +4 -4
  48. package/dist/elements/cfpb-file-upload/index.js +11 -4
  49. package/dist/elements/cfpb-file-upload/index.js.map +4 -4
  50. package/dist/elements/cfpb-form-choice/index.js +11 -3
  51. package/dist/elements/cfpb-form-choice/index.js.map +4 -4
  52. package/dist/elements/cfpb-icon-text/index.js +29 -0
  53. package/dist/elements/cfpb-icon-text/index.js.map +7 -0
  54. package/dist/elements/cfpb-label/index.js +36 -0
  55. package/dist/elements/cfpb-label/index.js.map +7 -0
  56. package/dist/elements/cfpb-multiselect/index.js +13 -4
  57. package/dist/elements/cfpb-multiselect/index.js.map +4 -4
  58. package/dist/elements/cfpb-pagination/index.js +32 -0
  59. package/dist/elements/cfpb-pagination/index.js.map +7 -0
  60. package/dist/elements/cfpb-tag-filter/index.js +2 -2
  61. package/dist/elements/cfpb-tag-filter/index.js.map +2 -2
  62. package/dist/elements/cfpb-tag-group/index.js +2 -2
  63. package/dist/elements/cfpb-tag-group/index.js.map +2 -2
  64. package/dist/elements/cfpb-tag-topic/index.js +3 -3
  65. package/dist/elements/cfpb-tag-topic/index.js.map +2 -2
  66. package/dist/elements/cfpb-utilities/index.js +2 -0
  67. package/dist/elements/cfpb-utilities/index.js.map +7 -0
  68. package/dist/elements/index.js +15 -5
  69. package/dist/elements/index.js.map +4 -4
  70. package/dist/index.css +1 -1
  71. package/dist/index.css.map +2 -2
  72. package/dist/index.js +15 -5
  73. package/dist/index.js.map +4 -4
  74. package/dist/utilities/index.css +1 -1
  75. package/dist/utilities/index.css.map +2 -2
  76. package/dist/utilities/index.js +1 -1
  77. package/dist/utilities/index.js.map +1 -1
  78. package/package.json +2 -2
  79. package/src/abstracts/heading-mixins.scss +6 -0
  80. package/src/abstracts/vars.scss +23 -0
  81. package/src/base/base.scss +15 -27
  82. package/src/components/cfpb-buttons/button.scss +4 -2
  83. package/src/components/cfpb-forms/tag.scss +3 -0
  84. package/src/components/cfpb-pagination/vars.scss +0 -4
  85. package/src/components/cfpb-typography/link.scss +4 -2
  86. package/src/components/cfpb-typography/mixins.scss +8 -0
  87. package/src/elements/cfpb-button/cfpb-button.component.scss +23 -0
  88. package/src/elements/cfpb-button/index.js +127 -19
  89. package/src/elements/cfpb-file-upload/index.js +1 -1
  90. package/src/elements/cfpb-form-choice/cfpb-form-choice.component.scss +6 -1
  91. package/src/elements/cfpb-form-choice/index.js +62 -29
  92. package/src/elements/cfpb-form-choice/index.spec.js +47 -0
  93. package/src/elements/cfpb-icon-text/cfpb-icon-text.component.scss +59 -0
  94. package/src/elements/cfpb-icon-text/index.js +150 -0
  95. package/src/elements/cfpb-label/cfpb-label.component.scss +36 -0
  96. package/src/elements/cfpb-label/index.js +62 -0
  97. package/src/elements/cfpb-multiselect/cfpb-multiselect.component.scss +225 -0
  98. package/src/elements/cfpb-multiselect/index.js +444 -0
  99. package/src/elements/cfpb-multiselect/multiselect-model.js +288 -0
  100. package/src/elements/cfpb-multiselect/multiselect-model.spec.js +236 -0
  101. package/src/elements/cfpb-pagination/cfpb-pagination.component.scss +72 -0
  102. package/src/elements/cfpb-pagination/index.js +211 -0
  103. package/src/elements/cfpb-tag-filter/index.js +2 -1
  104. package/src/elements/cfpb-tag-filter/index.spec.js +1 -1
  105. package/src/elements/cfpb-tag-group/index.js +2 -1
  106. package/src/elements/cfpb-tag-topic/cfpb-tag-topic.component.scss +2 -0
  107. package/src/elements/cfpb-tag-topic/index.js +7 -0
  108. package/src/elements/cfpb-utilities/i18n-service.js +128 -0
  109. package/src/elements/cfpb-utilities/i18n-service.spec.js +156 -0
  110. package/src/elements/cfpb-utilities/index.js +7 -0
  111. package/src/elements/cfpb-utilities/media-query-service.js +102 -0
  112. package/src/elements/cfpb-utilities/media-query-service.spec.js +126 -0
  113. package/src/elements/index.js +3 -0
  114. package/src/utilities/utilities.scss +8 -8
@@ -9,11 +9,11 @@
9
9
  //
10
10
  // Type hierarchy
11
11
  //
12
-
13
12
  body {
14
13
  color: $text;
15
14
  font-family: var(--font-stack);
16
15
  font-size: math.div($base-font-size, 16) * 100%;
16
+ font-size-adjust: var(--font-adjust-body);
17
17
  line-height: $base-line-height;
18
18
  -webkit-font-smoothing: antialiased;
19
19
  }
@@ -153,60 +153,48 @@ li {
153
153
  //
154
154
 
155
155
  a {
156
- border-width: 0;
157
- border-style: dotted;
158
- border-color: $link-underline;
159
156
  color: $link-text;
160
- text-decoration: none;
157
+
158
+ text-decoration-color: $link-underline;
159
+ text-decoration-line: underline;
160
+ text-decoration-thickness: 1px;
161
+ text-decoration-style: dotted;
162
+ text-underline-offset: 4.5px;
161
163
 
162
164
  // Note: The class definitions below are only for use in
163
165
  // demonstrating link states. Do not use in production.
164
166
 
165
167
  &:visited,
166
168
  &.visited {
167
- border-color: $link-underline-visited;
169
+ text-decoration-color: $link-underline-visited;
168
170
  color: $link-text-visited;
169
171
  }
170
172
 
171
173
  &:hover,
172
174
  &.hover {
173
- border-style: solid;
174
- border-color: $link-underline-hover;
175
+ text-decoration-style: solid;
176
+ text-decoration-color: $link-underline-hover;
175
177
  color: $link-text-hover;
176
178
  }
177
179
 
178
180
  &:focus,
179
181
  &.focus {
180
- border-style: solid;
182
+ text-decoration-style: solid;
181
183
  outline: thin dotted;
182
184
  outline-offset: 1px;
183
185
  }
184
186
 
185
187
  &:active,
186
188
  &.active {
187
- border-style: solid;
188
- border-color: $link-underline-active;
189
+ text-decoration-style: solid;
190
+ text-decoration-color: $link-underline-active;
189
191
  color: $link-text-active;
190
192
  }
191
193
  }
192
194
 
193
- //
194
- // Underlined links
195
- //
196
-
197
- p,
198
- li,
199
- dd {
200
- // Restrict bottom borders to inline text links ...
201
-
202
- a {
203
- border-bottom-width: 1px;
204
- }
205
- }
206
-
207
195
  nav a {
208
- // ... unless they're part of a nav list
209
- border-bottom-width: 0;
196
+ // Don't show underlines if they're part of a nav list.
197
+ text-decoration-line: none;
210
198
  }
211
199
 
212
200
  //
@@ -141,7 +141,8 @@ input.a-btn::-moz-focus-inner {
141
141
  //
142
142
 
143
143
  &--disabled,
144
- &[disabled] {
144
+ &[disabled],
145
+ &[aria-disabled='true'] {
145
146
  &,
146
147
  &:link,
147
148
  &:visited,
@@ -205,7 +206,8 @@ input.a-btn::-moz-focus-inner {
205
206
  }
206
207
 
207
208
  &--disabled:has(svg)::before,
208
- &[disabled]:has(svg)::before {
209
+ &[disabled]:has(svg)::before,
210
+ &[aria-disabled='true']:has(svg)::before {
209
211
  border-color: $btn-disabled-divider !important;
210
212
  }
211
213
 
@@ -50,6 +50,8 @@ a.a-tag-filter {
50
50
  }
51
51
 
52
52
  a.a-tag-filter {
53
+ text-decoration: none;
54
+
53
55
  // Colors for :link, :visited, :hover, :focus, :active.
54
56
  @include u-link-colors(
55
57
  var(--black),
@@ -101,6 +103,7 @@ a.a-tag-filter {
101
103
  a.a-tag-topic:hover,
102
104
  a.a-tag-topic:focus,
103
105
  a.a-tag-topic:active {
106
+ text-decoration: none;
104
107
  border-bottom: none;
105
108
  outline-offset: 1px;
106
109
 
@@ -11,7 +11,3 @@
11
11
 
12
12
  $pagination-text: var(--gray);
13
13
  $pagination-bg: var(--gray-5);
14
-
15
- // Sizing variables
16
-
17
- $pagination-btn-min-width-px: 130px;
@@ -3,10 +3,12 @@
3
3
 
4
4
  .a-link {
5
5
  border-bottom-width: 0;
6
+ text-decoration-line: none;
6
7
 
7
8
  .a-link__text {
8
- border-bottom-width: 1px;
9
- border-bottom-style: inherit;
9
+ text-decoration-line: underline;
10
+ text-decoration-style: dotted;
11
+ text-decoration-thickness: 1px;
10
12
 
11
13
  // See https://github.com/cfpb/consumerfinance.gov/pull/8252
12
14
  overflow-wrap: break-word;
@@ -77,9 +77,17 @@
77
77
 
78
78
  // Mobile only.
79
79
  @include respond-to-max($bp-xs-max) {
80
+ text-decoration: none;
81
+ border-top-style: dotted;
82
+ border-bottom-style: dotted;
80
83
  border-top-width: 1px;
81
84
  border-bottom-width: 1px;
82
85
 
86
+ &:hover {
87
+ border-top-style: solid;
88
+ border-bottom-style: solid;
89
+ }
90
+
83
91
  // We create a faux focus rectangle in the ::after pseudoelement to better
84
92
  // control the positioning of the focus rectangle, which would overwise
85
93
  // overlap the top border of the jumplink when it appears in a group.
@@ -1,8 +1,31 @@
1
1
  @use '@cfpb/cfpb-design-system/src/base' as *;
2
+ @use '@cfpb/cfpb-design-system/src/abstracts' as *;
2
3
  @use '@cfpb/cfpb-design-system/src/components/cfpb-buttons/button' as *;
4
+ @use '@cfpb/cfpb-design-system/src/components/cfpb-buttons/button-link' as *;
3
5
 
4
6
  :host {
5
7
  // This prevents the child button from having an empty gap after the button.
6
8
  display: flex;
7
9
  width: fit-content;
8
10
  }
11
+
12
+ :host([full-on-mobile]) {
13
+ // Mobile only.
14
+ @include respond-to-max($bp-xs-max) {
15
+ width: 100%;
16
+ }
17
+ }
18
+
19
+ :host([flush-left]) {
20
+ [role='button'] {
21
+ border-top-left-radius: 0;
22
+ border-bottom-left-radius: 0;
23
+ }
24
+ }
25
+
26
+ :host([flush-right]) {
27
+ [role='button'] {
28
+ border-top-right-radius: 0;
29
+ border-bottom-right-radius: 0;
30
+ }
31
+ }
@@ -1,5 +1,14 @@
1
1
  import { html, LitElement, css, unsafeCSS } from 'lit';
2
+ import { classMap } from 'lit/directives/class-map.js';
3
+ import { ref, createRef } from 'lit/directives/ref.js';
2
4
  import styles from './cfpb-button.component.scss';
5
+ import { CfpbIconText } from '../cfpb-icon-text';
6
+
7
+ // The variants are different color themes of the button.
8
+ const VALID_VARIANTS = ['primary', 'secondary', 'warning'];
9
+
10
+ // The types are a regular button, or submit/reset that are used in forms.
11
+ const VALID_TYPES = ['button', 'submit', 'reset'];
3
12
 
4
13
  /**
5
14
  *
@@ -12,48 +21,147 @@ export class CfpbButton extends LitElement {
12
21
  `;
13
22
 
14
23
  /**
15
- * @property {boolean} disabled - Whether to stack the tags vertically.
16
- * @property {string} type - The button type: secondary, warning, disabled.
24
+ * @property {string} type - The button type: button, submit, or reset.
25
+ * @property {string} href - The URL to link to (makes the button a link).
26
+ * @property {boolean} disabled - Whether the button is disabled or not.
27
+ * @property {string} variant - The button variant: secondary and warning.
28
+ * @property {boolean} fullOnMobile - Whether to be width 100% on mobile.
29
+ * @property {boolean} flushLeft - Whether button is not rounded on left.
30
+ * @property {boolean} flushRight - Whether button is not rounded on right.
31
+ * @property {boolean} styleAsLink - Style the button as a link.
32
+ * @returns {object} The map of properties.
17
33
  */
18
34
  static get properties() {
19
35
  return {
20
- disabled: { type: Boolean },
21
36
  type: { type: String },
37
+ href: { type: String },
38
+ disabled: { type: Boolean, reflect: true },
39
+ variant: { type: String },
40
+ fullOnMobile: {
41
+ type: Boolean,
42
+ attribute: 'full-on-mobile',
43
+ reflect: true,
44
+ },
45
+ flushLeft: {
46
+ type: Boolean,
47
+ attribute: 'flush-left',
48
+ reflect: true,
49
+ },
50
+ flushRight: {
51
+ type: Boolean,
52
+ attribute: 'flush-right',
53
+ reflect: true,
54
+ },
55
+ styleAsLink: {
56
+ type: Boolean,
57
+ attribute: 'style-as-link',
58
+ reflect: true,
59
+ },
22
60
  };
23
61
  }
24
62
 
63
+ // DOM references.
64
+ #iconTextDom = createRef();
65
+
25
66
  constructor() {
26
67
  super();
68
+ this.type = 'button';
69
+ this.variant = 'primary';
27
70
  this.disabled = false;
28
- this.type = '';
71
+ this.fullOnMobile = false;
72
+ this.styleAsLink = false;
73
+ }
74
+
75
+ /**
76
+ * @returns {boolean} True if it has an icon, false otherwise.
77
+ */
78
+ hasIcon() {
79
+ return this.#iconTextDom.value?.hasIcon();
80
+ }
81
+
82
+ /**
83
+ * Hide any icon in the slot.
84
+ */
85
+ hideIcon() {
86
+ this.#iconTextDom.value?.hideIcon();
87
+ }
88
+
89
+ /**
90
+ * Show any icon in the slot, if it was hidden.
91
+ */
92
+ showIcon() {
93
+ this.#iconTextDom.value?.showIcon();
94
+ }
95
+
96
+ /**
97
+ * Ensure the variant value is valid, and fall back to a default if not.
98
+ * @returns {string} A valid variant value string.
99
+ */
100
+ get #validVariant() {
101
+ return VALID_VARIANTS.includes(this.variant) ? this.variant : 'primary';
29
102
  }
30
103
 
104
+ /**
105
+ * Ensure the type value is valid, and fall back to a default if not.
106
+ * @returns {string} A valid type value string.
107
+ */
108
+ get #validType() {
109
+ return VALID_TYPES.includes(this.type) ? this.type : 'button';
110
+ }
111
+
112
+ /**
113
+ * The classes added to the button.
114
+ * @returns {object} A classmap of CSS class names.
115
+ */
31
116
  get #btnClass() {
32
- let btnClass = 'a-btn';
33
- switch (this.type) {
34
- case 'secondary':
35
- btnClass += ' a-btn--secondary';
36
- break;
37
- case 'warning':
38
- btnClass += ' a-btn--warning';
39
- break;
40
- case 'disabled':
41
- btnClass += ' a-btn--disabled';
42
- break;
43
- }
117
+ return {
118
+ 'a-btn': true,
119
+ [`a-btn--${this.#validVariant}`]: this.#validVariant !== 'primary',
120
+ [`a-btn--link`]: this.styleAsLink === true,
121
+ };
122
+ }
44
123
 
45
- return btnClass;
124
+ #renderTextAndIcon() {
125
+ return html`
126
+ <cfpb-icon-text ${ref(this.#iconTextDom)} ?disabled=${this.disabled}>
127
+ <slot></slot>
128
+ </cfpb-icon-text>
129
+ `;
46
130
  }
47
131
 
48
132
  render() {
133
+ const classes = classMap(this.#btnClass);
134
+
135
+ // Link button form.
136
+ if (this.href) {
137
+ return html`
138
+ <a
139
+ class=${classes}
140
+ href=${this.disabled ? undefined : this.href}
141
+ role="button"
142
+ aria-disabled=${String(this.disabled)}
143
+ tabindex=${this.disabled ? -1 : 0}
144
+ >
145
+ ${this.#renderTextAndIcon()}
146
+ </a>
147
+ `;
148
+ }
149
+
150
+ // Button form.
49
151
  return html`
50
- <button class="${this.#btnClass}" ?disabled=${this.disabled}>
51
- <slot></slot>
152
+ <button
153
+ class=${classes}
154
+ ?disabled=${this.disabled}
155
+ type=${this.#validType}
156
+ >
157
+ ${this.#renderTextAndIcon()}
52
158
  </button>
53
159
  `;
54
160
  }
55
161
 
56
162
  static init() {
163
+ CfpbIconText.init();
164
+
57
165
  window.customElements.get('cfpb-button') ||
58
166
  window.customElements.define('cfpb-button', CfpbButton);
59
167
  }
@@ -70,7 +70,7 @@ export class CfpbFileUpload extends LitElement {
70
70
  render() {
71
71
  return html`
72
72
  <cfpb-button
73
- type="secondary"
73
+ variant="secondary"
74
74
  @click="${() => {
75
75
  this.fileInput.value.click();
76
76
  }}"
@@ -17,7 +17,12 @@
17
17
  // Private variables.
18
18
  --choice-border-width-addendum: 0;
19
19
 
20
- .a-text-input--full {
20
+ &--in-list label {
21
+ box-sizing: border-box;
22
+ padding-top: math.div(5px, $base-font-size-px) + em;
23
+ padding-right: 0;
24
+ padding-bottom: math.div(5px, $base-font-size-px) + em;
25
+ padding-left: math.div(10px, $base-font-size-px) + em;
21
26
  width: 100%;
22
27
  }
23
28
 
@@ -1,8 +1,14 @@
1
1
  import { html, LitElement, css, unsafeCSS } from 'lit';
2
+ import { classMap } from 'lit/directives/class-map.js';
2
3
  import styles from './cfpb-form-choice.component.scss';
3
4
 
5
+ // The validation states are error, warning, or success.
6
+ const VALID_VALIDATION = ['error', 'warning', 'success'];
7
+
8
+ // The types are a checkbox or radio button.
9
+ const VALID_TYPES = ['checkbox', 'radio'];
10
+
4
11
  /**
5
- *
6
12
  * @element cfpb-form-choice
7
13
  * @slot - The label for the form input.
8
14
  */
@@ -17,6 +23,10 @@ export class CfpbFormChoice extends LitElement {
17
23
  * @property {boolean} large - Whether the choice has a large target area.
18
24
  * @property {string} validation - Validation style: error, warning, success.
19
25
  * @property {string} type - Choice type: checkbox or radio.
26
+ * @property {string} inlist - Whether the choice appears in a <li> list.
27
+ * @property {string} name - The name within a form.
28
+ * @property {string} value - The value to submit within a form.
29
+ * @returns {object} The map of properties.
20
30
  */
21
31
  static get properties() {
22
32
  return {
@@ -25,41 +35,25 @@ export class CfpbFormChoice extends LitElement {
25
35
  large: { type: Boolean },
26
36
  validation: { type: String },
27
37
  type: { type: String },
38
+ inlist: { type: Boolean, attribute: true },
39
+ name: { type: String },
40
+ value: { type: String },
28
41
  };
29
42
  }
30
43
 
31
44
  constructor() {
32
45
  super();
46
+ this.checked = false;
33
47
  this.disabled = false;
34
48
  this.large = false;
35
49
  this.validation = '';
36
50
  this.type = 'checkbox';
51
+ this.inlist = false;
52
+ this.name = '';
53
+ this.value = '';
37
54
  }
38
55
 
39
- get #baseClass() {
40
- let baseClass = `m-form-field m-form-field--${this.type}`;
41
-
42
- switch (this.validation) {
43
- case 'success':
44
- baseClass += ` m-form-field--${this.type}-success`;
45
- break;
46
- case 'warning':
47
- baseClass += ` m-form-field--${this.type}-warning`;
48
- break;
49
- case 'error':
50
- baseClass += ` m-form-field--${this.type}-error`;
51
- break;
52
- }
53
-
54
- if (this.large) {
55
- baseClass += ' m-form-field--lg-target';
56
- }
57
-
58
- return baseClass;
59
- }
60
-
61
- #onChange(evt) {
62
- evt.target.checked = this.checked;
56
+ #onChange() {
63
57
  this.dispatchEvent(
64
58
  new Event('change', {
65
59
  bubbles: true,
@@ -77,19 +71,58 @@ export class CfpbFormChoice extends LitElement {
77
71
  );
78
72
  }
79
73
 
74
+ focus() {
75
+ this.shadowRoot.querySelector('input').focus();
76
+ }
77
+
78
+ /**
79
+ * Ensure the validation value is valid, and fall back to a default if not.
80
+ * @returns {string|undefined} A valid validation value string, or undefined.
81
+ */
82
+ get #validValidation() {
83
+ return VALID_VALIDATION.includes(this.validation)
84
+ ? this.validation
85
+ : undefined;
86
+ }
87
+
88
+ /**
89
+ * Ensure the type value is valid, and fall back to a default if not.
90
+ * @returns {string} A type value string.
91
+ */
92
+ get #validType() {
93
+ return VALID_TYPES.includes(this.type) ? this.type : 'checkbox';
94
+ }
95
+
96
+ get #baseClass() {
97
+ const classes = {
98
+ 'm-form-field': true,
99
+ [`m-form-field--${this.type}`]: true,
100
+ 'm-form-field--lg-target': this.large,
101
+ 'm-form-field--in-list': this.inlist,
102
+ };
103
+
104
+ if (this.#validValidation)
105
+ classes[[`m-form-field--${this.type}-${this.validation}`]] =
106
+ this.validation;
107
+ return classes;
108
+ }
109
+
80
110
  render() {
111
+ const classes = classMap(this.#baseClass);
112
+
81
113
  return html`
82
- <div class="${this.#baseClass}" ?large=${this.large}>
114
+ <div class="${classes}" ?large=${this.large}>
83
115
  <input
84
116
  class="a-${this.type}"
85
- type="${this.type}"
86
- id="${this.type}"
117
+ type="${this.#validType}"
118
+ id="choice-input"
87
119
  ?disabled=${this.disabled}
88
120
  .checked=${this.checked}
89
121
  @change=${this.#onChange}
90
122
  @input=${this.#onInput}
123
+ aria-invalid=${this.#validValidation === 'error' ? 'true' : 'false'}
91
124
  />
92
- <label class="a-label" for="${this.type}">
125
+ <label class="a-label" for="choice-input">
93
126
  <slot></slot>
94
127
  </label>
95
128
  </div>
@@ -0,0 +1,47 @@
1
+ import { jest } from '@jest/globals';
2
+ import { CfpbFormChoice } from './index.js';
3
+
4
+ describe('<cfpb-form-choice>', () => {
5
+ let elm;
6
+
7
+ beforeEach(async () => {
8
+ CfpbFormChoice.init();
9
+ elm = document.createElement('cfpb-form-choice');
10
+ document.body.appendChild(elm);
11
+
12
+ await customElements.whenDefined('cfpb-form-choice');
13
+ await elm.updateComplete;
14
+ });
15
+
16
+ afterEach(() => {
17
+ document.body.removeChild(elm);
18
+ });
19
+
20
+ it('renders slotted content', async () => {
21
+ const slottedContent = document.createElement('span');
22
+ slottedContent.textContent = 'Earth';
23
+ elm.appendChild(slottedContent);
24
+ await elm.updateComplete;
25
+
26
+ const slot = elm.shadowRoot.querySelector('slot');
27
+ const assignedNodes = slot.assignedNodes({ flatten: true });
28
+
29
+ expect(assignedNodes.length).toBe(1);
30
+ expect(assignedNodes[0].textContent).toBe('Earth');
31
+ });
32
+
33
+ it('dispatches the correct event', async () => {
34
+ const inputMockHandler = jest.fn();
35
+ const changeMockHandler = jest.fn();
36
+ elm.addEventListener('input', inputMockHandler);
37
+ elm.addEventListener('change', changeMockHandler);
38
+
39
+ elm.shadowRoot.querySelector('label').click();
40
+
41
+ expect(inputMockHandler).toHaveBeenCalledTimes(1);
42
+ expect(inputMockHandler.mock.calls[0][0].target).toBe(elm);
43
+
44
+ expect(changeMockHandler).toHaveBeenCalledTimes(1);
45
+ expect(changeMockHandler.mock.calls[0][0].target).toBe(elm);
46
+ });
47
+ });
@@ -0,0 +1,59 @@
1
+ @use 'sass:math';
2
+ @use '@cfpb/cfpb-design-system/src/abstracts' as *;
3
+
4
+ @mixin u-btn-divider() {
5
+ content: '';
6
+ border-left: 1px solid var(--icon-text-divider);
7
+ order: 2;
8
+ place-self: normal;
9
+ }
10
+
11
+ :host {
12
+ // Theme variables.
13
+ --icon-text-divider: var(--pacific-60);
14
+
15
+ div {
16
+ // This prevents the child button from having an empty gap after the button.
17
+ display: flex;
18
+ width: fit-content;
19
+
20
+ // Hide SVG by default.
21
+ & ::slotted(svg) {
22
+ display: none;
23
+ }
24
+
25
+ &.u-has-icon {
26
+ gap: math.div(10px, $btn-font-size) + rem;
27
+ & slot::before {
28
+ @include u-btn-divider;
29
+ }
30
+
31
+ // Show SVG.
32
+ & ::slotted(svg) {
33
+ display: initial;
34
+ }
35
+ }
36
+
37
+ &.u-has-icon--left {
38
+ & ::slotted(svg) {
39
+ order: 1;
40
+ }
41
+ & ::slotted(span) {
42
+ order: 3;
43
+ }
44
+ }
45
+
46
+ &.u-has-icon--right {
47
+ & ::slotted(svg) {
48
+ order: 3;
49
+ }
50
+ & ::slotted(span) {
51
+ order: 1;
52
+ }
53
+ }
54
+ }
55
+ }
56
+
57
+ :host([disabled]) {
58
+ --icon-text-divider: var(--gray-60);
59
+ }