weg-shared-layout 0.0.54 → 0.0.58
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/dist/cjs/weg-footer_2.cjs.entry.js +58 -9
- package/dist/collection/components/weg-header/weg-header.js +59 -10
- package/dist/components/weg-header.js +1 -1
- package/dist/esm/weg-footer_2.entry.js +58 -9
- package/dist/types/components/weg-header/weg-header.d.ts +8 -2
- package/dist/types/components.d.ts +2 -2
- package/dist/weg-shared-layout/p-52294a6e.entry.js +1 -0
- package/dist/weg-shared-layout/weg-shared-layout.esm.js +1 -1
- package/docs/README.md +10 -7
- package/docs/angular.md +10 -7
- package/docs/nextjs.md +24 -11
- package/docs/publishing.md +8 -8
- package/docs/react.md +7 -12
- package/docs/vanilla.md +15 -8
- package/hydrate/index.js +58 -9
- package/hydrate/index.mjs +58 -9
- package/package.json +1 -1
- package/readme.md +71 -49
- package/dist/weg-shared-layout/p-699bc3aa.entry.js +0 -1
package/docs/react.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# React (SPA)
|
|
2
2
|
|
|
3
|
-
Guide for **client-rendered** React apps (Vite, Create React App,
|
|
3
|
+
Guide for **client-rendered** React apps (Vite, Create React App, and similar). If you use **Next.js App Router**, see **[Next.js](./nextjs.md)** because SSR and Server Components need a different integration.
|
|
4
4
|
|
|
5
5
|
## Components
|
|
6
6
|
|
|
@@ -9,7 +9,7 @@ Guide for **client-rendered** React apps (Vite, Create React App, etc.). If you
|
|
|
9
9
|
| `<weg-header>` | Site header — bundled logo, CMS nav (signed out), built-in nav (signed in), Sign in / Manage Account / Sign out |
|
|
10
10
|
| `<weg-footer>` | Site footer — social links, menu, credits, copyright |
|
|
11
11
|
|
|
12
|
-
Both are **presentational** [Stencil](https://stenciljs.com/) Web Components. They **do not fetch data
|
|
12
|
+
Both are **presentational** [Stencil](https://stenciljs.com/) Web Components. They **do not fetch data**. Your app passes a **`layout`** payload from the WEG CMS WEG21 API (for example via [`fetchWeg21Layout`](../src/utils/menus.ts)) or from the bundled offline fixtures [`weg21-bootstrap.json`](../src/assets/weg21-bootstrap.json) and [`weg21-menus.json`](../src/assets/weg21-menus.json).
|
|
13
13
|
|
|
14
14
|
`<weg-header>` additionally accepts **`signed-in`**, **`user-name`**, **`account-base-url`**, **`current-path`**, and emits **`wegAuthClick`**.
|
|
15
15
|
|
|
@@ -80,7 +80,7 @@ Sign-in is a normal `header.menu` link — add it in your CMS like Register. The
|
|
|
80
80
|
|
|
81
81
|
### Signed in
|
|
82
82
|
|
|
83
|
-
Set **`signed-in`** and optionally **`user-name`**.
|
|
83
|
+
Set **`signed-in`** and optionally **`user-name`**. If `layout.header.menu` is present, the component keeps using that menu while switching to signed-in UI. If `layout.header.menu` is empty, it falls back to the built-in signed-in menu: Find a job, Dashboard, Manage Account, and Sign out. Pass **`account-base-url`** when those fallback account links should use a non-production portal.
|
|
84
84
|
|
|
85
85
|
Handle **`wegAuthClick`** for sign-out (call your logout API, then redirect):
|
|
86
86
|
|
|
@@ -122,7 +122,7 @@ Without React Router, pass `window.location.pathname` (+ `search` when needed) o
|
|
|
122
122
|
|
|
123
123
|
| Prop / event | Purpose |
|
|
124
124
|
| --- | --- |
|
|
125
|
-
| `signedIn={boolean}` |
|
|
125
|
+
| `signedIn={boolean}` | Enables signed-in UI; the built-in signed-in fallback menu is used when `layout.header.menu` is empty |
|
|
126
126
|
| `userName={string}` | First name beside profile icon on Manage Account |
|
|
127
127
|
| `accountBaseUrl={string}` | Account portal origin for signed-in links (optional) |
|
|
128
128
|
| `currentPath={string}` | Current path — highlights active nav (e.g. `/career-advice/foo`) |
|
|
@@ -180,23 +180,17 @@ const currentPath = search ? `${pathname}${search}` : pathname;
|
|
|
180
180
|
import { useEffect, useState } from 'react';
|
|
181
181
|
import 'weg-shared-layout/weg-header';
|
|
182
182
|
import 'weg-shared-layout/weg-footer';
|
|
183
|
-
import { dummyWeg21LayoutData } from 'weg-shared-layout/menus';
|
|
183
|
+
import { dummyWeg21LayoutData, fetchWeg21Layout } from 'weg-shared-layout/menus';
|
|
184
184
|
const layoutFixture = dummyWeg21LayoutData();
|
|
185
185
|
|
|
186
186
|
type LayoutData = typeof layoutFixture;
|
|
187
187
|
|
|
188
|
-
const WEG21_API = 'https://warwickemploymentgroup.com/api/v1/weg21';
|
|
189
|
-
|
|
190
188
|
export function SiteLayout({ children }: { children: React.ReactNode }) {
|
|
191
189
|
const [layout, setLayout] = useState<LayoutData | null>(null);
|
|
192
190
|
|
|
193
191
|
useEffect(() => {
|
|
194
192
|
let cancelled = false;
|
|
195
|
-
|
|
196
|
-
.then((res) => {
|
|
197
|
-
if (!res.ok) throw new Error(`layout fetch failed: ${res.status}`);
|
|
198
|
-
return res.json() as Promise<LayoutData>;
|
|
199
|
-
})
|
|
193
|
+
fetchWeg21Layout({ apiKey: import.meta.env.VITE_WCMS_API_KEY })
|
|
200
194
|
.then((data) => {
|
|
201
195
|
if (!cancelled) setLayout(data);
|
|
202
196
|
})
|
|
@@ -277,6 +271,7 @@ declare module 'react' {
|
|
|
277
271
|
| Empty despite correct data | React set `layout` as attribute | React 19+, or `JSON.stringify`, or ref assignment. |
|
|
278
272
|
| Logo missing on header | Old build without inlined logo | Upgrade package; logo is bundled in `logo-data.ts`. |
|
|
279
273
|
| Auth always Sign in | `signed-in` not set | Bind `signedIn={!!session}`. |
|
|
274
|
+
| Signed-in links are not what you expected | `layout.header.menu` is still being used | Leave `layout.header.menu` empty if you want the built-in signed-in fallback nav. |
|
|
280
275
|
| Manage Account shows label not name | `user-name` not set | Pass `userName` when signed in. |
|
|
281
276
|
| Nav never highlights active page | `current-path` not set | Pass `currentPath` from your router (pathname + query when needed). |
|
|
282
277
|
| `onWegAuthClick` not firing | React CE event binding | Use `addEventListener` on a ref. |
|
package/docs/vanilla.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
| `<weg-header>` | Site header — bundled logo, CMS nav (signed out), built-in nav (signed in), Sign in / Manage Account / Sign out |
|
|
8
8
|
| `<weg-footer>` | Site footer — social links, menu, credits, copyright |
|
|
9
9
|
|
|
10
|
-
Both components are **presentational**: they do **not** fetch data. Pass the same `layout` payload to
|
|
10
|
+
Both components are **presentational**: they do **not** fetch data. Pass the same `layout` payload to both.
|
|
11
11
|
|
|
12
12
|
WEG21 API schema: [`weg21-bootstrap.json`](../src/assets/weg21-bootstrap.json), [`weg21-menus.json`](../src/assets/weg21-menus.json)
|
|
13
13
|
|
|
@@ -82,7 +82,7 @@ Renders `header.menu` from `layout`. Add sign-in as a normal link — `{ "label"
|
|
|
82
82
|
|
|
83
83
|
### Signed in
|
|
84
84
|
|
|
85
|
-
Set **`signedIn`** and optionally **`userName`**.
|
|
85
|
+
Set **`signedIn`** and optionally **`userName`**. If `layout.header.menu` is present, the component keeps using that menu while switching to signed-in UI. If the menu is empty, the built-in signed-in fallback menu is shown instead: Find a job, Dashboard, Manage Account, and Sign out. Set **`accountBaseUrl`** when those fallback account links should use a non-production portal.
|
|
86
86
|
|
|
87
87
|
```js
|
|
88
88
|
header.signedIn = true;
|
|
@@ -138,14 +138,21 @@ Logo **image** uses `header.logoSrc` (bundled if omitted). Logo **link** uses `h
|
|
|
138
138
|
|
|
139
139
|
## Without a bundler
|
|
140
140
|
|
|
141
|
-
|
|
141
|
+
If you are not using a bundler, you cannot import `weg-shared-layout/menus` directly from the package. In that case, fetch or embed a pre-mapped `layout` object yourself, or copy the same mapping logic as [`menusToLayoutData`](../src/utils/menus.ts) into your app.
|
|
142
|
+
|
|
143
|
+
At minimum, the object you assign still needs the same final shape:
|
|
142
144
|
|
|
143
145
|
```js
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
]
|
|
148
|
-
|
|
146
|
+
const layout = {
|
|
147
|
+
header: { menu: [...] },
|
|
148
|
+
footer: {
|
|
149
|
+
menu: [...],
|
|
150
|
+
social: [...],
|
|
151
|
+
credits: '...',
|
|
152
|
+
copyright: '...',
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
|
|
149
156
|
document.getElementById('header').layout = layout;
|
|
150
157
|
document.getElementById('footer').layout = layout;
|
|
151
158
|
```
|
package/hydrate/index.js
CHANGED
|
@@ -5920,8 +5920,8 @@ class WegHeader {
|
|
|
5920
5920
|
*/
|
|
5921
5921
|
userName;
|
|
5922
5922
|
/**
|
|
5923
|
-
* Account portal origin for
|
|
5924
|
-
* Defaults to production when omitted.
|
|
5923
|
+
* Account portal origin for the built-in signed-in fallback menu (when `header.menu` is empty).
|
|
5924
|
+
* Menu links from the CMS use their `href` as-is. Defaults to production when omitted.
|
|
5925
5925
|
*/
|
|
5926
5926
|
accountBaseUrl;
|
|
5927
5927
|
/**
|
|
@@ -6019,13 +6019,43 @@ class WegHeader {
|
|
|
6019
6019
|
getMenuToggleButton() {
|
|
6020
6020
|
return this.el.shadowRoot?.querySelector('.menu-toggle');
|
|
6021
6021
|
}
|
|
6022
|
+
static FOCUSABLE_SELECTOR = 'a[href]:not([tabindex="-1"]), button:not([disabled]):not([tabindex="-1"])';
|
|
6022
6023
|
getFirstNavFocusable() {
|
|
6023
6024
|
const panel = this.el.shadowRoot?.querySelector('#weg-header-main-nav');
|
|
6024
6025
|
if (!panel)
|
|
6025
6026
|
return null;
|
|
6026
|
-
const focusables = panel.querySelectorAll(
|
|
6027
|
+
const focusables = panel.querySelectorAll(WegHeader.FOCUSABLE_SELECTOR);
|
|
6027
6028
|
return focusables[0] ?? null;
|
|
6028
6029
|
}
|
|
6030
|
+
/** Focusable controls inside the open mobile menu chrome (toggle → logo → auth → nav). */
|
|
6031
|
+
getMobileMenuFocusables() {
|
|
6032
|
+
const header = this.el.shadowRoot?.querySelector('.header');
|
|
6033
|
+
if (!header)
|
|
6034
|
+
return [];
|
|
6035
|
+
return Array.from(header.querySelectorAll(WegHeader.FOCUSABLE_SELECTOR));
|
|
6036
|
+
}
|
|
6037
|
+
trapMobileMenuFocus(event) {
|
|
6038
|
+
if (!this.isMobileMenuActive() || event.key !== 'Tab')
|
|
6039
|
+
return;
|
|
6040
|
+
const focusables = this.getMobileMenuFocusables();
|
|
6041
|
+
if (focusables.length === 0)
|
|
6042
|
+
return;
|
|
6043
|
+
const first = focusables[0];
|
|
6044
|
+
const last = focusables[focusables.length - 1];
|
|
6045
|
+
const active = this.el.shadowRoot?.activeElement;
|
|
6046
|
+
const activeInTrap = active != null && focusables.includes(active);
|
|
6047
|
+
if (event.shiftKey) {
|
|
6048
|
+
if (!activeInTrap || active === first) {
|
|
6049
|
+
event.preventDefault();
|
|
6050
|
+
last.focus();
|
|
6051
|
+
}
|
|
6052
|
+
return;
|
|
6053
|
+
}
|
|
6054
|
+
if (!activeInTrap || active === last) {
|
|
6055
|
+
event.preventDefault();
|
|
6056
|
+
first.focus();
|
|
6057
|
+
}
|
|
6058
|
+
}
|
|
6029
6059
|
focusFirstNavItem() {
|
|
6030
6060
|
setTimeout(() => {
|
|
6031
6061
|
const attemptFocus = (retries = 8) => {
|
|
@@ -6137,6 +6167,10 @@ class WegHeader {
|
|
|
6137
6167
|
this.resolve();
|
|
6138
6168
|
}
|
|
6139
6169
|
handleKeyDown(event) {
|
|
6170
|
+
if (event.key === 'Tab') {
|
|
6171
|
+
this.trapMobileMenuFocus(event);
|
|
6172
|
+
return;
|
|
6173
|
+
}
|
|
6140
6174
|
if (event.key !== 'Escape')
|
|
6141
6175
|
return;
|
|
6142
6176
|
if (this.openDropdown) {
|
|
@@ -6396,6 +6430,20 @@ class WegHeader {
|
|
|
6396
6430
|
const name = this.userName?.trim();
|
|
6397
6431
|
return name || 'Manage Account';
|
|
6398
6432
|
}
|
|
6433
|
+
resolveManageAccountHref(link) {
|
|
6434
|
+
const fromMenu = link?.href?.trim();
|
|
6435
|
+
if (fromMenu)
|
|
6436
|
+
return fromMenu;
|
|
6437
|
+
return accountPath(this.getAccountHome(), '/account/manage');
|
|
6438
|
+
}
|
|
6439
|
+
findManageAccountLink(menu) {
|
|
6440
|
+
const accountHome = this.getAccountHome();
|
|
6441
|
+
for (const item of menu) {
|
|
6442
|
+
if (!isMenuGroup(item) && isManageAccountLink(item, accountHome))
|
|
6443
|
+
return item;
|
|
6444
|
+
}
|
|
6445
|
+
return null;
|
|
6446
|
+
}
|
|
6399
6447
|
shouldExcludeFromNavActive(item, accountHome) {
|
|
6400
6448
|
return isSignOutLink(item) || isSignInLink(item, accountHome);
|
|
6401
6449
|
}
|
|
@@ -6475,9 +6523,9 @@ class WegHeader {
|
|
|
6475
6523
|
[options.className || '']: !!options.className,
|
|
6476
6524
|
}, href: link.href, "aria-label": link.label, onClick: (event) => this.handleAuthClick(event, action, link.href, onNavigate) }, hAsync(Icon, null), !iconOnly ? link.label : null, iconOnly ? hAsync("span", { class: "sr-only" }, link.label) : null));
|
|
6477
6525
|
}
|
|
6478
|
-
renderManageAccountAnchor(onNavigate, iconOnly = false, ariaCurrentHref = null) {
|
|
6526
|
+
renderManageAccountAnchor(onNavigate, iconOnly = false, ariaCurrentHref = null, link) {
|
|
6479
6527
|
const label = this.getManageAccountLabel();
|
|
6480
|
-
const href =
|
|
6528
|
+
const href = this.resolveManageAccountHref(link);
|
|
6481
6529
|
const active = this.isNavItemActive(href, 'prefix');
|
|
6482
6530
|
const ariaCurrent = this.isAriaCurrentHref(href, ariaCurrentHref);
|
|
6483
6531
|
return (hAsync("a", { class: {
|
|
@@ -6499,7 +6547,7 @@ class WegHeader {
|
|
|
6499
6547
|
return (hAsync("li", { class: "main-nav__item main-nav__item--auth", key: "sign-in" }, this.renderAuthControl({ link: item, action: 'sign-in', onNavigate })));
|
|
6500
6548
|
}
|
|
6501
6549
|
if (isManageAccountLink(item, accountHome)) {
|
|
6502
|
-
return (hAsync("li", { class: "main-nav__item main-nav__item--auth", key: "manage-account" }, this.renderManageAccountAnchor(onNavigate, false, ariaCurrentHref)));
|
|
6550
|
+
return (hAsync("li", { class: "main-nav__item main-nav__item--auth", key: "manage-account" }, this.renderManageAccountAnchor(onNavigate, false, ariaCurrentHref, item)));
|
|
6503
6551
|
}
|
|
6504
6552
|
if (this.signedIn && isFindJobLink(item)) {
|
|
6505
6553
|
return this.renderIconNavLink(item, FindJobIcon, 'find-a-job-link', onNavigate, ariaCurrentHref);
|
|
@@ -6515,7 +6563,8 @@ class WegHeader {
|
|
|
6515
6563
|
}
|
|
6516
6564
|
renderCompactAuth(onNavigate) {
|
|
6517
6565
|
if (this.signedIn) {
|
|
6518
|
-
|
|
6566
|
+
const manageLink = this.findManageAccountLink(this.getActiveMenu());
|
|
6567
|
+
return this.renderManageAccountAnchor(onNavigate, true, null, manageLink ?? undefined);
|
|
6519
6568
|
}
|
|
6520
6569
|
const signIn = this.findSignInLink(this.resolved.menu);
|
|
6521
6570
|
if (!signIn?.href)
|
|
@@ -6544,12 +6593,12 @@ class WegHeader {
|
|
|
6544
6593
|
const logoSrc = this.getLogoSrc();
|
|
6545
6594
|
const closeMenu = () => this.closeMenu();
|
|
6546
6595
|
const mobileMenuActive = this.isMobileMenuActive();
|
|
6547
|
-
return (hAsync(Host, { key: '
|
|
6596
|
+
return (hAsync(Host, { key: '916472d15df8abf5270650126356ef2d7a475765', class: { 'weg-header--scroll-mode': this.headerScrollMode } }, hAsync("div", { key: '8c8d63f67cf7b27364cc828aa42c5b011cbb971e', class: "weg-header-shell" }, hAsync("a", { key: '96f6141f0bff7a51bbc8f46ea9b92d0c33142e1a', class: "skip-to-content", href: "#", title: SKIP_TO_CONTENT_LABEL, onClick: (event) => this.handleSkipToContent(event) }, SKIP_TO_CONTENT_LABEL), hAsync("header", { key: '4a727a72e031159ca991505a1a8a59a32271aedf', class: {
|
|
6548
6597
|
header: true,
|
|
6549
6598
|
'header--menu-open': mobileMenuActive,
|
|
6550
6599
|
'header--scroll-mode': this.headerScrollMode,
|
|
6551
6600
|
'header--scroll-hidden': this.headerScrollHidden && this.headerScrollMode,
|
|
6552
|
-
} }, hAsync("div", { key: '
|
|
6601
|
+
} }, hAsync("div", { key: '5ef2a85bf2087206a17e3cbf163dde4d549dcd0b', class: "header-inner" }, hAsync("div", { key: '3af6990b5c6d93b4332a0d53300daddb11e2ad2b', class: "sr-only", "aria-live": "polite", "aria-atomic": "true", "data-weg-sr-live": true }), hAsync("button", { key: '023322f1e49a676a5b20208b956624ce820e9b57', type: "button", class: "menu-toggle icon-button", "aria-label": mobileMenuActive ? CLOSE_MAIN_NAV_LABEL : OPEN_MAIN_NAV_LABEL, "aria-expanded": mobileMenuActive ? 'true' : 'false', "aria-controls": "weg-header-main-nav", onClick: () => this.toggleMenu() }, mobileMenuActive ? hAsync(CloseIcon, null) : hAsync(HamburgerIcon, null)), hAsync(Logo, { key: 'efa51b76936456c49059c67e9f2c9c2365b97d25', href: logoHref, src: logoSrc }), hAsync("div", { key: 'ee0eff5ae3a63cf9550261a4d1727c9a88ab3e4d', class: "header-actions" }, this.renderCompactAuth(mobileMenuActive ? closeMenu : undefined)), hAsync("div", { key: '7552a6a661e644dcb3d555ab5572f680cd0295de', class: "main-nav-panel", id: "weg-header-main-nav", role: mobileMenuActive ? 'dialog' : undefined, "aria-modal": mobileMenuActive ? 'true' : undefined, "aria-label": mobileMenuActive ? MAIN_NAV_LABEL : undefined }, this.renderNav(mobileMenuActive ? closeMenu : undefined)))))));
|
|
6553
6602
|
}
|
|
6554
6603
|
static get watchers() { return {
|
|
6555
6604
|
"layout": [{
|
package/hydrate/index.mjs
CHANGED
|
@@ -5918,8 +5918,8 @@ class WegHeader {
|
|
|
5918
5918
|
*/
|
|
5919
5919
|
userName;
|
|
5920
5920
|
/**
|
|
5921
|
-
* Account portal origin for
|
|
5922
|
-
* Defaults to production when omitted.
|
|
5921
|
+
* Account portal origin for the built-in signed-in fallback menu (when `header.menu` is empty).
|
|
5922
|
+
* Menu links from the CMS use their `href` as-is. Defaults to production when omitted.
|
|
5923
5923
|
*/
|
|
5924
5924
|
accountBaseUrl;
|
|
5925
5925
|
/**
|
|
@@ -6017,13 +6017,43 @@ class WegHeader {
|
|
|
6017
6017
|
getMenuToggleButton() {
|
|
6018
6018
|
return this.el.shadowRoot?.querySelector('.menu-toggle');
|
|
6019
6019
|
}
|
|
6020
|
+
static FOCUSABLE_SELECTOR = 'a[href]:not([tabindex="-1"]), button:not([disabled]):not([tabindex="-1"])';
|
|
6020
6021
|
getFirstNavFocusable() {
|
|
6021
6022
|
const panel = this.el.shadowRoot?.querySelector('#weg-header-main-nav');
|
|
6022
6023
|
if (!panel)
|
|
6023
6024
|
return null;
|
|
6024
|
-
const focusables = panel.querySelectorAll(
|
|
6025
|
+
const focusables = panel.querySelectorAll(WegHeader.FOCUSABLE_SELECTOR);
|
|
6025
6026
|
return focusables[0] ?? null;
|
|
6026
6027
|
}
|
|
6028
|
+
/** Focusable controls inside the open mobile menu chrome (toggle → logo → auth → nav). */
|
|
6029
|
+
getMobileMenuFocusables() {
|
|
6030
|
+
const header = this.el.shadowRoot?.querySelector('.header');
|
|
6031
|
+
if (!header)
|
|
6032
|
+
return [];
|
|
6033
|
+
return Array.from(header.querySelectorAll(WegHeader.FOCUSABLE_SELECTOR));
|
|
6034
|
+
}
|
|
6035
|
+
trapMobileMenuFocus(event) {
|
|
6036
|
+
if (!this.isMobileMenuActive() || event.key !== 'Tab')
|
|
6037
|
+
return;
|
|
6038
|
+
const focusables = this.getMobileMenuFocusables();
|
|
6039
|
+
if (focusables.length === 0)
|
|
6040
|
+
return;
|
|
6041
|
+
const first = focusables[0];
|
|
6042
|
+
const last = focusables[focusables.length - 1];
|
|
6043
|
+
const active = this.el.shadowRoot?.activeElement;
|
|
6044
|
+
const activeInTrap = active != null && focusables.includes(active);
|
|
6045
|
+
if (event.shiftKey) {
|
|
6046
|
+
if (!activeInTrap || active === first) {
|
|
6047
|
+
event.preventDefault();
|
|
6048
|
+
last.focus();
|
|
6049
|
+
}
|
|
6050
|
+
return;
|
|
6051
|
+
}
|
|
6052
|
+
if (!activeInTrap || active === last) {
|
|
6053
|
+
event.preventDefault();
|
|
6054
|
+
first.focus();
|
|
6055
|
+
}
|
|
6056
|
+
}
|
|
6027
6057
|
focusFirstNavItem() {
|
|
6028
6058
|
setTimeout(() => {
|
|
6029
6059
|
const attemptFocus = (retries = 8) => {
|
|
@@ -6135,6 +6165,10 @@ class WegHeader {
|
|
|
6135
6165
|
this.resolve();
|
|
6136
6166
|
}
|
|
6137
6167
|
handleKeyDown(event) {
|
|
6168
|
+
if (event.key === 'Tab') {
|
|
6169
|
+
this.trapMobileMenuFocus(event);
|
|
6170
|
+
return;
|
|
6171
|
+
}
|
|
6138
6172
|
if (event.key !== 'Escape')
|
|
6139
6173
|
return;
|
|
6140
6174
|
if (this.openDropdown) {
|
|
@@ -6394,6 +6428,20 @@ class WegHeader {
|
|
|
6394
6428
|
const name = this.userName?.trim();
|
|
6395
6429
|
return name || 'Manage Account';
|
|
6396
6430
|
}
|
|
6431
|
+
resolveManageAccountHref(link) {
|
|
6432
|
+
const fromMenu = link?.href?.trim();
|
|
6433
|
+
if (fromMenu)
|
|
6434
|
+
return fromMenu;
|
|
6435
|
+
return accountPath(this.getAccountHome(), '/account/manage');
|
|
6436
|
+
}
|
|
6437
|
+
findManageAccountLink(menu) {
|
|
6438
|
+
const accountHome = this.getAccountHome();
|
|
6439
|
+
for (const item of menu) {
|
|
6440
|
+
if (!isMenuGroup(item) && isManageAccountLink(item, accountHome))
|
|
6441
|
+
return item;
|
|
6442
|
+
}
|
|
6443
|
+
return null;
|
|
6444
|
+
}
|
|
6397
6445
|
shouldExcludeFromNavActive(item, accountHome) {
|
|
6398
6446
|
return isSignOutLink(item) || isSignInLink(item, accountHome);
|
|
6399
6447
|
}
|
|
@@ -6473,9 +6521,9 @@ class WegHeader {
|
|
|
6473
6521
|
[options.className || '']: !!options.className,
|
|
6474
6522
|
}, href: link.href, "aria-label": link.label, onClick: (event) => this.handleAuthClick(event, action, link.href, onNavigate) }, hAsync(Icon, null), !iconOnly ? link.label : null, iconOnly ? hAsync("span", { class: "sr-only" }, link.label) : null));
|
|
6475
6523
|
}
|
|
6476
|
-
renderManageAccountAnchor(onNavigate, iconOnly = false, ariaCurrentHref = null) {
|
|
6524
|
+
renderManageAccountAnchor(onNavigate, iconOnly = false, ariaCurrentHref = null, link) {
|
|
6477
6525
|
const label = this.getManageAccountLabel();
|
|
6478
|
-
const href =
|
|
6526
|
+
const href = this.resolveManageAccountHref(link);
|
|
6479
6527
|
const active = this.isNavItemActive(href, 'prefix');
|
|
6480
6528
|
const ariaCurrent = this.isAriaCurrentHref(href, ariaCurrentHref);
|
|
6481
6529
|
return (hAsync("a", { class: {
|
|
@@ -6497,7 +6545,7 @@ class WegHeader {
|
|
|
6497
6545
|
return (hAsync("li", { class: "main-nav__item main-nav__item--auth", key: "sign-in" }, this.renderAuthControl({ link: item, action: 'sign-in', onNavigate })));
|
|
6498
6546
|
}
|
|
6499
6547
|
if (isManageAccountLink(item, accountHome)) {
|
|
6500
|
-
return (hAsync("li", { class: "main-nav__item main-nav__item--auth", key: "manage-account" }, this.renderManageAccountAnchor(onNavigate, false, ariaCurrentHref)));
|
|
6548
|
+
return (hAsync("li", { class: "main-nav__item main-nav__item--auth", key: "manage-account" }, this.renderManageAccountAnchor(onNavigate, false, ariaCurrentHref, item)));
|
|
6501
6549
|
}
|
|
6502
6550
|
if (this.signedIn && isFindJobLink(item)) {
|
|
6503
6551
|
return this.renderIconNavLink(item, FindJobIcon, 'find-a-job-link', onNavigate, ariaCurrentHref);
|
|
@@ -6513,7 +6561,8 @@ class WegHeader {
|
|
|
6513
6561
|
}
|
|
6514
6562
|
renderCompactAuth(onNavigate) {
|
|
6515
6563
|
if (this.signedIn) {
|
|
6516
|
-
|
|
6564
|
+
const manageLink = this.findManageAccountLink(this.getActiveMenu());
|
|
6565
|
+
return this.renderManageAccountAnchor(onNavigate, true, null, manageLink ?? undefined);
|
|
6517
6566
|
}
|
|
6518
6567
|
const signIn = this.findSignInLink(this.resolved.menu);
|
|
6519
6568
|
if (!signIn?.href)
|
|
@@ -6542,12 +6591,12 @@ class WegHeader {
|
|
|
6542
6591
|
const logoSrc = this.getLogoSrc();
|
|
6543
6592
|
const closeMenu = () => this.closeMenu();
|
|
6544
6593
|
const mobileMenuActive = this.isMobileMenuActive();
|
|
6545
|
-
return (hAsync(Host, { key: '
|
|
6594
|
+
return (hAsync(Host, { key: '916472d15df8abf5270650126356ef2d7a475765', class: { 'weg-header--scroll-mode': this.headerScrollMode } }, hAsync("div", { key: '8c8d63f67cf7b27364cc828aa42c5b011cbb971e', class: "weg-header-shell" }, hAsync("a", { key: '96f6141f0bff7a51bbc8f46ea9b92d0c33142e1a', class: "skip-to-content", href: "#", title: SKIP_TO_CONTENT_LABEL, onClick: (event) => this.handleSkipToContent(event) }, SKIP_TO_CONTENT_LABEL), hAsync("header", { key: '4a727a72e031159ca991505a1a8a59a32271aedf', class: {
|
|
6546
6595
|
header: true,
|
|
6547
6596
|
'header--menu-open': mobileMenuActive,
|
|
6548
6597
|
'header--scroll-mode': this.headerScrollMode,
|
|
6549
6598
|
'header--scroll-hidden': this.headerScrollHidden && this.headerScrollMode,
|
|
6550
|
-
} }, hAsync("div", { key: '
|
|
6599
|
+
} }, hAsync("div", { key: '5ef2a85bf2087206a17e3cbf163dde4d549dcd0b', class: "header-inner" }, hAsync("div", { key: '3af6990b5c6d93b4332a0d53300daddb11e2ad2b', class: "sr-only", "aria-live": "polite", "aria-atomic": "true", "data-weg-sr-live": true }), hAsync("button", { key: '023322f1e49a676a5b20208b956624ce820e9b57', type: "button", class: "menu-toggle icon-button", "aria-label": mobileMenuActive ? CLOSE_MAIN_NAV_LABEL : OPEN_MAIN_NAV_LABEL, "aria-expanded": mobileMenuActive ? 'true' : 'false', "aria-controls": "weg-header-main-nav", onClick: () => this.toggleMenu() }, mobileMenuActive ? hAsync(CloseIcon, null) : hAsync(HamburgerIcon, null)), hAsync(Logo, { key: 'efa51b76936456c49059c67e9f2c9c2365b97d25', href: logoHref, src: logoSrc }), hAsync("div", { key: 'ee0eff5ae3a63cf9550261a4d1727c9a88ab3e4d', class: "header-actions" }, this.renderCompactAuth(mobileMenuActive ? closeMenu : undefined)), hAsync("div", { key: '7552a6a661e644dcb3d555ab5572f680cd0295de', class: "main-nav-panel", id: "weg-header-main-nav", role: mobileMenuActive ? 'dialog' : undefined, "aria-modal": mobileMenuActive ? 'true' : undefined, "aria-label": mobileMenuActive ? MAIN_NAV_LABEL : undefined }, this.renderNav(mobileMenuActive ? closeMenu : undefined)))))));
|
|
6551
6600
|
}
|
|
6552
6601
|
static get watchers() { return {
|
|
6553
6602
|
"layout": [{
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -1,25 +1,34 @@
|
|
|
1
1
|
# weg-shared-layout
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Shared layout Web Components built with Stencil.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
npm i weg-shared-layout
|
|
9
|
+
# or
|
|
10
|
+
pnpm add weg-shared-layout
|
|
9
11
|
```
|
|
10
12
|
|
|
11
|
-
##
|
|
13
|
+
## What this package contains
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
`weg-shared-layout` publishes two presentational Web Components:
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
- `<weg-header>`
|
|
18
|
+
- `<weg-footer>`
|
|
19
|
+
|
|
20
|
+
They do not fetch their own content. Your app loads layout data, maps it into the expected shape, and passes the same `layout` object to both components.
|
|
21
|
+
|
|
22
|
+
## WEG21 API flow
|
|
23
|
+
|
|
24
|
+
The package includes helpers for the WEG CMS WEG21 API:
|
|
16
25
|
|
|
17
26
|
| Endpoint | Purpose |
|
|
18
27
|
| --- | --- |
|
|
19
|
-
| `GET https://warwickemploymentgroup.com/api/v1/weg21` |
|
|
20
|
-
| `GET
|
|
28
|
+
| `GET https://warwickemploymentgroup.com/api/v1/weg21` | Footer copy, social links, and additional bootstrap metadata such as logo/favicon URLs |
|
|
29
|
+
| `GET https://warwickemploymentgroup.com/api/v1/weg21/menus` | Header nav (`ext` / `int`) and footer link columns |
|
|
21
30
|
|
|
22
|
-
|
|
31
|
+
Use `fetchWeg21Layout()` to fetch and map both endpoints:
|
|
23
32
|
|
|
24
33
|
```ts
|
|
25
34
|
import { fetchWeg21Layout } from 'weg-shared-layout/menus';
|
|
@@ -27,12 +36,16 @@ import { fetchWeg21Layout } from 'weg-shared-layout/menus';
|
|
|
27
36
|
const layout = await fetchWeg21Layout({
|
|
28
37
|
apiKey: process.env.WCMS_API_KEY,
|
|
29
38
|
});
|
|
30
|
-
// layout.header.menu, layout.footer.menu, layout.footer.social, …
|
|
31
39
|
```
|
|
32
40
|
|
|
33
|
-
|
|
41
|
+
You can also work offline with the bundled schema fixtures:
|
|
42
|
+
|
|
43
|
+
- `weg-shared-layout/weg21-bootstrap.json`
|
|
44
|
+
- `weg-shared-layout/weg21-menus.json`
|
|
45
|
+
|
|
46
|
+
Map those with `dummyWeg21LayoutData()` or `menusToLayoutData()`.
|
|
34
47
|
|
|
35
|
-
|
|
48
|
+
## Layout shape
|
|
36
49
|
|
|
37
50
|
```json
|
|
38
51
|
{
|
|
@@ -51,33 +64,29 @@ const layout = await fetchWeg21Layout({
|
|
|
51
64
|
}
|
|
52
65
|
```
|
|
53
66
|
|
|
54
|
-
| Field |
|
|
55
|
-
| --- | --- |
|
|
56
|
-
| `header.logoSrc` |
|
|
57
|
-
| `header.logoHref` |
|
|
58
|
-
| `header.menu` |
|
|
59
|
-
| `footer.menu` |
|
|
60
|
-
| `footer.social` |
|
|
61
|
-
| `footer.credits` / `footer.copyright` |
|
|
62
|
-
|
|
63
|
-
### Auth (Sign in / Sign out)
|
|
67
|
+
| Field | Notes |
|
|
68
|
+
| --- | --- |
|
|
69
|
+
| `header.logoSrc` | Optional logo image URL; bundled logo is used when omitted |
|
|
70
|
+
| `header.logoHref` | Optional logo link URL; defaults to the WEG homepage |
|
|
71
|
+
| `header.menu` | Mixed array of groups (`items`) and flat links (`href`) |
|
|
72
|
+
| `footer.menu` | `LayoutLink[][]` where each inner array is a footer column |
|
|
73
|
+
| `footer.social` | LinkedIn, Instagram, TikTok, or YouTube links |
|
|
74
|
+
| `footer.credits` / `footer.copyright` | Footer legal copy |
|
|
64
75
|
|
|
65
|
-
|
|
76
|
+
## Header auth behavior
|
|
66
77
|
|
|
67
|
-
|
|
78
|
+
When `signed-in` is `false`, the header renders `layout.header.menu` directly. Sign-in should be represented as a normal CMS link such as `{ "label": "Sign in", "href": "…" }`.
|
|
68
79
|
|
|
69
|
-
|
|
80
|
+
When `signed-in` is `true`, the component still uses `layout.header.menu` if you provide one. If `layout.header.menu` is empty, it falls back to the built-in signed-in menu: Find a job, Dashboard, Manage Account, and Sign out.
|
|
70
81
|
|
|
71
|
-
|
|
82
|
+
Pass:
|
|
72
83
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
| Dropdown parent | Active background when any child matches |
|
|
78
|
-
| Sign in / Sign out | Never highlighted |
|
|
84
|
+
- `signed-in` from your app session state
|
|
85
|
+
- `user-name` for the signed-in Manage Account label
|
|
86
|
+
- `account-base-url` only when using the built-in signed-in fallback menu against a non-production account portal
|
|
87
|
+
- `current-path` to highlight the active nav item
|
|
79
88
|
|
|
80
|
-
|
|
89
|
+
Sign-out emits `wegAuthClick`. The host app should call `event.preventDefault()` and run logout + redirect itself.
|
|
81
90
|
|
|
82
91
|
```js
|
|
83
92
|
header.addEventListener('wegAuthClick', async (event) => {
|
|
@@ -88,34 +97,47 @@ header.addEventListener('wegAuthClick', async (event) => {
|
|
|
88
97
|
});
|
|
89
98
|
```
|
|
90
99
|
|
|
91
|
-
|
|
100
|
+
## Nav active state
|
|
101
|
+
|
|
102
|
+
| Link type | Match rule |
|
|
103
|
+
| --- | --- |
|
|
104
|
+
| Flat nav link | Prefix match, so `/career-advice` matches `/career-advice/foo` |
|
|
105
|
+
| Dropdown child | Exact pathname, plus query string when the href includes `?` |
|
|
106
|
+
| Dropdown parent | Active background when any child matches |
|
|
107
|
+
| Sign in / Sign out | Never highlighted |
|
|
108
|
+
|
|
109
|
+
Active links use `weg-purple-200` (`#CDCFF8`). Omit `current-path` to disable active styling.
|
|
110
|
+
|
|
111
|
+
## Package entry points
|
|
92
112
|
|
|
93
|
-
|
|
|
113
|
+
| Import | Purpose |
|
|
94
114
|
| --- | --- |
|
|
95
|
-
| `weg-shared-layout/loader` | `defineCustomElements()`
|
|
96
|
-
| `weg-shared-layout/menus` | `fetchWeg21Layout`, `menusToLayoutData` |
|
|
97
|
-
| `weg-shared-layout/menus-data` | WEG21 API types
|
|
98
|
-
| `weg-shared-layout/layout-data` |
|
|
99
|
-
| `weg-shared-layout/weg21-bootstrap.json` | `GET /api/v1/weg21`
|
|
100
|
-
| `weg-shared-layout/weg21-menus.json` | `GET /api/v1/weg21/menus`
|
|
101
|
-
| `weg-shared-layout/weg-header` / `weg-footer` | Side-effect
|
|
102
|
-
| `weg-shared-layout/
|
|
115
|
+
| `weg-shared-layout/loader` | `defineCustomElements()` for registering all tags |
|
|
116
|
+
| `weg-shared-layout/menus` | `fetchWeg21Layout`, `menusToLayoutData`, `dummyWeg21LayoutData` |
|
|
117
|
+
| `weg-shared-layout/menus-data` | WEG21 API response types |
|
|
118
|
+
| `weg-shared-layout/layout-data` | `layout` prop types |
|
|
119
|
+
| `weg-shared-layout/weg21-bootstrap.json` | Sample `GET /api/v1/weg21` payload |
|
|
120
|
+
| `weg-shared-layout/weg21-menus.json` | Sample `GET /api/v1/weg21/menus` payload |
|
|
121
|
+
| `weg-shared-layout/weg-header` / `weg-footer` | Side-effect imports for individual tags |
|
|
122
|
+
| `weg-shared-layout/hydrate` | Stencil hydrate build for server-side rendering workflows |
|
|
123
|
+
| `weg-shared-layout/styles/container.css` | Shared `.container` layout utility |
|
|
124
|
+
| `weg-shared-layout` | Root package export with generated JSX/component typings plus `format()` |
|
|
103
125
|
|
|
104
126
|
## Documentation
|
|
105
127
|
|
|
106
|
-
|
|
128
|
+
See **[docs/README.md](./docs/README.md)** for the full index.
|
|
107
129
|
|
|
108
130
|
| Guide | Doc |
|
|
109
131
|
| --- | --- |
|
|
110
132
|
| Angular 16 | [docs/angular.md](./docs/angular.md) |
|
|
111
|
-
| Angular 16 demo
|
|
112
|
-
| React
|
|
113
|
-
| Next.js
|
|
133
|
+
| Angular 16 demo | [demo/angular16/README.md](./demo/angular16/README.md) |
|
|
134
|
+
| React SPA | [docs/react.md](./docs/react.md) |
|
|
135
|
+
| Next.js App Router | [docs/nextjs.md](./docs/nextjs.md) |
|
|
114
136
|
| Plain HTML / vanilla JS | [docs/vanilla.md](./docs/vanilla.md) |
|
|
115
|
-
| Publishing
|
|
137
|
+
| Publishing | [docs/publishing.md](./docs/publishing.md) |
|
|
116
138
|
|
|
117
|
-
## Publishing
|
|
139
|
+
## Publishing
|
|
118
140
|
|
|
119
|
-
|
|
141
|
+
`npm publish` does not run a build on its own; it publishes whatever is already on disk. This repo relies on `prepack` so a Stencil build runs before packing or publishing.
|
|
120
142
|
|
|
121
|
-
|
|
143
|
+
For the full release checklist, package contents, and CI notes, see **[docs/publishing.md](./docs/publishing.md)**.
|