@aleph-alpha/ui-library 1.13.0 → 1.15.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 (137) hide show
  1. package/README.md +1 -1
  2. package/config.d.ts +18 -5
  3. package/config.js +95 -5
  4. package/dist/system/index.d.ts +1718 -236
  5. package/dist/system/lib.js +19726 -16529
  6. package/docs/public-docs/component-directory.md +13 -0
  7. package/docs/public-docs/contributing.md +11 -0
  8. package/docs/public-docs/external-links.md +16 -0
  9. package/docs/public-docs/foundations.md +25 -0
  10. package/docs/public-docs/getting-started-designers.md +17 -0
  11. package/docs/public-docs/index.md +5 -0
  12. package/docs/public-docs/quick-start.md +230 -0
  13. package/docs/public-docs/standards-guidelines.md +15 -0
  14. package/package.json +3 -2
  15. package/src/components/UiCalendar/UiCalendar.stories.ts +1 -4
  16. package/src/components/UiCalendar/types.ts +1 -1
  17. package/src/components/UiKbd/UiKbd.stories.ts +1 -1
  18. package/src/components/UiNavigationMenu/UiNavigationMenu.stories.ts +1196 -0
  19. package/src/components/UiNavigationMenu/UiNavigationMenu.vue +39 -0
  20. package/src/components/UiNavigationMenu/UiNavigationMenuContent.vue +25 -0
  21. package/src/components/UiNavigationMenu/UiNavigationMenuIndicator.vue +14 -0
  22. package/src/components/UiNavigationMenu/UiNavigationMenuItem.vue +16 -0
  23. package/src/components/UiNavigationMenu/UiNavigationMenuLink.vue +27 -0
  24. package/src/components/UiNavigationMenu/UiNavigationMenuList.vue +16 -0
  25. package/src/components/UiNavigationMenu/UiNavigationMenuTrigger.vue +16 -0
  26. package/src/components/UiNavigationMenu/__tests__/UiNavigationMenu.test.ts +428 -0
  27. package/src/components/UiNavigationMenu/index.ts +11 -0
  28. package/src/components/UiNavigationMenu/types.ts +185 -0
  29. package/src/components/UiRangeCalendar/UiRangeCalendar.stories.ts +2 -4
  30. package/src/components/UiRangeCalendar/types.ts +1 -1
  31. package/src/components/UiSheet/UiSheet.stories.ts +715 -0
  32. package/src/components/UiSheet/__tests__/UiSheet.test.ts +229 -0
  33. package/src/components/UiSheet/index.ts +12 -0
  34. package/src/components/UiSheet/types.ts +83 -0
  35. package/src/components/UiSidebar/UiSidebar.stories.ts +1188 -0
  36. package/src/components/UiSidebar/UiSidebar.vue +20 -0
  37. package/src/components/UiSidebar/UiSidebarGroupAction.vue +18 -0
  38. package/src/components/UiSidebar/UiSidebarGroupLabel.vue +18 -0
  39. package/src/components/UiSidebar/UiSidebarHeaderTrigger.vue +53 -0
  40. package/src/components/UiSidebar/UiSidebarInput.vue +14 -0
  41. package/src/components/UiSidebar/UiSidebarMenuAction.vue +19 -0
  42. package/src/components/UiSidebar/UiSidebarMenuButton.vue +27 -0
  43. package/src/components/UiSidebar/UiSidebarMenuSkeleton.vue +16 -0
  44. package/src/components/UiSidebar/UiSidebarMenuSubButton.vue +24 -0
  45. package/src/components/UiSidebar/UiSidebarProvider.vue +16 -0
  46. package/src/components/UiSidebar/UiSidebarSeparator.vue +13 -0
  47. package/src/components/UiSidebar/__tests__/UiSidebar.test.ts +221 -0
  48. package/src/components/UiSidebar/index.ts +34 -0
  49. package/src/components/UiSidebar/types.ts +165 -0
  50. package/src/components/UiStepper/UiStepper.stories.ts +425 -0
  51. package/src/components/UiStepper/UiStepper.vue +27 -0
  52. package/src/components/UiStepper/UiStepperDescription.vue +20 -0
  53. package/src/components/UiStepper/UiStepperIndicator.vue +13 -0
  54. package/src/components/UiStepper/UiStepperItem.vue +25 -0
  55. package/src/components/UiStepper/UiStepperSeparator.vue +17 -0
  56. package/src/components/UiStepper/UiStepperTitle.vue +19 -0
  57. package/src/components/UiStepper/UiStepperTrigger.vue +18 -0
  58. package/src/components/UiStepper/__tests__/UiStepper.test.ts +167 -0
  59. package/src/components/UiStepper/index.ts +9 -0
  60. package/src/components/UiStepper/types.ts +65 -0
  61. package/src/components/core/alert/index.ts +2 -2
  62. package/src/components/core/alert-dialog/AlertDialogContent.vue +1 -1
  63. package/src/components/core/calendar/Calendar.vue +1 -1
  64. package/src/components/core/card/Card.vue +1 -1
  65. package/src/components/core/drawer/DrawerContent.vue +1 -1
  66. package/src/components/core/dropdown-menu/DropdownMenuContent.vue +1 -1
  67. package/src/components/core/dropdown-menu/DropdownMenuSubContent.vue +1 -1
  68. package/src/components/core/input/Input.vue +1 -1
  69. package/src/components/core/native-select/NativeSelect.vue +1 -1
  70. package/src/components/core/native-select/NativeSelectOptGroup.vue +1 -1
  71. package/src/components/core/native-select/NativeSelectOption.vue +1 -1
  72. package/src/components/core/navigation-menu/NavigationMenu.vue +40 -0
  73. package/src/components/core/navigation-menu/NavigationMenuContent.vue +28 -0
  74. package/src/components/core/navigation-menu/NavigationMenuIndicator.vue +26 -0
  75. package/src/components/core/navigation-menu/NavigationMenuItem.vue +19 -0
  76. package/src/components/core/navigation-menu/NavigationMenuLink.vue +27 -0
  77. package/src/components/core/navigation-menu/NavigationMenuList.vue +21 -0
  78. package/src/components/core/navigation-menu/NavigationMenuTrigger.vue +27 -0
  79. package/src/components/core/navigation-menu/NavigationMenuViewport.vue +26 -0
  80. package/src/components/core/navigation-menu/index.ts +14 -0
  81. package/src/components/core/popover/PopoverContent.vue +1 -1
  82. package/src/components/core/range-calendar/RangeCalendar.vue +1 -1
  83. package/src/components/core/select/SelectContent.vue +1 -1
  84. package/src/components/core/select/SelectTrigger.vue +1 -1
  85. package/src/components/core/sheet/Sheet.vue +15 -0
  86. package/src/components/core/sheet/SheetClose.vue +12 -0
  87. package/src/components/core/sheet/SheetContent.vue +56 -0
  88. package/src/components/core/sheet/SheetDescription.vue +19 -0
  89. package/src/components/core/sheet/SheetFooter.vue +9 -0
  90. package/src/components/core/sheet/SheetHeader.vue +9 -0
  91. package/src/components/core/sheet/SheetOverlay.vue +24 -0
  92. package/src/components/core/sheet/SheetTitle.vue +19 -0
  93. package/src/components/core/sheet/SheetTrigger.vue +12 -0
  94. package/src/components/core/sheet/index.ts +8 -0
  95. package/src/components/core/sidebar/Sidebar.vue +105 -0
  96. package/src/components/core/sidebar/SidebarContent.vue +21 -0
  97. package/src/components/core/sidebar/SidebarFooter.vue +16 -0
  98. package/src/components/core/sidebar/SidebarGroup.vue +16 -0
  99. package/src/components/core/sidebar/SidebarGroupAction.vue +25 -0
  100. package/src/components/core/sidebar/SidebarGroupContent.vue +16 -0
  101. package/src/components/core/sidebar/SidebarGroupLabel.vue +23 -0
  102. package/src/components/core/sidebar/SidebarHeader.vue +16 -0
  103. package/src/components/core/sidebar/SidebarInput.vue +17 -0
  104. package/src/components/core/sidebar/SidebarInset.vue +21 -0
  105. package/src/components/core/sidebar/SidebarMenu.vue +16 -0
  106. package/src/components/core/sidebar/SidebarMenuAction.vue +33 -0
  107. package/src/components/core/sidebar/SidebarMenuBadge.vue +26 -0
  108. package/src/components/core/sidebar/SidebarMenuButton.vue +49 -0
  109. package/src/components/core/sidebar/SidebarMenuButtonChild.vue +36 -0
  110. package/src/components/core/sidebar/SidebarMenuItem.vue +16 -0
  111. package/src/components/core/sidebar/SidebarMenuSkeleton.vue +32 -0
  112. package/src/components/core/sidebar/SidebarMenuSub.vue +22 -0
  113. package/src/components/core/sidebar/SidebarMenuSubButton.vue +38 -0
  114. package/src/components/core/sidebar/SidebarMenuSubItem.vue +16 -0
  115. package/src/components/core/sidebar/SidebarProvider.vue +102 -0
  116. package/src/components/core/sidebar/SidebarRail.vue +33 -0
  117. package/src/components/core/sidebar/SidebarSeparator.vue +17 -0
  118. package/src/components/core/sidebar/SidebarTrigger.vue +25 -0
  119. package/src/components/core/sidebar/index.ts +58 -0
  120. package/src/components/core/sidebar/utils.ts +19 -0
  121. package/src/components/core/stepper/Stepper.vue +20 -0
  122. package/src/components/core/stepper/StepperDescription.vue +23 -0
  123. package/src/components/core/stepper/StepperIndicator.vue +34 -0
  124. package/src/components/core/stepper/StepperItem.vue +23 -0
  125. package/src/components/core/stepper/StepperSeparator.vue +29 -0
  126. package/src/components/core/stepper/StepperTitle.vue +24 -0
  127. package/src/components/core/stepper/StepperTrigger.vue +22 -0
  128. package/src/components/core/stepper/index.ts +7 -0
  129. package/src/components/core/tabs/TabsTrigger.vue +1 -1
  130. package/src/components/core/tags-input/TagsInput.vue +1 -1
  131. package/src/components/core/textarea/Textarea.vue +1 -1
  132. package/src/components/index.ts +4 -0
  133. package/src/patterns/UiDatePicker/UiDatePicker.stories.ts +1 -4
  134. package/src/patterns/UiDatePicker/types.ts +1 -1
  135. package/src/theme/Background.stories.ts +84 -35
  136. package/src/theme/Extended.stories.ts +4 -4
  137. package/tokens.json +145 -8
