vueless 1.4.12-beta.9 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +1 -1
  2. package/index.d.ts +128 -0
  3. package/index.ts +128 -0
  4. package/package.json +10 -9
  5. package/ui.button/config.ts +1 -1
  6. package/ui.button-link/config.ts +1 -1
  7. package/ui.button-toggle/UToggle.vue +6 -4
  8. package/ui.button-toggle/storybook/stories.ts +4 -1
  9. package/ui.button-toggle/tests/UToggle.test-d.ts +48 -0
  10. package/ui.button-toggle/tests/UToggleTypes.vue +55 -0
  11. package/ui.button-toggle/types.ts +18 -6
  12. package/ui.container-accordion/types.ts +5 -2
  13. package/ui.container-collapsible/config.ts +1 -1
  14. package/ui.data-list/UDataList.vue +48 -17
  15. package/ui.data-list/storybook/stories.ts +4 -1
  16. package/ui.data-list/tests/UDataList.test-d.ts +76 -0
  17. package/ui.data-list/tests/UDataList.test.ts +50 -1
  18. package/ui.data-list/tests/UDataListTypes.vue +110 -0
  19. package/ui.data-list/types.ts +37 -5
  20. package/ui.data-table/UTable.vue +68 -10
  21. package/ui.data-table/config.ts +1 -1
  22. package/ui.data-table/storybook/stories.ts +4 -1
  23. package/ui.data-table/tests/UTable.test-d.ts +179 -0
  24. package/ui.data-table/tests/UTable.test.ts +1 -1
  25. package/ui.data-table/tests/UTableEmptyColumnsTypes.vue +27 -0
  26. package/ui.data-table/tests/UTableTypes.vue +193 -0
  27. package/ui.data-table/types.ts +91 -8
  28. package/ui.data-table/utilTable.ts +1 -1
  29. package/ui.dropdown/UDropdown.vue +19 -8
  30. package/ui.dropdown/storybook/stories.ts +4 -1
  31. package/ui.dropdown/tests/UDropdown.test-d.ts +61 -0
  32. package/ui.dropdown/tests/UDropdown.test.ts +8 -1
  33. package/ui.dropdown/tests/UDropdownTypes.vue +111 -0
  34. package/ui.dropdown/types.ts +27 -3
  35. package/ui.dropdown-badge/UDropdownBadge.vue +9 -4
  36. package/ui.dropdown-badge/storybook/stories.ts +6 -1
  37. package/ui.dropdown-badge/tests/UDropdownBadge.test-d.ts +55 -0
  38. package/ui.dropdown-badge/tests/UDropdownBadge.test.ts +8 -1
  39. package/ui.dropdown-badge/tests/UDropdownBadgeTypes.vue +107 -0
  40. package/ui.dropdown-badge/types.ts +17 -3
  41. package/ui.dropdown-button/UDropdownButton.vue +9 -4
  42. package/ui.dropdown-button/storybook/stories.ts +6 -1
  43. package/ui.dropdown-button/tests/UDropdownButton.test-d.ts +57 -0
  44. package/ui.dropdown-button/tests/UDropdownButton.test.ts +8 -1
  45. package/ui.dropdown-button/tests/UDropdownButtonTypes.vue +107 -0
  46. package/ui.dropdown-button/types.ts +17 -3
  47. package/ui.dropdown-link/UDropdownLink.vue +9 -4
  48. package/ui.dropdown-link/config.ts +1 -1
  49. package/ui.dropdown-link/storybook/stories.ts +4 -1
  50. package/ui.dropdown-link/tests/UDropdownLink.test-d.ts +55 -0
  51. package/ui.dropdown-link/tests/UDropdownLink.test.ts +8 -1
  52. package/ui.dropdown-link/tests/UDropdownLinkTypes.vue +107 -0
  53. package/ui.dropdown-link/types.ts +17 -3
  54. package/ui.form-calendar/config.ts +1 -1
  55. package/ui.form-checkbox/config.ts +1 -1
  56. package/ui.form-checkbox/tests/UCheckbox.test.ts +4 -4
  57. package/ui.form-checkbox/types.ts +4 -2
  58. package/ui.form-checkbox-group/UCheckboxGroup.vue +8 -2
  59. package/ui.form-date-picker/UDatePicker.vue +11 -1
  60. package/ui.form-date-picker/tests/UDatePicker.test.ts +47 -0
  61. package/ui.form-input/config.ts +1 -1
  62. package/ui.form-input-file/config.ts +1 -1
  63. package/ui.form-listbox/UListbox.vue +29 -12
  64. package/ui.form-listbox/config.ts +1 -1
  65. package/ui.form-listbox/storybook/stories.ts +4 -1
  66. package/ui.form-listbox/tests/UListbox.test-d.ts +63 -0
  67. package/ui.form-listbox/tests/UListboxTypes.vue +107 -0
  68. package/ui.form-listbox/types.ts +34 -2
  69. package/ui.form-radio/config.ts +1 -1
  70. package/ui.form-radio-group/URadioGroup.vue +15 -3
  71. package/ui.form-radio-group/config.ts +8 -3
  72. package/ui.form-radio-group/types.ts +2 -3
  73. package/ui.form-select/USelect.vue +49 -17
  74. package/ui.form-select/config.ts +4 -4
  75. package/ui.form-select/storybook/stories.ts +4 -1
  76. package/ui.form-select/tests/USelect.test-d.ts +76 -0
  77. package/ui.form-select/tests/USelect.test.ts +65 -1
  78. package/ui.form-select/tests/USelectTypes.vue +113 -0
  79. package/ui.form-select/types.ts +55 -4
  80. package/ui.form-switch/USwitch.vue +13 -12
  81. package/ui.form-switch/config.ts +2 -1
  82. package/ui.form-switch/tests/USwitch.test.ts +151 -0
  83. package/ui.form-textarea/config.ts +1 -1
  84. package/ui.image-avatar/types.ts +5 -2
  85. package/ui.image-avatar-group/types.ts +5 -2
  86. package/ui.image-icon/UIcon.vue +3 -2
  87. package/ui.image-icon/types.ts +2 -2
  88. package/ui.navigation-breadcrumbs/types.ts +5 -2
  89. package/ui.navigation-tabs/UTabs.vue +6 -4
  90. package/ui.navigation-tabs/storybook/stories.ts +4 -1
  91. package/ui.navigation-tabs/tests/UTabs.test-d.ts +50 -0
  92. package/ui.navigation-tabs/tests/UTabsTypes.vue +65 -0
  93. package/ui.navigation-tabs/types.ts +25 -3
  94. package/ui.skeleton-text/constants.ts +1 -1
  95. package/ui.text-badge/config.ts +1 -1
  96. package/utils/node/loaderIcon.js +1 -1
  97. package/v.tooltip/vTooltip.ts +1 -1
