@vaadin/side-nav 24.1.5 → 24.2.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.
package/README.md CHANGED
@@ -49,9 +49,14 @@ import '@vaadin/side-nav';
49
49
  ## Themes
50
50
 
51
51
  Vaadin components come with two built-in [themes](https://vaadin.com/docs/latest/styling), Lumo and Material.
52
- This component currently does not support Material theme.
53
52
  The [main entrypoint](https://github.com/vaadin/web-components/blob/main/packages/side-nav/vaadin-side-nav.js) of the package uses the Lumo theme.
54
53
 
54
+ To use the Material theme, import the component from the `theme/material` folder:
55
+
56
+ ```js
57
+ import '@vaadin/side-nav/theme/material/vaadin-side-nav.js';
58
+ ```
59
+
55
60
  You can also import the Lumo version of the component explicitly:
56
61
 
57
62
  ```js
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vaadin/side-nav",
3
- "version": "24.1.5",
3
+ "version": "24.2.0-alpha10",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -35,15 +35,15 @@
35
35
  "web-component"
36
36
  ],
37
37
  "dependencies": {
38
- "@vaadin/component-base": "~24.1.5",
39
- "@vaadin/vaadin-lumo-styles": "~24.1.5",
40
- "@vaadin/vaadin-material-styles": "~24.1.5",
41
- "@vaadin/vaadin-themable-mixin": "~24.1.5",
38
+ "@vaadin/component-base": "24.2.0-alpha10",
39
+ "@vaadin/vaadin-lumo-styles": "24.2.0-alpha10",
40
+ "@vaadin/vaadin-material-styles": "24.2.0-alpha10",
41
+ "@vaadin/vaadin-themable-mixin": "24.2.0-alpha10",
42
42
  "lit": "^2.0.0"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@esm-bundle/chai": "^4.3.4",
46
- "@vaadin/testing-helpers": "^0.4.2",
46
+ "@vaadin/testing-helpers": "^0.5.0",
47
47
  "lit": "^2.0.0",
48
48
  "sinon": "^13.0.2"
49
49
  },
@@ -51,5 +51,5 @@
51
51
  "web-types.json",
52
52
  "web-types.lit.json"
53
53
  ],
54
- "gitHead": "2150f0696b9205ed3651033301927516b87cf88f"
54
+ "gitHead": "ca16b5f88b00ae05fb6d7c7e9874525048e389f0"
55
55
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @license
3
- * Copyright (c) 2017 - 2023 Vaadin Ltd.
3
+ * Copyright (c) 2023 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
6
  import { css } from 'lit';
@@ -10,11 +10,21 @@ export const sideNavItemBaseStyles = css`
10
10
  display: block;
11
11
  }
12
12
 
13
+ :host([hidden]),
13
14
  [hidden] {
14
15
  display: none !important;
15
16
  }
16
17
 
17
- a {
18
+ :host([disabled]) {
19
+ pointer-events: none;
20
+ }
21
+
22
+ [part='content'] {
23
+ display: flex;
24
+ align-items: center;
25
+ }
26
+
27
+ [part='link'] {
18
28
  flex: auto;
19
29
  min-width: 0;
20
30
  display: flex;
@@ -28,19 +38,21 @@ export const sideNavItemBaseStyles = css`
28
38
  -webkit-appearance: none;
29
39
  appearance: none;
30
40
  flex: none;
41
+ position: relative;
42
+ margin: 0;
43
+ padding: 0;
44
+ border: 0;
45
+ background: transparent;
31
46
  }
32
47
 
33
- :host(:not([path])) a {
34
- position: relative;
48
+ [part='children'] {
49
+ padding: 0;
50
+ margin: 0;
51
+ list-style-type: none;
35
52
  }
36
53
 
37
- :host(:not([path])) button::after {
38
- content: '';
39
- position: absolute;
40
- top: 0;
41
- right: 0;
42
- bottom: 0;
43
- left: 0;
54
+ :host(:not([has-children])) button {
55
+ display: none !important;
44
56
  }
45
57
 
46
58
  slot[name='prefix'],
@@ -56,12 +68,6 @@ export const sideNavItemBaseStyles = css`
56
68
  text-overflow: ellipsis;
57
69
  white-space: nowrap;
58
70
  }
59
-
60
- slot[name='children'] {
61
- /* Needed to make role="list" work */
62
- display: block;
63
- width: 100%;
64
- }
65
71
  `;
66
72
 
67
73
  export const sideNavBaseStyles = css`
@@ -73,32 +79,24 @@ export const sideNavBaseStyles = css`
73
79
  display: none !important;
74
80
  }
75
81
 
76
- summary {
82
+ button {
77
83
  display: flex;
78
84
  align-items: center;
79
- justify-content: space-between;
80
- }
81
-
82
- summary ::slotted([slot='label']) {
83
- display: block;
84
- }
85
-
86
- summary::-webkit-details-marker {
87
- display: none;
88
- }
89
-
90
- summary::marker {
91
- content: '';
92
- }
93
-
94
- summary::after {
95
- display: inline-flex;
96
- align-items: center;
97
- justify-content: center;
85
+ justify-content: inherit;
86
+ width: 100%;
87
+ margin: 0;
88
+ padding: 0;
89
+ background-color: initial;
90
+ color: inherit;
91
+ border: initial;
92
+ outline: none;
93
+ font: inherit;
94
+ text-align: inherit;
98
95
  }
99
96
 
100
- slot {
101
- /* Needed to make role="list" work */
102
- display: block;
97
+ [part='children'] {
98
+ padding: 0;
99
+ margin: 0;
100
+ list-style-type: none;
103
101
  }
104
102
  `;
@@ -0,0 +1,46 @@
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 type { Constructor } from '@open-wc/dedupe-mixin';
7
+
8
+ export interface SideNavI18n {
9
+ toggle: string;
10
+ }
11
+
12
+ export declare function SideNavChildrenMixin<T extends Constructor<HTMLElement>>(
13
+ base: T,
14
+ ): Constructor<SideNavChildrenMixinClass> & T;
15
+
16
+ export declare class SideNavChildrenMixinClass {
17
+ /**
18
+ * The object used to localize this component.
19
+ *
20
+ * To change the default localization, replace the entire
21
+ * `i18n` object with a custom one.
22
+ *
23
+ * The object has the following structure and default values:
24
+ * ```
25
+ * {
26
+ * toggle: 'Toggle child items'
27
+ * }
28
+ * ```
29
+ */
30
+ i18n: SideNavI18n;
31
+
32
+ /**
33
+ * List of child items of this component.
34
+ */
35
+ protected readonly _items: HTMLElement[];
36
+
37
+ /**
38
+ * Name of the slot to be used for children.
39
+ */
40
+ protected readonly _itemsSlotName: string;
41
+
42
+ /**
43
+ * Count of child items.
44
+ */
45
+ protected _itemsCount: number;
46
+ }
@@ -0,0 +1,134 @@
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 { SlotController } from '@vaadin/component-base/src/slot-controller.js';
7
+
8
+ /**
9
+ * A controller that manages the item content children slot.
10
+ */
11
+ class ChildrenController extends SlotController {
12
+ constructor(host, slotName) {
13
+ super(host, slotName, null, { observe: true, multiple: true });
14
+ }
15
+
16
+ /**
17
+ * @protected
18
+ * @override
19
+ */
20
+ initAddedNode() {
21
+ this.host.requestUpdate();
22
+ }
23
+
24
+ /**
25
+ * @protected
26
+ * @override
27
+ */
28
+ teardownNode() {
29
+ this.host.requestUpdate();
30
+ }
31
+ }
32
+
33
+ /**
34
+ * @polymerMixin
35
+ */
36
+ export const SideNavChildrenMixin = (superClass) =>
37
+ class SideNavChildrenMixin extends superClass {
38
+ static get properties() {
39
+ return {
40
+ /**
41
+ * The object used to localize this component.
42
+ *
43
+ * To change the default localization, replace the entire
44
+ * `i18n` object with a custom one.
45
+ *
46
+ * The object has the following structure and default values:
47
+ * ```
48
+ * {
49
+ * toggle: 'Toggle child items'
50
+ * }
51
+ * ```
52
+ *
53
+ * @type {SideNavI18n}
54
+ * @default {English/US}
55
+ */
56
+ i18n: {
57
+ type: Object,
58
+ value: () => {
59
+ return {
60
+ toggle: 'Toggle child items',
61
+ };
62
+ },
63
+ },
64
+
65
+ /**
66
+ * Count of child items.
67
+ * @protected
68
+ */
69
+ _itemsCount: {
70
+ type: Number,
71
+ value: 0,
72
+ },
73
+ };
74
+ }
75
+
76
+ constructor() {
77
+ super();
78
+
79
+ this._childrenController = new ChildrenController(this, this._itemsSlotName);
80
+ }
81
+
82
+ /**
83
+ * List of child items of this component.
84
+ * @protected
85
+ */
86
+ get _items() {
87
+ return this._childrenController.nodes;
88
+ }
89
+
90
+ /**
91
+ * Name of the slot to be used for children.
92
+ * @protected
93
+ */
94
+ get _itemsSlotName() {
95
+ return 'children';
96
+ }
97
+
98
+ /** @protected */
99
+ firstUpdated() {
100
+ super.firstUpdated();
101
+
102
+ // Controller that detects changes to the side-nav items.
103
+ this.addController(this._childrenController);
104
+ }
105
+
106
+ /**
107
+ * @protected
108
+ * @override
109
+ */
110
+ willUpdate(props) {
111
+ super.willUpdate(props);
112
+
113
+ this._itemsCount = this._items.length;
114
+ }
115
+
116
+ /**
117
+ * @protected
118
+ * @override
119
+ */
120
+ updated(props) {
121
+ super.updated(props);
122
+
123
+ if (props.has('_itemsCount')) {
124
+ this.toggleAttribute('has-children', this._itemsCount > 0);
125
+ }
126
+
127
+ // Propagate i18n object to all the child items
128
+ if (props.has('_itemsCount') || props.has('i18n')) {
129
+ this._items.forEach((item) => {
130
+ item.i18n = this.i18n;
131
+ });
132
+ }
133
+ }
134
+ };
@@ -1,12 +1,14 @@
1
1
  /**
2
2
  * @license
3
- * Copyright (c) 2017 - 2023 Vaadin Ltd.
3
+ * Copyright (c) 2023 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
6
  import { LitElement } from 'lit';
7
+ import { DisabledMixin } from '@vaadin/a11y-base/src/disabled-mixin.js';
7
8
  import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
8
9
  import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
9
10
  import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
11
+ import { SideNavChildrenMixin } from './vaadin-side-nav-children-mixin.js';
10
12
 
11
13
  /**
12
14
  * Fired when the `expanded` property changes.
@@ -54,14 +56,42 @@ export type SideNavItemEventMap = HTMLElementEventMap & SideNavItemCustomEventMa
54
56
  * </vaadin-side-nav-item>
55
57
  * ```
56
58
  *
59
+ * ### Styling
60
+ *
61
+ * The following shadow DOM parts are available for styling:
62
+ *
63
+ * Part name | Description
64
+ * ----------------|----------------
65
+ * `content` | The element that wraps link and toggle button
66
+ * `children` | The element that wraps child items
67
+ * `link` | The clickable anchor used for navigation
68
+ * `toggle-button` | The toggle button
69
+ *
70
+ * The following state attributes are available for styling:
71
+ *
72
+ * Attribute | Description
73
+ * ---------------|-------------
74
+ * `disabled` | Set when the element is disabled.
75
+ * `expanded` | Set when the element is expanded.
76
+ * `has-children` | Set when the element has child items.
77
+ *
78
+ * See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
79
+ *
57
80
  * @fires {CustomEvent} expanded-changed - Fired when the `expanded` property changes.
58
81
  */
59
- declare class SideNavItem extends ElementMixin(ThemableMixin(PolylitMixin(LitElement))) {
82
+ declare class SideNavItem extends SideNavChildrenMixin(
83
+ DisabledMixin(ElementMixin(ThemableMixin(PolylitMixin(LitElement)))),
84
+ ) {
60
85
  /**
61
86
  * The path to navigate to
62
87
  */
63
88
  path: string | null | undefined;
64
89
 
90
+ /**
91
+ * The list of alternative paths matching this item
92
+ */
93
+ pathAliases: string[];
94
+
65
95
  /**
66
96
  * Whether to show the child items or not
67
97
  */
@@ -72,7 +102,7 @@ declare class SideNavItem extends ElementMixin(ThemableMixin(PolylitMixin(LitEle
72
102
  * Set when the item is appended to DOM or when navigated back
73
103
  * to the page that contains this item using the browser.
74
104
  */
75
- readonly active: boolean;
105
+ readonly current: boolean;
76
106
 
77
107
  addEventListener<K extends keyof SideNavItemEventMap>(
78
108
  type: K,
@@ -1,14 +1,18 @@
1
1
  /**
2
2
  * @license
3
- * Copyright (c) 2017 - 2023 Vaadin Ltd.
3
+ * Copyright (c) 2023 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
6
  import { html, LitElement } from 'lit';
7
7
  import { ifDefined } from 'lit/directives/if-defined.js';
8
+ import { DisabledMixin } from '@vaadin/a11y-base/src/disabled-mixin.js';
9
+ import { screenReaderOnly } from '@vaadin/a11y-base/src/styles/sr-only-styles.js';
8
10
  import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
9
11
  import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
12
+ import { matchPaths } from '@vaadin/component-base/src/url-utils.js';
10
13
  import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
11
14
  import { sideNavItemBaseStyles } from './vaadin-side-nav-base-styles.js';
15
+ import { SideNavChildrenMixin } from './vaadin-side-nav-children-mixin.js';
12
16
 
13
17
  function isEnabled() {
14
18
  return window.Vaadin && window.Vaadin.featureFlags && !!window.Vaadin.featureFlags.sideNavComponent;
@@ -49,14 +53,37 @@ function isEnabled() {
49
53
  * </vaadin-side-nav-item>
50
54
  * ```
51
55
  *
56
+ * ### Styling
57
+ *
58
+ * The following shadow DOM parts are available for styling:
59
+ *
60
+ * Part name | Description
61
+ * ----------------|----------------
62
+ * `content` | The element that wraps link and toggle button
63
+ * `children` | The element that wraps child items
64
+ * `link` | The clickable anchor used for navigation
65
+ * `toggle-button` | The toggle button
66
+ *
67
+ * The following state attributes are available for styling:
68
+ *
69
+ * Attribute | Description
70
+ * ---------------|-------------
71
+ * `disabled` | Set when the element is disabled.
72
+ * `expanded` | Set when the element is expanded.
73
+ * `has-children` | Set when the element has child items.
74
+ *
75
+ * See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
76
+ *
52
77
  * @fires {CustomEvent} expanded-changed - Fired when the `expanded` property changes.
53
78
  *
54
79
  * @extends LitElement
55
80
  * @mixes PolylitMixin
56
81
  * @mixes ThemableMixin
82
+ * @mixes DisabledMixin
57
83
  * @mixes ElementMixin
84
+ * @mixes SideNavChildrenMixin
58
85
  */
59
- class SideNavItem extends ElementMixin(ThemableMixin(PolylitMixin(LitElement))) {
86
+ class SideNavItem extends SideNavChildrenMixin(DisabledMixin(ElementMixin(ThemableMixin(PolylitMixin(LitElement))))) {
60
87
  static get is() {
61
88
  return 'vaadin-side-nav-item';
62
89
  }
@@ -68,6 +95,16 @@ class SideNavItem extends ElementMixin(ThemableMixin(PolylitMixin(LitElement)))
68
95
  */
69
96
  path: String,
70
97
 
98
+ /**
99
+ * The list of alternative paths matching this item
100
+ *
101
+ * @type {!Array<string>}
102
+ */
103
+ pathAliases: {
104
+ type: Array,
105
+ value: () => [],
106
+ },
107
+
71
108
  /**
72
109
  * Whether to show the child items or not
73
110
  *
@@ -87,7 +124,7 @@ class SideNavItem extends ElementMixin(ThemableMixin(PolylitMixin(LitElement)))
87
124
  *
88
125
  * @type {boolean}
89
126
  */
90
- active: {
127
+ current: {
91
128
  type: Boolean,
92
129
  value: false,
93
130
  readOnly: true,
@@ -97,7 +134,13 @@ class SideNavItem extends ElementMixin(ThemableMixin(PolylitMixin(LitElement)))
97
134
  }
98
135
 
99
136
  static get styles() {
100
- return sideNavItemBaseStyles;
137
+ return [screenReaderOnly, sideNavItemBaseStyles];
138
+ }
139
+
140
+ constructor() {
141
+ super();
142
+
143
+ this.__boundUpdateCurrent = this.__updateCurrent.bind(this);
101
144
  }
102
145
 
103
146
  /** @protected */
@@ -110,6 +153,8 @@ class SideNavItem extends ElementMixin(ThemableMixin(PolylitMixin(LitElement)))
110
153
  * @override
111
154
  */
112
155
  firstUpdated() {
156
+ super.firstUpdated();
157
+
113
158
  // By default, if the user hasn't provided a custom role,
114
159
  // the role attribute is set to "listitem".
115
160
  if (!this.hasAttribute('role')) {
@@ -124,84 +169,102 @@ class SideNavItem extends ElementMixin(ThemableMixin(PolylitMixin(LitElement)))
124
169
  updated(props) {
125
170
  super.updated(props);
126
171
 
127
- if (props.has('path')) {
128
- this.__updateActive();
172
+ if (props.has('path') || props.has('pathAliases')) {
173
+ this.__updateCurrent();
174
+ }
175
+
176
+ // Ensure all the child items are disabled
177
+ if (props.has('disabled') || props.has('_itemsCount')) {
178
+ this._items.forEach((item) => {
179
+ item.disabled = this.disabled;
180
+ });
129
181
  }
130
182
  }
131
183
 
132
184
  /** @protected */
133
185
  connectedCallback() {
134
186
  super.connectedCallback();
135
- this.__updateActive();
136
- this.__boundUpdateActive = this.__updateActive.bind(this);
137
- window.addEventListener('popstate', this.__boundUpdateActive);
187
+ this.__updateCurrent();
188
+
189
+ window.addEventListener('popstate', this.__boundUpdateCurrent);
138
190
  }
139
191
 
140
192
  /** @protected */
141
193
  disconnectedCallback() {
142
194
  super.disconnectedCallback();
143
- window.removeEventListener('popstate', this.__boundUpdateActive);
195
+ window.removeEventListener('popstate', this.__boundUpdateCurrent);
144
196
  }
145
197
 
146
198
  /** @protected */
147
199
  render() {
148
200
  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>
201
+ <div part="content" @click="${this._onContentClick}">
202
+ <a
203
+ id="link"
204
+ ?disabled="${this.disabled}"
205
+ tabindex="${this.disabled || !this.path ? '-1' : '0'}"
206
+ href="${ifDefined(this.disabled ? null : this.path)}"
207
+ part="link"
208
+ aria-current="${this.current ? 'page' : 'false'}"
209
+ >
210
+ <slot name="prefix"></slot>
211
+ <slot></slot>
212
+ <slot name="suffix"></slot>
213
+ </a>
153
214
  <button
154
215
  part="toggle-button"
155
- @click="${this.__toggleExpanded}"
156
- ?hidden="${!this.querySelector('[slot=children]')}"
216
+ ?disabled="${this.disabled}"
217
+ @click="${this._onButtonClick}"
157
218
  aria-controls="children"
158
219
  aria-expanded="${this.expanded}"
159
- aria-label="Toggle child items"
220
+ aria-labelledby="link i18n"
160
221
  ></button>
161
- </a>
162
- <slot name="children" role="list" part="children" id="children" ?hidden="${!this.expanded}"></slot>
222
+ </div>
223
+ <ul part="children" role="list" ?hidden="${!this.expanded}" aria-hidden="${this.expanded ? 'false' : 'true'}">
224
+ <slot name="children"></slot>
225
+ </ul>
226
+ <div class="sr-only" id="i18n">${this.i18n.toggle}</div>
163
227
  `;
164
228
  }
165
229
 
166
230
  /** @private */
167
- __toggleExpanded(e) {
168
- e.preventDefault();
169
- e.stopPropagation();
170
- this.expanded = !this.expanded;
231
+ _onButtonClick(event) {
232
+ // Prevent the event from being handled
233
+ // by the content click listener below
234
+ event.stopPropagation();
235
+ this.__toggleExpanded();
171
236
  }
172
237
 
173
238
  /** @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;
239
+ _onContentClick() {
240
+ // Toggle item expanded state unless the link has a non-empty path
241
+ if (this.path == null && this.hasAttribute('has-children')) {
242
+ this.__toggleExpanded();
183
243
  }
184
244
  }
185
245
 
186
246
  /** @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;
247
+ __toggleExpanded() {
248
+ this.expanded = !this.expanded;
249
+ }
250
+
251
+ /** @private */
252
+ __updateCurrent() {
253
+ this._setCurrent(this.__isCurrent());
254
+ if (this.current) {
255
+ this.expanded = this._items.length > 0;
193
256
  }
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;
257
+ }
258
+
259
+ /** @private */
260
+ __isCurrent() {
261
+ if (this.path == null) {
262
+ return false;
198
263
  }
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;
264
+ return (
265
+ matchPaths(document.location.pathname, this.path) ||
266
+ this.pathAliases.some((alias) => matchPaths(document.location.pathname, alias))
267
+ );
205
268
  }
206
269
  }
207
270