@cfpb/cfpb-design-system 4.2.3 → 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 (147) hide show
  1. package/CHANGELOG.md +178 -1
  2. package/dist/base/index.css +1 -1
  3. package/dist/base/index.css.map +2 -2
  4. package/dist/base/index.js.map +1 -1
  5. package/dist/components/cfpb-buttons/index.css +1 -1
  6. package/dist/components/cfpb-buttons/index.css.map +2 -2
  7. package/dist/components/cfpb-buttons/index.js.map +1 -1
  8. package/dist/components/cfpb-expandables/index.css.map +1 -1
  9. package/dist/components/cfpb-expandables/index.js +1 -1
  10. package/dist/components/cfpb-expandables/index.js.map +3 -3
  11. package/dist/components/cfpb-forms/index.css +1 -1
  12. package/dist/components/cfpb-forms/index.css.map +2 -2
  13. package/dist/components/cfpb-forms/index.js +1 -1
  14. package/dist/components/cfpb-forms/index.js.map +2 -2
  15. package/dist/components/cfpb-layout/index.css +1 -1
  16. package/dist/components/cfpb-layout/index.css.map +1 -1
  17. package/dist/components/cfpb-notifications/index.css.map +1 -1
  18. package/dist/components/cfpb-pagination/index.css.map +1 -1
  19. package/dist/components/cfpb-tables/index.css.map +1 -1
  20. package/dist/components/cfpb-typography/index.css +1 -1
  21. package/dist/components/cfpb-typography/index.css.map +2 -2
  22. package/dist/components/cfpb-typography/index.js.map +1 -1
  23. package/dist/elements/cfpb-button/index.js +4 -4
  24. package/dist/elements/cfpb-button/index.js.map +3 -3
  25. package/dist/elements/cfpb-checkbox-icon/index.js +29 -0
  26. package/dist/elements/{cfpb-checkbox → cfpb-checkbox-icon}/index.js.map +4 -4
  27. package/dist/elements/cfpb-expandable/index.css +2 -0
  28. package/dist/elements/cfpb-expandable/index.css.map +7 -0
  29. package/dist/elements/cfpb-expandable/index.js +33 -0
  30. package/dist/elements/cfpb-expandable/index.js.map +7 -0
  31. package/dist/elements/cfpb-file-upload/index.js +4 -4
  32. package/dist/elements/cfpb-file-upload/index.js.map +3 -3
  33. package/dist/elements/cfpb-form-alert/index.js +32 -0
  34. package/dist/elements/cfpb-form-alert/index.js.map +7 -0
  35. package/dist/elements/cfpb-form-choice/index.js +12 -3
  36. package/dist/elements/cfpb-form-choice/index.js.map +4 -4
  37. package/dist/elements/cfpb-form-search/index.js +41 -0
  38. package/dist/elements/cfpb-form-search/index.js.map +7 -0
  39. package/dist/elements/cfpb-form-search-input/index.js +41 -0
  40. package/dist/elements/cfpb-form-search-input/index.js.map +7 -0
  41. package/dist/elements/cfpb-icon-text/index.js +3 -3
  42. package/dist/elements/cfpb-icon-text/index.js.map +3 -3
  43. package/dist/elements/cfpb-label/index.js +3 -3
  44. package/dist/elements/cfpb-label/index.js.map +2 -2
  45. package/dist/elements/cfpb-list/index.js +39 -0
  46. package/dist/elements/cfpb-list/index.js.map +7 -0
  47. package/dist/elements/cfpb-list-item/index.js +39 -0
  48. package/dist/elements/cfpb-list-item/index.js.map +7 -0
  49. package/dist/elements/cfpb-multiselect/index.js +13 -4
  50. package/dist/elements/cfpb-multiselect/index.js.map +4 -4
  51. package/dist/elements/cfpb-pagination/index.js +3 -3
  52. package/dist/elements/cfpb-pagination/index.js.map +2 -2
  53. package/dist/elements/cfpb-select/index.css +2 -0
  54. package/dist/elements/cfpb-select/index.css.map +7 -0
  55. package/dist/elements/cfpb-select/index.js +42 -0
  56. package/dist/elements/cfpb-select/index.js.map +7 -0
  57. package/dist/elements/cfpb-select-list/index.js +39 -0
  58. package/dist/elements/cfpb-select-list/index.js.map +7 -0
  59. package/dist/elements/cfpb-tag-filter/index.js +3 -3
  60. package/dist/elements/cfpb-tag-filter/index.js.map +3 -3
  61. package/dist/elements/cfpb-tag-group/index.js +3 -3
  62. package/dist/elements/cfpb-tag-group/index.js.map +4 -4
  63. package/dist/elements/cfpb-tag-topic/index.js +4 -4
  64. package/dist/elements/cfpb-tag-topic/index.js.map +2 -2
  65. package/dist/elements/index.css +2 -0
  66. package/dist/elements/index.css.map +7 -0
  67. package/dist/elements/index.js +7 -6
  68. package/dist/elements/index.js.map +4 -4
  69. package/dist/index.css +1 -1
  70. package/dist/index.css.map +2 -2
  71. package/dist/index.js +7 -6
  72. package/dist/index.js.map +4 -4
  73. package/dist/utilities/index.css.map +1 -1
  74. package/dist/utilities/index.js +1 -1
  75. package/dist/utilities/index.js.map +3 -3
  76. package/package.json +1 -1
  77. package/src/base/base.scss +1 -1
  78. package/src/components/cfpb-buttons/button-link.scss +0 -1
  79. package/src/components/cfpb-expandables/expandable.js +3 -0
  80. package/src/components/cfpb-forms/multiselect.js +1 -1
  81. package/src/components/cfpb-typography/mixins.scss +3 -0
  82. package/src/elements/abstracts/custom-props.css +123 -0
  83. package/src/elements/abstracts/grid-mixins.scss +83 -0
  84. package/src/elements/abstracts/heading-mixins.scss +346 -0
  85. package/src/elements/abstracts/index.scss +7 -0
  86. package/src/elements/abstracts/media-queries.scss +35 -0
  87. package/src/elements/abstracts/sizing-vars.scss +65 -0
  88. package/src/elements/abstracts/vars-breakpoints.scss +16 -0
  89. package/src/elements/abstracts/vars.css +79 -0
  90. package/src/elements/base/base.scss +375 -0
  91. package/src/elements/base/font.scss +27 -0
  92. package/src/elements/base/index.scss +3 -0
  93. package/src/elements/base/normalize.scss +290 -0
  94. package/src/elements/cfpb-button/cfpb-button-group.scss +10 -0
  95. package/src/elements/cfpb-button/cfpb-button-link.scss +96 -0
  96. package/src/elements/cfpb-button/cfpb-button.component.scss +11 -4
  97. package/src/elements/cfpb-button/cfpb-button.scss +222 -0
  98. package/src/elements/cfpb-button/index.js +28 -29
  99. package/src/elements/cfpb-button/vars.css +30 -0
  100. package/src/elements/cfpb-checkbox-icon/cfpb-checkbox-icon.component.scss +88 -0
  101. package/src/elements/cfpb-checkbox-icon/index.js +104 -0
  102. package/src/elements/cfpb-expandable/cfpb-expandable.component.scss +218 -0
  103. package/src/elements/cfpb-expandable/index.js +127 -0
  104. package/src/elements/cfpb-file-upload/cfpb-file-upload.component.scss +2 -2
  105. package/src/elements/cfpb-file-upload/index.js +16 -18
  106. package/src/elements/cfpb-form-alert/cfpb-form-alert.component.scss +36 -0
  107. package/src/elements/cfpb-form-alert/index.js +55 -0
  108. package/src/elements/cfpb-form-choice/cfpb-form-choice.component.scss +42 -81
  109. package/src/elements/cfpb-form-choice/index.js +58 -18
  110. package/src/elements/cfpb-form-search/cfpb-form-search.component.scss +54 -0
  111. package/src/elements/cfpb-form-search/index.js +194 -0
  112. package/src/elements/cfpb-form-search-input/cfpb-form-search-input.component.scss +217 -0
  113. package/src/elements/cfpb-form-search-input/index.js +136 -0
  114. package/src/elements/cfpb-icon-text/cfpb-icon-text.component.scss +32 -39
  115. package/src/elements/cfpb-icon-text/index.js +32 -104
  116. package/src/elements/cfpb-label/cfpb-label.component.scss +2 -2
  117. package/src/elements/cfpb-label/index.js +6 -9
  118. package/src/elements/cfpb-list/cfpb-list.component.scss +23 -0
  119. package/src/elements/cfpb-list/index.js +357 -0
  120. package/src/elements/cfpb-list/index.spec.js +169 -0
  121. package/src/elements/cfpb-list-item/cfpb-list-item.component.scss +69 -0
  122. package/src/elements/cfpb-list-item/index.js +215 -0
  123. package/src/elements/cfpb-pagination/cfpb-pagination.component.scss +2 -7
  124. package/src/elements/cfpb-pagination/index.js +6 -8
  125. package/src/elements/cfpb-select/cfpb-select.component.scss +241 -0
  126. package/src/elements/cfpb-select/index.js +381 -0
  127. package/src/elements/cfpb-tag-filter/cfpb-tag-filter.component.scss +6 -3
  128. package/src/elements/cfpb-tag-filter/index.js +15 -7
  129. package/src/elements/cfpb-tag-group/cfpb-tag-group.component.scss +2 -2
  130. package/src/elements/cfpb-tag-group/index.js +53 -6
  131. package/src/elements/cfpb-tag-topic/index.js +5 -7
  132. package/src/elements/cfpb-utilities/parse-child-data.js +50 -0
  133. package/src/elements/cfpb-utilities/parse-child-data.spec.js +56 -0
  134. package/src/elements/cfpb-utilities/search-service.js +46 -0
  135. package/src/elements/cfpb-utilities/search-service.spec.js +138 -0
  136. package/src/elements/cfpb-utilities/transition/transition.scss +98 -0
  137. package/src/elements/index.js +7 -1
  138. package/src/index.scss +11 -0
  139. package/src/tokens/abstracts/custom-props.json +1642 -0
  140. package/src/tokens/abstracts/vars.json +1319 -0
  141. package/src/tokens/cfpb-button/vars.json +436 -0
  142. package/src/utilities/transition/max-height-transition.js +74 -0
  143. package/dist/elements/cfpb-checkbox/index.js +0 -29
  144. package/src/elements/cfpb-multiselect/cfpb-multiselect.component.scss +0 -225
  145. package/src/elements/cfpb-multiselect/index.js +0 -444
  146. package/src/elements/cfpb-multiselect/multiselect-model.js +0 -288
  147. package/src/elements/cfpb-multiselect/multiselect-model.spec.js +0 -236
