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.
Files changed (58) hide show
  1. package/locales/en.json +1 -1
  2. package/package.json +1 -1
  3. package/ui.container-accordion-item/UAccordionItem.vue +1 -5
  4. package/ui.container-accordion-item/tests/UAccordionItem.test.ts +5 -5
  5. package/ui.data-list/config.ts +1 -1
  6. package/ui.data-list/storybook/stories.ts +24 -24
  7. package/ui.data-list/tests/UDataList.test.ts +7 -7
  8. package/ui.data-table/config.ts +2 -0
  9. package/ui.data-table/storybook/stories.ts +3 -1
  10. package/ui.dropdown-badge/config.ts +1 -1
  11. package/ui.dropdown-badge/storybook/stories.ts +3 -3
  12. package/ui.dropdown-badge/tests/UDropdownBadge.test.ts +5 -5
  13. package/ui.dropdown-button/config.ts +1 -1
  14. package/ui.dropdown-button/storybook/stories.ts +7 -7
  15. package/ui.dropdown-button/tests/UDropdownButton.test.ts +5 -5
  16. package/ui.dropdown-link/config.ts +1 -1
  17. package/ui.dropdown-link/storybook/stories.ts +3 -3
  18. package/ui.dropdown-link/tests/UDropdownLink.test.ts +5 -5
  19. package/ui.form-calendar/UCalendar.vue +4 -0
  20. package/ui.form-calendar/config.ts +5 -0
  21. package/ui.form-checkbox/UCheckbox.vue +23 -4
  22. package/ui.form-checkbox/config.ts +4 -0
  23. package/ui.form-checkbox/tests/UCheckbox.test.ts +1 -1
  24. package/ui.form-checkbox-group/UCheckboxGroup.vue +2 -2
  25. package/ui.form-checkbox-group/config.ts +2 -0
  26. package/ui.form-checkbox-group/storybook/stories.ts +46 -9
  27. package/ui.form-checkbox-group/tests/UCheckboxGroup.test.ts +65 -0
  28. package/ui.form-checkbox-group/types.ts +10 -0
  29. package/ui.form-date-picker/config.ts +5 -0
  30. package/ui.form-input/UInput.vue +9 -3
  31. package/ui.form-input-counter/UInputCounter.vue +9 -0
  32. package/ui.form-input-counter/config.ts +5 -0
  33. package/ui.form-input-password/UInputPassword.vue +11 -12
  34. package/ui.form-input-password/tests/UInputPassword.test.ts +2 -2
  35. package/ui.form-label/ULabel.vue +5 -1
  36. package/ui.form-label/types.ts +5 -0
  37. package/ui.form-listbox/UListbox.vue +37 -11
  38. package/ui.form-listbox/config.ts +1 -1
  39. package/ui.form-listbox/storybook/stories.ts +25 -25
  40. package/ui.form-listbox/tests/UListbox.test.ts +6 -6
  41. package/ui.form-radio-group/URadioGroup.vue +2 -2
  42. package/ui.form-radio-group/config.ts +2 -0
  43. package/ui.form-radio-group/storybook/stories.ts +48 -8
  44. package/ui.form-radio-group/tests/URadioGroup.test.ts +68 -0
  45. package/ui.form-radio-group/types.ts +18 -4
  46. package/ui.form-select/USelect.vue +37 -2
  47. package/ui.form-select/config.ts +1 -1
  48. package/ui.form-select/tests/USelect.test.ts +6 -4
  49. package/ui.form-switch/USwitch.vue +20 -7
  50. package/ui.form-switch/config.ts +2 -0
  51. package/ui.form-switch/tests/USwitch.test.ts +6 -4
  52. package/ui.form-textarea/UTextarea.vue +15 -5
  53. package/ui.form-textarea/tests/UTextarea.test.ts +1 -1
  54. package/ui.navigation-pagination/UPagination.vue +14 -0
  55. package/ui.navigation-pagination/config.ts +9 -0
  56. package/ui.text-alert/UAlert.vue +8 -0
  57. package/ui.text-alert/config.ts +4 -0
  58. package/icons/storybook/notifications.svg +0 -1
