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

[ESL Anchornav] final update #2626

Merged
merged 4 commits into from
Sep 30, 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
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);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Mb configurable attribute ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

esl-anchornav-sticked="{activeClass: 'anchornav-sticked'}" ?

Copy link
Collaborator

Choose a reason for hiding this comment

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

It looks better, but it's cosmetic for now.

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