vueless 1.2.15-beta.3 → 1.2.15-beta.5
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/locales/en.json +1 -1
- package/package.json +1 -1
- package/ui.container-accordion-item/UAccordionItem.vue +1 -5
- package/ui.container-accordion-item/tests/UAccordionItem.test.ts +5 -5
- package/ui.data-list/config.ts +1 -1
- package/ui.data-list/storybook/stories.ts +24 -24
- package/ui.data-list/tests/UDataList.test.ts +7 -7
- package/ui.data-table/config.ts +2 -0
- package/ui.data-table/storybook/stories.ts +3 -1
- package/ui.dropdown-badge/config.ts +1 -1
- package/ui.dropdown-badge/storybook/stories.ts +3 -3
- package/ui.dropdown-badge/tests/UDropdownBadge.test.ts +5 -5
- package/ui.dropdown-button/config.ts +1 -1
- package/ui.dropdown-button/storybook/stories.ts +7 -7
- package/ui.dropdown-button/tests/UDropdownButton.test.ts +5 -5
- package/ui.dropdown-link/config.ts +1 -1
- package/ui.dropdown-link/storybook/stories.ts +3 -3
- package/ui.dropdown-link/tests/UDropdownLink.test.ts +5 -5
- package/ui.form-calendar/UCalendar.vue +4 -0
- package/ui.form-calendar/config.ts +5 -0
- package/ui.form-checkbox/UCheckbox.vue +23 -4
- package/ui.form-checkbox/config.ts +4 -0
- package/ui.form-checkbox/tests/UCheckbox.test.ts +1 -1
- package/ui.form-checkbox-group/UCheckboxGroup.vue +2 -2
- package/ui.form-checkbox-group/config.ts +2 -0
- package/ui.form-checkbox-group/storybook/stories.ts +46 -9
- package/ui.form-checkbox-group/tests/UCheckboxGroup.test.ts +65 -0
- package/ui.form-checkbox-group/types.ts +10 -0
- package/ui.form-date-picker/config.ts +5 -0
- package/ui.form-input/UInput.vue +9 -3
- package/ui.form-input-counter/UInputCounter.vue +9 -0
- package/ui.form-input-counter/config.ts +5 -0
- package/ui.form-input-password/UInputPassword.vue +11 -12
- package/ui.form-input-password/tests/UInputPassword.test.ts +2 -2
- package/ui.form-label/ULabel.vue +5 -1
- package/ui.form-label/types.ts +5 -0
- package/ui.form-listbox/UListbox.vue +37 -11
- package/ui.form-listbox/config.ts +1 -1
- package/ui.form-listbox/storybook/stories.ts +25 -25
- package/ui.form-listbox/tests/UListbox.test.ts +6 -6
- package/ui.form-radio-group/URadioGroup.vue +2 -2
- package/ui.form-radio-group/config.ts +2 -0
- package/ui.form-radio-group/storybook/stories.ts +48 -8
- package/ui.form-radio-group/tests/URadioGroup.test.ts +68 -0
- package/ui.form-radio-group/types.ts +18 -4
- package/ui.form-select/USelect.vue +37 -2
- package/ui.form-select/config.ts +1 -1
- package/ui.form-select/tests/USelect.test.ts +6 -4
- package/ui.form-switch/USwitch.vue +20 -7
- package/ui.form-switch/config.ts +2 -0
- package/ui.form-switch/tests/USwitch.test.ts +6 -4
- package/ui.form-textarea/UTextarea.vue +15 -5
- package/ui.form-textarea/tests/UTextarea.test.ts +1 -1
- package/ui.navigation-pagination/UPagination.vue +14 -0
- package/ui.navigation-pagination/config.ts +9 -0
- package/ui.text-alert/UAlert.vue +8 -0
- package/ui.text-alert/config.ts +4 -0
- package/icons/storybook/notifications.svg +0 -1
|
@@ -33,9 +33,9 @@ export default {
|
|
|
33
33
|
args: {
|
|
34
34
|
label: "Select your preferred communication methods:",
|
|
35
35
|
options: [
|
|
36
|
-
{ label: "Email Notifications",
|
|
37
|
-
{ label: "SMS Alerts",
|
|
38
|
-
{ label: "Push Notifications",
|
|
36
|
+
{ label: "Email Notifications", id: "email" },
|
|
37
|
+
{ label: "SMS Alerts", id: "sms" },
|
|
38
|
+
{ label: "Push Notifications", id: "push" },
|
|
39
39
|
],
|
|
40
40
|
},
|
|
41
41
|
argTypes: {
|
|
@@ -108,7 +108,7 @@ export const Options: StoryFn<UCheckboxGroupArgs> = (args: UCheckboxGroupArgs) =
|
|
|
108
108
|
<UAlert
|
|
109
109
|
:description="modelValue"
|
|
110
110
|
size="sm"
|
|
111
|
-
variant="
|
|
111
|
+
variant="soft"
|
|
112
112
|
color="success"
|
|
113
113
|
bordered
|
|
114
114
|
/>
|
|
@@ -144,11 +144,48 @@ export const LabelSlot = DefaultTemplate.bind({});
|
|
|
144
144
|
LabelSlot.args = {
|
|
145
145
|
name: "LabelSlot",
|
|
146
146
|
slotTemplate: `
|
|
147
|
-
<template #label>
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
<UIcon name="notifications" size="xs" />
|
|
151
|
-
</URow>
|
|
147
|
+
<template #label="{ label }">
|
|
148
|
+
{{ label }}
|
|
149
|
+
<span class="text-red-500">*</span>
|
|
152
150
|
</template>
|
|
153
151
|
`,
|
|
154
152
|
};
|
|
153
|
+
|
|
154
|
+
export const CustomKeys: StoryFn<UCheckboxGroupArgs> = (args: UCheckboxGroupArgs) => ({
|
|
155
|
+
components: { UCheckboxGroup, UCol, UAlert },
|
|
156
|
+
setup: () => ({ args, modelValue: ref([]) }),
|
|
157
|
+
template: `
|
|
158
|
+
<UCol>
|
|
159
|
+
<UCheckboxGroup v-bind="args" v-model="modelValue" />
|
|
160
|
+
|
|
161
|
+
<UAlert
|
|
162
|
+
:description="modelValue"
|
|
163
|
+
size="sm"
|
|
164
|
+
variant="soft"
|
|
165
|
+
color="success"
|
|
166
|
+
bordered
|
|
167
|
+
/>
|
|
168
|
+
</UCol>
|
|
169
|
+
`,
|
|
170
|
+
});
|
|
171
|
+
CustomKeys.args = {
|
|
172
|
+
name: "CustomKeys",
|
|
173
|
+
label: "Select your preferred features:",
|
|
174
|
+
labelKey: "name",
|
|
175
|
+
valueKey: "id",
|
|
176
|
+
options: [
|
|
177
|
+
{ id: "dark-mode", name: "Dark Mode" },
|
|
178
|
+
{ id: "auto-save", name: "Auto-save" },
|
|
179
|
+
{ id: "notifications", name: "Notifications" },
|
|
180
|
+
{ id: "two-factor", name: "Two-factor Authentication" },
|
|
181
|
+
],
|
|
182
|
+
};
|
|
183
|
+
CustomKeys.parameters = {
|
|
184
|
+
docs: {
|
|
185
|
+
description: {
|
|
186
|
+
story:
|
|
187
|
+
"Use `labelKey` and `valueKey` props to specify custom keys for label and value in option objects. " +
|
|
188
|
+
"This is useful when working with data from APIs that use different property names.",
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
};
|
|
@@ -185,6 +185,71 @@ describe("UCheckboxGroup.vue", () => {
|
|
|
185
185
|
expect(checkbox.attributes("data-test")).toBe(`${dataTestValue}-item-${idx}-label`);
|
|
186
186
|
});
|
|
187
187
|
});
|
|
188
|
+
|
|
189
|
+
it("LabelKey – uses custom label key for option labels", () => {
|
|
190
|
+
const customOptions = [
|
|
191
|
+
{ value: "option-1", name: "Option 1" },
|
|
192
|
+
{ value: "option-2", name: "Option 2" },
|
|
193
|
+
{ value: "option-3", name: "Option 3" },
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
const component = mount(UCheckboxGroup, {
|
|
197
|
+
props: {
|
|
198
|
+
options: customOptions,
|
|
199
|
+
labelKey: "name",
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const checkboxes = component.findAllComponents(UCheckbox);
|
|
204
|
+
|
|
205
|
+
checkboxes.forEach((checkbox, index) => {
|
|
206
|
+
expect(checkbox.props("label")).toBe(customOptions[index].name);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("ValueKey – uses custom value key for option values", () => {
|
|
211
|
+
const customOptions = [
|
|
212
|
+
{ id: "option-1", label: "Option 1" },
|
|
213
|
+
{ id: "option-2", label: "Option 2" },
|
|
214
|
+
{ id: "option-3", label: "Option 3" },
|
|
215
|
+
];
|
|
216
|
+
|
|
217
|
+
const component = mount(UCheckboxGroup, {
|
|
218
|
+
props: {
|
|
219
|
+
options: customOptions,
|
|
220
|
+
valueKey: "id",
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const checkboxes = component.findAllComponents(UCheckbox);
|
|
225
|
+
|
|
226
|
+
checkboxes.forEach((checkbox, index) => {
|
|
227
|
+
expect(checkbox.props("value")).toBe(customOptions[index].id);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("LabelKey and ValueKey – works with complex objects", async () => {
|
|
232
|
+
const customOptions = [
|
|
233
|
+
{ planId: "basic", title: "Basic Plan" },
|
|
234
|
+
{ planId: "pro", title: "Pro Plan" },
|
|
235
|
+
];
|
|
236
|
+
|
|
237
|
+
const component = mount(UCheckboxGroup, {
|
|
238
|
+
props: {
|
|
239
|
+
options: customOptions,
|
|
240
|
+
labelKey: "title",
|
|
241
|
+
valueKey: "planId",
|
|
242
|
+
modelValue: [],
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const checkboxes = component.findAllComponents(UCheckbox);
|
|
247
|
+
|
|
248
|
+
expect(checkboxes[0].props("label")).toBe("Basic Plan");
|
|
249
|
+
expect(checkboxes[0].props("value")).toBe("basic");
|
|
250
|
+
expect(checkboxes[1].props("label")).toBe("Pro Plan");
|
|
251
|
+
expect(checkboxes[1].props("value")).toBe("pro");
|
|
252
|
+
});
|
|
188
253
|
});
|
|
189
254
|
|
|
190
255
|
describe("Slots", () => {
|
|
@@ -16,6 +16,16 @@ export interface Props {
|
|
|
16
16
|
*/
|
|
17
17
|
options?: UCheckboxOption[];
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Label key in the item object of options.
|
|
21
|
+
*/
|
|
22
|
+
labelKey?: string;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Value key in the item object of options.
|
|
26
|
+
*/
|
|
27
|
+
valueKey?: string;
|
|
28
|
+
|
|
19
29
|
/**
|
|
20
30
|
* Checkbox group label.
|
|
21
31
|
*/
|
|
@@ -125,6 +125,11 @@ export default /*tw*/ {
|
|
|
125
125
|
},
|
|
126
126
|
timeLabel: "Time",
|
|
127
127
|
okLabel: "Ok",
|
|
128
|
+
/* These are used for a11y. */
|
|
129
|
+
previousYear: "Previous Year",
|
|
130
|
+
nextYear: "Next Year",
|
|
131
|
+
previousMonth: "Previous Month",
|
|
132
|
+
nextMonth: "Next Month",
|
|
128
133
|
},
|
|
129
134
|
defaults: {
|
|
130
135
|
size: "md",
|
package/ui.form-input/UInput.vue
CHANGED
|
@@ -91,7 +91,7 @@ const VALIDATION_RULES_REG_EX = {
|
|
|
91
91
|
|
|
92
92
|
const slots = useSlots();
|
|
93
93
|
|
|
94
|
-
const wrapperRef = useTemplateRef<
|
|
94
|
+
const wrapperRef = useTemplateRef<HTMLDivElement>("wrapper");
|
|
95
95
|
const inputRef = useTemplateRef<HTMLInputElement>("input");
|
|
96
96
|
const leftSlotWrapperRef = useTemplateRef<HTMLSpanElement>("leftSlotWrapper");
|
|
97
97
|
const labelComponentRef = useTemplateRef<InstanceType<typeof ULabel>>("labelComponent");
|
|
@@ -176,6 +176,10 @@ function onKeydown(event: KeyboardEvent) {
|
|
|
176
176
|
emit("keydown", event);
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
function onSlotClick() {
|
|
180
|
+
inputRef.value?.focus();
|
|
181
|
+
}
|
|
182
|
+
|
|
179
183
|
/**
|
|
180
184
|
* This trick prevents default browser autocomplete behavior.
|
|
181
185
|
* @param toggleState { boolean }
|
|
@@ -286,11 +290,12 @@ const {
|
|
|
286
290
|
<slot name="label" :label="label" />
|
|
287
291
|
</template>
|
|
288
292
|
|
|
289
|
-
<
|
|
293
|
+
<div ref="wrapper" v-bind="wrapperAttrs">
|
|
290
294
|
<span
|
|
291
295
|
v-if="hasSlotContent($slots['left'], { iconName: leftIcon }) || leftIcon"
|
|
292
296
|
v-bind="leftSlotAttrs"
|
|
293
297
|
ref="leftSlotWrapper"
|
|
298
|
+
@click="onSlotClick"
|
|
294
299
|
>
|
|
295
300
|
<!--
|
|
296
301
|
@slot Use it to add something before the text.
|
|
@@ -328,6 +333,7 @@ const {
|
|
|
328
333
|
<span
|
|
329
334
|
v-if="hasSlotContent($slots['right'], { iconName: rightIcon }) || rightIcon"
|
|
330
335
|
v-bind="rightSlotAttrs"
|
|
336
|
+
@click="onSlotClick"
|
|
331
337
|
>
|
|
332
338
|
<!--
|
|
333
339
|
@slot Use it to add something after the text.
|
|
@@ -337,6 +343,6 @@ const {
|
|
|
337
343
|
<UIcon v-if="rightIcon" color="neutral" :name="rightIcon" v-bind="rightIconAttrs" />
|
|
338
344
|
</slot>
|
|
339
345
|
</span>
|
|
340
|
-
</
|
|
346
|
+
</div>
|
|
341
347
|
</ULabel>
|
|
342
348
|
</template>
|
|
@@ -3,6 +3,7 @@ import { ref, computed, useTemplateRef, watch } from "vue";
|
|
|
3
3
|
|
|
4
4
|
import { useUI } from "../composables/useUI";
|
|
5
5
|
import { getDefaults } from "../utils/ui";
|
|
6
|
+
import { useComponentLocaleMessages } from "../composables/useComponentLocaleMassages";
|
|
6
7
|
|
|
7
8
|
import UIcon from "../ui.image-icon/UIcon.vue";
|
|
8
9
|
import UButton from "../ui.button/UButton.vue";
|
|
@@ -41,6 +42,12 @@ const emit = defineEmits([
|
|
|
41
42
|
"blur",
|
|
42
43
|
]);
|
|
43
44
|
|
|
45
|
+
const { localeMessages } = useComponentLocaleMessages<typeof defaultConfig.i18n>(
|
|
46
|
+
COMPONENT_NAME,
|
|
47
|
+
defaultConfig.i18n,
|
|
48
|
+
props?.config?.i18n,
|
|
49
|
+
);
|
|
50
|
+
|
|
44
51
|
const inputComponentRef = useTemplateRef<InstanceType<typeof UInput>>("inputComponent");
|
|
45
52
|
|
|
46
53
|
const addIntervalId = ref<number | null>(null);
|
|
@@ -181,6 +188,7 @@ const {
|
|
|
181
188
|
:size="size"
|
|
182
189
|
variant="outlined"
|
|
183
190
|
:disabled="isSubtractButtonDisabled || disabled"
|
|
191
|
+
:aria-label="localeMessages.subtract"
|
|
184
192
|
v-bind="subtractButtonAttrs"
|
|
185
193
|
:data-test="getDataTest('subtract')"
|
|
186
194
|
@click.prevent
|
|
@@ -221,6 +229,7 @@ const {
|
|
|
221
229
|
square
|
|
222
230
|
variant="outlined"
|
|
223
231
|
:disabled="isAddButtonDisabled || disabled"
|
|
232
|
+
:aria-label="localeMessages.add"
|
|
224
233
|
v-bind="addButtonAttrs"
|
|
225
234
|
:data-test="getDataTest('add')"
|
|
226
235
|
@click.prevent
|
|
@@ -31,6 +31,11 @@ export default /*tw*/ {
|
|
|
31
31
|
subtractButton: "{>actionButton}",
|
|
32
32
|
addIcon: "{UIcon}",
|
|
33
33
|
subtractIcon: "{UIcon}",
|
|
34
|
+
/* These are used for a11y. */
|
|
35
|
+
i18n: {
|
|
36
|
+
add: "Add",
|
|
37
|
+
subtract: "Subtract",
|
|
38
|
+
},
|
|
34
39
|
defaults: {
|
|
35
40
|
size: "md",
|
|
36
41
|
decimalSeparator: ",",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { ref, computed, useId } from "vue";
|
|
2
|
+
import { ref, computed, useId, useTemplateRef, nextTick } from "vue";
|
|
3
3
|
|
|
4
4
|
import { useUI } from "../composables/useUI";
|
|
5
5
|
import { getDefaults } from "../utils/ui";
|
|
@@ -42,6 +42,7 @@ const emit = defineEmits([
|
|
|
42
42
|
|
|
43
43
|
const elementId = props.id || useId();
|
|
44
44
|
|
|
45
|
+
const inputRef = useTemplateRef<InstanceType<typeof UInput>>("input");
|
|
45
46
|
const isShownPassword = ref(false);
|
|
46
47
|
|
|
47
48
|
const localValue = computed({
|
|
@@ -59,8 +60,12 @@ const passwordIcon = computed(() => {
|
|
|
59
60
|
: config.value.defaults.passwordHiddenIcon || "";
|
|
60
61
|
});
|
|
61
62
|
|
|
62
|
-
function onClickShowPassword() {
|
|
63
|
+
async function onClickShowPassword() {
|
|
63
64
|
isShownPassword.value = !isShownPassword.value;
|
|
65
|
+
|
|
66
|
+
await nextTick();
|
|
67
|
+
|
|
68
|
+
inputRef.value?.inputRef?.focus();
|
|
64
69
|
}
|
|
65
70
|
|
|
66
71
|
function onFocus(event: FocusEvent) {
|
|
@@ -86,6 +91,7 @@ const { getDataTest, config, passwordInputAttrs, passwordIconAttrs, passwordIcon
|
|
|
86
91
|
<template>
|
|
87
92
|
<UInput
|
|
88
93
|
:id="elementId"
|
|
94
|
+
ref="input"
|
|
89
95
|
v-model="localValue"
|
|
90
96
|
:type="inputType"
|
|
91
97
|
:label="label"
|
|
@@ -120,29 +126,22 @@ const { getDataTest, config, passwordInputAttrs, passwordIconAttrs, passwordIcon
|
|
|
120
126
|
</template>
|
|
121
127
|
|
|
122
128
|
<template #right>
|
|
123
|
-
<
|
|
129
|
+
<div v-bind="passwordIconWrapperAttrs" @click="onClickShowPassword">
|
|
124
130
|
<!--
|
|
125
131
|
@slot Use it to add something instead of the password icon.
|
|
126
132
|
@binding {string} icon-name
|
|
127
133
|
@binding {boolean} visible
|
|
128
|
-
@binding {function} toggle
|
|
129
134
|
-->
|
|
130
|
-
<slot
|
|
131
|
-
name="right"
|
|
132
|
-
:icon-name="passwordIcon"
|
|
133
|
-
:visible="isShownPassword"
|
|
134
|
-
:toggle="onClickShowPassword"
|
|
135
|
-
>
|
|
135
|
+
<slot name="right" :icon-name="passwordIcon" :visible="isShownPassword">
|
|
136
136
|
<UIcon
|
|
137
137
|
:name="passwordIcon"
|
|
138
138
|
color="neutral"
|
|
139
139
|
interactive
|
|
140
140
|
v-bind="passwordIconAttrs"
|
|
141
141
|
:data-test="getDataTest('password-icon')"
|
|
142
|
-
@click="onClickShowPassword"
|
|
143
142
|
/>
|
|
144
143
|
</slot>
|
|
145
|
-
</
|
|
144
|
+
</div>
|
|
146
145
|
</template>
|
|
147
146
|
</UInput>
|
|
148
147
|
</template>
|
|
@@ -190,14 +190,14 @@ describe("UInputPassword.vue", () => {
|
|
|
190
190
|
expect(input.attributes("type")).toBe("password");
|
|
191
191
|
expect(passwordIcon.props("name")).toBe("visibility_off-fill");
|
|
192
192
|
|
|
193
|
-
await flushPromises();
|
|
194
193
|
await passwordIcon.trigger("click");
|
|
194
|
+
await flushPromises();
|
|
195
195
|
|
|
196
196
|
expect(input.attributes("type")).toBe("text");
|
|
197
197
|
expect(passwordIcon.props("name")).toBe("visibility-fill");
|
|
198
198
|
|
|
199
|
-
await flushPromises();
|
|
200
199
|
await passwordIcon.trigger("click");
|
|
200
|
+
await flushPromises();
|
|
201
201
|
|
|
202
202
|
expect(input.attributes("type")).toBe("password");
|
|
203
203
|
expect(passwordIcon.props("name")).toBe("visibility_off-fill");
|
package/ui.form-label/ULabel.vue
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, useTemplateRef, useSlots } from "vue";
|
|
2
|
+
import { computed, useTemplateRef, useSlots, useId } from "vue";
|
|
3
3
|
|
|
4
4
|
import { useUI } from "../composables/useUI";
|
|
5
5
|
import { getDefaults } from "../utils/ui";
|
|
@@ -26,6 +26,8 @@ const emit = defineEmits([
|
|
|
26
26
|
|
|
27
27
|
const slots = useSlots();
|
|
28
28
|
|
|
29
|
+
const elementId = props.id || useId();
|
|
30
|
+
|
|
29
31
|
const wrapperRef = useTemplateRef<HTMLDivElement>("wrapper");
|
|
30
32
|
const labelRef = useTemplateRef<HTMLLabelElement>("label");
|
|
31
33
|
|
|
@@ -102,6 +104,7 @@ const { getDataTest, wrapperAttrs, contentAttrs, labelAttrs, descriptionAttrs, e
|
|
|
102
104
|
<component
|
|
103
105
|
:is="tag"
|
|
104
106
|
v-if="label || hasSlotContent(slots['label'], { label })"
|
|
107
|
+
:id="elementId"
|
|
105
108
|
ref="label"
|
|
106
109
|
:for="props.for"
|
|
107
110
|
v-bind="labelAttrs"
|
|
@@ -140,6 +143,7 @@ const { getDataTest, wrapperAttrs, contentAttrs, labelAttrs, descriptionAttrs, e
|
|
|
140
143
|
<component
|
|
141
144
|
:is="tag"
|
|
142
145
|
v-if="label || hasSlotContent(slots['label'], { label })"
|
|
146
|
+
:id="elementId"
|
|
143
147
|
v-bind="labelAttrs"
|
|
144
148
|
ref="label"
|
|
145
149
|
:for="props.for"
|
package/ui.form-label/types.ts
CHANGED
|
@@ -110,6 +110,19 @@ const addOptionKeyCombination = computed(() => {
|
|
|
110
110
|
return isMac ? "(⌘ + Enter)" : "(Ctrl + Enter)";
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
+
const listboxAriaMultiselectable = computed(() => props.multiple || undefined);
|
|
114
|
+
|
|
115
|
+
const listboxAriaActivedescendant = computed(() =>
|
|
116
|
+
pointer.value >= 0 ? `${elementId}-${pointer.value}` : undefined,
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const getOptionAriaSelected = (option: Option) => {
|
|
120
|
+
if (option && option.groupLabel) return undefined;
|
|
121
|
+
if (option.divider) return undefined;
|
|
122
|
+
|
|
123
|
+
return !!isSelectedOption(option);
|
|
124
|
+
};
|
|
125
|
+
|
|
113
126
|
const filteredOptions = computed(() => {
|
|
114
127
|
const normalizedSearch = searchModel.value.toLowerCase().trim();
|
|
115
128
|
|
|
@@ -337,6 +350,12 @@ function onInputSearchBlur(event: FocusEvent) {
|
|
|
337
350
|
}
|
|
338
351
|
|
|
339
352
|
defineExpose({
|
|
353
|
+
/**
|
|
354
|
+
* Current pointer index value.
|
|
355
|
+
* @property {Ref<number>}
|
|
356
|
+
*/
|
|
357
|
+
pointer,
|
|
358
|
+
|
|
340
359
|
/**
|
|
341
360
|
* Allows setting the pointer to a specific index.
|
|
342
361
|
* @property {Function}
|
|
@@ -442,14 +461,20 @@ const {
|
|
|
442
461
|
@update:model-value="onSearchChange"
|
|
443
462
|
/>
|
|
444
463
|
|
|
445
|
-
<ul
|
|
464
|
+
<ul
|
|
465
|
+
v-bind="listAttrs"
|
|
466
|
+
role="listbox"
|
|
467
|
+
:aria-multiselectable="listboxAriaMultiselectable"
|
|
468
|
+
:aria-activedescendant="listboxAriaActivedescendant"
|
|
469
|
+
>
|
|
446
470
|
<li
|
|
447
471
|
v-for="(option, index) of filteredOptions"
|
|
448
|
-
:id="`${elementId}-${index}`"
|
|
449
472
|
:key="index"
|
|
450
473
|
v-bind="listItemAttrs"
|
|
451
474
|
ref="option"
|
|
452
475
|
:role="!(option && option.groupLabel) ? 'option' : undefined"
|
|
476
|
+
:aria-selected="getOptionAriaSelected(option)"
|
|
477
|
+
:aria-disabled="Boolean(option.disabled) || undefined"
|
|
453
478
|
:data-group-label="Boolean(option.groupLabel)"
|
|
454
479
|
>
|
|
455
480
|
<UDivider v-if="option.divider" v-bind="optionDividerAttrs" />
|
|
@@ -505,10 +530,18 @@ const {
|
|
|
505
530
|
|
|
506
531
|
<!-- group title -->
|
|
507
532
|
<template v-if="option && (option.groupLabel || option.isSubGroup) && !option.isHidden">
|
|
508
|
-
<div
|
|
533
|
+
<div
|
|
534
|
+
v-if="option.groupLabel"
|
|
535
|
+
role="group"
|
|
536
|
+
:aria-label="option.groupLabel"
|
|
537
|
+
v-bind="groupAttrs"
|
|
538
|
+
v-text="option.groupLabel"
|
|
539
|
+
/>
|
|
509
540
|
|
|
510
541
|
<div
|
|
511
542
|
v-else-if="option.isSubGroup"
|
|
543
|
+
role="group"
|
|
544
|
+
:aria-label="String(option[labelKey])"
|
|
512
545
|
:style="getMarginForSubCategory(option.level)"
|
|
513
546
|
v-bind="subGroupAttrs"
|
|
514
547
|
v-text="option[labelKey]"
|
|
@@ -516,13 +549,7 @@ const {
|
|
|
516
549
|
</template>
|
|
517
550
|
</li>
|
|
518
551
|
|
|
519
|
-
<li
|
|
520
|
-
v-if="!filteredOptions.length"
|
|
521
|
-
:id="`${elementId}-empty`"
|
|
522
|
-
ref="empty-option"
|
|
523
|
-
role="option"
|
|
524
|
-
v-bind="optionAttrs"
|
|
525
|
-
>
|
|
552
|
+
<li v-if="!filteredOptions.length" ref="empty-option" role="option" v-bind="optionAttrs">
|
|
526
553
|
<!-- @slot Use it to add something instead of empty state. -->
|
|
527
554
|
<slot name="empty">
|
|
528
555
|
<span v-bind="optionContentAttrs" v-text="localeMessages.noDataToShow" />
|
|
@@ -532,7 +559,6 @@ const {
|
|
|
532
559
|
<!-- Add button -->
|
|
533
560
|
<template v-if="addOption">
|
|
534
561
|
<li
|
|
535
|
-
:id="`${elementId}-addOption`"
|
|
536
562
|
ref="add-option"
|
|
537
563
|
role="option"
|
|
538
564
|
v-bind="addOptionLabelWrapperAttrs"
|
|
@@ -37,11 +37,11 @@ export default {
|
|
|
37
37
|
component: UListbox,
|
|
38
38
|
args: {
|
|
39
39
|
options: [
|
|
40
|
-
{ label: "New York",
|
|
41
|
-
{ label: "Los Angeles",
|
|
42
|
-
{ label: "Chicago",
|
|
43
|
-
{ label: "Houston",
|
|
44
|
-
{ label: "San Francisco",
|
|
40
|
+
{ label: "New York", value: 1 },
|
|
41
|
+
{ label: "Los Angeles", value: 2 },
|
|
42
|
+
{ label: "Chicago", value: 3 },
|
|
43
|
+
{ label: "Houston", value: 4 },
|
|
44
|
+
{ label: "San Francisco", value: 5 },
|
|
45
45
|
],
|
|
46
46
|
},
|
|
47
47
|
argTypes: {
|
|
@@ -176,12 +176,12 @@ GroupedOptions.parameters = {
|
|
|
176
176
|
export const Divider = DefaultTemplate.bind({});
|
|
177
177
|
Divider.args = {
|
|
178
178
|
options: [
|
|
179
|
-
{ label: "North America",
|
|
180
|
-
{ label: "South America",
|
|
179
|
+
{ label: "North America", value: "1" },
|
|
180
|
+
{ label: "South America", value: "2" },
|
|
181
181
|
{ divider: true },
|
|
182
|
-
{ label: "Europe",
|
|
182
|
+
{ label: "Europe", value: "3" },
|
|
183
183
|
{ divider: true },
|
|
184
|
-
{ label: "Asia",
|
|
184
|
+
{ label: "Asia", value: "4" },
|
|
185
185
|
],
|
|
186
186
|
};
|
|
187
187
|
Divider.parameters = {
|
|
@@ -198,12 +198,12 @@ export const OptionSettings = DefaultTemplate.bind({});
|
|
|
198
198
|
OptionSettings.args = {
|
|
199
199
|
multiple: true,
|
|
200
200
|
options: [
|
|
201
|
-
{ label: "New York",
|
|
202
|
-
{ label: "Kyiv",
|
|
203
|
-
{ label: "Los Angeles",
|
|
201
|
+
{ label: "New York", value: "1", disabled: true },
|
|
202
|
+
{ label: "Kyiv", value: "2", disabled: true },
|
|
203
|
+
{ label: "Los Angeles", value: "3", isHidden: true },
|
|
204
204
|
{
|
|
205
205
|
label: "Chicago",
|
|
206
|
-
|
|
206
|
+
value: "4",
|
|
207
207
|
onClick: (option: Option) =>
|
|
208
208
|
alert("onClick function for the third option: " + JSON.stringify(option)),
|
|
209
209
|
},
|
|
@@ -244,7 +244,7 @@ export const OptionSlots: StoryFn<DefaultUListboxArgs> = (args) => ({
|
|
|
244
244
|
:options="[
|
|
245
245
|
{
|
|
246
246
|
label: 'John Doe',
|
|
247
|
-
|
|
247
|
+
value: '1',
|
|
248
248
|
role: 'Developer',
|
|
249
249
|
avatar: johnDoe,
|
|
250
250
|
status: 'online',
|
|
@@ -252,7 +252,7 @@ export const OptionSlots: StoryFn<DefaultUListboxArgs> = (args) => ({
|
|
|
252
252
|
},
|
|
253
253
|
{
|
|
254
254
|
label: 'Jane Smith',
|
|
255
|
-
|
|
255
|
+
value: '2',
|
|
256
256
|
role: 'Designer',
|
|
257
257
|
avatar: emilyDavis,
|
|
258
258
|
status: 'away',
|
|
@@ -260,7 +260,7 @@ export const OptionSlots: StoryFn<DefaultUListboxArgs> = (args) => ({
|
|
|
260
260
|
},
|
|
261
261
|
{
|
|
262
262
|
label: 'Mike Johnson',
|
|
263
|
-
|
|
263
|
+
value: '3',
|
|
264
264
|
role: 'Product Manager',
|
|
265
265
|
avatar: alexJohnson,
|
|
266
266
|
status: 'offline',
|
|
@@ -268,7 +268,7 @@ export const OptionSlots: StoryFn<DefaultUListboxArgs> = (args) => ({
|
|
|
268
268
|
},
|
|
269
269
|
{
|
|
270
270
|
label: 'Sarah Wilson',
|
|
271
|
-
|
|
271
|
+
value: '4',
|
|
272
272
|
role: 'QA Engineer',
|
|
273
273
|
avatar: patMorgan,
|
|
274
274
|
status: 'online',
|
|
@@ -287,7 +287,7 @@ export const OptionSlots: StoryFn<DefaultUListboxArgs> = (args) => ({
|
|
|
287
287
|
:options="[
|
|
288
288
|
{
|
|
289
289
|
label: 'John Doe',
|
|
290
|
-
|
|
290
|
+
value: '1',
|
|
291
291
|
role: 'Developer',
|
|
292
292
|
avatar: johnDoe,
|
|
293
293
|
status: 'online',
|
|
@@ -295,7 +295,7 @@ export const OptionSlots: StoryFn<DefaultUListboxArgs> = (args) => ({
|
|
|
295
295
|
},
|
|
296
296
|
{
|
|
297
297
|
label: 'Jane Smith',
|
|
298
|
-
|
|
298
|
+
value: '2',
|
|
299
299
|
role: 'Designer',
|
|
300
300
|
avatar: emilyDavis,
|
|
301
301
|
status: 'away',
|
|
@@ -303,7 +303,7 @@ export const OptionSlots: StoryFn<DefaultUListboxArgs> = (args) => ({
|
|
|
303
303
|
},
|
|
304
304
|
{
|
|
305
305
|
label: 'Mike Johnson',
|
|
306
|
-
|
|
306
|
+
value: '3',
|
|
307
307
|
role: 'Product Manager',
|
|
308
308
|
avatar: alexJohnson,
|
|
309
309
|
status: 'offline',
|
|
@@ -311,7 +311,7 @@ export const OptionSlots: StoryFn<DefaultUListboxArgs> = (args) => ({
|
|
|
311
311
|
},
|
|
312
312
|
{
|
|
313
313
|
label: 'Sarah Wilson',
|
|
314
|
-
|
|
314
|
+
value: '4',
|
|
315
315
|
role: 'QA Engineer',
|
|
316
316
|
avatar: patMorgan,
|
|
317
317
|
status: 'online',
|
|
@@ -341,7 +341,7 @@ export const OptionSlots: StoryFn<DefaultUListboxArgs> = (args) => ({
|
|
|
341
341
|
:options="[
|
|
342
342
|
{
|
|
343
343
|
label: 'John Doe',
|
|
344
|
-
|
|
344
|
+
value: '1',
|
|
345
345
|
role: 'Developer',
|
|
346
346
|
avatar: johnDoe,
|
|
347
347
|
status: 'online',
|
|
@@ -349,7 +349,7 @@ export const OptionSlots: StoryFn<DefaultUListboxArgs> = (args) => ({
|
|
|
349
349
|
},
|
|
350
350
|
{
|
|
351
351
|
label: 'Jane Smith',
|
|
352
|
-
|
|
352
|
+
value: '2',
|
|
353
353
|
role: 'Designer',
|
|
354
354
|
avatar: emilyDavis,
|
|
355
355
|
status: 'away',
|
|
@@ -357,7 +357,7 @@ export const OptionSlots: StoryFn<DefaultUListboxArgs> = (args) => ({
|
|
|
357
357
|
},
|
|
358
358
|
{
|
|
359
359
|
label: 'Mike Johnson',
|
|
360
|
-
|
|
360
|
+
value: '3',
|
|
361
361
|
role: 'Product Manager',
|
|
362
362
|
avatar: alexJohnson,
|
|
363
363
|
status: 'offline',
|
|
@@ -365,7 +365,7 @@ export const OptionSlots: StoryFn<DefaultUListboxArgs> = (args) => ({
|
|
|
365
365
|
},
|
|
366
366
|
{
|
|
367
367
|
label: 'Sarah Wilson',
|
|
368
|
-
|
|
368
|
+
value: '4',
|
|
369
369
|
role: 'QA Engineer',
|
|
370
370
|
avatar: patMorgan,
|
|
371
371
|
status: 'online',
|