@@ -9,9 +9,9 @@ import type { Props } from "../types";
9
9
 
10
10
  describe("UListbox.vue", () => {
11
11
  const defaultOptions = [
12
- { label: "Option 1", id: "option1" },
13
- { label: "Option 2", id: "option2" },
14
- { label: "Option 3", id: "option3" },
12
+ { label: "Option 1", value: "option1" },
13
+ { label: "Option 2", value: "option2" },
14
+ { label: "Option 3", value: "option3" },
15
15
  ];
16
16
 
17
17
  const highlightedClass = "bg-primary/5";
@@ -140,10 +140,10 @@ describe("UListbox.vue", () => {
140
140
 
141
141
  await options[1].trigger("click");
142
142
 
143
- expect(component.emitted("update:modelValue")![0][0]).toEqual([defaultOptions[0].id]);
143
+ expect(component.emitted("update:modelValue")![0][0]).toEqual([defaultOptions[0].value]);
144
144
  expect(component.emitted("update:modelValue")![1][0]).toEqual([
145
- defaultOptions[0].id,
146
- defaultOptions[1].id,
145
+ defaultOptions[0].value,
146
+ defaultOptions[1].value,
147
147
  ]);
148
148
  });
149
149
 
@@ -86,8 +86,8 @@ const { getDataTest, groupLabelAttrs, listAttrs, groupRadioAttrs } = useUI<Confi
86
86
  v-for="(option, index) in options"
87
87
  :key="index"
88
88
  :model-value="selectedItem"
89
- :value="option.value"
90
- :label="option.label"
89
+ :value="option[valueKey]"
90
+ :label="String(option[labelKey] || undefined)"
91
91
  :description="option.description"
92
92
  :disabled="disabled"
93
93
  v-bind="groupRadioAttrs"
@@ -14,6 +14,8 @@ export default /*tw*/ {
14
14
  defaults: {
15
15
  color: "primary",
16
16
  size: "md",
17
+ labelKey: "label",
18
+ valueKey: "value",
17
19
  disabled: false,
18
20
  },
19
21
  };
@@ -8,10 +8,11 @@ import {
8
8
 
9
9
  import URadioGroup from "../../ui.form-radio-group/URadioGroup.vue";
10
10
  import URadio from "../../ui.form-radio/URadio.vue";
11
+ import UAlert from "../../ui.text-alert/UAlert.vue";
11
12
  import UCol from "../../ui.container-col/UCol.vue";
13
+ import URow from "../../ui.container-row/URow.vue";
12
14
  import UBadge from "../../ui.text-badge/UBadge.vue";
13
15
  import UText from "../../ui.text-block/UText.vue";
14
- import URow from "../../ui.container-row/URow.vue";
15
16
 
16
17
  import type { Meta, StoryFn } from "@storybook/vue3-vite";
17
18
  import type { Props } from "../types";
@@ -28,9 +29,9 @@ export default {
28
29
  args: {
29
30
  label: "Select your preferred delivery option:",
30
31
  options: [
31
- { value: "standard", label: "Standard Shipping (3-5 business days)" },
32
- { value: "express", label: "Express Shipping (1-2 business days)" },
33
- { value: "pickup", label: "In-Store Pickup (Available same day)" },
32
+ { id: "standard", label: "Standard Shipping (3-5 business days)" },
33
+ { id: "express", label: "Express Shipping (1-2 business days)" },
34
+ { id: "pickup", label: "In-Store Pickup (Available same day)" },
34
35
  ],
35
36
  },
36
37
  argTypes: {
@@ -54,16 +55,17 @@ const DefaultTemplate: StoryFn<URadioGroupArgs> = (args: URadioGroupArgs) => ({
54
55
  });
55
56
 
56
57
  const EnumTemplate: StoryFn<URadioGroupArgs> = (args: URadioGroupArgs, { argTypes }) => ({
57
- components: { URadioGroup, UCol },
58
+ components: { URadioGroup, URow },
58
59
  setup: () => ({ args, argTypes, getArgs }),
59
60
  template: `
60
- <UCol>
61
+ <URow>
61
62
  <URadioGroup
62
63
  v-for="option in argTypes?.[args.enum]?.options"
63
64
  v-bind="getArgs(args, option)"
64
65
  :key="option"
66
+ :name="option"
65
67
  />
66
- </UCol>
68
+ </URow>
67
69
  `,
68
70
  });
69
71
 
@@ -125,8 +127,46 @@ LabelSlot.args = {
125
127
  name: "LabelSlot",
126
128
  slotTemplate: `
127
129
  <template #label="{ label }">
128
- <span class="text-red-500">*</span>
129
130
  {{ label }}
131
+ <span class="text-red-500">*</span>
130
132
  </template>
131
133
  `,
132
134
  };
135
+
136
+ export const CustomKeys: StoryFn<URadioGroupArgs> = (args: URadioGroupArgs) => ({
137
+ components: { URadioGroup, UCol, UAlert },
138
+ setup: () => ({ args }),
139
+ template: `
140
+ <UCol>
141
+ <URadioGroup v-bind="args" v-model="args.modelValue" />
142
+
143
+ <UAlert
144
+ :description="args.modelValue"
145
+ size="sm"
146
+ variant="soft"
147
+ color="success"
148
+ bordered
149
+ />
150
+ </UCol>
151
+ `,
152
+ });
153
+ CustomKeys.args = {
154
+ name: "CustomKeys",
155
+ label: "Select your subscription plan:",
156
+ labelKey: "title",
157
+ valueKey: "id",
158
+ options: [
159
+ { id: "basic", title: "Basic Plan - $9.99/month" },
160
+ { id: "pro", title: "Pro Plan - $19.99/month" },
161
+ { id: "enterprise", title: "Enterprise Plan - $49.99/month" },
162
+ ],
163
+ };
164
+ CustomKeys.parameters = {
165
+ docs: {
166
+ description: {
167
+ story:
168
+ "Use `labelKey` and `valueKey` props to specify custom keys for label and value in option objects. " +
169
+ "This is useful when working with data from APIs that use different property names.",
170
+ },
171
+ },
172
+ };
@@ -194,6 +194,74 @@ describe("URadioGroup.vue", () => {
194
194
  expect(radio.attributes("data-test")).toBe(`${dataTestValue}-item-${idx}-label`);
195
195
  });
196
196
  });
197
+
198
+ it("LabelKey – uses custom label key for option labels", () => {
199
+ const customOptions = [
200
+ { value: "option-1", name: "Option 1" },
201
+ { value: "option-2", name: "Option 2" },
202
+ { value: "option-3", name: "Option 3" },
203
+ ];
204
+
205
+ const component = mount(URadioGroup, {
206
+ props: {
207
+ name: defaultName,
208
+ options: customOptions,
209
+ labelKey: "name",
210
+ },
211
+ });
212
+
213
+ const radios = component.findAllComponents(URadio);
214
+
215
+ radios.forEach((radio, index) => {
216
+ expect(radio.props("label")).toBe(customOptions[index].name);
217
+ });
218
+ });
219
+
220
+ it("ValueKey – uses custom value key for option values", () => {
221
+ const customOptions = [
222
+ { id: "option-1", label: "Option 1" },
223
+ { id: "option-2", label: "Option 2" },
224
+ { id: "option-3", label: "Option 3" },
225
+ ];
226
+
227
+ const component = mount(URadioGroup, {
228
+ props: {
229
+ name: defaultName,
230
+ options: customOptions,
231
+ valueKey: "id",
232
+ },
233
+ });
234
+
235
+ const radios = component.findAllComponents(URadio);
236
+
237
+ radios.forEach((radio, index) => {
238
+ expect(radio.props("value")).toBe(customOptions[index].id);
239
+ });
240
+ });
241
+
242
+ it("LabelKey and ValueKey – works with complex objects", async () => {
243
+ const customOptions = [
244
+ { planId: "basic", title: "Basic Plan" },
245
+ { planId: "pro", title: "Pro Plan" },
246
+ ];
247
+
248
+ const component = mount(URadioGroup, {
249
+ props: {
250
+ name: defaultName,
251
+ options: customOptions,
252
+ labelKey: "title",
253
+ valueKey: "planId",
254
+ modelValue: "",
255
+ },
256
+ });
257
+
258
+ const radios = component.findAllComponents(URadio);
259
+
260
+ expect(radios[0].props("label")).toBe("Basic Plan");
261
+ expect(radios[0].props("value")).toBe("basic");
262
+ expect(radios[1].props("label")).toBe("Pro Plan");
263
+ expect(radios[1].props("value")).toBe("pro");
264
+ });
197
265
  });
198
266
 
199
267
  describe("Slots", () => {
@@ -4,12 +4,16 @@ import type { UnknownObject, UnknownArray, ComponentConfig } from "../types";
4
4
 
5
5
  export type Config = typeof defaultConfig;
6
6
 
7
- export interface URadioGroupOption {
8
- value: string | number | boolean | UnknownArray | UnknownObject;
9
- label: string;
7
+ export interface BaseOption {
8
+ value?: string | number | boolean | UnknownArray | UnknownObject;
9
+ label?: string;
10
10
  description?: string;
11
11
  }
12
12
 
13
+ export interface Option extends BaseOption {
14
+ [key: string]: string | number | boolean | UnknownArray | UnknownObject | undefined;
15
+ }
16
+
13
17
  export type SetRadioGroupSelectedItem =
14
18
  | ((value: string | number | boolean | UnknownArray | UnknownObject) => void)
15
19
  | null;
@@ -23,7 +27,17 @@ export interface Props {
23
27
  /**
24
28
  * Radio group options.
25
29
  */
26
- options?: URadioGroupOption[];
30
+ options?: Option[];
31
+
32
+ /**
33
+ * Label key in the item object of options.
34
+ */
35
+ labelKey?: string;
36
+
37
+ /**
38
+ * Value key in the item object of options.
39
+ */
40
+ valueKey?: string;
27
41
 
28
42
  /**
29
43
  * Radio group label.
@@ -218,6 +218,28 @@ const toggleIconName = computed(() => {
218
218
  return props.toggleIcon ? config.value.defaults.toggleIcon : "";
219
219
  });
220
220
 
221
+ const ariaExpanded = computed(() => isOpen.value);
222
+
223
+ const ariaActiveDescendant = computed(() => {
224
+ if (!isOpen.value || !listboxRef.value) return undefined;
225
+ const pointer = listboxRef.value.pointer;
226
+
227
+ if (pointer === undefined || pointer < 0) return undefined;
228
+
229
+ return `${elementId}-${pointer}`;
230
+ });
231
+
232
+ const ariaInvalid = computed(() => Boolean(props.error) ?? undefined);
233
+
234
+ const ariaLabelledBy = computed(() => (props.label ? elementId : undefined));
235
+
236
+ const ariaDescribedBy = computed(() => {
237
+ if (props.error) return `error-${elementId}`;
238
+ if (props.description) return `description-${elementId}`;
239
+
240
+ return undefined;
241
+ });
242
+
221
243
  watch(localValue, setLabelPosition, { deep: true });
222
244
 
223
245
  onMounted(() => {
@@ -232,6 +254,10 @@ function onSearchChange(query: string) {
232
254
  emit("searchChange", query);
233
255
  }
234
256
 
257
+ function onSearchUpdate(query: string) {
258
+ emit("update:search", query);
259
+ }
260
+
235
261
  function onKeydownAddOption(event: KeyboardEvent) {
236
262
  if (!isOpen.value) return;
237
263
 
@@ -484,6 +510,7 @@ const {
484
510
  <template>
485
511
  <ULabel
486
512
  ref="labelComponent"
513
+ :for="elementId"
487
514
  :size="size"
488
515
  :label="label"
489
516
  :error="error"
@@ -515,7 +542,15 @@ const {
515
542
  ref="wrapper"
516
543
  :tabindex="searchable || disabled ? -1 : 0"
517
544
  role="combobox"
518
- :aria-owns="'listbox-' + elementId"
545
+ :aria-expanded="ariaExpanded"
546
+ aria-haspopup="listbox"
547
+ :aria-controls="`listbox-${elementId}`"
548
+ :aria-activedescendant="ariaActiveDescendant"
549
+ :aria-disabled="disabled || undefined"
550
+ :aria-invalid="ariaInvalid"
551
+ :aria-labelledby="ariaLabelledBy"
552
+ :aria-describedby="ariaDescribedBy"
553
+ :aria-autocomplete="searchable ? 'list' : undefined"
519
554
  v-bind="wrapperAttrs"
520
555
  @focus="activate"
521
556
  @blur="onBlur"
@@ -830,7 +865,7 @@ const {
830
865
  @blur="onListboxBlur"
831
866
  @search-blur="onListboxSearchBlur"
832
867
  @search-change="onSearchChange"
833
- @update:search="(value) => emit('update:search', value)"
868
+ @update:search="onSearchUpdate"
834
869
  >
835
870
  <template #before-option="{ option, index }">
836
871
  <!--
@@ -138,7 +138,7 @@ export default /*tw*/ {
138
138
  size: "md",
139
139
  labelAlign: "topInside",
140
140
  openDirection: "auto",
141
- valueKey: "id",
141
+ valueKey: "value",
142
142
  labelKey: "label",
143
143
  groupLabelKey: "label",
144
144
  multipleVariant: "inline",
@@ -11,9 +11,9 @@ import type { Props } from "../types";
11
11
 
12
12
  describe("USelect.vue", () => {
13
13
  const defaultOptions = [
14
- { label: "Option 1", id: "option1" },
15
- { label: "Option 2", id: "option2" },
16
- { label: "Option 3", id: "option3" },
14
+ { label: "Option 1", value: "option1" },
15
+ { label: "Option 2", value: "option2" },
16
+ { label: "Option 3", value: "option3" },
17
17
  ];
18
18
 
19
19
  describe("Props", () => {
@@ -448,7 +448,9 @@ describe("USelect.vue", () => {
448
448
  },
449
449
  });
450
450
 
451
- expect(component.find("[vl-key='wrapper']").attributes("aria-owns")).toBe(`listbox-${id}`);
451
+ expect(component.find("[vl-key='wrapper']").attributes("aria-controls")).toBe(
452
+ `listbox-${id}`,
453
+ );
452
454
  });
453
455
 
454
456
  it("Data Test – applies the correct data-test attributes", async () => {
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { computed, useId, useTemplateRef } from "vue";
2
+ import { computed, useId, useSlots, useTemplateRef } from "vue";
3
3
 
4
4
  import { useUI } from "../composables/useUI";
5
5
  import { getDefaults } from "../utils/ui";
@@ -29,7 +29,7 @@ const emit = defineEmits([
29
29
  "update:modelValue",
30
30
  ]);
31
31
 
32
- const wrapperRef = useTemplateRef<HTMLLabelElement>("wrapper");
32
+ const wrapperRef = useTemplateRef<HTMLDivElement>("wrapper");
33
33
 
34
34
  const { localeMessages } = useComponentLocaleMessages<typeof defaultConfig.i18n>(
35
35
  COMPONENT_NAME,
@@ -42,8 +42,14 @@ const checkedValue = computed({
42
42
  set: (value) => emit("update:modelValue", value),
43
43
  });
44
44
 
45
+ const slots = useSlots();
46
+
45
47
  const elementId = props.id || useId();
46
48
 
49
+ const hasLabel = computed(() => Boolean(props.label || slots.label));
50
+
51
+ const inputAriaLabelledBy = computed(() => (hasLabel.value ? elementId : undefined));
52
+
47
53
  const switchLabel = computed(() => {
48
54
  return checkedValue.value ? localeMessages.value.active : localeMessages.value.inactive;
49
55
  });
@@ -62,7 +68,8 @@ function toggle() {
62
68
  }
63
69
  }
64
70
 
65
- function onClickToggle() {
71
+ function onClickToggle(event: Event) {
72
+ event.stopPropagation();
66
73
  toggle();
67
74
  }
68
75
 
@@ -70,10 +77,14 @@ function onKeydownSpace() {
70
77
  toggle();
71
78
  }
72
79
 
80
+ function onClickWrapper() {
81
+ toggle();
82
+ }
83
+
73
84
  defineExpose({
74
85
  /**
75
86
  * A reference to the component's wrapper element for direct DOM manipulation.
76
- * @property {HTMLLabelElement}
87
+ * @property {HTMLDivElement}
77
88
  */
78
89
  wrapperRef,
79
90
  });
@@ -118,13 +129,13 @@ const {
118
129
  <slot name="label" :label="label" />
119
130
  </template>
120
131
 
121
- <label
132
+ <div
122
133
  ref="wrapper"
123
134
  tabindex="0"
124
- :for="elementId"
125
135
  v-bind="wrapperAttrs"
126
136
  @keydown.enter="onKeydownSpace"
127
137
  @keydown.space.prevent="onKeydownSpace"
138
+ @click="onClickWrapper"
128
139
  >
129
140
  <input
130
141
  :id="elementId"
@@ -132,6 +143,8 @@ const {
132
143
  tabindex="-1"
133
144
  type="checkbox"
134
145
  :disabled="disabled"
146
+ :aria-labelledby="inputAriaLabelledBy"
147
+ :aria-label="!hasLabel ? localeMessages.switch : undefined"
135
148
  v-bind="inputAttrs"
136
149
  @click="onClickToggle"
137
150
  />
@@ -146,6 +159,6 @@ const {
146
159
  </span>
147
160
 
148
161
  <span v-if="toggleLabel" v-bind="toggleLabelAttrs" v-text="switchLabel" />
149
- </label>
162
+ </div>
150
163
  </ULabel>
151
164
  </template>
@@ -74,7 +74,9 @@ export default /*tw*/ {
74
74
  },
75
75
  ],
76
76
  },
77
+ /* These are used for a11y. */
77
78
  i18n: {
79
+ switch: "Switch",
78
80
  inactive: "Off",
79
81
  active: "On",
80
82
  },
@@ -106,7 +106,7 @@ describe("USwitch.vue", () => {
106
106
  },
107
107
  });
108
108
 
109
- expect(component.find("label").attributes("class")).toContain(color);
109
+ expect(component.get("[vl-key='wrapper']").attributes("class")).toContain(`bg-${color}`);
110
110
  });
111
111
  });
112
112
 
@@ -169,7 +169,9 @@ describe("USwitch.vue", () => {
169
169
  const labelComponent = component.findComponent(ULabel);
170
170
 
171
171
  expect(labelComponent.props("disabled")).toBe(true);
172
- expect(component.find("label").attributes("class")).toContain("pointer-events-none");
172
+ expect(component.get("[vl-key='wrapper']").attributes("class")).toContain(
173
+ "pointer-events-none",
174
+ );
173
175
  });
174
176
 
175
177
  it("Id – applies the correct id attribute", () => {
@@ -210,7 +212,7 @@ describe("USwitch.vue", () => {
210
212
  },
211
213
  });
212
214
 
213
- expect(component.findAll("label")[1].text()).toBe(customLabelContent);
215
+ expect(component.find("label").text()).toBe(customLabelContent);
214
216
  });
215
217
 
216
218
  it("Label – exposes label prop to slot", () => {
@@ -225,7 +227,7 @@ describe("USwitch.vue", () => {
225
227
  },
226
228
  });
227
229
 
228
- expect(component.findAll("label")[1].text()).toBe(`Modified ${defaultLabel}`);
230
+ expect(component.find("label").text()).toBe(`Modified ${defaultLabel}`);
229
231
  });
230
232
  });
231
233
 
@@ -63,7 +63,7 @@ const elementId = props.id || useId();
63
63
  const textareaRef = useTemplateRef<HTMLTextAreaElement>("textarea");
64
64
  const labelComponentRef = useTemplateRef<InstanceType<typeof ULabel>>("labelComponent");
65
65
  const leftSlotWrapperRef = useTemplateRef<HTMLDivElement>("leftSlotWrapper");
66
- const wrapperRef = useTemplateRef<HTMLLabelElement>("wrapper");
66
+ const wrapperRef = useTemplateRef<HTMLDivElement>("wrapper");
67
67
 
68
68
  const currentRows = ref(Number(props.rows));
69
69
 
@@ -192,10 +192,14 @@ function onMousedown() {
192
192
  emit("mousedown");
193
193
  }
194
194
 
195
+ function onSlotClick() {
196
+ textareaRef.value?.focus();
197
+ }
198
+
195
199
  defineExpose({
196
200
  /**
197
201
  * A reference to the component's wrapper element for direct DOM manipulation.
198
- * @property {HTMLLabelElement}
202
+ * @property {HTMLDivElement}
199
203
  */
200
204
  wrapperRef,
201
205
 
@@ -246,12 +250,13 @@ const {
246
250
  <slot name="label" :label="label" />
247
251
  </template>
248
252
 
249
- <label ref="wrapper" :for="elementId" v-bind="wrapperAttrs">
253
+ <div ref="wrapper" v-bind="wrapperAttrs">
250
254
  <span
251
255
  v-if="hasSlotContent($slots['left'])"
252
256
  ref="leftSlotWrapper"
253
257
  :for="elementId"
254
258
  v-bind="leftSlotAttrs"
259
+ @click="onSlotClick"
255
260
  >
256
261
  <!-- @slot Use it to add something before the text. -->
257
262
  <slot name="left" />
@@ -277,10 +282,15 @@ const {
277
282
  @click="onClick"
278
283
  />
279
284
 
280
- <span v-if="hasSlotContent($slots['right'])" :for="elementId" v-bind="rightSlotAttrs">
285
+ <span
286
+ v-if="hasSlotContent($slots['right'])"
287
+ :for="elementId"
288
+ v-bind="rightSlotAttrs"
289
+ @click="onSlotClick"
290
+ >
281
291
  <!-- @slot Use it to add something after the text. -->
282
292
  <slot name="right" />
283
293
  </span>
284
- </label>
294
+ </div>
285
295
  </ULabel>
286
296
  </template>
@@ -528,7 +528,7 @@ describe("UTextarea.vue", () => {
528
528
  });
529
529
 
530
530
  expect(component.vm.wrapperRef).toBeDefined();
531
- expect(component.vm.wrapperRef!.tagName).toBe("LABEL");
531
+ expect(component.vm.wrapperRef!.tagName).toBe("DIV");
532
532
  });
533
533
 
534
534
  it("Textarea Element – exposes textarea element ref", () => {
@@ -4,6 +4,7 @@ import { range } from "lodash-es";
4
4
 
5
5
  import { useUI } from "../composables/useUI";
6
6
  import { getDefaults } from "../utils/ui";
7
+ import { useComponentLocaleMessages } from "../composables/useComponentLocaleMassages";
7
8
 
8
9
  import UButton from "../ui.button/UButton.vue";
9
10
  import UIcon from "../ui.image-icon/UIcon.vue";
@@ -32,6 +33,12 @@ const emit = defineEmits([
32
33
  "update:modelValue",
33
34
  ]);
34
35
 
36
+ const { localeMessages } = useComponentLocaleMessages<typeof defaultConfig.i18n>(
37
+ COMPONENT_NAME,
38
+ defaultConfig.i18n,
39
+ props?.config?.i18n,
40
+ );
41
+
35
42
  const paginationRef = useTemplateRef<HTMLDivElement>("pagination");
36
43
 
37
44
  const currentPage = computed({
@@ -133,6 +140,7 @@ const {
133
140
  :label="firstLabel"
134
141
  :square="!firstLabel"
135
142
  :disabled="prevIsDisabled"
143
+ :aria-label="firstLabel || localeMessages.first"
136
144
  v-bind="firstButtonAttrs"
137
145
  :data-test="getDataTest('first')"
138
146
  @click="goToFirstPage"
@@ -156,6 +164,7 @@ const {
156
164
  :label="prevLabel"
157
165
  :square="!prevLabel"
158
166
  :disabled="prevIsDisabled"
167
+ :aria-label="prevLabel || localeMessages.prev"
159
168
  v-bind="prevButtonAttrs"
160
169
  :data-test="getDataTest('prev')"
161
170
  @click="goToPrevPage"
@@ -185,6 +194,8 @@ const {
185
194
  :variant="variant"
186
195
  :label="String(page.number)"
187
196
  :disabled="disabled"
197
+ :aria-label="`${localeMessages.currentPage} ${page.number}`"
198
+ :aria-current="true"
188
199
  v-bind="activeButtonAttrs"
189
200
  :data-test="getDataTest('active')"
190
201
  />
@@ -194,6 +205,7 @@ const {
194
205
  variant="ghost"
195
206
  :label="String(page.number)"
196
207
  :disabled="disabled"
208
+ :aria-label="`${localeMessages.goToPage} ${page.number}`"
197
209
  v-bind="inactiveButtonAttrs"
198
210
  :data-test="getDataTest('inactive')"
199
211
  @click="selectPage(page.number)"
@@ -205,6 +217,7 @@ const {
205
217
  :label="nextLabel"
206
218
  :square="!nextLabel"
207
219
  :disabled="nextIsDisabled"
220
+ :aria-label="nextLabel || localeMessages.next"
208
221
  v-bind="nextButtonAttrs"
209
222
  :data-test="getDataTest('next')"
210
223
  @click="goToNextPage"
@@ -229,6 +242,7 @@ const {
229
242
  :label="lastLabel"
230
243
  :square="!lastLabel"
231
244
  :disabled="nextIsDisabled"
245
+ :aria-label="lastLabel || localeMessages.last"
232
246
  v-bind="lastButtonAttrs"
233
247
  :data-test="getDataTest('last')"
234
248
  @click="goToLastPage"
@@ -34,6 +34,15 @@ export default /*tw*/ {
34
34
  firstIcon: "{>paginationIcon}",
35
35
  prevIcon: "{>paginationIcon}",
36
36
  nextIcon: "{>paginationIcon}",
37
+ /* These are used for a11y. */
38
+ i18n: {
39
+ first: "Go to first page",
40
+ last: "Go to last page",
41
+ prev: "Go to previous page",
42
+ next: "Go to next page",
43
+ currentPage: "Current page, page",
44
+ goToPage: "Go to page",
45
+ },
37
46
  defaults: {
38
47
  variant: "solid",
39
48
  size: "md",