@carbon/vue 3.0.5-alpha.0 → 3.0.7-alpha.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 (64) hide show
  1. package/README.md +16 -0
  2. package/dist/carbon-vue-3.common.js +22859 -11008
  3. package/dist/carbon-vue-3.common.js.map +1 -1
  4. package/dist/carbon-vue-3.css +1 -0
  5. package/dist/carbon-vue-3.umd.js +22859 -11008
  6. package/dist/carbon-vue-3.umd.js.map +1 -1
  7. package/dist/carbon-vue-3.umd.min.js +30 -2
  8. package/dist/carbon-vue-3.umd.min.js.map +1 -1
  9. package/package.json +5 -4
  10. package/src/components/CvAccordion/CvAccordion.stories.js +1 -0
  11. package/src/components/CvButton/CvButton.stories.js +3 -3
  12. package/src/components/CvButton/CvButtonSkeleton.stories.js +79 -0
  13. package/src/components/CvButton/CvButtonSkeleton.vue +19 -0
  14. package/src/components/CvButton/index.js +8 -1
  15. package/src/components/CvDatePicker/CvDatePicker-notes.md +86 -0
  16. package/src/components/CvDatePicker/CvDatePicker.stories.js +241 -0
  17. package/src/components/CvDatePicker/CvDatePicker.vue +402 -0
  18. package/src/components/CvDatePicker/CvDatePickerSkeleton.vue +106 -0
  19. package/src/components/CvDatePicker/index.js +4 -0
  20. package/src/components/CvDatePicker/plugins/appendToPlugin.js +64 -0
  21. package/src/components/CvDatePicker/plugins/fixEventsPlugin.js +79 -0
  22. package/src/components/CvDatePicker/plugins/monthSelectPlugin.js +86 -0
  23. package/src/components/CvDatePicker/plugins/rangePlugin.js +47 -0
  24. package/src/components/CvDropdown/CvDropdown.stories.mdx +407 -0
  25. package/src/components/CvDropdown/CvDropdown.vue +418 -0
  26. package/src/components/CvDropdown/CvDropdownItem.vue +86 -0
  27. package/src/components/CvDropdown/CvDropdownSkeleton.vue +17 -0
  28. package/src/components/CvDropdown/index.js +5 -0
  29. package/src/components/CvForm/CvForm.stories.mdx +193 -0
  30. package/src/components/CvForm/CvForm.vue +9 -0
  31. package/src/components/CvForm/CvFormGroup.vue +27 -0
  32. package/src/components/CvForm/CvFormItem.vue +9 -0
  33. package/src/components/CvForm/index.js +5 -0
  34. package/src/components/CvInlineLoading/CvInlineLoading.stories.mdx +87 -0
  35. package/src/components/CvInlineLoading/CvInlineLoading.vue +144 -0
  36. package/src/components/CvInlineLoading/consts.js +9 -0
  37. package/src/components/CvInlineLoading/index.js +4 -0
  38. package/src/components/CvModal/CvModal.stories.mdx +236 -0
  39. package/src/components/CvModal/CvModal.vue +401 -0
  40. package/src/components/CvModal/index.js +3 -0
  41. package/src/components/CvMultiSelect/CvMultiSelect.stories.mdx +333 -0
  42. package/src/components/CvMultiSelect/CvMultiSelect.vue +705 -0
  43. package/src/components/CvMultiSelect/consts.js +16 -0
  44. package/src/components/CvMultiSelect/index.js +3 -0
  45. package/src/components/CvNumberInput/CvNumberInput.stories.js +265 -0
  46. package/src/components/CvNumberInput/CvNumberInput.vue +227 -0
  47. package/src/components/CvNumberInput/CvNumberInputSkeleton.stories.js +30 -0
  48. package/src/components/CvNumberInput/CvNumberInputSkeleton.vue +10 -0
  49. package/src/components/CvNumberInput/StorybookGroupConst.js +1 -0
  50. package/src/components/CvNumberInput/__tests__/__snapshots__/CvNumberInput.spec.js.snap +21 -0
  51. package/src/components/CvNumberInput/index.js +5 -0
  52. package/src/components/CvTabs/CvTab.vue +77 -0
  53. package/src/components/CvTabs/CvTabs.stories.mdx +175 -0
  54. package/src/components/CvTabs/CvTabs.vue +406 -0
  55. package/src/components/CvTabs/CvTabsSkeleton.vue +24 -0
  56. package/src/components/CvTabs/_SbContainer.vue +22 -0
  57. package/src/components/CvTabs/index.js +5 -0
  58. package/src/components/CvToggle/CvToggle-Skeleton.vue +29 -0
  59. package/src/components/CvToggle/CvToggle.stories.js +232 -0
  60. package/src/components/CvToggle/CvToggle.vue +70 -0
  61. package/src/components/CvToggle/__tests__/__snapshots__/CvToggle.spec.js.snap +7 -0
  62. package/src/components/CvToggle/index.js +4 -0
  63. package/src/global/storybook-utils/story-source-code.js +1 -0
  64. package/src/use/cvInput.js +34 -0
