@vaadin/side-nav 24.1.0-alpha10

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.
@@ -0,0 +1,212 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2017 - 2023 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import { html, LitElement } from 'lit';
7
+ import { ifDefined } from 'lit/directives/if-defined.js';
8
+ import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
9
+ import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
10
+ import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
11
+ import { sideNavItemBaseStyles } from './vaadin-side-nav-base-styles.js';
12
+
13
+ function isEnabled() {
14
+ return window.Vaadin && window.Vaadin.featureFlags && !!window.Vaadin.featureFlags.sideNavComponent;
15
+ }
16
+
17
+ /**
18
+ * A navigation item to be used within `<vaadin-side-nav>`. Represents a navigation target.
19
+ * Not intended to be used separately.
20
+ *
21
+ * ```html
22
+ * <vaadin-side-nav-item>
23
+ * Item 1
24
+ * <vaadin-side-nav-item path="/path1" slot="children">
25
+ * Child item 1
26
+ * </vaadin-side-nav-item>
27
+ * <vaadin-side-nav-item path="/path2" slot="children">
28
+ * Child item 2
29
+ * </vaadin-side-nav-item>
30
+ * </vaadin-side-nav-item>
31
+ * ```
32
+ *
33
+ * ### Customization
34
+ *
35
+ * You can configure the item by using `slot` names.
36
+ *
37
+ * Slot name | Description
38
+ * ----------|-------------
39
+ * `prefix` | A slot for content before the label (e.g. an icon).
40
+ * `suffix` | A slot for content after the label (e.g. an icon).
41
+ *
42
+ * #### Example
43
+ *
44
+ * ```html
45
+ * <vaadin-side-nav-item>
46
+ * <vaadin-icon icon="vaadin:chart" slot="prefix"></vaadin-icon>
47
+ * Item
48
+ * <span theme="badge primary" slot="suffix">Suffix</span>
49
+ * </vaadin-side-nav-item>
50
+ * ```
51
+ *
52
+ * @fires {CustomEvent} expanded-changed - Fired when the `expanded` property changes.
53
+ *
54
+ * @extends LitElement
55
+ * @mixes PolylitMixin
56
+ * @mixes ThemableMixin
57
+ * @mixes ElementMixin
58
+ */
59
+ class SideNavItem extends ElementMixin(ThemableMixin(PolylitMixin(LitElement))) {
60
+ static get is() {
61
+ return 'vaadin-side-nav-item';
62
+ }
63
+
64
+ static get properties() {
65
+ return {
66
+ /**
67
+ * The path to navigate to
68
+ */
69
+ path: String,
70
+
71
+ /**
72
+ * Whether to show the child items or not
73
+ *
74
+ * @type {boolean}
75
+ */
76
+ expanded: {
77
+ type: Boolean,
78
+ value: false,
79
+ notify: true,
80
+ reflectToAttribute: true,
81
+ },
82
+
83
+ /**
84
+ * Whether the path of the item matches the current path.
85
+ * Set when the item is appended to DOM or when navigated back
86
+ * to the page that contains this item using the browser.
87
+ *
88
+ * @type {boolean}
89
+ */
90
+ active: {
91
+ type: Boolean,
92
+ value: false,
93
+ readOnly: true,
94
+ reflectToAttribute: true,
95
+ },
96
+ };
97
+ }
98
+
99
+ static get styles() {
100
+ return sideNavItemBaseStyles;
101
+ }
102
+
103
+ /** @protected */
104
+ get _button() {
105
+ return this.shadowRoot.querySelector('button');
106
+ }
107
+
108
+ /**
109
+ * @protected
110
+ * @override
111
+ */
112
+ firstUpdated() {
113
+ // By default, if the user hasn't provided a custom role,
114
+ // the role attribute is set to "listitem".
115
+ if (!this.hasAttribute('role')) {
116
+ this.setAttribute('role', 'listitem');
117
+ }
118
+ }
119
+
120
+ /**
121
+ * @protected
122
+ * @override
123
+ */
124
+ updated(props) {
125
+ super.updated(props);
126
+
127
+ if (props.has('path')) {
128
+ this.__updateActive();
129
+ }
130
+ }
131
+
132
+ /** @protected */
133
+ connectedCallback() {
134
+ super.connectedCallback();
135
+ this.__updateActive();
136
+ this.__boundUpdateActive = this.__updateActive.bind(this);
137
+ window.addEventListener('popstate', this.__boundUpdateActive);
138
+ }
139
+
140
+ /** @protected */
141
+ disconnectedCallback() {
142
+ super.disconnectedCallback();
143
+ window.removeEventListener('popstate', this.__boundUpdateActive);
144
+ }
145
+
146
+ /** @protected */
147
+ render() {
148
+ return html`
149
+ <a href="${ifDefined(this.path)}" part="item" aria-current="${this.active ? 'page' : 'false'}">
150
+ <slot name="prefix"></slot>
151
+ <slot></slot>
152
+ <slot name="suffix"></slot>
153
+ <button
154
+ part="toggle-button"
155
+ @click="${this.__toggleExpanded}"
156
+ ?hidden="${!this.querySelector('[slot=children]')}"
157
+ aria-controls="children"
158
+ aria-expanded="${this.expanded}"
159
+ aria-label="Toggle child items"
160
+ ></button>
161
+ </a>
162
+ <slot name="children" role="list" part="children" id="children" ?hidden="${!this.expanded}"></slot>
163
+ `;
164
+ }
165
+
166
+ /** @private */
167
+ __toggleExpanded(e) {
168
+ e.preventDefault();
169
+ e.stopPropagation();
170
+ this.expanded = !this.expanded;
171
+ }
172
+
173
+ /** @private */
174
+ __updateActive() {
175
+ if (!this.path && this.path !== '') {
176
+ this._setActive(false);
177
+ return;
178
+ }
179
+ this._setActive(this.__calculateActive());
180
+ this.toggleAttribute('child-active', document.location.pathname.startsWith(this.path));
181
+ if (this.active) {
182
+ this.expanded = true;
183
+ }
184
+ }
185
+
186
+ /** @private */
187
+ __calculateActive() {
188
+ const pathAbsolute = this.path.startsWith('/');
189
+ // Absolute path or no base uri in use. No special comparison needed
190
+ if (pathAbsolute) {
191
+ // Compare an absolute view path
192
+ return document.location.pathname === this.path;
193
+ }
194
+ const hasBaseUri = document.baseURI !== document.location.href;
195
+ if (!hasBaseUri) {
196
+ // Compare a relative view path (strip the starting slash)
197
+ return document.location.pathname.substring(1) === this.path;
198
+ }
199
+ const pathRelativeToRoot = document.location.pathname;
200
+ const basePath = new URL(document.baseURI).pathname;
201
+ const pathWithoutBase = pathRelativeToRoot.substring(basePath.length);
202
+ const pathRelativeToBase =
203
+ basePath !== pathRelativeToRoot && pathRelativeToRoot.startsWith(basePath) ? pathWithoutBase : pathRelativeToRoot;
204
+ return pathRelativeToBase === this.path;
205
+ }
206
+ }
207
+
208
+ if (isEnabled()) {
209
+ customElements.define(SideNavItem.is, SideNavItem);
210
+ }
211
+
212
+ export { SideNavItem };
@@ -0,0 +1,83 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2023 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import { LitElement } from 'lit';
7
+ import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
8
+ import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
9
+ import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
10
+
11
+ /**
12
+ * Fired when the `collapsed` property changes.
13
+ */
14
+ export type SideNavCollapsedChangedEvent = CustomEvent<{ value: boolean }>;
15
+
16
+ export interface SideNavCustomEventMap {
17
+ 'collapsed-changed': SideNavCollapsedChangedEvent;
18
+ }
19
+
20
+ export type SideNavEventMap = HTMLElementEventMap & SideNavCustomEventMap;
21
+
22
+ /**
23
+ * `<vaadin-side-nav>` is a Web Component for navigation menus.
24
+ *
25
+ * ```html
26
+ * <vaadin-side-nav>
27
+ * <vaadin-side-nav-item>Item 1</vaadin-side-nav-item>
28
+ * <vaadin-side-nav-item>Item 2</vaadin-side-nav-item>
29
+ * <vaadin-side-nav-item>Item 3</vaadin-side-nav-item>
30
+ * <vaadin-side-nav-item>Item 4</vaadin-side-nav-item>
31
+ * </vaadin-side-nav>
32
+ * ```
33
+ *
34
+ * ### Customization
35
+ *
36
+ * You can configure the component by using `slot` names.
37
+ *
38
+ * Slot name | Description
39
+ * ----------|-------------
40
+ * `label` | The label (text) inside the side nav.
41
+ *
42
+ * #### Example
43
+ *
44
+ * ```html
45
+ * <vaadin-side-nav>
46
+ * <span slot="label">Main menu</span>
47
+ * <vaadin-side-nav-item>Item</vaadin-side-nav-item>
48
+ * </vaadin-side-nav>
49
+ * ```
50
+ *
51
+ * @fires {CustomEvent} collapsed-changed - Fired when the `collapsed` property changes.
52
+ */
53
+ declare class SideNav extends ElementMixin(ThemableMixin(PolylitMixin(LitElement))) {
54
+ /**
55
+ * Whether the side nav is collapsible. When enabled, the toggle icon is shown.
56
+ */
57
+ collapsible: boolean;
58
+
59
+ /**
60
+ * Whether the side nav is collapsed. When collapsed, the items are hidden.
61
+ */
62
+ collapsed: boolean;
63
+
64
+ addEventListener<K extends keyof SideNavEventMap>(
65
+ type: K,
66
+ listener: (this: SideNav, ev: SideNavEventMap[K]) => void,
67
+ options?: AddEventListenerOptions | boolean,
68
+ ): void;
69
+
70
+ removeEventListener<K extends keyof SideNavEventMap>(
71
+ type: K,
72
+ listener: (this: SideNav, ev: SideNavEventMap[K]) => void,
73
+ options?: EventListenerOptions | boolean,
74
+ ): void;
75
+ }
76
+
77
+ declare global {
78
+ interface HTMLElementTagNameMap {
79
+ 'vaadin-side-nav': SideNav;
80
+ }
81
+ }
82
+
83
+ export { SideNav };
@@ -0,0 +1,139 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2023 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import { html, LitElement } from 'lit';
7
+ import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
8
+ import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
9
+ import { generateUniqueId } from '@vaadin/component-base/src/unique-id-utils.js';
10
+ import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
11
+ import { sideNavBaseStyles } from './vaadin-side-nav-base-styles.js';
12
+
13
+ function isEnabled() {
14
+ return window.Vaadin && window.Vaadin.featureFlags && !!window.Vaadin.featureFlags.sideNavComponent;
15
+ }
16
+
17
+ /**
18
+ * `<vaadin-side-nav>` is a Web Component for navigation menus.
19
+ *
20
+ * ```html
21
+ * <vaadin-side-nav>
22
+ * <vaadin-side-nav-item>Item 1</vaadin-side-nav-item>
23
+ * <vaadin-side-nav-item>Item 2</vaadin-side-nav-item>
24
+ * <vaadin-side-nav-item>Item 3</vaadin-side-nav-item>
25
+ * <vaadin-side-nav-item>Item 4</vaadin-side-nav-item>
26
+ * </vaadin-side-nav>
27
+ * ```
28
+ *
29
+ * ### Customization
30
+ *
31
+ * You can configure the component by using `slot` names.
32
+ *
33
+ * Slot name | Description
34
+ * ----------|-------------
35
+ * `label` | The label (text) inside the side nav.
36
+ *
37
+ * #### Example
38
+ *
39
+ * ```html
40
+ * <vaadin-side-nav>
41
+ * <span slot="label">Main menu</span>
42
+ * <vaadin-side-nav-item>Item</vaadin-side-nav-item>
43
+ * </vaadin-side-nav>
44
+ * ```
45
+ *
46
+ * @fires {CustomEvent} collapsed-changed - Fired when the `collapsed` property changes.
47
+ *
48
+ * @extends LitElement
49
+ * @mixes PolylitMixin
50
+ * @mixes ThemableMixin
51
+ * @mixes ElementMixin
52
+ */
53
+ class SideNav extends ElementMixin(ThemableMixin(PolylitMixin(LitElement))) {
54
+ static get is() {
55
+ return 'vaadin-side-nav';
56
+ }
57
+
58
+ static get properties() {
59
+ return {
60
+ /**
61
+ * Whether the side nav is collapsible. When enabled, the toggle icon is shown.
62
+ *
63
+ * @type {boolean}
64
+ */
65
+ collapsible: {
66
+ type: Boolean,
67
+ value: false,
68
+ reflectToAttribute: true,
69
+ },
70
+
71
+ /**
72
+ * Whether the side nav is collapsed. When collapsed, the items are hidden.
73
+ *
74
+ * @type {boolean}
75
+ */
76
+ collapsed: {
77
+ type: Boolean,
78
+ value: false,
79
+ notify: true,
80
+ reflectToAttribute: true,
81
+ },
82
+ };
83
+ }
84
+
85
+ static get styles() {
86
+ return sideNavBaseStyles;
87
+ }
88
+
89
+ /** @protected */
90
+ firstUpdated() {
91
+ // By default, if the user hasn't provided a custom role,
92
+ // the role attribute is set to "navigation".
93
+ if (!this.hasAttribute('role')) {
94
+ this.setAttribute('role', 'navigation');
95
+ }
96
+ }
97
+
98
+ /** @protected */
99
+ render() {
100
+ const label = this.querySelector('[slot="label"]');
101
+ if (label && this.collapsible) {
102
+ return html`
103
+ <details ?open="${!this.collapsed}" @toggle="${this.__toggleCollapsed}">${this.__renderBody(label)}</details>
104
+ `;
105
+ }
106
+ return this.__renderBody(label);
107
+ }
108
+
109
+ /** @private */
110
+ __renderBody(label) {
111
+ if (label) {
112
+ if (!label.id) label.id = `side-nav-label-${generateUniqueId()}`;
113
+ this.setAttribute('aria-labelledby', label.id);
114
+ } else {
115
+ this.removeAttribute('aria-labelledby');
116
+ }
117
+ return html`
118
+ <summary part="label" ?hidden="${label == null}">
119
+ <slot name="label" @slotchange="${() => this.requestUpdate()}"></slot>
120
+ </summary>
121
+ <slot role="list"></slot>
122
+ `;
123
+ }
124
+
125
+ /** @private */
126
+ __toggleCollapsed(e) {
127
+ this.collapsed = !e.target.open;
128
+ }
129
+ }
130
+
131
+ if (isEnabled()) {
132
+ customElements.define(SideNav.is, SideNav);
133
+ } else {
134
+ console.warn(
135
+ 'WARNING: The side-nav component is currently an experimental feature and needs to be explicitly enabled. To enable the component, `import "@vaadin/side-nav/enable.js"` *before* importing the side-nav module itself.',
136
+ );
137
+ }
138
+
139
+ export { SideNav };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2017 - 2023 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import './vaadin-side-nav-styles.js';
7
+ import '../../src/vaadin-side-nav-item.js';
@@ -0,0 +1,164 @@
1
+ import '@vaadin/vaadin-lumo-styles/color.js';
2
+ import '@vaadin/vaadin-lumo-styles/typography.js';
3
+ import '@vaadin/vaadin-lumo-styles/sizing.js';
4
+ import '@vaadin/vaadin-lumo-styles/spacing.js';
5
+ import '@vaadin/vaadin-lumo-styles/style.js';
6
+ import '@vaadin/vaadin-lumo-styles/font-icons.js';
7
+ import { css, registerStyles } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
8
+
9
+ export const sideNavItemStyles = css`
10
+ a {
11
+ gap: var(--lumo-space-xs);
12
+ padding: var(--lumo-space-s);
13
+ padding-inline-start: calc(var(--lumo-space-s) + var(--_child-indent, 0px));
14
+ border-radius: var(--lumo-border-radius-m);
15
+ transition: background-color 140ms, color 140ms;
16
+ cursor: var(--lumo-clickable-cursor, default);
17
+ min-height: var(--lumo-icon-size-m);
18
+ }
19
+
20
+ button {
21
+ border: 0;
22
+ margin: calc((var(--lumo-icon-size-m) - var(--lumo-size-s)) / 2) 0;
23
+ margin-inline-end: calc(var(--lumo-space-xs) * -1);
24
+ padding: 0;
25
+ background: transparent;
26
+ font: inherit;
27
+ color: var(--lumo-tertiary-text-color);
28
+ width: var(--lumo-size-s);
29
+ height: var(--lumo-size-s);
30
+ cursor: var(--lumo-clickable-cursor, default);
31
+ transition: color 140ms;
32
+ }
33
+
34
+ @media (any-hover: hover) {
35
+ a:hover {
36
+ color: var(--lumo-header-text-color);
37
+ }
38
+
39
+ button:hover {
40
+ color: var(--lumo-body-text-color);
41
+ }
42
+ }
43
+
44
+ a:active:focus {
45
+ background-color: var(--lumo-contrast-5pct);
46
+ }
47
+
48
+ button::before {
49
+ font-family: lumo-icons;
50
+ content: var(--lumo-icons-dropdown);
51
+ font-size: 1.5em;
52
+ line-height: var(--lumo-size-s);
53
+ display: inline-block;
54
+ transform: rotate(-90deg);
55
+ transition: transform 140ms;
56
+ }
57
+
58
+ :host([expanded]) button::before {
59
+ transform: none;
60
+ }
61
+
62
+ @supports selector(:focus-visible) {
63
+ a,
64
+ button {
65
+ outline: none;
66
+ }
67
+
68
+ a:focus-visible,
69
+ button:focus-visible {
70
+ border-radius: var(--lumo-border-radius-m);
71
+ box-shadow: 0 0 0 2px var(--lumo-primary-color-50pct);
72
+ }
73
+ }
74
+
75
+ a:active {
76
+ color: var(--lumo-header-text-color);
77
+ }
78
+
79
+ slot:not([name]) {
80
+ margin: 0 var(--lumo-space-xs);
81
+ }
82
+
83
+ slot[name='prefix']::slotted(:is(vaadin-icon, [class*='icon'])) {
84
+ color: var(--lumo-contrast-60pct);
85
+ }
86
+
87
+ :host([active]) slot[name='prefix']::slotted(:is(vaadin-icon, [class*='icon'])) {
88
+ color: inherit;
89
+ }
90
+
91
+ slot[name='children'] {
92
+ --_child-indent: calc(var(--_child-indent-2, 0px) + var(--vaadin-side-nav-child-indent, var(--lumo-space-l)));
93
+ }
94
+
95
+ slot[name='children']::slotted(*) {
96
+ --_child-indent-2: var(--_child-indent);
97
+ }
98
+
99
+ :host([active]) a {
100
+ color: var(--lumo-primary-text-color);
101
+ background-color: var(--lumo-primary-color-10pct);
102
+ }
103
+ `;
104
+
105
+ registerStyles('vaadin-side-nav-item', sideNavItemStyles, { moduleId: 'lumo-side-nav-item' });
106
+
107
+ export const sideNavStyles = css`
108
+ :host {
109
+ font-family: var(--lumo-font-family);
110
+ font-size: var(--lumo-font-size-m);
111
+ font-weight: 500;
112
+ line-height: var(--lumo-line-height-xs);
113
+ color: var(--lumo-body-text-color);
114
+ -webkit-tap-highlight-color: transparent;
115
+ }
116
+
117
+ summary {
118
+ cursor: var(--lumo-clickable-cursor, default);
119
+ border-radius: var(--lumo-border-radius-m);
120
+ }
121
+
122
+ summary ::slotted([slot='label']) {
123
+ font-size: var(--lumo-font-size-s);
124
+ color: var(--lumo-secondary-text-color);
125
+ margin: var(--lumo-space-s);
126
+ border-radius: inherit;
127
+ }
128
+
129
+ summary::after {
130
+ font-family: lumo-icons;
131
+ color: var(--lumo-tertiary-text-color);
132
+ font-size: var(--lumo-icon-size-m);
133
+ width: var(--lumo-size-s);
134
+ height: var(--lumo-size-s);
135
+ transition: transform 140ms;
136
+ margin: 0 var(--lumo-space-xs);
137
+ }
138
+
139
+ :host([collapsible]) summary::after {
140
+ content: var(--lumo-icons-dropdown);
141
+ }
142
+
143
+ @media (any-hover: hover) {
144
+ summary:hover::after {
145
+ color: var(--lumo-body-text-color);
146
+ }
147
+ }
148
+
149
+ :host([collapsed]) summary::after {
150
+ transform: rotate(-90deg);
151
+ }
152
+
153
+ @supports selector(:focus-visible) {
154
+ summary {
155
+ outline: none;
156
+ }
157
+
158
+ summary:focus-visible {
159
+ box-shadow: 0 0 0 2px var(--lumo-primary-color-50pct);
160
+ }
161
+ }
162
+ `;
163
+
164
+ registerStyles('vaadin-side-nav', sideNavStyles, { moduleId: 'lumo-side-nav' });
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2017 - 2023 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import './vaadin-side-nav-item.js';
7
+ import '../../src/vaadin-side-nav.js';
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2017 - 2023 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import '../../src/vaadin-side-nav.js';
7
+ import '../../src/vaadin-side-nav-item.js';
@@ -0,0 +1 @@
1
+ export * from './src/vaadin-side-nav-item.js';
@@ -0,0 +1,2 @@
1
+ import './theme/lumo/vaadin-side-nav.js';
2
+ export * from './src/vaadin-side-nav-item.js';
@@ -0,0 +1 @@
1
+ export * from './src/vaadin-side-nav.js';
@@ -0,0 +1,2 @@
1
+ import './theme/lumo/vaadin-side-nav.js';
2
+ export * from './src/vaadin-side-nav.js';