Skip to content

Commit

Permalink
Merge pull request #2626 from exadel-inc/feature/esl-anchornav-final-…
Browse files Browse the repository at this point in the history
…update

[ESL Anchornav] final update
  • Loading branch information
ala-n authored Sep 30, 2024
2 parents 4f9e4e5 + 851290a commit 0f1bd57
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 9 deletions.
43 changes: 43 additions & 0 deletions src/modules/esl-anchornav/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,46 @@ Authors: *Dmytro Shovchko*.
<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.

### How it works

The component collects anchors on the page, builds a list of anchors using the user-defined renderer function, and appends it to the inner items container element (it will be an element with `esl-anchors-items` attribute). After that, the component observes the position of the collected anchors to detect currently active anchor and marks it with active class marker.

For example, markup may be the following:
```html
<esl-anchornav>Anchors: <nav esl-anchors-items></nav></esl-anchornav>
```
If for some reason you do not add an element with this attribute to the component content, it will not be a mistake. A div with the `esl-anchors-items` attribute will be created and added to the component content in this case.

You can assign anchors to any element on the page. To do this, you must give this element the `esl-anchor` attribute. Another mandatory requirement for an element is that it must contain two attributes `id` and `title` (this is the text to be displayed in the list).

### Items renderer

For all collected anchors it is used renderer function which builds the inner content of the anchors list. Here is a default renderer:
```ts
ESLAnchornav.setRenderer((data: ESLAnchorData, index: number, anchornav: ESLAnchornav) => `<a class="esl-anchornav-item" href="#${data.id}">${data.title}</a>`);
```
You can define your own renderer. You can define several renderers with different names and use them on different components.

### ESLAnchornav

#### Public API
- `setRenderer` - a static method to set item renderer
- `getRenderer` - a static method to get item renderer with specified name
- `active` (ESLAnchorData) - active anchor data
- `offset` (number) - anchornav top offset, used when detects active anchors (0 by default)
- `update` - updates component

#### Attributes | Properties:

- `renderer` - item renderer which is used to build inner markup
- `active-class` - CSS classes to set on active item (and remove when item inactive)

#### Events

- `esl:anchornav:activechanged` - event to dispatch on `ESLAnchornav` when active item changed
- `esl:anchornav:updated` - event to dispatch on `ESLAnchornav` updated state

### ESLAnchornavSticked

To implement the sticky behavior of a component, you can use the `ESLAnchornavSticked` mixin. Register the mixin and add the `esl-anchornav-sticked` attribute to the anchor element container.
4 changes: 2 additions & 2 deletions src/modules/esl-anchornav/core/esl-anchornav-sticked.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export class ESLAnchornavSticked extends ESLMixinElement {
public set sticked(value: boolean) {
if (this._sticked === value) return;
this._sticked = value;
this.$$attr('sticked', value);
this.$$cls(`${ESLAnchornavSticked.is}-active`, value);
this._onStateChange();
}

Expand Down Expand Up @@ -57,7 +57,7 @@ export class ESLAnchornavSticked extends ESLMixinElement {
}

@listen({event: 'resize', target: (that: ESLAnchornavSticked) => ESLResizeObserverTarget.for(that.$host)})
protected _onResize({borderBoxSize}: ESLElementResizeEvent): void {
protected _onResize(e: ESLElementResizeEvent): void {
this._onStateChange();
}
}
25 changes: 25 additions & 0 deletions src/modules/esl-anchornav/core/esl-anchornav.shape.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type {ESLBaseElementShape} from '../../esl-base-element/core/esl-base-element.shape';
import type {ESLAnchornav} from './esl-anchornav';

/**
* Tag declaration interface of {@link ESLAnchornav} element
* Used for TSX declaration
*/
export interface ESLAnchornavTagShape<T extends ESLAnchornav = ESLAnchornav> extends ESLBaseElementShape<T> {
/** Item renderer which is used to build inner markup */
renderer?: string;
/** CSS classes to set on active item */
'active-class'?: string;

/** Allowed children */
children?: any;
}

declare global {
namespace JSX {
export interface IntrinsicElements {
/** {@link ESLAnchornav} custom tag */
'esl-anchornav': ESLAnchornavTagShape;
}
}
}
15 changes: 8 additions & 7 deletions src/modules/esl-anchornav/core/esl-anchornav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export class ESLAnchornav extends ESLBaseElement {
@prop('esl:anchornav:activechanged') public ACTIVECHANGED_EVENT: string;
@prop('esl:anchornav:updated') public UPDATED_EVENT: string;
@prop('[esl-anchor]') protected ANCHOR_SELECTOR: string;
@prop([0, 0.01, 0.99, 1]) protected INTERSECTION_THRESHOLD: number[];

/** Item renderer which is used to build inner markup */
@attr({defaultValue: 'default', name: 'renderer'}) public rendererName: string;
Expand Down Expand Up @@ -99,13 +100,13 @@ export class ESLAnchornav extends ESLBaseElement {
return getViewportForEl(this);
}

/** Data for prepend anchor */
protected get prependData(): ESLAnchorData[] {
/** Permanent anchors to prepend to the list */
protected get anchorsToPrepend(): ESLAnchorData[] {
return [];
}

/** Data for append anchor */
protected get appendData(): ESLAnchorData[] {
/** Permanent anchors to append to the list */
protected get anchorsToAppend(): ESLAnchorData[] {
return [];
}

Expand Down Expand Up @@ -197,16 +198,16 @@ export class ESLAnchornav extends ESLBaseElement {
})
protected _onAnchornavRequest(): void {
this._anchors = [...document.querySelectorAll<HTMLElement>(this.ANCHOR_SELECTOR)].map(this.getDataFrom);
this._anchors.unshift(...this.prependData);
this._anchors.push(...this.appendData);
this._anchors.unshift(...this.anchorsToPrepend);
this._anchors.push(...this.anchorsToAppend);
this.update();
}

@listen({
event: 'intersects',
target: (that: ESLAnchornav) => ESLIntersectionTarget.for(that.$anchors, {
root: that.$viewport,
threshold: [0, 0.01, 0.99, 1],
threshold: that.INTERSECTION_THRESHOLD,
rootMargin: `-${that.offset + 1}px 0px 0px 0px`
})
})
Expand Down

0 comments on commit 0f1bd57

Please sign in to comment.