@ptcwebops/ptcw-design 6.4.11-beta → 6.4.12-beta

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.
@@ -0,0 +1,487 @@
1
+ import { Host, h } from "@stencil/core";
2
+ export class PtcSubnavV2 {
3
+ constructor() {
4
+ this.handleClickOutside = (event) => {
5
+ const target = event.target;
6
+ if (!this.el.contains(target)) {
7
+ this.showOverflowMenu = false;
8
+ }
9
+ };
10
+ this.handleKeyDown = (event) => {
11
+ // Handle Escape key to close dropdowns
12
+ if (event.key === "Escape" && this.showOverflowMenu) {
13
+ this.showOverflowMenu = false;
14
+ // Return focus to the button that opened the menu
15
+ if (this.overflowButton) {
16
+ this.overflowButton.focus();
17
+ }
18
+ }
19
+ };
20
+ this.handleScroll = () => {
21
+ // Throttle scroll events for better performance
22
+ if (this.scrollThrottleTimeout) {
23
+ return;
24
+ }
25
+ this.scrollThrottleTimeout = window.setTimeout(() => {
26
+ // Don't update active state if user just manually navigated
27
+ if (this.isManualNavigation) {
28
+ this.scrollThrottleTimeout = null;
29
+ return;
30
+ }
31
+ // Fallback scroll handler to ensure active state is updated
32
+ const navItems = this.getNavItems();
33
+ const subnavHeight = this.el.offsetHeight;
34
+ const scrollPosition = window.scrollY + subnavHeight + 100; // Offset for better detection
35
+ // Find the section that should be active based on scroll position
36
+ let activeSection = navItems[0].id; // Default to first section
37
+ for (let i = navItems.length - 1; i >= 0; i--) {
38
+ const section = document.getElementById(navItems[i].id);
39
+ if (section && section.offsetTop <= scrollPosition) {
40
+ activeSection = navItems[i].id;
41
+ break;
42
+ }
43
+ }
44
+ // Only update if different from current active
45
+ if (activeSection !== this.currentActive) {
46
+ this.currentActive = activeSection;
47
+ // Update mobile selected item if in mobile mode
48
+ if (this.isMobile) {
49
+ const activeItem = navItems.find((item) => item.id === activeSection);
50
+ if (activeItem) {
51
+ this.selectedMobileItem = activeItem;
52
+ }
53
+ }
54
+ }
55
+ this.scrollThrottleTimeout = null;
56
+ }, 16); // ~60fps throttling
57
+ };
58
+ this.handleResize = () => {
59
+ if (this.navContainer) {
60
+ // Add a small delay to ensure DOM has been updated
61
+ setTimeout(() => {
62
+ this.calculateVisibleItems();
63
+ }, 10);
64
+ }
65
+ };
66
+ this.setupIntersectionObserver = () => {
67
+ // Get all sections that correspond to our navigation items
68
+ const navItems = this.getNavItems();
69
+ // Create intersection observer
70
+ this.intersectionObserver = new IntersectionObserver((entries) => {
71
+ // Clear any existing timeout
72
+ if (this.scrollTimeout) {
73
+ clearTimeout(this.scrollTimeout);
74
+ }
75
+ // Debounce the intersection changes
76
+ this.scrollTimeout = window.setTimeout(() => {
77
+ // Don't update active state if user just manually navigated
78
+ if (this.isManualNavigation) {
79
+ return;
80
+ }
81
+ // Find all intersecting sections
82
+ const intersectingSections = entries.filter((entry) => entry.isIntersecting);
83
+ if (intersectingSections.length > 0) {
84
+ // Sort by intersection ratio (how much of the section is visible)
85
+ // and then by position (top to bottom)
86
+ intersectingSections.sort((a, b) => {
87
+ // First sort by intersection ratio (higher = more visible)
88
+ if (b.intersectionRatio !== a.intersectionRatio) {
89
+ return (b.intersectionRatio - a.intersectionRatio);
90
+ }
91
+ // If intersection ratios are equal, sort by position
92
+ return (a.boundingClientRect.top -
93
+ b.boundingClientRect.top);
94
+ });
95
+ // Get the most visible section
96
+ const mostVisibleSection = intersectingSections[0];
97
+ const sectionId = mostVisibleSection.target.id;
98
+ if (sectionId && sectionId !== this.currentActive) {
99
+ this.currentActive = sectionId;
100
+ // Update mobile selected item if in mobile mode
101
+ if (this.isMobile) {
102
+ const activeItem = navItems.find((item) => item.id === sectionId);
103
+ if (activeItem) {
104
+ this.selectedMobileItem = activeItem;
105
+ }
106
+ }
107
+ }
108
+ }
109
+ }, 100); // Increased debounce time for better performance
110
+ }, {
111
+ root: null,
112
+ rootMargin: "-20% 0px -60% 0px",
113
+ threshold: [0, 0.1, 0.25, 0.5, 0.75, 1.0], // Multiple thresholds for better detection
114
+ });
115
+ // Observe all sections
116
+ navItems.forEach((item) => {
117
+ const section = document.getElementById(item.id);
118
+ if (section) {
119
+ this.intersectionObserver.observe(section);
120
+ }
121
+ });
122
+ };
123
+ this.extractNavItemsFromSlots = () => {
124
+ // Look for slot content with nav-items in the light DOM
125
+ const slotElement = this.el.querySelector('[slot="nav-items"]');
126
+ if (slotElement) {
127
+ const links = slotElement.querySelectorAll("a");
128
+ const items = Array.from(links)
129
+ .map((link) => {
130
+ var _a, _b;
131
+ return ({
132
+ id: ((_a = link.getAttribute("href")) === null || _a === void 0 ? void 0 : _a.substring(1)) || "",
133
+ label: ((_b = link.textContent) === null || _b === void 0 ? void 0 : _b.trim()) || "",
134
+ title: link.getAttribute("title") || "",
135
+ trackerId: link.getAttribute("tracker-id") || "",
136
+ });
137
+ })
138
+ .filter((item) => item.id && item.label);
139
+ if (items.length > 0) {
140
+ return items;
141
+ }
142
+ }
143
+ return null;
144
+ };
145
+ this.getNavItems = () => {
146
+ // Extract navigation items from slots (SEO-friendly approach)
147
+ const slotItems = this.extractNavItemsFromSlots();
148
+ if (slotItems) {
149
+ return slotItems;
150
+ }
151
+ // If navItems prop is provided, parse and use it (fallback)
152
+ if (this.navItems && this.navItems.trim()) {
153
+ try {
154
+ const parsedItems = JSON.parse(this.navItems);
155
+ // Validate that it's an array with proper structure
156
+ if (Array.isArray(parsedItems) && parsedItems.length > 0) {
157
+ // Ensure each item has required properties
158
+ const validItems = parsedItems.filter((item) => item &&
159
+ typeof item.id === "string" &&
160
+ typeof item.label === "string");
161
+ if (validItems.length > 0) {
162
+ return validItems;
163
+ }
164
+ }
165
+ }
166
+ catch (error) {
167
+ console.warn("Invalid navItems JSON provided to ptc-subnav-v2:", error);
168
+ }
169
+ }
170
+ // Return empty array if no navigation items found
171
+ console.warn("No navigation items found in slots or props for ptc-subnav-v2");
172
+ return [];
173
+ };
174
+ this.calculateMobileDropdownItems = () => {
175
+ const navItems = this.getNavItems();
176
+ // Handle case where no navigation items are found
177
+ if (navItems.length === 0) {
178
+ this.mobileDropdownItems = [];
179
+ return;
180
+ }
181
+ const currentItem = this.selectedMobileItem || {
182
+ id: this.currentActive,
183
+ };
184
+ // Filter out the currently selected item from all nav items
185
+ this.mobileDropdownItems = navItems.filter((item) => item.id !== currentItem.id);
186
+ };
187
+ this.hasActiveOverflowItem = () => {
188
+ return this.overflowItems.some((item) => item.id === this.currentActive);
189
+ };
190
+ this.calculateVisibleItems = () => {
191
+ const navItems = this.getNavItems();
192
+ // Handle case where no navigation items are found
193
+ if (navItems.length === 0) {
194
+ this.visibleItems = [];
195
+ this.overflowItems = [];
196
+ this.selectedMobileItem = null;
197
+ return;
198
+ }
199
+ // Check if we're in mobile mode (below 767px)
200
+ const wasMobile = this.isMobile;
201
+ this.isMobile = window.innerWidth <= 767;
202
+ // Force re-render if mobile state changed
203
+ if (wasMobile !== this.isMobile) {
204
+ this.showOverflowMenu = false; // Close any open menus when switching modes
205
+ }
206
+ if (this.isMobile) {
207
+ // On mobile, show only the current active item
208
+ const activeItem = navItems.find((item) => item.id === this.currentActive) ||
209
+ navItems[0];
210
+ this.selectedMobileItem = activeItem;
211
+ this.visibleItems = [activeItem];
212
+ this.overflowItems = navItems.filter((item) => item.id !== activeItem.id);
213
+ }
214
+ else {
215
+ // Desktop behavior - calculate based on available width
216
+ const containerWidth = this.navContainer
217
+ ? this.navContainer.offsetWidth
218
+ : 0;
219
+ const buttonWidth = this.overflowButton
220
+ ? this.overflowButton.offsetWidth + 32
221
+ : 0; // 32px for gap
222
+ const availableWidth = containerWidth - buttonWidth - 48; // 48px for right margin
223
+ // Fallback if container width is not available yet
224
+ if (containerWidth === 0) {
225
+ this.visibleItems = navItems;
226
+ this.overflowItems = [];
227
+ return;
228
+ }
229
+ let visibleCount = 0;
230
+ let totalWidth = 0;
231
+ // Create temporary elements to measure width
232
+ const tempContainer = document.createElement("div");
233
+ tempContainer.style.position = "absolute";
234
+ tempContainer.style.visibility = "hidden";
235
+ tempContainer.style.whiteSpace = "nowrap";
236
+ tempContainer.style.fontSize = "14px";
237
+ tempContainer.style.fontWeight = "700";
238
+ tempContainer.style.padding = "20px 0 17px 0";
239
+ document.body.appendChild(tempContainer);
240
+ for (let i = 0; i < navItems.length; i++) {
241
+ const item = navItems[i];
242
+ tempContainer.textContent = item.label;
243
+ const itemWidth = tempContainer.offsetWidth + 32; // 32px for gap
244
+ if (totalWidth + itemWidth <= availableWidth) {
245
+ totalWidth += itemWidth;
246
+ visibleCount++;
247
+ }
248
+ else {
249
+ break;
250
+ }
251
+ }
252
+ document.body.removeChild(tempContainer);
253
+ this.visibleItems = navItems.slice(0, visibleCount);
254
+ this.overflowItems = navItems.slice(visibleCount);
255
+ }
256
+ // Don't automatically show overflow menu - it should be closed by default
257
+ // this.showOverflowMenu = this.overflowItems.length > 0;
258
+ };
259
+ this.toggleOverflowMenu = () => {
260
+ this.showOverflowMenu = !this.showOverflowMenu;
261
+ };
262
+ this.handleNavClick = (event, section) => {
263
+ event.preventDefault();
264
+ this.currentActive = section;
265
+ // Update mobile selected item if in mobile mode
266
+ if (this.isMobile) {
267
+ const navItems = this.getNavItems();
268
+ const activeItem = navItems.find((item) => item.id === section);
269
+ if (activeItem) {
270
+ this.selectedMobileItem = activeItem;
271
+ this.calculateMobileDropdownItems();
272
+ }
273
+ }
274
+ // Set manual navigation flag to prevent intersection observer from overriding
275
+ this.isManualNavigation = true;
276
+ // Clear any existing manual navigation timeout
277
+ if (this.manualNavigationTimeout) {
278
+ clearTimeout(this.manualNavigationTimeout);
279
+ }
280
+ // Reset manual navigation flag after scroll animation completes
281
+ this.manualNavigationTimeout = window.setTimeout(() => {
282
+ this.isManualNavigation = false;
283
+ }, 1500); // Give enough time for smooth scroll to complete
284
+ // Emit custom event for parent components
285
+ const customEvent = new CustomEvent("sectionChange", {
286
+ detail: { section },
287
+ bubbles: true,
288
+ composed: true,
289
+ });
290
+ this.el.dispatchEvent(customEvent);
291
+ // Smooth scroll to section if it exists
292
+ const targetElement = document.querySelector(`#${section}`);
293
+ if (targetElement) {
294
+ targetElement.scrollIntoView({
295
+ behavior: "smooth",
296
+ block: "start",
297
+ });
298
+ }
299
+ };
300
+ this.activeSection = "overview";
301
+ this.navItems = "";
302
+ this.currentActive = "overview";
303
+ this.visibleItems = [];
304
+ this.overflowItems = [];
305
+ this.showOverflowMenu = false;
306
+ this.containerWidth = 0;
307
+ this.isMobile = false;
308
+ this.selectedMobileItem = null;
309
+ this.isManualNavigation = false;
310
+ this.mobileDropdownItems = [];
311
+ }
312
+ componentWillLoad() {
313
+ this.currentActive = this.activeSection;
314
+ // Initialize selected mobile item with the current active section
315
+ const navItems = this.getNavItems();
316
+ if (navItems.length > 0) {
317
+ this.selectedMobileItem =
318
+ navItems.find((item) => item.id === this.activeSection) ||
319
+ navItems[0];
320
+ }
321
+ else {
322
+ this.selectedMobileItem = null;
323
+ }
324
+ this.calculateMobileDropdownItems();
325
+ }
326
+ componentDidLoad() {
327
+ // Bind the functions once
328
+ this.boundHandleResize = this.handleResize.bind(this);
329
+ this.boundHandleClickOutside = this.handleClickOutside.bind(this);
330
+ this.boundHandleScroll = this.handleScroll.bind(this);
331
+ this.boundHandleKeyDown = this.handleKeyDown.bind(this);
332
+ // Recalculate navigation items in case slots are used
333
+ this.calculateMobileDropdownItems();
334
+ this.handleResize();
335
+ window.addEventListener("resize", this.boundHandleResize);
336
+ document.addEventListener("click", this.boundHandleClickOutside);
337
+ document.addEventListener("keydown", this.boundHandleKeyDown);
338
+ // Setup intersection observer for scroll-based active state
339
+ this.setupIntersectionObserver();
340
+ // Add scroll listener as fallback
341
+ window.addEventListener("scroll", this.boundHandleScroll, {
342
+ passive: true,
343
+ });
344
+ }
345
+ disconnectedCallback() {
346
+ window.removeEventListener("resize", this.boundHandleResize);
347
+ document.removeEventListener("click", this.boundHandleClickOutside);
348
+ document.removeEventListener("keydown", this.boundHandleKeyDown);
349
+ window.removeEventListener("scroll", this.boundHandleScroll);
350
+ // Clean up intersection observer
351
+ if (this.intersectionObserver) {
352
+ this.intersectionObserver.disconnect();
353
+ }
354
+ // Clean up scroll timeout
355
+ if (this.scrollTimeout) {
356
+ clearTimeout(this.scrollTimeout);
357
+ }
358
+ // Clean up scroll throttle timeout
359
+ if (this.scrollThrottleTimeout) {
360
+ clearTimeout(this.scrollThrottleTimeout);
361
+ }
362
+ // Clean up manual navigation timeout
363
+ if (this.manualNavigationTimeout) {
364
+ clearTimeout(this.manualNavigationTimeout);
365
+ }
366
+ }
367
+ render() {
368
+ var _a, _b;
369
+ const navItems = this.getNavItems();
370
+ // If no navigation items, don't render the navigation
371
+ if (navItems.length === 0) {
372
+ return (h(Host, null, h("div", { class: "subnav-container" }, h("div", { class: "subnav-menu-right" }, h("slot", { name: "subnav-menu-right" })))));
373
+ }
374
+ return (h(Host, null, h("div", { class: "subnav-container" }, h("nav", { class: "subnav-menu-left", role: "navigation", "aria-label": "Sub navigation", ref: (el) => (this.navContainer = el) }, this.isMobile ? (
375
+ // Mobile layout - dropdown with arrows
376
+ h("div", { class: "mobile-dropdown-container" }, h("button", { class: "mobile-dropdown-button", onClick: this.toggleOverflowMenu, onKeyDown: (e) => {
377
+ if (e.key === "Enter" ||
378
+ e.key === " ") {
379
+ e.preventDefault();
380
+ this.toggleOverflowMenu();
381
+ }
382
+ }, "aria-expanded": this.showOverflowMenu, "aria-haspopup": "true", "aria-label": `${((_a = this.selectedMobileItem) === null || _a === void 0 ? void 0 : _a.label) || "Select Section"} - Choose navigation section`, "tracker-id": "mobile-dropdown-button", ref: (el) => (this.overflowButton = el) }, h("span", { class: "mobile-selected-item" }, ((_b = this.selectedMobileItem) === null || _b === void 0 ? void 0 : _b.label) ||
383
+ "Select Section"), h("span", { class: `mobile-arrow ${this.showOverflowMenu ? "rotated" : ""}`, "aria-hidden": "true" }, h("svg", { xmlns: "http://www.w3.org/2000/svg", width: "12", height: "8", viewBox: "0 0 12 8", fill: "none", "aria-hidden": "true" }, h("path", { d: "M1 1.5L5.94975 6.44975L10.8995 1.5", stroke: "#00890B", "stroke-width": "2", "stroke-linecap": "round" })))), this.showOverflowMenu && (h("ul", { class: "mobile-dropdown", role: "menu", "aria-label": "Navigation sections" }, this.mobileDropdownItems.map((item) => (h("li", { key: item.id }, h("a", { href: `#${item.id}`, class: `mobile-dropdown-item mf-listen ${this
384
+ .currentActive ===
385
+ item.id
386
+ ? "active"
387
+ : ""}`, role: "menuitem", "aria-current": this
388
+ .currentActive ===
389
+ item.id
390
+ ? "page"
391
+ : undefined, "tracker-id": item.trackerId ||
392
+ `mobile-nav-${item.id}`, title: item.title || "", onClick: (e) => {
393
+ this.handleNavClick(e, item.id);
394
+ this.showOverflowMenu = false;
395
+ } }, item.label)))))))) : (
396
+ // Desktop layout - original behavior
397
+ h("ul", { class: "desktop-menu" }, this.visibleItems.map((item) => (h("li", { key: item.id, class: "mf-listen", "tracker-id": item.trackerId || `nav-${item.id}` }, h("a", { href: `#${item.id}`, class: this.currentActive === item.id
398
+ ? "active"
399
+ : "", title: item.title || "", onClick: (e) => this.handleNavClick(e, item.id), "aria-current": this.currentActive === item.id
400
+ ? "page"
401
+ : undefined }, item.label)))), this.overflowItems.length > 0 && (h("li", { class: "overflow-menu-container mf-listen", "tracker-id": "overflow-menu" }, h("button", { class: `overflow-button ${this.hasActiveOverflowItem() ? "selected-active" : ""}`, onClick: this.toggleOverflowMenu, onKeyDown: (e) => {
402
+ if (e.key === "Enter" ||
403
+ e.key === " ") {
404
+ e.preventDefault();
405
+ this.toggleOverflowMenu();
406
+ }
407
+ }, "aria-expanded": this.showOverflowMenu, "aria-haspopup": "true", "aria-label": "Show more navigation items", "tracker-id": "overflow-button", ref: (el) => (this.overflowButton = el) }, h("span", { class: "overflow-dots", "aria-hidden": "true" }, h("svg", { xmlns: "http://www.w3.org/2000/svg", width: "10", height: "10", viewBox: "0 0 10 10", fill: "none", "aria-hidden": "true" }, h("circle", { cx: "1.25", cy: "5", r: "1.25", fill: "#617480" }), h("circle", { cx: "5", cy: "5", r: "1.25", fill: "#617480" }), h("circle", { cx: "8.75", cy: "5", r: "1.25", fill: "#617480" })))), this.showOverflowMenu && (h("div", { class: "overflow-dropdown", role: "menu", "aria-label": "Additional navigation sections" }, this.overflowItems.map((item) => (h("a", { key: item.id, href: `#${item.id}`, class: `mf-listen ${this
408
+ .currentActive ===
409
+ item.id
410
+ ? "active"
411
+ : ""}`, title: item.title || "", "tracker-id": item.trackerId ||
412
+ `overflow-link-${item.id}`, role: "menuitem", onClick: (e) => {
413
+ this.handleNavClick(e, item.id);
414
+ this.showOverflowMenu = false;
415
+ }, "aria-current": this
416
+ .currentActive ===
417
+ item.id
418
+ ? "page"
419
+ : undefined }, item.label)))))))))), h("div", { class: "subnav-menu-right" }, h("slot", { name: "subnav-menu-right" })))));
420
+ }
421
+ static get is() { return "ptc-subnav-v2"; }
422
+ static get encapsulation() { return "shadow"; }
423
+ static get originalStyleUrls() {
424
+ return {
425
+ "$": ["ptc-subnav-v2.scss"]
426
+ };
427
+ }
428
+ static get styleUrls() {
429
+ return {
430
+ "$": ["ptc-subnav-v2.css"]
431
+ };
432
+ }
433
+ static get properties() {
434
+ return {
435
+ "activeSection": {
436
+ "type": "string",
437
+ "mutable": false,
438
+ "complexType": {
439
+ "original": "string",
440
+ "resolved": "string",
441
+ "references": {}
442
+ },
443
+ "required": false,
444
+ "optional": false,
445
+ "docs": {
446
+ "tags": [],
447
+ "text": ""
448
+ },
449
+ "attribute": "active-section",
450
+ "reflect": false,
451
+ "defaultValue": "\"overview\""
452
+ },
453
+ "navItems": {
454
+ "type": "string",
455
+ "mutable": false,
456
+ "complexType": {
457
+ "original": "string",
458
+ "resolved": "string",
459
+ "references": {}
460
+ },
461
+ "required": false,
462
+ "optional": false,
463
+ "docs": {
464
+ "tags": [],
465
+ "text": ""
466
+ },
467
+ "attribute": "nav-items",
468
+ "reflect": false,
469
+ "defaultValue": "\"\""
470
+ }
471
+ };
472
+ }
473
+ static get states() {
474
+ return {
475
+ "currentActive": {},
476
+ "visibleItems": {},
477
+ "overflowItems": {},
478
+ "showOverflowMenu": {},
479
+ "containerWidth": {},
480
+ "isMobile": {},
481
+ "selectedMobileItem": {},
482
+ "isManualNavigation": {},
483
+ "mobileDropdownItems": {}
484
+ };
485
+ }
486
+ static get elementRef() { return "el"; }
487
+ }