@fiscozen/navbar 0.1.11 → 0.2.1
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/CHANGELOG.md +21 -0
- package/dist/navbar.css +1 -1
- package/dist/navbar.js +1329 -1224
- package/dist/navbar.umd.cjs +5 -5
- package/dist/src/FzNavbar.vue.d.ts +21 -5
- package/dist/src/types.d.ts +27 -3
- package/package.json +2 -2
- package/src/FzNavbar.vue +123 -22
- package/src/__tests__/FzNavbar.spec.ts +274 -0
- package/src/__tests__/__snapshots__/FzNavbar.spec.ts.snap +18 -18
- package/src/types.ts +28 -3
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import { FzNavbarProps } from './types';
|
|
2
|
+
declare function handleMenuButtonClick(): void;
|
|
2
3
|
declare function __VLS_template(): {
|
|
3
4
|
attrs: Partial<{}>;
|
|
4
|
-
slots:
|
|
5
|
-
isHorizontal: boolean;
|
|
6
|
-
isVertical: boolean;
|
|
7
|
-
isMobile: true;
|
|
8
|
-
}) => any>> & {
|
|
5
|
+
slots: {
|
|
9
6
|
'brand-logo'?(_: {
|
|
10
7
|
isMobile: false;
|
|
11
8
|
isHorizontal: boolean;
|
|
@@ -26,11 +23,25 @@ declare function __VLS_template(): {
|
|
|
26
23
|
isVertical: boolean;
|
|
27
24
|
isMobile: false;
|
|
28
25
|
}): any;
|
|
26
|
+
notifications?(_: {
|
|
27
|
+
isHorizontal: boolean;
|
|
28
|
+
isVertical: boolean;
|
|
29
|
+
isMobile: true;
|
|
30
|
+
}): any;
|
|
29
31
|
'user-menu'?(_: {
|
|
30
32
|
isHorizontal: boolean;
|
|
31
33
|
isMobile: false;
|
|
32
34
|
isVertical: boolean;
|
|
33
35
|
}): any;
|
|
36
|
+
'user-menu'?(_: {
|
|
37
|
+
isHorizontal: boolean;
|
|
38
|
+
isMobile: true;
|
|
39
|
+
isVertical: boolean;
|
|
40
|
+
}): any;
|
|
41
|
+
'menu-button'?(_: {
|
|
42
|
+
isOpen: boolean;
|
|
43
|
+
toggle: typeof handleMenuButtonClick;
|
|
44
|
+
}): any;
|
|
34
45
|
};
|
|
35
46
|
refs: {};
|
|
36
47
|
rootEl: HTMLElement;
|
|
@@ -38,11 +49,16 @@ declare function __VLS_template(): {
|
|
|
38
49
|
type __VLS_TemplateResult = ReturnType<typeof __VLS_template>;
|
|
39
50
|
declare const __VLS_component: import('vue').DefineComponent<FzNavbarProps, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
|
|
40
51
|
"fznavbar:menuButtonClick": () => any;
|
|
52
|
+
"update:isMenuOpen": (value: boolean) => any;
|
|
41
53
|
}, string, import('vue').PublicProps, Readonly<FzNavbarProps> & Readonly<{
|
|
42
54
|
"onFznavbar:menuButtonClick"?: (() => any) | undefined;
|
|
55
|
+
"onUpdate:isMenuOpen"?: ((value: boolean) => any) | undefined;
|
|
43
56
|
}>, {
|
|
44
57
|
variant: import('./types').FzNavbarVariant;
|
|
58
|
+
position: import('./types').FzNavbarPosition;
|
|
45
59
|
breakpoints: Partial<Record<import('@fiscozen/style').Breakpoint, `${number}px`>>;
|
|
60
|
+
mobileBreakpoint: number | `${number}px`;
|
|
61
|
+
respectSafeArea: boolean;
|
|
46
62
|
}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, HTMLElement>;
|
|
47
63
|
declare const _default: __VLS_WithTemplateSlots<typeof __VLS_component, __VLS_TemplateResult["slots"]>;
|
|
48
64
|
export default _default;
|
package/dist/src/types.d.ts
CHANGED
|
@@ -1,20 +1,44 @@
|
|
|
1
1
|
import { Breakpoint } from '@fiscozen/style';
|
|
2
2
|
export type FzNavbarVariant = 'horizontal' | 'vertical';
|
|
3
|
+
export type FzNavbarPosition = 'static' | 'fixed' | 'sticky';
|
|
3
4
|
interface FzNavbarProps {
|
|
4
5
|
/**
|
|
5
6
|
* The main direction of the navbar
|
|
6
7
|
*/
|
|
7
|
-
variant
|
|
8
|
+
variant?: FzNavbarVariant;
|
|
8
9
|
/**
|
|
9
|
-
* Whether the main navigation menu is open (mobile)
|
|
10
|
+
* Whether the main navigation menu is open (mobile). Supports v-model.
|
|
10
11
|
*/
|
|
11
12
|
isMenuOpen?: boolean;
|
|
12
13
|
/**
|
|
13
|
-
*
|
|
14
|
+
* @deprecated Use `mobileBreakpoint` instead. When both are passed, `mobileBreakpoint` takes precedence.
|
|
15
|
+
*
|
|
16
|
+
* Override breakpoints for managing custom size inside the navbar.
|
|
14
17
|
*/
|
|
15
18
|
breakpoints?: Partial<Record<Breakpoint, `${number}px`>>;
|
|
19
|
+
/**
|
|
20
|
+
* Width (in pixels) at and above which the navbar renders its desktop layout.
|
|
21
|
+
* Below this value the compact mobile layout is used.
|
|
22
|
+
*
|
|
23
|
+
* Replaces the global mutation of `@fiscozen/style` breakpoints that consumers
|
|
24
|
+
* used to perform to align with their own desktop threshold.
|
|
25
|
+
*/
|
|
26
|
+
mobileBreakpoint?: number | `${number}px`;
|
|
27
|
+
/**
|
|
28
|
+
* CSS positioning strategy applied to the root `<header>`. Replaces the need
|
|
29
|
+
* for consumers to add `class="fixed top-0 left-0"` (or similar) on the call site.
|
|
30
|
+
*/
|
|
31
|
+
position?: FzNavbarPosition;
|
|
32
|
+
/**
|
|
33
|
+
* When `true`, the navbar adds `env(safe-area-inset-*)` to its top, left and right padding
|
|
34
|
+
* so it renders correctly on devices with a notch / dynamic island.
|
|
35
|
+
*
|
|
36
|
+
* Defaults to `false` to preserve current visual behavior for existing consumers.
|
|
37
|
+
*/
|
|
38
|
+
respectSafeArea?: boolean;
|
|
16
39
|
}
|
|
17
40
|
type FzNavbarEmits = {
|
|
18
41
|
'fznavbar:menuButtonClick': [];
|
|
42
|
+
'update:isMenuOpen': [value: boolean];
|
|
19
43
|
};
|
|
20
44
|
export type { FzNavbarProps, FzNavbarEmits };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fiscozen/navbar",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Design System Navbar Component",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
},
|
|
33
33
|
"license": "ISC",
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@fiscozen/composables": "1.0.3",
|
|
36
35
|
"@fiscozen/button": "3.0.1",
|
|
36
|
+
"@fiscozen/composables": "1.0.3",
|
|
37
37
|
"@fiscozen/style": "0.3.0"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
package/src/FzNavbar.vue
CHANGED
|
@@ -7,29 +7,59 @@ import { useBreakpoints } from '@fiscozen/composables'
|
|
|
7
7
|
|
|
8
8
|
const props = withDefaults(defineProps<FzNavbarProps>(), {
|
|
9
9
|
variant: 'horizontal',
|
|
10
|
-
breakpoints: undefined
|
|
10
|
+
breakpoints: undefined,
|
|
11
|
+
mobileBreakpoint: undefined,
|
|
12
|
+
position: 'static',
|
|
13
|
+
respectSafeArea: false
|
|
11
14
|
})
|
|
12
15
|
|
|
16
|
+
const emit = defineEmits<FzNavbarEmits>()
|
|
17
|
+
|
|
18
|
+
if (props.breakpoints !== undefined) {
|
|
19
|
+
// eslint-disable-next-line no-console
|
|
20
|
+
console.warn(
|
|
21
|
+
'[FzNavbar] The `breakpoints` prop is deprecated and will be removed in a future major. Use `mobileBreakpoint` instead.'
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
13
25
|
const computedBreakpoints = computed(() => {
|
|
26
|
+
if (props.mobileBreakpoint !== undefined) {
|
|
27
|
+
const value =
|
|
28
|
+
typeof props.mobileBreakpoint === 'number'
|
|
29
|
+
? (`${props.mobileBreakpoint}px` as `${number}px`)
|
|
30
|
+
: props.mobileBreakpoint
|
|
31
|
+
return { ...breakpoints, lg: value }
|
|
32
|
+
}
|
|
14
33
|
return {
|
|
15
34
|
...breakpoints,
|
|
16
35
|
...(props.breakpoints ?? {})
|
|
17
36
|
}
|
|
18
37
|
})
|
|
19
38
|
|
|
20
|
-
const emit = defineEmits<FzNavbarEmits>()
|
|
21
|
-
|
|
22
39
|
const { isGreater } = useBreakpoints(computedBreakpoints.value)
|
|
23
40
|
const isGreaterThanLg = isGreater('lg')
|
|
24
41
|
const isMobile = computed(() => !isGreaterThanLg.value)
|
|
25
|
-
const isVertical = computed(() =>
|
|
26
|
-
const isHorizontal = computed(() =>
|
|
42
|
+
const isVertical = computed(() => props.variant === 'vertical')
|
|
43
|
+
const isHorizontal = computed(() => props.variant === 'horizontal')
|
|
44
|
+
|
|
45
|
+
const localMenuOpen = computed<boolean | undefined>({
|
|
46
|
+
get: () => props.isMenuOpen,
|
|
47
|
+
set: (value) => emit('update:isMenuOpen', Boolean(value))
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
function handleMenuButtonClick() {
|
|
51
|
+
localMenuOpen.value = !localMenuOpen.value
|
|
52
|
+
emit('fznavbar:menuButtonClick')
|
|
53
|
+
}
|
|
27
54
|
</script>
|
|
28
55
|
|
|
29
56
|
<template>
|
|
30
57
|
<header
|
|
31
|
-
class="z-10 flex p-12 shadow"
|
|
58
|
+
class="fz-navbar z-10 m-0 box-border flex items-center border-0 p-12 shadow"
|
|
32
59
|
:class="{
|
|
60
|
+
'fz-navbar--fixed': position === 'fixed',
|
|
61
|
+
'fz-navbar--sticky': position === 'sticky',
|
|
62
|
+
'fz-navbar--safe-area': respectSafeArea,
|
|
33
63
|
'justify-between': isMobile,
|
|
34
64
|
'h-full w-56 flex-col': isVertical && !isMobile,
|
|
35
65
|
'h-56 w-full': isHorizontal || isMobile
|
|
@@ -39,11 +69,14 @@ const isHorizontal = computed(() => Boolean(props.variant === 'horizontal'))
|
|
|
39
69
|
<div :class="{ 'mr-32': isHorizontal, 'mb-32': isVertical }">
|
|
40
70
|
<slot name="brand-logo" :isMobile :isHorizontal :isVertical></slot>
|
|
41
71
|
</div>
|
|
42
|
-
<div
|
|
72
|
+
<div
|
|
73
|
+
class="flex items-center gap-4"
|
|
74
|
+
:class="{ 'flex-row': isHorizontal, 'flex-col': isVertical }"
|
|
75
|
+
>
|
|
43
76
|
<slot name="navigation" :isVertical :isHorizontal :isMobile></slot>
|
|
44
77
|
</div>
|
|
45
78
|
<div
|
|
46
|
-
class="flex gap-16"
|
|
79
|
+
class="flex items-center gap-16"
|
|
47
80
|
:class="{ 'ml-auto flex-row': isHorizontal, 'mt-auto flex-col': isVertical }"
|
|
48
81
|
>
|
|
49
82
|
<slot name="notifications" :isHorizontal :isVertical :isMobile></slot>
|
|
@@ -51,25 +84,93 @@ const isHorizontal = computed(() => Boolean(props.variant === 'horizontal'))
|
|
|
51
84
|
</div>
|
|
52
85
|
</template>
|
|
53
86
|
<template v-else>
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
87
|
+
<slot name="menu-button" :isOpen="Boolean(localMenuOpen)" :toggle="handleMenuButtonClick">
|
|
88
|
+
<FzIconButton
|
|
89
|
+
:iconName="localMenuOpen ? 'xmark' : 'bars'"
|
|
90
|
+
variant="secondary"
|
|
91
|
+
tooltip="menu"
|
|
92
|
+
@click="handleMenuButtonClick"
|
|
93
|
+
/>
|
|
94
|
+
</slot>
|
|
60
95
|
<div>
|
|
61
96
|
<slot name="brand-logo" :isMobile :isHorizontal :isVertical></slot>
|
|
62
97
|
</div>
|
|
63
|
-
<div>
|
|
64
|
-
<slot
|
|
65
|
-
|
|
66
|
-
:isHorizontal
|
|
67
|
-
:isVertical
|
|
68
|
-
:isMobile
|
|
69
|
-
></slot>
|
|
98
|
+
<div class="flex items-center gap-16">
|
|
99
|
+
<slot name="notifications" :isHorizontal :isVertical :isMobile></slot>
|
|
100
|
+
<slot name="user-menu" :isHorizontal :isMobile :isVertical></slot>
|
|
70
101
|
</div>
|
|
71
102
|
</template>
|
|
72
103
|
</header>
|
|
73
104
|
</template>
|
|
74
105
|
|
|
75
|
-
<style
|
|
106
|
+
<style>
|
|
107
|
+
/*
|
|
108
|
+
* CSS custom properties — defaults are pinned to the `@fiscozen/style` pixel
|
|
109
|
+
* spacing tokens that match the Tailwind utility classes used in the template
|
|
110
|
+
* (p-12 = 12px, h-56/w-56 = 56px, mr-32/mb-32 = 32px, gap-16 = 16px). The
|
|
111
|
+
* package's CSS rule `.fz-navbar.{w-56,h-56,...}` has higher specificity than
|
|
112
|
+
* the consumer's generated Tailwind class on its own, so the fallback values
|
|
113
|
+
* must match the design-system pixel scale — otherwise stock-Tailwind rem
|
|
114
|
+
* defaults (14rem/3rem/8rem/4rem) leak through and the navbar renders ~4×
|
|
115
|
+
* larger than intended.
|
|
116
|
+
*
|
|
117
|
+
* Consumers can still override per-instance via inline style or scoped CSS.
|
|
118
|
+
*/
|
|
119
|
+
|
|
120
|
+
.fz-navbar {
|
|
121
|
+
z-index: var(--fz-navbar-z-index, 10);
|
|
122
|
+
padding: var(--fz-navbar-padding, 12px);
|
|
123
|
+
box-shadow: var(
|
|
124
|
+
--fz-navbar-shadow,
|
|
125
|
+
0 1px 3px 0 rgb(0 0 0 / 0.1),
|
|
126
|
+
0 1px 2px -1px rgb(0 0 0 / 0.1)
|
|
127
|
+
);
|
|
128
|
+
background: var(--fz-navbar-bg, transparent);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.fz-navbar.h-56 {
|
|
132
|
+
height: var(--fz-navbar-height, 56px);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.fz-navbar.w-56 {
|
|
136
|
+
width: var(--fz-navbar-width, 56px);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.fz-navbar > .mr-32 {
|
|
140
|
+
margin-right: var(--fz-navbar-brand-gap, 32px);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.fz-navbar > .mb-32 {
|
|
144
|
+
margin-bottom: var(--fz-navbar-brand-gap, 32px);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.fz-navbar > .gap-16 {
|
|
148
|
+
gap: var(--fz-navbar-actions-gap, 16px);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.fz-navbar--fixed {
|
|
152
|
+
position: fixed;
|
|
153
|
+
top: 0;
|
|
154
|
+
left: 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.fz-navbar--sticky {
|
|
158
|
+
position: sticky;
|
|
159
|
+
top: 0;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.fz-navbar--safe-area {
|
|
163
|
+
padding-top: calc(
|
|
164
|
+
var(--fz-navbar-padding, 12px) +
|
|
165
|
+
max(env(safe-area-inset-top, 0px), var(--safe-area-inset-top, 0px))
|
|
166
|
+
);
|
|
167
|
+
padding-left: calc(
|
|
168
|
+
var(--fz-navbar-padding, 12px) +
|
|
169
|
+
max(env(safe-area-inset-left, 0px), var(--safe-area-inset-left, 0px))
|
|
170
|
+
);
|
|
171
|
+
padding-right: calc(
|
|
172
|
+
var(--fz-navbar-padding, 12px) +
|
|
173
|
+
max(env(safe-area-inset-right, 0px), var(--safe-area-inset-right, 0px))
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
</style>
|
|
@@ -2,6 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
|
2
2
|
import { mount, VueWrapper } from '@vue/test-utils'
|
|
3
3
|
import FzNavbar from '../FzNavbar.vue'
|
|
4
4
|
import { FzIconButton } from '@fiscozen/button'
|
|
5
|
+
import { breakpoints } from '@fiscozen/style'
|
|
5
6
|
|
|
6
7
|
const navigation = `
|
|
7
8
|
<div class="link">one</div>
|
|
@@ -847,4 +848,277 @@ describe('FzNavbar', () => {
|
|
|
847
848
|
expect(wrapper.html()).toMatchSnapshot()
|
|
848
849
|
})
|
|
849
850
|
})
|
|
851
|
+
|
|
852
|
+
// ============================================
|
|
853
|
+
// v0.2.0 — ADDITIVE API
|
|
854
|
+
// ============================================
|
|
855
|
+
describe('mobileBreakpoint prop', () => {
|
|
856
|
+
it('should accept a numeric value', () => {
|
|
857
|
+
wrapper = mount(FzNavbar, {
|
|
858
|
+
props: {
|
|
859
|
+
variant: 'horizontal',
|
|
860
|
+
mobileBreakpoint: 1200
|
|
861
|
+
}
|
|
862
|
+
})
|
|
863
|
+
expect(wrapper.exists()).toBe(true)
|
|
864
|
+
})
|
|
865
|
+
|
|
866
|
+
it('should accept a pixel-string value', () => {
|
|
867
|
+
wrapper = mount(FzNavbar, {
|
|
868
|
+
props: {
|
|
869
|
+
variant: 'horizontal',
|
|
870
|
+
mobileBreakpoint: '1200px'
|
|
871
|
+
}
|
|
872
|
+
})
|
|
873
|
+
expect(wrapper.exists()).toBe(true)
|
|
874
|
+
})
|
|
875
|
+
|
|
876
|
+
it('should not mutate the imported @fiscozen/style breakpoints object', () => {
|
|
877
|
+
const before = { ...breakpoints }
|
|
878
|
+
mount(FzNavbar, {
|
|
879
|
+
props: {
|
|
880
|
+
variant: 'horizontal',
|
|
881
|
+
mobileBreakpoint: 1400
|
|
882
|
+
}
|
|
883
|
+
})
|
|
884
|
+
expect(breakpoints).toEqual(before)
|
|
885
|
+
})
|
|
886
|
+
|
|
887
|
+
it('should take precedence over the legacy `breakpoints` prop when both are provided', () => {
|
|
888
|
+
wrapper = mount(FzNavbar, {
|
|
889
|
+
props: {
|
|
890
|
+
variant: 'horizontal',
|
|
891
|
+
breakpoints: { lg: '900px' },
|
|
892
|
+
mobileBreakpoint: 1500
|
|
893
|
+
}
|
|
894
|
+
})
|
|
895
|
+
expect(wrapper.exists()).toBe(true)
|
|
896
|
+
})
|
|
897
|
+
})
|
|
898
|
+
|
|
899
|
+
describe('position prop', () => {
|
|
900
|
+
it('should not add positioning classes by default', () => {
|
|
901
|
+
wrapper = mount(FzNavbar, {
|
|
902
|
+
props: { variant: 'horizontal' }
|
|
903
|
+
})
|
|
904
|
+
const header = wrapper.find('header')
|
|
905
|
+
expect(header.classes()).not.toContain('fz-navbar--fixed')
|
|
906
|
+
expect(header.classes()).not.toContain('fz-navbar--sticky')
|
|
907
|
+
})
|
|
908
|
+
|
|
909
|
+
it('should add the fixed-position class when position="fixed"', () => {
|
|
910
|
+
wrapper = mount(FzNavbar, {
|
|
911
|
+
props: { variant: 'horizontal', position: 'fixed' }
|
|
912
|
+
})
|
|
913
|
+
expect(wrapper.find('header').classes()).toContain('fz-navbar--fixed')
|
|
914
|
+
})
|
|
915
|
+
|
|
916
|
+
it('should add the sticky-position class when position="sticky"', () => {
|
|
917
|
+
wrapper = mount(FzNavbar, {
|
|
918
|
+
props: { variant: 'horizontal', position: 'sticky' }
|
|
919
|
+
})
|
|
920
|
+
expect(wrapper.find('header').classes()).toContain('fz-navbar--sticky')
|
|
921
|
+
})
|
|
922
|
+
})
|
|
923
|
+
|
|
924
|
+
describe('respectSafeArea prop', () => {
|
|
925
|
+
it('should not add the safe-area class by default', () => {
|
|
926
|
+
wrapper = mount(FzNavbar, {
|
|
927
|
+
props: { variant: 'horizontal' }
|
|
928
|
+
})
|
|
929
|
+
expect(wrapper.find('header').classes()).not.toContain('fz-navbar--safe-area')
|
|
930
|
+
})
|
|
931
|
+
|
|
932
|
+
it('should add the safe-area class when respectSafeArea is true', () => {
|
|
933
|
+
wrapper = mount(FzNavbar, {
|
|
934
|
+
props: { variant: 'horizontal', respectSafeArea: true }
|
|
935
|
+
})
|
|
936
|
+
expect(wrapper.find('header').classes()).toContain('fz-navbar--safe-area')
|
|
937
|
+
})
|
|
938
|
+
})
|
|
939
|
+
|
|
940
|
+
describe('v-model:isMenuOpen', () => {
|
|
941
|
+
it('should emit update:isMenuOpen when the default menu button is clicked', async () => {
|
|
942
|
+
window.innerWidth = 1023
|
|
943
|
+
vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1023)
|
|
944
|
+
|
|
945
|
+
wrapper = mount(FzNavbar, {
|
|
946
|
+
props: { variant: 'horizontal', isMenuOpen: false }
|
|
947
|
+
})
|
|
948
|
+
await wrapper.vm.$nextTick()
|
|
949
|
+
const menuButton = wrapper.findComponent(FzIconButton)
|
|
950
|
+
const buttonElement = menuButton.find('button')
|
|
951
|
+
await (buttonElement.exists() ? buttonElement : menuButton).trigger('click')
|
|
952
|
+
|
|
953
|
+
const updates = wrapper.emitted('update:isMenuOpen')
|
|
954
|
+
expect(updates).toBeTruthy()
|
|
955
|
+
expect(updates![0]).toEqual([true])
|
|
956
|
+
})
|
|
957
|
+
|
|
958
|
+
it('should emit `false` when toggling from an open state', async () => {
|
|
959
|
+
window.innerWidth = 1023
|
|
960
|
+
vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1023)
|
|
961
|
+
|
|
962
|
+
wrapper = mount(FzNavbar, {
|
|
963
|
+
props: { variant: 'horizontal', isMenuOpen: true }
|
|
964
|
+
})
|
|
965
|
+
await wrapper.vm.$nextTick()
|
|
966
|
+
const menuButton = wrapper.findComponent(FzIconButton)
|
|
967
|
+
const buttonElement = menuButton.find('button')
|
|
968
|
+
await (buttonElement.exists() ? buttonElement : menuButton).trigger('click')
|
|
969
|
+
|
|
970
|
+
const updates = wrapper.emitted('update:isMenuOpen')
|
|
971
|
+
expect(updates).toBeTruthy()
|
|
972
|
+
expect(updates![0]).toEqual([false])
|
|
973
|
+
})
|
|
974
|
+
|
|
975
|
+
it('should still emit fznavbar:menuButtonClick alongside update:isMenuOpen', async () => {
|
|
976
|
+
window.innerWidth = 1023
|
|
977
|
+
vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1023)
|
|
978
|
+
|
|
979
|
+
wrapper = mount(FzNavbar, {
|
|
980
|
+
props: { variant: 'horizontal', isMenuOpen: false }
|
|
981
|
+
})
|
|
982
|
+
await wrapper.vm.$nextTick()
|
|
983
|
+
const menuButton = wrapper.findComponent(FzIconButton)
|
|
984
|
+
const buttonElement = menuButton.find('button')
|
|
985
|
+
await (buttonElement.exists() ? buttonElement : menuButton).trigger('click')
|
|
986
|
+
|
|
987
|
+
expect(wrapper.emitted('fznavbar:menuButtonClick')).toHaveLength(1)
|
|
988
|
+
expect(wrapper.emitted('update:isMenuOpen')).toHaveLength(1)
|
|
989
|
+
})
|
|
990
|
+
})
|
|
991
|
+
|
|
992
|
+
describe('menu-button slot', () => {
|
|
993
|
+
it('should render the default FzIconButton when the slot is not provided', async () => {
|
|
994
|
+
window.innerWidth = 1023
|
|
995
|
+
vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1023)
|
|
996
|
+
|
|
997
|
+
wrapper = mount(FzNavbar, {
|
|
998
|
+
props: { variant: 'horizontal' }
|
|
999
|
+
})
|
|
1000
|
+
await wrapper.vm.$nextTick()
|
|
1001
|
+
expect(wrapper.findComponent(FzIconButton).exists()).toBe(true)
|
|
1002
|
+
})
|
|
1003
|
+
|
|
1004
|
+
it('should replace the default button when the menu-button slot is provided', async () => {
|
|
1005
|
+
window.innerWidth = 1023
|
|
1006
|
+
vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1023)
|
|
1007
|
+
|
|
1008
|
+
wrapper = mount(FzNavbar, {
|
|
1009
|
+
props: { variant: 'horizontal', isMenuOpen: false },
|
|
1010
|
+
slots: {
|
|
1011
|
+
'menu-button': `
|
|
1012
|
+
<template #menu-button="{ isOpen, toggle }">
|
|
1013
|
+
<button id="custom-menu" :data-is-open="isOpen" @click="toggle">menu</button>
|
|
1014
|
+
</template>
|
|
1015
|
+
`
|
|
1016
|
+
}
|
|
1017
|
+
})
|
|
1018
|
+
await wrapper.vm.$nextTick()
|
|
1019
|
+
expect(wrapper.find('#custom-menu').exists()).toBe(true)
|
|
1020
|
+
expect(wrapper.findComponent(FzIconButton).exists()).toBe(false)
|
|
1021
|
+
})
|
|
1022
|
+
|
|
1023
|
+
it('should expose isOpen and toggle in the slot scope', async () => {
|
|
1024
|
+
window.innerWidth = 1023
|
|
1025
|
+
vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1023)
|
|
1026
|
+
|
|
1027
|
+
wrapper = mount(FzNavbar, {
|
|
1028
|
+
props: { variant: 'horizontal', isMenuOpen: true },
|
|
1029
|
+
slots: {
|
|
1030
|
+
'menu-button': `
|
|
1031
|
+
<template #menu-button="{ isOpen, toggle }">
|
|
1032
|
+
<button id="custom-menu" :data-is-open="isOpen" @click="toggle">menu</button>
|
|
1033
|
+
</template>
|
|
1034
|
+
`
|
|
1035
|
+
}
|
|
1036
|
+
})
|
|
1037
|
+
await wrapper.vm.$nextTick()
|
|
1038
|
+
const customButton = wrapper.find('#custom-menu')
|
|
1039
|
+
expect(customButton.attributes('data-is-open')).toBe('true')
|
|
1040
|
+
|
|
1041
|
+
await customButton.trigger('click')
|
|
1042
|
+
const updates = wrapper.emitted('update:isMenuOpen')
|
|
1043
|
+
expect(updates).toBeTruthy()
|
|
1044
|
+
expect(updates![0]).toEqual([false])
|
|
1045
|
+
})
|
|
1046
|
+
})
|
|
1047
|
+
|
|
1048
|
+
describe('mobile slot rendering (no XOR)', () => {
|
|
1049
|
+
it('should render both notifications and user-menu slots on mobile when both are filled', async () => {
|
|
1050
|
+
window.innerWidth = 1023
|
|
1051
|
+
vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1023)
|
|
1052
|
+
|
|
1053
|
+
wrapper = mount(FzNavbar, {
|
|
1054
|
+
props: { variant: 'horizontal' },
|
|
1055
|
+
slots: {
|
|
1056
|
+
notifications: '<div id="notification"></div>',
|
|
1057
|
+
'user-menu': '<div id="avatar"></div>'
|
|
1058
|
+
}
|
|
1059
|
+
})
|
|
1060
|
+
await wrapper.vm.$nextTick()
|
|
1061
|
+
expect(wrapper.find('#notification').exists()).toBe(true)
|
|
1062
|
+
expect(wrapper.find('#avatar').exists()).toBe(true)
|
|
1063
|
+
})
|
|
1064
|
+
|
|
1065
|
+
it('should render only notifications on mobile when user-menu is empty', async () => {
|
|
1066
|
+
window.innerWidth = 1023
|
|
1067
|
+
vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1023)
|
|
1068
|
+
|
|
1069
|
+
wrapper = mount(FzNavbar, {
|
|
1070
|
+
props: { variant: 'horizontal' },
|
|
1071
|
+
slots: {
|
|
1072
|
+
notifications: '<div id="notification"></div>'
|
|
1073
|
+
}
|
|
1074
|
+
})
|
|
1075
|
+
await wrapper.vm.$nextTick()
|
|
1076
|
+
expect(wrapper.find('#notification').exists()).toBe(true)
|
|
1077
|
+
expect(wrapper.find('#avatar').exists()).toBe(false)
|
|
1078
|
+
})
|
|
1079
|
+
|
|
1080
|
+
it('should render only user-menu on mobile when notifications is empty', async () => {
|
|
1081
|
+
window.innerWidth = 1023
|
|
1082
|
+
vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1023)
|
|
1083
|
+
|
|
1084
|
+
wrapper = mount(FzNavbar, {
|
|
1085
|
+
props: { variant: 'vertical' },
|
|
1086
|
+
slots: {
|
|
1087
|
+
'user-menu': '<div id="avatar"></div>'
|
|
1088
|
+
}
|
|
1089
|
+
})
|
|
1090
|
+
await wrapper.vm.$nextTick()
|
|
1091
|
+
expect(wrapper.find('#avatar').exists()).toBe(true)
|
|
1092
|
+
expect(wrapper.find('#notification').exists()).toBe(false)
|
|
1093
|
+
})
|
|
1094
|
+
})
|
|
1095
|
+
|
|
1096
|
+
describe('breakpoints prop deprecation warning', () => {
|
|
1097
|
+
it('should emit a console.warn in dev when the deprecated `breakpoints` prop is used', () => {
|
|
1098
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
1099
|
+
|
|
1100
|
+
mount(FzNavbar, {
|
|
1101
|
+
props: {
|
|
1102
|
+
variant: 'horizontal',
|
|
1103
|
+
breakpoints: { lg: '1200px' }
|
|
1104
|
+
}
|
|
1105
|
+
})
|
|
1106
|
+
|
|
1107
|
+
expect(warnSpy).toHaveBeenCalled()
|
|
1108
|
+
expect(warnSpy.mock.calls[0][0]).toMatch(/breakpoints.*deprecated/i)
|
|
1109
|
+
|
|
1110
|
+
warnSpy.mockRestore()
|
|
1111
|
+
})
|
|
1112
|
+
|
|
1113
|
+
it('should not warn when the deprecated prop is not used', () => {
|
|
1114
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
1115
|
+
|
|
1116
|
+
mount(FzNavbar, {
|
|
1117
|
+
props: { variant: 'horizontal' }
|
|
1118
|
+
})
|
|
1119
|
+
|
|
1120
|
+
expect(warnSpy).not.toHaveBeenCalled()
|
|
1121
|
+
warnSpy.mockRestore()
|
|
1122
|
+
})
|
|
1123
|
+
})
|
|
850
1124
|
})
|