@cfpb/cfpb-design-system 4.2.4 → 4.3.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 (124) hide show
  1. package/CHANGELOG.md +166 -1
  2. package/dist/components/cfpb-expandables/index.js +1 -1
  3. package/dist/components/cfpb-expandables/index.js.map +3 -3
  4. package/dist/components/cfpb-forms/index.js +1 -1
  5. package/dist/components/cfpb-forms/index.js.map +2 -2
  6. package/dist/elements/cfpb-button/index.js +4 -4
  7. package/dist/elements/cfpb-button/index.js.map +3 -3
  8. package/dist/elements/cfpb-checkbox-icon/index.js +29 -0
  9. package/dist/elements/{cfpb-checkbox → cfpb-checkbox-icon}/index.js.map +4 -4
  10. package/dist/elements/cfpb-expandable/index.css +2 -0
  11. package/dist/elements/cfpb-expandable/index.css.map +7 -0
  12. package/dist/elements/cfpb-expandable/index.js +33 -0
  13. package/dist/elements/cfpb-expandable/index.js.map +7 -0
  14. package/dist/elements/cfpb-file-upload/index.js +4 -4
  15. package/dist/elements/cfpb-file-upload/index.js.map +3 -3
  16. package/dist/elements/cfpb-form-alert/index.js +32 -0
  17. package/dist/elements/cfpb-form-alert/index.js.map +7 -0
  18. package/dist/elements/cfpb-form-choice/index.js +12 -3
  19. package/dist/elements/cfpb-form-choice/index.js.map +4 -4
  20. package/dist/elements/cfpb-form-search/index.js +41 -0
  21. package/dist/elements/cfpb-form-search/index.js.map +7 -0
  22. package/dist/elements/cfpb-form-search-input/index.js +41 -0
  23. package/dist/elements/cfpb-form-search-input/index.js.map +7 -0
  24. package/dist/elements/cfpb-icon-text/index.js +3 -3
  25. package/dist/elements/cfpb-icon-text/index.js.map +3 -3
  26. package/dist/elements/cfpb-label/index.js +3 -3
  27. package/dist/elements/cfpb-label/index.js.map +2 -2
  28. package/dist/elements/cfpb-list/index.js +39 -0
  29. package/dist/elements/cfpb-list/index.js.map +7 -0
  30. package/dist/elements/cfpb-list-item/index.js +39 -0
  31. package/dist/elements/cfpb-list-item/index.js.map +7 -0
  32. package/dist/elements/cfpb-multiselect/index.js +13 -4
  33. package/dist/elements/cfpb-multiselect/index.js.map +4 -4
  34. package/dist/elements/cfpb-pagination/index.js +3 -3
  35. package/dist/elements/cfpb-pagination/index.js.map +2 -2
  36. package/dist/elements/cfpb-select/index.css +2 -0
  37. package/dist/elements/cfpb-select/index.css.map +7 -0
  38. package/dist/elements/cfpb-select/index.js +42 -0
  39. package/dist/elements/cfpb-select/index.js.map +7 -0
  40. package/dist/elements/cfpb-select-list/index.js +39 -0
  41. package/dist/elements/cfpb-select-list/index.js.map +7 -0
  42. package/dist/elements/cfpb-tag-filter/index.js +3 -3
  43. package/dist/elements/cfpb-tag-filter/index.js.map +3 -3
  44. package/dist/elements/cfpb-tag-group/index.js +3 -3
  45. package/dist/elements/cfpb-tag-group/index.js.map +4 -4
  46. package/dist/elements/cfpb-tag-topic/index.js +4 -4
  47. package/dist/elements/cfpb-tag-topic/index.js.map +1 -1
  48. package/dist/elements/index.css +2 -0
  49. package/dist/elements/index.css.map +7 -0
  50. package/dist/elements/index.js +7 -6
  51. package/dist/elements/index.js.map +4 -4
  52. package/dist/index.js +7 -6
  53. package/dist/index.js.map +4 -4
  54. package/dist/utilities/index.js +1 -1
  55. package/dist/utilities/index.js.map +3 -3
  56. package/package.json +1 -1
  57. package/src/components/cfpb-expandables/expandable.js +3 -0
  58. package/src/components/cfpb-forms/multiselect.js +1 -1
  59. package/src/elements/abstracts/custom-props.css +123 -0
  60. package/src/elements/abstracts/grid-mixins.scss +83 -0
  61. package/src/elements/abstracts/heading-mixins.scss +346 -0
  62. package/src/elements/abstracts/index.scss +7 -0
  63. package/src/elements/abstracts/media-queries.scss +35 -0
  64. package/src/elements/abstracts/sizing-vars.scss +65 -0
  65. package/src/elements/abstracts/vars-breakpoints.scss +16 -0
  66. package/src/elements/abstracts/vars.css +79 -0
  67. package/src/elements/base/base.scss +375 -0
  68. package/src/elements/base/font.scss +27 -0
  69. package/src/elements/base/index.scss +3 -0
  70. package/src/elements/base/normalize.scss +290 -0
  71. package/src/elements/cfpb-button/cfpb-button-group.scss +10 -0
  72. package/src/elements/cfpb-button/cfpb-button-link.scss +96 -0
  73. package/src/elements/cfpb-button/cfpb-button.component.scss +11 -4
  74. package/src/elements/cfpb-button/cfpb-button.scss +222 -0
  75. package/src/elements/cfpb-button/index.js +28 -29
  76. package/src/elements/cfpb-button/vars.css +30 -0
  77. package/src/elements/cfpb-checkbox-icon/cfpb-checkbox-icon.component.scss +88 -0
  78. package/src/elements/cfpb-checkbox-icon/index.js +104 -0
  79. package/src/elements/cfpb-expandable/cfpb-expandable.component.scss +218 -0
  80. package/src/elements/cfpb-expandable/index.js +127 -0
  81. package/src/elements/cfpb-file-upload/cfpb-file-upload.component.scss +2 -2
  82. package/src/elements/cfpb-file-upload/index.js +16 -18
  83. package/src/elements/cfpb-form-alert/cfpb-form-alert.component.scss +36 -0
  84. package/src/elements/cfpb-form-alert/index.js +55 -0
  85. package/src/elements/cfpb-form-choice/cfpb-form-choice.component.scss +42 -81
  86. package/src/elements/cfpb-form-choice/index.js +58 -18
  87. package/src/elements/cfpb-form-search/cfpb-form-search.component.scss +54 -0
  88. package/src/elements/cfpb-form-search/index.js +194 -0
  89. package/src/elements/cfpb-form-search-input/cfpb-form-search-input.component.scss +217 -0
  90. package/src/elements/cfpb-form-search-input/index.js +136 -0
  91. package/src/elements/cfpb-icon-text/cfpb-icon-text.component.scss +32 -39
  92. package/src/elements/cfpb-icon-text/index.js +32 -104
  93. package/src/elements/cfpb-label/cfpb-label.component.scss +2 -2
  94. package/src/elements/cfpb-label/index.js +6 -9
  95. package/src/elements/cfpb-list/cfpb-list.component.scss +23 -0
  96. package/src/elements/cfpb-list/index.js +357 -0
  97. package/src/elements/cfpb-list/index.spec.js +169 -0
  98. package/src/elements/cfpb-list-item/cfpb-list-item.component.scss +69 -0
  99. package/src/elements/cfpb-list-item/index.js +215 -0
  100. package/src/elements/cfpb-pagination/cfpb-pagination.component.scss +2 -7
  101. package/src/elements/cfpb-pagination/index.js +6 -8
  102. package/src/elements/cfpb-select/cfpb-select.component.scss +241 -0
  103. package/src/elements/cfpb-select/index.js +381 -0
  104. package/src/elements/cfpb-tag-filter/cfpb-tag-filter.component.scss +6 -3
  105. package/src/elements/cfpb-tag-filter/index.js +15 -7
  106. package/src/elements/cfpb-tag-group/cfpb-tag-group.component.scss +2 -2
  107. package/src/elements/cfpb-tag-group/index.js +53 -6
  108. package/src/elements/cfpb-tag-topic/index.js +5 -7
  109. package/src/elements/cfpb-utilities/parse-child-data.js +50 -0
  110. package/src/elements/cfpb-utilities/parse-child-data.spec.js +56 -0
  111. package/src/elements/cfpb-utilities/search-service.js +46 -0
  112. package/src/elements/cfpb-utilities/search-service.spec.js +138 -0
  113. package/src/elements/cfpb-utilities/transition/transition.scss +98 -0
  114. package/src/elements/index.js +7 -1
  115. package/src/index.scss +11 -0
  116. package/src/tokens/abstracts/custom-props.json +1642 -0
  117. package/src/tokens/abstracts/vars.json +1319 -0
  118. package/src/tokens/cfpb-button/vars.json +436 -0
  119. package/src/utilities/transition/max-height-transition.js +74 -0
  120. package/dist/elements/cfpb-checkbox/index.js +0 -29
  121. package/src/elements/cfpb-multiselect/cfpb-multiselect.component.scss +0 -225
  122. package/src/elements/cfpb-multiselect/index.js +0 -444
  123. package/src/elements/cfpb-multiselect/multiselect-model.js +0 -288
  124. package/src/elements/cfpb-multiselect/multiselect-model.spec.js +0 -236
