Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(esl-anchornav): create esl-anchornav to provide anchor navigation #2577

Merged
merged 15 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .commitlintrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ rules:
-
- esl-a11y-group
- esl-alert
- esl-anchornav
- esl-animate
- esl-base-element
- esl-carousel
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
57 changes: 57 additions & 0 deletions site/src/esl-anchornav/esl-anchornav.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
@BG_COLOR: #f7f7f7;
@SHADOW: 1px 2px 3px rgba(0, 0, 0, 0.2);

esl-anchornav {
[esl-anchornav-items] {
display: flex;
gap: 10px;
flex-wrap: wrap;
}

.esl-anchornav-item.active {
font-weight: bold;
text-decoration: underline;
}
}

[esl-anchor].highlighted {
border-top: 1px dotted #f00;
}

.esl-anchornav-pseudo-fixed {
position: sticky;
top: 0;
right: 0;
padding: 10px 0 10px 10px;

esl-anchornav {
min-width: 160px;
background: @BG_COLOR;
box-shadow: @SHADOW;
}

[esl-anchornav-items] {
display: flex;
justify-content: center;
}
}

[esl-anchornav-sticked] {
background-color: @BG_COLOR;
padding-block: 5px;

&[sticked] {
box-shadow: @SHADOW;
}

esl-anchornav {
display: flex;
gap: 10px;
}

.uip-preview-inner & {
top: -10px;
margin-inline: -10px;
padding-inline: 10px;
}
}
17 changes: 17 additions & 0 deletions site/src/esl-anchornav/esl-anchornav.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {ESLAnchor, ESLAnchornav, ESLAnchornavSticked} from '@exadel/esl/modules/esl-anchornav/core';

import type {ESLAnchornavRender, ESLAnchorData} from '@exadel/esl/modules/esl-anchornav/core';

const demoRenderer: ESLAnchornavRender = (data: ESLAnchorData): Element => {
const a = document.createElement('a');
a.href = `#${data.id}`;
a.className = 'esl-anchornav-item';
a.dataset.index = `${data.index + 1}`;
a.textContent = data.title;
return a;
};

ESLAnchor.register();
ESLAnchornav.setRenderer(demoRenderer);
ESLAnchornav.register();
ESLAnchornavSticked.register();
1 change: 1 addition & 0 deletions site/src/localdev.less
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
@import './esl-share/esl-share.less';
@import './esl-events-demo/esl-events-demo.less';
@import './esl-popup/esl-d-popup-game.less';
@import './esl-anchornav/esl-anchornav.less';

@import './back-link/back-link';

Expand Down
3 changes: 3 additions & 0 deletions site/src/localdev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ ESLOpenState.register();
// Share component loading
import (/* webpackChunkName: 'common/esl-share' */'./esl-share/esl-share');

// Anchornav component loading
import (/* webpackChunkName: 'common/esl-anchornav' */'./esl-anchornav/esl-anchornav');

