From e937fa4eae23ca2638fed6ca13fc29390f41a16c Mon Sep 17 00:00:00 2001 From: Curtis Dulmage Date: Mon, 29 Jul 2024 13:44:01 -0400 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20[Navigation]=20Improve=20current=20?= =?UTF-8?q?highlight=20(#64)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :adhesive_bandage: [PageLoad] Remove hash from url * :sparkles: [NavTracker] Improve current navigation highlight --- src/components/Navigation.astro | 63 +++++++-------------------------- src/layouts/Page.astro | 2 +- src/pages/index.astro | 9 +++-- src/scripts/NavTracker.ts | 25 +++++++++++++ src/scripts/PageLoad.ts | 4 +++ src/scripts/Portfolio.ts | 27 ++++++++++++-- 6 files changed, 73 insertions(+), 57 deletions(-) create mode 100644 src/scripts/NavTracker.ts diff --git a/src/components/Navigation.astro b/src/components/Navigation.astro index e03dd87..18be042 100644 --- a/src/components/Navigation.astro +++ b/src/components/Navigation.astro @@ -3,6 +3,10 @@ interface ItemDescriptor { id: string; name: string; description: string; + // Not using a `current` prop as we need to calculate the + // current item via a `script` instead of `frontmatter`. + // current?: boolean; + // class:list={['Link', {current: item.current}]} } interface Props { @@ -166,61 +170,18 @@ const {id, items = []} = Astro.props; } /* --- Active Item iteration --- */ - /* TODO: Replace these with an `active` class instead. */ - - html[data-current-index='0'] .Item:nth-child(1) .Link, - html[data-current-index='1'] .Item:nth-child(2) .Link, - html[data-current-index='2'] .Item:nth-child(3) .Link, - html[data-current-index='3'] .Item:nth-child(4) .Link, - html[data-current-index='4'] .Item:nth-child(5) .Link, - html[data-current-index='5'] .Item:nth-child(6) .Link, - html[data-current-index='6'] .Item:nth-child(7) .Link, - html[data-current-index='7'] .Item:nth-child(8) .Link, - html[data-current-index='8'] .Item:nth-child(9) .Link, - html[data-current-index='9'] .Item:nth-child(10) .Link, - html[data-current-index='10'] .Item:nth-child(11) .Link, - html[data-current-index='11'] .Item:nth-child(12) .Link, - html[data-current-index='12'] .Item:nth-child(13) .Link, - html[data-current-index='13'] .Item:nth-child(14) .Link, - html[data-current-index='14'] .Item:nth-child(15) .Link { + + .Item[data-current] .Link { cursor: url('../assets/svg/cursors/CursorHorns.svg'), not-allowed; color: var(--color-primary); translate: calc(var(--nav-item-shift-x) * -1) 0; - } - html[data-current-index='0'] .Item:nth-child(1) .Link:active, - html[data-current-index='1'] .Item:nth-child(2) .Link:active, - html[data-current-index='2'] .Item:nth-child(3) .Link:active, - html[data-current-index='3'] .Item:nth-child(4) .Link:active, - html[data-current-index='4'] .Item:nth-child(5) .Link:active, - html[data-current-index='5'] .Item:nth-child(6) .Link:active, - html[data-current-index='6'] .Item:nth-child(7) .Link:active, - html[data-current-index='7'] .Item:nth-child(8) .Link:active, - html[data-current-index='8'] .Item:nth-child(9) .Link:active, - html[data-current-index='9'] .Item:nth-child(10) .Link:active, - html[data-current-index='10'] .Item:nth-child(11) .Link:active, - html[data-current-index='11'] .Item:nth-child(12) .Link:active, - html[data-current-index='12'] .Item:nth-child(13) .Link:active, - html[data-current-index='13'] .Item:nth-child(14) .Link:active, - html[data-current-index='14'] .Item:nth-child(15) .Link:active { - cursor: url('../assets/svg/cursors/CursorHornsClicked.svg'), not-allowed; - } + &:active { + cursor: url('../assets/svg/cursors/CursorHornsClicked.svg'), not-allowed; + } - html[data-current-index='0'] .Item:nth-child(1) .Link::before, - html[data-current-index='1'] .Item:nth-child(2) .Link::before, - html[data-current-index='2'] .Item:nth-child(3) .Link::before, - html[data-current-index='3'] .Item:nth-child(4) .Link::before, - html[data-current-index='4'] .Item:nth-child(5) .Link::before, - html[data-current-index='5'] .Item:nth-child(6) .Link::before, - html[data-current-index='6'] .Item:nth-child(7) .Link::before, - html[data-current-index='7'] .Item:nth-child(8) .Link::before, - html[data-current-index='8'] .Item:nth-child(9) .Link::before, - html[data-current-index='9'] .Item:nth-child(10) .Link::before, - html[data-current-index='10'] .Item:nth-child(11) .Link::before, - html[data-current-index='11'] .Item:nth-child(12) .Link::before, - html[data-current-index='12'] .Item:nth-child(13) .Link::before, - html[data-current-index='13'] .Item:nth-child(14) .Link::before, - html[data-current-index='14'] .Item:nth-child(15) .Link::before { - opacity: 0; + &::before { + opacity: 0; + } } diff --git a/src/layouts/Page.astro b/src/layouts/Page.astro index 77e59b9..cb077e4 100644 --- a/src/layouts/Page.astro +++ b/src/layouts/Page.astro @@ -27,7 +27,7 @@ const OG_IMAGE = new URL('/dulmage-social.png', Astro.site).href; --- - + import {NAV_ID, NAV_TOGGLE_ID, OVERLAY_ID, PERSONAL_EMAIL} from '@data/app'; + import {NavTracker} from '@scripts/NavTracker'; import {Overlay} from '@scripts/Overlay'; import {Portfolio} from '@scripts/Portfolio'; import {SecretEmail} from '@scripts/SecretEmail'; import {Toggler} from '@scripts/Toggler'; + const navTrackerInstance = new NavTracker(`#${NAV_ID} li`); + navTrackerInstance.update(); + const overlayInstance = new Overlay(OVERLAY_ID); overlayInstance.init(); - // Only pass `{scroller: 'main'}` if we want scroll-snapping. - const portfolioInstance = new Portfolio(); + const portfolioInstance = new Portfolio({}, ({mostVisibleIndex}) => { + navTrackerInstance.safeUpdate(mostVisibleIndex); + }); portfolioInstance.init(); const emailInstance = new SecretEmail(PERSONAL_EMAIL); diff --git a/src/scripts/NavTracker.ts b/src/scripts/NavTracker.ts new file mode 100644 index 0000000..2d8723b --- /dev/null +++ b/src/scripts/NavTracker.ts @@ -0,0 +1,25 @@ +export class NavTracker { + #items: NodeListOf; + #lastSavedIndex = 0; + + constructor(selector = 'nav ul li') { + this.#items = document.querySelectorAll(selector); + } + + update(currentIndex = 0) { + this.#items.forEach((element, index) => { + if (index === currentIndex) { + element.setAttribute('data-current', 'true'); + } else { + element.removeAttribute('data-current'); + } + }); + } + + safeUpdate(currentIndex = 0) { + if (this.#lastSavedIndex === currentIndex) return; + + this.#lastSavedIndex = currentIndex; + this.update(currentIndex); + } +} diff --git a/src/scripts/PageLoad.ts b/src/scripts/PageLoad.ts index 86cc8fa..85742dc 100644 --- a/src/scripts/PageLoad.ts +++ b/src/scripts/PageLoad.ts @@ -26,7 +26,11 @@ export class PageLoad { } static #handleReadyState() { + // Before running the timer to "update document", we want to make sure any + // hash URL is removed and we reset our scroll position. + history.replaceState(null, '', ' '); window.scrollTo(0, 0); + window.setTimeout(PageLoad.#updateDocument, PageLoad.MOTION_DELAY); } diff --git a/src/scripts/Portfolio.ts b/src/scripts/Portfolio.ts index 7c94da3..e123e97 100644 --- a/src/scripts/Portfolio.ts +++ b/src/scripts/Portfolio.ts @@ -11,8 +11,19 @@ interface Selectors { scroller?: string; } +interface ScrollData { + currentIndex: number; + nextIndex: number; + mostVisibleIndex: number; + indexProgress: number; +} + +type CustomScrollFn = (data: ScrollData) => void; + export class Portfolio { #selector: Required; + #onScroll: CustomScrollFn | undefined; + #sections: NodeListOf; #scroller: Element | null; #channels: RgbChannels; @@ -31,12 +42,13 @@ export class Portfolio { // user-agent sniff in order to work around it. static SUPPORT_URL_UPDATES = false; - constructor(selectors?: Selectors) { + constructor(selectors?: Selectors, onScroll?: CustomScrollFn) { this.#selector = { cssColorProp: selectors?.cssColorProp || CSS_PORTFOLIO_COLOR_PROP, section: selectors?.section || SECTION_SELECTOR, scroller: selectors?.scroller || '', }; + this.#onScroll = onScroll; this.#sections = document.querySelectorAll(this.#selector.section); this.#scroller = Boolean(this.#selector.scroller) @@ -58,6 +70,10 @@ export class Portfolio { : this.#index + 1; } + get mostVisibleIndex() { + return this.sectionScrollProgress > 60 ? this.nextIndex : this.currentIndex; + } + get channels() { return this.#channels; } @@ -136,8 +152,6 @@ export class Portfolio { if (this.#index === newIndex) return; this.#index = newIndex; - document.documentElement.dataset.currentIndex = newIndex.toString(); - this.#updateUrlHash(); } @@ -170,6 +184,13 @@ export class Portfolio { this.#rafId = requestAnimationFrame(this.#rafCallback); this.#isTicking = true; + + this.#onScroll?.({ + currentIndex: this.currentIndex, + nextIndex: this.nextIndex, + mostVisibleIndex: this.mostVisibleIndex, + indexProgress: this.sectionScrollProgress, + }); }; #handleResize = () => {