Last active
May 4, 2023 12:51
-
-
Save jpzwarte/cb4b872e65fdc9742c9727474ba3e36b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<style> | |
dna-page-header[stuck], | |
dna-tab-bar[stuck] { | |
box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.2); | |
// Make sure the shadow does not bleed at the top | |
clip-path: inset(0 0 -10px 0); | |
} | |
</style> | |
<dna-page-header ${sticky()} heading="Hello world!"></dna-page-header> | |
<dna-tab-bar ${sticky({ stuckTo: 'dna-page-header' })}> | |
<dna-tab-button>Tab 1</dna-tab-button> | |
<dna-tab-button>Tab 2</dna-tab-button> | |
</dna-tab-bar> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* eslint-disable @typescript-eslint/no-non-null-assertion */ | |
import type { DirectiveParameters, ElementPart, PartInfo } from 'lit/directive.js'; | |
import { AsyncDirective } from 'lit/async-directive.js'; | |
import { PartType, directive } from 'lit/directive.js'; | |
export interface StickyDirectiveConfig { | |
offset?: number; | |
position?: 'top' | 'bottom'; | |
stuckTo?: string | HTMLElement; | |
} | |
export class StickyDirective extends AsyncDirective { | |
config?: StickyDirectiveConfig; | |
host?: HTMLElement; | |
stuckTo?: HTMLElement | null; | |
#onScroll = (): void => { | |
const { top } = this.host!.getBoundingClientRect(); | |
if (this.stuckTo) { | |
const { bottom } = this.stuckTo.getBoundingClientRect(); | |
this.host!.toggleAttribute('stuck', top <= bottom); | |
this.host!.style.insetBlockStart = `${bottom}px`; | |
} else { | |
this.host?.toggleAttribute('stuck', top <= this.config!.offset!); | |
} | |
}; | |
constructor(partInfo: PartInfo) { | |
super(partInfo); | |
if (partInfo.type !== PartType.ELEMENT) { | |
throw new Error('The `sticky` directive must be used on the element itself'); | |
} | |
} | |
disconnected(): void { | |
this.#removeScrollListener(); | |
this.#reset(this.host!, this.config!); | |
} | |
reconnected(): void { | |
this.#addScrollListener(); | |
} | |
// eslint-disable-next-line @typescript-eslint/no-empty-function | |
render(_config: StickyDirectiveConfig): void {} | |
override update(part: ElementPart, [config = {}]: DirectiveParameters<this>): void { | |
this.host = part.element as HTMLElement; | |
this.config = { position: 'top', ...config }; | |
if (typeof this.config.stuckTo === 'string') { | |
this.stuckTo = (this.host.getRootNode() as HTMLElement).querySelector(this.config.stuckTo); | |
} else if (typeof this.config.stuckTo === 'undefined') { | |
this.config.offset ??= 0; | |
} | |
this.#setup(this.host, this.config); | |
} | |
#reset(host: HTMLElement, config: StickyDirectiveConfig): void { | |
const { position = 'top' } = config; | |
host.style.position = ''; | |
if (position === 'top') { | |
host.style.insetBlockStart = ''; | |
} else { | |
host.style.insetBlockEnd = ''; | |
} | |
} | |
#setup(host: HTMLElement, config: StickyDirectiveConfig): void { | |
const { offset, position = 'top' } = config; | |
host.style.position = 'sticky'; | |
if (typeof offset !== 'undefined') { | |
host.style[`insetBlock${position === 'top' ? 'Start' : 'End'}`] = `${offset}px`; | |
} | |
this.#addScrollListener(); | |
} | |
#addScrollListener(): void { | |
document.addEventListener('scroll', this.#onScroll, { passive: true }); | |
} | |
#removeScrollListener(): void { | |
document.removeEventListener('scroll', this.#onScroll); | |
} | |
} | |
export const sticky = directive(StickyDirective); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment