Skip to content

Commit

Permalink
Merge pull request #2577 from exadel-inc/feat/esl-anchornav
Browse files Browse the repository at this point in the history
feat(esl-anchornav): create esl-anchornav to provide anchor navigation
  • Loading branch information
ala-n authored Aug 13, 2024
2 parents 3dabf32 + 9caa111 commit bb9ceea
Show file tree
Hide file tree
Showing 22 changed files with 566 additions and 0 deletions.
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] {
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);
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

0 comments on commit bb9ceea

Please sign in to comment.