@cfpb/cfpb-design-system 4.2.4 → 4.3.1

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 (218) hide show
  1. package/CHANGELOG.md +186 -1
  2. package/dist/components/cfpb-buttons/index.css +1 -1
  3. package/dist/components/cfpb-buttons/index.css.map +2 -2
  4. package/dist/components/cfpb-buttons/index.js +1 -1
  5. package/dist/components/cfpb-buttons/index.js.map +1 -1
  6. package/dist/components/cfpb-expandables/index.css +1 -1
  7. package/dist/components/cfpb-expandables/index.css.map +2 -2
  8. package/dist/components/cfpb-expandables/index.js +1 -1
  9. package/dist/components/cfpb-expandables/index.js.map +4 -4
  10. package/dist/components/cfpb-forms/index.css +1 -1
  11. package/dist/components/cfpb-forms/index.css.map +2 -2
  12. package/dist/components/cfpb-forms/index.js +1 -1
  13. package/dist/components/cfpb-forms/index.js.map +2 -2
  14. package/dist/components/cfpb-icons/index.css +1 -1
  15. package/dist/components/cfpb-icons/index.css.map +2 -2
  16. package/dist/components/cfpb-icons/index.js +1 -1
  17. package/dist/components/cfpb-icons/index.js.map +1 -1
  18. package/dist/components/cfpb-layout/index.css +1 -1
  19. package/dist/components/cfpb-layout/index.css.map +2 -2
  20. package/dist/components/cfpb-layout/index.js +1 -1
  21. package/dist/components/cfpb-layout/index.js.map +1 -1
  22. package/dist/components/cfpb-notifications/index.css +1 -1
  23. package/dist/components/cfpb-notifications/index.css.map +2 -2
  24. package/dist/components/cfpb-notifications/index.js +1 -1
  25. package/dist/components/cfpb-notifications/index.js.map +1 -1
  26. package/dist/components/cfpb-pagination/index.css +1 -1
  27. package/dist/components/cfpb-pagination/index.css.map +2 -2
  28. package/dist/components/cfpb-pagination/index.js +1 -1
  29. package/dist/components/cfpb-pagination/index.js.map +1 -1
  30. package/dist/components/cfpb-tables/index.css +1 -1
  31. package/dist/components/cfpb-tables/index.css.map +2 -2
  32. package/dist/components/cfpb-tables/index.js +1 -1
  33. package/dist/components/cfpb-tables/index.js.map +1 -1
  34. package/dist/components/cfpb-tooltips/index.css +1 -1
  35. package/dist/components/cfpb-tooltips/index.css.map +2 -2
  36. package/dist/components/cfpb-tooltips/index.js +1 -1
  37. package/dist/components/cfpb-tooltips/index.js.map +1 -1
  38. package/dist/components/cfpb-typography/index.css +1 -1
  39. package/dist/components/cfpb-typography/index.css.map +2 -2
  40. package/dist/components/cfpb-typography/index.js +1 -1
  41. package/dist/components/cfpb-typography/index.js.map +1 -1
  42. package/dist/elements/abstracts/index.js +2 -0
  43. package/dist/elements/abstracts/index.js.map +7 -0
  44. package/dist/elements/base/index.css +3 -0
  45. package/dist/elements/base/index.css.map +7 -0
  46. package/dist/elements/base/index.js +2 -0
  47. package/dist/elements/base/index.js.map +7 -0
  48. package/dist/elements/cfpb-button/index.js +4 -4
  49. package/dist/elements/cfpb-button/index.js.map +3 -3
  50. package/dist/elements/cfpb-checkbox-icon/index.js +29 -0
  51. package/dist/elements/{cfpb-checkbox → cfpb-checkbox-icon}/index.js.map +4 -4
  52. package/dist/elements/cfpb-expandable/index.css +2 -0
  53. package/dist/elements/cfpb-expandable/index.css.map +7 -0
  54. package/dist/elements/cfpb-expandable/index.js +33 -0
  55. package/dist/elements/cfpb-expandable/index.js.map +7 -0
  56. package/dist/elements/cfpb-file-upload/index.js +4 -4
  57. package/dist/elements/cfpb-file-upload/index.js.map +3 -3
  58. package/dist/elements/cfpb-form-alert/index.js +32 -0
  59. package/dist/elements/cfpb-form-alert/index.js.map +7 -0
  60. package/dist/elements/cfpb-form-choice/index.js +12 -3
  61. package/dist/elements/cfpb-form-choice/index.js.map +4 -4
  62. package/dist/elements/cfpb-form-search/index.js +41 -0
  63. package/dist/elements/cfpb-form-search/index.js.map +7 -0
  64. package/dist/elements/cfpb-form-search-input/index.js +41 -0
  65. package/dist/elements/cfpb-form-search-input/index.js.map +7 -0
  66. package/dist/elements/cfpb-icon-text/index.js +3 -3
  67. package/dist/elements/cfpb-icon-text/index.js.map +3 -3
  68. package/dist/elements/cfpb-label/index.js +3 -3
  69. package/dist/elements/cfpb-label/index.js.map +2 -2
  70. package/dist/elements/cfpb-list/index.js +39 -0
  71. package/dist/elements/cfpb-list/index.js.map +7 -0
  72. package/dist/elements/cfpb-list-item/index.js +39 -0
  73. package/dist/elements/cfpb-list-item/index.js.map +7 -0
  74. package/dist/elements/cfpb-multiselect/index.js +13 -4
  75. package/dist/elements/cfpb-multiselect/index.js.map +4 -4
  76. package/dist/elements/cfpb-pagination/index.js +3 -3
  77. package/dist/elements/cfpb-pagination/index.js.map +2 -2
  78. package/dist/elements/cfpb-select/index.css +2 -0
  79. package/dist/elements/cfpb-select/index.css.map +7 -0
  80. package/dist/elements/cfpb-select/index.js +42 -0
  81. package/dist/elements/cfpb-select/index.js.map +7 -0
  82. package/dist/elements/cfpb-select-list/index.js +39 -0
  83. package/dist/elements/cfpb-select-list/index.js.map +7 -0
  84. package/dist/elements/cfpb-tag-filter/index.js +3 -3
  85. package/dist/elements/cfpb-tag-filter/index.js.map +3 -3
  86. package/dist/elements/cfpb-tag-group/index.js +3 -3
  87. package/dist/elements/cfpb-tag-group/index.js.map +4 -4
  88. package/dist/elements/cfpb-tag-topic/index.js +4 -4
  89. package/dist/elements/cfpb-tag-topic/index.js.map +2 -2
  90. package/dist/elements/index.css +2 -0
  91. package/dist/elements/index.css.map +7 -0
  92. package/dist/elements/index.js +7 -6
  93. package/dist/elements/index.js.map +4 -4
  94. package/dist/index.css +1 -1
  95. package/dist/index.css.map +3 -3
  96. package/dist/index.js +7 -6
  97. package/dist/index.js.map +4 -4
  98. package/dist/utilities/index.css +1 -1
  99. package/dist/utilities/index.css.map +2 -2
  100. package/dist/utilities/index.js +1 -1
  101. package/dist/utilities/index.js.map +4 -4
  102. package/package.json +1 -1
  103. package/src/components/cfpb-buttons/button-group.scss +1 -1
  104. package/src/components/cfpb-buttons/button-link.scss +10 -54
  105. package/src/components/cfpb-buttons/button.scss +3 -3
  106. package/src/components/cfpb-buttons/vars.scss +1 -1
  107. package/src/components/cfpb-expandables/expandable-group.scss +1 -1
  108. package/src/components/cfpb-expandables/expandable.js +3 -0
  109. package/src/components/cfpb-expandables/expandable.scss +1 -1
  110. package/src/components/cfpb-expandables/summary.scss +1 -1
  111. package/src/components/cfpb-forms/form-alert.scss +1 -1
  112. package/src/components/cfpb-forms/form-field.scss +6 -6
  113. package/src/components/cfpb-forms/form.scss +1 -1
  114. package/src/components/cfpb-forms/label.scss +2 -2
  115. package/src/components/cfpb-forms/multiselect.js +1 -1
  116. package/src/components/cfpb-forms/multiselect.scss +1 -1
  117. package/src/components/cfpb-forms/range.scss +7 -7
  118. package/src/components/cfpb-forms/search-input.scss +1 -1
  119. package/src/components/cfpb-forms/select.scss +1 -1
  120. package/src/components/cfpb-forms/tag.scss +1 -1
  121. package/src/components/cfpb-forms/text-input.scss +1 -1
  122. package/src/components/cfpb-icons/icon.scss +1 -1
  123. package/src/components/cfpb-layout/card-group.scss +1 -1
  124. package/src/components/cfpb-layout/card.scss +1 -1
  125. package/src/components/cfpb-layout/email-signup.scss +1 -1
  126. package/src/components/cfpb-layout/featured-content-module.scss +1 -1
  127. package/src/components/cfpb-layout/hero.scss +1 -1
  128. package/src/components/cfpb-layout/layout.scss +9 -9
  129. package/src/components/cfpb-layout/well.scss +1 -1
  130. package/src/components/cfpb-notifications/banner.scss +1 -1
  131. package/src/components/cfpb-notifications/notification.scss +1 -1
  132. package/src/components/cfpb-pagination/pagination.scss +1 -1
  133. package/src/components/cfpb-tables/table.scss +1 -1
  134. package/src/components/cfpb-tooltips/tooltip.scss +1 -1
  135. package/src/components/cfpb-typography/date.scss +1 -1
  136. package/src/components/cfpb-typography/list.scss +1 -1
  137. package/src/components/cfpb-typography/meta-header.scss +1 -1
  138. package/src/components/cfpb-typography/mixins.scss +1 -1
  139. package/src/components/cfpb-typography/pull-quote.scss +1 -1
  140. package/src/components/cfpb-typography/slug-header.scss +1 -1
  141. package/src/components/cfpb-typography/tagline.scss +1 -1
  142. package/src/elements/abstracts/custom-props.css +123 -0
  143. package/src/{abstracts → elements/abstracts}/grid-mixins.scss +2 -1
  144. package/src/{abstracts → elements/abstracts}/heading-mixins.scss +1 -0
  145. package/src/{abstracts → elements/abstracts}/index.scss +1 -0
  146. package/src/{abstracts → elements/abstracts}/media-queries.scss +1 -1
  147. package/src/elements/abstracts/sizing-vars.scss +66 -0
  148. package/src/elements/abstracts/vars.css +79 -0
  149. package/src/{base → elements/base}/base.scss +14 -14
  150. package/src/elements/cfpb-button/cfpb-button-group.scss +12 -0
  151. package/src/elements/cfpb-button/cfpb-button-link.scss +103 -0
  152. package/src/elements/cfpb-button/cfpb-button.component.scss +11 -4
  153. package/src/elements/cfpb-button/cfpb-button.scss +218 -0
  154. package/src/elements/cfpb-button/index.js +44 -30
  155. package/src/elements/cfpb-button/vars.css +30 -0
  156. package/src/elements/cfpb-checkbox-icon/cfpb-checkbox-icon.component.scss +88 -0
  157. package/src/elements/cfpb-checkbox-icon/index.js +104 -0
  158. package/src/elements/cfpb-expandable/cfpb-expandable.component.scss +218 -0
  159. package/src/elements/cfpb-expandable/index.js +127 -0
  160. package/src/elements/cfpb-file-upload/cfpb-file-upload.component.scss +2 -2
  161. package/src/elements/cfpb-file-upload/index.js +25 -27
  162. package/src/elements/cfpb-form-alert/cfpb-form-alert.component.scss +36 -0
  163. package/src/elements/cfpb-form-alert/index.js +55 -0
  164. package/src/elements/cfpb-form-choice/cfpb-form-choice.component.scss +42 -81
  165. package/src/elements/cfpb-form-choice/index.js +58 -18
  166. package/src/elements/cfpb-form-search/cfpb-form-search.component.scss +54 -0
  167. package/src/elements/cfpb-form-search/index.js +194 -0
  168. package/src/elements/cfpb-form-search-input/cfpb-form-search-input.component.scss +217 -0
  169. package/src/elements/cfpb-form-search-input/index.js +140 -0
  170. package/src/elements/cfpb-icon-text/cfpb-icon-text.component.scss +33 -39
  171. package/src/elements/cfpb-icon-text/index.js +32 -104
  172. package/src/elements/cfpb-label/cfpb-label.component.scss +2 -2
  173. package/src/elements/cfpb-label/index.js +6 -9
  174. package/src/elements/cfpb-list/cfpb-list.component.scss +34 -0
  175. package/src/elements/cfpb-list/index.js +379 -0
  176. package/src/elements/cfpb-list/index.spec.js +214 -0
  177. package/src/elements/cfpb-list-item/cfpb-list-item.component.scss +69 -0
  178. package/src/elements/cfpb-list-item/index.js +215 -0
  179. package/src/elements/cfpb-pagination/cfpb-pagination.component.scss +2 -7
  180. package/src/elements/cfpb-pagination/index.js +6 -8
  181. package/src/elements/cfpb-select/cfpb-select.component.scss +241 -0
  182. package/src/elements/cfpb-select/index.js +371 -0
  183. package/src/elements/cfpb-select/multiple-select-event-proxy.js +88 -0
  184. package/src/elements/cfpb-select/single-select-event-proxy.js +47 -0
  185. package/src/elements/cfpb-tag-filter/cfpb-tag-filter.component.scss +6 -3
  186. package/src/elements/cfpb-tag-filter/index.js +15 -7
  187. package/src/elements/cfpb-tag-group/cfpb-tag-group.component.scss +2 -2
  188. package/src/elements/cfpb-tag-group/index.js +53 -6
  189. package/src/elements/cfpb-tag-topic/cfpb-tag-topic.component.scss +2 -2
  190. package/src/elements/cfpb-tag-topic/index.js +5 -7
  191. package/src/elements/cfpb-utilities/parse-child-data.js +50 -0
  192. package/src/elements/cfpb-utilities/parse-child-data.spec.js +56 -0
  193. package/src/elements/cfpb-utilities/search-service.js +46 -0
  194. package/src/elements/cfpb-utilities/search-service.spec.js +138 -0
  195. package/src/elements/cfpb-utilities/transition/transition.scss +98 -0
  196. package/src/elements/index.js +7 -1
  197. package/src/index.js +2 -2
  198. package/src/index.scss +14 -2
  199. package/src/tokens/abstracts/custom-props.json +1642 -0
  200. package/src/tokens/abstracts/vars.json +1319 -0
  201. package/src/tokens/cfpb-button/vars.json +436 -0
  202. package/src/utilities/breakpoint-state.js +1 -1
  203. package/src/utilities/transition/max-height-transition.js +74 -0
  204. package/src/utilities/utilities.scss +1 -1
  205. package/dist/elements/cfpb-checkbox/index.js +0 -29
  206. package/src/abstracts/custom-props.scss +0 -175
  207. package/src/abstracts/vars.scss +0 -184
  208. package/src/elements/cfpb-multiselect/cfpb-multiselect.component.scss +0 -225
  209. package/src/elements/cfpb-multiselect/index.js +0 -444
  210. package/src/elements/cfpb-multiselect/multiselect-model.js +0 -288
  211. package/src/elements/cfpb-multiselect/multiselect-model.spec.js +0 -236
  212. /package/src/{abstracts → elements/abstracts}/index.js +0 -0
  213. /package/src/{abstracts → elements/abstracts}/vars-breakpoints.js +0 -0
  214. /package/src/{abstracts → elements/abstracts}/vars-breakpoints.scss +0 -0
  215. /package/src/{base → elements/base}/font.scss +0 -0
  216. /package/src/{base → elements/base}/index.js +0 -0
  217. /package/src/{base → elements/base}/index.scss +0 -0
  218. /package/src/{base → elements/base}/normalize.scss +0 -0