@@ -0,0 +1,39 @@
1
+ <script setup lang="ts">
2
+ import { NavigationMenu as ShadcnNavigationMenu } from '@/components/core/navigation-menu';
3
+ import type { UiNavigationMenuProps, UiNavigationMenuEmits } from './types';
4
+
5
+ defineOptions({
6
+ name: 'UiNavigationMenu',
7
+ });
8
+
9
+ const props = withDefaults(defineProps<UiNavigationMenuProps>(), {
10
+ modelValue: undefined,
11
+ defaultValue: undefined,
12
+ orientation: 'horizontal',
13
+ dir: 'ltr',
14
+ delayDuration: 200,
15
+ skipDelayDuration: 300,
16
+ disableClickTrigger: false,
17
+ disableHoverTrigger: false,
18
+ viewport: true,
19
+ });
20
+
21
+ const emit = defineEmits<UiNavigationMenuEmits>();
22
+ </script>
23
+
24
+ <template>
25
+ <ShadcnNavigationMenu
26
+ :model-value="props.modelValue"
27
+ :default-value="props.defaultValue"
28
+ :orientation="props.orientation"
29
+ :dir="props.dir"
30
+ :delay-duration="props.delayDuration"
31
+ :skip-delay-duration="props.skipDelayDuration"
32
+ :disable-click-trigger="props.disableClickTrigger"
33
+ :disable-hover-trigger="props.disableHoverTrigger"
34
+ :viewport="props.viewport"
35
+ @update:model-value="emit('update:modelValue', $event)"
36
+ >
37
+ <slot />
38
+ </ShadcnNavigationMenu>
39
+ </template>
@@ -0,0 +1,25 @@
1
+ <script setup lang="ts">
2
+ import { NavigationMenuContent as ShadcnNavigationMenuContent } from '@/components/core/navigation-menu';
3
+ import type { UiNavigationMenuContentProps, UiNavigationMenuContentEmits } from './types';
4
+
5
+ defineOptions({
6
+ name: 'UiNavigationMenuContent',
7
+ });
8
+
9
+ const props = defineProps<UiNavigationMenuContentProps>();
10
+
11
+ const emit = defineEmits<UiNavigationMenuContentEmits>();
12
+ </script>
13
+
14
+ <template>
15
+ <ShadcnNavigationMenuContent
16
+ :force-mount="props.forceMount ? true : undefined"
17
+ :disable-outside-pointer-events="props.disableOutsidePointerEvents"
18
+ @escape-key-down="emit('escapeKeyDown', $event)"
19
+ @pointer-down-outside="emit('outsideClick', $event)"
20
+ @focus-outside="emit('outsideFocus', $event)"
21
+ @interact-outside="emit('outsideInteract', $event)"
22
+ >
23
+ <slot />
24
+ </ShadcnNavigationMenuContent>
25
+ </template>
@@ -0,0 +1,14 @@
1
+ <script setup lang="ts">
2
+ import { NavigationMenuIndicator as ShadcnNavigationMenuIndicator } from '@/components/core/navigation-menu';
3
+ import type { UiNavigationMenuIndicatorProps } from './types';
4
+
5
+ defineOptions({
6
+ name: 'UiNavigationMenuIndicator',
7
+ });
8
+
9
+ const props = defineProps<UiNavigationMenuIndicatorProps>();
10
+ </script>
11
+
12
+ <template>
13
+ <ShadcnNavigationMenuIndicator :force-mount="props.forceMount ? true : undefined" />
14
+ </template>
@@ -0,0 +1,16 @@
1
+ <script setup lang="ts">
2
+ import { NavigationMenuItem as ShadcnNavigationMenuItem } from '@/components/core/navigation-menu';
3
+ import type { UiNavigationMenuItemProps } from './types';
4
+
5
+ defineOptions({
6
+ name: 'UiNavigationMenuItem',
7
+ });
8
+
9
+ const props = defineProps<UiNavigationMenuItemProps>();
10
+ </script>
11
+
12
+ <template>
13
+ <ShadcnNavigationMenuItem :value="props.value" :as-child="props.asChild">
14
+ <slot />
15
+ </ShadcnNavigationMenuItem>
16
+ </template>
@@ -0,0 +1,27 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue';
3
+ import { NavigationMenuLink as ShadcnNavigationMenuLink } from '@/components/core/navigation-menu';
4
+ import type { UiNavigationMenuLinkProps, UiNavigationMenuLinkEmits } from './types';
5
+
6
+ defineOptions({
7
+ name: 'UiNavigationMenuLink',
8
+ });
9
+
10
+ const props = defineProps<UiNavigationMenuLinkProps>();
11
+
12
+ const emit = defineEmits<UiNavigationMenuLinkEmits>();
13
+
14
+ // Compute aria-current value: 'page' when active, undefined otherwise
15
+ const ariaCurrent = computed(() => (props.active ? 'page' : undefined));
16
+ </script>
17
+
18
+ <template>
19
+ <ShadcnNavigationMenuLink
20
+ :active="props.active"
21
+ :as-child="props.asChild"
22
+ :aria-current="ariaCurrent"
23
+ @select="emit('select', $event)"
24
+ >
25
+ <slot />
26
+ </ShadcnNavigationMenuLink>
27
+ </template>
@@ -0,0 +1,16 @@
1
+ <script setup lang="ts">
2
+ import { NavigationMenuList as ShadcnNavigationMenuList } from '@/components/core/navigation-menu';
3
+ import type { UiNavigationMenuListProps } from './types';
4
+
5
+ defineOptions({
6
+ name: 'UiNavigationMenuList',
7
+ });
8
+
9
+ const props = defineProps<UiNavigationMenuListProps>();
10
+ </script>
11
+
12
+ <template>
13
+ <ShadcnNavigationMenuList :as-child="props.asChild">
14
+ <slot />
15
+ </ShadcnNavigationMenuList>
16
+ </template>
@@ -0,0 +1,16 @@
1
+ <script setup lang="ts">
2
+ import { NavigationMenuTrigger as ShadcnNavigationMenuTrigger } from '@/components/core/navigation-menu';
3
+ import type { UiNavigationMenuTriggerProps } from './types';
4
+
5
+ defineOptions({
6
+ name: 'UiNavigationMenuTrigger',
7
+ });
8
+
9
+ const props = defineProps<UiNavigationMenuTriggerProps>();
10
+ </script>
11
+
12
+ <template>
13
+ <ShadcnNavigationMenuTrigger :disabled="props.disabled" :as-child="props.asChild">
14
+ <slot />
15
+ </ShadcnNavigationMenuTrigger>
16
+ </template>
@@ -0,0 +1,428 @@
1
+ import { render, waitFor } from '@testing-library/vue';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { describe, expect, test, vi } from 'vitest';
4
+ import { ref } from 'vue';
5
+ import {
6
+ UiNavigationMenu,
7
+ UiNavigationMenuList,
8
+ UiNavigationMenuItem,
9
+ UiNavigationMenuTrigger,
10
+ UiNavigationMenuContent,
11
+ UiNavigationMenuLink,
12
+ UiNavigationMenuIndicator,
13
+ } from '../index';
14
+
15
+ // Helper to render a basic navigation menu
16
+ function renderNavigationMenu(
17
+ options: {
18
+ triggerText?: string;
19
+ links?: string[];
20
+ modelValue?: string;
21
+ defaultValue?: string;
22
+ disableHoverTrigger?: boolean;
23
+ disableClickTrigger?: boolean;
24
+ itemValue?: string;
25
+ } = {},
26
+ ) {
27
+ const {
28
+ triggerText = 'Products',
29
+ links = ['Link 1', 'Link 2'],
30
+ modelValue,
31
+ defaultValue,
32
+ disableHoverTrigger = false,
33
+ disableClickTrigger = false,
34
+ itemValue = 'products',
35
+ } = options;
36
+
37
+ return render({
38
+ components: {
39
+ UiNavigationMenu,
40
+ UiNavigationMenuList,
41
+ UiNavigationMenuItem,
42
+ UiNavigationMenuTrigger,
43
+ UiNavigationMenuContent,
44
+ UiNavigationMenuLink,
45
+ },
46
+ template: `
47
+ <UiNavigationMenu
48
+ ${modelValue !== undefined ? ':model-value="modelValue"' : ''}
49
+ ${defaultValue !== undefined ? ':default-value="defaultValue"' : ''}
50
+ :disable-hover-trigger="disableHoverTrigger"
51
+ :disable-click-trigger="disableClickTrigger"
52
+ >
53
+ <UiNavigationMenuList>
54
+ <UiNavigationMenuItem :value="itemValue">
55
+ <UiNavigationMenuTrigger>
56
+ {{ triggerText }}
57
+ </UiNavigationMenuTrigger>
58
+ <UiNavigationMenuContent>
59
+ <UiNavigationMenuLink v-for="link in links" :key="link">
60
+ {{ link }}
61
+ </UiNavigationMenuLink>
62
+ </UiNavigationMenuContent>
63
+ </UiNavigationMenuItem>
64
+ </UiNavigationMenuList>
65
+ </UiNavigationMenu>
66
+ `,
67
+ setup() {
68
+ return {
69
+ triggerText,
70
+ links,
71
+ modelValue,
72
+ defaultValue,
73
+ disableHoverTrigger,
74
+ disableClickTrigger,
75
+ itemValue,
76
+ };
77
+ },
78
+ });
79
+ }
80
+
81
+ describe('UiNavigationMenu', () => {
82
+ test('supports controlled modelValue', async () => {
83
+ const { getByText } = render({
84
+ components: {
85
+ UiNavigationMenu,
86
+ UiNavigationMenuList,
87
+ UiNavigationMenuItem,
88
+ UiNavigationMenuTrigger,
89
+ UiNavigationMenuContent,
90
+ UiNavigationMenuLink,
91
+ },
92
+ template: `
93
+ <UiNavigationMenu :model-value="modelValue">
94
+ <UiNavigationMenuList>
95
+ <UiNavigationMenuItem value="products">
96
+ <UiNavigationMenuTrigger>Products</UiNavigationMenuTrigger>
97
+ <UiNavigationMenuContent>
98
+ <UiNavigationMenuLink>Product Link</UiNavigationMenuLink>
99
+ </UiNavigationMenuContent>
100
+ </UiNavigationMenuItem>
101
+ </UiNavigationMenuList>
102
+ </UiNavigationMenu>
103
+ `,
104
+ setup() {
105
+ const modelValue = ref('products');
106
+ return { modelValue };
107
+ },
108
+ });
109
+
110
+ await waitFor(() => {
111
+ expect(getByText('Product Link')).toBeVisible();
112
+ });
113
+ });
114
+
115
+ test('suppresses viewport rendering when viewport is false', () => {
116
+ const { container } = render({
117
+ components: {
118
+ UiNavigationMenu,
119
+ UiNavigationMenuList,
120
+ UiNavigationMenuItem,
121
+ UiNavigationMenuTrigger,
122
+ UiNavigationMenuContent,
123
+ },
124
+ template: `
125
+ <UiNavigationMenu :viewport="false">
126
+ <UiNavigationMenuList>
127
+ <UiNavigationMenuItem>
128
+ <UiNavigationMenuTrigger>Products</UiNavigationMenuTrigger>
129
+ <UiNavigationMenuContent>Content</UiNavigationMenuContent>
130
+ </UiNavigationMenuItem>
131
+ </UiNavigationMenuList>
132
+ </UiNavigationMenu>
133
+ `,
134
+ });
135
+
136
+ expect(
137
+ container.querySelector('[data-slot="navigation-menu-viewport"]'),
138
+ ).not.toBeInTheDocument();
139
+ });
140
+
141
+ test('emits update:modelValue when item changes', async () => {
142
+ const user = userEvent.setup();
143
+ const onUpdateModelValue = vi.fn();
144
+
145
+ const { getByRole } = render({
146
+ components: {
147
+ UiNavigationMenu,
148
+ UiNavigationMenuList,
149
+ UiNavigationMenuItem,
150
+ UiNavigationMenuTrigger,
151
+ UiNavigationMenuContent,
152
+ UiNavigationMenuLink,
153
+ },
154
+ template: `
155
+ <UiNavigationMenu @update:model-value="onUpdateModelValue" :disable-hover-trigger="true">
156
+ <UiNavigationMenuList>
157
+ <UiNavigationMenuItem value="products">
158
+ <UiNavigationMenuTrigger>Products</UiNavigationMenuTrigger>
159
+ <UiNavigationMenuContent>
160
+ <UiNavigationMenuLink>Link</UiNavigationMenuLink>
161
+ </UiNavigationMenuContent>
162
+ </UiNavigationMenuItem>
163
+ </UiNavigationMenuList>
164
+ </UiNavigationMenu>
165
+ `,
166
+ setup() {
167
+ return { onUpdateModelValue };
168
+ },
169
+ });
170
+
171
+ await user.click(getByRole('button', { name: 'Products' }));
172
+
173
+ await waitFor(() => {
174
+ expect(onUpdateModelValue).toHaveBeenCalledWith('products');
175
+ });
176
+ });
177
+ });
178
+
179
+ describe('UiNavigationMenuTrigger', () => {
180
+ test('forwards disableHoverTrigger — click opens when hover is disabled', async () => {
181
+ const user = userEvent.setup();
182
+ const { getByRole, getByText, queryByText } = renderNavigationMenu({
183
+ disableHoverTrigger: true,
184
+ });
185
+
186
+ // Hover should not open the menu
187
+ await user.hover(getByRole('button', { name: 'Products' }));
188
+ expect(queryByText('Link 1')).not.toBeInTheDocument();
189
+
190
+ // Click should open the menu
191
+ await user.click(getByRole('button', { name: 'Products' }));
192
+
193
+ await waitFor(() => {
194
+ expect(getByText('Link 1')).toBeVisible();
195
+ });
196
+ });
197
+
198
+ test('forwards disabled prop', () => {
199
+ const { getByRole } = render({
200
+ components: {
201
+ UiNavigationMenu,
202
+ UiNavigationMenuList,
203
+ UiNavigationMenuItem,
204
+ UiNavigationMenuTrigger,
205
+ UiNavigationMenuContent,
206
+ },
207
+ template: `
208
+ <UiNavigationMenu>
209
+ <UiNavigationMenuList>
210
+ <UiNavigationMenuItem>
211
+ <UiNavigationMenuTrigger :disabled="true">Disabled</UiNavigationMenuTrigger>
212
+ <UiNavigationMenuContent>Content</UiNavigationMenuContent>
213
+ </UiNavigationMenuItem>
214
+ </UiNavigationMenuList>
215
+ </UiNavigationMenu>
216
+ `,
217
+ });
218
+
219
+ expect(getByRole('button', { name: 'Disabled' })).toBeDisabled();
220
+ });
221
+ });
222
+
223
+ describe('UiNavigationMenuContent', () => {
224
+ test('content is present in DOM without interaction when forceMount is true', () => {
225
+ const { getByText } = render({
226
+ components: {
227
+ UiNavigationMenu,
228
+ UiNavigationMenuList,
229
+ UiNavigationMenuItem,
230
+ UiNavigationMenuTrigger,
231
+ UiNavigationMenuContent,
232
+ UiNavigationMenuLink,
233
+ },
234
+ template: `
235
+ <UiNavigationMenu>
236
+ <UiNavigationMenuList>
237
+ <UiNavigationMenuItem value="products">
238
+ <UiNavigationMenuTrigger>Products</UiNavigationMenuTrigger>
239
+ <UiNavigationMenuContent :force-mount="true">
240
+ <UiNavigationMenuLink>Force Mounted Link</UiNavigationMenuLink>
241
+ </UiNavigationMenuContent>
242
+ </UiNavigationMenuItem>
243
+ </UiNavigationMenuList>
244
+ </UiNavigationMenu>
245
+ `,
246
+ });
247
+
248
+ expect(getByText('Force Mounted Link')).toBeInTheDocument();
249
+ });
250
+
251
+ test('forwards escapeKeyDown event', async () => {
252
+ const user = userEvent.setup();
253
+ const onEscapeKeyDown = vi.fn();
254
+
255
+ const { getByRole, getByText } = render({
256
+ components: {
257
+ UiNavigationMenu,
258
+ UiNavigationMenuList,
259
+ UiNavigationMenuItem,
260
+ UiNavigationMenuTrigger,
261
+ UiNavigationMenuContent,
262
+ UiNavigationMenuLink,
263
+ },
264
+ template: `
265
+ <UiNavigationMenu>
266
+ <UiNavigationMenuList>
267
+ <UiNavigationMenuItem value="products">
268
+ <UiNavigationMenuTrigger>Products</UiNavigationMenuTrigger>
269
+ <UiNavigationMenuContent @escape-key-down="onEscapeKeyDown">
270
+ <UiNavigationMenuLink>Link</UiNavigationMenuLink>
271
+ </UiNavigationMenuContent>
272
+ </UiNavigationMenuItem>
273
+ </UiNavigationMenuList>
274
+ </UiNavigationMenu>
275
+ `,
276
+ setup() {
277
+ return { onEscapeKeyDown };
278
+ },
279
+ });
280
+
281
+ await user.hover(getByRole('button', { name: 'Products' }));
282
+ await waitFor(() => expect(getByText('Link')).toBeVisible());
283
+
284
+ await user.keyboard('{Escape}');
285
+
286
+ expect(onEscapeKeyDown).toHaveBeenCalled();
287
+ });
288
+ });
289
+
290
+ describe('UiNavigationMenuLink', () => {
291
+ test('forwards active prop as data-active attribute', async () => {
292
+ const user = userEvent.setup();
293
+ const { getByRole, getByText } = render({
294
+ components: {
295
+ UiNavigationMenu,
296
+ UiNavigationMenuList,
297
+ UiNavigationMenuItem,
298
+ UiNavigationMenuTrigger,
299
+ UiNavigationMenuContent,
300
+ UiNavigationMenuLink,
301
+ },
302
+ template: `
303
+ <UiNavigationMenu>
304
+ <UiNavigationMenuList>
305
+ <UiNavigationMenuItem value="products">
306
+ <UiNavigationMenuTrigger>Products</UiNavigationMenuTrigger>
307
+ <UiNavigationMenuContent>
308
+ <UiNavigationMenuLink :active="true">Active Link</UiNavigationMenuLink>
309
+ <UiNavigationMenuLink :active="false">Inactive Link</UiNavigationMenuLink>
310
+ </UiNavigationMenuContent>
311
+ </UiNavigationMenuItem>
312
+ </UiNavigationMenuList>
313
+ </UiNavigationMenu>
314
+ `,
315
+ });
316
+
317
+ await user.hover(getByRole('button', { name: 'Products' }));
318
+
319
+ await waitFor(() => {
320
+ expect(getByText('Active Link')).toHaveAttribute('data-active');
321
+ expect(getByText('Inactive Link')).not.toHaveAttribute('data-active');
322
+ });
323
+ });
324
+
325
+ test('forwards select event', async () => {
326
+ const user = userEvent.setup();
327
+ const onSelect = vi.fn();
328
+
329
+ const { getByRole, getByText } = render({
330
+ components: {
331
+ UiNavigationMenu,
332
+ UiNavigationMenuList,
333
+ UiNavigationMenuItem,
334
+ UiNavigationMenuTrigger,
335
+ UiNavigationMenuContent,
336
+ UiNavigationMenuLink,
337
+ },
338
+ template: `
339
+ <UiNavigationMenu>
340
+ <UiNavigationMenuList>
341
+ <UiNavigationMenuItem value="products">
342
+ <UiNavigationMenuTrigger>Products</UiNavigationMenuTrigger>
343
+ <UiNavigationMenuContent>
344
+ <UiNavigationMenuLink @select="onSelect">Click Me</UiNavigationMenuLink>
345
+ </UiNavigationMenuContent>
346
+ </UiNavigationMenuItem>
347
+ </UiNavigationMenuList>
348
+ </UiNavigationMenu>
349
+ `,
350
+ setup() {
351
+ return { onSelect };
352
+ },
353
+ });
354
+
355
+ await user.hover(getByRole('button', { name: 'Products' }));
356
+ await waitFor(() => expect(getByText('Click Me')).toBeVisible());
357
+
358
+ await user.click(getByText('Click Me'));
359
+
360
+ expect(onSelect).toHaveBeenCalled();
361
+ });
362
+
363
+ test('sets aria-current="page" when active', async () => {
364
+ const user = userEvent.setup();
365
+ const { getByRole, getByText } = render({
366
+ components: {
367
+ UiNavigationMenu,
368
+ UiNavigationMenuList,
369
+ UiNavigationMenuItem,
370
+ UiNavigationMenuTrigger,
371
+ UiNavigationMenuContent,
372
+ UiNavigationMenuLink,
373
+ },
374
+ template: `
375
+ <UiNavigationMenu>
376
+ <UiNavigationMenuList>
377
+ <UiNavigationMenuItem value="products">
378
+ <UiNavigationMenuTrigger>Products</UiNavigationMenuTrigger>
379
+ <UiNavigationMenuContent>
380
+ <UiNavigationMenuLink :active="true">Active Link</UiNavigationMenuLink>
381
+ <UiNavigationMenuLink :active="false">Inactive Link</UiNavigationMenuLink>
382
+ </UiNavigationMenuContent>
383
+ </UiNavigationMenuItem>
384
+ </UiNavigationMenuList>
385
+ </UiNavigationMenu>
386
+ `,
387
+ });
388
+
389
+ await user.hover(getByRole('button', { name: 'Products' }));
390
+
391
+ await waitFor(() => {
392
+ expect(getByText('Active Link')).toHaveAttribute('aria-current', 'page');
393
+ expect(getByText('Inactive Link')).not.toHaveAttribute('aria-current');
394
+ });
395
+ });
396
+ });
397
+
398
+ describe('UiNavigationMenuIndicator', () => {
399
+ test('indicator is present in DOM without interaction when forceMount is true', async () => {
400
+ const { container } = render({
401
+ components: {
402
+ UiNavigationMenu,
403
+ UiNavigationMenuList,
404
+ UiNavigationMenuItem,
405
+ UiNavigationMenuTrigger,
406
+ UiNavigationMenuContent,
407
+ UiNavigationMenuIndicator,
408
+ },
409
+ template: `
410
+ <UiNavigationMenu>
411
+ <UiNavigationMenuList>
412
+ <UiNavigationMenuItem>
413
+ <UiNavigationMenuTrigger>Products</UiNavigationMenuTrigger>
414
+ <UiNavigationMenuContent>Content</UiNavigationMenuContent>
415
+ </UiNavigationMenuItem>
416
+ <UiNavigationMenuIndicator :force-mount="true" />
417
+ </UiNavigationMenuList>
418
+ </UiNavigationMenu>
419
+ `,
420
+ });
421
+
422
+ await waitFor(() => {
423
+ expect(
424
+ container.querySelector('[data-slot="navigation-menu-indicator"]'),
425
+ ).toBeInTheDocument();
426
+ });
427
+ });
428
+ });
@@ -0,0 +1,11 @@
1
+ export { default as UiNavigationMenu } from './UiNavigationMenu.vue';
2
+ export { default as UiNavigationMenuList } from './UiNavigationMenuList.vue';
3
+ export { default as UiNavigationMenuItem } from './UiNavigationMenuItem.vue';
4
+ export { default as UiNavigationMenuTrigger } from './UiNavigationMenuTrigger.vue';
5
+ export { default as UiNavigationMenuContent } from './UiNavigationMenuContent.vue';
6
+ export { default as UiNavigationMenuLink } from './UiNavigationMenuLink.vue';
7
+ export { default as UiNavigationMenuIndicator } from './UiNavigationMenuIndicator.vue';
8
+
9
+ export { navigationMenuTriggerStyle } from '@/components/core/navigation-menu';
10
+
11
+ export * from './types';