@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.
- package/CHANGELOG.md +60 -1
- package/dist/base/index.css +1 -1
- package/dist/base/index.css.map +2 -2
- package/dist/base/index.js +1 -1
- package/dist/base/index.js.map +1 -1
- package/dist/components/cfpb-buttons/index.css +1 -1
- package/dist/components/cfpb-buttons/index.css.map +2 -2
- package/dist/components/cfpb-buttons/index.js +1 -1
- package/dist/components/cfpb-buttons/index.js.map +1 -1
- package/dist/components/cfpb-expandables/index.css +1 -1
- package/dist/components/cfpb-expandables/index.css.map +2 -2
- package/dist/components/cfpb-expandables/index.js +1 -1
- package/dist/components/cfpb-expandables/index.js.map +1 -1
- package/dist/components/cfpb-forms/index.css +1 -1
- package/dist/components/cfpb-forms/index.css.map +2 -2
- package/dist/components/cfpb-forms/index.js +1 -1
- package/dist/components/cfpb-forms/index.js.map +1 -1
- package/dist/components/cfpb-icons/index.css +1 -1
- package/dist/components/cfpb-icons/index.css.map +2 -2
- package/dist/components/cfpb-icons/index.js +1 -1
- package/dist/components/cfpb-icons/index.js.map +1 -1
- package/dist/components/cfpb-layout/index.css +1 -1
- package/dist/components/cfpb-layout/index.css.map +2 -2
- package/dist/components/cfpb-layout/index.js +1 -1
- package/dist/components/cfpb-layout/index.js.map +1 -1
- package/dist/components/cfpb-notifications/index.css +1 -1
- package/dist/components/cfpb-notifications/index.css.map +2 -2
- package/dist/components/cfpb-notifications/index.js +1 -1
- package/dist/components/cfpb-notifications/index.js.map +1 -1
- package/dist/components/cfpb-pagination/index.css +1 -1
- package/dist/components/cfpb-pagination/index.css.map +2 -2
- package/dist/components/cfpb-pagination/index.js +1 -1
- package/dist/components/cfpb-pagination/index.js.map +1 -1
- package/dist/components/cfpb-tables/index.css +1 -1
- package/dist/components/cfpb-tables/index.css.map +2 -2
- package/dist/components/cfpb-tables/index.js +1 -1
- package/dist/components/cfpb-tables/index.js.map +1 -1
- package/dist/components/cfpb-tooltips/index.css +1 -1
- package/dist/components/cfpb-tooltips/index.css.map +2 -2
- package/dist/components/cfpb-tooltips/index.js +1 -1
- package/dist/components/cfpb-tooltips/index.js.map +1 -1
- package/dist/components/cfpb-typography/index.css +1 -1
- package/dist/components/cfpb-typography/index.css.map +2 -2
- package/dist/components/cfpb-typography/index.js +1 -1
- package/dist/components/cfpb-typography/index.js.map +1 -1
- package/dist/elements/cfpb-button/index.js +21 -4
- package/dist/elements/cfpb-button/index.js.map +4 -4
- package/dist/elements/cfpb-file-upload/index.js +11 -4
- package/dist/elements/cfpb-file-upload/index.js.map +4 -4
- package/dist/elements/cfpb-form-choice/index.js +11 -3
- package/dist/elements/cfpb-form-choice/index.js.map +4 -4
- package/dist/elements/cfpb-icon-text/index.js +29 -0
- package/dist/elements/cfpb-icon-text/index.js.map +7 -0
- package/dist/elements/cfpb-label/index.js +36 -0
- package/dist/elements/cfpb-label/index.js.map +7 -0
- package/dist/elements/cfpb-multiselect/index.js +13 -4
- package/dist/elements/cfpb-multiselect/index.js.map +4 -4
- package/dist/elements/cfpb-pagination/index.js +32 -0
- package/dist/elements/cfpb-pagination/index.js.map +7 -0
- package/dist/elements/cfpb-tag-filter/index.js +2 -2
- package/dist/elements/cfpb-tag-filter/index.js.map +2 -2
- package/dist/elements/cfpb-tag-group/index.js +2 -2
- package/dist/elements/cfpb-tag-group/index.js.map +2 -2
- package/dist/elements/cfpb-tag-topic/index.js +3 -3
- package/dist/elements/cfpb-tag-topic/index.js.map +2 -2
- package/dist/elements/cfpb-utilities/index.js +2 -0
- package/dist/elements/cfpb-utilities/index.js.map +7 -0
- package/dist/elements/index.js +15 -5
- package/dist/elements/index.js.map +4 -4
- package/dist/index.css +1 -1
- package/dist/index.css.map +2 -2
- package/dist/index.js +15 -5
- package/dist/index.js.map +4 -4
- package/dist/utilities/index.css +1 -1
- package/dist/utilities/index.css.map +2 -2
- package/dist/utilities/index.js +1 -1
- package/dist/utilities/index.js.map +1 -1
- package/package.json +2 -2
- package/src/abstracts/heading-mixins.scss +6 -0
- package/src/abstracts/vars.scss +23 -0
- package/src/base/base.scss +15 -27
- package/src/components/cfpb-buttons/button.scss +4 -2
- package/src/components/cfpb-forms/tag.scss +3 -0
- package/src/components/cfpb-pagination/vars.scss +0 -4
- package/src/components/cfpb-typography/link.scss +4 -2
- package/src/components/cfpb-typography/mixins.scss +8 -0
- package/src/elements/cfpb-button/cfpb-button.component.scss +23 -0
- package/src/elements/cfpb-button/index.js +127 -19
- package/src/elements/cfpb-file-upload/index.js +1 -1
- package/src/elements/cfpb-form-choice/cfpb-form-choice.component.scss +6 -1
- package/src/elements/cfpb-form-choice/index.js +62 -29
- package/src/elements/cfpb-form-choice/index.spec.js +47 -0
- package/src/elements/cfpb-icon-text/cfpb-icon-text.component.scss +59 -0
- package/src/elements/cfpb-icon-text/index.js +150 -0
- package/src/elements/cfpb-label/cfpb-label.component.scss +36 -0
- package/src/elements/cfpb-label/index.js +62 -0
- package/src/elements/cfpb-multiselect/cfpb-multiselect.component.scss +225 -0
- package/src/elements/cfpb-multiselect/index.js +444 -0
- package/src/elements/cfpb-multiselect/multiselect-model.js +288 -0
- package/src/elements/cfpb-multiselect/multiselect-model.spec.js +236 -0
- package/src/elements/cfpb-pagination/cfpb-pagination.component.scss +72 -0
- package/src/elements/cfpb-pagination/index.js +211 -0
- package/src/elements/cfpb-tag-filter/index.js +2 -1
- package/src/elements/cfpb-tag-filter/index.spec.js +1 -1
- package/src/elements/cfpb-tag-group/index.js +2 -1
- package/src/elements/cfpb-tag-topic/cfpb-tag-topic.component.scss +2 -0
- package/src/elements/cfpb-tag-topic/index.js +7 -0
- package/src/elements/cfpb-utilities/i18n-service.js +128 -0
- package/src/elements/cfpb-utilities/i18n-service.spec.js +156 -0
- package/src/elements/cfpb-utilities/index.js +7 -0
- package/src/elements/cfpb-utilities/media-query-service.js +102 -0
- package/src/elements/cfpb-utilities/media-query-service.spec.js +126 -0
- package/src/elements/index.js +3 -0
- package/src/utilities/utilities.scss +8 -8
package/src/base/base.scss
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
169
|
+
text-decoration-color: $link-underline-visited;
|
|
168
170
|
color: $link-text-visited;
|
|
169
171
|
}
|
|
170
172
|
|
|
171
173
|
&:hover,
|
|
172
174
|
&.hover {
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
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
|
-
|
|
188
|
-
|
|
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
|
-
//
|
|
209
|
-
|
|
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
|
|
|
@@ -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
|
-
|
|
9
|
-
|
|
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 {
|
|
16
|
-
* @property {string}
|
|
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.
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
|
51
|
-
|
|
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
|
}
|
|
@@ -17,7 +17,12 @@
|
|
|
17
17
|
// Private variables.
|
|
18
18
|
--choice-border-width-addendum: 0;
|
|
19
19
|
|
|
20
|
-
|
|
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
|
-
|
|
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="${
|
|
114
|
+
<div class="${classes}" ?large=${this.large}>
|
|
83
115
|
<input
|
|
84
116
|
class="a-${this.type}"
|
|
85
|
-
type="${this
|
|
86
|
-
id="
|
|
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="
|
|
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
|
+
}
|