@@ -0,0 +1,241 @@
1
+ @use 'sass:math';
2
+ @use '@cfpb/cfpb-design-system/src/abstracts' as *;
3
+ @use '@cfpb/cfpb-design-system/src/base' as *;
4
+ @use '@cfpb/cfpb-design-system/src/utilities' as *;
5
+ @use '@cfpb/cfpb-design-system/src/components/cfpb-icons/icon';
6
+ @use '../cfpb-utilities/transition/transition.scss' as *;
7
+
8
+ :host {
9
+ // Theme
10
+ --select-border-default: var(--gray-60); // $input-border
11
+ --select-border-width-default: 1px;
12
+ --select-border-error: var(--red);
13
+ --select-border-width-error: 2px;
14
+ --select-border-hover-default: var(--pacific); // $input-border-hover
15
+ --select-border-focus-default: var(--pacific); // $input-border-focused
16
+ --select-bg-disabled-default: var(--gray-10); // $input-bg-disabled
17
+ --select-icon-bg-default: var(--gray-10);
18
+ --select-text-disabled-default: var(--gray-dark); // $input-text-disabled
19
+
20
+ // Private variables.
21
+ --select-border: var(--select-border-default);
22
+ --select-border-width: var(--select-border-width-default);
23
+ --select-border-hover: var(--select-border-hover-default);
24
+ --select-border-focus: var(--select-border-focus-default);
25
+
26
+ // Hide light DOM content.
27
+ ::slotted(ul),
28
+ ::slotted(ol) {
29
+ display: none !important;
30
+ }
31
+
32
+ //
33
+ // Custom scrollbar
34
+ //
35
+
36
+ // Reset lack of scrollbar on Apple devices.
37
+ ::-webkit-scrollbar {
38
+ appearance: none;
39
+ width: 7px;
40
+ }
41
+
42
+ ::-webkit-scrollbar-thumb {
43
+ background-color: var(--pacific-20);
44
+ box-shadow: 0 0 1px var(--gray-10);
45
+ }
46
+
47
+ //
48
+ // Base styling
49
+ //
50
+
51
+ button {
52
+ // This line-height settings is larger than base.scss to
53
+ // accommodate the SVG height.
54
+ line-height: math.div(21px, $base-font-size-px);
55
+
56
+ // Remove default focus ring.
57
+ outline: none;
58
+ }
59
+
60
+ //
61
+ // Recommended select pattern
62
+ //
63
+
64
+ .o-select {
65
+ position: relative;
66
+ display: grid;
67
+ grid-template-columns: 1fr 38px;
68
+
69
+ &:hover {
70
+ border-color: var(--select-border-hover);
71
+ box-shadow: 0 0 0 1px var(--select-border-hover);
72
+ }
73
+
74
+ &:focus,
75
+ &:focus-within {
76
+ outline: 1px dotted var(--select-border-focus);
77
+ outline-offset: 2px;
78
+
79
+ box-shadow: 0 0 0 1px var(--select-border-focus);
80
+ }
81
+
82
+ //
83
+ // Header
84
+ //
85
+
86
+ &__header {
87
+ padding: 0;
88
+ border: 0;
89
+ background-color: transparent;
90
+ cursor: pointer;
91
+
92
+ display: flex;
93
+
94
+ // Single select.
95
+ &:has(.o-select__label) {
96
+ justify-content: space-between;
97
+ align-items: center;
98
+ gap: 10px;
99
+ }
100
+
101
+ // Multiselect.
102
+ cfpb-form-search-input {
103
+ width: 100%;
104
+ }
105
+
106
+ .o-select__cue-close,
107
+ .o-select__cue-open {
108
+ display: none;
109
+ }
110
+
111
+ &[aria-expanded='false'] .o-select__cue-open {
112
+ display: block;
113
+ }
114
+
115
+ &[aria-expanded='true'] .o-select__cue-close {
116
+ display: block;
117
+ }
118
+ }
119
+
120
+ .no-results {
121
+ padding: 6.5px 10px;
122
+ }
123
+
124
+ // Using the button element with .o-select__header requires setting
125
+ // an explicit width.
126
+ button.o-select__header {
127
+ width: 100%;
128
+ text-align: left;
129
+ }
130
+
131
+ //
132
+ // select text elements
133
+ //
134
+
135
+ &__label {
136
+ // Grow to available width.
137
+ flex-grow: 1;
138
+ padding: math.div(7px, $base-font-size-px) + em
139
+ math.div(10px, $base-font-size-px) + em;
140
+
141
+ cursor: pointer;
142
+ }
143
+
144
+ &__cues {
145
+ align-self: center;
146
+ background: var(--select-icon-bg-default);
147
+ border-left: 1px solid var(--select-border-default);
148
+ padding: math.div(7px, $base-font-size-px) + em
149
+ math.div(10px, $base-font-size-px) + em;
150
+ }
151
+
152
+ &__content {
153
+ box-sizing: border-box;
154
+ overflow-x: hidden;
155
+ overflow-y: scroll;
156
+ position: absolute;
157
+ z-index: 10;
158
+ width: calc(100% + 2px);
159
+ left: -1px;
160
+ border: 2px solid var(--pacific);
161
+ background-color: var(--white);
162
+ padding: 0;
163
+
164
+ &::after {
165
+ padding-bottom: math.div(15px, $base-font-size-px) + em;
166
+ width: 100%;
167
+ }
168
+ }
169
+
170
+ //
171
+ // select with a border modifier
172
+ //
173
+
174
+ &--border {
175
+ border: 1px solid var(--select-border);
176
+ }
177
+
178
+ @media print {
179
+ // Hide the interactive select cues when printing
180
+ &__header[aria-expanded='true'] &__cue-close,
181
+ &__header[aria-expanded='false'] &__cue-open {
182
+ display: none;
183
+ } // Ensure all selects are expanded when printing.
184
+ // To accommodate print stylesheets that display the raw URL after links,
185
+ // set an enormous max height to accommodate selects that have a lot of links.
186
+ &__content[aria-expanded='false'] {
187
+ display: block;
188
+ max-height: 99999px !important;
189
+ }
190
+ }
191
+
192
+ &--down {
193
+ &::before {
194
+ bottom: -1px;
195
+ }
196
+
197
+ .o-select__content {
198
+ border-top: 0;
199
+ top: 35px;
200
+ }
201
+ }
202
+
203
+ &--up {
204
+ .o-select__content {
205
+ border-bottom: 0;
206
+ top: unset;
207
+ }
208
+ }
209
+ }
210
+ }
211
+
212
+ // The drop-down is expanded.
213
+ :host([open]) {
214
+ .o-select {
215
+ // The divider between __header and __content.
216
+ &::before {
217
+ position: absolute;
218
+ z-index: 11;
219
+ content: '';
220
+ display: block;
221
+ width: 100%;
222
+ border-top: 1px solid var(--select-border);
223
+ pointer-events: none;
224
+ }
225
+ }
226
+ }
227
+
228
+ // Used when the set language reads right-to-left
229
+ html[lang='ar'] {
230
+ :host {
231
+ .o-select {
232
+ &__header {
233
+ text-align: right;
234
+ }
235
+
236
+ &__cues {
237
+ text-align: left;
238
+ }
239
+ }
240
+ }
241
+ }
@@ -0,0 +1,381 @@
1
+ import { html, LitElement, css, unsafeCSS, nothing } from 'lit';
2
+ import { ref, createRef } from 'lit/directives/ref.js';
3
+ import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
4
+ import styles from './cfpb-select.component.scss';
5
+ import expandIcon from '../../components/cfpb-icons/icons/down.svg';
6
+ import collapseIcon from '../../components/cfpb-icons/icons/up.svg';
7
+ import { CfpbFormSearchInput } from '../cfpb-form-search-input';
8
+ import { SearchService } from '../cfpb-utilities/search-service.js';
9
+ import { MaxHeightTransition } from '../../utilities/transition/max-height-transition';
10
+ import { FlyoutMenu } from '../../utilities/behavior/flyout-menu';
11
+ import { CfpbList } from '../cfpb-list';
12
+ import { CfpbTagGroup } from '../cfpb-tag-group';
13
+
14
+ /**
15
+ *
16
+ * @element cfpb-button
17
+ * @slot - The main content for the button.
18
+ */
19
+ export class CfpbSelect extends LitElement {
20
+ static styles = css`
21
+ ${unsafeCSS(styles)}
22
+ `;
23
+
24
+ // Flyout menu options.
25
+ #flyoutMenu;
26
+ #transition;
27
+ #search;
28
+ #root = createRef();
29
+ #headerDom = createRef();
30
+ #contentDom = createRef();
31
+ #input = createRef();
32
+ #tagGroup = createRef();
33
+ #list = createRef();
34
+ #displayLabel = createRef();
35
+ #boundOnOutsideClick;
36
+ #noResults = false;
37
+
38
+ /**
39
+ * @property {boolean} multiple - Whether the select supports multiple or not.
40
+ * @property {boolean} isExpanded - Whether the select is expanded or not.
41
+ * @property {Array} selectedTexts - Text of selected options.
42
+ * @returns {object} The map of properties.
43
+ */
44
+ static get properties() {
45
+ return {
46
+ multiple: { type: Boolean, reflect: true },
47
+ disabled: { type: Boolean },
48
+ validation: { type: String },
49
+ label: { type: String },
50
+ name: { type: String },
51
+ title: { type: Boolean, attribute: true },
52
+ value: { type: String },
53
+ maxlength: { type: Number },
54
+ placeholder: { type: String },
55
+ ariaLabelInput: { type: String, attribute: 'aria-label-input' },
56
+ ariaLabelList: { type: String, attribute: 'aria-label-list' },
57
+
58
+ isExpanded: { type: Boolean, attribute: 'open', reflect: true },
59
+ selectedTexts: { type: Array },
60
+ optionList: { type: Array },
61
+ };
62
+ }
63
+
64
+ constructor() {
65
+ super();
66
+
67
+ this.options = [];
68
+ this.selectedTexts = [];
69
+ this.optionList = [];
70
+
71
+ this.#boundOnOutsideClick = this.#onOutsideClick.bind(this);
72
+ }
73
+
74
+ firstUpdated() {
75
+ this.#initFlyoutMenu();
76
+ }
77
+
78
+ disconnectedCallback() {
79
+ document.removeEventListener('pointerdown', this.#boundOnOutsideClick);
80
+ super.disconnectedCallback();
81
+ }
82
+
83
+ #onOutsideClick(evt) {
84
+ const path = evt.composedPath();
85
+ if (!path.includes(this)) {
86
+ this.isExpanded = false;
87
+ }
88
+ }
89
+
90
+ #onSlotChange(evt) {
91
+ const slot = evt.target;
92
+
93
+ const list = slot
94
+ .assignedNodes({ flatten: true })
95
+ .filter(
96
+ (node) =>
97
+ node.nodeType === Node.ELEMENT_NODE &&
98
+ (node.tagName === 'UL' || node.tagName === 'OL'),
99
+ );
100
+
101
+ if (!list || !list[0]) {
102
+ return;
103
+ }
104
+
105
+ // Extract list items (with their text or link info)
106
+ const items = [...list[0].querySelectorAll('li')].map((li) => {
107
+ const checked = li.querySelector('b');
108
+ if (checked) {
109
+ return {
110
+ value: li.textContent.trim(),
111
+ checked: 'true',
112
+ };
113
+ }
114
+ return { value: li.textContent.trim() };
115
+ });
116
+
117
+ this.optionList = items;
118
+
119
+ this.#search = new SearchService(
120
+ items.map((item) => {
121
+ return item.value;
122
+ }),
123
+ );
124
+ }
125
+
126
+ #onInput(evt) {
127
+ this.#flyoutMenu.suspend();
128
+ if (!this.isExpanded) this.isExpanded = true;
129
+ const visibleItems = this.#list.value.filterItems(
130
+ this.#search.search(evt.target.value),
131
+ );
132
+
133
+ if (visibleItems.length === 0) {
134
+ this.#noResults = true;
135
+ this.requestUpdate();
136
+ } else {
137
+ this.#noResults = false;
138
+ this.requestUpdate();
139
+ this.#flyoutMenu.resume();
140
+ }
141
+ }
142
+
143
+ #onClear() {
144
+ this.#list.value.showAllItems();
145
+ }
146
+
147
+ #initFlyoutMenu() {
148
+ const root = this.#root.value;
149
+ const contentDom = this.#contentDom.value;
150
+
151
+ // If it's expanded we don't set an initial height,
152
+ // as it will be calculated internally.
153
+ const initialClass = this.isExpanded
154
+ ? MaxHeightTransition.CLASSES.MH_DEFAULT
155
+ : MaxHeightTransition.CLASSES.MH_ZERO;
156
+ this.#transition = new MaxHeightTransition(contentDom).init(initialClass);
157
+
158
+ this.#flyoutMenu = new FlyoutMenu(root);
159
+
160
+ this.#flyoutMenu.setTransition(
161
+ this.#transition,
162
+ this.#transition.maxHeightZero,
163
+ this.#transition.maxHeightDynamic,
164
+ );
165
+
166
+ this.#flyoutMenu.init(this.isExpanded);
167
+
168
+ // Add events.
169
+ this.#flyoutMenu.addEventListener('expandbegin', () => {
170
+ this.isExpanded = true;
171
+ contentDom.classList.remove('u-hidden');
172
+ this.dispatchEvent(
173
+ new CustomEvent('expandbegin', {
174
+ detail: { target: this },
175
+ bubbles: true,
176
+ composed: true,
177
+ }),
178
+ );
179
+ });
180
+ this.#flyoutMenu.addEventListener('collapseend', () => {
181
+ this.isExpanded = false;
182
+ contentDom.classList.add('u-hidden');
183
+
184
+ // Remove direction classes.
185
+ this.#root.value.classList.remove(`o-select--up`);
186
+ this.#root.value.classList.remove(`o-select--down`);
187
+ });
188
+
189
+ this.#transition.addEventListener('transitiondir', (evt) => {
190
+ this.#root.value.classList.add(`o-select--${evt.dir}`);
191
+ });
192
+ }
193
+
194
+ updated(changedProps) {
195
+ if (changedProps.has('isExpanded')) {
196
+ const oldVal = changedProps.get('isExpanded');
197
+ const newVal = this.isExpanded;
198
+
199
+ if (newVal !== oldVal) {
200
+ if (newVal) {
201
+ this.#flyoutMenu.expand();
202
+ document.addEventListener('pointerdown', this.#boundOnOutsideClick);
203
+ } else {
204
+ this.#flyoutMenu.collapse();
205
+ document.removeEventListener(
206
+ 'pointerdown',
207
+ this.#boundOnOutsideClick,
208
+ );
209
+ }
210
+ }
211
+ }
212
+ }
213
+
214
+ #onClick(evt) {
215
+ const target = evt.currentTarget;
216
+
217
+ if (this.multiple) {
218
+ if (target.tagName === 'CFPB-FORM-SEARCH-INPUT') {
219
+ if (this.isExpanded) this.#flyoutMenu.suspend();
220
+ else this.#flyoutMenu.expand();
221
+ } else {
222
+ this.#flyoutMenu.resume();
223
+ }
224
+ } else if (target.classList.contains('o-select__label')) {
225
+ this.#headerDom.value.focus();
226
+ this.isExpanded = !this.isExpanded;
227
+ }
228
+ }
229
+
230
+ #onItemClick() {
231
+ if (this.multiple) {
232
+ this.optionList = this.#list.value.childData;
233
+ this.requestUpdate();
234
+ } else {
235
+ const checkedItems = this.#list.value.checkedItems;
236
+ const selectedValue = checkedItems[0]?.value;
237
+
238
+ this.#displayLabel.value.innerHTML = selectedValue ? selectedValue : '';
239
+
240
+ // Update optionList so the selection persists.
241
+ this.optionList = this.optionList.map((item) => ({
242
+ ...item,
243
+ checked: item.value === selectedValue,
244
+ }));
245
+
246
+ this.requestUpdate();
247
+
248
+ // Now close the dropdown.
249
+ this.isExpanded = false;
250
+
251
+ // Move focus back to the header.
252
+ if (this.multiple) this.#input.value.focus();
253
+ else this.#headerDom.value.focus();
254
+ }
255
+ }
256
+
257
+ #onTagClick(evt) {
258
+ const tagList = this.#tagGroup.value.tagList.filter((item) => {
259
+ return item !== evt.detail.target;
260
+ });
261
+
262
+ this.optionList = this.optionList.map((item) => ({
263
+ ...item,
264
+ checked: !!tagList.find((tag) => tag.value === item.value),
265
+ }));
266
+
267
+ this.requestUpdate();
268
+ }
269
+
270
+ #onKeyDown(evt) {
271
+ switch (evt.key) {
272
+ case 'ArrowDown':
273
+ evt.preventDefault();
274
+ this.#contentDom.value.querySelector('cfpb-list-item').focus();
275
+ break;
276
+ }
277
+ }
278
+
279
+ render() {
280
+ return html`
281
+ <!--Light DOM content-->
282
+ <slot @slotchange=${this.#onSlotChange}></slot>
283
+
284
+ ${this.multiple
285
+ ? html`<cfpb-tag-group
286
+ ${ref(this.#tagGroup)}
287
+ .childData=${this.optionList
288
+ .filter((item) => {
289
+ return item.checked;
290
+ })
291
+ .map((item) => {
292
+ return { text: item.value, tagName: 'cfpb-tag-filter' };
293
+ })}
294
+ @tag-click=${this.#onTagClick}
295
+ >
296
+ </cfpb-tag-group>`
297
+ : nothing}
298
+
299
+ <div
300
+ class="o-select o-select--border"
301
+ data-js-hook="behavior_flyout-menu"
302
+ ${ref(this.#root)}
303
+ >
304
+ ${this.#renderInput()}
305
+
306
+ <button
307
+ class="o-select__header"
308
+ title="Expand content"
309
+ data-js-hook="behavior_flyout-menu_trigger"
310
+ ${ref(this.#headerDom)}
311
+ @keydown=${this.#onKeyDown}
312
+ >
313
+ <span class="o-select__cues" @click=${this.#onClick}>
314
+ <span class="o-select__cue-open" role="img" aria-label="Show">
315
+ ${unsafeSVG(expandIcon)}
316
+ <span class="u-visually-hidden">Show</span>
317
+ </span>
318
+ <span class="o-select__cue-close" role="img" aria-label="Hide">
319
+ ${unsafeSVG(collapseIcon)}
320
+ <span class="u-visually-hidden">Hide</span>
321
+ </span>
322
+ </span>
323
+ </button>
324
+ <div
325
+ class="o-select__content"
326
+ data-js-hook="behavior_flyout-menu_content"
327
+ ${ref(this.#contentDom)}
328
+ >
329
+ <cfpb-list
330
+ @item-click=${this.#onItemClick}
331
+ ?multiple=${this.multiple}
332
+ .childData=${this.optionList}
333
+ type=${this.multiple ? 'checkbox' : 'check'}
334
+ aria-label=${this.ariaLabelList
335
+ ? this.ariaLabelList
336
+ : 'Choose an item…'}
337
+ ${ref(this.#list)}
338
+ >
339
+ </cfpb-list>
340
+ <div class=${this.#noResults ? 'no-results' : 'u-hidden'}>
341
+ No results found
342
+ </div>
343
+ </div>
344
+ </div>
345
+ `;
346
+ }
347
+
348
+ #renderInput() {
349
+ return this.multiple
350
+ ? html`
351
+ <cfpb-form-search-input
352
+ ${ref(this.#input)}
353
+ borderless
354
+ ?name=${this.name}
355
+ ?value=${this.value}
356
+ ?placeholder=${this.placeholder}
357
+ title=${this.title}
358
+ ?maxlength=${this.maxlength}
359
+ aria-label=${this.ariaLabelInput}
360
+ ?validation=${this.validation}
361
+ @clear=${this.#onClear}
362
+ @input=${this.#onInput}
363
+ @click=${this.#onClick}
364
+ ></cfpb-form-search-input>
365
+ `
366
+ : html`<div
367
+ class="o-select__label"
368
+ ${ref(this.#displayLabel)}
369
+ @click=${this.#onClick}
370
+ ></div>`;
371
+ }
372
+
373
+ static init() {
374
+ CfpbFormSearchInput.init();
375
+ CfpbList.init();
376
+ CfpbTagGroup.init();
377
+
378
+ window.customElements.get('cfpb-select') ||
379
+ window.customElements.define('cfpb-select', CfpbSelect);
380
+ }
381
+ }
@@ -1,15 +1,18 @@
1
1
  @use 'sass:math';
2
- @use '@cfpb/cfpb-design-system/src/abstracts' as *;
3
- @use '@cfpb/cfpb-design-system/src/components/cfpb-buttons/vars' as *;
2
+ @use '@cfpb/cfpb-design-system/src/elements/abstracts' as *;
3
+ @use '@cfpb/cfpb-design-system/src/elements/cfpb-button/vars' as *;
4
4
 
5
5
  :host {
6
6
  button {
7
+ // This line-height isn't 19 or 22, as 20 creates a 30px high tag.
8
+ line-height: math.div(20px, $base-font-size-px);
9
+
7
10
  // Filter tags appear in filtered contexts, often as part of multiselects.
8
- line-height: math.div(19px, $base-font-size-px);
9
11
  font-size: math.div(16px, $btn-font-size) + rem;
10
12
 
11
13
  display: flex;
12
14
  gap: math.div(10px, $btn-font-size) + rem;
15
+ align-items: center;
13
16
 
14
17
  border: 1px solid var(--teal);
15
18
  padding: 4px 6px;
@@ -17,15 +17,15 @@ export class CfpbTagFilter extends LitElement {
17
17
  * @property {string} for - Associate the label with an ID elsewhere.
18
18
  * @returns {object} The map of properties.
19
19
  */
20
- static get properties() {
21
- return {
22
- for: { type: String },
23
- };
24
- }
20
+ static properties = {
21
+ for: { type: String },
22
+ value: { type: String },
23
+ };
25
24
 
26
25
  constructor() {
27
26
  super();
28
27
  this.for = '';
28
+ this.value = '';
29
29
  }
30
30
 
31
31
  #onClick() {
@@ -38,11 +38,19 @@ export class CfpbTagFilter extends LitElement {
38
38
  );
39
39
  }
40
40
 
41
+ #onSlotChange() {
42
+ const slot = this.shadowRoot.querySelector('slot');
43
+ this.value = slot
44
+ .assignedNodes({ flatten: true })
45
+ .map((node) => node.textContent.trim())
46
+ .join(' ');
47
+ }
48
+
41
49
  render() {
42
50
  const slot =
43
51
  this.for === ''
44
- ? html`<slot></slot>`
45
- : html`<label for="${this.for}"><slot></slot></label>`;
52
+ ? html`<slot @slotchange=${this.#onSlotChange}></slot>`
53
+ : html`<label for=${this.for}><slot></slot></label>`;
46
54
  return html`<button @click=${this.#onClick}>
47
55
  ${slot} ${unsafeHTML(icon)}
48
56
  </button>`;
@@ -1,6 +1,6 @@
1
1
  @use 'sass:math';
2
- @use '@cfpb/cfpb-design-system/src/abstracts' as *;
3
- @use '@cfpb/cfpb-design-system/src/components/cfpb-buttons/vars' as *;
2
+ @use '@cfpb/cfpb-design-system/src/elements/abstracts' as *;
3
+ @use '@cfpb/cfpb-design-system/src/elements/cfpb-button/vars' as *;
4
4
 
5
5
  :host {
6
6
  // Tag group sets the spacing between tags.