@cfpb/cfpb-design-system 4.0.4 → 4.1.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 +25 -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 +12 -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-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-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/index.js +14 -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 +14 -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 +1 -1
- package/src/abstracts/heading-mixins.scss +6 -0
- package/src/abstracts/vars.scss +23 -0
- package/src/base/base.scss +1 -1
- package/src/elements/cfpb-button/cfpb-button.component.scss +8 -0
- package/src/elements/cfpb-button/index.js +100 -17
- 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-label/cfpb-label.component.scss +36 -0
- package/src/elements/cfpb-label/index.js +61 -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-tag-filter/index.js +1 -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/index.js +6 -0
- package/src/elements/index.js +2 -0
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { html, LitElement, css, unsafeCSS } from 'lit';
|
|
2
|
+
import { classMap } from 'lit/directives/class-map.js';
|
|
2
3
|
import styles from './cfpb-button.component.scss';
|
|
3
4
|
|
|
5
|
+
// The variants are different color themes of the button.
|
|
6
|
+
const VALID_VARIANTS = ['primary', 'secondary', 'warning'];
|
|
7
|
+
|
|
8
|
+
// The types are a regular button, or submit/reset that are used in forms.
|
|
9
|
+
const VALID_TYPES = ['button', 'submit', 'reset'];
|
|
10
|
+
|
|
4
11
|
/**
|
|
5
12
|
*
|
|
6
13
|
* @element cfpb-button
|
|
@@ -12,12 +19,23 @@ export class CfpbButton extends LitElement {
|
|
|
12
19
|
`;
|
|
13
20
|
|
|
14
21
|
/**
|
|
22
|
+
* @property {string} href - The URL to link to (makes the button a link).
|
|
15
23
|
* @property {boolean} disabled - Whether to stack the tags vertically.
|
|
16
|
-
* @property {string}
|
|
24
|
+
* @property {string} variant - The button variant: secondary and warning.
|
|
25
|
+
* @property {boolean} fullOnMobile - Whether to be width 100% on mobile.
|
|
26
|
+
* @property {string} type - The button type: button, submit, or reset.
|
|
27
|
+
* @returns {object} The map of properties.
|
|
17
28
|
*/
|
|
18
29
|
static get properties() {
|
|
19
30
|
return {
|
|
20
|
-
|
|
31
|
+
href: { type: String },
|
|
32
|
+
disabled: { type: Boolean, reflect: true },
|
|
33
|
+
variant: { type: String },
|
|
34
|
+
fullOnMobile: {
|
|
35
|
+
type: Boolean,
|
|
36
|
+
attribute: 'full-on-mobile',
|
|
37
|
+
reflect: true,
|
|
38
|
+
},
|
|
21
39
|
type: { type: String },
|
|
22
40
|
};
|
|
23
41
|
}
|
|
@@ -25,29 +43,94 @@ export class CfpbButton extends LitElement {
|
|
|
25
43
|
constructor() {
|
|
26
44
|
super();
|
|
27
45
|
this.disabled = false;
|
|
28
|
-
this.
|
|
46
|
+
this.variant = 'primary';
|
|
47
|
+
this.fullOnMobile = false;
|
|
48
|
+
this.type = 'button';
|
|
29
49
|
}
|
|
30
50
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Hide any icon in the slot.
|
|
53
|
+
*/
|
|
54
|
+
hideIcon() {
|
|
55
|
+
const icon = this.#findIconInSlot();
|
|
56
|
+
if (icon) icon.style.display = 'none';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Show any icon in the slot, if it was hidden.
|
|
61
|
+
*/
|
|
62
|
+
showIcon() {
|
|
63
|
+
const icon = this.#findIconInSlot();
|
|
64
|
+
if (icon) icon.style.display = '';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Find the icon SVG in the slot.
|
|
69
|
+
* @returns {Node} The icon SVG node.
|
|
70
|
+
*/
|
|
71
|
+
#findIconInSlot() {
|
|
72
|
+
const slot = this.shadowRoot.querySelector('slot');
|
|
73
|
+
const nodes = slot.assignedNodes({ flatten: true });
|
|
74
|
+
|
|
75
|
+
for (const node of nodes) {
|
|
76
|
+
if (node.tagName.toLowerCase() === 'svg') {
|
|
77
|
+
return node;
|
|
78
|
+
}
|
|
43
79
|
}
|
|
80
|
+
}
|
|
44
81
|
|
|
45
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Ensure the variant value is valid, and fall back to a default if not.
|
|
84
|
+
* @returns {string} A valid variant value string.
|
|
85
|
+
*/
|
|
86
|
+
get #validVariant() {
|
|
87
|
+
return VALID_VARIANTS.includes(this.variant) ? this.variant : 'primary';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Ensure the type value is valid, and fall back to a default if not.
|
|
92
|
+
* @returns {string} A valid type value string.
|
|
93
|
+
*/
|
|
94
|
+
get #validType() {
|
|
95
|
+
return VALID_TYPES.includes(this.type) ? this.type : 'button';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The classes added to the button.
|
|
100
|
+
* @returns {object} A classmap of CSS class names.
|
|
101
|
+
*/
|
|
102
|
+
get #btnClass() {
|
|
103
|
+
return {
|
|
104
|
+
'a-btn': true,
|
|
105
|
+
[`a-btn--${this.#validVariant}`]: this.#validVariant !== 'primary',
|
|
106
|
+
};
|
|
46
107
|
}
|
|
47
108
|
|
|
48
109
|
render() {
|
|
110
|
+
const classes = classMap(this.#btnClass);
|
|
111
|
+
|
|
112
|
+
// Link button form.
|
|
113
|
+
if (this.href) {
|
|
114
|
+
return html`
|
|
115
|
+
<a
|
|
116
|
+
class=${classes}
|
|
117
|
+
href=${this.disabled ? undefined : this.href}
|
|
118
|
+
role="button"
|
|
119
|
+
aria-disabled=${String(this.disabled)}
|
|
120
|
+
tabindex=${this.disabled ? -1 : 0}
|
|
121
|
+
>
|
|
122
|
+
<slot></slot
|
|
123
|
+
></a>
|
|
124
|
+
`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Button form.
|
|
49
128
|
return html`
|
|
50
|
-
<button
|
|
129
|
+
<button
|
|
130
|
+
class=${classes}
|
|
131
|
+
?disabled=${this.disabled}
|
|
132
|
+
type=${this.#validType}
|
|
133
|
+
>
|
|
51
134
|
<slot></slot>
|
|
52
135
|
</button>
|
|
53
136
|
`;
|
|
@@ -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,36 @@
|
|
|
1
|
+
@use 'sass:math';
|
|
2
|
+
@use '@cfpb/cfpb-design-system/src/abstracts' as *;
|
|
3
|
+
|
|
4
|
+
:host {
|
|
5
|
+
.a-label {
|
|
6
|
+
slot {
|
|
7
|
+
display: inline-block;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
&__helper {
|
|
11
|
+
color: $label-helper;
|
|
12
|
+
font-size: math.div(16px, $base-font-size-px) + rem;
|
|
13
|
+
font-weight: normal;
|
|
14
|
+
|
|
15
|
+
&--block {
|
|
16
|
+
display: block;
|
|
17
|
+
|
|
18
|
+
// Add a gap between the label helper and label.
|
|
19
|
+
margin-top: math.div(10px, $size-vi) + em;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
&--heading {
|
|
24
|
+
display: block;
|
|
25
|
+
|
|
26
|
+
margin-bottom: math.div(10px, $size-iv) + em;
|
|
27
|
+
|
|
28
|
+
@include heading-4($has-margin-bottom: false);
|
|
29
|
+
|
|
30
|
+
// Add a gap between the label helper and label heading
|
|
31
|
+
.a-label__helper--block {
|
|
32
|
+
margin-top: math.div(10px, $base-font-size-px) + rem;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { html, LitElement, css, unsafeCSS } from 'lit';
|
|
2
|
+
import styles from './cfpb-label.component.scss';
|
|
3
|
+
import { ifDefined } from 'lit/directives/if-defined.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
*
|
|
7
|
+
* @element cfpb-multiselect.
|
|
8
|
+
* @slot - The main content for the upload button.
|
|
9
|
+
*/
|
|
10
|
+
export class CfpbLabel extends LitElement {
|
|
11
|
+
static styles = css`
|
|
12
|
+
${unsafeCSS(styles)}
|
|
13
|
+
`;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @property {string} for - Associate the label with an ID elsewhere.
|
|
17
|
+
* @property {string} type - Associate the label with an ID elsewhere.
|
|
18
|
+
* @returns {object} The map of properties.
|
|
19
|
+
*/
|
|
20
|
+
static get properties() {
|
|
21
|
+
return {
|
|
22
|
+
// Other properties.
|
|
23
|
+
block: { type: Boolean, reflect: true },
|
|
24
|
+
for: { type: String },
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
constructor() {
|
|
29
|
+
super();
|
|
30
|
+
this.block = false;
|
|
31
|
+
this.for = '';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get #helperClass() {
|
|
35
|
+
let helperClass = 'a-label__helper';
|
|
36
|
+
if (this.block) {
|
|
37
|
+
helperClass += ' a-label__helper--block';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return helperClass;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
render() {
|
|
44
|
+
return html`
|
|
45
|
+
<label
|
|
46
|
+
class="a-label a-label--heading"
|
|
47
|
+
for=${ifDefined(this.for && this.for.trim() ? this.for : undefined)}
|
|
48
|
+
>
|
|
49
|
+
<slot name="label"></slot>
|
|
50
|
+
<small class="${this.#helperClass}">
|
|
51
|
+
<slot name="helper"></slot>
|
|
52
|
+
</small>
|
|
53
|
+
</label>
|
|
54
|
+
`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
static init() {
|
|
58
|
+
window.customElements.get('cfpb-label') ||
|
|
59
|
+
window.customElements.define('cfpb-label', CfpbLabel);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
@use 'sass:math';
|
|
2
|
+
@use '@cfpb/cfpb-design-system/src/base' as *;
|
|
3
|
+
@use '@cfpb/cfpb-design-system/src/abstracts' as *;
|
|
4
|
+
@use '@cfpb/cfpb-design-system/src/components/cfpb-buttons/vars' as *;
|
|
5
|
+
|
|
6
|
+
:host {
|
|
7
|
+
// Theme variables.
|
|
8
|
+
--select-input-border: var(--gray-60);
|
|
9
|
+
--select-input-border-hover: var(--pacific);
|
|
10
|
+
--select-input-border-focus: var(--pacific);
|
|
11
|
+
--select-input-bg: var(--white);
|
|
12
|
+
--select-input-text: var(--black);
|
|
13
|
+
|
|
14
|
+
// Initial and no-js state.
|
|
15
|
+
select.o-multiselect {
|
|
16
|
+
display: block;
|
|
17
|
+
box-sizing: border-box;
|
|
18
|
+
width: 100%;
|
|
19
|
+
padding: math.div(7px, $base-font-size-px) + em;
|
|
20
|
+
|
|
21
|
+
// Fixed height breaks the bottom border
|
|
22
|
+
// mid-character to indicate there's more content.
|
|
23
|
+
height: 5.5em;
|
|
24
|
+
padding-top: math.div(4px, $base-font-size-px) + em;
|
|
25
|
+
padding-bottom: math.div(4px, $base-font-size-px) + em;
|
|
26
|
+
border: 1px solid var(--select-border-default);
|
|
27
|
+
|
|
28
|
+
option {
|
|
29
|
+
padding: math.div(2px, $base-font-size-px) + em
|
|
30
|
+
math.div(6px, $base-font-size-px) + em;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.o-multiselect {
|
|
35
|
+
position: relative;
|
|
36
|
+
|
|
37
|
+
& header {
|
|
38
|
+
position: relative;
|
|
39
|
+
|
|
40
|
+
&::after {
|
|
41
|
+
// Arrow box width must be odd size to properly center the bg image
|
|
42
|
+
width: math.div($select-height, $base-font-size-px) + em;
|
|
43
|
+
box-sizing: border-box;
|
|
44
|
+
border: 1px solid var(--select-border-default);
|
|
45
|
+
position: absolute;
|
|
46
|
+
top: 0;
|
|
47
|
+
right: 0;
|
|
48
|
+
bottom: 0;
|
|
49
|
+
background-color: var(--select-icon-bg-default);
|
|
50
|
+
|
|
51
|
+
--cfpb-background-icon-svg: 'down';
|
|
52
|
+
|
|
53
|
+
background-size: auto $cf-icon-height;
|
|
54
|
+
background-repeat: no-repeat;
|
|
55
|
+
background-position: center center;
|
|
56
|
+
content: '';
|
|
57
|
+
pointer-events: none;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
& input[type='text'] {
|
|
62
|
+
width: 100%;
|
|
63
|
+
min-height: 35px;
|
|
64
|
+
|
|
65
|
+
// Reset the browser's default styling.
|
|
66
|
+
appearance: none;
|
|
67
|
+
display: inline-block;
|
|
68
|
+
padding: math.div(7px, $base-font-size-px) + em;
|
|
69
|
+
border: 1px solid var(--select-input-border);
|
|
70
|
+
outline: 0 solid var(--select-input-border);
|
|
71
|
+
background: var(--select-input-bg);
|
|
72
|
+
color: var(--select-input-text);
|
|
73
|
+
box-sizing: border-box;
|
|
74
|
+
|
|
75
|
+
&:hover,
|
|
76
|
+
&.hover {
|
|
77
|
+
border-color: var(--select-input-border-hover);
|
|
78
|
+
outline: 1px solid var(--select-input-border-hover);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
&:focus,
|
|
82
|
+
&.focus {
|
|
83
|
+
border-color: var(--select-input-border-focus);
|
|
84
|
+
box-shadow: 0 0 0 1px var(--select-input-border-focus);
|
|
85
|
+
outline: 1px dotted var(--select-input-border-focus);
|
|
86
|
+
outline-offset: 2px;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
& fieldset {
|
|
91
|
+
// Resets
|
|
92
|
+
border-color: var(--select-border-default);
|
|
93
|
+
border-top: none;
|
|
94
|
+
margin: 0;
|
|
95
|
+
padding: 0;
|
|
96
|
+
|
|
97
|
+
// Styles
|
|
98
|
+
box-sizing: border-box;
|
|
99
|
+
overflow-x: hidden;
|
|
100
|
+
overflow-y: scroll;
|
|
101
|
+
position: absolute;
|
|
102
|
+
z-index: 10;
|
|
103
|
+
|
|
104
|
+
max-height: 0;
|
|
105
|
+
margin-top: -1px;
|
|
106
|
+
width: 100%;
|
|
107
|
+
|
|
108
|
+
transition: max-height 0.25s ease-out;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
&.u-active {
|
|
112
|
+
fieldset {
|
|
113
|
+
margin-top: 0;
|
|
114
|
+
|
|
115
|
+
// This needs to match the value set in _bindEvents in Multiselect.js.
|
|
116
|
+
// See https://github.com/cfpb/design-system/blob/4d26d5af04317bcc00b4677aa866fe8d526e82e0/packages/cfpb-forms/src/organisms/Multiselect.js#L340
|
|
117
|
+
max-height: 140px;
|
|
118
|
+
|
|
119
|
+
border-color: var(--pacific);
|
|
120
|
+
border-width: 2px;
|
|
121
|
+
border-top: 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Reverse arrow when search drop-down is open.
|
|
125
|
+
header::after {
|
|
126
|
+
--cfpb-background-icon-svg: 'up';
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
& ul {
|
|
131
|
+
list-style-type: none;
|
|
132
|
+
background-color: var(--white);
|
|
133
|
+
padding: 0;
|
|
134
|
+
padding-top: math.div(5px, $base-font-size-px) + em;
|
|
135
|
+
padding-bottom: math.div(5px, $base-font-size-px) + em;
|
|
136
|
+
|
|
137
|
+
li {
|
|
138
|
+
margin: 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
li:first-child {
|
|
142
|
+
.a-label {
|
|
143
|
+
padding-top: math.div(10px, $base-font-size-px) + em;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
&.u-filtered li:not(.u-filter-match) {
|
|
148
|
+
display: none;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
&.u-no-results,
|
|
152
|
+
&.u-max-selections {
|
|
153
|
+
padding: math.div(10px, $base-font-size-px) + em;
|
|
154
|
+
li {
|
|
155
|
+
display: none;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
&::after {
|
|
159
|
+
display: list-item;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
&.u-no-results::after {
|
|
164
|
+
content: 'No results found';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
&.u-max-selections {
|
|
168
|
+
pointer-events: none;
|
|
169
|
+
|
|
170
|
+
&::after {
|
|
171
|
+
content: 'Reached maximum number of selections';
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.u-invisible {
|
|
178
|
+
visibility: hidden;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* button {
|
|
182
|
+
// Filter tags appear in filtered contexts, often as part of multiselects.
|
|
183
|
+
line-height: math.div(19px, $base-font-size-px);
|
|
184
|
+
|
|
185
|
+
display: flex;
|
|
186
|
+
gap: math.div(10px, $btn-font-size) + rem;
|
|
187
|
+
|
|
188
|
+
border: 1px solid var(--teal);
|
|
189
|
+
padding: 4px 6px;
|
|
190
|
+
background-color: var(--teal-20);
|
|
191
|
+
border-radius: math.div(3px, $base-font-size-px) + rem;
|
|
192
|
+
color: var(--black);
|
|
193
|
+
text-align: left;
|
|
194
|
+
min-width: fit-content;
|
|
195
|
+
|
|
196
|
+
&:hover {
|
|
197
|
+
background-color: var(--teal-40);
|
|
198
|
+
cursor: pointer;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
&:focus {
|
|
202
|
+
outline: 1px dotted var(--teal);
|
|
203
|
+
outline-offset: 1px;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
&:active {
|
|
207
|
+
background-color: var(--teal-60);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
svg {
|
|
212
|
+
pointer-events: none;
|
|
213
|
+
|
|
214
|
+
// Prevent flexbox from squishing icon when tag text is long.
|
|
215
|
+
flex: none;
|
|
216
|
+
|
|
217
|
+
height: 1rem;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// If the contents are wrapped in a label, negate the label's display.
|
|
221
|
+
label {
|
|
222
|
+
display: contents;
|
|
223
|
+
pointer-events: none;
|
|
224
|
+
} */
|
|
225
|
+
}
|