Skip to content

Commit b60de79

Browse files
authored
WD-30045 - Implement sliding navigation in snapcraft.io (#5437)
* WD-30045 - Move changes from global-nav to snapcraft.io * WD-30045 - Navigation mobile view fixed * WD-30045 - Navigation desktop view fixed * WD-30045 - Pretty print dev logs * WD-30045 - Fix SCSS linting * WD-30045 - Fix Copilot comments * WD-30045 - Fix bug in adding "back" buttons to global-nav * WD-30045 - Fix margin on /docs page and other PR comments
1 parent c3acdfc commit b60de79

13 files changed

Lines changed: 422 additions & 89 deletions

File tree

entrypoint

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ RUN_COMMAND="gunicorn webapp.app:create_app() --bind $1 --worker-class gevent --
66

77
if [ "${FLASK_DEBUG}" = true ] || [ "${FLASK_DEBUG}" = 1 ]; then
88
RUN_COMMAND="${RUN_COMMAND} --reload --log-level debug --timeout 9999"
9+
RUN_COMMAND="${RUN_COMMAND} --logger-class canonicalwebteam.flask_base.log_utils.GunicornDevLogger"
910
fi
1011

1112
${RUN_COMMAND}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"@babel/preset-react": "7.26.3",
2727
"@babel/preset-typescript": "7.26.0",
2828
"@canonical/cookie-policy": "3.6.5",
29-
"@canonical/global-nav": "3.7.3",
29+
"@canonical/global-nav": "3.8.0",
3030
"@canonical/react-components": "3.6.0",
3131
"@canonical/store-components": "0.54.2",
3232
"@dnd-kit/core": "6.3.1",

static/js/base/base.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import "./navigation";
21
import "./ga";
32
import "./contactForm";
43
import "./sentry";

static/js/base/global-nav.ts

Lines changed: 0 additions & 63 deletions
This file was deleted.
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
export const setActiveDropdown = (
2+
dropdownToggleButton: HTMLElement,
3+
isActive = true,
4+
) => {
5+
// set active state of the dropdown toggle (to slide the panel into view)
6+
const dropdownToggleEl = dropdownToggleButton.closest(
7+
".p-navigation__item--dropdown-toggle",
8+
);
9+
if (dropdownToggleEl) {
10+
dropdownToggleEl.classList.toggle("is-active", isActive);
11+
dropdownToggleEl.classList.toggle("is-selected", isActive);
12+
const globalNavButton = dropdownToggleEl.querySelector(
13+
":scope > .p-navigation__link",
14+
);
15+
// fix some states from global-nav elements in mobile
16+
if (globalNavButton) {
17+
globalNavButton.setAttribute("aria-expanded", isActive.toString());
18+
globalNavButton.classList.toggle("is-selected", isActive);
19+
}
20+
}
21+
22+
// set active state of the parent dropdown panel (to fade it out of view)
23+
const parentLevelDropdown = dropdownToggleEl?.closest(
24+
".p-navigation__dropdown",
25+
);
26+
if (parentLevelDropdown) {
27+
parentLevelDropdown.classList.toggle("is-active", isActive);
28+
}
29+
30+
// set active state of the top navigation list under p-navigation__nav
31+
// to set the position of the sliding panel properly
32+
const topLevelNavigation = dropdownToggleButton.closest(".p-navigation__nav");
33+
if (topLevelNavigation) {
34+
const topLevelItems = topLevelNavigation.querySelectorAll(
35+
":scope > .p-navigation__items",
36+
);
37+
38+
for (const item of topLevelItems) {
39+
// in case there are more than one top level navigation lists, we need to
40+
// mark as active the one that contains the clicked button and hide the rest
41+
if (item.contains(dropdownToggleButton)) {
42+
item.classList.toggle("is-active", isActive);
43+
} else {
44+
item.classList.toggle("u-hide", isActive);
45+
}
46+
}
47+
}
48+
};
49+
50+
export const setListFocusable = (list: Element) => {
51+
// turn on focusability for all direct children in the target dropdown
52+
if (list) {
53+
for (const item of list.children) {
54+
item.children[0].setAttribute("tabindex", "0");
55+
}
56+
}
57+
};
58+
59+
export const setFocusable = (target: Element) => {
60+
// if target dropdown is not a list, find the list in it
61+
const isList =
62+
target.classList.contains("p-navigation__dropdown") ||
63+
target.classList.contains("p-navigation__items");
64+
65+
if (!isList) {
66+
// find all lists in the target dropdown and make them focusable
67+
target.querySelectorAll(".p-navigation__dropdown").forEach((element) => {
68+
setListFocusable(element);
69+
});
70+
} else {
71+
setListFocusable(target);
72+
}
73+
};
74+
75+
export const collapseDropdown = (
76+
dropdownToggleButton: HTMLElement,
77+
targetDropdown: HTMLElement,
78+
) => {
79+
targetDropdown.setAttribute("aria-hidden", "true");
80+
setActiveDropdown(dropdownToggleButton, false);
81+
};
82+
83+
export const expandDropdown = (
84+
dropdownToggleButton: HTMLElement,
85+
targetDropdown: HTMLElement,
86+
) => {
87+
setActiveDropdown(dropdownToggleButton);
88+
targetDropdown.setAttribute("aria-hidden", "false");
89+
setFocusable(targetDropdown);
90+
};
91+
92+
const toggleAnimationPlaying = (
93+
element: Element,
94+
animationDuration?: number,
95+
) => {
96+
const endAnimation = () => {
97+
element.classList.toggle("js-animation-playing", false);
98+
};
99+
element.classList.toggle("js-animation-playing", true);
100+
// force browser to flush all pending style and layout calculations immediately
101+
void (element as HTMLElement).offsetWidth;
102+
setTimeout(endAnimation, animationDuration);
103+
};
104+
105+
export const setupAnimationStart = (
106+
elements: Element[],
107+
animationDuration?: number,
108+
) => {
109+
elements.forEach((toggle: Element) => {
110+
toggleAnimationPlaying(toggle.parentElement!, animationDuration);
111+
});
112+
};
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
export function patchAllCanonicalMobileMarkup() {
2+
const allCanonicalMobile = document.getElementById("all-canonical-mobile");
3+
const topMobileSections = allCanonicalMobile?.querySelectorAll(
4+
".global-nav__dropdown-toggle",
5+
);
6+
7+
topMobileSections?.forEach((section: Element) => {
8+
const sectionLink = section.querySelector("button.p-navigation__link");
9+
const sectionHref = sectionLink?.getAttribute("href");
10+
11+
const sectionLinksList = section.querySelector("ul");
12+
sectionLinksList?.setAttribute("aria-hidden", "true");
13+
14+
// add the back button as the first item of the section
15+
if (sectionHref && sectionLinksList) {
16+
sectionLinksList.prepend(createBackButtonItem(sectionHref));
17+
}
18+
});
19+
}
20+
21+
function createFromHTML(html: string) {
22+
const div = window.document.createElement("div");
23+
div.innerHTML = html;
24+
return div.childNodes[0];
25+
}
26+
27+
function createBackButtonItem(href: string) {
28+
// remove the # from the href
29+
const ariaControls = href.slice(1);
30+
return createFromHTML(`<li class="p-navigation__item--dropdown-close">
31+
<a href=${href} aria-controls=${ariaControls} class="p-navigation__link js-back-button">
32+
Back
33+
</a>
34+
</li>`);
35+
}

static/js/base/navigation/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import "./login";
2+
3+
import { createNav as createAllCanonicalNav } from "@canonical/global-nav";
4+
import { initNavigationListeners } from "./listeners";
5+
import { patchAllCanonicalMobileMarkup } from "./globalNav";
6+
7+
// initialize global-nav ("All Canonical" link) and the rest of the navigation
8+
window.addEventListener("DOMContentLoaded", function () {
9+
createAllCanonicalNav();
10+
patchAllCanonicalMobileMarkup();
11+
initNavigationListeners();
12+
});

0 commit comments

Comments
 (0)