@vaadin/side-nav 24.1.2 → 24.2.0-alpha2

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.
@@ -4,11 +4,14 @@
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
+ import { ifDefined } from 'lit/directives/if-defined.js';
8
+ import { FocusMixin } from '@vaadin/a11y-base/src/focus-mixin.js';
7
9
  import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
8
10
  import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
9
11
  import { generateUniqueId } from '@vaadin/component-base/src/unique-id-utils.js';
10
12
  import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
11
13
  import { sideNavBaseStyles } from './vaadin-side-nav-base-styles.js';
14
+ import { SideNavChildrenMixin } from './vaadin-side-nav-children-mixin.js';
12
15
 
13
16
  function isEnabled() {
14
17
  return window.Vaadin && window.Vaadin.featureFlags && !!window.Vaadin.featureFlags.sideNavComponent;
@@ -43,18 +46,43 @@ function isEnabled() {
43
46
  * </vaadin-side-nav>
44
47
  * ```
45
48
  *
49
+ * ### Styling
50
+ *
51
+ * The following shadow DOM parts are available for styling:
52
+ *
53
+ * Part name | Description
54
+ * ----------------|----------------
55
+ * `label` | The label element
56
+ * `children` | The element that wraps child items
57
+ * `toggle-button` | The toggle button
58
+ *
59
+ * The following state attributes are available for styling:
60
+ *
61
+ * Attribute | Description
62
+ * -------------|-------------
63
+ * `collapsed` | Set when the element is collapsed.
64
+ * `focus-ring` | Set when the label is focused using the keyboard.
65
+ * `focused` | Set when the label is focused.
66
+ *
67
+ * See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
68
+ *
46
69
  * @fires {CustomEvent} collapsed-changed - Fired when the `collapsed` property changes.
47
70
  *
48
71
  * @extends LitElement
49
72
  * @mixes PolylitMixin
50
73
  * @mixes ThemableMixin
51
74
  * @mixes ElementMixin
75
+ * @mixes SideNavChildrenMixin
52
76
  */
53
- class SideNav extends ElementMixin(ThemableMixin(PolylitMixin(LitElement))) {
77
+ class SideNav extends SideNavChildrenMixin(FocusMixin(ElementMixin(ThemableMixin(PolylitMixin(LitElement))))) {
54
78
  static get is() {
55
79
  return 'vaadin-side-nav';
56
80
  }
57
81
 
82
+ static get shadowRootOptions() {
83
+ return { ...LitElement.shadowRootOptions, delegatesFocus: true };
84
+ }
85
+
58
86
  static get properties() {
59
87
  return {
60
88
  /**
@@ -86,8 +114,30 @@ class SideNav extends ElementMixin(ThemableMixin(PolylitMixin(LitElement))) {
86
114
  return sideNavBaseStyles;
87
115
  }
88
116
 
117
+ constructor() {
118
+ super();
119
+
120
+ this._labelId = `side-nav-label-${generateUniqueId()}`;
121
+ }
122
+
123
+ /**
124
+ * Name of the slot to be used for children.
125
+ * @protected
126
+ * @override
127
+ */
128
+ get _itemsSlotName() {
129
+ return '';
130
+ }
131
+
132
+ /** @protected */
133
+ get focusElement() {
134
+ return this.shadowRoot.querySelector('button');
135
+ }
136
+
89
137
  /** @protected */
90
138
  firstUpdated() {
139
+ super.firstUpdated();
140
+
91
141
  // By default, if the user hasn't provided a custom role,
92
142
  // the role attribute is set to "navigation".
93
143
  if (!this.hasAttribute('role')) {
@@ -97,34 +147,55 @@ class SideNav extends ElementMixin(ThemableMixin(PolylitMixin(LitElement))) {
97
147
 
98
148
  /** @protected */
99
149
  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
- `;
150
+ return html`
151
+ <button
152
+ part="label"
153
+ @click="${this._onLabelClick}"
154
+ aria-expanded="${ifDefined(this.collapsible ? !this.collapsed : null)}"
155
+ aria-controls="children"
156
+ >
157
+ <slot name="label" @slotchange="${this._onLabelSlotChange}"></slot>
158
+ <span part="toggle-button" aria-hidden="true"></span>
159
+ </button>
160
+ <ul id="children" part="children" ?hidden="${this.collapsed}" aria-hidden="${this.collapsed ? 'true' : 'false'}">
161
+ <slot></slot>
162
+ </ul>
163
+ `;
164
+ }
165
+
166
+ /**
167
+ * @param {Event} event
168
+ * @return {boolean}
169
+ * @protected
170
+ * @override
171
+ */
172
+ _shouldSetFocus(event) {
173
+ return event.composedPath()[0] === this.focusElement;
174
+ }
175
+
176
+ /** @private */
177
+ _onLabelClick() {
178
+ if (this.collapsible) {
179
+ this.__toggleCollapsed();
105
180
  }
106
- return this.__renderBody(label);
107
181
  }
108
182
 
109
183
  /** @private */
110
- __renderBody(label) {
184
+ _onLabelSlotChange() {
185
+ const label = this.querySelector('[slot="label"]');
111
186
  if (label) {
112
- if (!label.id) label.id = `side-nav-label-${generateUniqueId()}`;
187
+ if (!label.id) {
188
+ label.id = this._labelId;
189
+ }
113
190
  this.setAttribute('aria-labelledby', label.id);
114
191
  } else {
115
192
  this.removeAttribute('aria-labelledby');
116
193
  }
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
194
  }
124
195
 
125
196
  /** @private */
126
- __toggleCollapsed(e) {
127
- this.collapsed = !e.target.open;
197
+ __toggleCollapsed() {
198
+ this.collapsed = !this.collapsed;
128
199
  }
129
200
  }
130
201
 
@@ -4,10 +4,12 @@ import '@vaadin/vaadin-lumo-styles/sizing.js';
4
4
  import '@vaadin/vaadin-lumo-styles/spacing.js';
5
5
  import '@vaadin/vaadin-lumo-styles/style.js';
6
6
  import '@vaadin/vaadin-lumo-styles/font-icons.js';
7
+ import { fieldButton } from '@vaadin/vaadin-lumo-styles/mixins/field-button.js';
7
8
  import { css, registerStyles } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
8
9
 
9
10
  export const sideNavItemStyles = css`
10
- a {
11
+ [part='link'] {
12
+ width: 100%;
11
13
  gap: var(--lumo-space-xs);
12
14
  padding: var(--lumo-space-s);
13
15
  padding-inline-start: calc(var(--lumo-space-s) + var(--_child-indent, 0px));
@@ -17,74 +19,72 @@ export const sideNavItemStyles = css`
17
19
  min-height: var(--lumo-icon-size-m);
18
20
  }
19
21
 
20
- button {
21
- border: 0;
22
- margin: calc((var(--lumo-icon-size-m) - var(--lumo-size-s)) / 2) 0;
22
+ [part='link'][href] {
23
+ cursor: pointer;
24
+ }
25
+
26
+ [part='toggle-button'] {
23
27
  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
28
  width: var(--lumo-size-s);
29
29
  height: var(--lumo-size-s);
30
- cursor: var(--lumo-clickable-cursor, default);
31
- transition: color 140ms;
30
+ }
31
+
32
+ :host([has-children]) [part='content'] {
33
+ padding-inline-end: var(--lumo-space-s);
32
34
  }
33
35
 
34
36
  @media (any-hover: hover) {
35
- a:hover {
37
+ [part='link']:hover {
36
38
  color: var(--lumo-header-text-color);
37
39
  }
38
40
 
39
- button:hover {
41
+ [part='toggle-button']:hover {
40
42
  color: var(--lumo-body-text-color);
41
43
  }
42
44
  }
43
45
 
44
- a:active:focus {
46
+ [part='link']:active:focus {
45
47
  background-color: var(--lumo-contrast-5pct);
46
48
  }
47
49
 
48
- button::before {
49
- font-family: lumo-icons;
50
+ [part='toggle-button']::before {
50
51
  content: var(--lumo-icons-dropdown);
51
- font-size: 1.5em;
52
- line-height: var(--lumo-size-s);
53
- display: inline-block;
54
52
  transform: rotate(-90deg);
55
53
  transition: transform 140ms;
56
54
  }
57
55
 
58
- :host([expanded]) button::before {
56
+ :host([expanded]) [part='toggle-button']::before {
59
57
  transform: none;
60
58
  }
61
59
 
62
60
  @supports selector(:focus-visible) {
63
- a,
64
- button {
61
+ [part='link'],
62
+ [part='toggle-button'] {
65
63
  outline: none;
66
64
  }
67
65
 
68
- a:focus-visible,
69
- button:focus-visible {
66
+ [part='link']:focus-visible,
67
+ [part='toggle-button']:focus-visible {
70
68
  border-radius: var(--lumo-border-radius-m);
71
69
  box-shadow: 0 0 0 2px var(--lumo-primary-color-50pct);
72
70
  }
73
71
  }
74
72
 
75
- a:active {
73
+ [part='link']:active {
76
74
  color: var(--lumo-header-text-color);
77
75
  }
78
76
 
79
77
  slot:not([name]) {
80
- margin: 0 var(--lumo-space-xs);
78
+ margin: 0 var(--lumo-space-s);
81
79
  }
82
80
 
83
81
  slot[name='prefix']::slotted(:is(vaadin-icon, [class*='icon'])) {
82
+ padding: 0.1em;
83
+ flex-shrink: 0;
84
84
  color: var(--lumo-contrast-60pct);
85
85
  }
86
86
 
87
- :host([active]) slot[name='prefix']::slotted(:is(vaadin-icon, [class*='icon'])) {
87
+ :host([current]) slot[name='prefix']::slotted(:is(vaadin-icon, [class*='icon'])) {
88
88
  color: inherit;
89
89
  }
90
90
 
@@ -96,10 +96,10 @@ export const sideNavItemStyles = css`
96
96
  --_child-indent-2: var(--_child-indent);
97
97
  }
98
98
 
99
- :host([active]) a {
99
+ :host([current]) [part='link'] {
100
100
  color: var(--lumo-primary-text-color);
101
101
  background-color: var(--lumo-primary-color-10pct);
102
102
  }
103
103
  `;
104
104
 
105
- registerStyles('vaadin-side-nav-item', sideNavItemStyles, { moduleId: 'lumo-side-nav-item' });
105
+ registerStyles('vaadin-side-nav-item', [fieldButton, sideNavItemStyles], { moduleId: 'lumo-side-nav-item' });
@@ -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 './vaadin-side-nav-item-styles.js';
@@ -16,49 +16,58 @@ export const sideNavStyles = css`
16
16
  -webkit-tap-highlight-color: transparent;
17
17
  }
18
18
 
19
- summary {
20
- cursor: var(--lumo-clickable-cursor, default);
19
+ [part='label'] {
20
+ display: flex;
21
+ align-items: center;
22
+ width: 100%;
23
+ outline: none;
24
+ box-sizing: border-box;
21
25
  border-radius: var(--lumo-border-radius-m);
26
+ font-family: var(--lumo-font-family);
27
+ font-size: var(--lumo-font-size-s);
28
+ font-weight: 500;
29
+ line-height: var(--lumo-line-height-xs);
22
30
  }
23
31
 
24
- summary ::slotted([slot='label']) {
25
- font-size: var(--lumo-font-size-s);
32
+ [part='label'] ::slotted([slot='label']) {
26
33
  color: var(--lumo-secondary-text-color);
27
34
  margin: var(--lumo-space-s);
28
- border-radius: inherit;
29
35
  }
30
36
 
31
- summary::after {
32
- font-family: lumo-icons;
33
- color: var(--lumo-tertiary-text-color);
34
- font-size: var(--lumo-icon-size-m);
37
+ :host([focus-ring]) [part='label'] {
38
+ box-shadow: 0 0 0 2px var(--lumo-primary-color-50pct);
39
+ }
40
+
41
+ [part='toggle-button'] {
42
+ display: inline-flex;
43
+ align-items: center;
44
+ justify-content: center;
35
45
  width: var(--lumo-size-s);
36
46
  height: var(--lumo-size-s);
37
- transition: transform 140ms;
38
- margin: 0 var(--lumo-space-xs);
47
+ margin-inline-start: auto;
48
+ margin-inline-end: var(--lumo-space-xs);
49
+ font-size: var(--lumo-icon-size-m);
50
+ line-height: 1;
51
+ color: var(--lumo-contrast-60pct);
52
+ font-family: 'lumo-icons';
53
+ cursor: var(--lumo-clickable-cursor);
39
54
  }
40
55
 
41
- :host([collapsible]) summary::after {
42
- content: var(--lumo-icons-dropdown);
56
+ [part='toggle-button']::before {
57
+ content: var(--lumo-icons-angle-right);
43
58
  }
44
59
 
45
- @media (any-hover: hover) {
46
- summary:hover::after {
47
- color: var(--lumo-body-text-color);
48
- }
60
+ :host(:not([collapsible])) [part='toggle-button'] {
61
+ display: none !important;
49
62
  }
50
63
 
51
- :host([collapsed]) summary::after {
52
- transform: rotate(-90deg);
64
+ :host(:not([collapsed])) [part='toggle-button'] {
65
+ transform: rotate(90deg);
53
66
  }
54
67
 
55
- @supports selector(:focus-visible) {
56
- summary {
57
- outline: none;
58
- }
59
-
60
- summary:focus-visible {
61
- box-shadow: 0 0 0 2px var(--lumo-primary-color-50pct);
68
+ @media (any-hover: hover) {
69
+ [part='label']:hover [part='toggle-button'] {
70
+ color: var(--lumo-body-text-color);
62
71
  }
63
72
  }
64
73
  `;
@@ -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 './vaadin-side-nav-item.js';
@@ -0,0 +1,134 @@
1
+ import '@vaadin/vaadin-material-styles/color.js';
2
+ import '@vaadin/vaadin-material-styles/font-icons.js';
3
+ import '@vaadin/vaadin-material-styles/typography.js';
4
+ import { fieldButton } from '@vaadin/vaadin-material-styles/mixins/field-button.js';
5
+ import { css, registerStyles } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
6
+
7
+ export const sideNavItemStyles = css`
8
+ [part='link'] {
9
+ position: relative;
10
+ width: 100%;
11
+ min-height: 32px;
12
+ margin: 4px 0;
13
+ gap: 8px;
14
+ padding: 4px 8px;
15
+ padding-inline-start: calc(8px + var(--_child-indent, 0px));
16
+ font-family: var(--material-font-family);
17
+ font-size: var(--material-small-font-size);
18
+ line-height: 1;
19
+ font-weight: 500;
20
+ color: var(--material-body-text-color);
21
+ transition: background-color 140ms, color 140ms;
22
+ border-radius: 4px;
23
+ cursor: default;
24
+ }
25
+
26
+ [part='link'][href] {
27
+ cursor: pointer;
28
+ }
29
+
30
+ :host([current]) [part='link'] {
31
+ color: var(--material-primary-text-color);
32
+ }
33
+
34
+ :host([current]) [part='link']::before {
35
+ position: absolute;
36
+ content: '';
37
+ inset: 0;
38
+ background-color: var(--material-primary-color);
39
+ opacity: 0.12;
40
+ border-radius: 4px;
41
+ }
42
+
43
+ [part='toggle-button'] {
44
+ width: 32px;
45
+ height: 32px;
46
+ margin-inline-end: -4px;
47
+ transform: rotate(90deg);
48
+ }
49
+
50
+ [part='toggle-button']::before {
51
+ font-family: 'material-icons';
52
+ font-size: 24px;
53
+ width: 24px;
54
+ display: inline-block;
55
+ content: var(--material-icons-chevron-right);
56
+ }
57
+
58
+ [part='toggle-button']::after {
59
+ display: inline-block;
60
+ content: '';
61
+ position: absolute;
62
+ top: 0;
63
+ left: 0;
64
+ width: 100%;
65
+ height: 100%;
66
+ border-radius: 50%;
67
+ background-color: var(--material-disabled-text-color);
68
+ transform: scale(0);
69
+ opacity: 0;
70
+ transition: transform 0s 0.8s, opacity 0.8s;
71
+ will-change: transform, opacity;
72
+ }
73
+
74
+ [part='toggle-button']:focus-visible::after {
75
+ transition-duration: 0.08s, 0.01s;
76
+ transition-delay: 0s, 0s;
77
+ transform: scale(1.25);
78
+ opacity: 0.16;
79
+ }
80
+
81
+ :host([expanded]) [part='toggle-button'] {
82
+ transform: rotate(270deg);
83
+ }
84
+
85
+ :host([has-children]) [part='content'] {
86
+ padding-inline-end: 8px;
87
+ }
88
+
89
+ @media (any-hover: hover) {
90
+ :host(:not([current])) [part='link'][href]:hover {
91
+ background-color: var(--material-secondary-background-color);
92
+ }
93
+
94
+ [part='toggle-button']:hover {
95
+ color: var(--material-body-text-color);
96
+ }
97
+ }
98
+
99
+ @supports selector(:focus-visible) {
100
+ [part='link'],
101
+ [part='toggle-button'] {
102
+ outline: none;
103
+ }
104
+
105
+ :host(:not([current])) [part='link']:focus-visible {
106
+ background-color: var(--material-divider-color);
107
+ }
108
+
109
+ :host([current]) [part='link']:focus-visible::before {
110
+ opacity: 0.24;
111
+ }
112
+ }
113
+
114
+ slot[name='prefix']::slotted(:is(vaadin-icon, [class*='icon'])) {
115
+ padding: 0.1em;
116
+ flex-shrink: 0;
117
+ margin-inline-end: 24px;
118
+ color: var(--material-secondary-text-color);
119
+ }
120
+
121
+ :host([current]) slot[name='prefix']::slotted(:is(vaadin-icon, [class*='icon'])) {
122
+ color: inherit;
123
+ }
124
+
125
+ slot[name='children'] {
126
+ --_child-indent: calc(var(--_child-indent-2, 0px) + var(--vaadin-side-nav-child-indent, 24px));
127
+ }
128
+
129
+ slot[name='children']::slotted(*) {
130
+ --_child-indent-2: var(--_child-indent);
131
+ }
132
+ `;
133
+
134
+ registerStyles('vaadin-side-nav-item', [fieldButton, sideNavItemStyles], { moduleId: 'material-side-nav-item' });
@@ -0,0 +1,7 @@
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 './vaadin-side-nav-item-styles.js';
7
+ import '../../src/vaadin-side-nav-item.js';
@@ -0,0 +1,69 @@
1
+ import '@vaadin/vaadin-material-styles/color.js';
2
+ import '@vaadin/vaadin-material-styles/font-icons.js';
3
+ import '@vaadin/vaadin-material-styles/typography.js';
4
+ import { css, registerStyles } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
5
+
6
+ export const sideNavStyles = css`
7
+ :host {
8
+ -webkit-tap-highlight-color: transparent;
9
+ outline: none;
10
+ }
11
+
12
+ [part='label'] {
13
+ display: flex;
14
+ align-items: center;
15
+ width: 100%;
16
+ min-height: 40px;
17
+ margin: 4px 0;
18
+ padding: 4px 8px;
19
+ outline: none;
20
+ box-sizing: border-box;
21
+ font-family: var(--material-font-family);
22
+ font-size: var(--material-small-font-size);
23
+ color: var(--material-secondary-text-color);
24
+ line-height: 1;
25
+ font-weight: 500;
26
+ border-radius: 4px;
27
+ }
28
+
29
+ :host([focus-ring]) [part='label'] {
30
+ background-color: var(--material-divider-color);
31
+ }
32
+
33
+ [part='toggle-button'] {
34
+ display: inline-flex;
35
+ align-items: center;
36
+ justify-content: center;
37
+ width: 24px;
38
+ height: 24px;
39
+ padding: 4px;
40
+ margin-inline-start: auto;
41
+ margin-inline-end: -4px;
42
+ font-size: var(--material-icon-font-size);
43
+ line-height: 1;
44
+ color: var(--material-secondary-text-color);
45
+ font-family: 'material-icons';
46
+ transform: rotate(90deg);
47
+ }
48
+
49
+ [part='toggle-button']::before {
50
+ content: var(--material-icons-chevron-right);
51
+ font-size: 24px;
52
+ }
53
+
54
+ :host(:not([collapsible])) [part='toggle-button'] {
55
+ display: none !important;
56
+ }
57
+
58
+ :host(:not([collapsed])) [part='toggle-button'] {
59
+ transform: rotate(270deg);
60
+ }
61
+
62
+ @media (any-hover: hover) {
63
+ [part='label']:hover [part='toggle-button'] {
64
+ color: var(--material-body-text-color);
65
+ }
66
+ }
67
+ `;
68
+
69
+ registerStyles('vaadin-side-nav', sideNavStyles, { moduleId: 'material-side-nav' });
@@ -1,7 +1,8 @@
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
+ import './vaadin-side-nav-item.js';
7
+ import './vaadin-side-nav-styles.js';
6
8
  import '../../src/vaadin-side-nav.js';
7
- import '../../src/vaadin-side-nav-item.js';