@designcrowd/library.brand-page 6.1.17-ast-header-button → 6.1.17-ast-header-button-2
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/lib/src/{CreatedOnBrandCrowd-de-de-Cwq9enJs.js → CreatedOnBrandCrowd-de-de-CGQppEEP.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-en-us-ILK04gFj.js → CreatedOnBrandCrowd-en-us-Do8u4cNA.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-es-es-CbjxLxtu.js → CreatedOnBrandCrowd-es-es-Cw9i-wIi.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-fr-BwBDiK9z.js → CreatedOnBrandCrowd-fr-ca-DK9cVARn.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-ca-BwBDiK9z.js → CreatedOnBrandCrowd-fr-fr-DK9cVARn.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-pt-DvhJROpG.js → CreatedOnBrandCrowd-pt-br-CKvle-Uo.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-br-DvhJROpG.js → CreatedOnBrandCrowd-pt-pt-CKvle-Uo.js} +1 -1
- package/lib/src/{CreatedOnDcom-de-de-BK9imWOh.js → CreatedOnDcom-de-de-DZGQbS8x.js} +1 -1
- package/lib/src/{CreatedOnDcom-en-us-CyH-_qz1.js → CreatedOnDcom-en-us-CAGyRg9Z.js} +1 -1
- package/lib/src/{CreatedOnDcom-es-es-Dh51qYEA.js → CreatedOnDcom-es-es-B_Xbs4CX.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-ca-BArRxwWX.js → CreatedOnDcom-fr-ca-B_Af6mgX.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-fr-CdglI-ZB.js → CreatedOnDcom-fr-fr-Dn8OKUFf.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-br-DCehns-G.js → CreatedOnDcom-pt-br-DFsESCzy.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-pt-B8hl-oew.js → CreatedOnDcom-pt-pt-CjbOJw5o.js} +1 -1
- package/lib/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.mixin.d.ts +0 -11
- package/lib/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.mixin.js +6 -11
- package/lib/src/brand-page/components/page/Page.mixin.d.ts +2 -1
- package/lib/src/brand-page/components/page/Page.mixin.js +11 -1
- package/lib/src/brand-page/components/page-header/HeaderLayout.stories.d.ts +1 -0
- package/lib/src/brand-page/components/page-header/HeaderLayout.stories.js +9 -0
- package/lib/src/brand-page/components/page-header/PageHeader.mixin.js +3 -3
- package/lib/src/brand-page/constants/sections/header-display.constants.d.ts +8 -0
- package/lib/src/brand-page/constants/sections/header-display.constants.js +11 -0
- package/lib/src/brand-page/models/index.d.ts +4 -1
- package/lib/src/brand-page/models/index.js +5 -3
- package/lib/src/brand-page/utils/view-model.util.d.ts +0 -1
- package/lib/src/brand-page/utils/view-model.util.js +5 -5
- package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.d.ts +1 -0
- package/lib/src/bundled-translations-Cs7jWCj8.js +181 -0
- package/lib/src/bundles/bundled-translations.json +3 -0
- package/lib/src/css/library.brand-page-brandCrowd.min.css +216 -159
- package/lib/src/css/library.brand-page-brandPage.min.css +216 -159
- package/lib/src/css/library.brand-page-designCom.min.css +216 -159
- package/lib/src/css/library.brand-page-preview.min.css +216 -159
- package/lib/src/index.mjs +1 -1
- package/lib/src/{lib-KFjPFfxn.js → lib-BFusPgV2.js} +12063 -12041
- package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.js +4 -4
- package/package.json +1 -1
- package/src/brand-page/components/navigation/hamburger/HamburgerNavigation.vue +1 -0
- package/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.mixin.ts +6 -11
- package/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.vue +35 -3
- package/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.vue.test.ts +23 -17
- package/src/brand-page/components/page/Page.mixin.ts +17 -0
- package/src/brand-page/components/page/Page.vue +2 -6
- package/src/brand-page/components/page-header/HeaderLayout.stories.ts +10 -0
- package/src/brand-page/components/page-header/HeaderLayout.vue +35 -35
- package/src/brand-page/components/page-header/PageHeader.mixin.ts +3 -3
- package/src/brand-page/components/page-header/PageHeader.vue.test.ts +9 -3
- package/src/brand-page/components/page-header/i18n/page-header.json +5 -0
- package/src/brand-page/constants/sections/header-display.constants.test.ts +45 -0
- package/src/brand-page/constants/sections/header-display.constants.ts +19 -0
- package/src/brand-page/models/index.ts +10 -3
- package/src/brand-page/utils/__snapshots__/migration.util.test.ts.snap +0 -6
- package/src/brand-page/utils/__snapshots__/migration2.util.test.ts.snap +0 -2
- package/src/brand-page/utils/__snapshots__/model.util.test.ts.snap +0 -4
- package/src/brand-page/utils/view-model.util.test.ts +7 -2
- package/src/brand-page/utils/view-model.util.ts +6 -6
- package/src/bundles/bundled-translations.json +3 -0
- package/src/shared/page-settings/components/header-settings-panel/HeaderSettingsPanel.button-section.test.ts +25 -14
- package/src/shared/page-settings/components/header-settings-panel/HeaderSettingsPanel.vue +38 -26
- package/src/shared/page-settings/components/page-settings-section-styles/HeaderButtonSettings.vue +34 -9
- package/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.ts +4 -4
- package/lib/src/bundled-translations-B8fAEel0.js +0 -179
|
@@ -349,10 +349,10 @@ export default defineComponent({
|
|
|
349
349
|
...this.headerButton,
|
|
350
350
|
...patch,
|
|
351
351
|
};
|
|
352
|
-
// Desktop and mobile must hold the SAME button config
|
|
353
|
-
//
|
|
354
|
-
//
|
|
355
|
-
//
|
|
352
|
+
// Desktop and mobile must hold the SAME button config. The whole object
|
|
353
|
+
// is written to both from one source rather than merged per-viewport, so
|
|
354
|
+
// a pair that somehow diverged is reconciled instead of each side keeping
|
|
355
|
+
// its own leftovers.
|
|
356
356
|
//
|
|
357
357
|
// Each viewport gets its own copy: one shared object would alias the two
|
|
358
358
|
// branches of store state to a single instance.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@designcrowd/library.brand-page",
|
|
3
3
|
"description": "A collection of Maker/Designer applications",
|
|
4
|
-
"version": "6.1.17-ast-header-button",
|
|
4
|
+
"version": "6.1.17-ast-header-button-2",
|
|
5
5
|
"author": "Design.com Eng",
|
|
6
6
|
"license": "UNLICENSED",
|
|
7
7
|
"type": "module",
|
package/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.mixin.ts
CHANGED
|
@@ -26,18 +26,13 @@ export default defineComponent({
|
|
|
26
26
|
type: Object as PropType<NavigationDesktop | NavigationMobile>,
|
|
27
27
|
required: true,
|
|
28
28
|
},
|
|
29
|
+
// Provided only when the header row does not render the CTA (the caller
|
|
30
|
+
// decides via headerRowRendersCta) — absent means the drawer shows none.
|
|
29
31
|
headerButtonViewModel: {
|
|
30
32
|
type: Object as PropType<ButtonViewModel>,
|
|
31
33
|
required: false,
|
|
32
34
|
default: undefined,
|
|
33
35
|
},
|
|
34
|
-
// The desktop drawer suppresses the header CTA — the desktop header row already
|
|
35
|
-
// renders it. Only the mobile drawer carries the button.
|
|
36
|
-
isMobile: {
|
|
37
|
-
type: Boolean,
|
|
38
|
-
required: false,
|
|
39
|
-
default: false,
|
|
40
|
-
},
|
|
41
36
|
},
|
|
42
37
|
emits: ['close-side-menu'],
|
|
43
38
|
data() {
|
|
@@ -87,7 +82,7 @@ export default defineComponent({
|
|
|
87
82
|
return rest;
|
|
88
83
|
},
|
|
89
84
|
showHeaderButton(): boolean {
|
|
90
|
-
return
|
|
85
|
+
return Boolean(this.headerButtonViewModel);
|
|
91
86
|
},
|
|
92
87
|
routes(): any[] {
|
|
93
88
|
return this.visibleNavItems.map((navItem, _i) => {
|
|
@@ -129,9 +124,9 @@ export default defineComponent({
|
|
|
129
124
|
closeSideMenu(): void {
|
|
130
125
|
this.$emit('close-side-menu');
|
|
131
126
|
},
|
|
132
|
-
// In the maker preview the CTA is inert (
|
|
133
|
-
// prevent lands before the inner anchor/router-link fires. On the
|
|
134
|
-
// the click follows the link and the drawer closes behind it.
|
|
127
|
+
// In the maker preview the CTA is inert (see docs/decisions/0019): capture-phase
|
|
128
|
+
// so the prevent lands before the inner anchor/router-link fires. On the
|
|
129
|
+
// published site the click follows the link and the drawer closes behind it.
|
|
135
130
|
onDrawerCtaClick(event: MouseEvent): void {
|
|
136
131
|
if (this.context.isPreview) {
|
|
137
132
|
event.preventDefault();
|
|
@@ -9,15 +9,19 @@
|
|
|
9
9
|
}"
|
|
10
10
|
@click="closeSideMenu"
|
|
11
11
|
/>
|
|
12
|
+
<!-- Without a CTA the panel itself scrolls, as it always has. With one, the nav
|
|
13
|
+
list takes over the scrolling so the pinned button never scrolls away. -->
|
|
12
14
|
<div
|
|
13
15
|
class="c-sidebar"
|
|
14
16
|
:class="{
|
|
15
|
-
'c-sidebar--open
|
|
17
|
+
'c-sidebar--open': isSideMenuOpen,
|
|
16
18
|
'c-sidebar--closed': !isSideMenuOpen,
|
|
19
|
+
'tw-overflow-auto': isSideMenuOpen && !showHeaderButton,
|
|
20
|
+
'c-sidebar--has-cta': showHeaderButton,
|
|
17
21
|
}"
|
|
18
22
|
:style="defaultButtonStyle"
|
|
19
23
|
>
|
|
20
|
-
<div class="tw-w-full tw-flex tw-justify-end">
|
|
24
|
+
<div class="c-drawer-header tw-w-full tw-flex tw-justify-end">
|
|
21
25
|
<button
|
|
22
26
|
class="tw-mt-4 tw-w-8 tw-h-8 tw-bg-transparent tw-border-none tw-cursor-pointer"
|
|
23
27
|
:style="closeButtonStyle"
|
|
@@ -27,7 +31,7 @@
|
|
|
27
31
|
<Icon name="close" size="sm" />
|
|
28
32
|
</button>
|
|
29
33
|
</div>
|
|
30
|
-
<div class="tw-flex tw-flex-col">
|
|
34
|
+
<div class="c-nav-list tw-flex tw-flex-col">
|
|
31
35
|
<template
|
|
32
36
|
v-for="(navItem, index) in visibleNavItems"
|
|
33
37
|
:key="`${navItem.relativePath}-${index}`"
|
|
@@ -166,10 +170,38 @@ export default {
|
|
|
166
170
|
transform: rotate(180deg);
|
|
167
171
|
}
|
|
168
172
|
|
|
173
|
+
/* Design: the drawer CTA is pinned to the bottom of the panel and stays put while a
|
|
174
|
+
long link list scrolls. Every rule here is scoped to --has-cta, so a drawer without
|
|
175
|
+
the button keeps its original block flow and panel-level scrolling. */
|
|
176
|
+
.c-sidebar--has-cta {
|
|
177
|
+
@apply tw-flex tw-flex-col;
|
|
178
|
+
overflow: hidden;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.c-sidebar--has-cta .c-drawer-header {
|
|
182
|
+
flex-shrink: 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.c-sidebar--has-cta .c-nav-list {
|
|
186
|
+
/* min-height:0 is what lets the list shrink below its content height, so the list —
|
|
187
|
+
not the panel — is what scrolls once there are more links than fit. */
|
|
188
|
+
min-height: 0;
|
|
189
|
+
overflow-y: auto;
|
|
190
|
+
}
|
|
191
|
+
|
|
169
192
|
.c-drawer-cta {
|
|
170
193
|
@apply tw-relative tw-p-4;
|
|
171
194
|
}
|
|
172
195
|
|
|
196
|
+
.c-sidebar--has-cta .c-drawer-cta {
|
|
197
|
+
/* Pushes the button to the bottom when the links don't fill the panel; the nav
|
|
198
|
+
list's own scrolling keeps it there when they overflow. */
|
|
199
|
+
margin-top: auto;
|
|
200
|
+
flex-shrink: 0;
|
|
201
|
+
/* Opaque against the panel so scrolled links don't show through. */
|
|
202
|
+
background-color: inherit;
|
|
203
|
+
}
|
|
204
|
+
|
|
173
205
|
/* Hairline separator from the nav list, drawn in the drawer's own text colour so it
|
|
174
206
|
works on any panel background. A pseudo-element carries the fade — opacity on the
|
|
175
207
|
section itself would wash out the button. */
|
package/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.vue.test.ts
CHANGED
|
@@ -21,7 +21,6 @@ const createContext = (): BrandPageContext =>
|
|
|
21
21
|
({
|
|
22
22
|
brandPageSlug: SLUG,
|
|
23
23
|
brandPageToken: '',
|
|
24
|
-
isMobile: false,
|
|
25
24
|
isPreview: false,
|
|
26
25
|
isProduction: false,
|
|
27
26
|
isServerRender: false,
|
|
@@ -67,7 +66,6 @@ const aHeaderButton = (): HeaderButton => ({
|
|
|
67
66
|
});
|
|
68
67
|
|
|
69
68
|
const mountDrawer = async (options: {
|
|
70
|
-
isMobile?: boolean;
|
|
71
69
|
headerButton?: HeaderButton;
|
|
72
70
|
isPreview?: boolean;
|
|
73
71
|
}) => {
|
|
@@ -104,7 +102,6 @@ const mountDrawer = async (options: {
|
|
|
104
102
|
slug: SLUG,
|
|
105
103
|
styles: headerViewModel.navigation,
|
|
106
104
|
headerButtonViewModel: headerViewModel.headerButtonViewModel,
|
|
107
|
-
isMobile: options.isMobile ?? false,
|
|
108
105
|
},
|
|
109
106
|
global: { plugins: [router] },
|
|
110
107
|
});
|
|
@@ -117,7 +114,6 @@ describe('NavigationSideMenu - header call-to-action button (BP-5852)', () => {
|
|
|
117
114
|
|
|
118
115
|
test('renders the CTA in the mobile drawer', async () => {
|
|
119
116
|
const wrapper = await mountDrawer({
|
|
120
|
-
isMobile: true,
|
|
121
117
|
headerButton: aHeaderButton(),
|
|
122
118
|
});
|
|
123
119
|
|
|
@@ -127,29 +123,40 @@ describe('NavigationSideMenu - header call-to-action button (BP-5852)', () => {
|
|
|
127
123
|
expect(wrapper.get(CTA).text()).toContain('Book now');
|
|
128
124
|
});
|
|
129
125
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
126
|
+
test('renders no CTA when the site has no header button', async () => {
|
|
127
|
+
const wrapper = await mountDrawer({});
|
|
128
|
+
|
|
129
|
+
expect(wrapper.find(CTA).exists()).toBe(false);
|
|
130
|
+
expect(
|
|
131
|
+
wrapper.find('[data-component="navigation-side-menu-cta"]').exists(),
|
|
132
|
+
).toBe(false);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('with a CTA the panel hands scrolling to the nav list and pins the button last', async () => {
|
|
133
136
|
const wrapper = await mountDrawer({
|
|
134
|
-
isMobile: false,
|
|
135
137
|
headerButton: aHeaderButton(),
|
|
136
138
|
});
|
|
137
139
|
|
|
138
|
-
|
|
140
|
+
const sidebar = wrapper.get('.c-sidebar');
|
|
141
|
+
|
|
142
|
+
expect(sidebar.classes()).toContain('c-sidebar--has-cta');
|
|
143
|
+
expect(sidebar.classes()).not.toContain('tw-overflow-auto');
|
|
144
|
+
expect(sidebar.element.lastElementChild).toBe(
|
|
145
|
+
wrapper.get('.c-drawer-cta').element,
|
|
146
|
+
);
|
|
139
147
|
});
|
|
140
148
|
|
|
141
|
-
test('
|
|
142
|
-
const wrapper = await mountDrawer({
|
|
149
|
+
test('without a CTA the panel keeps its original scrolling and no layout class', async () => {
|
|
150
|
+
const wrapper = await mountDrawer({});
|
|
143
151
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
).
|
|
152
|
+
const sidebar = wrapper.get('.c-sidebar');
|
|
153
|
+
|
|
154
|
+
expect(sidebar.classes()).toContain('tw-overflow-auto');
|
|
155
|
+
expect(sidebar.classes()).not.toContain('c-sidebar--has-cta');
|
|
148
156
|
});
|
|
149
157
|
|
|
150
158
|
test('closes the drawer when the CTA is clicked', async () => {
|
|
151
159
|
const wrapper = await mountDrawer({
|
|
152
|
-
isMobile: true,
|
|
153
160
|
headerButton: aHeaderButton(),
|
|
154
161
|
});
|
|
155
162
|
|
|
@@ -160,7 +167,6 @@ describe('NavigationSideMenu - header call-to-action button (BP-5852)', () => {
|
|
|
160
167
|
|
|
161
168
|
test('a preview CTA click is inert: no navigation, drawer stays open', async () => {
|
|
162
169
|
const wrapper = await mountDrawer({
|
|
163
|
-
isMobile: true,
|
|
164
170
|
headerButton: aHeaderButton(),
|
|
165
171
|
isPreview: true,
|
|
166
172
|
});
|
|
@@ -2,6 +2,7 @@ import type { PropType } from 'vue';
|
|
|
2
2
|
import { defineComponent } from 'vue';
|
|
3
3
|
import type {
|
|
4
4
|
BrandPageViewModel,
|
|
5
|
+
ButtonViewModel,
|
|
5
6
|
ContentViewModel,
|
|
6
7
|
PageViewModel,
|
|
7
8
|
} from '@brand-page/view-models';
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
DEFAULT_FONT_FAMILY_KEY,
|
|
15
16
|
FALLBACK_FONT_FAMILIES,
|
|
16
17
|
FONT_FAMILY_CONFIGS,
|
|
18
|
+
headerRowRendersCta,
|
|
17
19
|
} from '../../constants';
|
|
18
20
|
import {
|
|
19
21
|
getSlugFromContext,
|
|
@@ -370,6 +372,21 @@ export default defineComponent({
|
|
|
370
372
|
);
|
|
371
373
|
},
|
|
372
374
|
|
|
375
|
+
// The mobile drawer carries the header CTA exactly when the header row does
|
|
376
|
+
// not — one shared predicate so the two can never render it twice.
|
|
377
|
+
drawerHeaderButtonViewModel(): ButtonViewModel | undefined {
|
|
378
|
+
const header = this.viewModel.headerViewModelMobile;
|
|
379
|
+
|
|
380
|
+
if (
|
|
381
|
+
!header.headerButtonViewModel ||
|
|
382
|
+
headerRowRendersCta(header.displayStyle, this.shouldShowNavigation)
|
|
383
|
+
) {
|
|
384
|
+
return undefined;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return header.headerButtonViewModel;
|
|
388
|
+
},
|
|
389
|
+
|
|
373
390
|
shouldShowNavigation(): boolean {
|
|
374
391
|
// Show navigation if more than 1 non-hidden page exists, or if there's 1 page with non-hidden subpages
|
|
375
392
|
const visibleNavItems = this.context.nav.navItems.filter(
|
|
@@ -296,8 +296,7 @@
|
|
|
296
296
|
:page-navigation="viewModel.nav"
|
|
297
297
|
@close-side-menu="closeSideMenu"
|
|
298
298
|
/>
|
|
299
|
-
<!-- No header CTA
|
|
300
|
-
already renders it (BP-5852 placement spec). -->
|
|
299
|
+
<!-- No header CTA: the desktop hamburger header row renders it. -->
|
|
301
300
|
</template>
|
|
302
301
|
<template #mobile>
|
|
303
302
|
<NavigationSideMenu
|
|
@@ -306,10 +305,7 @@
|
|
|
306
305
|
:slug="slug"
|
|
307
306
|
:styles="viewModel.headerViewModelMobile.navigation"
|
|
308
307
|
:page-navigation="viewModel.nav"
|
|
309
|
-
:header-button-view-model="
|
|
310
|
-
viewModel.headerViewModelMobile.headerButtonViewModel
|
|
311
|
-
"
|
|
312
|
-
:is-mobile="true"
|
|
308
|
+
:header-button-view-model="drawerHeaderButtonViewModel"
|
|
313
309
|
@close-side-menu="closeSideMenu"
|
|
314
310
|
/>
|
|
315
311
|
</template>
|
|
@@ -43,6 +43,7 @@ const Template: StoryFn = (args) => ({
|
|
|
43
43
|
nav-id="nav-1"
|
|
44
44
|
:show-nav="args.showNav"
|
|
45
45
|
:display-style="args.displayStyle"
|
|
46
|
+
:has-cta="args.hasCta"
|
|
46
47
|
logo-width="100px"
|
|
47
48
|
>
|
|
48
49
|
<template v-slot:logo>
|
|
@@ -51,6 +52,9 @@ const Template: StoryFn = (args) => ({
|
|
|
51
52
|
<template v-slot:nav>
|
|
52
53
|
<div style="border: 1px solid red; display: inline-block;">Nav | Nav | Nav | Nav | Nav | Nav | Nav | Nav</div>
|
|
53
54
|
</template>
|
|
55
|
+
<template v-slot:cta>
|
|
56
|
+
<div style="border: 1px solid green; display: inline-block; padding: 8px 16px;">Contact Us</div>
|
|
57
|
+
</template>
|
|
54
58
|
</HeaderLayout>
|
|
55
59
|
</div>
|
|
56
60
|
`,
|
|
@@ -60,3 +64,9 @@ export const Sample = Template.bind({});
|
|
|
60
64
|
Sample.args = {
|
|
61
65
|
displayStyle: HEADER_DISPLAY.DESKTOP_TAB_5,
|
|
62
66
|
};
|
|
67
|
+
|
|
68
|
+
export const WithHeaderButton = Template.bind({});
|
|
69
|
+
WithHeaderButton.args = {
|
|
70
|
+
displayStyle: HEADER_DISPLAY.DESKTOP_TAB_1,
|
|
71
|
+
hasCta: true,
|
|
72
|
+
};
|
|
@@ -17,18 +17,14 @@
|
|
|
17
17
|
<slot name="nav" />
|
|
18
18
|
</div>
|
|
19
19
|
</div>
|
|
20
|
-
<div
|
|
21
|
-
v-if="showCta"
|
|
22
|
-
data-component="header-layout-cta"
|
|
23
|
-
class="cta-container"
|
|
24
|
-
>
|
|
20
|
+
<div v-if="showCta" data-test-header-layout-cta class="cta-container">
|
|
25
21
|
<slot name="cta" />
|
|
26
22
|
</div>
|
|
27
23
|
</header>
|
|
28
24
|
</template>
|
|
29
25
|
<script setup lang="ts">
|
|
30
26
|
import { computed, withDefaults } from 'vue';
|
|
31
|
-
import {
|
|
27
|
+
import { headerRowRendersCta, type HeaderDisplayStyle } from '../../constants';
|
|
32
28
|
|
|
33
29
|
const props = withDefaults(
|
|
34
30
|
defineProps<{
|
|
@@ -44,25 +40,9 @@ const props = withDefaults(
|
|
|
44
40
|
},
|
|
45
41
|
);
|
|
46
42
|
|
|
47
|
-
// The mobile hamburger row is deliberately CTA-free: the drawer carries the button
|
|
48
|
-
// (NavigationSideMenu) so the compact row keeps only the logo and the icon. When the
|
|
49
|
-
// nav itself is hidden (single-page site) there is no icon and no drawer, so the row
|
|
50
|
-
// carries the button after all — otherwise it would be unreachable on mobile.
|
|
51
|
-
const DRAWER_ONLY_CTA_DISPLAY_STYLES: HeaderDisplayStyle[] = [
|
|
52
|
-
HEADER_DISPLAY.MOBILE_HAMBURGER_1,
|
|
53
|
-
HEADER_DISPLAY.MOBILE_HAMBURGER_2,
|
|
54
|
-
HEADER_DISPLAY.MOBILE_HAMBURGER_3,
|
|
55
|
-
HEADER_DISPLAY.MOBILE_HAMBURGER_4,
|
|
56
|
-
HEADER_DISPLAY.MOBILE_HAMBURGER_NO_LOGO_1,
|
|
57
|
-
HEADER_DISPLAY.MOBILE_HAMBURGER_NO_LOGO_2,
|
|
58
|
-
HEADER_DISPLAY.MOBILE_HAMBURGER_NO_LOGO_3,
|
|
59
|
-
];
|
|
60
|
-
|
|
61
43
|
const showCta = computed(
|
|
62
44
|
(): boolean =>
|
|
63
|
-
props.hasCta &&
|
|
64
|
-
(props.showNav === false ||
|
|
65
|
-
!DRAWER_ONLY_CTA_DISPLAY_STYLES.includes(props.displayStyle)),
|
|
45
|
+
props.hasCta && headerRowRendersCta(props.displayStyle, props.showNav),
|
|
66
46
|
);
|
|
67
47
|
</script>
|
|
68
48
|
<style scoped>
|
|
@@ -413,15 +393,11 @@ const showCta = computed(
|
|
|
413
393
|
}
|
|
414
394
|
|
|
415
395
|
/* ==========================================================================
|
|
416
|
-
Header CTA placement
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
join - inline immediately after the nav links
|
|
422
|
-
edge - pinned to the header's outer right edge, nav left untouched
|
|
423
|
-
corner - floats top-right, out of flow, independent of a centered stack
|
|
424
|
-
row - full-width row below the whole stack (mobile bar nav)
|
|
396
|
+
Header CTA placement. Every rule is additive and gated on the CTA being
|
|
397
|
+
rendered (`.has-header-cta` / `.cta-container`), so a header without the
|
|
398
|
+
button lays out exactly as before. The placement archetypes (join / edge /
|
|
399
|
+
corner / row) and the per-family reasoning live in
|
|
400
|
+
docs/architecture/capabilities/page-header.md.
|
|
425
401
|
========================================================================== */
|
|
426
402
|
|
|
427
403
|
/* join - DESKTOP_TAB_1: appended flex child; the nav's `flex: 1` right-aligns the
|
|
@@ -468,13 +444,26 @@ const showCta = computed(
|
|
|
468
444
|
right: 0;
|
|
469
445
|
}
|
|
470
446
|
|
|
471
|
-
/* row - MOBILE_TAB_1
|
|
472
|
-
|
|
447
|
+
/* row - MOBILE_TAB_1: auto-places into an implicit third grid row below the stack
|
|
448
|
+
(both explicit rows are taken by the logo and the nav), giving logo / nav / button. */
|
|
473
449
|
.mobile-tab-vertical-stack-centered-logo-centered-menu .cta-container,
|
|
474
450
|
.mobile-tab-vertical-stack-centered-menu-centered-logo .cta-container {
|
|
475
451
|
width: 100%;
|
|
476
452
|
justify-self: stretch;
|
|
477
453
|
}
|
|
454
|
+
|
|
455
|
+
/* row - MOBILE_TAB_2: the button belongs directly under the nav and ABOVE the logo,
|
|
456
|
+
so the grid grows a middle row for it and the logo's existing `row2` names move
|
|
457
|
+
down to the third. Auto-placement would otherwise drop it below the logo. */
|
|
458
|
+
.mobile-tab-vertical-stack-centered-menu-centered-logo.show-nav.has-header-cta {
|
|
459
|
+
grid-template-rows:
|
|
460
|
+
[row1-start] var(--tab-nav-height)
|
|
461
|
+
[row1-end cta-start] auto [cta-end row2-start] 1fr [row2-end];
|
|
462
|
+
}
|
|
463
|
+
.mobile-tab-vertical-stack-centered-menu-centered-logo.show-nav.has-header-cta
|
|
464
|
+
.cta-container {
|
|
465
|
+
grid-row: cta-start / cta-end;
|
|
466
|
+
}
|
|
478
467
|
.mobile-tab-vertical-stack-centered-logo-centered-menu.no-nav.has-header-cta,
|
|
479
468
|
.mobile-tab-vertical-stack-centered-menu-centered-logo.no-nav.has-header-cta {
|
|
480
469
|
@apply tw-flex-wrap tw-justify-center;
|
|
@@ -525,7 +514,18 @@ const showCta = computed(
|
|
|
525
514
|
.desktop-hamburger-horizontal-stack-left-logo-right-menu .cta-container,
|
|
526
515
|
.desktop-hamburger-horizontal-stack-centered-logo-right-menu .cta-container {
|
|
527
516
|
grid-column: cta-start;
|
|
528
|
-
|
|
517
|
+
/* Explicit row: the CTA sits in DOM order after the nav, and sparse grid
|
|
518
|
+
auto-placement never backtracks to an earlier column on the same row —
|
|
519
|
+
without this the button wraps to an implicit second row. */
|
|
520
|
+
grid-row: 1;
|
|
521
|
+
/* Centre the button on the hamburger icon's line: the icon is start-aligned
|
|
522
|
+
with the nav's --page-padding margin, so mirror that margin and let the
|
|
523
|
+
(taller) button overflow a line of the icon's height evenly. */
|
|
524
|
+
align-self: start;
|
|
525
|
+
margin-top: var(--page-padding);
|
|
526
|
+
height: var(--hamburger-nav-height);
|
|
527
|
+
display: flex;
|
|
528
|
+
align-items: center;
|
|
529
529
|
@apply tw-mr-4;
|
|
530
530
|
}
|
|
531
531
|
|
|
@@ -60,9 +60,9 @@ export default defineComponent({
|
|
|
60
60
|
onHeaderContainerClick() {
|
|
61
61
|
this.$emit('header-container-click');
|
|
62
62
|
},
|
|
63
|
-
// In the maker preview the CTA is inert (
|
|
64
|
-
// prevent lands before the inner anchor/router-link fires, and the stop
|
|
65
|
-
// click from selecting the header panel.
|
|
63
|
+
// In the maker preview the CTA is inert (see docs/decisions/0019): capture-phase
|
|
64
|
+
// so the prevent lands before the inner anchor/router-link fires, and the stop
|
|
65
|
+
// keeps the click from selecting the header panel. Published sites: no-op.
|
|
66
66
|
onHeaderCtaClick(event: MouseEvent): void {
|
|
67
67
|
if (this.context.isPreview) {
|
|
68
68
|
event.preventDefault();
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from '@src/brand-page/constants';
|
|
20
20
|
import type { NavItemViewModel } from '@brand-page/view-models';
|
|
21
21
|
import { mapToPageHeaderViewModel } from '@brand-page/utils';
|
|
22
|
+
import { setLocaleAsync } from '@src/composables/useTranslate';
|
|
22
23
|
|
|
23
24
|
// NavigationCarousel calls `new ResizeObserver(...)`, so this has to be constructible.
|
|
24
25
|
global.ResizeObserver = class {
|
|
@@ -148,6 +149,11 @@ const mountHeader = async (options: {
|
|
|
148
149
|
};
|
|
149
150
|
|
|
150
151
|
describe('PageHeader - header call-to-action button (BP-5852)', () => {
|
|
152
|
+
// the empty-label fallback resolves through i18n
|
|
153
|
+
beforeAll(async () => {
|
|
154
|
+
await setLocaleAsync('en-US');
|
|
155
|
+
});
|
|
156
|
+
|
|
151
157
|
beforeEach(() => {
|
|
152
158
|
setActivePinia(createPinia());
|
|
153
159
|
});
|
|
@@ -157,9 +163,9 @@ describe('PageHeader - header call-to-action button (BP-5852)', () => {
|
|
|
157
163
|
const wrapper = await mountHeader({});
|
|
158
164
|
|
|
159
165
|
expect(wrapper.find(CTA).exists()).toBe(false);
|
|
160
|
-
expect(
|
|
161
|
-
|
|
162
|
-
)
|
|
166
|
+
expect(wrapper.find('[data-test-header-layout-cta]').exists()).toBe(
|
|
167
|
+
false,
|
|
168
|
+
);
|
|
163
169
|
});
|
|
164
170
|
|
|
165
171
|
test('renders no CTA when the button is configured but hidden', async () => {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HEADER_DISPLAY,
|
|
3
|
+
headerRowRendersCta,
|
|
4
|
+
} from './header-display.constants';
|
|
5
|
+
|
|
6
|
+
// One predicate decides header-row vs drawer for the CTA; Page.mixin uses it
|
|
7
|
+
// inverted for the drawer, so these cases pin that the two can never overlap.
|
|
8
|
+
describe('headerRowRendersCta', () => {
|
|
9
|
+
test.each([
|
|
10
|
+
HEADER_DISPLAY.MOBILE_HAMBURGER_1,
|
|
11
|
+
HEADER_DISPLAY.MOBILE_HAMBURGER_2,
|
|
12
|
+
HEADER_DISPLAY.MOBILE_HAMBURGER_3,
|
|
13
|
+
HEADER_DISPLAY.MOBILE_HAMBURGER_4,
|
|
14
|
+
HEADER_DISPLAY.MOBILE_HAMBURGER_NO_LOGO_1,
|
|
15
|
+
HEADER_DISPLAY.MOBILE_HAMBURGER_NO_LOGO_2,
|
|
16
|
+
HEADER_DISPLAY.MOBILE_HAMBURGER_NO_LOGO_3,
|
|
17
|
+
])('%s with a visible nav: the drawer carries the CTA', (displayStyle) => {
|
|
18
|
+
expect(headerRowRendersCta(displayStyle, true)).toBe(false);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test.each([
|
|
22
|
+
HEADER_DISPLAY.MOBILE_HAMBURGER_1,
|
|
23
|
+
HEADER_DISPLAY.MOBILE_HAMBURGER_NO_LOGO_1,
|
|
24
|
+
])(
|
|
25
|
+
'%s with the nav hidden: no drawer exists, the row carries it',
|
|
26
|
+
(displayStyle) => {
|
|
27
|
+
expect(headerRowRendersCta(displayStyle, false)).toBe(true);
|
|
28
|
+
},
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
test.each([
|
|
32
|
+
HEADER_DISPLAY.DESKTOP_HAMBURGER_1,
|
|
33
|
+
HEADER_DISPLAY.DESKTOP_HAMBURGER_2,
|
|
34
|
+
HEADER_DISPLAY.DESKTOP_TAB_1,
|
|
35
|
+
HEADER_DISPLAY.MOBILE_TAB_1,
|
|
36
|
+
HEADER_DISPLAY.MOBILE_TAB_2,
|
|
37
|
+
HEADER_DISPLAY.MOBILE_TAB_NO_LOGO_1,
|
|
38
|
+
])('%s: the row carries the CTA', (displayStyle) => {
|
|
39
|
+
expect(headerRowRendersCta(displayStyle, true)).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('an unknown/deprecated display style defaults to the row', () => {
|
|
43
|
+
expect(headerRowRendersCta('align-left', true)).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -442,3 +442,22 @@ export const HEADER_DISPLAY_STYLE_CONFIGS: HeaderDisplayStyleConfig[] = [
|
|
|
442
442
|
},
|
|
443
443
|
},
|
|
444
444
|
];
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Whether the header ROW renders the call-to-action button, as opposed to the
|
|
448
|
+
* mobile nav drawer. The compact mobile hamburger row hands the CTA to the
|
|
449
|
+
* drawer — unless the nav is hidden (single visible page), when there is no
|
|
450
|
+
* drawer and the row must carry it. Derived from the configs so a future
|
|
451
|
+
* style classifies itself by its own viewMode/navType.
|
|
452
|
+
*/
|
|
453
|
+
export const headerRowRendersCta = (
|
|
454
|
+
displayStyle: string,
|
|
455
|
+
showNav: boolean,
|
|
456
|
+
): boolean =>
|
|
457
|
+
!showNav ||
|
|
458
|
+
!HEADER_DISPLAY_STYLE_CONFIGS.some(
|
|
459
|
+
(config) =>
|
|
460
|
+
config.option.value === displayStyle &&
|
|
461
|
+
config.viewMode === 'mobile' &&
|
|
462
|
+
config.navType === 'hamburger',
|
|
463
|
+
);
|
|
@@ -753,8 +753,11 @@ export abstract class AbstractPageHeaderStyles {
|
|
|
753
753
|
style: BrandPageCssStyle;
|
|
754
754
|
viewport: ViewportsType;
|
|
755
755
|
|
|
756
|
-
/** Absent means no header call-to-action button — never seed a default.
|
|
757
|
-
|
|
756
|
+
/** Absent means no header call-to-action button — never seed a default.
|
|
757
|
+
* `declare` keeps the class field type-only: a plain optional field is still
|
|
758
|
+
* DEFINED on every instance (an own `undefined` key), which would change the
|
|
759
|
+
* serialized shape released migrations receive. */
|
|
760
|
+
declare headerButton?: HeaderButton;
|
|
758
761
|
|
|
759
762
|
protected constructor(options: AbstractPageHeaderStylesType = {}) {
|
|
760
763
|
if (typeof options.viewport === 'undefined') {
|
|
@@ -794,7 +797,11 @@ export abstract class AbstractPageHeaderStyles {
|
|
|
794
797
|
backgroundOpacity: options.backgroundStyle?.backgroundOpacity ?? 100,
|
|
795
798
|
};
|
|
796
799
|
|
|
797
|
-
|
|
800
|
+
// Assign only when present: constructor output is part of released
|
|
801
|
+
// migrations' input contract, so an absent field must stay absent.
|
|
802
|
+
if (options.headerButton) {
|
|
803
|
+
this.headerButton = options.headerButton;
|
|
804
|
+
}
|
|
798
805
|
}
|
|
799
806
|
}
|
|
800
807
|
|
|
@@ -156,7 +156,6 @@ exports[`migration.util > Banner Contents - text Align and Font Family should no
|
|
|
156
156
|
"backgroundOpacity": 100,
|
|
157
157
|
},
|
|
158
158
|
"displayStyle": "desktop-tab-vertical-stack-centered-logo-centered-menu",
|
|
159
|
-
"headerButton": undefined,
|
|
160
159
|
"isLogoOptional": false,
|
|
161
160
|
"isLogoVisible": true,
|
|
162
161
|
"logoImage": {
|
|
@@ -232,7 +231,6 @@ exports[`migration.util > Banner Contents - text Align and Font Family should no
|
|
|
232
231
|
"backgroundOpacity": 100,
|
|
233
232
|
},
|
|
234
233
|
"displayStyle": "mobile-tab-vertical-stack-centered-logo-centered-menu",
|
|
235
|
-
"headerButton": undefined,
|
|
236
234
|
"isLogoOptional": false,
|
|
237
235
|
"isLogoVisible": true,
|
|
238
236
|
"logoImage": {
|
|
@@ -655,7 +653,6 @@ exports[`migration.util > migrateBrandPageModel should return brand page model w
|
|
|
655
653
|
"backgroundOpacity": 100,
|
|
656
654
|
},
|
|
657
655
|
"displayStyle": "desktop-tab-vertical-stack-centered-logo-centered-menu",
|
|
658
|
-
"headerButton": undefined,
|
|
659
656
|
"isLogoOptional": false,
|
|
660
657
|
"isLogoVisible": true,
|
|
661
658
|
"logoImage": {
|
|
@@ -731,7 +728,6 @@ exports[`migration.util > migrateBrandPageModel should return brand page model w
|
|
|
731
728
|
"backgroundOpacity": 100,
|
|
732
729
|
},
|
|
733
730
|
"displayStyle": "mobile-tab-vertical-stack-centered-logo-centered-menu",
|
|
734
|
-
"headerButton": undefined,
|
|
735
731
|
"isLogoOptional": false,
|
|
736
732
|
"isLogoVisible": true,
|
|
737
733
|
"logoImage": {
|
|
@@ -1138,7 +1134,6 @@ exports[`migration.util > migrateBrandPageModel should return brand page model w
|
|
|
1138
1134
|
"backgroundOpacity": 100,
|
|
1139
1135
|
},
|
|
1140
1136
|
"displayStyle": "desktop-tab-vertical-stack-centered-logo-centered-menu",
|
|
1141
|
-
"headerButton": undefined,
|
|
1142
1137
|
"isLogoOptional": false,
|
|
1143
1138
|
"isLogoVisible": true,
|
|
1144
1139
|
"logoImage": {
|
|
@@ -1214,7 +1209,6 @@ exports[`migration.util > migrateBrandPageModel should return brand page model w
|
|
|
1214
1209
|
"backgroundOpacity": 100,
|
|
1215
1210
|
},
|
|
1216
1211
|
"displayStyle": "mobile-tab-vertical-stack-centered-logo-centered-menu",
|
|
1217
|
-
"headerButton": undefined,
|
|
1218
1212
|
"isLogoOptional": false,
|
|
1219
1213
|
"isLogoVisible": true,
|
|
1220
1214
|
"logoImage": {
|
|
@@ -141,7 +141,6 @@ exports[`migration02.ts > migration02 should return converted letterspacing 1`]
|
|
|
141
141
|
"backgroundOpacity": 100,
|
|
142
142
|
},
|
|
143
143
|
"displayStyle": "desktop-tab-vertical-stack-centered-logo-centered-menu",
|
|
144
|
-
"headerButton": undefined,
|
|
145
144
|
"isLogoOptional": false,
|
|
146
145
|
"isLogoVisible": true,
|
|
147
146
|
"logoDraftToken": "",
|
|
@@ -218,7 +217,6 @@ exports[`migration02.ts > migration02 should return converted letterspacing 1`]
|
|
|
218
217
|
"backgroundOpacity": 100,
|
|
219
218
|
},
|
|
220
219
|
"displayStyle": "mobile-tab-vertical-stack-centered-logo-centered-menu",
|
|
221
|
-
"headerButton": undefined,
|
|
222
220
|
"isLogoOptional": false,
|
|
223
221
|
"isLogoVisible": true,
|
|
224
222
|
"logoDraftToken": "",
|