@bethinkpl/design-system 41.1.5 → 43.0.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 (73) hide show
  1. package/dist/design-system.css +1 -1
  2. package/dist/design-system.js +16800 -13061
  3. package/dist/design-system.js.map +1 -1
  4. package/dist/lib/js/components/Buttons/IconButton/IconButton.vue.d.ts +2 -0
  5. package/dist/lib/js/components/Cards/Card/Card.consts.d.ts +14 -0
  6. package/dist/lib/js/components/Cards/Card/Card.vue.d.ts +10 -5
  7. package/dist/lib/js/components/Cards/CardExpandable/CardExpandable.vue.d.ts +35 -15
  8. package/dist/lib/js/components/DatePickers/DateBox/DateBox.vue.d.ts +2 -0
  9. package/dist/lib/js/components/DatePickers/DatePicker/DatePicker.vue.d.ts +2 -0
  10. package/dist/lib/js/components/DatePickers/DateRangePicker/DateRangePicker.vue.d.ts +2 -0
  11. package/dist/lib/js/components/Drawer/DrawerSection/DrawerSection.vue.d.ts +6 -0
  12. package/dist/lib/js/components/Form/InputField/useInputFieldWithinForm.d.ts +1 -1
  13. package/dist/lib/js/components/Form/RadioButton/RadioButton.vue.d.ts +2 -0
  14. package/dist/lib/js/components/Form/SelectField/SelectField.types.d.ts +26 -0
  15. package/dist/lib/js/components/Form/SelectField/SelectField.utils.d.ts +7 -0
  16. package/dist/lib/js/components/Form/SelectField/SelectField.vue.d.ts +107 -0
  17. package/dist/lib/js/components/Form/SelectField/SelectFieldOption.vue.d.ts +19 -0
  18. package/dist/lib/js/components/Form/SelectField/index.d.ts +4 -0
  19. package/dist/lib/js/components/Form/SelectField/useSelectFieldWithinForm.d.ts +8 -0
  20. package/dist/lib/js/components/Headers/OverlayHeader/OverlayHeader.vue.d.ts +4 -0
  21. package/dist/lib/js/components/Headers/SectionHeader/SectionHeader.vue.d.ts +4 -0
  22. package/dist/lib/js/components/Icons/Icon/Icon.consts.d.ts +2 -0
  23. package/dist/lib/js/components/Modals/Modal/Modal.vue.d.ts +4 -0
  24. package/dist/lib/js/components/Modals/ModalDialog/ModalDialog.vue.d.ts +4 -0
  25. package/dist/lib/js/components/Pagination/Pagination.vue.d.ts +6 -0
  26. package/dist/lib/js/components/ProgressBar/ProgressBar.vue.d.ts +2 -0
  27. package/dist/lib/js/components/ProgressDonutChart/ProgressDonutChart.vue.d.ts +2 -0
  28. package/dist/lib/js/components/RichList/BasicRichListItem/BasicRichListItem.vue.d.ts +2 -0
  29. package/dist/lib/js/components/RichList/RichListItem/RichListItem.vue.d.ts +2 -0
  30. package/dist/lib/js/components/SelectList/SelectListItem/SelectListItem.vue.d.ts +2 -0
  31. package/dist/lib/js/components/SelectList/SelectListItemToggle/SelectListItemToggle.vue.d.ts +2 -0
  32. package/dist/lib/js/components/Statuses/AccessStatus/AccessStatus.vue.d.ts +2 -0
  33. package/dist/lib/js/components/Statuses/BlockadeStatus/BlockadeStatus.vue.d.ts +2 -0
  34. package/dist/lib/js/components/SurveyQuestions/SurveyQuestionOpenEnded/SurveyQuestionOpenEnded.vue.d.ts +1009 -169
  35. package/dist/lib/js/components/SurveyQuestions/SurveyQuestionScale/SurveyQuestionScale.vue.d.ts +1000 -160
  36. package/dist/lib/js/components/Switch/Switch.vue.d.ts +2 -0
  37. package/dist/lib/js/components/Tile/Tile.sb.shared.d.ts +2 -0
  38. package/dist/lib/js/components/Toast/Toast.vue.d.ts +33 -15
  39. package/dist/lib/js/components/Toggles/ToggleButton/ToggleButton.vue.d.ts +2 -0
  40. package/dist/lib/js/composables/useFormFieldWithinForm.d.ts +3 -3
  41. package/dist/lib/js/icons/fontawesome.d.ts +2 -0
  42. package/dist/lib/js/index.d.ts +2 -1
  43. package/lib/js/components/Cards/Card/Card.consts.ts +18 -0
  44. package/lib/js/components/Cards/Card/Card.spec.ts +159 -33
  45. package/lib/js/components/Cards/Card/Card.stories.ts +21 -2
  46. package/lib/js/components/Cards/Card/Card.vue +90 -23
  47. package/lib/js/components/Dropdown/Dropdown.vue +6 -10
  48. package/lib/js/components/Form/Form.stories.ts +15 -1
  49. package/lib/js/components/Form/SelectField/SelectField.spec.ts +495 -0
  50. package/lib/js/components/Form/SelectField/SelectField.stories.ts +287 -0
  51. package/lib/js/components/Form/SelectField/SelectField.types.ts +65 -0
  52. package/lib/js/components/Form/SelectField/SelectField.utils.ts +83 -0
  53. package/lib/js/components/Form/SelectField/SelectField.vue +342 -0
  54. package/lib/js/components/Form/SelectField/SelectFieldOption.vue +51 -0
  55. package/lib/js/components/Form/SelectField/index.ts +5 -0
  56. package/lib/js/components/Form/SelectField/useSelectFieldWithinForm.ts +27 -0
  57. package/lib/js/components/SelectList/SelectListItem/SelectListItem.spec.ts +77 -0
  58. package/lib/js/components/SelectList/SelectListItem/SelectListItem.vue +4 -1
  59. package/lib/js/components/SurveyQuestions/SurveyQuestionOpenEnded/SurveyQuestionOpenEnded.vue +7 -12
  60. package/lib/js/components/SurveyQuestions/SurveyQuestionScale/SurveyQuestionScale.vue +7 -12
  61. package/lib/js/composables/useFormFieldWithinForm.ts +4 -4
  62. package/lib/js/icons/fontawesome.ts +4 -0
  63. package/lib/js/index.ts +2 -1
  64. package/lib/js/styles/Shadows/Shadows.stories.scss +16 -0
  65. package/lib/js/styles/Shadows/Shadows.stories.ts +4 -0
  66. package/lib/styles/mixins/_dropdown-surface.scss +24 -0
  67. package/lib/styles/settings/_shadows.scss +18 -0
  68. package/package.json +1 -1
  69. package/dist/lib/js/components/Modal/Modal.vue.d.ts +0 -395
  70. package/dist/lib/js/components/Modal/index.d.ts +0 -3
  71. package/lib/js/components/Modal/Modal.spec.ts +0 -58
  72. package/lib/js/components/Modal/Modal.vue +0 -137
  73. package/lib/js/components/Modal/index.ts +0 -3
