@bethinkpl/design-system 42.0.0 → 44.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 +17470 -13135
- package/dist/design-system.js.map +1 -1
- 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 +33 -15
- package/dist/lib/js/components/Form/PinInputField/PinInputField.consts.d.ts +14 -0
- package/dist/lib/js/components/Form/PinInputField/PinInputField.types.d.ts +12 -0
- package/dist/lib/js/components/Form/PinInputField/PinInputField.utils.d.ts +2 -0
- package/dist/lib/js/components/Form/PinInputField/PinInputField.vue.d.ts +91 -0
- package/dist/lib/js/components/Form/PinInputField/index.d.ts +5 -0
- package/dist/lib/js/components/Form/PinInputField/usePinInputFieldWithinForm.d.ts +8 -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/Form/TextAreaField/TextAreaField.types.d.ts +9 -0
- package/dist/lib/js/components/Form/TextAreaField/TextAreaField.vue.d.ts +76 -0
- package/dist/lib/js/components/Form/TextAreaField/TextAreaFieldAutosized.vue.d.ts +6 -0
- package/dist/lib/js/components/Form/TextAreaField/index.d.ts +4 -0
- package/dist/lib/js/components/LabelValue/LabelValueItem/LabelValueItem.vue.d.ts +12 -1
- package/dist/lib/js/components/Pagination/Pagination.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/SurveyQuestions/SurveyQuestionOpenEnded/SurveyQuestionOpenEnded.vue.d.ts +33 -15
- package/dist/lib/js/components/SurveyQuestions/SurveyQuestionScale/SurveyQuestionScale.vue.d.ts +33 -15
- package/dist/lib/js/components/Toast/Toast.consts.d.ts +11 -9
- package/dist/lib/js/components/Toast/Toast.vue.d.ts +51 -415
- package/dist/lib/js/composables/useFormFieldWithinForm.d.ts +3 -3
- package/dist/lib/js/composables/useTextFieldWithinForm.d.ts +8 -0
- package/dist/lib/js/index.d.ts +6 -0
- 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/CheckboxGroupField/CheckboxGroupField.spec.ts +15 -0
- package/lib/js/components/Form/CheckboxGroupField/CheckboxGroupField.vue +5 -2
- package/lib/js/components/Form/Form.stories.ts +37 -2
- package/lib/js/components/Form/InputField/InputField.spec.ts +11 -1
- package/lib/js/components/Form/InputField/InputField.vue +7 -4
- package/lib/js/components/Form/PinInputField/PinInputField.consts.ts +18 -0
- package/lib/js/components/Form/PinInputField/PinInputField.spec.ts +394 -0
- package/lib/js/components/Form/PinInputField/PinInputField.stories.ts +124 -0
- package/lib/js/components/Form/PinInputField/PinInputField.types.ts +42 -0
- package/lib/js/components/Form/PinInputField/PinInputField.utils.ts +5 -0
- package/lib/js/components/Form/PinInputField/PinInputField.vue +211 -0
- package/lib/js/components/Form/PinInputField/index.ts +6 -0
- package/lib/js/components/Form/PinInputField/usePinInputFieldWithinForm.ts +29 -0
- package/lib/js/components/Form/SelectField/SelectField.spec.ts +495 -0
- package/lib/js/components/Form/SelectField/SelectField.stories.ts +282 -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/Form/TextAreaField/TextAreaField.spec.ts +341 -0
- package/lib/js/components/Form/TextAreaField/TextAreaField.stories.ts +99 -0
- package/lib/js/components/Form/TextAreaField/TextAreaField.types.ts +23 -0
- package/lib/js/components/Form/TextAreaField/TextAreaField.vue +190 -0
- package/lib/js/components/Form/TextAreaField/TextAreaFieldAutosized.vue +19 -0
- package/lib/js/components/Form/TextAreaField/index.ts +5 -0
- package/lib/js/components/LabelValue/LabelValueItem/LabelValueItem.spec.ts +26 -0
- package/lib/js/components/LabelValue/LabelValueItem/LabelValueItem.stories.ts +16 -3
- package/lib/js/components/LabelValue/LabelValueItem/LabelValueItem.vue +5 -0
- package/lib/js/components/LoadingBar/LoadingBar.spec.ts +84 -0
- package/lib/js/components/LoadingBar/LoadingBar.vue +30 -19
- 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/Toast/Toast.consts.ts +4 -2
- package/lib/js/components/Toast/Toast.spec.ts +177 -0
- package/lib/js/components/Toast/Toast.stories.ts +65 -31
- package/lib/js/components/Toast/Toast.vue +176 -195
- package/lib/js/composables/useFormFieldWithinForm.ts +4 -4
- package/lib/js/{components/Form/InputField/useInputFieldWithinForm.ts → composables/useTextFieldWithinForm.ts} +7 -2
- package/lib/js/index.ts +6 -0
- 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/Form/InputField/useInputFieldWithinForm.d.ts +0 -8
|
@@ -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 `useTextFieldWithinForm`. 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
|
+
}
|