@@ -0,0 +1,107 @@
1
+ <!-- Type-level fixture: checked by `npm run ts:check`, never mounted.
2
+ Proves slot props infer from the `options` prop at a real call site. -->
3
+ <template>
4
+ <UDropdownButton :options="options">
5
+ <!-- Forwarded through `UDropdown` to `UListbox`, so they inherit its group flattening — a
6
+ flattened member need not carry the parent's members, hence the optional types. -->
7
+ <template #before-option="{ option, index }">
8
+ {{ expectOptionalString(option.label) }}
9
+ {{ expectOptionalNumber(option.qty) }}
10
+ {{ expectNumber(index) }}
11
+ </template>
12
+
13
+ <template #option="{ option, index }">
14
+ {{ expectOptionalString(option.label) }}
15
+ {{ expectNumber(index) }}
16
+ </template>
17
+
18
+ <template #after-option="{ option, index }">
19
+ {{ expectOptionalString(option.label) }}
20
+ {{ expectNumber(index) }}
21
+ </template>
22
+
23
+ <template #empty>empty</template>
24
+ </UDropdownButton>
25
+
26
+ <!-- Options from a non-literal source still expose the declared option shape. -->
27
+ <UDropdownButton :options="typedOptions">
28
+ <template #option="{ option }">{{ expectOptionalString(option.label) }}</template>
29
+ </UDropdownButton>
30
+
31
+ <UDropdownButton :options="options">
32
+ <template #option="{ option }">
33
+ <!-- @vue-expect-error `nonExistent` is not a key of the inferred option. -->
34
+ {{ option.nonExistent }}
35
+ </template>
36
+ </UDropdownButton>
37
+
38
+ <!-- `label` is `UDropdown`'s `displayLabel`, a join of `option[labelKey]` lookups. -->
39
+ <UDropdownButton :options="options">
40
+ <template #default="{ label }">
41
+ <!-- @vue-expect-error the label is `undefined` when it cannot be resolved. -->
42
+ {{ expectString(label) }}
43
+ </template>
44
+ </UDropdownButton>
45
+
46
+ <!-- Regression guard: an option slot must never promise the caller's members as present. -->
47
+ <UDropdownButton
48
+ :options="groupedOptions"
49
+ group-label-key="groupName"
50
+ group-value-key="children"
51
+ label-key="label"
52
+ >
53
+ <template #option="{ option }">
54
+ <!-- @vue-expect-error a flattened group member need not carry `label`. -->
55
+ {{ expectString(option.label) }}
56
+ </template>
57
+ <template #before-option="{ option }">
58
+ <!-- @vue-expect-error a flattened group member need not carry `qty`. -->
59
+ {{ expectNumber(option.qty) }}
60
+ </template>
61
+ <template #after-option="{ option }">
62
+ <!-- @vue-expect-error a flattened group member need not carry `groupName`. -->
63
+ {{ expectString(option.groupName) }}
64
+ </template>
65
+ </UDropdownButton>
66
+ </template>
67
+
68
+ <script setup lang="ts">
69
+ import { ref } from "vue";
70
+
71
+ import UDropdownButton from "../UDropdownButton.vue";
72
+
73
+ interface UserOption {
74
+ label: string;
75
+ value: string;
76
+ qty: number;
77
+ }
78
+
79
+ const options = [{ label: "a", value: "a", qty: 1 }];
80
+ const typedOptions = ref<UserOption[]>([]);
81
+
82
+ /* A group whose members lack the parent's members — exactly what `filterGroups` flattens. */
83
+ const groupedOptions = [
84
+ {
85
+ groupName: "Group A",
86
+ label: "Group A",
87
+ qty: 1,
88
+ children: [{ value: "c1", title: "Child" }],
89
+ },
90
+ ];
91
+
92
+ function expectString(value: string) {
93
+ return value;
94
+ }
95
+
96
+ function expectNumber(value: number) {
97
+ return value;
98
+ }
99
+
100
+ function expectOptionalString(value: string | undefined) {
101
+ return value;
102
+ }
103
+
104
+ function expectOptionalNumber(value: number | undefined) {
105
+ return value;
106
+ }
107
+ </script>
@@ -1,11 +1,25 @@
1
1
  import defaultConfig from "./config";
