mirror of
https://github.com/modernweb-dev/rocket.git
synced 2026-03-21 15:54:57 +00:00
Compare commits
3 Commits
@rocket/se
...
@rocket/na
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9090d64b9 | ||
|
|
728a205b7b | ||
|
|
67ba29d45a |
@@ -47,3 +47,9 @@ export const headlineConverter = () => html`
|
||||
```
|
||||
|
||||
How it then works is very similar to https://www.11ty.dev/docs/plugins/navigation/
|
||||
|
||||
## Sidebar redirects
|
||||
|
||||
By default, the sidebar nav redirects clicks on category headings to the first child page in that category.
|
||||
|
||||
To disable those redirects, override `_includes/_joiningBlocks/_layoutSidebar/sidebar/20-navigation.njk` and add the `no-redirects` attribute to the `<rocket-navigation>` element.
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# @rocket/navigation
|
||||
|
||||
## 0.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 728a205: feat(navigation): add no-redirects attribute
|
||||
|
||||
By default, the sidebar nav redirects clicks on category headings to
|
||||
their first child.
|
||||
|
||||
To disable those redirects, override
|
||||
\_includes/\_joiningBlocks/\_layoutSidebar/sidebar/20-navigation.njk
|
||||
and add the no-redirects attribute to the <rocket-navigation>
|
||||
element.
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@rocket/navigation",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.1",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -1,3 +1,27 @@
|
||||
/**
|
||||
* Debounce a function
|
||||
* @template {(this: any, ...args: any[]) => void} T
|
||||
* @param {T} func function
|
||||
* @param {number} wait time in milliseconds to debounce
|
||||
* @param {boolean} immediate when true, run immediately and on the leading edge
|
||||
* @return {T} debounced function
|
||||
*/
|
||||
function debounce(func, wait, immediate) {
|
||||
/** @type {number|undefined} */
|
||||
let timeout;
|
||||
return /** @type {typeof func}*/ (function () {
|
||||
let args = /** @type {Parameters<typeof func>} */ (/** @type {unknown}*/ (arguments));
|
||||
const later = () => {
|
||||
timeout = undefined;
|
||||
if (!immediate) func.apply(this, args);
|
||||
};
|
||||
const callNow = immediate && !timeout;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
if (callNow) func.apply(this, args);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {object} NavigationListItem
|
||||
* @property {HTMLElement} headline
|
||||
@@ -5,12 +29,17 @@
|
||||
* @property {number} top
|
||||
*/
|
||||
|
||||
/**
|
||||
* @element rocket-navigation
|
||||
* @attr {Boolean} no-redirects - if set, will not redirect to first child of nav category when clicking on category header.
|
||||
*/
|
||||
export class RocketNavigation extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
/** @type NavigationListItem[] */
|
||||
this.list = [];
|
||||
this.__scrollHandler = this.__scrollHandler.bind(this);
|
||||
this.__clickHandler = this.__clickHandler.bind(this);
|
||||
this.__scrollHandler = debounce(this.__scrollHandler.bind(this), 25, true);
|
||||
this.__isSetup = false;
|
||||
}
|
||||
|
||||
@@ -20,27 +49,7 @@ export class RocketNavigation extends HTMLElement {
|
||||
}
|
||||
this.__isSetup = true;
|
||||
|
||||
this.addEventListener('click', ev => {
|
||||
const el = /** @type {HTMLElement} */ (ev.target);
|
||||
if (el.classList.contains('anchor')) {
|
||||
const anchor = /** @type {HTMLAnchorElement} */ (el);
|
||||
ev.preventDefault();
|
||||
this.dispatchEvent(new Event('close-overlay', { bubbles: true }));
|
||||
// wait for closing animation to finish before start scrolling
|
||||
setTimeout(() => {
|
||||
const parsedUrl = new URL(anchor.href);
|
||||
document.location.hash = parsedUrl.hash;
|
||||
}, 250);
|
||||
}
|
||||
const links = el.parentElement?.querySelectorAll('ul a');
|
||||
if (links && links.length > 1) {
|
||||
const subLink = /** @type {HTMLAnchorElement} */ (links[1]);
|
||||
if (!subLink.classList.contains('anchor')) {
|
||||
ev.preventDefault();
|
||||
subLink.click();
|
||||
}
|
||||
}
|
||||
});
|
||||
this.addEventListener('click', this.__clickHandler);
|
||||
|
||||
const anchors = /** @type {NodeListOf<HTMLAnchorElement>} */ (this.querySelectorAll(
|
||||
'li.current a.anchor',
|
||||
@@ -57,12 +66,41 @@ export class RocketNavigation extends HTMLElement {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: debounce
|
||||
window.addEventListener('scroll', this.__scrollHandler);
|
||||
window.addEventListener('scroll', this.__scrollHandler, { passive: true });
|
||||
|
||||
this.__scrollHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Event} ev
|
||||
*/
|
||||
__clickHandler(ev) {
|
||||
const el = /** @type {HTMLElement} */ (ev.target);
|
||||
if (el.classList.contains('anchor')) {
|
||||
const anchor =
|
||||
el instanceof HTMLAnchorElement
|
||||
? el
|
||||
: /** @type{HTMLAnchorElement} */ (el.querySelector('a.anchor'));
|
||||
ev.preventDefault();
|
||||
this.dispatchEvent(new Event('close-overlay', { bubbles: true }));
|
||||
// wait for closing animation to finish before start scrolling
|
||||
setTimeout(() => {
|
||||
const parsedUrl = new URL(anchor.href);
|
||||
document.location.hash = parsedUrl.hash;
|
||||
}, 250);
|
||||
}
|
||||
if (!this.hasAttribute('no-redirects')) {
|
||||
const links = el.parentElement?.querySelectorAll('ul a');
|
||||
if (links && links.length > 1) {
|
||||
const subLink = /** @type {HTMLAnchorElement} */ (links[1]);
|
||||
if (!subLink.classList.contains('anchor')) {
|
||||
ev.preventDefault();
|
||||
subLink.click();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
__scrollHandler() {
|
||||
for (const listObj of this.list) {
|
||||
listObj.top = listObj.headline.getBoundingClientRect().top;
|
||||
|
||||
@@ -58,7 +58,8 @@ describe('rocket-navigation', () => {
|
||||
expect(anchorSpy).to.not.be.called;
|
||||
});
|
||||
|
||||
it('will mark the currently "active" headline in the menu', async () => {
|
||||
it('will mark the currently "active" headline in the menu', async function () {
|
||||
this.timeout(5000);
|
||||
function addBlock(headline, length = 5) {
|
||||
return html`
|
||||
<h2 id="${headline}">${headline}</h2>
|
||||
@@ -96,20 +97,20 @@ describe('rocket-navigation', () => {
|
||||
}
|
||||
</style>
|
||||
`);
|
||||
await aTimeout(0);
|
||||
await aTimeout(50);
|
||||
const anchorLis = wrapper.querySelectorAll('.menu-item.anchor');
|
||||
expect(anchorLis[0]).to.have.class('current');
|
||||
expect(anchorLis[1]).to.not.have.class('current');
|
||||
expect(anchorLis[2]).to.not.have.class('current');
|
||||
|
||||
document.querySelector('#middle').scrollIntoView();
|
||||
await aTimeout(20);
|
||||
await aTimeout(100);
|
||||
expect(anchorLis[0]).to.not.have.class('current');
|
||||
expect(anchorLis[1]).to.have.class('current');
|
||||
expect(anchorLis[2]).to.not.have.class('current');
|
||||
|
||||
document.querySelector('#bottom').scrollIntoView();
|
||||
await aTimeout(20);
|
||||
await aTimeout(100);
|
||||
expect(anchorLis[0]).to.not.have.class('current');
|
||||
expect(anchorLis[1]).to.not.have.class('current');
|
||||
expect(anchorLis[2]).to.have.class('current');
|
||||
|
||||
Reference in New Issue
Block a user