@@ -0,0 +1,705 @@
1
+ <template>
2
+ <div
3
+ ref="el"
4
+ :class="[
5
+ `cv-multi-select ${carbonPrefix}--multi-select__wrapper ${carbonPrefix}--list-box__wrapper`,
6
+ {
7
+ [`${carbonPrefix}--multi-select__wrapper--inline`]: inline,
8
+ [`${carbonPrefix}--list-box__wrapper--inline`]: inline,
9
+ [`${carbonPrefix}--multi-select__wrapper--inline--invalid]`]:
10
+ inlineInvalid,
11
+ [`${carbonPrefix}--list-box__wrapper--inline--invalid`]: inlineInvalid,
12
+ [`${carbonPrefix}--multi-select--filterable`]: isFilterable,
13
+ },
14
+ ]"
15
+ @focusout="onFocusOut"
16
+ >
17
+ <label
18
+ v-if="title"
19
+ :id="`${uid}-label`"
20
+ :class="[
21
+ `${carbonPrefix}--label`,
22
+ { [`${carbonPrefix}--label--disabled`]: disabled },
23
+ ]"
24
+ >{{ title }}</label
25
+ >
26
+ <div
27
+ :aria-labelledby="`${uid}-label`"
28
+ role="listbox"
29
+ tabindex="-1"
30
+ :class="[
31
+ `${carbonPrefix}--multi-select ${carbonPrefix}--list-box`,
32
+ {
33
+ [`${carbonPrefix}--list-box--up`]: isDirectionUp,
34
+ [`${carbonPrefix}--list-box--light`]: isLight,
35
+ [`${carbonPrefix}--list-box--expanded`]: data.open,
36
+ [`${carbonPrefix}--multi-select--warning`]: isWarning,
37
+ [`${carbonPrefix}--list-box--warning`]: isWarning,
38
+ [`${carbonPrefix}--multi-select--invalid`]: data.isInvalid,
39
+ [`${carbonPrefix}--multi-select--disabled`]: disabled,
40
+ [`${carbonPrefix}--list-box--disabled`]: disabled,
41
+ [`${carbonPrefix}--multi-select--inline`]: inline,
42
+ [`${carbonPrefix}--list-box--inline`]: inline,
43
+ [`${carbonPrefix}--multi-select--selected`]:
44
+ data.selectedItems.length > 0,
45
+ [`${carbonPrefix}--combo-box`]: isFilterable,
46
+ },
47
+ ]"
48
+ :data-invalid="data.isInvalid ? true : undefined"
49
+ v-bind="$attrs"
50
+ @keydown.down.prevent="onDown"
51
+ @keydown.up.prevent="onUp"
52
+ @keydown.enter.prevent="onEnter"
53
+ @keydown.esc.prevent="onEsc"
54
+ @click="onClick"
55
+ >
56
+ <warning-alt-filled
57
+ v-if="isWarning"
58
+ :class="`${carbonPrefix}--list-box__invalid-icon ${carbonPrefix}--list-box__invalid-icon--warning`"
59
+ />
60
+ <warning-filled
61
+ v-if="data.isInvalid"
62
+ :class="`${carbonPrefix}--list-box__invalid-icon`"
63
+ />
64
+ <button
65
+ ref="elButton"
66
+ type="button"
67
+ :class="`${carbonPrefix}--list-box__field`"
68
+ :aria-disabled="disabled"
69
+ aria-haspopup="listbox"
70
+ :aria-expanded="data.open ? 'true' : 'false'"
71
+ :aria-owns="uid"
72
+ :aria-controls="uid"
73
+ tabindex="0"
74
+ :aria-label="data.open ? 'close menu' : 'open menu'"
75
+ data-toggle="true"
76
+ >
77
+ <span ref="elTag">
78
+ <cv-tag
79
+ v-if="data.selectedItems.length > 0"
80
+ :class="{
81
+ [`${carbonPrefix}--list-box__selection--multi`]:
82
+ isFilterable && data.selectedItems.length > 0,
83
+ }"
84
+ :disabled="disabled"
85
+ :kind="filterTagKind"
86
+ :filter="true"
87
+ :label="`${data.selectedItems.length}`"
88
+ :style="filterableTagOverride"
89
+ @remove="clearValues"
90
+ />
91
+ </span>
92
+ <span
93
+ v-if="!isFilterable"
94
+ :class="`${carbonPrefix}--list-box__label`"
95
+ >{{ label }}</span
96
+ >
97
+ <template v-else>
98
+ <input
99
+ ref="elInput"
100
+ v-model="internalFilter"
101
+ :class="[
102
+ `${carbonPrefix}--text-input`,
103
+ {
104
+ [`${carbonPrefix}--text-input--empty`]:
105
+ !internalFilter || internalFilter.length === 0,
106
+ },
107
+ ]"
108
+ :aria-controls="uid"
109
+ aria-autocomplete="list"
110
+ role="combobox"
111
+ :aria-expanded="data.open ? 'true' : 'false'"
112
+ autocomplete="off"
113
+ :placeholder="label"
114
+ @input="onInput"
115
+ @focus="inputFocus"
116
+ @click.stop.prevent="inputClick"
117
+ />
118
+ <div
119
+ v-if="internalFilter.length > 0"
120
+ role="button"
121
+ :class="`${carbonPrefix}--list-box__selection`"
122
+ tabindex="0"
123
+ title="Clear filter"
124
+ @click.stop="clearFilter"
125
+ @keydown.enter.stop.prevent="clearFilter"
126
+ @keydown.space.stop.prevent
127
+ @keyup.space.stop.prevent="clearFilter"
128
+ >
129
+ <close-icon :style="moveCloseIcon" />
130
+ </div>
131
+ </template>
132
+ <div
133
+ :class="[
134
+ `${carbonPrefix}--list-box__menu-icon`,
135
+ { [`${carbonPrefix}--list-box__menu-icon--open`]: data.open },
136
+ ]"
137
+ role="button"
138
+ >
139
+ <chevron-down :aria-label="data.open ? 'Close menu' : 'Open menu'" />
140
+ </div>
141
+ </button>
142
+
143
+ <div
144
+ :id="uid"
145
+ ref="elList"
146
+ :aria-labelledby="`${uid}-label`"
147
+ :class="`${carbonPrefix}--list-box__menu`"
148
+ role="listbox"
149
+ >
150
+ <div
151
+ v-for="(item, index) in data.options"
152
+ ref="elOption"
153
+ role="menuitem"
154
+ :key="`multi-select-${index}`"
155
+ :class="[
156
+ `${carbonPrefix}--list-box__menu-item`,
157
+ {
158
+ [`${carbonPrefix}--list-box__menu-item--highlighted`]:
159
+ highlighted === item.value,
160
+ },
161
+ ]"
162
+ @click.stop.prevent="onItemClick(item.value)"
163
+ @mousemove="onMousemove(item.value)"
164
+ @mousedown.prevent
165
+ >
166
+ <div :class="`${carbonPrefix}--list-box__menu-item__option`">
167
+ <cv-checkbox
168
+ v-model="data.selectedItems"
169
+ tabindex="-1"
170
+ :form-item="false"
171
+ :value="item.value"
172
+ :name="item.name"
173
+ :label="item.label"
174
+ :disabled="item.disabled"
175
+ style="pointer-events: none"
176
+ />
177
+ </div>
178
+ </div>
179
+ </div>
180
+ </div>
181
+ <div v-if="data.isInvalid" :class="`${carbonPrefix}--form-requirement`">
182
+ <slot name="invalid-message">{{ invalidMessage }}</slot>
183
+ </div>
184
+ <div v-else-if="isWarning" :class="`${carbonPrefix}--form-requirement`">
185
+ <slot name="warning-message">{{ warningMessage }}</slot>
186
+ </div>
187
+ <div
188
+ v-if="isHelper"
189
+ :class="[
190
+ `${carbonPrefix}--form__helper-text`,
191
+ { [`${carbonPrefix}--form__helper-text--disabled`]: disabled },
192
+ ]"
193
+ >
194
+ <slot name="helper-text">{{ helperText }}</slot>
195
+ </div>
196
+ </div>
197
+ </template>
198
+
199
+ <script setup>
200
+ import { carbonPrefix } from '../../global/settings';
201
+ import { props as propsCvId, useCvId } from '../../use/cvId';
202
+ import { props as propsTheme, useIsLight } from '../../use/cvTheme';
203
+ import WarningFilled from '@carbon/icons-vue/es/warning--filled/16';
204
+ import WarningAltFilled from '@carbon/icons-vue/es/warning--alt--filled/16';
205
+ import ChevronDown from '@carbon/icons-vue/es/chevron--down/16';
206
+ import CloseIcon from '@carbon/icons-vue/es/close/16';
207
+ import CvCheckbox from '../CvCheckbox/CvCheckbox.vue';
208
+ import CvTag from '../CvTag/CvTag.vue';
209
+ import {
210
+ computed,
211
+ nextTick,
212
+ onMounted,
213
+ onUpdated,
214
+ reactive,
215
+ ref,
216
+ useSlots,
217
+ watch,
218
+ } from 'vue';
219
+ import {
220
+ DIRECTION_BOTTOM,
221
+ DIRECTION_TOP,
222
+ DIRECTIONS,
223
+ FIXED,
224
+ selectionFeedbackOptions,
225
+ TOP,
226
+ TOP_AFTER_REOPEN,
227
+ } from './consts';
228
+ import { tagKinds } from '../CvTag/consts';
229
+
230
+ defineOptions({
231
+ inheritAttrs: false,
232
+ });
233
+
234
+ /**
235
+ * Select options shape
236
+ * @typedef {Object} MultiSelectOption
237
+ * @property {string} name
238
+ * @property {string} label
239
+ * @property {string} value
240
+ * @property {boolean|undefined} disabled
241
+ */
242
+
243
+ const props = defineProps({
244
+ /**
245
+ * Automatically filter items based on label
246
+ */
247
+ autoFilter: { type: Boolean, default: false },
248
+ /**
249
+ * Highlight the first filtered item match. Requires either `autoFilter`
250
+ * or `filterable` to be `true`.
251
+ */
252
+ autoHighlight: { type: Boolean, default: false },
253
+ /**
254
+ * Specify the direction of the multiselect dropdown. Can be either `top` or `bottom`.
255
+ */
256
+ direction: {
257
+ type: String,
258
+ default: DIRECTION_BOTTOM,
259
+ validator: val => DIRECTIONS.includes(val),
260
+ },
261
+ /**
262
+ * Disable the control
263
+ */
264
+ disabled: { type: Boolean, default: false },
265
+ /**
266
+ * enable filterable multi-select. User must manually update the options in response to the `filter` event.
267
+ */
268
+ filterable: { type: Boolean, default: false },
269
+ /**
270
+ * A CvTag is displayed for sections and this property controls the color/kind on tag shown.
271
+ */
272
+ filterTagKind: {
273
+ type: String,
274
+ default: 'high-contrast',
275
+ validator: val => tagKinds.includes(val),
276
+ },
277
+ /**
278
+ * Provide helper text that is used alongside the control label for additional help
279
+ */
280
+ helperText: { type: String, default: undefined },
281
+ /**
282
+ * The value of an option to initially highlight
283
+ */
284
+ highlight: String,
285
+ /**
286
+ * Specify 'true' to create an inline multi-select.
287
+ */
288
+ inline: { type: Boolean, default: false },
289
+ /**
290
+ * Message which is displayed if the value is invalid.
291
+ */
292
+ invalidMessage: { type: String, default: undefined },
293
+ /**
294
+ * Generic label that will be used as the textual representation of what this field is for
295
+ */
296
+ label: {
297
+ type: String,
298
+ default: 'Choose options',
299
+ },
300
+ /***
301
+ * options in the form [{ label: '', value: '', name: '', disabled: false}]
302
+ * Array<MultiSelectOption>
303
+ */
304
+ options: {
305
+ type: Array,
306
+ required: true,
307
+ validator(list) {
308
+ const result = list.every(
309
+ item =>
310
+ typeof item.name === 'string' &&
311
+ typeof item.label === 'string' &&
312
+ typeof item.value === 'string'
313
+ );
314
+ if (!result) {
315
+ console.warn(
316
+ 'CvMultiSelect - all options must have name, label and value'
317
+ );
318
+ }
319
+ return result;
320
+ },
321
+ },
322
+ /**
323
+ * Specify feedback (mode) of the selection.
324
+ * - top: selected item jumps to top
325
+ * - fixed: selected item stays at its position
326
+ * - top-after-reopen: selected items jump to top after reopening dropdown
327
+ */
328
+ selectionFeedback: {
329
+ type: String,
330
+ default: selectionFeedbackOptions[TOP_AFTER_REOPEN],
331
+ validator(val) {
332
+ if (!selectionFeedbackOptions.includes(val)) {
333
+ console.warn(
334
+ `CvMultiSelect: invalid selectionFeedback "${val}", use one of ${selectionFeedbackOptions}`
335
+ );
336
+ return false;
337
+ }
338
+ return true;
339
+ },
340
+ },
341
+ /**
342
+ * Provide text to be used in a <label> element that is tied to the multiselect via ARIA attributes.
343
+ */
344
+ title: String,
345
+ /***
346
+ * Allow users to pass in arbitrary items from their collection that are pre-selected
347
+ */
348
+ value: { type: Array, default: () => [] },
349
+ /**
350
+ * Provide the text that is displayed and put the control in warning state
351
+ */
352
+ warningMessage: { type: String, default: undefined },
353
+ modelValue: {
354
+ type: Array,
355
+ validator: val => {
356
+ console.error(
357
+ `v-model for cv-multi-select is deprecated. Specify "v-model:value" instead [${val}]`
358
+ );
359
+ return true;
360
+ },
361
+ default: undefined,
362
+ },
363
+ ...propsCvId,
364
+ ...propsTheme,
365
+ });
366
+ const uid = useCvId(props, true);
367
+ const isLight = useIsLight(props);
368
+
369
+ const data = reactive({
370
+ open: false,
371
+ /***
372
+ * Array<MultiSelectOption>
373
+ */
374
+ options: [],
375
+ selectedItems: [],
376
+ highlighted: null,
377
+ filter: '',
378
+ isHelper: false,
379
+ isWarning: false,
380
+ isInvalid: false,
381
+ });
382
+ const emit = defineEmits(['update:value', 'change', 'filter']);
383
+ watch(
384
+ () => data.selectedItems,
385
+ () => {
386
+ if (JSON.stringify(data.selectedItems) !== JSON.stringify(props.value)) {
387
+ emit('change', data.selectedItems);
388
+ emit('update:value', data.selectedItems);
389
+ }
390
+ },
391
+ {
392
+ deep: true,
393
+ }
394
+ );
395
+
396
+ const isDirectionUp = computed(() => {
397
+ return props.direction === DIRECTION_TOP;
398
+ });
399
+ const moveCloseIcon = computed(() => {
400
+ if (isWarning.value || data.isInvalid)
401
+ return { left: '-1rem', position: 'relative' };
402
+ else return undefined;
403
+ });
404
+ const inlineInvalid = computed(() => {
405
+ return props.inline && data.isInvalid;
406
+ });
407
+ const isFilterable = computed(() => {
408
+ return props.filterable || props.autoFilter;
409
+ });
410
+
411
+ function updateSelectedItems() {
412
+ data.selectedItems = props.value.filter(
413
+ /***
414
+ * @param {string} item
415
+ * @returns {boolean}
416
+ */
417
+ item =>
418
+ data.options.some(
419
+ /***
420
+ * @param {MultiSelectOption} opt
421
+ * @returns {boolean}
422
+ */
423
+ opt => opt.value === item.trim()
424
+ )
425
+ );
426
+ }
427
+
428
+ const slots = useSlots();
429
+ onMounted(checkSlots);
430
+ onUpdated(checkSlots);
431
+ onMounted(updateOptions);
432
+ onMounted(updateSelectedItems);
433
+
434
+ watch(() => props.value, updateSelectedItems);
435
+ watch(() => props.options, updateOptions);
436
+ watch(() => props.selectionFeedback, updateOptions);
437
+
438
+ const highlighted = computed({
439
+ get() {
440
+ return data.highlighted;
441
+ },
442
+ set(val) {
443
+ let firstMatchIndex = data.options.findIndex(
444
+ /***
445
+ * @param {MultiSelectOption} item
446
+ * @returns {boolean}
447
+ */
448
+ item => item.value === val
449
+ );
450
+ if (firstMatchIndex < 0) {
451
+ firstMatchIndex = data.options.length ? 0 : -1;
452
+ data.highlighted = firstMatchIndex >= 0 ? data.options[0].value : '';
453
+ } else {
454
+ data.highlighted = val;
455
+ }
456
+ if (firstMatchIndex >= 0) {
457
+ nextTick(() => {
458
+ // $nextTick to prevent highlight check ahead of list update on filter
459
+ checkHighlightPosition(firstMatchIndex);
460
+ });
461
+ }
462
+ },
463
+ });
464
+ onMounted(() => {
465
+ highlighted.value = props.value ? props.value : props.highlight; // override highlight with value if provided
466
+ });
467
+
468
+ const internalFilter = computed({
469
+ get() {
470
+ return data.filter;
471
+ },
472
+ set(val) {
473
+ data.filter = val ? val : '';
474
+ emit('filter', val);
475
+ },
476
+ });
477
+ const filterableTagOverride = computed(() => {
478
+ // <style carbon tweaks - DO NOT USE STYLE TAG as it causes SSR issues
479
+ return props.filterable ? { marginTop: 0, marginBottom: 0 } : {};
480
+ });
481
+
482
+ const elInput = ref(null);
483
+ const elList = ref(null);
484
+ const elOption = ref(null);
485
+ const elButton = ref(null);
486
+ const el = ref(null);
487
+ const elTag = ref(null);
488
+
489
+ function checkSlots() {
490
+ // NOTE: slots is not reactive so needs to be managed on updated
491
+ data.isInvalid = !!(slots['invalid-message'] || props.invalidMessage?.length);
492
+ data.isWarning = !!(slots['warning-message'] || props.warningMessage?.length);
493
+ data.isHelper = !!(slots['helper-text'] || props.helperText?.length);
494
+ }
495
+ const isWarning = computed(() => {
496
+ if (data.isInvalid) return false;
497
+ else return !!data.isWarning;
498
+ });
499
+ const isHelper = computed(() => {
500
+ if (data.isInvalid || props.inline || isWarning.value) return false;
501
+ else return !!data.isHelper;
502
+ });
503
+
504
+ function clearFilter() {
505
+ internalFilter.value = '';
506
+ elInput.value?.focus();
507
+ doOpen(true);
508
+ updateOptions();
509
+ }
510
+ function checkHighlightPosition(newHighlight) {
511
+ if (elList.value && elOption.value && elOption.value[newHighlight]) {
512
+ if (elList.value?.scrollTop >= elOption.value[newHighlight].offsetTop) {
513
+ elList.value.scrollTop = elOption.value[newHighlight].offsetTop;
514
+ } else if (
515
+ elList.value.scrollTop + elList.value.clientHeight <
516
+ elOption.value[newHighlight].offsetTop +
517
+ elOption.value[newHighlight].offsetHeight
518
+ ) {
519
+ elList.value.scrollTop =
520
+ elOption.value[newHighlight].offsetTop +
521
+ elOption.value[newHighlight].offsetHeight -
522
+ elList.value.clientHeight;
523
+ }
524
+ }
525
+ }
526
+ function doMove(up) {
527
+ if (data.options.length > 0) {
528
+ // re-query could have changed
529
+ const currentHighlight = data.options.findIndex(
530
+ /***
531
+ * @param {MultiSelectOption} item
532
+ * @returns {boolean}
533
+ */
534
+ item => item.value === highlighted.value
535
+ );
536
+ let newHighlight;
537
+
538
+ if (up) {
539
+ if (currentHighlight <= 0) {
540
+ newHighlight = data.options.length - 1;
541
+ } else {
542
+ newHighlight = currentHighlight - 1;
543
+ }
544
+ } else {
545
+ if (currentHighlight >= data.options.length - 1) {
546
+ newHighlight = 0;
547
+ } else {
548
+ newHighlight = currentHighlight + 1;
549
+ }
550
+ }
551
+ highlighted.value = data.options[newHighlight].value;
552
+ }
553
+ }
554
+ function updateOptions() {
555
+ if (props.autoFilter) {
556
+ const escFilter = internalFilter.value.replace(
557
+ /[.*+?^${}()|[\]\\]/g,
558
+ '\\$&'
559
+ );
560
+ const pat = new RegExp(escFilter, 'iu');
561
+ data.options = props.options.filter(opt => pat.test(opt.label)).slice(0);
562
+ } else {
563
+ data.options = props.options.slice(0);
564
+ }
565
+ highlighted.value = props.highlight;
566
+
567
+ // multi select unique part
568
+ if (props.selectionFeedback !== selectionFeedbackOptions[FIXED]) {
569
+ // if included in data value move to top
570
+ data.options.sort(
571
+ /***
572
+ * @param {MultiSelectOption} a
573
+ * @param {MultiSelectOption} b
574
+ * @returns {number}
575
+ */
576
+ (a, b) =>
577
+ (data.selectedItems.includes(a.value) ? -1 : 1) -
578
+ (data.selectedItems.includes(b.value) ? -1 : 1)
579
+ );
580
+ }
581
+ }
582
+ function updateHighlight() {
583
+ let firstMatchIndex;
584
+ if (props.autoHighlight && data.options.length > 0) {
585
+ // then highlight first match
586
+ const filterRegex = new RegExp(internalFilter.value, 'iu');
587
+ firstMatchIndex = data.options.findIndex(
588
+ /***
589
+ * @param {MultiSelectOption} item
590
+ * @returns {boolean}
591
+ */
592
+ item => filterRegex.test(item.label)
593
+ );
594
+ if (firstMatchIndex < 0) {
595
+ firstMatchIndex = 0;
596
+ }
597
+ highlighted.value = data.options[firstMatchIndex].value;
598
+ }
599
+ }
600
+ function onInput() {
601
+ doOpen(true);
602
+
603
+ updateOptions();
604
+ updateHighlight();
605
+ }
606
+ function doOpen(newVal) {
607
+ if (
608
+ newVal &&
609
+ !data.open &&
610
+ props.selectionFeedback === selectionFeedbackOptions[TOP_AFTER_REOPEN]
611
+ ) {
612
+ updateOptions();
613
+ }
614
+ data.open = newVal;
615
+ }
616
+ function onDown() {
617
+ if (!data.open) {
618
+ doOpen(true);
619
+ } else {
620
+ doMove(false);
621
+ }
622
+ }
623
+ function onUp() {
624
+ if (data.open) {
625
+ doMove(true);
626
+ }
627
+ }
628
+ function inputOrButtonFocus() {
629
+ if (props.filterable) {
630
+ elInput.value?.focus();
631
+ } else {
632
+ elButton.value?.focus();
633
+ }
634
+ }
635
+ function onEsc() {
636
+ doOpen(false);
637
+ inputOrButtonFocus();
638
+ }
639
+ function onEnter() {
640
+ if (data.open) {
641
+ onItemClick(highlighted.value);
642
+ elInput.value?.focus();
643
+ internalFilter.value = '';
644
+
645
+ doOpen(false);
646
+ updateOptions();
647
+ } else {
648
+ doOpen(true);
649
+ }
650
+ }
651
+ function onClick(ev) {
652
+ if (props.disabled) {
653
+ ev.preventDefault();
654
+ } else {
655
+ if (data.open) {
656
+ inputOrButtonFocus();
657
+ // done this way round otherwise will auto open on focus.
658
+ nextTick(() => {
659
+ doOpen(false);
660
+ });
661
+ } else {
662
+ doOpen(true);
663
+ inputOrButtonFocus();
664
+ }
665
+ }
666
+ }
667
+ function clearValues() {
668
+ data.selectedItems = [];
669
+ inputOrButtonFocus();
670
+ }
671
+ function onFocusOut(ev) {
672
+ if (
673
+ !el.value?.contains(ev.relatedTarget) &&
674
+ !elTag.value?.contains(ev.target)
675
+ ) {
676
+ doOpen(false);
677
+ }
678
+ }
679
+ function onMousemove(val) {
680
+ highlighted.value = val;
681
+ }
682
+ function onItemClick(val) {
683
+ const option = props.options.find(item => item.value === val);
684
+ if (option && !option.disabled) {
685
+ const index = data.selectedItems.findIndex(item => val === item);
686
+ if (index > -1) {
687
+ data.selectedItems.splice(index, 1);
688
+ } else {
689
+ data.selectedItems.push(val);
690
+ }
691
+ if (props.selectionFeedback === selectionFeedbackOptions[TOP]) {
692
+ updateOptions();
693
+ }
694
+ elButton.value?.focus();
695
+ }
696
+ }
697
+ function inputClick() {
698
+ if (!data.open) {
699
+ doOpen(true);
700
+ }
701
+ }
702
+ function inputFocus() {
703
+ doOpen(true);
704
+ }
705
+ </script>
@@ -0,0 +1,16 @@
1
+ const TOP_AFTER_REOPEN = 0;
2
+ const TOP = 1;
3
+ const FIXED = 2;
4
+ const DIRECTION_BOTTOM = 'bottom';
5
+ const DIRECTION_TOP = 'top';
6
+ const DIRECTIONS = [DIRECTION_TOP, DIRECTION_BOTTOM];
7
+ const selectionFeedbackOptions = ['top-after-reopen', 'top', 'fixed'];
8
+ export {
9
+ TOP_AFTER_REOPEN,
10
+ TOP,
11
+ FIXED,
12
+ selectionFeedbackOptions,
13
+ DIRECTIONS,
14
+ DIRECTION_TOP,
15
+ DIRECTION_BOTTOM,
16
+ };