2
2
 
3
- import type { Option } from "../ui.form-listbox/types";
3
+ import type { ListboxOption, SlotOption } from "../ui.form-listbox/types";
4
4
  import type { ComponentConfig, UnknownObject } from "../types";
5
5
 
6
6
  export type Config = typeof defaultConfig;
7
7
 
8
- export interface Props {
8
+ /* Option slots are forwarded through `UDropdown` to `UListbox`, inheriting its group flattening —
9
+ a flattened member need not carry the parent's members, so `SlotOption` is `Partial`. */
10
+ export interface UDropdownButtonSlots<TOption extends ListboxOption = ListboxOption> {
11
+ /* `label` is `UDropdown`'s `displayLabel`, a join of `option[labelKey]` lookups — `undefined`
12
+ when the label cannot be resolved. */
13
+ default?: (props: { label: string | undefined; opened: boolean }) => unknown;
14
+ left?: (props: { opened: boolean }) => unknown;
15
+ toggle?: (props: { opened: boolean }) => unknown;
16
+ "before-option"?: (props: { option: SlotOption<TOption>; index: number }) => unknown;
17
+ option?: (props: { option: SlotOption<TOption>; index: number }) => unknown;
18
+ "after-option"?: (props: { option: SlotOption<TOption>; index: number }) => unknown;
19
+ empty?: () => unknown;
20
+ }
21
+
22
+ export interface Props<TOption extends ListboxOption = ListboxOption> {
9
23
  /**
10
24
  * Selected button.
11
25
  */
@@ -24,7 +38,7 @@ export interface Props {
24
38
  /**
25
39
  * Options list.
26
40
  */
27
- options?: Option[];
41
+ options?: TOption[];
28
42
 
29
43
  /**
30
44
  * Label key in the item object of options.
@@ -1,4 +1,4 @@
1
- <script setup lang="ts">
1
+ <script setup lang="ts" generic="TOption extends ListboxOption">
2
2
  import { computed, useTemplateRef } from "vue";
3
3
 
4
4
  import { useUI } from "../composables/useUI";
@@ -11,11 +11,13 @@ import UDropdown from "../ui.dropdown/UDropdown.vue";
11
11
  import { COMPONENT_NAME } from "./constants";
12
12
  import defaultConfig from "./config";
13
13
 
14
- import type { Props, Config } from "./types";
14
+ import type { Props, Config, UDropdownLinkSlots } from "./types";
15
+ import type { ListboxOption } from "../ui.form-listbox/types";
16
+ import type { ComponentExposed } from "../types";
15
17
 
16
18
  defineOptions({ inheritAttrs: false });
17
19
 
18
- const props = withDefaults(defineProps<Props>(), {
20
+ const props = withDefaults(defineProps<Props<TOption>>(), {
19
21
  ...getDefaults<Props, Config>(defaultConfig, COMPONENT_NAME),
20
22
  options: () => [],
21
23
  modelValue: "",
@@ -59,7 +61,10 @@ const emit = defineEmits([
59
61
  "update:search",
60
62
  ]);
61
63
 
62
- type UDropdownRef = InstanceType<typeof UDropdown>;
64
+ defineSlots<UDropdownLinkSlots<TOption>>();
65
+
66
+ /* `UDropdown` is generic, so its type is a function — `InstanceType` does not apply. */
67
+ type UDropdownRef = ComponentExposed<typeof UDropdown>;
63
68
 
64
69
  const dropdownRef = useTemplateRef<UDropdownRef>("dropdown");
65
70
 
@@ -3,7 +3,7 @@ export default /*tw*/ {
3
3
  base: `
4
4
  {UDropdown}
5
5
  inline-flex gap-0.5 items-center justify-between rounded-small h-max
6
- outline-solid outline-transparent
6
+ outline-solid outline-0 outline-transparent
7
7
  focus-visible:outline-offset-4
8
8
  `,
9
9
  defaults: {
@@ -6,7 +6,7 @@ import {
6
6
  getDocsDescription,
7
7
  } from "../../utils/storybook";
8
8
 
9
- import UDropdownLink from "../../ui.dropdown-link/UDropdownLink.vue";
9
+ import UDropdownLinkComponent from "../../ui.dropdown-link/UDropdownLink.vue";
10
10
  import URow from "../../ui.container-row/URow.vue";
11
11
  import UCol from "../../ui.container-col/UCol.vue";
12
12
  import UIcon from "../../ui.image-icon/UIcon.vue";
@@ -24,6 +24,9 @@ import emilyDavis from "../../ui.form-select/storybook/assets/images/emily-davis
24
24
  import alexJohnson from "../../ui.form-select/storybook/assets/images/alex-johnson.png";
25
25
  import patMorgan from "../../ui.form-select/storybook/assets/images/pat-morgan.png";
26
26
 
27
+ /* Generic SFCs are typed as a function, which drops the compiler-generated `__name`. */
28
+ const UDropdownLink = UDropdownLinkComponent as typeof UDropdownLinkComponent & { __name: string };
29
+
27
30
  interface DefaultUDropdownLinkArgs extends Props {
28
31
  slotTemplate?: string;
29
32
  }
@@ -0,0 +1,55 @@
1
+ import { describe, it, expectTypeOf } from "vitest";
2
+
3
+ import type { Props, UDropdownLinkSlots } from "../types";
4
+ import type { ListboxOption, SlotOption } from "../../ui.form-listbox/types";
5
+
6
+ interface UserOption {
7
+ label: string;
8
+ value: string;
9
+ qty: number;
10
+ }
11
+
12
+ type OptionProps = Parameters<NonNullable<UDropdownLinkSlots<UserOption>["option"]>>[0];
13
+
14
+ describe("UDropdownLink types", () => {
15
+ it("infers the caller's option shape in a forwarded option slot", () => {
16
+ expectTypeOf<OptionProps["option"]["label"]>().toEqualTypeOf<string | undefined>();
17
+ expectTypeOf<OptionProps["option"]["qty"]>().toEqualTypeOf<number | undefined>();
18
+ expectTypeOf<OptionProps["index"]>().toEqualTypeOf<number>();
19
+ });
20
+
21
+ it("does not expose undeclared option properties to a slot", () => {
22
+ type HasIndexSignature = string extends keyof OptionProps["option"] ? true : false;
23
+
24
+ expectTypeOf<HasIndexSignature>().toEqualTypeOf<false>();
25
+ });
26
+
27
+ it("reuses UListbox's SlotOption rather than a second dialect", () => {
28
+ expectTypeOf<OptionProps["option"]>().toEqualTypeOf<SlotOption<UserOption>>();
29
+ });
30
+
31
+ it("does not promise the caller's members on any option slot", () => {
32
+ /* Forwarded through `UDropdown` to `UListbox`, whose `filterGroups` concats nested group
33
+ * members into the flat list — a member need not carry the parent's members. */
34
+ type BeforeProps = Parameters<NonNullable<UDropdownLinkSlots<UserOption>["before-option"]>>[0];
35
+ type AfterProps = Parameters<NonNullable<UDropdownLinkSlots<UserOption>["after-option"]>>[0];
36
+
37
+ type GroupMember = { value: string; title: string };
38
+
39
+ expectTypeOf<GroupMember>().toExtend<BeforeProps["option"]>();
40
+ expectTypeOf<GroupMember>().toExtend<OptionProps["option"]>();
41
+ expectTypeOf<GroupMember>().toExtend<AfterProps["option"]>();
42
+ });
43
+
44
+ it("types the default slot label as possibly undefined", () => {
45
+ /* It is `UDropdown`'s `displayLabel`, a join of `option[labelKey]` lookups. */
46
+ type DefaultProps = Parameters<NonNullable<UDropdownLinkSlots<UserOption>["default"]>>[0];
47
+
48
+ expectTypeOf<DefaultProps["label"]>().toEqualTypeOf<string | undefined>();
49
+ });
50
+
51
+ it("resolves Props with no type argument", () => {
52
+ expectTypeOf<Props>().toEqualTypeOf<Props<ListboxOption>>();
53
+ expectTypeOf<Props<UserOption>["options"]>().toEqualTypeOf<UserOption[] | undefined>();
54
+ });
55
+ });
@@ -4,10 +4,17 @@ import { describe, it, expect } from "vitest";
4
4
  import UDropdownLink from "../UDropdownLink.vue";
5
5
  import ULink from "../../ui.button-link/ULink.vue";
6
6
  import UIcon from "../../ui.image-icon/UIcon.vue";
7
- import UListbox from "../../ui.form-listbox/UListbox.vue";
7
+ import UListboxComponent from "../../ui.form-listbox/UListbox.vue";
8
8
 
9
9
  import type { Props } from "../types";
10
10
 
11
+ import type { ComponentProps } from "../../types";
12
+
13
+ /* Generic SFCs are typed as a function, which `findComponent` does not accept. */
14
+ const UListbox = UListboxComponent as unknown as new () => {
15
+ $props: ComponentProps<typeof UListboxComponent>;
16
+ };
17
+
11
18
  describe("UDropdownLink.vue", () => {
12
19
  const defaultOptions = [
13
20
  { value: 1, label: "Option 1" },
@@ -0,0 +1,107 @@
1
+ <!-- Type-level fixture: checked by `npm run ts:check`, never mounted.
2
+ Proves slot props infer from the `options` prop at a real call site. -->
3
+ <template>
4
+ <UDropdownLink :options="options">
5
+ <!-- Forwarded through `UDropdown` to `UListbox`, so they inherit its group flattening — a
6
+ flattened member need not carry the parent's members, hence the optional types. -->
7
+ <template #before-option="{ option, index }">
8
+ {{ expectOptionalString(option.label) }}
9
+ {{ expectOptionalNumber(option.qty) }}
10
+ {{ expectNumber(index) }}
11
+ </template>
12
+
13
+ <template #option="{ option, index }">
14
+ {{ expectOptionalString(option.label) }}
15
+ {{ expectNumber(index) }}
16
+ </template>
17
+
18
+ <template #after-option="{ option, index }">
19
+ {{ expectOptionalString(option.label) }}
20
+ {{ expectNumber(index) }}
21
+ </template>
22
+
23
+ <template #empty>empty</template>
24
+ </UDropdownLink>
25
+
26
+ <!-- Options from a non-literal source still expose the declared option shape. -->
27
+ <UDropdownLink :options="typedOptions">
28
+ <template #option="{ option }">{{ expectOptionalString(option.label) }}</template>
29
+ </UDropdownLink>
30
+
31
+ <UDropdownLink :options="options">
32
+ <template #option="{ option }">
33
+ <!-- @vue-expect-error `nonExistent` is not a key of the inferred option. -->
34
+ {{ option.nonExistent }}
35
+ </template>
36
+ </UDropdownLink>
37
+
38
+ <!-- `label` is `UDropdown`'s `displayLabel`, a join of `option[labelKey]` lookups. -->
39
+ <UDropdownLink :options="options">
40
+ <template #default="{ label }">
41
+ <!-- @vue-expect-error the label is `undefined` when it cannot be resolved. -->
42
+ {{ expectString(label) }}
43
+ </template>
44
+ </UDropdownLink>
45
+
46
+ <!-- Regression guard: an option slot must never promise the caller's members as present. -->
47
+ <UDropdownLink
48
+ :options="groupedOptions"
49
+ group-label-key="groupName"
50
+ group-value-key="children"
51
+ label-key="label"
52
+ >
53
+ <template #option="{ option }">
54
+ <!-- @vue-expect-error a flattened group member need not carry `label`. -->
55
+ {{ expectString(option.label) }}
56
+ </template>
57
+ <template #before-option="{ option }">
58
+ <!-- @vue-expect-error a flattened group member need not carry `qty`. -->
59
+ {{ expectNumber(option.qty) }}
60
+ </template>
61
+ <template #after-option="{ option }">
62
+ <!-- @vue-expect-error a flattened group member need not carry `groupName`. -->
63
+ {{ expectString(option.groupName) }}
64
+ </template>
65
+ </UDropdownLink>
66
+ </template>
67
+
68
+ <script setup lang="ts">
69
+ import { ref } from "vue";
70
+
71
+ import UDropdownLink from "../UDropdownLink.vue";
72
+
73
+ interface UserOption {
74
+ label: string;
75
+ value: string;
76
+ qty: number;
77
+ }
78
+
79
+ const options = [{ label: "a", value: "a", qty: 1 }];
80
+ const typedOptions = ref<UserOption[]>([]);
81
+
82
+ /* A group whose members lack the parent's members — exactly what `filterGroups` flattens. */
83
+ const groupedOptions = [
84
+ {
85
+ groupName: "Group A",
86
+ label: "Group A",
87
+ qty: 1,
88
+ children: [{ value: "c1", title: "Child" }],
89
+ },
90
+ ];
91
+
92
+ function expectString(value: string) {
93
+ return value;
94
+ }
95
+
96
+ function expectNumber(value: number) {
97
+ return value;
98
+ }
99
+
100
+ function expectOptionalString(value: string | undefined) {
101
+ return value;
102
+ }
103
+
104
+ function expectOptionalNumber(value: number | undefined) {
105
+ return value;
106
+ }
107
+ </script>
@@ -1,11 +1,25 @@
1
1
  import defaultConfig from "./config";
2
2
 
3
- import type { Option } from "../ui.form-listbox/types";
3
+ import type { ListboxOption, SlotOption } from "../ui.form-listbox/types";
4
4
  import type { ComponentConfig, UnknownObject } from "../types";
5
5
 
6
6
  export type Config = typeof defaultConfig;
7
7
 
8
- export interface Props {
8
+ /* Option slots are forwarded through `UDropdown` to `UListbox`, inheriting its group flattening —
9
+ a flattened member need not carry the parent's members, so `SlotOption` is `Partial`. */
10
+ export interface UDropdownLinkSlots<TOption extends ListboxOption = ListboxOption> {
11
+ /* `label` is `UDropdown`'s `displayLabel`, a join of `option[labelKey]` lookups — `undefined`
12
+ when the label cannot be resolved. */
13
+ default?: (props: { label: string | undefined; opened: boolean }) => unknown;
14
+ left?: (props: { opened: boolean }) => unknown;
15
+ toggle?: (props: { opened: boolean }) => unknown;
16
+ "before-option"?: (props: { option: SlotOption<TOption>; index: number }) => unknown;
17
+ option?: (props: { option: SlotOption<TOption>; index: number }) => unknown;
18
+ "after-option"?: (props: { option: SlotOption<TOption>; index: number }) => unknown;
19
+ empty?: () => unknown;
20
+ }
21
+
22
+ export interface Props<TOption extends ListboxOption = ListboxOption> {
9
23
  /**
10
24
  * Selected link.
11
25
  */
@@ -24,7 +38,7 @@ export interface Props {
24
38
  /**
25
39
  * Options list.
26
40
  */
27
- options?: Option[];
41
+ options?: TOption[];
28
42
 
29
43
  /**
30
44
  * Label key in the item object of options.
@@ -56,7 +56,7 @@ export default /*tw*/ {
56
56
  timepicker: "mt-2 pl-1 pt-3 text-medium flex items-stretch justify-between gap-2 border-t border-muted",
57
57
  timepickerLabel: "w-full self-center",
58
58
  timepickerInputWrapper: `
59
- flex items-center rounded-medium border border-solid border-default outline-solid outline-transparent
59
+ flex items-center rounded-medium border border-solid border-default outline-solid outline-small outline-transparent
60
60
  hover:focus-within:border-primary focus-within:border-primary
61
61
  focus-within:outline focus-within:outline-small focus-within:outline-primary
62
62
  `,
@@ -12,7 +12,7 @@ export default /*tw*/ {
12
12
  },
13
13
  checkbox: {
14
14
  base: `
15
- {>checkboxBase} ml-0 bg-default border border-solid border-default outline-solid outline-transparent
15
+ {>checkboxBase} ml-0 bg-default border border-solid border-default outline-solid outline-medium outline-transparent
16
16
  appearance-none p-0 print:color-adjust-exact inline-block align-middle bg-origin-border select-none shrink-0
17
17
  hover:border-lifted
18
18
  active:border-{color} active:bg-{color}/15
@@ -33,7 +33,7 @@ describe("UCheckbox.vue", () => {
33
33
  it("Value – returns correct value type when checkbox is checked", async () => {
34
34
  const testValues = ["string-value", 42, true, { id: 1, name: "test" }, [1, 2, 3]];
35
35
 
36
- testValues.forEach(async (testValue) => {
36
+ for (const testValue of testValues) {
37
37
  const component = mount(UCheckbox, {
38
38
  props: {
39
39
  value: testValue,
@@ -48,7 +48,7 @@ describe("UCheckbox.vue", () => {
48
48
  const emittedValues = component.emitted("update:modelValue");
49
49
 
50
50
  expect(emittedValues![0][0]).toEqual([testValue]);
51
- });
51
+ }
52
52
  });
53
53
 
54
54
  it("TrueValue and FalseValue – returns correct values when checkbox is checked and unchecked", async () => {
@@ -59,7 +59,7 @@ describe("UCheckbox.vue", () => {
59
59
  { trueValue: true, falseValue: false },
60
60
  ];
61
61
 
62
- testValuePairs.forEach(async ({ trueValue, falseValue }) => {
62
+ for (const { trueValue, falseValue } of testValuePairs) {
63
63
  const component = mount(UCheckbox, {
64
64
  props: {
65
65
  trueValue,
@@ -81,7 +81,7 @@ describe("UCheckbox.vue", () => {
81
81
  emittedValues = component.emitted("update:modelValue");
82
82
 
83
83
  expect(emittedValues![1][0]).toEqual(falseValue);
84
- });
84
+ }
85
85
  });
86
86
 
87
87
  it("Partial – displays correct icon when checkbox is partially checked", () => {
@@ -4,16 +4,18 @@ import type { ComponentConfig, UnknownObject, UnknownArray } from "../types";
4
4
 
5
5
  export type Config = typeof defaultConfig;
6
6
 
7
- export interface UCheckboxOption {
7
+ export interface BaseCheckboxOption {
8
8
  value?: string | number | boolean | UnknownObject | UnknownArray;
9
9
  falseValue?: string | number | boolean | UnknownObject | UnknownArray;
10
10
  trueValue?: string | number | boolean | UnknownObject | UnknownArray;
11
11
  label?: string;
12
12
  icon?: string;
13
13
  description?: string;
14
- [key: string]: string | number | boolean | UnknownObject | UnknownArray | undefined;
15
14
  }
16
15
 
16
+ /* Any object shape is a valid option, only reserved option keys are type checked. */
17
+ export type UCheckboxOption = BaseCheckboxOption & (object | UnknownObject);
18
+
17
19
  export interface Props {
18
20
  /**
19
21
  * Checkbox value.
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { provide, ref, watch, useTemplateRef } from "vue";
2
+ import { computed, provide, ref, watch, useTemplateRef } from "vue";
3
3
  import { isEqual } from "lodash-es";
4
4
 
5
5
  import { useUI } from "../composables/useUI";
@@ -12,6 +12,7 @@ import { COMPONENT_NAME } from "./constants";
12
12
  import defaultConfig from "./config";
13
13
 
14
14
  import type { UnknownObject } from "../types";
15
+ import type { BaseCheckboxOption } from "../ui.form-checkbox/types";
15
16
  import type { Props, Config } from "./types";
16
17
 
17
18
  defineOptions({ inheritAttrs: false });
@@ -35,6 +36,11 @@ const listRef = useTemplateRef<HTMLDivElement>("list");
35
36
 
36
37
  const checkedItems = ref([] as UnknownObject[]);
37
38
 
39
+ /* Indexable view of the options for `labelKey` / `valueKey` lookups. */
40
+ const keyedOptions = computed(
41
+ () => props.options as (BaseCheckboxOption & Record<string, BaseCheckboxOption["value"]>)[],
42
+ );
43
+
38
44
  provide<(value: UnknownObject[]) => void>(
39
45
  "setCheckboxGroupCheckedItems",
40
46
  (value: UnknownObject[]) => {
@@ -116,7 +122,7 @@ const { getDataTest, groupLabelAttrs, groupCheckboxAttrs, listAttrs } =
116
122
  <!-- @slot Use it to add UCheckbox directly. -->
117
123
  <slot>
118
124
  <UCheckbox
119
- v-for="(option, index) in options"
125
+ v-for="(option, index) in keyedOptions"
120
126
  :key="index"
121
127
  :model-value="modelValue"
122
128
  :value="option[valueKey]"
@@ -119,6 +119,16 @@ function deactivate() {
119
119
  customView.value = View.Day;
120
120
  }
121
121
 
122
+ function deactivateOnBlur(event: FocusEvent) {
123
+ const related = event.relatedTarget as HTMLElement | null;
124
+
125
+ if (related && wrapperRef.value?.contains(related)) {
126
+ return;
127
+ }
128
+
129
+ deactivate();
130
+ }
131
+
122
132
  function onUserFormatDateChange(value: string) {
123
133
  userFormatDate.value = value.trim();
124
134
  }
@@ -279,7 +289,7 @@ watchEffect(() => {
279
289
  </script>
280
290
 
281
291
  <template>
282
- <div v-bind="wrapperAttrs" ref="wrapper" :data-test="getDataTest()">
292
+ <div v-bind="wrapperAttrs" ref="wrapper" :data-test="getDataTest()" @focusout="deactivateOnBlur">
283
293
  <UInput
284
294
  :id="elementId"
285
295
  :key="String(userFormatDate)"
@@ -356,6 +356,53 @@ describe("UDatePicker.vue", () => {
356
356
  expect(calendar.isVisible()).toBe(true);
357
357
  });
358
358
 
359
+ it("Calendar – hides when focus leaves the whole component", async () => {
360
+ const component = mount(UDatePicker, {
361
+ props: {
362
+ modelValue: new Date(),
363
+ },
364
+ attachTo: document.body,
365
+ });
366
+
367
+ const input = component.getComponent(UInput).find("input");
368
+ const calendar = component.findComponent({ name: "UCalendar" });
369
+
370
+ await input.trigger("focus");
371
+ expect(calendar.isVisible()).toBe(true);
372
+
373
+ const outsideElement = document.createElement("button");
374
+
375
+ document.body.appendChild(outsideElement);
376
+
377
+ await component.trigger("focusout", { relatedTarget: outsideElement });
378
+
379
+ expect(calendar.isVisible()).toBe(false);
380
+
381
+ outsideElement.remove();
382
+ component.unmount();
383
+ });
384
+
385
+ it("Calendar – stays open when focus moves inside the component", async () => {
386
+ const component = mount(UDatePicker, {
387
+ props: {
388
+ modelValue: new Date(),
389
+ },
390
+ attachTo: document.body,
391
+ });
392
+
393
+ const input = component.getComponent(UInput).find("input");
394
+ const calendar = component.findComponent({ name: "UCalendar" });
395
+
396
+ await input.trigger("focus");
397
+ expect(calendar.isVisible()).toBe(true);
398
+
399
+ await component.trigger("focusout", { relatedTarget: calendar.element });
400
+
401
+ expect(calendar.isVisible()).toBe(true);
402
+
403
+ component.unmount();
404
+ });
405
+
359
406
  it("Calendar – hides when escape key is pressed on calendar", async () => {
360
407
  const component = mount(UDatePicker, {
361
408
  props: {
@@ -12,7 +12,7 @@ export default /*tw*/ {
12
12
  wrapper: {
13
13
  base: `
14
14
  flex gap-3 w-full px-3 relative bg-default transition
15
- border border-solid rounded-medium border-default outline-solid outline-transparent
15
+ border border-solid rounded-medium border-default outline-solid outline-small outline-transparent
16
16
  hover:border-lifted hover:focus-within:border-primary focus-within:border-primary
17
17
  focus-within:outline focus-within:outline-small focus-within:outline-primary focus-within:transition
18
18
  `,
@@ -3,7 +3,7 @@ export default /*tw*/ {
3
3
  dropzone: {
4
4
  base: `
5
5
  p-3 size-auto w-full bg-default transition
6
- rounded-medium border border-solid border-default outline-solid outline-transparent
6
+ rounded-medium border border-solid border-default outline-solid outline-small outline-transparent
7
7
  hover:border-lifted hover:focus-within:border-primary focus-within:border-primary
8
8
  focus-within:outline focus-within:outline-small focus-within:outline-primary
9
9
  `,