@@ -0,0 +1,371 @@
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
+ import { SingleSelectEventProxy } from './single-select-event-proxy.js';
15
+ import { MultipleSelectEventProxy } from './multiple-select-event-proxy.js';
16
+
17
+ /**
18
+ *
19
+ * @element cfpb-select
20
+ * @slot - The main content for the select.
21
+ */
22
+ export class CfpbSelect extends LitElement {
23
+ static styles = css`
24
+ ${unsafeCSS(styles)}
25
+ `;
26
+
27
+ #eventProxy;
28
+ #flyoutMenu;
29
+ #transition;
30
+ #search;
31
+ #root = createRef();
32
+ #headerDom = createRef();
33
+ #contentDom = createRef();
34
+ #input = createRef();
35
+ #tagGroup = createRef();
36
+ #list = createRef();
37
+ #displayLabel = createRef();
38
+ #boundOnOutsideFocus;
39
+ #noResults = false;
40
+
41
+ /**
42
+ * @property {boolean} multiple - Whether the select supports multiple or not.
43
+ * @property {boolean} isExpanded - Whether the select is expanded or not.
44
+ * @property {Array} selectedTexts - Text of selected options.
45
+ * @returns {object} The map of properties.
46
+ */
47
+ static get properties() {
48
+ return {
49
+ multiple: { type: Boolean, reflect: true },
50
+ disabled: { type: Boolean },
51
+ validation: { type: String },
52
+ label: { type: String },
53
+ name: { type: String },
54
+ title: { type: Boolean, attribute: true },
55
+ value: { type: String },
56
+ maxlength: { type: Number },
57
+ placeholder: { type: String },
58
+ ariaLabelInput: { type: String, attribute: 'aria-label-input' },
59
+ ariaLabelList: { type: String, attribute: 'aria-label-list' },
60
+
61
+ isExpanded: { type: Boolean, attribute: 'open', reflect: true },
62
+ selectedTexts: { type: Array },
63
+ optionList: { type: Array },
64
+ };
65
+ }
66
+
67
+ constructor() {
68
+ super();
69
+
70
+ this.multiple = false;
71
+ this.options = [];
72
+ this.selectedTexts = [];
73
+ this.optionList = [];
74
+
75
+ this.#boundOnOutsideFocus = this.#onFocusOutside.bind(this);
76
+ }
77
+
78
+ firstUpdated() {
79
+ this.#initFlyoutMenu();
80
+
81
+ this.addEventListener('focus', () => {
82
+ this.#eventProxy.onFocus();
83
+ });
84
+
85
+ this.addEventListener('keydown', (evt) => {
86
+ this.#eventProxy.onKeyDown(evt, this);
87
+ });
88
+ }
89
+
90
+ disconnectedCallback() {
91
+ document.removeEventListener('pointerdown', this.#boundOnOutsideFocus);
92
+ document.removeEventListener('focusin', this.#boundOnOutsideFocus);
93
+ super.disconnectedCallback();
94
+ }
95
+
96
+ #onFocusOutside(evt) {
97
+ const path = evt.composedPath();
98
+ if (!path.includes(this)) {
99
+ this.isExpanded = false;
100
+ }
101
+ }
102
+
103
+ #onSlotChange(evt) {
104
+ const slot = evt.target;
105
+
106
+ const list = slot
107
+ .assignedNodes({ flatten: true })
108
+ .filter(
109
+ (node) =>
110
+ node.nodeType === Node.ELEMENT_NODE &&
111
+ (node.tagName === 'UL' || node.tagName === 'OL'),
112
+ );
113
+
114
+ if (!list || !list[0]) {
115
+ return;
116
+ }
117
+
118
+ // Extract list items (with their text or link info)
119
+ const items = [...list[0].querySelectorAll('li')].map((li) => {
120
+ const checked =
121
+ li.hasAttribute('data-checked') || li.hasAttribute('checked');
122
+
123
+ const itemValue = li.textContent.trim();
124
+
125
+ if (checked) {
126
+ if (!this.multiple) this.#displayLabel.value.textContent = itemValue;
127
+ return {
128
+ value: itemValue,
129
+ checked: 'true',
130
+ };
131
+ }
132
+
133
+ return { value: itemValue };
134
+ });
135
+
136
+ this.optionList = items;
137
+
138
+ this.#search = new SearchService(
139
+ items.map((item) => {
140
+ return item.value;
141
+ }),
142
+ );
143
+ }
144
+
145
+ #onInput(evt) {
146
+ this.#flyoutMenu.suspend();
147
+ if (!this.isExpanded) this.isExpanded = true;
148
+ const visibleItems = this.#list.value.filterItems(
149
+ this.#search.search(evt.target.value),
150
+ );
151
+
152
+ if (visibleItems.length === 0) {
153
+ this.#noResults = true;
154
+ this.requestUpdate();
155
+ } else {
156
+ this.#noResults = false;
157
+ this.requestUpdate();
158
+ this.#flyoutMenu.resume();
159
+ }
160
+ }
161
+
162
+ #onClear() {
163
+ this.#list.value.showAllItems();
164
+ }
165
+
166
+ #initFlyoutMenu() {
167
+ const root = this.#root.value;
168
+ const contentDom = this.#contentDom.value;
169
+
170
+ // If it's expanded we don't set an initial height,
171
+ // as it will be calculated internally.
172
+ const initialClass = this.isExpanded
173
+ ? MaxHeightTransition.CLASSES.MH_DEFAULT
174
+ : MaxHeightTransition.CLASSES.MH_ZERO;
175
+ this.#transition = new MaxHeightTransition(contentDom).init(initialClass);
176
+
177
+ this.#flyoutMenu = new FlyoutMenu(root);
178
+
179
+ this.#flyoutMenu.setTransition(
180
+ this.#transition,
181
+ this.#transition.maxHeightZero,
182
+ this.#transition.maxHeightDynamic,
183
+ );
184
+
185
+ this.#flyoutMenu.init(this.isExpanded);
186
+
187
+ // Add events.
188
+ this.#flyoutMenu.addEventListener('expandbegin', () => {
189
+ this.isExpanded = true;
190
+ contentDom.classList.remove('u-hidden');
191
+ this.dispatchEvent(
192
+ new CustomEvent('expandbegin', {
193
+ detail: { target: this },
194
+ bubbles: true,
195
+ composed: true,
196
+ }),
197
+ );
198
+ });
199
+ this.#flyoutMenu.addEventListener('collapseend', () => {
200
+ this.isExpanded = false;
201
+ contentDom.classList.add('u-hidden');
202
+
203
+ // Remove direction classes.
204
+ this.#root.value.classList.remove(`o-select--up`);
205
+ this.#root.value.classList.remove(`o-select--down`);
206
+ });
207
+
208
+ this.#transition.addEventListener('transitiondir', (evt) => {
209
+ this.#root.value.classList.add(`o-select--${evt.dir}`);
210
+ });
211
+ }
212
+
213
+ updated(changedProps) {
214
+ if (changedProps.has('multiple')) {
215
+ this.#eventProxy = this.#createEventProxy();
216
+ }
217
+
218
+ if (changedProps.has('isExpanded')) {
219
+ const oldVal = changedProps.get('isExpanded');
220
+ const newVal = this.isExpanded;
221
+
222
+ if (newVal !== oldVal) {
223
+ if (newVal) {
224
+ this.#flyoutMenu.expand();
225
+ document.addEventListener('pointerdown', this.#boundOnOutsideFocus);
226
+ document.addEventListener('focusin', this.#boundOnOutsideFocus);
227
+ } else {
228
+ this.#flyoutMenu.collapse();
229
+ document.removeEventListener(
230
+ 'pointerdown',
231
+ this.#boundOnOutsideFocus,
232
+ );
233
+ document.removeEventListener('focusin', this.#boundOnOutsideFocus);
234
+ }
235
+ }
236
+ }
237
+ }
238
+
239
+ #createEventProxy() {
240
+ const common = {
241
+ list: this.#list.value,
242
+ flyout: () => this.#flyoutMenu,
243
+ };
244
+
245
+ return this.multiple
246
+ ? new MultipleSelectEventProxy({
247
+ ...common,
248
+ input: this.#input.value,
249
+ tagGroup: this.#tagGroup.value,
250
+ })
251
+ : new SingleSelectEventProxy({
252
+ ...common,
253
+ displayLabel: this.#displayLabel.value,
254
+ header: this.#headerDom.value,
255
+ });
256
+ }
257
+
258
+ #onClick(evt) {
259
+ this.#eventProxy?.onClick(evt, this);
260
+ }
261
+
262
+ #onItemClick(evt) {
263
+ this.#eventProxy?.onItemClick(evt, this);
264
+ }
265
+
266
+ #onTagClick(evt) {
267
+ this.#eventProxy?.onTagClick(evt, this);
268
+ }
269
+
270
+ render() {
271
+ return html`
272
+ <!--Light DOM content-->
273
+ <slot @slotchange=${this.#onSlotChange}></slot>
274
+
275
+ ${this.multiple
276
+ ? html`<cfpb-tag-group
277
+ ${ref(this.#tagGroup)}
278
+ .childData=${this.optionList
279
+ .filter((item) => {
280
+ return item.checked;
281
+ })
282
+ .map((item) => {
283
+ return { text: item.value, tagName: 'cfpb-tag-filter' };
284
+ })}
285
+ @tag-click=${this.#onTagClick}
286
+ >
287
+ </cfpb-tag-group>`
288
+ : nothing}
289
+
290
+ <div
291
+ class="o-select o-select--border"
292
+ data-js-hook="behavior_flyout-menu"
293
+ ${ref(this.#root)}
294
+ >
295
+ ${this.#renderInput()}
296
+
297
+ <button
298
+ class="o-select__header"
299
+ title="Expand content"
300
+ data-js-hook="behavior_flyout-menu_trigger"
301
+ ${ref(this.#headerDom)}
302
+ >
303
+ <span class="o-select__cues" @click=${this.#onClick}>
304
+ <span class="o-select__cue-open" role="img" aria-label="Show">
305
+ ${unsafeSVG(expandIcon)}
306
+ <span class="u-visually-hidden">Show</span>
307
+ </span>
308
+ <span class="o-select__cue-close" role="img" aria-label="Hide">
309
+ ${unsafeSVG(collapseIcon)}
310
+ <span class="u-visually-hidden">Hide</span>
311
+ </span>
312
+ </span>
313
+ </button>
314
+ <div
315
+ class="o-select__content"
316
+ data-js-hook="behavior_flyout-menu_content"
317
+ ${ref(this.#contentDom)}
318
+ >
319
+ <cfpb-list
320
+ @item-click=${this.#onItemClick}
321
+ ?multiple=${this.multiple}
322
+ .childData=${this.optionList}
323
+ type=${this.multiple ? 'checkbox' : 'check'}
324
+ aria-label=${this.ariaLabelList
325
+ ? this.ariaLabelList
326
+ : 'Choose an item…'}
327
+ ${ref(this.#list)}
328
+ >
329
+ </cfpb-list>
330
+ <div class=${this.#noResults ? 'no-results' : 'u-hidden'}>
331
+ No results found
332
+ </div>
333
+ </div>
334
+ </div>
335
+ `;
336
+ }
337
+
338
+ #renderInput() {
339
+ return this.multiple
340
+ ? html`
341
+ <cfpb-form-search-input
342
+ ${ref(this.#input)}
343
+ borderless
344
+ ?name=${this.name}
345
+ ?value=${this.value}
346
+ ?placeholder=${this.placeholder}
347
+ title=${this.title}
348
+ ?maxlength=${this.maxlength}
349
+ aria-label=${this.ariaLabelInput}
350
+ ?validation=${this.validation}
351
+ @clear=${this.#onClear}
352
+ @input=${this.#onInput}
353
+ @click=${this.#onClick}
354
+ ></cfpb-form-search-input>
355
+ `
356
+ : html`<div
357
+ class="o-select__label"
358
+ ${ref(this.#displayLabel)}
359
+ @click=${this.#onClick}
360
+ ></div>`;
361
+ }
362
+
363
+ static init() {
364
+ CfpbFormSearchInput.init();
365
+ CfpbList.init();
366
+ CfpbTagGroup.init();
367
+
368
+ window.customElements.get('cfpb-select') ||
369
+ window.customElements.define('cfpb-select', CfpbSelect);
370
+ }
371
+ }
@@ -0,0 +1,88 @@
1
+ export class MultipleSelectEventProxy {
2
+ constructor({ list, input, tagGroup, flyout }) {
3
+ this.list = list;
4
+ this.input = input;
5
+ this.tagGroup = tagGroup;
6
+ this.flyout = flyout;
7
+ }
8
+
9
+ onClick(evt, host) {
10
+ const target = evt.currentTarget;
11
+
12
+ if (target.tagName === 'CFPB-FORM-SEARCH-INPUT') {
13
+ host.isExpanded ? this.flyout()?.suspend() : this.flyout()?.expand();
14
+ } else {
15
+ this.flyout()?.resume();
16
+ }
17
+ }
18
+
19
+ onItemClick(evt, host) {
20
+ host.optionList = this.list.childData ?? [];
21
+ evt.currentTarget.focusItemAt(evt.detail.index);
22
+ //host.requestUpdate();
23
+ }
24
+
25
+ onTagClick(evt, host) {
26
+ const remaining =
27
+ this.tagGroup.tagList.filter((tag) => tag !== evt.detail.target) ?? [];
28
+
29
+ host.optionList = host.optionList.map((item) => ({
30
+ ...item,
31
+ checked: remaining.some((tag) => tag.value === item.value),
32
+ }));
33
+
34
+ //host.requestUpdate();
35
+ }
36
+
37
+ onKeyDown(evt, host) {
38
+ const focused = host.shadowRoot.activeElement.tagName;
39
+
40
+ if (focused === 'CFPB-FORM-SEARCH-INPUT') {
41
+ switch (evt.key) {
42
+ case 'Tab':
43
+ evt.preventDefault();
44
+ if (evt.shiftKey) {
45
+ if (host.isExpanded) host.isExpanded = false;
46
+ this.input.focus();
47
+ } else {
48
+ if (!host.isExpanded) host.isExpanded = true;
49
+ this.list.focusItemAt(0);
50
+ }
51
+ break;
52
+ case 'ArrowDown':
53
+ evt.preventDefault();
54
+ if (!host.isExpanded) host.isExpanded = true;
55
+ else this.list.focusItemAt(0);
56
+ break;
57
+ case 'Enter':
58
+ evt.preventDefault();
59
+ host.isExpanded = !host.isExpanded;
60
+ break;
61
+ }
62
+ }
63
+
64
+ if (focused === 'CFPB-LIST-ITEM') {
65
+ switch (evt.key) {
66
+ case 'Tab':
67
+ if (evt.shiftKey) {
68
+ if (this.list.focusedIndex === 0) {
69
+ //host.shadowRoot.activeElement.blur();
70
+ evt.preventDefault();
71
+ this.list.focusItemAt(-1);
72
+ this.input.focus();
73
+ }
74
+ }
75
+ break;
76
+ }
77
+ }
78
+
79
+ if (evt.key === 'Escape') {
80
+ evt.preventDefault();
81
+ host.isExpanded = false;
82
+ }
83
+ }
84
+
85
+ onFocus() {
86
+ this.input.focus();
87
+ }
88
+ }
@@ -0,0 +1,47 @@
1
+ export class SingleSelectEventProxy {
2
+ constructor({ list, displayLabel, header }) {
3
+ this.list = list;
4
+ this.displayLabel = displayLabel;
5
+ this.header = header;
6
+ }
7
+
8
+ onClick(evt, host) {
9
+ if (evt.currentTarget.classList.contains('o-select__label')) {
10
+ this.header.focus();
11
+ host.isExpanded = !host.isExpanded;
12
+ }
13
+ }
14
+
15
+ onItemClick(evt, host) {
16
+ const selected = this.list.checkedItems[0]?.value ?? '';
17
+
18
+ if (this.displayLabel) {
19
+ this.displayLabel.textContent = selected;
20
+ }
21
+
22
+ host.optionList = host.optionList.map((item) => ({
23
+ ...item,
24
+ checked: item.value === selected,
25
+ }));
26
+
27
+ //host.requestUpdate();
28
+ host.isExpanded = false;
29
+ }
30
+
31
+ onKeyDown(evt, host) {
32
+ const focused = host.shadowRoot.activeElement.tagName;
33
+ if (focused === 'BUTTON') {
34
+ switch (evt.key) {
35
+ case 'ArrowDown':
36
+ evt.preventDefault();
37
+ host.isExpanded = true;
38
+ this.list.querySelector('cfpb-list-item')?.focus();
39
+ break;
40
+ }
41
+ }
42
+ }
43
+
44
+ onFocus() {
45
+ // Additional actions done on focus can be added here.
46
+ }
47
+ }
@@ -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.
@@ -1,5 +1,6 @@
1
1
  import { html, LitElement, css, unsafeCSS } from 'lit';
2
2
  import styles from './cfpb-tag-group.component.scss';
3
+ import { parseChildData } from '../cfpb-utilities/parse-child-data';
3
4
 
4
5
  const SUPPORTED_TAG_LIST = ['CFPB-TAG-FILTER', 'CFPB-TAG-TOPIC'];
5
6
 
@@ -21,16 +22,16 @@ export class CfpbTagGroup extends LitElement {
21
22
  `;
22
23
 
23
24
  /**
25
+ * @property {string} childData - Structure data to create child components.
24
26
  * @property {boolean} stacked - Whether to stack the tags vertically.
25
27
  * @property {Array} tagList - List of the tags in the tag group.
26
28
  * @returns {object} The map of properties.
27
29
  */
28
- static get properties() {
29
- return {
30
- stacked: { type: Boolean, reflect: true },
31
- tagList: { type: Array },
32
- };
33
- }
30
+ static properties = {
31
+ childData: { type: String, attribute: 'childdata' },
32
+ stacked: { type: Boolean, reflect: true },
33
+ tagList: { type: Array },
34
+ };
34
35
 
35
36
  // Private properties.
36
37
  #observer;
@@ -39,6 +40,7 @@ export class CfpbTagGroup extends LitElement {
39
40
 
40
41
  constructor() {
41
42
  super();
43
+ this.childData = '';
42
44
  this.stacked = false;
43
45
  this.tagList = [];
44
46
  this.#observer = new MutationObserver(this.#onMutation.bind(this));
@@ -67,6 +69,51 @@ export class CfpbTagGroup extends LitElement {
67
69
  });
68
70
  }
69
71
 
72
+ updated(changedProps) {
73
+ if (changedProps.has('childData')) {
74
+ const parsed = parseChildData(this.childData);
75
+ if (parsed) {
76
+ this.#renderTagsFromData(parsed);
77
+ }
78
+ }
79
+ }
80
+
81
+ #renderTagsFromData(arr) {
82
+ if (!Array.isArray(arr)) return;
83
+
84
+ this.#clearAllTags();
85
+
86
+ arr.forEach((data, index) => {
87
+ const tag = document.createElement(data.tagName);
88
+ // e.g. 'cfpb-tag-filter' or 'cfpb-tag-topic'
89
+ if (data.text) tag.textContent = data.text;
90
+ if (data.href) tag.href = data.href;
91
+ // any other props from `data`
92
+ this.addTag(tag, index);
93
+ });
94
+ }
95
+
96
+ /**
97
+ * Remove all previous tags from shadow DOM and light DOM.
98
+ */
99
+ #clearAllTags() {
100
+ // Remove shadow DOM wrappers.
101
+ if (this.#tagMap) {
102
+ this.#tagMap.forEach((wrapped) => {
103
+ if (wrapped.parentElement) wrapped.remove();
104
+ });
105
+ this.#tagMap.clear();
106
+ }
107
+
108
+ // Remove light DOM tags.
109
+ [...this.children].forEach((child) => {
110
+ if (SUPPORTED_TAG_LIST.includes(child.tagName)) child.remove();
111
+ });
112
+
113
+ // Reset tagList
114
+ this.tagList = [];
115
+ }
116
+
70
117
  /**
71
118
  * Set up a MutationObserver to watch changes in the light DOM.
72
119
  */
@@ -1,6 +1,6 @@
1
1
  @use 'sass:math';
2
- @use '@cfpb/cfpb-design-system/src/base' as *;
3
- @use '@cfpb/cfpb-design-system/src/abstracts' as *;
2
+ @use '@cfpb/cfpb-design-system/src/elements/base' as *;
3
+ @use '@cfpb/cfpb-design-system/src/elements/abstracts' as *;
4
4
  @use '@cfpb/cfpb-design-system/src/utilities' as *;
5
5
  @use '@cfpb/cfpb-design-system/src/components/cfpb-typography/mixins' as *;
6
6