@c2n/side-nav 0.0.6
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/LICENSE +21 -0
- package/README.md +19 -0
- package/dist/side-nav.js +223 -0
- package/package.json +60 -0
- package/types/src/side-nav.d.ts +126 -0
- package/types/src/side-nav.d.ts.map +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Nguyen Thai Vinh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# @c2n/side-nav
|
|
2
|
+
|
|
3
|
+
`c2-side-nav` is an app-shell layout with a navigation drawer beside the page content. It pushes the content on large screens (`side` mode) and slides over it with a backdrop on small ones (`over` mode), switching automatically at 600px and 960px.
|
|
4
|
+
|
|
5
|
+
```html
|
|
6
|
+
<c2-side-nav opened style="height: 100dvh">
|
|
7
|
+
<nav slot="side-nav-content">…</nav>
|
|
8
|
+
<main>
|
|
9
|
+
<button side-nav-toggle>Menu</button>
|
|
10
|
+
…
|
|
11
|
+
</main>
|
|
12
|
+
</c2-side-nav>
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
- Attributes: `opened`, `desktop-mode` / `tablet-mode` (`side` | `over`), `responsive-tablet`, `position` (`start` | `end`).
|
|
16
|
+
- Any element with a `side-nav-toggle` attribute anywhere inside (page header or drawer) toggles the drawer; `toggle()` does the same from code. A non-zero `--c2-side-nav__close--width` gives an icon rail; hide labels with `c2-side-nav:not([opened]) .label { display: none }`.
|
|
17
|
+
- Event: `opened-change` (`detail.opened`) for user and breakpoint driven changes.
|
|
18
|
+
- Over mode: backdrop, page scroll lock, Escape to close, dialog semantics and focus management. Set `--c2-side-nav__over--position: absolute` to keep it inside a positioned parent.
|
|
19
|
+
- Theming: `--c2-side-nav--*` (widths, background, padding, borders, radius, transition), `__divider--*`, `__over--*`, `__backdrop--*`, `__scrollbar--color`.
|
package/dist/side-nav.js
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { LitElement, html, nothing, unsafeCSS } from "lit";
|
|
2
|
+
import { customElement, property, query, state } from "lit/decorators.js";
|
|
3
|
+
import { isServer } from "lit-html/is-server.js";
|
|
4
|
+
import { classMap } from "lit/directives/class-map.js";
|
|
5
|
+
import { BreakpointsObserver } from "@c2n/core/controllers/breakpoints-observer.js";
|
|
6
|
+
import { Breakpoints, smartFixedPosition } from "@c2n/core/dom-helper.js";
|
|
7
|
+
//#region src/side-nav.scss?inline
|
|
8
|
+
var side_nav_default = "/* ex : var((width: 24px), width, c2-checkbox) returns var(--c2-checkbox-width, 24px) */\n:host {\n display: flex;\n align-items: stretch;\n min-width: 0;\n}\n\n:host([hidden]) {\n display: none;\n}\n\n.c2-side-nav {\n display: flex;\n flex: 1 1 auto;\n min-width: 0;\n align-items: stretch;\n}\n.c2-side-nav.end {\n flex-direction: row-reverse;\n}\n\n.drawer {\n position: relative;\n flex: none;\n box-sizing: border-box;\n overflow: hidden;\n width: var(--c2-side-nav__close--width,0px);\n z-index: 1;\n transition: width var(--c2-side-nav--transition-duration,250ms) cubic-bezier(0, 0, 0, 1), transform var(--c2-side-nav--transition-duration,250ms) cubic-bezier(0, 0, 0, 1), visibility 0s linear var(--c2-side-nav--transition-duration,250ms);\n}\n\n:host([opened]) .drawer {\n width: var(--c2-side-nav__open--width,240px);\n}\n\n.c2-side-nav:not(.ready) .drawer {\n transition: none;\n}\n\n.panel {\n box-sizing: border-box;\n width: var(--c2-side-nav__open--width,240px);\n height: 100%;\n overflow-y: auto;\n overflow-x: hidden;\n overscroll-behavior: contain;\n scrollbar-width: thin;\n scrollbar-color: var(--c2-side-nav__scrollbar--color,rgba(0, 0, 0, 0.2)) transparent;\n background-color: var(--c2-side-nav--background-color,#fafafa);\n color: var(--c2-side-nav--color);\n border-top-left-radius: var(--c2-side-nav--border-top-left-radius);\n border-top-right-radius: var(--c2-side-nav--border-top-right-radius);\n border-bottom-left-radius: var(--c2-side-nav--border-bottom-left-radius);\n border-bottom-right-radius: var(--c2-side-nav--border-bottom-right-radius);\n padding-top: var(--c2-side-nav--padding-top,16px);\n padding-right: var(--c2-side-nav--padding-right,16px);\n padding-bottom: var(--c2-side-nav--padding-bottom,16px);\n padding-left: var(--c2-side-nav--padding-left,16px);\n border-top: var(--c2-side-nav--border-top);\n border-bottom: var(--c2-side-nav--border-bottom);\n border-right: var(--c2-side-nav--border-right);\n border-left: var(--c2-side-nav--border-left);\n border-inline-end: var(--c2-side-nav__divider--width,1px) solid var(--c2-side-nav__divider--color,#e4e4e7);\n transition: background-color var(--c2-side-nav--transition-duration,250ms) cubic-bezier(0.2, 0, 0, 1);\n}\n\n.c2-side-nav.end .panel {\n border-inline-end: none;\n border-inline-start: var(--c2-side-nav__divider--width,1px) solid var(--c2-side-nav__divider--color,#e4e4e7);\n}\n\n.content {\n flex: 1 1 auto;\n min-width: 0;\n}\n\n.c2-side-nav.over .drawer {\n position: var(--c2-side-nav__over--position,fixed);\n top: var(--c2-side-nav__over--top,0px);\n left: 0;\n height: var(--c2-side-nav__over--height,100dvh);\n width: var(--c2-side-nav__over--width,var(--c2-side-nav__open--width,240px));\n z-index: calc(var(--c2-side-nav__backdrop--z-index,1000) + 1);\n transform: translateX(-100%);\n visibility: hidden;\n overflow: visible;\n}\n.c2-side-nav.over.end .drawer {\n left: auto;\n right: 0;\n transform: translateX(100%);\n}\n.c2-side-nav.over .panel {\n width: 100%;\n border-inline-end: none;\n border-inline-start: none;\n background-color: var(--c2-side-nav__over--background-color,var(--c2-side-nav--background-color,#fafafa));\n border-top-left-radius: var(--c2-side-nav__over--border-top-left-radius,var(--c2-side-nav--border-top-left-radius));\n border-top-right-radius: var(--c2-side-nav__over--border-top-right-radius,var(--c2-side-nav--border-top-right-radius));\n border-bottom-left-radius: var(--c2-side-nav__over--border-bottom-left-radius,var(--c2-side-nav--border-bottom-left-radius));\n border-bottom-right-radius: var(--c2-side-nav__over--border-bottom-right-radius,var(--c2-side-nav--border-bottom-right-radius));\n padding-top: var(--c2-side-nav__over--padding-top,var(--c2-side-nav--padding-top,16px));\n padding-right: var(--c2-side-nav__over--padding-right,var(--c2-side-nav--padding-right,16px));\n padding-bottom: var(--c2-side-nav__over--padding-bottom,var(--c2-side-nav--padding-bottom,16px));\n padding-left: var(--c2-side-nav__over--padding-left,var(--c2-side-nav--padding-left,16px));\n box-shadow: var(--c2-side-nav__over--box-shadow,0 12px 40px rgba(0, 0, 0, 0.18));\n}\n\n:host([opened]) .c2-side-nav.over .drawer {\n transform: none;\n visibility: visible;\n transition: transform var(--c2-side-nav--transition-duration,250ms) cubic-bezier(0, 0, 0, 1), visibility 0s;\n}\n\n.backdrop {\n position: var(--c2-side-nav__over--position,fixed);\n inset: 0;\n z-index: var(--c2-side-nav__backdrop--z-index,1000);\n background-color: var(--c2-side-nav__backdrop--background-color,rgba(0, 0, 0, 0.32));\n backdrop-filter: var(--c2-side-nav__backdrop--backdrop-filter,none);\n animation: c2-side-nav-fade-in var(--c2-side-nav--transition-duration,250ms) cubic-bezier(0.2, 0, 0, 1);\n}\n\n@keyframes c2-side-nav-fade-in {\n from {\n opacity: 0;\n }\n}";
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region \0@oxc-project+runtime@0.148.0/helpers/esm/decorate.js
|
|
11
|
+
function __decorate(decorators, target, key, desc) {
|
|
12
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
13
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
14
|
+
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;
|
|
15
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/side-nav.ts
|
|
19
|
+
var BREAKPOINT_QUERIES = [
|
|
20
|
+
Breakpoints.Phone,
|
|
21
|
+
Breakpoints.Tablet,
|
|
22
|
+
Breakpoints.Desktop
|
|
23
|
+
];
|
|
24
|
+
var FOCUSABLE = "a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])";
|
|
25
|
+
var SideNav = class SideNav extends LitElement {
|
|
26
|
+
constructor(..._args) {
|
|
27
|
+
super(..._args);
|
|
28
|
+
this.opened = false;
|
|
29
|
+
this.desktopMode = "side";
|
|
30
|
+
this.tabletMode = "over";
|
|
31
|
+
this.responsiveTablet = false;
|
|
32
|
+
this.position = "start";
|
|
33
|
+
this.breakpoint = "desktop";
|
|
34
|
+
this.ready = false;
|
|
35
|
+
this.preferredOpened = false;
|
|
36
|
+
this.previouslyFocused = null;
|
|
37
|
+
this.scrollLocked = false;
|
|
38
|
+
this.breakpointsController = new BreakpointsObserver(this, BREAKPOINT_QUERIES, (isInitial) => this.handleBreakpoint(isInitial));
|
|
39
|
+
this.handleBackdropClick = () => {
|
|
40
|
+
this.setOpened(false, true);
|
|
41
|
+
};
|
|
42
|
+
this.handleKeydown = (event) => {
|
|
43
|
+
if (event.key === "Escape" && this.opened && this.mode === "over") {
|
|
44
|
+
event.preventDefault();
|
|
45
|
+
this.setOpened(false, true);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
static {
|
|
50
|
+
this.styles = unsafeCSS(side_nav_default);
|
|
51
|
+
}
|
|
52
|
+
/** The mode in effect for the current viewport: `over` on phones, otherwise `tablet-mode` / `desktop-mode`. */
|
|
53
|
+
get mode() {
|
|
54
|
+
if (this.breakpoint === "phone") return "over";
|
|
55
|
+
if (this.breakpoint === "tablet") return this.tabletMode;
|
|
56
|
+
return this.desktopMode;
|
|
57
|
+
}
|
|
58
|
+
/** Opens or closes the drawer as a user action would (fires `opened-change`). */
|
|
59
|
+
toggle(opened = !this.opened) {
|
|
60
|
+
this.setOpened(opened, true);
|
|
61
|
+
}
|
|
62
|
+
connectedCallback() {
|
|
63
|
+
super.connectedCallback();
|
|
64
|
+
this.preferredOpened = this.opened;
|
|
65
|
+
}
|
|
66
|
+
disconnectedCallback() {
|
|
67
|
+
super.disconnectedCallback();
|
|
68
|
+
window.removeEventListener("keydown", this.handleKeydown);
|
|
69
|
+
this.lockScroll(false);
|
|
70
|
+
}
|
|
71
|
+
collapsesAt(breakpoint) {
|
|
72
|
+
if (breakpoint === "phone") return true;
|
|
73
|
+
if (breakpoint === "tablet") return this.tabletMode === "over" || this.responsiveTablet;
|
|
74
|
+
return this.desktopMode === "over";
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* On load the `opened` attribute is meant for the layout the author designed for: it is ignored on phones and for the
|
|
78
|
+
* implicit tablet overlay, but honoured when an overlay mode was chosen explicitly (`desktop-mode="over"`).
|
|
79
|
+
*/
|
|
80
|
+
closesOnLoadAt(breakpoint) {
|
|
81
|
+
if (breakpoint === "phone") return true;
|
|
82
|
+
if (breakpoint === "tablet") return this.responsiveTablet || this.tabletMode === "over" && !this.hasAttribute("tablet-mode");
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
handleBreakpoint(isInitial) {
|
|
86
|
+
const query = this.breakpointsController.currentBreakpoint;
|
|
87
|
+
this.breakpoint = query === Breakpoints.Phone ? "phone" : query === Breakpoints.Tablet ? "tablet" : "desktop";
|
|
88
|
+
if (isInitial) {
|
|
89
|
+
this.preferredOpened = this.opened;
|
|
90
|
+
if (this.closesOnLoadAt(this.breakpoint)) this.opened = false;
|
|
91
|
+
this.updateComplete.then(() => requestAnimationFrame(() => this.ready = true));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (this.collapsesAt(this.breakpoint)) {
|
|
95
|
+
if (this.opened) {
|
|
96
|
+
this.preferredOpened = true;
|
|
97
|
+
this.setOpened(false, true);
|
|
98
|
+
}
|
|
99
|
+
} else if (this.opened !== this.preferredOpened) this.setOpened(this.preferredOpened, true);
|
|
100
|
+
}
|
|
101
|
+
setOpened(opened, emit) {
|
|
102
|
+
if (this.opened === opened) return;
|
|
103
|
+
this.opened = opened;
|
|
104
|
+
if (this.mode === "side") this.preferredOpened = opened;
|
|
105
|
+
if (emit) this.dispatchEvent(new CustomEvent("opened-change", {
|
|
106
|
+
bubbles: true,
|
|
107
|
+
composed: true,
|
|
108
|
+
detail: { opened }
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
/** Click delegation for `[side-nav-toggle]` elements anywhere in the light DOM (drawer or content). */
|
|
112
|
+
handleToggleClick(event) {
|
|
113
|
+
const path = event.composedPath();
|
|
114
|
+
const triggerIndex = path.findIndex((node) => node instanceof Element && node.hasAttribute("side-nav-toggle"));
|
|
115
|
+
if (triggerIndex === -1 || path[triggerIndex] === this) return;
|
|
116
|
+
if (path.slice(triggerIndex + 1).find((node) => node instanceof Element && node.localName === this.localName) !== this) return;
|
|
117
|
+
this.toggle();
|
|
118
|
+
}
|
|
119
|
+
lockScroll(lock) {
|
|
120
|
+
if (lock === this.scrollLocked) return;
|
|
121
|
+
if (lock && (isServer || getComputedStyle(this.drawer).position !== "fixed")) return;
|
|
122
|
+
this.scrollLocked = lock;
|
|
123
|
+
smartFixedPosition(lock);
|
|
124
|
+
}
|
|
125
|
+
syncOverlayState() {
|
|
126
|
+
const overlayOpen = this.opened && this.mode === "over";
|
|
127
|
+
window.removeEventListener("keydown", this.handleKeydown);
|
|
128
|
+
if (overlayOpen) {
|
|
129
|
+
window.addEventListener("keydown", this.handleKeydown);
|
|
130
|
+
this.lockScroll(true);
|
|
131
|
+
this.previouslyFocused = document.activeElement;
|
|
132
|
+
this.focusDrawer();
|
|
133
|
+
} else {
|
|
134
|
+
this.lockScroll(false);
|
|
135
|
+
const focused = document.activeElement;
|
|
136
|
+
if (focused && this.previouslyFocused instanceof HTMLElement && (focused === this || this.contains(focused))) this.previouslyFocused.focus();
|
|
137
|
+
this.previouslyFocused = null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
focusDrawer() {
|
|
141
|
+
const slot = this.renderRoot.querySelector("slot[name=\"side-nav-content\"]");
|
|
142
|
+
for (const element of slot?.assignedElements({ flatten: true }) ?? []) {
|
|
143
|
+
const target = element.matches(FOCUSABLE) ? element : element.querySelector(FOCUSABLE);
|
|
144
|
+
if (target instanceof HTMLElement) {
|
|
145
|
+
target.focus();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
this.drawer.focus();
|
|
150
|
+
}
|
|
151
|
+
/** In side mode a collapsed drawer with a non-zero closed width is a rail and must stay interactive. */
|
|
152
|
+
get drawerInert() {
|
|
153
|
+
if (this.opened) return false;
|
|
154
|
+
if (this.mode === "over") return true;
|
|
155
|
+
if (isServer) return true;
|
|
156
|
+
return (parseFloat(getComputedStyle(this).getPropertyValue("--c2-side-nav__close--width")) || 0) === 0;
|
|
157
|
+
}
|
|
158
|
+
updated(changed) {
|
|
159
|
+
this.drawer.toggleAttribute("inert", this.drawerInert);
|
|
160
|
+
this.contentElement.toggleAttribute("inert", this.opened && this.mode === "over");
|
|
161
|
+
if (changed.has("opened") || changed.has("breakpoint") || changed.has("desktopMode") || changed.has("tabletMode")) {
|
|
162
|
+
if ((changed.has("opened") ? changed.get("opened") && this.mode === "over" : void 0) !== (this.opened && this.mode === "over") || changed.has("breakpoint")) this.syncOverlayState();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
render() {
|
|
166
|
+
const over = this.mode === "over";
|
|
167
|
+
const overlayOpen = over && this.opened;
|
|
168
|
+
return html`
|
|
169
|
+
<div class="c2-side-nav ${classMap({
|
|
170
|
+
over,
|
|
171
|
+
ready: this.ready,
|
|
172
|
+
end: this.position === "end"
|
|
173
|
+
})}" @click=${this.handleToggleClick}>
|
|
174
|
+
${overlayOpen ? html`<div class="backdrop" part="backdrop" @click=${this.handleBackdropClick}></div>` : nothing}
|
|
175
|
+
<div
|
|
176
|
+
class="drawer"
|
|
177
|
+
part="drawer"
|
|
178
|
+
role=${over ? "dialog" : nothing}
|
|
179
|
+
aria-modal=${over ? "true" : nothing}
|
|
180
|
+
aria-hidden=${!this.opened && over ? "true" : nothing}
|
|
181
|
+
tabindex="-1"
|
|
182
|
+
>
|
|
183
|
+
<div class="panel" part="panel">
|
|
184
|
+
<slot name="side-nav-content"></slot>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
<div class="content" part="content">
|
|
188
|
+
<slot></slot>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
`;
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
__decorate([property({
|
|
195
|
+
type: Boolean,
|
|
196
|
+
reflect: true
|
|
197
|
+
})], SideNav.prototype, "opened", void 0);
|
|
198
|
+
__decorate([property({
|
|
199
|
+
type: String,
|
|
200
|
+
reflect: true,
|
|
201
|
+
attribute: "desktop-mode"
|
|
202
|
+
})], SideNav.prototype, "desktopMode", void 0);
|
|
203
|
+
__decorate([property({
|
|
204
|
+
type: String,
|
|
205
|
+
reflect: true,
|
|
206
|
+
attribute: "tablet-mode"
|
|
207
|
+
})], SideNav.prototype, "tabletMode", void 0);
|
|
208
|
+
__decorate([property({
|
|
209
|
+
type: Boolean,
|
|
210
|
+
reflect: true,
|
|
211
|
+
attribute: "responsive-tablet"
|
|
212
|
+
})], SideNav.prototype, "responsiveTablet", void 0);
|
|
213
|
+
__decorate([property({
|
|
214
|
+
type: String,
|
|
215
|
+
reflect: true
|
|
216
|
+
})], SideNav.prototype, "position", void 0);
|
|
217
|
+
__decorate([state()], SideNav.prototype, "breakpoint", void 0);
|
|
218
|
+
__decorate([state()], SideNav.prototype, "ready", void 0);
|
|
219
|
+
__decorate([query(".drawer")], SideNav.prototype, "drawer", void 0);
|
|
220
|
+
__decorate([query(".content")], SideNav.prototype, "contentElement", void 0);
|
|
221
|
+
SideNav = __decorate([customElement("c2-side-nav")], SideNav);
|
|
222
|
+
//#endregion
|
|
223
|
+
export { SideNav };
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@c2n/side-nav",
|
|
3
|
+
"version": "0.0.6",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/side-nav.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"default": "./dist/side-nav.js",
|
|
9
|
+
"types": "./types/src/side-nav.d.ts"
|
|
10
|
+
},
|
|
11
|
+
"./custom-elements.json": "./custom-elements.json"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"types"
|
|
16
|
+
],
|
|
17
|
+
"keywords": [
|
|
18
|
+
"side-nav",
|
|
19
|
+
"web component",
|
|
20
|
+
"lit"
|
|
21
|
+
],
|
|
22
|
+
"author": "code2nguyen@gmail.com",
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"registry": "https://registry.npmjs.org",
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/code2nguyen/web-components.git"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"dev": "vite",
|
|
33
|
+
"build": "wireit",
|
|
34
|
+
"build:only": "vite build",
|
|
35
|
+
"type-check": "wireit"
|
|
36
|
+
},
|
|
37
|
+
"wireit": {
|
|
38
|
+
"type-check": {
|
|
39
|
+
"dependencies": [
|
|
40
|
+
"../../core:build"
|
|
41
|
+
],
|
|
42
|
+
"command": "tsc -p tsconfig.lib.json --composite false"
|
|
43
|
+
},
|
|
44
|
+
"build": {
|
|
45
|
+
"dependencies": [
|
|
46
|
+
"type-check"
|
|
47
|
+
],
|
|
48
|
+
"command": "vite build"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@c2n/core": "0.0.6",
|
|
53
|
+
"lit": "3.3.3"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@c2n/config": "*"
|
|
57
|
+
},
|
|
58
|
+
"customElements": "custom-elements.json",
|
|
59
|
+
"gitHead": "2921054bc75299da66dad11c1e374246ff88a51e"
|
|
60
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { LitElement, type PropertyValues } from 'lit';
|
|
2
|
+
export type DisplayMode = 'over' | 'side';
|
|
3
|
+
export type SideNavPosition = 'start' | 'end';
|
|
4
|
+
export type SideNavBreakpoint = 'phone' | 'tablet' | 'desktop';
|
|
5
|
+
export interface OpenedChangeEventDetail {
|
|
6
|
+
opened: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* A layout shell with a navigation drawer beside the page content. The drawer goes in the `side-nav-content` slot, the
|
|
10
|
+
* page in the default slot, and the host stretches both to its own height. In `side` mode the drawer collapses in the
|
|
11
|
+
* flow and pushes the content; in `over` mode it slides over the content with a backdrop, locks page scrolling, traps
|
|
12
|
+
* focus as a dialog and closes on Escape or a backdrop click. Phones always use `over`; `tablet-mode` and
|
|
13
|
+
* `desktop-mode` pick the mode for the two larger breakpoints (600px and 960px), and the drawer remembers whether it
|
|
14
|
+
* was open when the viewport crosses them.
|
|
15
|
+
*
|
|
16
|
+
* Any element inside the side nav (drawer or content) carrying a `side-nav-toggle` attribute toggles the nearest enclosing side nav when
|
|
17
|
+
* clicked, so a menu button in the page or a collapse button in the drawer header needs no script. With a non-zero
|
|
18
|
+
* `--c2-side-nav__close--width` the collapsed drawer becomes an icon rail; the reflected `opened` attribute lets your
|
|
19
|
+
* CSS hide the labels in that state (`c2-side-nav:not([opened]) .label { display: none }`).
|
|
20
|
+
*
|
|
21
|
+
* @tag c2-side-nav
|
|
22
|
+
*
|
|
23
|
+
* @slot side-nav-content - The drawer content: a `c2-list` of links, a logo, anything.
|
|
24
|
+
* @slot default - The page content beside the drawer.
|
|
25
|
+
*
|
|
26
|
+
* @event {CustomEvent<OpenedChangeEventDetail>} opened-change - Fired when the user or a breakpoint change opens or closes the drawer (not when `opened` is set from code). `detail.opened` is the new state.
|
|
27
|
+
*
|
|
28
|
+
* @cssproperty {pixel} [--c2-side-nav__open--width=240px]
|
|
29
|
+
* @cssproperty {pixel} [--c2-side-nav__close--width=0px] - Width of the collapsed drawer; a non-zero value gives a rail.
|
|
30
|
+
* @cssproperty {time} [--c2-side-nav--transition-duration=250ms]
|
|
31
|
+
*
|
|
32
|
+
* @cssproperty {color} [--c2-side-nav--background-color=#fafafa]
|
|
33
|
+
* @cssproperty {color} --c2-side-nav--color
|
|
34
|
+
*
|
|
35
|
+
* @cssproperty {border-radius} --c2-side-nav--border-top-left-radius
|
|
36
|
+
* @cssproperty {border-radius} --c2-side-nav--border-top-right-radius
|
|
37
|
+
* @cssproperty {border-radius} --c2-side-nav--border-bottom-left-radius
|
|
38
|
+
* @cssproperty {border-radius} --c2-side-nav--border-bottom-right-radius
|
|
39
|
+
*
|
|
40
|
+
* @cssproperty {padding} [--c2-side-nav--padding-top=16px]
|
|
41
|
+
* @cssproperty {padding} [--c2-side-nav--padding-right=16px]
|
|
42
|
+
* @cssproperty {padding} [--c2-side-nav--padding-bottom=16px]
|
|
43
|
+
* @cssproperty {padding} [--c2-side-nav--padding-left=16px]
|
|
44
|
+
*
|
|
45
|
+
* @cssproperty {border} --c2-side-nav--border-top
|
|
46
|
+
* @cssproperty {border} --c2-side-nav--border-right
|
|
47
|
+
* @cssproperty {border} --c2-side-nav--border-bottom
|
|
48
|
+
* @cssproperty {border} --c2-side-nav--border-left
|
|
49
|
+
*
|
|
50
|
+
* @cssproperty {pixel} [--c2-side-nav__divider--width=1px] - Line between the drawer and the content, on whichever side the drawer is.
|
|
51
|
+
* @cssproperty {color} [--c2-side-nav__divider--color=#e4e4e7]
|
|
52
|
+
*
|
|
53
|
+
* @cssproperty {color} [--c2-side-nav__scrollbar--color=rgba(0, 0, 0, 0.2)]
|
|
54
|
+
*
|
|
55
|
+
* @cssproperty {position} [--c2-side-nav__over--position=fixed] - `absolute` keeps the overlay inside a positioned parent.
|
|
56
|
+
* @cssproperty {pixel} [--c2-side-nav__over--top=0px]
|
|
57
|
+
* @cssproperty {pixel} [--c2-side-nav__over--height=100dvh]
|
|
58
|
+
* @cssproperty {pixel} --c2-side-nav__over--width - Defaults to the open width.
|
|
59
|
+
* @cssproperty {color} --c2-side-nav__over--background-color - Defaults to the drawer background.
|
|
60
|
+
* @cssproperty {box-shadow} [--c2-side-nav__over--box-shadow=0 12px 40px rgba(0, 0, 0, 0.18)]
|
|
61
|
+
* @cssproperty {border-radius} --c2-side-nav__over--border-top-left-radius
|
|
62
|
+
* @cssproperty {border-radius} --c2-side-nav__over--border-top-right-radius
|
|
63
|
+
* @cssproperty {border-radius} --c2-side-nav__over--border-bottom-left-radius
|
|
64
|
+
* @cssproperty {border-radius} --c2-side-nav__over--border-bottom-right-radius
|
|
65
|
+
* @cssproperty {padding} --c2-side-nav__over--padding-top
|
|
66
|
+
* @cssproperty {padding} --c2-side-nav__over--padding-right
|
|
67
|
+
* @cssproperty {padding} --c2-side-nav__over--padding-bottom
|
|
68
|
+
* @cssproperty {padding} --c2-side-nav__over--padding-left
|
|
69
|
+
*
|
|
70
|
+
* @cssproperty {color} [--c2-side-nav__backdrop--background-color=rgba(0, 0, 0, 0.32)]
|
|
71
|
+
* @cssproperty {filter} [--c2-side-nav__backdrop--backdrop-filter=none]
|
|
72
|
+
* @cssproperty {number} [--c2-side-nav__backdrop--z-index=1000]
|
|
73
|
+
*/
|
|
74
|
+
export declare class SideNav extends LitElement {
|
|
75
|
+
static styles: import("lit").CSSResult;
|
|
76
|
+
/** Whether the drawer is open. On phones (and on tablets unless `tablet-mode` is set explicitly) the initial value is ignored and the drawer starts closed. */
|
|
77
|
+
opened: boolean;
|
|
78
|
+
/** How the drawer behaves at 960px and up: `side` pushes the content, `over` slides above it. */
|
|
79
|
+
desktopMode: DisplayMode;
|
|
80
|
+
/** How the drawer behaves between 600px and 959px. */
|
|
81
|
+
tabletMode: DisplayMode;
|
|
82
|
+
/** Also collapse the drawer on tablets in `side` mode when the viewport shrinks to that size. */
|
|
83
|
+
responsiveTablet: boolean;
|
|
84
|
+
/** Which edge the drawer is on: `start` (left in LTR) or `end`. */
|
|
85
|
+
position: SideNavPosition;
|
|
86
|
+
private breakpoint;
|
|
87
|
+
private ready;
|
|
88
|
+
private drawer;
|
|
89
|
+
private contentElement;
|
|
90
|
+
/** The state the user wants in `side` mode, restored when the viewport grows back. */
|
|
91
|
+
private preferredOpened;
|
|
92
|
+
private previouslyFocused;
|
|
93
|
+
private scrollLocked;
|
|
94
|
+
private breakpointsController;
|
|
95
|
+
/** The mode in effect for the current viewport: `over` on phones, otherwise `tablet-mode` / `desktop-mode`. */
|
|
96
|
+
get mode(): DisplayMode;
|
|
97
|
+
/** Opens or closes the drawer as a user action would (fires `opened-change`). */
|
|
98
|
+
toggle(opened?: boolean): void;
|
|
99
|
+
connectedCallback(): void;
|
|
100
|
+
disconnectedCallback(): void;
|
|
101
|
+
private collapsesAt;
|
|
102
|
+
/**
|
|
103
|
+
* On load the `opened` attribute is meant for the layout the author designed for: it is ignored on phones and for the
|
|
104
|
+
* implicit tablet overlay, but honoured when an overlay mode was chosen explicitly (`desktop-mode="over"`).
|
|
105
|
+
*/
|
|
106
|
+
private closesOnLoadAt;
|
|
107
|
+
private handleBreakpoint;
|
|
108
|
+
private setOpened;
|
|
109
|
+
private handleBackdropClick;
|
|
110
|
+
private handleKeydown;
|
|
111
|
+
/** Click delegation for `[side-nav-toggle]` elements anywhere in the light DOM (drawer or content). */
|
|
112
|
+
private handleToggleClick;
|
|
113
|
+
private lockScroll;
|
|
114
|
+
private syncOverlayState;
|
|
115
|
+
private focusDrawer;
|
|
116
|
+
/** In side mode a collapsed drawer with a non-zero closed width is a rail and must stay interactive. */
|
|
117
|
+
private get drawerInert();
|
|
118
|
+
protected updated(changed: PropertyValues): void;
|
|
119
|
+
render(): import("lit-html").TemplateResult<1>;
|
|
120
|
+
}
|
|
121
|
+
declare global {
|
|
122
|
+
interface HTMLElementTagNameMap {
|
|
123
|
+
'c2-side-nav': SideNav;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=side-nav.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"side-nav.d.ts","sourceRoot":"","sources":["../../src/side-nav.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAA4B,KAAK,cAAc,EAAE,MAAM,KAAK,CAAA;AAQ/E,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,CAAA;AACzC,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,KAAK,CAAA;AAC7C,MAAM,MAAM,iBAAiB,GAAG,OAAO,GAAG,QAAQ,GAAG,SAAS,CAAA;AAE9D,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,OAAO,CAAA;CAChB;AAKD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiEG;AACH,qBACa,OAAQ,SAAQ,UAAU;IACrC,OAAgB,MAAM,0BAAoB;IAE1C,+JAA+J;IACnH,MAAM,UAAQ;IAE1D,iGAAiG;IAC3B,WAAW,EAAE,WAAW,CAAS;IAEvG,sDAAsD;IACe,UAAU,EAAE,WAAW,CAAS;IAErG,iGAAiG;IACrB,gBAAgB,UAAQ;IAEpG,mEAAmE;IACxB,QAAQ,EAAE,eAAe,CAAU;IAErE,OAAO,CAAC,UAAU,CAA+B;IACjD,OAAO,CAAC,KAAK,CAAQ;IAEZ,OAAO,CAAC,MAAM,CAAc;IAC3B,OAAO,CAAC,cAAc,CAAc;IAEvD,sFAAsF;IACtF,OAAO,CAAC,eAAe,CAAQ;IAC/B,OAAO,CAAC,iBAAiB,CAAuB;IAChD,OAAO,CAAC,YAAY,CAAQ;IAE5B,OAAO,CAAC,qBAAqB,CAAqG;IAElI,+GAA+G;IAC/G,IAAI,IAAI,IAAI,WAAW,CAItB;IAED,iFAAiF;IACjF,MAAM,CAAC,MAAM,UAAe;IAInB,iBAAiB,IAAI,IAAI;IAKzB,oBAAoB,IAAI,IAAI;IAMrC,OAAO,CAAC,WAAW;IAMnB;;;OAGG;IACH,OAAO,CAAC,cAAc;IAMtB,OAAO,CAAC,gBAAgB;IAqBxB,OAAO,CAAC,SAAS;IASjB,OAAO,CAAC,mBAAmB,CAE1B;IAED,OAAO,CAAC,aAAa,CAKpB;IAED,uGAAuG;IACvG,OAAO,CAAC,iBAAiB;IAWzB,OAAO,CAAC,UAAU;IAQlB,OAAO,CAAC,gBAAgB;IAkBxB,OAAO,CAAC,WAAW;IAYnB,wGAAwG;IACxG,OAAO,KAAK,WAAW,GAMtB;cAEkB,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI;IAYhD,MAAM;CAwBhB;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,aAAa,EAAE,OAAO,CAAA;KACvB;CACF"}
|