@onsvisual/svelte-components 1.1.0 → 1.1.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.
@@ -1,271 +1,245 @@
1
- function getBoolFromString(stringToConvert) {
2
- return stringToConvert === "true";
3
- }
1
+ import { AddTick } from "tick-manager";
2
+ import { getViewportDetails } from "viewport-details";
3
+
4
+ const callbacks = [];
5
+ let running = false;
4
6
 
5
- function toggleSubnav(element) {
6
- const subnav = element;
7
- subnav.classList.toggle("js-expandable-active");
8
- subnav.querySelectorAll(".js-expandable__content").forEach((el) => {
9
- el.classList.toggle("js-nav-hidden");
10
- });
11
-
12
- const elementAria = getBoolFromString(element.querySelector("a:first-child").ariaExpanded);
13
- subnav.querySelector("a:first-child").ariaExpanded = !elementAria;
14
- const subnavAria = getBoolFromString(
15
- element.querySelector(".js-expandable__content").ariaExpanded
16
- );
17
- subnav.querySelector(".js-expandable__content").ariaExpanded = !subnavAria;
7
+ function onViewportChange(callback) {
8
+ callbacks.push(callback);
9
+
10
+ if (!running) {
11
+ running = true;
12
+ AddTick(tick);
13
+ }
18
14
  }
19
15
 
20
- function toggleMenu(toggleElement, menuElement) {
21
- const toggle = toggleElement;
22
- const menu = menuElement;
23
- toggle.classList.toggle("menu-is-expanded");
24
- const toggleAriaState = getBoolFromString(toggle.querySelector("a").ariaExpanded);
25
- toggle.querySelector("a").ariaExpanded = !toggleAriaState;
26
- menu.classList.toggle("nav-main--hidden");
27
- const menuAriaState = getBoolFromString(menuElement.ariaExpanded);
28
- menu.ariaExpanded = !menuAriaState;
16
+ function tick() {
17
+ const viewportDetails = getViewportDetails();
18
+
19
+ if (viewportDetails.resized) {
20
+ callbacks.forEach((callback) => callback(viewportDetails));
21
+ }
29
22
  }
30
23
 
31
- function toggleSearch(toggleElement, searchElement) {
32
- const toggle = toggleElement;
33
- const search = searchElement;
34
- const langAttribute = document.documentElement.lang;
35
- toggle.classList.toggle("search-is-expanded");
36
- const toggleAriaState = getBoolFromString(toggle.querySelector("a").ariaExpanded);
37
- toggle.querySelector("a").ariaExpanded = !toggleAriaState;
38
- let searchStr = "";
39
- if (langAttribute === "en") {
40
- searchStr = "Hide search";
41
- if (toggle.querySelector(".nav--controls__text").textContent.includes("Hide")) {
42
- searchStr = "Search";
24
+ const attrExpanded = "aria-expanded";
25
+ const attrHidden = "aria-hidden";
26
+ const attrDisabled = "aria-disabled";
27
+
28
+ class NavigationToggle {
29
+ constructor(el, toggle, navigation, hideClass) {
30
+ this.toggle = toggle;
31
+ this.navigation = navigation;
32
+ this.hideClass = hideClass;
33
+ this.searchToggleBtn = el.querySelector(".ons-js-toggle-header-search");
34
+ this.menuBtn = el.querySelector(".ons-js-toggle-nav-menu");
35
+ this.menuEl = el.querySelector(".ons-js-nav-menu");
36
+ this.searchBtn = el.querySelector(".ons-btn--search");
37
+ this.searchEl = el.querySelector(".ons-js-header-search");
38
+
39
+ this.toggle.classList.remove("ons-u-d-no");
40
+ this.toggle.classList.remove("disabled");
41
+ this.setAria();
42
+ onViewportChange(this.setAria.bind(this));
43
+ }
44
+
45
+ registerEvents() {
46
+ this.toggle.addEventListener("click", this.toggleNav.bind(this));
47
+ }
48
+
49
+ toggleNav() {
50
+ if (this.navigation.getAttribute(attrHidden) === "false") {
51
+ this.closeNav();
52
+ } else {
53
+ this.openNav();
43
54
  }
44
- } else {
45
- searchStr = "Cuddio";
46
- if (toggle.querySelector(".nav--controls__text").textContent.includes("Cuddio")) {
47
- searchStr = "Chwilio";
55
+ }
56
+
57
+ openNav() {
58
+ const input = [...this.navigation.getElementsByTagName("INPUT")][0];
59
+
60
+ this.toggle.classList.add("active");
61
+ this.navigation.setAttribute(attrHidden, "false");
62
+ this.navigation.classList.remove(this.hideClass);
63
+
64
+ if (input) {
65
+ input.focus();
48
66
  }
67
+
68
+ if (this.toggle === this.searchToggleBtn) {
69
+ this.updateSearchIcon(true, this.toggle);
70
+ }
71
+
72
+ this.toggle.setAttribute(attrExpanded, "true");
73
+
74
+ this.toggleMenuAndSearch();
49
75
  }
50
- toggle.querySelector(".nav--controls__text").textContent = searchStr;
51
- search.classList.toggle("nav-search--hidden");
52
- const searchAriaState = getBoolFromString(search.ariaExpanded);
53
- search.ariaExpanded = !searchAriaState;
54
- }
55
76
 
56
- function cloneSecondaryNav() {
57
- // On mobile move secondary nav items in header to primary nav
58
- const navLink = document.querySelectorAll(".js-nav-clone__link");
59
- const navList = document.querySelector(".js-nav-clone__list");
60
-
61
- if (
62
- document.body.classList.contains("viewport-sm") &&
63
- navList.querySelectorAll(".js-nav-clone__link").length > 0
64
- ) {
65
- // Remove from separate UL and add into primary
66
- navLink.forEach((l) => {
67
- const link = l;
68
- link.parentNode.style.display = "none";
69
- const newNavItem = document.createElement("li");
70
- newNavItem.classList.add("primary-nav__item");
71
-
72
- link.classList.remove("secondary-nav__link");
73
- link.classList.add("primary-nav__link", "col");
74
-
75
- newNavItem.insertAdjacentElement("beforeend", link);
76
-
77
- const primaryNavList = document.querySelector(".primary-nav__list li.primary-nav__language");
78
- primaryNavList.insertAdjacentElement("beforebegin", newNavItem);
79
- });
80
- } else if (
81
- !document.body.classList.contains("viewport-sm") &&
82
- document.querySelector(".secondary-nav__item").style.display === "none"
83
- ) {
84
- // Remove from primary nav and add into separate secondary list
85
- navLink.forEach((l, i) => {
86
- const index = i + 1;
87
- const link = l;
88
- link.classList.add("secondary-nav__link");
89
- link.classList.remove("primary-nav__link", "col");
90
- link.parentNode.remove();
91
- const cloneList = document.querySelector(`.js-nav-clone__list li:nth-child(${index})`);
92
- cloneList.insertAdjacentElement("beforeend", link);
93
- link.parentNode.style.display = "block";
94
- });
77
+ closeNav() {
78
+ this.toggle.classList.remove("active");
79
+ this.navigation.setAttribute(attrHidden, "true");
80
+ this.navigation.classList.add(this.hideClass);
81
+
82
+ if (this.toggle === this.searchToggleBtn) {
83
+ this.updateSearchIcon(false, this.toggle);
84
+ }
85
+
86
+ this.toggle.setAttribute(attrExpanded, "false");
95
87
  }
96
- }
97
88
 
98
- function clonePrimaryItems() {
99
- const detectDuplicate = document.querySelectorAll(".js-nav__duplicate");
100
- const expandableList = document.querySelectorAll(".js-expandable");
101
-
102
- // Clone primary nav items into sub-menu on mobile, so it can still be selected on mobile
103
- if (document.body.classList.contains("viewport-sm") && detectDuplicate.length === 0) {
104
- expandableList.forEach((item) => {
105
- const href = item.querySelector("a").getAttribute("href");
106
- const text = item.querySelector(".submenu-title").innerText;
107
- const childList = item.querySelector(".js-expandable__content");
108
-
109
- const newLink = document.createElement("a");
110
- newLink.classList.add("primary-nav__child-link");
111
- newLink.href = href;
112
- newLink.innerText = text.trim();
113
-
114
- const newItem = document.createElement("li");
115
- newItem.classList.add("primary-nav__child-item", "js-nav__duplicate", "js-expandable__child");
116
- newItem.insertAdjacentElement("beforeend", newLink);
117
- childList.insertBefore(newItem, childList.firstChild);
118
- });
119
- } else if (!document.body.classList.contains("viewport-sm") && detectDuplicate.length > 0) {
120
- detectDuplicate.forEach((duplicate) => {
121
- duplicate.remove();
122
- });
89
+ updateSearchIcon(isOpen, toggle) {
90
+ const icons = {
91
+ open: `
92
+ <span class="ons-btn__inner">
93
+ <svg class="ons-icon ons-icon--close" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" focusable="false" fill="currentColor" role="img" title="ons-icon-close">
94
+ <path d="M12 0C5.4 0 0 5.4 0 12C0 18.6 5.4 24 12 24C18.6 24 24 18.6 24 12C24 5.4 18.6 0 12 0ZM17.3143 15.5143C17.6571 15.8571 17.6571 16.3714 17.3143 16.7143L16.7143 17.3143C16.3714 17.6571 15.8571 17.6571 15.5143 17.3143L12 13.8L8.48571 17.3143C8.14286 17.6571 7.62857 17.6571 7.28571 17.3143L6.68571 16.7143C6.34286 16.3714 6.34286 15.8571 6.68571 15.5143L10.2 12L6.68571 8.48571C6.34286 8.14286 6.34286 7.62857 6.68571 7.28571L7.28571 6.68571C7.62857 6.34286 8.14286 6.34286 8.48571 6.68571L12 10.2L15.5143 6.68571C15.8571 6.34286 16.3714 6.34286 16.7143 6.68571L17.3143 7.28571C17.6571 7.62857 17.6571 8.14286 17.3143 8.48571L13.8 12L17.3143 15.5143Z"/>
95
+ </svg>
96
+ <span class="ons-btn__text"></span>
97
+ </span>`,
98
+ close: `
99
+ <span class="ons-btn__inner">
100
+ <svg class="ons-icon ons-icon--search ons-u-mr-2xs" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" fill="currentColor" role="img" title="ons-icon-search">
101
+ <path d="M11.86 10.23 8.62 6.99a4.63 4.63 0 1 0-6.34 1.64 4.55 4.55 0 0 0 2.36.64 4.65 4.65 0 0 0 2.33-.65l3.24 3.23a.46.46 0 0 0 .65 0l1-1a.48.48 0 0 0 0-.62Zm-5-3.32a3.28 3.28 0 0 1-2.31.93 3.22 3.22 0 1 1 2.35-.93Z"></path>
102
+ </svg>
103
+ <span class="ons-btn__text"></span>
104
+ </span>`
105
+ };
106
+ if (isOpen != null) {
107
+ toggle.innerHTML = isOpen ? icons.open : icons.close;
108
+ toggle.classList.toggle("ons-btn--close", isOpen);
109
+ toggle.classList.toggle("ons-btn--search-icon", !isOpen);
110
+ }
123
111
  }
124
- }
125
112
 
126
- export default function initNav() {
127
- window.addEventListener("resize", () => {
128
- clonePrimaryItems();
129
- cloneSecondaryNav();
130
- });
131
-
132
- document.addEventListener("keydown", (e) => {
133
- if (e.key === "Escape") {
134
- // Find all nav items currently hovered (with open submenu)
135
- document.querySelectorAll(".primary-nav__item:hover > ul").forEach((submenu) => {
136
- submenu.classList.add("ons-u-hidden");
137
- const parentItem = submenu.closest(".primary-nav__item");
138
- // Handler to restore submenu on mouse leave
139
- const handleMouseLeave = () => {
140
- submenu.classList.remove("ons-u-hidden");
141
- parentItem.removeEventListener("mouseleave", handleMouseLeave);
142
- };
143
- parentItem.addEventListener("mouseleave", handleMouseLeave, { once: true });
144
- });
113
+ isHidden(el) {
114
+ if (!el) {
115
+ return true;
145
116
  }
146
- });
147
117
 
148
- const primaryNav = document.querySelector("#nav-primary");
149
- const searchBar = document.querySelector("#searchBar");
150
- const navItem = document.querySelectorAll(".js-nav");
151
- const expandableItems = document.querySelectorAll(".js-expandable");
118
+ const style = window.getComputedStyle(el);
119
+ if (style.display === "none") {
120
+ return true;
121
+ }
152
122
 
153
- clonePrimaryItems();
154
- cloneSecondaryNav();
123
+ // `offsetParent` can be `null` for reasons other than `display: none` (e.g. fixed positioning),
124
+ // so use layout rects as a more reliable proxy for visibility.
125
+ return el.getClientRects().length === 0;
126
+ }
155
127
 
156
- primaryNav.classList.add("nav-main--hidden");
157
- primaryNav.ariaExpanded = false;
128
+ setAria(viewportDetails = null) {
129
+ const isToggleHidden = this.isHidden(this.toggle);
130
+ this.toggle.setAttribute(attrDisabled, "false");
131
+
132
+ this.searchToggleBtn?.setAttribute(attrDisabled, "false");
133
+
134
+ // iOS Safari/Chrome can change viewport height during scroll when the browser chrome
135
+ // hides/shows, which can trigger resize events. If the user has the navigation open
136
+ // on a small screen, don't force-close it in response to these resizes.
137
+ if (
138
+ viewportDetails &&
139
+ !isToggleHidden &&
140
+ this.navigation.getAttribute(attrHidden) === "false"
141
+ ) {
142
+ return;
143
+ }
158
144
 
159
- expandableItems.forEach((item) => {
160
- item.addEventListener("click", (event) => {
161
- if (document.body.classList.contains("viewport-sm")) {
162
- event.preventDefault();
163
- toggleSubnav(item);
164
- }
165
- });
166
- });
167
-
168
- // stop parent element from taking over all click events
169
- document.querySelectorAll(".js-expandable > .js-expandable__content").forEach((elem) => {
170
- elem.addEventListener("click", (event) => {
171
- event.stopPropagation();
172
- });
173
- });
174
-
175
- navItem.forEach((item) => {
176
- item.addEventListener("keydown", (e) => {
177
- const focusedItem = document.querySelector(".js-expandable__child a:focus"); // only selects child item that is in focus
178
- const keycode = e.keyCode;
179
- const up = 38;
180
- const down = 40;
181
- const right = 39;
182
- const left = 37;
183
- const esc = 27;
184
- const tab = 9;
185
- if (keycode === tab && focusedItem) {
186
- item.classList.remove("primary-nav__item--focus");
187
- item.nextElementSibling?.focus();
188
- }
189
- if (keycode === esc) {
190
- item.classList.remove("primary-nav__item--focus");
191
- const closestNav = item.closest(".js-nav");
192
- const link = closestNav.querySelector("a");
193
- link.classList.add("hide-children");
194
- link.focus();
195
- link.addEventListener("focusout", () => {
196
- link.classList.remove("hide-children");
197
- });
198
- }
199
- if (keycode === down) {
200
- e.preventDefault();
201
- item.classList.add("primary-nav__item--focus");
202
- if (focusedItem) {
203
- focusedItem.parentElement.nextElementSibling?.querySelector("a").focus();
204
- } else {
205
- item.querySelector(".js-expandable__child a")?.focus();
206
- }
207
- }
208
- if (keycode === up) {
209
- e.preventDefault();
210
- if (focusedItem && focusedItem.parentElement.previousElementSibling) {
211
- focusedItem.parentElement.previousElementSibling?.querySelector("a").focus();
212
- } else {
213
- item.classList.remove("primary-nav__item--focus");
214
- item.querySelector("a")?.focus();
215
- }
216
- }
217
- if (keycode === right) {
218
- e.preventDefault();
219
- item.classList.remove("primary-nav__item--focus");
220
- const closestNav = item.closest(".js-nav");
221
- closestNav.nextElementSibling?.querySelector("a").focus();
222
- }
223
- if (keycode === left) {
224
- e.preventDefault();
225
- item.classList.remove("primary-nav__item--focus");
226
- const closestNav = item.closest(".js-nav");
227
- closestNav.previousElementSibling?.querySelector("a").focus();
145
+ if (!isToggleHidden) {
146
+ // close nav by default if toggle button is visible
147
+ this.closeNav();
148
+ } else {
149
+ // if toggle is hidden, set nav to open (for visible desktop nav)
150
+ this.toggle.setAttribute(attrExpanded, "false"); // set mobile menu expanded to false if hidden
151
+ this.navigation.removeAttribute(attrHidden);
152
+ this.navigation.classList.remove(this.hideClass);
153
+
154
+ if (this.hideClass !== "ons-u-d-no") {
155
+ this.navigation.classList.remove(this.hideClass);
156
+ } else {
157
+ this.closeNav();
228
158
  }
229
- });
230
- });
231
-
232
- const expandBehaviour = (item, expandedBool) => {
233
- if (!document.body.classList.contains("viewport-sm")) {
234
- const navLink = item.querySelector(".primary-nav__link");
235
- navLink.ariaExpanded = expandedBool;
236
- const expandable = item.querySelector(".js-expandable__content");
237
- expandable.ariaExpanded = expandedBool;
238
159
  }
239
- };
240
-
241
- expandableItems.forEach((item) => {
242
- item.addEventListener("focusin", () => expandBehaviour(item, true));
243
- item.addEventListener("pointerenter", () => expandBehaviour(item, true));
244
- });
245
-
246
- expandableItems.forEach((item) => {
247
- item.addEventListener("focusout", () => expandBehaviour(item, false));
248
- item.addEventListener("pointerleave", () => expandBehaviour(item, false));
249
- });
250
-
251
- const menuToggle = document.querySelector("#menu-toggle");
252
- const menuToggleContainer = menuToggle.parentNode;
253
- const searchToggle = document.querySelector("#search-toggle");
254
- const searchToggleContainer = searchToggle.parentNode;
255
-
256
- menuToggle.addEventListener("click", (event) => {
257
- event.preventDefault();
258
- if (!searchBar.classList.contains("nav-search--hidden")) {
259
- toggleSearch(searchToggleContainer, searchBar);
160
+ }
161
+
162
+ toggleMenuAndSearch() {
163
+ if (this.menuBtn) {
164
+ const isMenuOpen = this.menuBtn.getAttribute("aria-expanded") === "true";
165
+
166
+ if (isMenuOpen && this.toggle == this.menuBtn && this.searchBtn) {
167
+ this.updateSearchIcon(false, this.searchToggleBtn && this.searchBtn);
168
+ this.searchBtn.setAttribute("aria-expanded", "false");
169
+ this.searchEl.setAttribute("aria-hidden", "true");
170
+ this.searchEl.classList.add("ons-u-d-no");
171
+ this.searchToggleBtn.classList.remove("active");
172
+ }
260
173
  }
261
- toggleMenu(menuToggleContainer, primaryNav);
262
- });
263
174
 
264
- searchToggle.addEventListener("click", (event) => {
265
- event.preventDefault();
266
- if (!primaryNav.classList.contains("nav-main--hidden")) {
267
- toggleMenu(menuToggleContainer, primaryNav);
175
+ if (this.searchBtn) {
176
+ const isSearchOpen = this.searchBtn.getAttribute("aria-expanded") === "true";
177
+
178
+ if (isSearchOpen && this.toggle == this.searchToggleBtn && this.menuBtn) {
179
+ this.menuBtn.setAttribute("aria-expanded", "false");
180
+ this.menuEl.setAttribute("aria-hidden", "true");
181
+ this.menuEl.classList.add("ons-u-d-no");
182
+ this.menuBtn.classList.remove("active");
183
+ }
268
184
  }
269
- toggleSearch(searchToggleContainer, searchBar);
270
- });
185
+ }
186
+ }
187
+
188
+ export default async function initNav(el) {
189
+ const toggleNavigationBtn = el.querySelector(".ons-js-navigation-button");
190
+ const navigationEl = el.querySelector(".ons-js-navigation");
191
+ const navigationHideClass = "ons-u-d-no@2xs@l";
192
+ const toggleSubNavigationBtn = el.querySelector(".ons-js-sub-navigation-button");
193
+ const subNavigationEl = el.querySelector(".ons-js-secondary-nav");
194
+ const subNavigationHideClass = "ons-u-d-no";
195
+ const toggleSearchBtn = el.querySelector(".ons-js-toggle-search");
196
+ const searchEl = el.querySelector(".ons-js-navigation-search");
197
+ const searchHideClass = "ons-u-d-no@xs@l";
198
+ const toggleServicesBtn = el.querySelector(".ons-js-toggle-services");
199
+ const servicesEl = el.querySelector(".ons-js-services-mobile-nav");
200
+ const servicesHideClass = "ons-u-d-no";
201
+ const toggleHeaderSearchBtn = el.querySelector(".ons-js-toggle-header-search");
202
+ const headerSearchHideClass = "ons-u-d-no";
203
+ const headerSearchEl = el.querySelector(".ons-js-header-search");
204
+ const menuEl = el.querySelector(".ons-js-nav-menu");
205
+ const toggleHeaderMenuBtn = el.querySelector(".ons-js-toggle-nav-menu");
206
+
207
+ if (toggleNavigationBtn) {
208
+ new NavigationToggle(
209
+ el,
210
+ toggleNavigationBtn,
211
+ navigationEl,
212
+ navigationHideClass
213
+ ).registerEvents();
214
+ }
215
+
216
+ if (toggleSubNavigationBtn) {
217
+ new NavigationToggle(
218
+ el,
219
+ toggleSubNavigationBtn,
220
+ subNavigationEl,
221
+ subNavigationHideClass
222
+ ).registerEvents();
223
+ }
224
+
225
+ if (toggleSearchBtn) {
226
+ new NavigationToggle(el, toggleSearchBtn, searchEl, searchHideClass).registerEvents();
227
+ }
228
+
229
+ if (toggleHeaderSearchBtn) {
230
+ new NavigationToggle(
231
+ el,
232
+ toggleHeaderSearchBtn,
233
+ headerSearchEl,
234
+ headerSearchHideClass
235
+ ).registerEvents();
236
+ }
237
+
238
+ if (toggleHeaderMenuBtn) {
239
+ new NavigationToggle(el, toggleHeaderMenuBtn, menuEl, headerSearchHideClass).registerEvents();
240
+ }
241
+
242
+ if (toggleServicesBtn) {
243
+ new NavigationToggle(el, toggleServicesBtn, servicesEl, servicesHideClass).registerEvents();
244
+ }
271
245
  }
@@ -0,0 +1 @@
1
+ export default function initNav(): void;