@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.
- package/dist/design-system.css +1 -1
- package/dist/design-system.js +16800 -13061
- package/dist/design-system.js.map +1 -1
- package/dist/lib/js/components/Buttons/IconButton/IconButton.vue.d.ts +2 -0
- package/dist/lib/js/components/Cards/Card/Card.consts.d.ts +14 -0
- package/dist/lib/js/components/Cards/Card/Card.vue.d.ts +10 -5
- package/dist/lib/js/components/Cards/CardExpandable/CardExpandable.vue.d.ts +35 -15
- package/dist/lib/js/components/DatePickers/DateBox/DateBox.vue.d.ts +2 -0
- package/dist/lib/js/components/DatePickers/DatePicker/DatePicker.vue.d.ts +2 -0
- package/dist/lib/js/components/DatePickers/DateRangePicker/DateRangePicker.vue.d.ts +2 -0
- package/dist/lib/js/components/Drawer/DrawerSection/DrawerSection.vue.d.ts +6 -0
- package/dist/lib/js/components/Form/InputField/useInputFieldWithinForm.d.ts +1 -1
- package/dist/lib/js/components/Form/RadioButton/RadioButton.vue.d.ts +2 -0
- package/dist/lib/js/components/Form/SelectField/SelectField.types.d.ts +26 -0
- package/dist/lib/js/components/Form/SelectField/SelectField.utils.d.ts +7 -0
- package/dist/lib/js/components/Form/SelectField/SelectField.vue.d.ts +107 -0
- package/dist/lib/js/components/Form/SelectField/SelectFieldOption.vue.d.ts +19 -0
- package/dist/lib/js/components/Form/SelectField/index.d.ts +4 -0
- package/dist/lib/js/components/Form/SelectField/useSelectFieldWithinForm.d.ts +8 -0
- package/dist/lib/js/components/Headers/OverlayHeader/OverlayHeader.vue.d.ts +4 -0
- package/dist/lib/js/components/Headers/SectionHeader/SectionHeader.vue.d.ts +4 -0
- package/dist/lib/js/components/Icons/Icon/Icon.consts.d.ts +2 -0
- package/dist/lib/js/components/Modals/Modal/Modal.vue.d.ts +4 -0
- package/dist/lib/js/components/Modals/ModalDialog/ModalDialog.vue.d.ts +4 -0
- package/dist/lib/js/components/Pagination/Pagination.vue.d.ts +6 -0
- package/dist/lib/js/components/ProgressBar/ProgressBar.vue.d.ts +2 -0
- package/dist/lib/js/components/ProgressDonutChart/ProgressDonutChart.vue.d.ts +2 -0
- package/dist/lib/js/components/RichList/BasicRichListItem/BasicRichListItem.vue.d.ts +2 -0
- package/dist/lib/js/components/RichList/RichListItem/RichListItem.vue.d.ts +2 -0
- package/dist/lib/js/components/SelectList/SelectListItem/SelectListItem.vue.d.ts +2 -0
- package/dist/lib/js/components/SelectList/SelectListItemToggle/SelectListItemToggle.vue.d.ts +2 -0
- package/dist/lib/js/components/Statuses/AccessStatus/AccessStatus.vue.d.ts +2 -0
- package/dist/lib/js/components/Statuses/BlockadeStatus/BlockadeStatus.vue.d.ts +2 -0
- package/dist/lib/js/components/SurveyQuestions/SurveyQuestionOpenEnded/SurveyQuestionOpenEnded.vue.d.ts +1009 -169
- package/dist/lib/js/components/SurveyQuestions/SurveyQuestionScale/SurveyQuestionScale.vue.d.ts +1000 -160
- package/dist/lib/js/components/Switch/Switch.vue.d.ts +2 -0
- package/dist/lib/js/components/Tile/Tile.sb.shared.d.ts +2 -0
- package/dist/lib/js/components/Toast/Toast.vue.d.ts +33 -15
- package/dist/lib/js/components/Toggles/ToggleButton/ToggleButton.vue.d.ts +2 -0
- package/dist/lib/js/composables/useFormFieldWithinForm.d.ts +3 -3
- package/dist/lib/js/icons/fontawesome.d.ts +2 -0
- package/dist/lib/js/index.d.ts +2 -1
- package/lib/js/components/Cards/Card/Card.consts.ts +18 -0
- package/lib/js/components/Cards/Card/Card.spec.ts +159 -33
- package/lib/js/components/Cards/Card/Card.stories.ts +21 -2
- package/lib/js/components/Cards/Card/Card.vue +90 -23
- package/lib/js/components/Dropdown/Dropdown.vue +6 -10
- package/lib/js/components/Form/Form.stories.ts +15 -1
- package/lib/js/components/Form/SelectField/SelectField.spec.ts +495 -0
- package/lib/js/components/Form/SelectField/SelectField.stories.ts +287 -0
- package/lib/js/components/Form/SelectField/SelectField.types.ts +65 -0
- package/lib/js/components/Form/SelectField/SelectField.utils.ts +83 -0
- package/lib/js/components/Form/SelectField/SelectField.vue +342 -0
- package/lib/js/components/Form/SelectField/SelectFieldOption.vue +51 -0
- package/lib/js/components/Form/SelectField/index.ts +5 -0
- package/lib/js/components/Form/SelectField/useSelectFieldWithinForm.ts +27 -0
- package/lib/js/components/SelectList/SelectListItem/SelectListItem.spec.ts +77 -0
- package/lib/js/components/SelectList/SelectListItem/SelectListItem.vue +4 -1
- package/lib/js/components/SurveyQuestions/SurveyQuestionOpenEnded/SurveyQuestionOpenEnded.vue +7 -12
- package/lib/js/components/SurveyQuestions/SurveyQuestionScale/SurveyQuestionScale.vue +7 -12
- package/lib/js/composables/useFormFieldWithinForm.ts +4 -4
- package/lib/js/icons/fontawesome.ts +4 -0
- package/lib/js/index.ts +2 -1
- package/lib/js/styles/Shadows/Shadows.stories.scss +16 -0
- package/lib/js/styles/Shadows/Shadows.stories.ts +4 -0
- package/lib/styles/mixins/_dropdown-surface.scss +24 -0
- package/lib/styles/settings/_shadows.scss +18 -0
- package/package.json +1 -1
- package/dist/lib/js/components/Modal/Modal.vue.d.ts +0 -395
- package/dist/lib/js/components/Modal/index.d.ts +0 -3
- package/lib/js/components/Modal/Modal.spec.ts +0 -58
- package/lib/js/components/Modal/Modal.vue +0 -137
- 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,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"
|
|
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);
|
package/lib/js/components/SurveyQuestions/SurveyQuestionOpenEnded/SurveyQuestionOpenEnded.vue
CHANGED
|
@@ -3,15 +3,13 @@
|
|
|
3
3
|
class="ds-surveyQuestionOpenEnded"
|
|
4
4
|
:class="{ '-ds-disabled': state === SURVEY_QUESTION_STATES.DISABLED }"
|
|
5
5
|
>
|
|
6
|
-
<ds-modal
|
|
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
|
|
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
|
|
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
|
|
4
|
+
export function useFormFieldWithinForm<T>(
|
|
5
5
|
name: MaybeRefOrGetter<string | undefined>,
|
|
6
|
-
modelValue: Ref<T
|
|
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<
|
|
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';
|