@@ -1,225 +0,0 @@
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
- }
@@ -1,444 +0,0 @@
1
- import { html, LitElement, css, unsafeCSS } from 'lit';
2
- import styles from './cfpb-multiselect.component.scss';
3
- import { MultiselectModel } from './multiselect-model.js';
4
- import { CfpbFormChoice } from '../cfpb-form-choice';
5
- import { CfpbLabel } from '../cfpb-label';
6
-
7
- // Constants for direction.
8
- const DIR_PREV = 'prev';
9
- const DIR_NEXT = 'next';
10
-
11
- // Constants for key binding.
12
- const KEY_RETURN = 'Enter';
13
- const KEY_ESCAPE = 'Escape';
14
- const KEY_UP = 'ArrowUp';
15
- const KEY_DOWN = 'ArrowDown';
16
- const KEY_TAB = 'Tab';
17
-
18
- /**
19
- *
20
- * @element cfpb-multiselect.
21
- * @slot - The main content for the upload button.
22
- */
23
- export class CfpbMultiselect extends LitElement {
24
- static styles = css`
25
- ${unsafeCSS(styles)}
26
- `;
27
-
28
- static get properties() {
29
- return {
30
- // Other properties.
31
- name: { type: String },
32
- options: { type: Array, state: true },
33
- selectedLabel: { type: String, state: true },
34
- label: { type: String, attribute: true },
35
- };
36
- }
37
-
38
- // DOM references.
39
- #containerDom;
40
- #fieldsetDom;
41
- #searchDom;
42
- #optionsDom;
43
- #optionItemDoms;
44
-
45
- #model;
46
- #isBlurSkipped;
47
-
48
- constructor() {
49
- super();
50
- this.options = [];
51
- this.selectedLabel = '';
52
- this.#isBlurSkipped = false;
53
- }
54
-
55
- firstUpdated() {
56
- // Set DOM references.
57
- const root = this.renderRoot;
58
- this.#containerDom = root.querySelector('.o-multiselect');
59
- this.#fieldsetDom = root.querySelector('fieldset');
60
- this.#searchDom = root.querySelector('input');
61
- this.#optionsDom = root.querySelector('ul');
62
- }
63
-
64
- #slotChanged() {
65
- this.#initializeFromLightDom();
66
-
67
- this.#model = new MultiselectModel(this.options, this.name, {}).init();
68
-
69
- // Wait for lit to finish its render cycle so we can query the list items.
70
- this.updateComplete.then(() => {
71
- this.#optionItemDoms = Array.from(this.renderRoot.querySelectorAll('li'));
72
- });
73
- }
74
-
75
- #initializeFromLightDom() {
76
- const fallbackSelect = this.querySelector('select');
77
- if (fallbackSelect) {
78
- this.name = fallbackSelect.name;
79
-
80
- // Read options.
81
- let index = 0;
82
- this.options = Array.from(fallbackSelect.options).map((opt) => ({
83
- value: opt.value,
84
- label: opt.label,
85
- text: opt.text,
86
- checked: opt.selected,
87
- index: index++,
88
- }));
89
-
90
- this.selectedLabel = fallbackSelect.selectedOptions[0]?.label || '';
91
-
92
- // Remove or hide the fallback <select>
93
- fallbackSelect.style.display = 'none';
94
- }
95
- }
96
-
97
- /**
98
- * Set the filtered matched state.
99
- */
100
- #filterMatches() {
101
- this.#optionsDom.classList.remove('u-no-results');
102
- this.#optionsDom.classList.add('u-filtered');
103
-
104
- let filteredIndices = this.#model.lastFilterIndicesList;
105
- for (let i = 0, len = filteredIndices.length; i < len; i++) {
106
- this.#optionItemDoms[filteredIndices[i]].classList.remove(
107
- 'u-filter-match',
108
- );
109
- }
110
-
111
- filteredIndices = this.#model.filterIndicesList;
112
- for (let j = 0, len = filteredIndices.length; j < len; j++) {
113
- this.#optionItemDoms[filteredIndices[j]].classList.add('u-filter-match');
114
- }
115
- }
116
-
117
- /**
118
- * Resets the filtered option list.
119
- */
120
- #resetFilter() {
121
- this.#optionsDom.classList.remove('u-filtered', 'u-no-results');
122
-
123
- for (let i = 0, len = this.#optionsDom.children.length; i < len; i++) {
124
- this.#optionsDom.children[i].classList.remove('u-filter-match');
125
- }
126
-
127
- this.#model.clearFilter();
128
- }
129
-
130
- /**
131
- * Updates the list of options to show the user there
132
- * are no matching results.
133
- */
134
- #filterNoMatches() {
135
- this.#optionsDom.classList.add('u-no-results');
136
- this.#optionsDom.classList.remove('u-filtered');
137
- }
138
-
139
- /**
140
- * Filter the options list.
141
- * Every time we filter we have two lists of indices:
142
- * - The matching options (filterIndices).
143
- * - The matching options of the last filter (_lastFilterIndices).
144
- * We need to turn off the filter for any of the last filter matches
145
- * that are not in the new set, and turn on the filter for the matches
146
- * that are not in the last set.
147
- * @param {Array} filterIndices - List of indices to filter from the options.
148
- * @returns {boolean} True if options are filtered, false otherwise.
149
- */
150
- #filterList(filterIndices) {
151
- if (filterIndices.length > 0) {
152
- this.#filterMatches();
153
- return true;
154
- }
155
-
156
- this.#filterNoMatches();
157
- return false;
158
- }
159
-
160
- /**
161
- * Evaluates the list of options based on the user's query in the
162
- * search input.
163
- * @param {string} value - Text the user has entered in the search query.
164
- */
165
- #evaluate(value) {
166
- this.#resetFilter();
167
- this.#model.resetIndex();
168
- const matchedIndices = this.#model.filterIndices(value);
169
- this.#filterList(matchedIndices);
170
- }
171
-
172
- /**
173
- * Expand the multiselect drop down.
174
- */
175
- expand() {
176
- this.#containerDom.classList.add('u-active');
177
- this.#fieldsetDom.classList.remove('u-invisible');
178
- this.#fieldsetDom.setAttribute('aria-hidden', false);
179
-
180
- const event = new Event('expandbegin', { bubbles: true, composed: true });
181
- this.dispatchEvent(event);
182
- }
183
-
184
- /**
185
- * Collapse the multiselect drop down.
186
- */
187
- collapse() {
188
- this.#containerDom.classList.remove('u-active');
189
- this.#fieldsetDom.classList.add('u-invisible');
190
- this.#fieldsetDom.setAttribute('aria-hidden', true);
191
- this.#model.resetIndex();
192
-
193
- const event = new Event('collapsebegin', { bubbles: true, composed: true });
194
- this.dispatchEvent(event);
195
- }
196
-
197
- /**
198
- * Highlights an option in the list.
199
- * @param {string} direction
200
- * Direction to highlight compared to the current focus.
201
- */
202
- #highlight(direction) {
203
- if (direction === DIR_NEXT) {
204
- this.#model.index = this.#model.index + 1;
205
- } else if (direction === DIR_PREV) {
206
- this.#model.index = this.#model.index - 1;
207
- }
208
-
209
- const index = this.#model.index;
210
- if (index > -1) {
211
- let filteredIndex = index;
212
- const filterIndices = this.#model.filterIndicesList;
213
- if (filterIndices.length > 0) {
214
- filteredIndex = filterIndices[index];
215
- }
216
- const option = this.#model.getOption(filteredIndex);
217
- const value = option.value;
218
- const item = this.#optionsDom.querySelector(
219
- '[data-option="' + value + '"]',
220
- );
221
- this.#isBlurSkipped = true;
222
- item.focus();
223
- } else {
224
- this.#isBlurSkipped = false;
225
- this.#searchDom.focus();
226
- }
227
- }
228
-
229
- /**
230
- * Resets the search input and filtering.
231
- */
232
- #resetSearch() {
233
- this.#searchDom.value = '';
234
- this.#resetFilter();
235
- }
236
-
237
- /**
238
- * Tracks a user's selections and updates the list in the dom.
239
- * @param {number} optionIndex - The index position of the chosen option.
240
- */
241
- #updateSelections(optionIndex) {
242
- const option =
243
- this.#model.getOption(optionIndex) ||
244
- this.#model.getOption(this.#model.index);
245
-
246
- if (option) {
247
- if (option.checked) {
248
- if (this.#optionsDom.classList.contains('u-max-selections')) {
249
- this.#optionsDom.classList.remove('u-max-selections');
250
- }
251
- }
252
- }
253
-
254
- if (this.#model.isAtMaxSelections() && !option.checked) return;
255
-
256
- if (this.#optionsDom.classList.contains('u-max-selections')) {
257
- this.#optionsDom.classList.remove('u-max-selections');
258
- }
259
-
260
- this.#model.toggleOption(optionIndex);
261
-
262
- this.#model.resetIndex();
263
- this.#isBlurSkipped = false;
264
-
265
- if (this.#fieldsetDom.getAttribute('aria-hidden') === 'false') {
266
- this.#searchDom.focus();
267
- }
268
-
269
- // Spread is used to create a new array reference,
270
- // which triggers a lit lifecycle update.
271
- this.options = [...this.#model.options];
272
-
273
- if (this.#model.isAtMaxSelections()) {
274
- this.#optionsDom.classList.add('u-max-selections');
275
- }
276
- }
277
-
278
- #searchInputFocus() {
279
- if (this.#fieldsetDom.getAttribute('aria-hidden') === 'true') {
280
- this.expand();
281
- }
282
- }
283
-
284
- #searchInputBlur() {
285
- if (
286
- !this.#isBlurSkipped &&
287
- this.#fieldsetDom.getAttribute('aria-hidden') === 'false'
288
- ) {
289
- this.collapse();
290
- }
291
- }
292
-
293
- #searchInputKeyDown(event) {
294
- const key = event.key;
295
- if (
296
- this.#fieldsetDom.getAttribute('aria-hidden') === 'true' &&
297
- key !== KEY_TAB
298
- ) {
299
- this.expand();
300
- }
301
-
302
- if (key === KEY_RETURN) {
303
- event.preventDefault();
304
- this.#highlight(DIR_NEXT);
305
- } else if (key === KEY_ESCAPE) {
306
- this.#resetSearch();
307
- this.collapse();
308
- } else if (key === KEY_DOWN) {
309
- this.#highlight(DIR_NEXT);
310
- } else if (
311
- key === KEY_TAB &&
312
- !event.shiftKey &&
313
- this.#fieldsetDom.getAttribute('aria-hidden') === 'false'
314
- ) {
315
- this.collapse();
316
- }
317
- }
318
-
319
- /**
320
- * Handles checkbox change event.
321
- * @param {number} index - The index position of the checkbox within the list.
322
- */
323
- #onChangeCheckbox(index) {
324
- //opt.checked = !opt.checked;
325
- this.#resetSearch();
326
- this.#updateSelections(index);
327
- }
328
-
329
- /**
330
- * @param {MouseEvent} event - The key down event.
331
- */
332
- #onKeyDownList(event) {
333
- const key = event.key;
334
- const target = event.target;
335
- const checked = target.checked;
336
-
337
- if (key === KEY_RETURN) {
338
- event.preventDefault();
339
-
340
- /* Programmatically checking a checkbox does not fire a change event
341
- so we need to manually create an event and dispatch it from the input.
342
- */
343
- target.checked = !checked;
344
- //const evt = new Event('change', { bubbles: false, cancelable: true });
345
- //target.dispatchEvent(evt);
346
- } else if (key === KEY_ESCAPE) {
347
- this.#searchDom.focus();
348
- this.collapse();
349
- } else if (key === KEY_UP) {
350
- this.#highlight(DIR_PREV);
351
- } else if (key === KEY_DOWN) {
352
- this.#highlight(DIR_NEXT);
353
- }
354
- }
355
-
356
- render() {
357
- // Track the index position of the option in the list.
358
- let index = 0;
359
-
360
- const renderTags = [];
361
- const renderChoices = [];
362
-
363
- this.options.map((item) => {
364
- renderTags.push(html`${this.#renderTag(item)}`);
365
- renderChoices.push(html`${this.#renderChoice(item, index++)}`);
366
- });
367
-
368
- const label = this.label || 'Select options';
369
-
370
- return html`
371
- <!-- Fallback content like <select> and <options>s -->
372
- <slot @slotchange=${this.#slotChanged}></slot>
373
-
374
- <cfpb-label for="search-input">
375
- <div slot="label">${label}</div>
376
- </cfpb-label>
377
- <div class="o-multiselect">
378
- <cfpb-tag-group
379
- @tag-click=${(evt) => {
380
- this.#updateSelections(
381
- Number(evt.detail.target.getAttribute('data-index')),
382
- );
383
- }}
384
- >
385
- ${renderTags}
386
- </cfpb-tag-group>
387
- <header>
388
- <input
389
- id="search-input"
390
- type="text"
391
- autocomplete="off"
392
- aria-label="${label}"
393
- value=${this.selectedLabel || ''}
394
- @input=${(event) => this.#evaluate(event.target.value)}
395
- @focus=${this.#searchInputFocus}
396
- @blur=${this.#searchInputBlur}
397
- @keydown=${this.#searchInputKeyDown}
398
- />
399
- </header>
400
-
401
- <fieldset class="u-invisible" aria-hidden="true">
402
- <ul role="listbox" @keydown=${this.#onKeyDownList}>
403
- ${renderChoices}
404
- </ul>
405
- </fieldset>
406
- </div>
407
- `;
408
- }
409
-
410
- #renderTag(item) {
411
- let htmlSnippet = html``;
412
- if (item.checked === true) {
413
- htmlSnippet = html`<cfpb-tag-filter data-index="${item.index}"
414
- >${item.value}</cfpb-tag-filter
415
- >`;
416
- }
417
-
418
- return htmlSnippet;
419
- }
420
-
421
- #renderChoice(opt, index) {
422
- return html`
423
- <li role="option">
424
- <cfpb-form-choice
425
- data-option="${opt.label}"
426
- inlist="true"
427
- .checked="${opt.checked}"
428
- @change=${() => this.#onChangeCheckbox(index)}
429
- @mousedown=${() => (this.#isBlurSkipped = true)}
430
- >
431
- ${opt.label}
432
- </cfpb-form-choice>
433
- </li>
434
- `;
435
- }
436
-
437
- static init() {
438
- CfpbFormChoice.init();
439
- CfpbLabel.init();
440
-
441
- window.customElements.get('cfpb-multiselect') ||
442
- window.customElements.define('cfpb-multiselect', CfpbMultiselect);
443
- }
444
- }