if (document.querySelector('uip-root')) {
// Init UI Playground
import (/* webpackChunkName: "common/playground" */'./playground/ui-playground');
Expand Down
1 change: 1 addition & 0 deletions site/static/assets/examples/anchornav.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions site/views/components/esl-anchornav.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
layout: content
title: ESL Anchornav
seoTitle: ESL Anchornav - custom element that collects content anchors from the page and provides anchor navigation
name: ESL Anchornav
tags: [components, beta]
aside:
source: src/modules/esl-anchornav
examples:
- anchornav
---

{% mdRender 'src/modules/esl-anchornav/README.md', 'intro' %}
106 changes: 106 additions & 0 deletions site/views/examples/anchornav.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
---
layout: content
title: Anchornav
seoTitle: Anchornav component for prompt navigation to different sections of a page
name: Anchornav
tags: [examples, beta, playground]
icon: examples/anchornav.svg
aside:
components:
- esl-anchornav
---
{% import 'lorem.njk' as lorem %}

{% set imageSrcBase = '/assets/' | url %}

<section class="row">
<div class="col-12">
<uip-root>
<script type="text/html"
label="Anchornav fixed"
uip-snippet
uip-snippet-js="js-snippet-anchornav-element">
<div class="d-flex">
<div>
<!-- paragraph 4 -->
<div esl-anchor id="my-anchor-1" title="Anchor one"></div>
<p>⚓ № 1</p>
<!-- paragraph 5 -->
<div esl-anchor id="my-anchor-2" title="Anchor two"></div>
<p>⚓ № 2</p>
<!-- paragraph 6 -->
<div esl-anchor id="my-anchor-3" title="Anchor three"></div>
<p>⚓ № 3</p>
<!-- paragraph 7 -->
<div esl-anchor id="my-anchor-4" title="Anchor four"></div>
<p>⚓ № 4</p>
<!-- paragraph 6 -->
<div esl-anchor id="my-anchor-5" title="Anchor five"></div>
<p>⚓ № 5</p>
<!-- paragraph 5 -->
<div esl-anchor id="my-anchor-6" title="Anchor six"></div>
<p>⚓ № 6</p>
<!-- paragraph 6 -->
<div esl-anchor id="my-anchor-7" title="Anchor seven"></div>
<p>⚓ № 7</p>
<!-- paragraph 7 -->
</div>
<div>
<div class="esl-anchornav-pseudo-fixed">
<esl-anchornav>
<div class="h4 text-center">Anchornav</div>
</esl-anchornav>
</div>
</div>
</div>
</script>

<script type="text/html"
label="Anchornav sticked"
uip-snippet
uip-snippet-js="js-snippet-anchornav-element">
<div>
<!-- paragraph 3 -->
<div esl-anchornav-sticked><esl-anchornav>Anchors: <nav esl-anchors-items></nav></esl-anchornav></div>
<!-- paragraph 4 -->
<div esl-anchor id="my-anchor-1" title="Anchor one"></div>
<br><p>⚓ № 1</p>
<!-- paragraph 8 -->
<div esl-anchor id="my-anchor-2" title="Anchor two"></div>
<br><p>⚓ № 2</p>
<!-- paragraph 9 -->
<div esl-anchor id="my-anchor-3" title="Anchor three"></div>
<br><p>⚓ № 3</p>
<!-- paragraph 10 -->
<div esl-anchor id="my-anchor-4" title="Anchor four"></div>
<br><p>⚓ № 4</p>
<!-- paragraph 9 -->
<div esl-anchor id="my-anchor-5" title="Anchor five"></div>
<br><p>⚓ № 5</p>
<!-- paragraph 8 -->
<div esl-anchor id="my-anchor-6" title="Anchor six"></div>
<br><p>⚓ № 6</p>
<!-- paragraph 9 -->
<div esl-anchor id="my-anchor-7" title="Anchor seven"></div>
<br><p>⚓ № 7</p>
<!-- paragraph 10 -->
</div>
</script>

<script id="js-snippet-anchornav-element" type="text/plain">
import { ESLAnchornav, ESLAnchorMixin, ESLAnchornavStickedMixin } from '@exadel/esl';
ESLAnchornav.register();
ESLAnchorMixin.register();
ESLAnchornavStickedMixin.register();
</script>

<uip-snippets class="uip-toolbar" dropdown-view="@xs"></uip-snippets>
<uip-settings label="Settings" resizable vertical="@+sm">
<uip-bool-setting label="Highlight anchor" target="[esl-anchor]" mode="append" attribute="class" value="highlighted"></uip-bool-setting>
</uip-settings>
<uip-preview style="max-height: 500px"></uip-preview>
<uip-editor label="Source code (HTML)" collapsible copy></uip-editor>
<uip-editor source="js" label="Source code (JS)" collapsible collapsed copy></uip-editor>
</uip-root>
</div>
</section>
2 changes: 2 additions & 0 deletions src/modules/all.less
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,5 @@
@import './esl-share/core.less';

@import './esl-carousel/all.less';

@import './esl-anchornav/core.less';
3 changes: 3 additions & 0 deletions src/modules/all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,6 @@ export * from './esl-share/core';

// Carousel
export * from './esl-carousel/core';

// Anchornav
export * from './esl-anchornav/core';
11 changes: 11 additions & 0 deletions src/modules/esl-anchornav/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# [ESL](../../../) Anchornav

Version: *1.0.0-beta*.

Authors: *Dmytro Shovchko*.

***Important Notice: the component is under beta version, it is tested and ready to use but be aware of its potential critical API changes.***

<a name="intro"></a>

The ESL Anchornav component allows users to quickly jump to specific page content via predefined anchors. The list of anchors is collected from the page dynamically, so any page updates will be processed and the component updates the navigation list.
3 changes: 3 additions & 0 deletions src/modules/esl-anchornav/core.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@import './core/esl-anchor.less';
@import './core/esl-anchornav.less';
@import './core/esl-anchornav-sticked.less';
5 changes: 5 additions & 0 deletions src/modules/esl-anchornav/core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type * from './core/esl-anchornav-types';

export * from './core/esl-anchornav';
export * from './core/esl-anchornav-sticked';
export * from './core/esl-anchor';
4 changes: 4 additions & 0 deletions src/modules/esl-anchornav/core/esl-anchor.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[esl-anchor] {
ala-n marked this conversation as resolved.
Show resolved Hide resolved
margin-block-end: -2px;
height: 2px;
}
38 changes: 38 additions & 0 deletions src/modules/esl-anchornav/core/esl-anchor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {ESLMixinElement} from '../../esl-mixin-element/core';
import {ExportNs} from '../../esl-utils/environment/export-ns';
import {prop} from '../../esl-utils/decorators';
import {ESLEventUtils} from '../../esl-event-listener/core';

/**
* ESLAnchor - custom mixin element for setting up anchor for {@link ESLAnchornav} attaching
*
* Use example:
* `<div esl-anchor id="my-anchor-id" title="My anchor title"></div>`
*/
@ExportNs('Anchor')
export class ESLAnchor extends ESLMixinElement {
static override is = 'esl-anchor';

@prop('esl:anchor:change') public CHANGE_EVENT: string;

protected override connectedCallback(): void {
super.connectedCallback();
this.sendRequestEvent();
}

protected override disconnectedCallback(): void {
this.sendRequestEvent();
super.disconnectedCallback();
}

/** Sends a broadcast event to Anchornav components to refresh the list of anchors */
protected sendRequestEvent(): void {
ESLEventUtils.dispatch(document.body, this.CHANGE_EVENT);
}
}

declare global {
export interface ESLLibrary {
Anchor: typeof ESLAnchor;
}
}
4 changes: 4 additions & 0 deletions src/modules/esl-anchornav/core/esl-anchornav-sticked.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[esl-anchornav-sticked] {
position: sticky;
top: 0;
}
63 changes: 63 additions & 0 deletions src/modules/esl-anchornav/core/esl-anchornav-sticked.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {ESLMixinElement} from '../../esl-mixin-element/core';
import {listen} from '../../esl-utils/decorators';
import {ESLIntersectionTarget, ESLResizeObserverTarget} from '../../esl-event-listener/core';
import {getViewportForEl} from '../../esl-utils/dom/scroll';
import {ESLAnchornav} from './esl-anchornav';

import type {ESLIntersectionEvent, ESLElementResizeEvent} from '../../esl-event-listener/core';

/**
* ESLAnchornavSticked - custom mixin element for sticky positioned of {@link ESLAnchornav} element
*
* Use example:
* `<div esl-anchornav-sticked><esl-anchornav></esl-anchornav></div>`
*/
export class ESLAnchornavSticked extends ESLMixinElement {
static override is = 'esl-anchornav-sticked';

protected _sticked: boolean = false;

/** The height of this anchornav container */
public get anchornavHeight(): number {
return this.$host.getBoundingClientRect().height;
}

/** Sticked state */
public get sticked(): boolean {
return this._sticked;
}
public set sticked(value: boolean) {
if (this._sticked === value) return;
this._sticked = value;
this.$$attr('sticked', value);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't sound good to have potentially conflicting marker attributes. Do we really need it ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we need marker for sticky state styling

this._onStateChange();
}

/** Childs anchornav element */
protected get $anchornav(): ESLAnchornav | null {
return this.$host.querySelector<ESLAnchornav>(ESLAnchornav.is);
}

/** Handles changing sticky state */
protected _onStateChange(): void {
if (!this.$anchornav) return;
this.$anchornav.offset = this.sticked ? this.anchornavHeight : 0;
}

@listen({
event: 'intersects',
target: (that: ESLAnchornavSticked) => ESLIntersectionTarget.for(that.$host, {
root: getViewportForEl(that.$host),
rootMargin: '-1px 0px 0px 0px',
threshold: [0.99, 1]
})
})
protected _onIntersection(e: ESLIntersectionEvent): void {
this.sticked = e.intersectionRect.y > e.boundingClientRect.y;
}

@listen({event: 'resize', target: (that: ESLAnchornavSticked) => ESLResizeObserverTarget.for(that.$host)})
protected _onResize({borderBoxSize}: ESLElementResizeEvent): void {
this._onStateChange();
}
}
10 changes: 10 additions & 0 deletions src/modules/esl-anchornav/core/esl-anchornav-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/** {@link ESLAnchornav} item renderer */
export type ESLAnchornavRender = (data: ESLAnchorData) => string | Element;

/** {@link ESLAnchornav} anchor data interface */
export interface ESLAnchorData {
id: string;
title: string;
index: number; // order number in the anchor list
$anchor: HTMLElement;
}
3 changes: 3 additions & 0 deletions src/modules/esl-anchornav/core/esl-anchornav.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
esl-anchornav {
display: block;
}
Loading
Loading