Skip to content

Instantly share code, notes, and snippets.

@jpzwarte
Last active May 4, 2023 12:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jpzwarte/cb4b872e65fdc9742c9727474ba3e36b to your computer and use it in GitHub Desktop.
Save jpzwarte/cb4b872e65fdc9742c9727474ba3e36b to your computer and use it in GitHub Desktop.
<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>
/* 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