@@ -0,0 +1,342 @@
1
+ <template>
2
+ <form-field v-bind="formFieldProps">
3
+ <template #field="{ fieldId, messageId }">
4
+ <select-root
5
+ v-model="value"
6
+ :autocomplete="autocomplete"
7
+ :disabled="isDisabled"
8
+ :required="formFieldProps.hasRequiredIndicator"
9
+ @update:open="onOpenChange"
10
+ >
11
+ <select-trigger
12
+ :id="fieldId"
13
+ :aria-describedby="hasMessage ? messageId : undefined"
14
+ :aria-label="formFieldProps.label ? undefined : ariaLabel"
15
+ :class="[
16
+ 'ds-selectField__trigger',
17
+ {
18
+ '-ds-error': formFieldProps.state === FORM_FIELD_STATES.ERROR,
19
+ '-ds-disabled': isDisabled,
20
+ },
21
+ ]"
22
+ >
23
+ <ds-icon
24
+ v-if="leftIcon"
25
+ class="ds-selectField__leftIcon"
26
+ :icon="leftIcon"
27
+ :size="ICON_SIZES.X_SMALL"
28
+ />
29
+ <select-value class="ds-selectField__value" :placeholder="placeholder" />
30
+ <select-icon as-child>
31
+ <ds-icon
32
+ class="ds-selectField__icon"
33
+ :icon="ICONS.FA_CHEVRON_DOWN"
34
+ :size="ICON_SIZES.X_SMALL"
35
+ />
36
+ </select-icon>
37
+ </select-trigger>
38
+
39
+ <select-portal>
40
+ <select-content
41
+ class="ds-selectField__content"
42
+ position="popper"
43
+ align="start"
44
+ :side-offset="4"
45
+ :style="contentStyle"
46
+ >
47
+ <select-viewport as-child>
48
+ <ds-select-list>
49
+ <template
50
+ v-for="(group, groupIndex) in normalizedOptions"
51
+ :key="groupIndex"
52
+ >
53
+ <select-separator v-if="groupIndex > 0" as-child>
54
+ <ds-select-list-item-divider />
55
+ </select-separator>
56
+
57
+ <!-- A labelled group gets real `role="group"` semantics. -->
58
+ <select-group v-if="group.label">
59
+ <select-label as-child>
60
+ <ds-select-list-section-title
61
+ :label="group.label"
62
+ :is-uppercase="isGroupLabelUppercase"
63
+ />
64
+ </select-label>
65
+ <select-field-option
66
+ v-for="option in group.options"
67
+ :key="option.value"
68
+ :option="option"
69
+ :is-selected="option.value === value"
70
+ />
71
+ </select-group>
72
+
73
+ <!--
74
+ Ungrouped options render as direct children of the listbox:
75
+ an unlabelled SelectGroup would emit `role="group"` with a
76
+ dangling `aria-labelledby`.
77
+ -->
78
+ <template v-else>
79
+ <select-field-option
80
+ v-for="option in group.options"
81
+ :key="option.value"
82
+ :option="option"
83
+ :is-selected="option.value === value"
84
+ />
85
+ </template>
86
+ </template>
87
+ </ds-select-list>
88
+ </select-viewport>
89
+ </select-content>
90
+ </select-portal>
91
+ </select-root>
92
+ </template>
93
+ <!-- begin: FormField slots -->
94
+ <template v-if="$slots.help" #help>
95
+ <slot name="help" />
96
+ </template>
97
+ <template v-if="$slots.labelAside" #labelAside>
98
+ <slot name="labelAside" />
99
+ </template>
100
+ <template v-if="$slots.message" #message="{ messageId }">
101
+ <slot name="message" :message-id="messageId" />
102
+ </template>
103
+ <template v-if="$slots.fieldStatus" #fieldStatus>
104
+ <slot name="fieldStatus" />
105
+ </template>
106
+ <!-- end: FormField slots -->
107
+ </form-field>
108
+ </template>
109
+
110
+ <style lang="scss" scoped>
111
+ @import '../../../../styles/settings/spacings';
112
+ @import '../../../../styles/settings/radiuses';
113
+ @import '../../../../styles/settings/borders';
114
+ @import '../../../../styles/settings/colors/tokens';
115
+ @import '../../../../styles/settings/typography/tokens';
116
+ @import '../../../../styles/settings/shadows';
117
+ @import '../../../../styles/settings/animations';
118
+
119
+ .ds-selectField {
120
+ $root: &;
121
+
122
+ // The element rules come before `&__trigger` so that the trigger's state overrides, which
123
+ // target the same elements at a higher specificity, stay in ascending order.
124
+ &__leftIcon {
125
+ color: $color-neutral-icon-weak;
126
+ }
127
+
128
+ &__value {
129
+ @include formText-s-default-regular;
130
+
131
+ color: $color-neutral-text-heavy;
132
+ flex: 1;
133
+ // override the default min-width so long values can ellipsize
134
+ min-width: 0;
135
+ overflow: hidden;
136
+ text-align: left;
137
+ text-overflow: ellipsis;
138
+ white-space: nowrap;
139
+
140
+ &[data-placeholder] {
141
+ color: $color-neutral-text-weak;
142
+ }
143
+ }
144
+
145
+ &__icon {
146
+ color: $color-neutral-icon;
147
+ transition: $default-cubic-bezier-transition;
148
+ }
149
+
150
+ &__trigger {
151
+ align-items: center;
152
+ // FormField__field is `align-items: flex-start`, so the trigger must opt into full width.
153
+ align-self: stretch;
154
+ background: $color-default-background;
155
+ border: $border-xs solid $color-neutral-border-strong;
156
+ border-radius: $radius-s;
157
+ box-shadow: $shadow-inset-m;
158
+ cursor: pointer;
159
+ display: flex;
160
+ font-family: inherit;
161
+ gap: $space-4;
162
+ min-height: 32px;
163
+ // Focus is conveyed by the border color below, so the UA ring is suppressed
164
+ outline: none;
165
+ padding: 0 $space-4;
166
+ text-align: left;
167
+
168
+ // The trigger is a button, so `[data-state='open']` replaces InputField's `:focus-within`.
169
+ &[data-state='open'] {
170
+ border-color: $color-primary-border;
171
+
172
+ #{$root}__icon {
173
+ transform: rotate(180deg);
174
+ }
175
+ }
176
+
177
+ &:focus-visible {
178
+ border-color: $color-primary-border;
179
+ }
180
+
181
+ &.-ds-disabled {
182
+ border-color: $color-neutral-border-strong-disabled;
183
+ box-shadow: none;
184
+ cursor: default;
185
+
186
+ #{$root}__leftIcon {
187
+ color: $color-neutral-icon-weak-disabled;
188
+ }
189
+
190
+ #{$root}__value {
191
+ color: $color-neutral-text-heavy-disabled;
192
+
193
+ &[data-placeholder] {
194
+ color: $color-neutral-text-weak-disabled;
195
+ }
196
+ }
197
+
198
+ #{$root}__icon {
199
+ color: $color-neutral-icon-disabled;
200
+ }
201
+ }
202
+
203
+ // `:focus-visible` is excluded so that focusing an errored trigger shows the primary
204
+ // border, the same way InputField's error rule yields to `:focus-within`.
205
+ &.-ds-error:not([data-state='open']):not(:focus-visible) {
206
+ border-color: $color-danger-border;
207
+ }
208
+
209
+ &:hover:not(.-ds-disabled):not([data-state='open']) {
210
+ border-color: $color-neutral-border-strong-hovered;
211
+
212
+ #{$root}__icon {
213
+ color: $color-neutral-icon-hovered;
214
+ }
215
+ }
216
+ }
217
+ }
218
+ </style>
219
+
220
+ <!--
221
+ `ds-selectField__content` is styled unscoped on purpose. SelectContent renders through
222
+ Presence's slot rather than as its root, so Vue never forwards this component's scope id to
223
+ that element — a scoped rule would not match it. It is also portalled to `<body>`, so
224
+ `:deep()` has no ancestor to hang off either. Same approach as DatePicker.vue.
225
+ -->
226
+ <style lang="scss">
227
+ @import '../../../../styles/settings/z-indexes';
228
+ @import '../../../../styles/mixins/dropdown-surface';
229
+
230
+ .ds-selectField__content {
231
+ @include dropdownSurface;
232
+ @include dropdownSurfaceRadiusBottom;
233
+
234
+ // SelectViewport owns the scrolling; this is the flex column that caps the height.
235
+ display: flex;
236
+ flex-direction: column;
237
+ max-height: min(
238
+ var(--select-field-max-height, 320px),
239
+ var(--reka-select-content-available-height)
240
+ );
241
+ // Widens to fit the longest option but never narrower than the trigger, and never wider
242
+ // than the space available in the viewport.
243
+ max-width: var(--reka-select-content-available-width);
244
+ min-width: var(--reka-select-trigger-width);
245
+ // Portalled to <body>, so it needs to clear modal content. It sits on the modal layer
246
+ // rather than above it: the listbox is appended after the (also teleported) modal, so an
247
+ // equal z-index still paints it on top, and tooltips at 16777271 stay above both.
248
+ z-index: $z-index-modal;
249
+ }
250
+ </style>
251
+
252
+ <script lang="ts" setup>
253
+ import { computed, CSSProperties } from 'vue';
254
+ import {
255
+ SelectContent,
256
+ SelectGroup,
257
+ SelectIcon,
258
+ SelectLabel,
259
+ SelectPortal,
260
+ SelectRoot,
261
+ SelectSeparator,
262
+ SelectTrigger,
263
+ SelectValue,
264
+ SelectViewport,
265
+ } from 'reka-ui';
266
+ import FormField, { FORM_FIELD_STATES, FormFieldProps } from '../FormField';
267
+ import { extractFormFieldProps } from '../FormField/FormField.utils';
268
+ import DsIcon, { ICON_SIZES, ICONS } from '../../Icons/Icon';
269
+ import DsSelectList from '../../SelectList/SelectList.vue';
270
+ import DsSelectListItemDivider from '../../SelectList/SelectListItemDivider/SelectListItemDivider.vue';
271
+ import DsSelectListSectionTitle from '../../SelectList/SelectListSectionTitle/SelectListSectionTitle.vue';
272
+ import SelectFieldOption from './SelectFieldOption.vue';
273
+ import { SelectFieldProps, SelectFieldSlots, SelectFieldValue } from './SelectField.types';
274
+ import { assertOptionValues, normalizeOptions, toCssLength } from './SelectField.utils';
275
+ import { useSelectFieldWithinForm } from './useSelectFieldWithinForm';
276
+
277
+ const {
278
+ options,
279
+ placeholder,
280
+ leftIcon,
281
+ ariaLabel,
282
+ maxHeight,
283
+ name,
284
+ autocomplete,
285
+ isGroupLabelUppercase = true,
286
+ ...rest
287
+ } = defineProps<SelectFieldProps>();
288
+
289
+ const slots = defineSlots<SelectFieldSlots>();
290
+
291
+ const emit = defineEmits<{
292
+ 'open-change': [isOpen: boolean];
293
+ }>();
294
+
295
+ const modelValue = defineModel<SelectFieldValue>();
296
+
297
+ const { value, errors, onClose } = useSelectFieldWithinForm(() => name, modelValue);
298
+
299
+ const { formFieldProps, isDisabled, hasMessage } = useFormFieldState();
300
+ const { normalizedOptions } = useOptions();
301
+
302
+ const contentStyle = computed<CSSProperties | undefined>(() => {
303
+ const resolvedMaxHeight = toCssLength(maxHeight);
304
+
305
+ return resolvedMaxHeight
306
+ ? ({ '--select-field-max-height': resolvedMaxHeight } as CSSProperties)
307
+ : undefined;
308
+ });
309
+
310
+ function onOpenChange(isOpen: boolean) {
311
+ // Closing the listbox is a select's equivalent of blur.
312
+ if (!isOpen) {
313
+ onClose(new Event('blur'));
314
+ }
315
+
316
+ emit('open-change', isOpen);
317
+ }
318
+
319
+ function useFormFieldState() {
320
+ const formFieldProps = computed<FormFieldProps>(() =>
321
+ // this is needed to avoid passing modelValue to FormField as prop
322
+ extractFormFieldProps(rest, errors.value),
323
+ );
324
+
325
+ const isDisabled = computed(() => formFieldProps.value.state === FORM_FIELD_STATES.DISABLED);
326
+
327
+ // Avoids pointing `aria-describedby` at a message element that is never rendered.
328
+ const hasMessage = computed(() => !!(formFieldProps.value.messageText || slots.message));
329
+
330
+ return { formFieldProps, isDisabled, hasMessage };
331
+ }
332
+
333
+ function useOptions() {
334
+ const normalizedOptions = computed(() => normalizeOptions(options));
335
+
336
+ // Dev-time guard, called synchronously so invalid options fail at mount instead of
337
+ // surfacing as an unhandled rejection while the offscreen content fragment renders.
338
+ assertOptionValues(normalizedOptions.value);
339
+
340
+ return { normalizedOptions };
341
+ }
342
+ </script>
@@ -0,0 +1,51 @@
1
+ <template>
2
+ <select-item
3
+ as-child
4
+ :value="option.value"
5
+ :disabled="option.isDisabled"
6
+ :text-value="option.label"
7
+ :aria-label="option.eyebrowText ? getOptionAccessibleName(option) : undefined"
8
+ class="ds-selectFieldOption"
9
+ >
10
+ <ds-select-list-item
11
+ :label="option.label"
12
+ :icon-left="option.iconLeft"
13
+ :eyebrow-text="option.eyebrowText"
14
+ :is-selected="isSelected"
15
+ :state="
16
+ option.isDisabled
17
+ ? SELECT_LIST_ITEM_STATES.DISABLED
18
+ : SELECT_LIST_ITEM_STATES.DEFAULT
19
+ "
20
+ >
21
+ <!--
22
+ reka-ui names each option via `aria-labelledby` pointing at the id rendered by
23
+ SelectItemText, so the visible label must live inside it.
24
+ -->
25
+ <template #text>
26
+ <select-item-text>{{ option.label }}</select-item-text>
27
+ </template>
28
+ </ds-select-list-item>
29
+ </select-item>
30
+ </template>
31
+
32
+ <style lang="scss">
33
+ .ds-selectFieldOption {
34
+ // The highlighted state is conveyed by SelectListItem's own styling, so the UA focus ring
35
+ // is suppressed
36
+ outline: none;
37
+ }
38
+ </style>
39
+
40
+ <script lang="ts" setup>
41
+ import { SelectItem, SelectItemText } from 'reka-ui';
42
+ import DsSelectListItem from '../../SelectList/SelectListItem/SelectListItem.vue';
43
+ import { SELECT_LIST_ITEM_STATES } from '../../SelectList/SelectListItem/SelectListItem.consts';
44
+ import { SelectFieldOption as SelectFieldOptionType } from './SelectField.types';
45
+ import { getOptionAccessibleName } from './SelectField.utils';
46
+
47
+ const { option, isSelected = false } = defineProps<{
48
+ option: SelectFieldOptionType;
49
+ isSelected?: boolean;
50
+ }>();
51
+ </script>
@@ -0,0 +1,5 @@
1
+ import SelectField from './SelectField.vue';
2
+
3
+ export * from './SelectField.types';
4
+
5
+ export default SelectField;
@@ -0,0 +1,27 @@
1
+ import { MaybeRefOrGetter, Ref } from 'vue';
2
+ import { useFormFieldWithinForm } from '../../../composables/useFormFieldWithinForm';
3
+ import { SelectFieldValue } from './SelectField.types';
4
+
5
+ /**
6
+ * Select counterpart of `useInputFieldWithinForm`. A select has no `input` event, so closing
7
+ * the listbox stands in for blur — that is when the field is marked touched.
8
+ *
9
+ * Revalidation on selection needs no handler here: vee-validate's `useField` defaults to
10
+ * `validateOnValueUpdate: true`, so changing the value already re-runs validation.
11
+ */
12
+ export function useSelectFieldWithinForm(
13
+ name: MaybeRefOrGetter<string | undefined>,
14
+ modelValue: Ref<SelectFieldValue | undefined>,
15
+ ) {
16
+ const { value, errors, field } = useFormFieldWithinForm(name, modelValue);
17
+
18
+ const onClose = (event: Event) => {
19
+ field?.handleBlur(event, true);
20
+ };
21
+
22
+ return {
23
+ value,
24
+ errors,
25
+ onClose,
26
+ };
27
+ }
@@ -0,0 +1,77 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { mount } from '@vue/test-utils';
3
+ import { h } from 'vue';
4
+ import { ComponentProps } from 'vue-component-type-helpers';
5
+ import SelectListItem from './SelectListItem.vue';
6
+ import { SELECT_LIST_ITEM_SIZES, SELECT_LIST_ITEM_STATES } from './SelectListItem.consts';
7
+ import Icon, { ICONS } from '../../Icons/Icon';
8
+
9
+ const setup = (props: ComponentProps<typeof SelectListItem>, slots = {}) =>
10
+ mount(SelectListItem, { props, slots });
11
+
12
+ describe('SelectListItem', () => {
13
+ it('should render the label', () => {
14
+ const wrapper = setup({ label: 'Basic listItem' });
15
+
16
+ expect(wrapper.find('.ds-selectListItem').exists()).toBe(true);
17
+ expect(wrapper.find('.ds-selectListItem__text').text()).toBe('Basic listItem');
18
+ expect(wrapper.find('.ds-selectListItem').attributes('title')).toBe('Basic listItem');
19
+ });
20
+
21
+ it('should render the eyebrow text when provided', () => {
22
+ const wrapper = setup({ label: 'Label', eyebrowText: 'Eyebrow' });
23
+
24
+ expect(wrapper.find('.ds-selectListItem__eyebrowText').text()).toBe('Eyebrow');
25
+ });
26
+
27
+ it.each(Object.values(SELECT_LIST_ITEM_SIZES))('should render in size: %s', (size) => {
28
+ const wrapper = setup({ label: 'Label', size });
29
+
30
+ expect(wrapper.find('.ds-selectListItem').classes()).toContain(`-ds-${size}`);
31
+ });
32
+
33
+ it.each([
34
+ { state: SELECT_LIST_ITEM_STATES.DISABLED, expectedClass: '-ds-disabled' },
35
+ { state: SELECT_LIST_ITEM_STATES.LOADING, expectedClass: '-ds-loading' },
36
+ ])('should apply $expectedClass for state $state', ({ state, expectedClass }) => {
37
+ const wrapper = setup({ label: 'Label', state });
38
+
39
+ expect(wrapper.find('.ds-selectListItem').classes()).toContain(expectedClass);
40
+ });
41
+
42
+ it('should render the check icon when selected', () => {
43
+ const wrapper = setup({ label: 'Label', isSelected: true });
44
+
45
+ expect(
46
+ wrapper.findComponent<typeof Icon>('.ds-selectListItem__iconRight').props().icon,
47
+ ).toEqual(ICONS.FA_CHECK_SOLID);
48
+ });
49
+
50
+ it('should not render the check icon when not selected', () => {
51
+ const wrapper = setup({ label: 'Label' });
52
+
53
+ expect(wrapper.find('.ds-selectListItem__iconRight').exists()).toBe(false);
54
+ });
55
+
56
+ describe('text slot', () => {
57
+ it('should fall back to the label when the slot is not provided', () => {
58
+ const wrapper = setup({ label: 'Fallback label' });
59
+
60
+ expect(wrapper.find('.ds-selectListItem__text').text()).toBe('Fallback label');
61
+ });
62
+
63
+ it('should override the rendered label when the slot is provided', () => {
64
+ const wrapper = setup(
65
+ { label: 'Prop label' },
66
+ { text: () => h('span', { id: 'custom-text' }, 'Slot label') },
67
+ );
68
+
69
+ const text = wrapper.find('.ds-selectListItem__text');
70
+
71
+ expect(text.find('#custom-text').exists()).toBe(true);
72
+ expect(text.text()).toBe('Slot label');
73
+ // `label` is still used for the title attribute and typeahead fallback.
74
+ expect(wrapper.find('.ds-selectListItem').attributes('title')).toBe('Prop label');
75
+ });
76
+ });
77
+ });
@@ -27,7 +27,9 @@
27
27
  :class="{ '-ds-uppercase': isEyebrowTextUppercase }"
28
28
  >{{ eyebrowText }}</span
29
29
  >
30
- <span class="ds-selectListItem__text">{{ label }}</span>
30
+ <span class="ds-selectListItem__text"
31
+ ><slot name="text">{{ label }}</slot></span
32
+ >
31
33
  </span>
32
34
 
33
35
  <slot name="metadata" />
@@ -197,6 +199,7 @@ const {
197
199
  const slots = defineSlots<{
198
200
  accessory?: () => any;
199
201
  metadata?: () => any;
202
+ text?: () => any;
200
203
  }>();
201
204
 
202
205
  const isLoading = computed(() => state === SELECT_LIST_ITEM_STATES.LOADING);
@@ -3,15 +3,13 @@
3
3
  class="ds-surveyQuestionOpenEnded"
4
4
  :class="{ '-ds-disabled': state === SURVEY_QUESTION_STATES.DISABLED }"
5
5
  >
6
- <ds-modal v-if="showModal" @close-modal="showModal = false">
6
+ <ds-modal
7
+ v-if="showModal"
8
+ :footer-secondary-button-text="t('ds.globals.confirmation')"
9
+ @close-modal="showModal = false"
10
+ @secondary-button-click="showModal = false"
11
+ >
7
12
  <slot name="explanation" />
8
- <template #footer>
9
- <div>
10
- <ds-button :type="BUTTON_TYPES.OUTLINED" @click="showModal = false">
11
- {{ t('ds.globals.confirmation') }}
12
- </ds-button>
13
- </div>
14
- </template>
15
13
  </ds-modal>
16
14
  <ds-card>
17
15
  <template #content>
@@ -82,8 +80,7 @@
82
80
 
83
81
  <script lang="ts">
84
82
  import DsCard from '../../Cards/Card';
85
- import DsModal from '../../Modal';
86
- import DsButton, { BUTTON_TYPES } from '../../Buttons/Button';
83
+ import DsModal from '../../Modals/Modal';
87
84
  import IconButton, { ICON_BUTTON_COLORS } from '../../Buttons/IconButton';
88
85
  import { ICON_SIZES, ICONS } from '../../Icons/Icon';
89
86
  import { SURVEY_QUESTION_STATES, SurveyQuestionState } from '../SurveyQuestion.consts';
@@ -97,7 +94,6 @@ export default defineComponent({
97
94
  name: 'SurveyQuestionOpenEnded',
98
95
  components: {
99
96
  SurveyQuestionTextarea,
100
- DsButton,
101
97
  DsCard,
102
98
  IconButton,
103
99
  DsModal,
@@ -138,7 +134,6 @@ export default defineComponent({
138
134
  ICONS: Object.freeze(ICONS),
139
135
  ICON_BUTTON_COLORS: Object.freeze(ICON_BUTTON_COLORS),
140
136
  ICON_SIZES: Object.freeze(ICON_SIZES),
141
- BUTTON_TYPES: Object.freeze(BUTTON_TYPES),
142
137
  SURVEY_QUESTION_STATES: Object.freeze(SURVEY_QUESTION_STATES),
143
138
  };
144
139
  },
@@ -1,14 +1,12 @@
1
1
  <template>
2
2
  <div class="ds-surveyQuestionScale">
3
- <ds-modal v-if="showModal" @close-modal="showModal = false">
3
+ <ds-modal
4
+ v-if="showModal"
5
+ :footer-secondary-button-text="t('ds.globals.confirmation')"
6
+ @close-modal="showModal = false"
7
+ @secondary-button-click="showModal = false"
8
+ >
4
9
  <slot name="explanation" />
5
- <template #footer>
6
- <div>
7
- <ds-button :type="BUTTON_TYPES.OUTLINED" @click="showModal = false">
8
- {{ t('ds.globals.confirmation') }}
9
- </ds-button>
10
- </div>
11
- </template>
12
10
  </ds-modal>
13
11
  <ds-card>
14
12
  <template #content>
@@ -234,8 +232,7 @@
234
232
  import DsCard from '../../Cards/Card';
235
233
  import IconButton, { ICON_BUTTON_COLORS } from '../../Buttons/IconButton';
236
234
  import { ICON_SIZES, ICONS } from '../../Icons/Icon';
237
- import DsButton, { BUTTON_TYPES } from '../../Buttons/Button';
238
- import DsModal from '../../Modal';
235
+ import DsModal from '../../Modals/Modal';
239
236
  import SurveyToggle, {
240
237
  SURVEY_TOGGLE_MEANINGS,
241
238
  SURVEY_TOGGLE_STATES,
@@ -261,7 +258,6 @@ export default defineComponent({
261
258
  IconButton,
262
259
  SurveyToggle,
263
260
  DsModal,
264
- DsButton,
265
261
  },
266
262
  props: {
267
263
  title: {
@@ -321,7 +317,6 @@ export default defineComponent({
321
317
  ICONS: Object.freeze(ICONS),
322
318
  ICON_SIZES: Object.freeze(ICON_SIZES),
323
319
  ICON_BUTTON_COLORS: Object.freeze(ICON_BUTTON_COLORS),
324
- BUTTON_TYPES: Object.freeze(BUTTON_TYPES),
325
320
  SURVEY_TOGGLE_COLORS: Object.freeze(SURVEY_TOGGLE_MEANINGS),
326
321
  SURVEY_TOGGLE_STATES: Object.freeze(SURVEY_TOGGLE_STATES),
327
322
  SURVEY_TOGGLE_STATUSES: Object.freeze(SURVEY_TOGGLE_STATUSES),
@@ -1,9 +1,9 @@
1
1
  import { inject, MaybeRefOrGetter, ref, Ref, toValue } from 'vue';
2
2
  import { FormContextKey, useField } from 'vee-validate';
3
3
 
4
- export function useFormFieldWithinForm<T = string>(
4
+ export function useFormFieldWithinForm<T>(
5
5
  name: MaybeRefOrGetter<string | undefined>,
6
- modelValue: Ref<T | undefined>,
6
+ modelValue: Ref<T>,
7
7
  ) {
8
8
  const nameValue = toValue(name);
9
9
  const form = inject(FormContextKey, null);
@@ -14,9 +14,9 @@ export function useFormFieldWithinForm<T = string>(
14
14
  );
15
15
  }
16
16
 
17
- const field = nameValue ? useField<Array<string>>(nameValue, undefined) : null;
17
+ const field = nameValue ? useField<T>(nameValue, undefined) : null;
18
18
 
19
- const value = field ? field.value : modelValue;
19
+ const value: Ref<T> = field ? field.value : modelValue;
20
20
 
21
21
  return {
22
22
  field,
@@ -248,6 +248,8 @@ import { faCircleDot as fasCircleDot } from '@fortawesome/pro-solid-svg-icons/fa
248
248
  import { faDot as fasDot } from '@fortawesome/pro-solid-svg-icons/faDot';
249
249
  import { faFlagCheckered as fasFlagCheckered } from '@fortawesome/pro-solid-svg-icons/faFlagCheckered';
250
250
  import { faSquare as fasSquare } from '@fortawesome/pro-solid-svg-icons/faSquare';
251
+ import { faBellSlash as fasBellSlash } from '@fortawesome/pro-solid-svg-icons/faBellSlash';
252
+ import { faBellOn as fasBellOn } from '@fortawesome/pro-solid-svg-icons/faBellOn';
251
253
 
252
254
  import { faChevronLeft as falChevronLeft } from '@fortawesome/pro-light-svg-icons/faChevronLeft';
253
255
  import { faChevronRight as falChevronRight } from '@fortawesome/pro-light-svg-icons/faChevronRight';
@@ -305,6 +307,8 @@ export const FONTAWESOME_ICONS = {
305
307
  FA_BADGE_PERCENT: faBadgePercent,
306
308
  FA_BAN: faBan,
307
309
  FA_BARS: faBars,
310
+ FA_BELL_SLASH_SOLID: fasBellSlash,
311
+ FA_BELL_ON_SOLID: fasBellOn,
308
312
  FA_BELL_SLASH: faBellSlash,
309
313
  FA_BELL: faBell,
310
314
  FA_BOLT: faBolt,
package/lib/js/index.ts CHANGED
@@ -46,7 +46,6 @@ export { default as IconButton } from './components/Buttons/IconButton';
46
46
  export { default as DsIconButton } from './components/Buttons/IconButton';
47
47
  export * from './components/Buttons/IconButton/IconButton.consts';
48
48
  export { default as DsInputField } from './components/Form/InputField';
49
- export { default as Modal } from './components/Modal';
50
49
  export { default as DsModal } from './components/Modals/Modal';
51
50
  export * from './components/Modals/Modal/Modal.consts';
52
51
  export { default as DsModalDialog } from './components/Modals/ModalDialog';
@@ -54,6 +53,8 @@ export { default as NumberInCircle } from './components/NumberInCircle';
54
53
  export { default as DsNumberInCircle } from './components/NumberInCircle';
55
54
  export * from './components/NumberInCircle/NumberInCircle.consts';
56
55
  export { default as DsPasswordField } from './components/Form/PasswordField';
56
+ export { default as DsSelectField } from './components/Form/SelectField';
57
+ export * from './components/Form/SelectField';
57
58
  export { default as TabItem } from './components/TabItem';
58
59
  export { default as DsTabItem } from './components/TabItem';
59
60
  export * from './components/TabItem/TabItem.consts';