@fluid-topics/ftds-toc 0.0.0-stage → 2.2.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,115 @@
1
- # Temporary Holding Version
1
+ ## Install
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ ```shell
4
+ npm install @fluid-topics/ftds-toc
5
+ yarn add @fluid-topics/ftds-toc
6
+ ```
7
+
8
+ ## Usage
9
+
10
+ The design system table of contents (TOC). It is a navigation component with two levels (section and sub-section) that links to anchors of a page.
11
+
12
+ The link matching the current page location is flagged with `aria-current="page"`. It is updated on click and on `hashchange`, or can be forced with the `current` property.
13
+
14
+ It is sticky, and hidden on screens up to 1440px wide.
15
+
16
+ ```typescript
17
+ import { html } from "lit"
18
+ import "@fluid-topics/ftds-toc"
19
+
20
+ function render() {
21
+ return html`
22
+ <ftds-toc
23
+ label="Content"
24
+ .items=${ [
25
+ { value: "general", label: "General", href: "#general" },
26
+ {
27
+ value: "provider",
28
+ label: "Provider",
29
+ href: "#provider",
30
+ children: [
31
+ { value: "settings", label: "Settings", href: "#settings" },
32
+ ],
33
+ },
34
+ ] }
35
+ ></ftds-toc>`
36
+ }
37
+ ```
38
+
39
+ ## Keyboard navigation
40
+
41
+ It follows the [APG treeview navigation pattern](https://www.w3.org/WAI/ARIA/apg/patterns/treeview/examples/treeview-navigation/), without expand and collapse. Only one item is in the tab sequence (the current one, or the first).
42
+
43
+ | Key | Action |
44
+ |---------------------|----------------------------------------------------------|
45
+ | `ArrowDown` / `Up` | Moves the focus to the next / previous item |
46
+ | `ArrowRight` | Moves the focus to the first child of the item |
47
+ | `ArrowLeft` | Moves the focus to the parent of a child item |
48
+ | `Home` / `End` | Moves the focus to the first / last item |
49
+ | A letter | Moves the focus to the next item starting with that letter |
50
+ | `Space` / `Enter` | Activates the link |
51
+
52
+ ## Events
53
+
54
+ `item-selected` (`TocItemSelectedEvent`, `detail` is the selected item) is dispatched when an item is clicked or activated with the keyboard.
55
+ It is cancelable: calling `preventDefault()` on it cancels the native navigation of the link, so the host can scroll to the section itself.
56
+
57
+ ## Building the items from the page
58
+
59
+ Instead of writing the items by hand, `FtTocController` (from `@fluid-topics/ft-wc-utils`) can build them from the sections of a host component and track the section currently in view.
60
+
61
+ Mark each section of the host's light DOM with `data-toc-label`. Add `data-toc-level="1"` to make a sub-section of the previous section.
62
+ An element without an `id` is given one. The sections are rescanned whenever the host's light DOM changes.
63
+
64
+ ```typescript
65
+ import { html } from "lit"
66
+ import { FtLitElement, FtTocController } from "@fluid-topics/ft-wc-utils"
67
+ import { TocItemSelectedEvent } from "@fluid-topics/ftds-toc"
68
+ import "@fluid-topics/ftds-toc"
69
+
70
+ class MyPage extends FtLitElement {
71
+ private readonly toc = new FtTocController(this, {
72
+ scrollContainer: () => this.renderRoot.querySelector("main"),
73
+ })
74
+
75
+ protected render() {
76
+ return html`
77
+ <ftds-toc label="Content"
78
+ .items=${ this.toc.items }
79
+ .current=${ this.toc.current }
80
+ @item-selected=${ this.onItemSelected }>
81
+ </ftds-toc>
82
+ <main>
83
+ <slot></slot>
84
+ </main>`
85
+ }
86
+
87
+ private onItemSelected(e: TocItemSelectedEvent) {
88
+ if (this.toc.scrollTo(e.detail.value)) {
89
+ e.preventDefault()
90
+ }
91
+ }
92
+ }
93
+ ```
94
+
95
+ ```typescript
96
+ html`
97
+ <my-page>
98
+ <section data-toc-label="General">...</section>
99
+ <section data-toc-label="Provider">...</section>
100
+ <section data-toc-label="Settings" data-toc-level="1">...</section>
101
+ </my-page>`
102
+ ```
103
+
104
+ | Member | Description |
105
+ |-----------------------------------|-----------------------------------------------------------------------------------------------------------|
106
+ | `scrollContainer` (option) | The element scrolling the sections. |
107
+ | `isVisible` (option) | Returns whether the TOC is visible. The scroll is not tracked while it returns `false`. Defaults to always visible. |
108
+ | `scrollOffset` (option) | Distance in pixels from the top of the container at which a section is reached. Defaults to `24`. |
109
+ | `scrollSettleDelay` (option) | Milliseconds without scroll event after which a scroll triggered by `scrollTo` is over. Defaults to `150`. |
110
+ | `items` | The items to give to `ftds-toc`. |
111
+ | `current` | The value of the section in view, to give to `ftds-toc`. |
112
+ | `scrollTo(value)` | Scrolls to the section, makes it current and keeps it current until the scroll ends. |
113
+ | `refresh()` | Rescans the sections. |
114
+
115
+ `ftds-page-layout` does this for you with its `withToc` property.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ import { customElements } from "@fluid-topics/ft-wc-utils";
2
+ import definitions from "./definitions";
3
+ customElements(definitions);
@@ -0,0 +1,5 @@
1
+ import { FtdsToc } from "./ftds-toc";
2
+ declare const _default: {
3
+ "ftds-toc": typeof FtdsToc;
4
+ };
5
+ export default _default;
@@ -0,0 +1,4 @@
1
+ import { FtdsToc } from "./ftds-toc";
2
+ export default {
3
+ "ftds-toc": FtdsToc,
4
+ };
@@ -0,0 +1,28 @@
1
+ import { PropertyValues, TemplateResult } from "lit";
2
+ import { ElementDefinitionsMap, FtLitElement } from "@fluid-topics/ft-wc-utils";
3
+ import { FtdsTocItem, FtdsTocProperties } from "./ftds-toc.properties";
4
+ export declare class TocItemSelectedEvent extends CustomEvent<FtdsTocItem> {
5
+ constructor(item: FtdsTocItem);
6
+ }
7
+ export declare class FtdsToc extends FtLitElement implements FtdsTocProperties {
8
+ static elementDefinitions: ElementDefinitionsMap;
9
+ static styles: import("lit").CSSResult;
10
+ items: FtdsTocItem[];
11
+ label: string;
12
+ current?: string;
13
+ private focusedValue?;
14
+ connectedCallback(): void;
15
+ disconnectedCallback(): void;
16
+ protected render(): TemplateResult<1>;
17
+ protected renderRootItem(item: FtdsTocItem): TemplateResult;
18
+ protected renderLink(item: FtdsTocItem, isChild: boolean, childrenActive: boolean, ownedGroupId?: string): TemplateResult;
19
+ protected willUpdate(props: PropertyValues): void;
20
+ private readonly onHashChange;
21
+ private get allItems();
22
+ private get tabbableValue();
23
+ private focusItem;
24
+ private onFocusOut;
25
+ private onKeydown;
26
+ private select;
27
+ private currentFromLocation;
28
+ }
@@ -0,0 +1,199 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { html, nothing, } from "lit";
8
+ import { property, state, } from "lit/decorators.js";
9
+ import { classMap } from "lit/directives/class-map.js";
10
+ import { ifDefined } from "lit/directives/if-defined.js";
11
+ import { repeat } from "lit/directives/repeat.js";
12
+ import { FtLitElement, jsonProperty, } from "@fluid-topics/ft-wc-utils";
13
+ import { FtdsTypography, FtdsTypographyVariants, } from "@fluid-topics/ftds-typography";
14
+ import { styles } from "./ftds-toc.styles";
15
+ export class TocItemSelectedEvent extends CustomEvent {
16
+ constructor(item) {
17
+ super("item-selected", { detail: item, cancelable: true });
18
+ }
19
+ }
20
+ export class FtdsToc extends FtLitElement {
21
+ constructor() {
22
+ super(...arguments);
23
+ this.items = [];
24
+ this.label = "";
25
+ this.onHashChange = () => this.currentFromLocation();
26
+ }
27
+ connectedCallback() {
28
+ super.connectedCallback();
29
+ window.addEventListener("hashchange", this.onHashChange);
30
+ }
31
+ disconnectedCallback() {
32
+ window.removeEventListener("hashchange", this.onHashChange);
33
+ super.disconnectedCallback();
34
+ }
35
+ render() {
36
+ return html `
37
+ <nav aria-label="${ifDefined(this.label || undefined)}" @focusout=${this.onFocusOut}>
38
+ ${this.label ? html `
39
+ <ftds-typography class="title" variant="${FtdsTypographyVariants.caption1semibold}">
40
+ ${this.label}
41
+ </ftds-typography>` : nothing}
42
+ <ul role="tree" aria-label="${ifDefined(this.label || undefined)}" @keydown=${this.onKeydown}>
43
+ ${repeat(this.items, (item) => item.value, (item) => this.renderRootItem(item))}
44
+ </ul>
45
+ </nav>
46
+ `;
47
+ }
48
+ renderRootItem(item) {
49
+ var _a;
50
+ const children = (_a = item.children) !== null && _a !== void 0 ? _a : [];
51
+ const groupId = `group-${this.items.indexOf(item)}`;
52
+ const childrenActive = children.some((child) => child.value === this.current);
53
+ return html `
54
+ <li role="none">
55
+ ${this.renderLink(item, false, childrenActive, children.length > 0 ? groupId : undefined)}
56
+ ${children.length > 0 ? html `
57
+ <ul role="group" id="${groupId}">
58
+ ${repeat(children, (child) => child.value, (child) => html `
59
+ <li role="none">${this.renderLink(child, true, false)}</li>`)}
60
+ </ul>` : nothing}
61
+ </li>
62
+ `;
63
+ }
64
+ renderLink(item, isChild, childrenActive, ownedGroupId) {
65
+ const isCurrent = item.value === this.current;
66
+ const emphasized = isCurrent || childrenActive;
67
+ return html `
68
+ <a
69
+ part="${isChild ? "child-item" : "root-item"}"
70
+ class="${classMap({ child: isChild, "children-active": childrenActive })}"
71
+ role="treeitem"
72
+ href="${item.href}"
73
+ data-value="${item.value}"
74
+ tabindex="${item.value === this.tabbableValue ? 0 : -1}"
75
+ aria-owns="${ifDefined(ownedGroupId)}"
76
+ aria-current="${ifDefined(isCurrent ? "page" : undefined)}"
77
+ @click=${(e) => this.select(item, e)}
78
+ >
79
+ ${isChild ? html `<span class="bullet" aria-hidden="true">•</span>` : nothing}
80
+ <ftds-typography
81
+ class="label"
82
+ variant="${emphasized ? FtdsTypographyVariants.body1semibold : FtdsTypographyVariants.body1medium}"
83
+ >
84
+ ${item.label}
85
+ </ftds-typography>
86
+ </a>
87
+ `;
88
+ }
89
+ willUpdate(props) {
90
+ super.willUpdate(props);
91
+ if (props.has("items") && this.current === undefined) {
92
+ this.currentFromLocation();
93
+ }
94
+ }
95
+ get allItems() {
96
+ return this.items.flatMap((item) => { var _a; return [item, ...((_a = item.children) !== null && _a !== void 0 ? _a : [])]; });
97
+ }
98
+ get tabbableValue() {
99
+ var _a;
100
+ const values = this.allItems.map((item) => item.value);
101
+ return (_a = [this.focusedValue, this.current].find((v) => v !== undefined && values.includes(v))) !== null && _a !== void 0 ? _a : values[0];
102
+ }
103
+ focusItem(item) {
104
+ var _a, _b;
105
+ if (item) {
106
+ this.focusedValue = item.value;
107
+ (_b = (_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector(`a[data-value="${item.value}"]`)) === null || _b === void 0 ? void 0 : _b.focus();
108
+ }
109
+ }
110
+ onFocusOut(e) {
111
+ if (!this.shadowRoot.contains(e.relatedTarget)) {
112
+ this.focusedValue = undefined;
113
+ }
114
+ }
115
+ onKeydown(e) {
116
+ var _a;
117
+ if (e.ctrlKey || e.altKey || e.metaKey) {
118
+ return;
119
+ }
120
+ const target = e.target.closest("a[data-value]");
121
+ const items = this.allItems;
122
+ const index = items.findIndex((item) => item.value === (target === null || target === void 0 ? void 0 : target.dataset.value));
123
+ if (index < 0) {
124
+ return;
125
+ }
126
+ const item = items[index];
127
+ const parent = this.items.find((root) => { var _a; return (_a = root.children) === null || _a === void 0 ? void 0 : _a.includes(item); });
128
+ let handled = true;
129
+ switch (e.key) {
130
+ case " ":
131
+ target.click();
132
+ break;
133
+ case "ArrowDown":
134
+ this.focusItem(items[index + 1]);
135
+ break;
136
+ case "ArrowUp":
137
+ this.focusItem(items[index - 1]);
138
+ break;
139
+ case "Home":
140
+ this.focusItem(items[0]);
141
+ break;
142
+ case "End":
143
+ this.focusItem(items[items.length - 1]);
144
+ break;
145
+ case "ArrowRight":
146
+ this.focusItem((_a = item.children) === null || _a === void 0 ? void 0 : _a[0]);
147
+ break;
148
+ case "ArrowLeft":
149
+ this.focusItem(parent);
150
+ break;
151
+ default:
152
+ if (e.key.length === 1 && /\p{L}/u.test(e.key)) {
153
+ const char = e.key.toLowerCase();
154
+ const ordered = [...items.slice(index + 1), ...items.slice(0, index)];
155
+ this.focusItem(ordered.find((i) => i.label.trim().toLowerCase().startsWith(char)));
156
+ }
157
+ else {
158
+ handled = false;
159
+ }
160
+ }
161
+ if (handled) {
162
+ e.preventDefault();
163
+ e.stopPropagation();
164
+ }
165
+ }
166
+ select(item, click) {
167
+ this.current = item.value;
168
+ if (!this.dispatchEvent(new TocItemSelectedEvent(item))) {
169
+ click.preventDefault();
170
+ }
171
+ }
172
+ currentFromLocation() {
173
+ const hash = window.location.hash;
174
+ if (!hash) {
175
+ return;
176
+ }
177
+ const all = this.items.flatMap((item) => { var _a; return [item, ...((_a = item.children) !== null && _a !== void 0 ? _a : [])]; });
178
+ const match = all.find((item) => item.href === hash || item.href.endsWith(hash));
179
+ if (match) {
180
+ this.current = match.value;
181
+ }
182
+ }
183
+ }
184
+ FtdsToc.elementDefinitions = {
185
+ "ftds-typography": FtdsTypography,
186
+ };
187
+ FtdsToc.styles = styles;
188
+ __decorate([
189
+ jsonProperty([])
190
+ ], FtdsToc.prototype, "items", void 0);
191
+ __decorate([
192
+ property()
193
+ ], FtdsToc.prototype, "label", void 0);
194
+ __decorate([
195
+ property({ reflect: true })
196
+ ], FtdsToc.prototype, "current", void 0);
197
+ __decorate([
198
+ state()
199
+ ], FtdsToc.prototype, "focusedValue", void 0);