@vaadin/app-layout 23.0.0-alpha1 → 23.0.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vaadin/app-layout",
3
- "version": "23.0.0-alpha1",
3
+ "version": "23.0.0-alpha2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -34,17 +34,17 @@
34
34
  "dependencies": {
35
35
  "@polymer/iron-resizable-behavior": "^3.0.0",
36
36
  "@polymer/polymer": "^3.0.0",
37
- "@vaadin/button": "23.0.0-alpha1",
38
- "@vaadin/component-base": "23.0.0-alpha1",
39
- "@vaadin/vaadin-lumo-styles": "23.0.0-alpha1",
40
- "@vaadin/vaadin-material-styles": "23.0.0-alpha1",
41
- "@vaadin/vaadin-themable-mixin": "23.0.0-alpha1"
37
+ "@vaadin/button": "23.0.0-alpha2",
38
+ "@vaadin/component-base": "23.0.0-alpha2",
39
+ "@vaadin/vaadin-lumo-styles": "23.0.0-alpha2",
40
+ "@vaadin/vaadin-material-styles": "23.0.0-alpha2",
41
+ "@vaadin/vaadin-themable-mixin": "23.0.0-alpha2"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@esm-bundle/chai": "^4.3.4",
45
- "@vaadin/tabs": "23.0.0-alpha1",
45
+ "@vaadin/tabs": "23.0.0-alpha2",
46
46
  "@vaadin/testing-helpers": "^0.3.2",
47
47
  "sinon": "^9.2.1"
48
48
  },
49
- "gitHead": "fbcb07328fdf88260e3b461088d207426b21c710"
49
+ "gitHead": "070f586dead02ca41b66717820c647f48bf1665f"
50
50
  }
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { isIOS } from '@vaadin/component-base/src/browser-utils.js';
7
7
 
8
- export const _detectIosNavbar = function () {
8
+ export function _detectIosNavbar() {
9
9
  /* c8 ignore next 11 */
10
10
  if (isIOS) {
11
11
  const innerHeight = window.innerHeight;
@@ -18,7 +18,7 @@ export const _detectIosNavbar = function () {
18
18
  document.documentElement.style.setProperty('--vaadin-viewport-offset-bottom', '');
19
19
  }
20
20
  }
21
- };
21
+ }
22
22
 
23
23
  _detectIosNavbar();
24
24
  window.addEventListener('resize', _detectIosNavbar);
@@ -4,19 +4,10 @@ $_documentContainer.innerHTML = `
4
4
  <style>
5
5
  /* Use units so that the values can be used in calc() */
6
6
  html {
7
- --safe-area-inset-top: constant(safe-area-inset-top, 0px);
8
- --safe-area-inset-right: constant(safe-area-inset-right, 0px);
9
- --safe-area-inset-bottom: constant(safe-area-inset-bottom, 0px);
10
- --safe-area-inset-left: constant(safe-area-inset-left, 0px);
11
- }
12
-
13
- @supports (padding-left: env(safe-area-inset-left)) {
14
- html {
15
- --safe-area-inset-top: env(safe-area-inset-top, 0px);
16
- --safe-area-inset-right: env(safe-area-inset-right, 0px);
17
- --safe-area-inset-bottom: env(safe-area-inset-bottom, 0px);
18
- --safe-area-inset-left: env(safe-area-inset-left, 0px);
19
- }
7
+ --safe-area-inset-top: env(safe-area-inset-top, 0px);
8
+ --safe-area-inset-right: env(safe-area-inset-right, 0px);
9
+ --safe-area-inset-bottom: env(safe-area-inset-bottom, 0px);
10
+ --safe-area-inset-left: env(safe-area-inset-left, 0px);
20
11
  }
21
12
  </style>
22
13
  `;
@@ -3,9 +3,14 @@
3
3
  * Copyright (c) 2021 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
+ import { ControllerMixin } from '@vaadin/component-base/src/controller-mixin.js';
6
7
  import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
7
8
  import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
8
9
 
10
+ export interface AppLayoutI18n {
11
+ drawer: string;
12
+ }
13
+
9
14
  /**
10
15
  * Fired when the `drawerOpened` property changes.
11
16
  */
@@ -72,7 +77,6 @@ export type AppLayoutEventMap = HTMLElementEventMap & AppLayoutCustomEventMap;
72
77
  * --------------|---------------------------------------------------------|
73
78
  * `navbar` | Container for the navigation bar
74
79
  * `drawer` | Container for the drawer area
75
- * `content` | Container for page content.
76
80
  *
77
81
  * See [Styling Components](https://vaadin.com/docs/latest/ds/customization/styling-components) documentation.
78
82
  *
@@ -119,7 +123,29 @@ export type AppLayoutEventMap = HTMLElementEventMap & AppLayoutCustomEventMap;
119
123
  * @fires {CustomEvent} overlay-changed - Fired when the `overlay` property changes.
120
124
  * @fires {CustomEvent} primary-section-changed - Fired when the `primarySection` property changes.
121
125
  */
122
- declare class AppLayout extends ElementMixin(ThemableMixin(HTMLElement)) {
126
+ declare class AppLayout extends ElementMixin(ThemableMixin(ControllerMixin(HTMLElement))) {
127
+ /**
128
+ * The object used to localize this component.
129
+ * To change the default localization, replace the entire
130
+ * `i18n` object with a custom one.
131
+ *
132
+ * To update individual properties, extend the existing i18n object as follows:
133
+ * ```js
134
+ * appLayout.i18n = {
135
+ * ...appLayout.i18n,
136
+ * drawer: 'Drawer'
137
+ * }
138
+ * ```
139
+ *
140
+ * The object has the following structure and default values:
141
+ * ```
142
+ * {
143
+ * drawer: 'Drawer'
144
+ * }
145
+ * ```
146
+ */
147
+ i18n: AppLayoutI18n;
148
+
123
149
  /**
124
150
  * Defines whether navbar or drawer will come first visually.
125
151
  * - By default (`primary-section="navbar"`), the navbar takes the full available width and moves the drawer down.
@@ -10,9 +10,15 @@ import { mixinBehaviors } from '@polymer/polymer/lib/legacy/class.js';
10
10
  import { FlattenedNodesObserver } from '@polymer/polymer/lib/utils/flattened-nodes-observer.js';
11
11
  import { afterNextRender, beforeNextRender } from '@polymer/polymer/lib/utils/render-status.js';
12
12
  import { html, PolymerElement } from '@polymer/polymer/polymer-element.js';
13
+ import { ControllerMixin } from '@vaadin/component-base/src/controller-mixin.js';
13
14
  import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
15
+ import { FocusTrapController } from '@vaadin/component-base/src/focus-trap-controller.js';
14
16
  import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
15
17
 
18
+ /**
19
+ * @typedef {import('./vaadin-app-layout.js').AppLayoutI18n} AppLayoutI18n
20
+ */
21
+
16
22
  /**
17
23
  * `<vaadin-app-layout>` is a Web Component providing a quick and easy way to get a common application layout structure done.
18
24
  *
@@ -54,7 +60,6 @@ import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mix
54
60
  * --------------|---------------------------------------------------------|
55
61
  * `navbar` | Container for the navigation bar
56
62
  * `drawer` | Container for the drawer area
57
- * `content` | Container for page content.
58
63
  *
59
64
  * See [Styling Components](https://vaadin.com/docs/latest/ds/customization/styling-components) documentation.
60
65
  *
@@ -104,8 +109,11 @@ import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mix
104
109
  * @extends HTMLElement
105
110
  * @mixes ElementMixin
106
111
  * @mixes ThemableMixin
112
+ * @mixes ControllerMixin
107
113
  */
108
- class AppLayout extends ElementMixin(ThemableMixin(mixinBehaviors([IronResizableBehavior], PolymerElement))) {
114
+ class AppLayout extends ElementMixin(
115
+ ThemableMixin(ControllerMixin(mixinBehaviors([IronResizableBehavior], PolymerElement)))
116
+ ) {
109
117
  static get template() {
110
118
  return html`
111
119
  <style>
@@ -148,7 +156,6 @@ class AppLayout extends ElementMixin(ThemableMixin(mixinBehaviors([IronResizable
148
156
 
149
157
  :host(:not([no-scroll])) [content] {
150
158
  overflow: auto;
151
- -webkit-overflow-scrolling: touch;
152
159
  }
153
160
 
154
161
  [content] {
@@ -201,16 +208,20 @@ class AppLayout extends ElementMixin(ThemableMixin(mixinBehaviors([IronResizable
201
208
  right: auto;
202
209
  bottom: var(--vaadin-app-layout-navbar-offset-bottom, var(--vaadin-viewport-offset-bottom, 0));
203
210
  left: var(--vaadin-app-layout-navbar-offset-left, 0);
204
- transition: transform var(--vaadin-app-layout-transition);
211
+ transition: transform var(--vaadin-app-layout-transition), visibility var(--vaadin-app-layout-transition);
205
212
  transform: translateX(-100%);
206
213
  max-width: 90%;
207
214
  width: 16em;
208
215
  box-sizing: border-box;
209
216
  padding: var(--safe-area-inset-top) 0 var(--safe-area-inset-bottom) var(--safe-area-inset-left);
210
217
  outline: none;
218
+ /* The drawer should be inaccessible by the tabbing navigation when it is closed. */
219
+ visibility: hidden;
211
220
  }
212
221
 
213
222
  :host([drawer-opened]) [part='drawer'] {
223
+ /* The drawer should be accessible by the tabbing navigation when it is opened. */
224
+ visibility: visible;
214
225
  transform: translateX(0%);
215
226
  touch-action: manipulation;
216
227
  }
@@ -287,7 +298,7 @@ class AppLayout extends ElementMixin(ThemableMixin(mixinBehaviors([IronResizable
287
298
  <slot name="navbar"></slot>
288
299
  </div>
289
300
  <div part="backdrop" on-click="_close" on-touchstart="_close"></div>
290
- <div part="drawer" id="drawer">
301
+ <div part="drawer" id="drawer" on-keydown="__onDrawerKeyDown">
291
302
  <slot name="drawer" id="drawerSlot"></slot>
292
303
  </div>
293
304
  <div content>
@@ -306,6 +317,39 @@ class AppLayout extends ElementMixin(ThemableMixin(mixinBehaviors([IronResizable
306
317
 
307
318
  static get properties() {
308
319
  return {
320
+ /**
321
+ * The object used to localize this component.
322
+ * To change the default localization, replace the entire
323
+ * `i18n` object with a custom one.
324
+ *
325
+ * To update individual properties, extend the existing i18n object as follows:
326
+ * ```js
327
+ * appLayout.i18n = {
328
+ * ...appLayout.i18n,
329
+ * drawer: 'Drawer'
330
+ * }
331
+ * ```
332
+ *
333
+ * The object has the following structure and default values:
334
+ * ```
335
+ * {
336
+ * drawer: 'Drawer'
337
+ * }
338
+ * ```
339
+ *
340
+ * @type {AppLayoutI18n}
341
+ * @default {English/US}
342
+ */
343
+ i18n: {
344
+ type: Object,
345
+ observer: '__i18nChanged',
346
+ value: () => {
347
+ return {
348
+ drawer: 'Drawer'
349
+ };
350
+ }
351
+ },
352
+
309
353
  /**
310
354
  * Defines whether navbar or drawer will come first visually.
311
355
  * - By default (`primary-section="navbar"`), the navbar takes the full available width and moves the drawer down.
@@ -318,7 +362,7 @@ class AppLayout extends ElementMixin(ThemableMixin(mixinBehaviors([IronResizable
318
362
  value: 'navbar',
319
363
  notify: true,
320
364
  reflectToAttribute: true,
321
- observer: '_primarySectionObserver'
365
+ observer: '__primarySectionChanged'
322
366
  },
323
367
 
324
368
  /**
@@ -334,7 +378,7 @@ class AppLayout extends ElementMixin(ThemableMixin(mixinBehaviors([IronResizable
334
378
  notify: true,
335
379
  value: true,
336
380
  reflectToAttribute: true,
337
- observer: '_drawerOpenedObserver'
381
+ observer: '__drawerOpenedChanged'
338
382
  },
339
383
 
340
384
  /**
@@ -371,6 +415,9 @@ class AppLayout extends ElementMixin(ThemableMixin(mixinBehaviors([IronResizable
371
415
  this.__boundResizeListener = this._resize.bind(this);
372
416
  this.__drawerToggleClickListener = this._drawerToggleClick.bind(this);
373
417
  this.__closeOverlayDrawerListener = this.__closeOverlayDrawer.bind(this);
418
+ this.__trapFocusInDrawer = this.__trapFocusInDrawer.bind(this);
419
+ this.__releaseFocusFromDrawer = this.__releaseFocusFromDrawer.bind(this);
420
+ this.__focusTrapController = new FocusTrapController(this);
374
421
  }
375
422
 
376
423
  /** @protected */
@@ -404,6 +451,12 @@ class AppLayout extends ElementMixin(ThemableMixin(mixinBehaviors([IronResizable
404
451
  window.addEventListener('close-overlay-drawer', this.__closeOverlayDrawerListener);
405
452
  }
406
453
 
454
+ /** @protected */
455
+ ready() {
456
+ super.ready();
457
+ this.addController(this.__focusTrapController);
458
+ }
459
+
407
460
  /** @protected */
408
461
  disconnectedCallback() {
409
462
  super.disconnectedCallback();
@@ -423,31 +476,59 @@ class AppLayout extends ElementMixin(ThemableMixin(mixinBehaviors([IronResizable
423
476
  window.dispatchEvent(new CustomEvent('close-overlay-drawer'));
424
477
  }
425
478
 
426
- /** @private */
427
- _primarySectionObserver(value) {
428
- const isValid = ['navbar', 'drawer'].indexOf(value) !== -1;
479
+ /**
480
+ * A callback for the `primarySection` property observer.
481
+ *
482
+ * Ensures the property is set to its default value `navbar`
483
+ * whenever the new value is not one of the valid values: `navbar`, `drawer`.
484
+ *
485
+ * @param {string} value
486
+ * @private
487
+ */
488
+ __primarySectionChanged(value) {
489
+ const isValid = ['navbar', 'drawer'].includes(value);
429
490
  if (!isValid) {
430
491
  this.set('primarySection', 'navbar');
431
492
  }
432
493
  }
433
494
 
434
- /** @private */
435
- _drawerOpenedObserver() {
436
- const drawer = this.$.drawer;
437
-
438
- drawer.removeAttribute('tabindex');
439
-
495
+ /**
496
+ * A callback for the `drawerOpened` property observer.
497
+ *
498
+ * When the drawer opens, the method ensures the drawer has a proper height and sets focus on it.
499
+ * As long as the drawer is open, the focus is trapped within the drawer.
500
+ *
501
+ * When the drawer closes, the method releases focus from the drawer, setting focus on the drawer toggle.
502
+ *
503
+ * @param {boolean} drawerOpened
504
+ * @param {boolean} oldDrawerOpened
505
+ * @private
506
+ */
507
+ __drawerOpenedChanged(drawerOpened, oldDrawerOpened) {
440
508
  if (this.overlay) {
441
- if (this.drawerOpened) {
442
- drawer.setAttribute('tabindex', 0);
443
- drawer.focus();
509
+ if (drawerOpened) {
444
510
  this._updateDrawerHeight();
511
+ this.__trapFocusInDrawer();
512
+ } else if (oldDrawerOpened) {
513
+ this.__releaseFocusFromDrawer();
445
514
  }
446
515
  }
447
516
 
448
517
  this.notifyResize();
449
518
  }
450
519
 
520
+ /**
521
+ * A callback for the `i18n` property observer.
522
+ *
523
+ * The method ensures the drawer has ARIA attributes updated
524
+ * once the `i18n` property changes.
525
+ *
526
+ * @private
527
+ */
528
+ __i18nChanged() {
529
+ this.__updateDrawerAriaAttributes();
530
+ }
531
+
451
532
  /** @protected */
452
533
  _afterFirstRender() {
453
534
  this._blockAnimationUntilAfterNextRender();
@@ -515,8 +596,7 @@ class AppLayout extends ElementMixin(ThemableMixin(mixinBehaviors([IronResizable
515
596
 
516
597
  /** @protected */
517
598
  _updateOverlayMode() {
518
- const overlay = this._getCustomPropertyValue('--vaadin-app-layout-drawer-overlay') == 'true';
519
- const drawer = this.$.drawer;
599
+ const overlay = this._getCustomPropertyValue('--vaadin-app-layout-drawer-overlay') === 'true';
520
600
 
521
601
  if (!this.overlay && overlay) {
522
602
  // Changed from not overlay to overlay
@@ -526,28 +606,109 @@ class AppLayout extends ElementMixin(ThemableMixin(mixinBehaviors([IronResizable
526
606
 
527
607
  this._setOverlay(overlay);
528
608
 
609
+ if (!this.overlay && this._drawerStateSaved) {
610
+ this.drawerOpened = this._drawerStateSaved;
611
+ this._drawerStateSaved = null;
612
+ }
613
+
614
+ this._updateDrawerHeight();
615
+ this.__updateDrawerAriaAttributes();
616
+
617
+ if (this.overlay !== overlay) {
618
+ this.notifyResize();
619
+ }
620
+ }
621
+
622
+ /**
623
+ * Updates ARIA attributes on the drawer depending on the drawer mode.
624
+ *
625
+ * - In the overlay mode, the method marks the drawer with ARIA attributes as a dialog
626
+ * labelled with the `i18n.drawer` property.
627
+ * - In the normal mode, the method removes the ARIA attributes that has been set for the overlay mode.
628
+ *
629
+ * @private
630
+ */
631
+ __updateDrawerAriaAttributes() {
632
+ const drawer = this.$.drawer;
529
633
  if (this.overlay) {
530
634
  drawer.setAttribute('role', 'dialog');
531
635
  drawer.setAttribute('aria-modal', 'true');
532
- drawer.setAttribute('aria-label', 'drawer');
636
+ drawer.setAttribute('aria-label', this.i18n.drawer);
533
637
  } else {
534
- if (this._drawerStateSaved) {
535
- this.drawerOpened = this._drawerStateSaved;
536
- this._drawerStateSaved = null;
537
- }
538
-
539
638
  drawer.removeAttribute('role');
540
639
  drawer.removeAttribute('aria-modal');
541
640
  drawer.removeAttribute('aria-label');
542
641
  }
642
+ }
543
643
 
544
- this._updateDrawerHeight();
644
+ /**
645
+ * Returns a promise that resolves when the drawer opening/closing CSS transition ends.
646
+ *
647
+ * The method relies on the `--vaadin-app-layout-transition` CSS variable to detect whether
648
+ * the drawer has a CSS transition that needs to be awaited. If the CSS variable equals `none`,
649
+ * the promise resolves immediately.
650
+ *
651
+ * @return {Promise}
652
+ * @private
653
+ */
654
+ __drawerTransitionComplete() {
655
+ return new Promise((resolve) => {
656
+ if (this._getCustomPropertyValue('--vaadin-app-layout-transition') === 'none') {
657
+ resolve();
658
+ return;
659
+ }
545
660
 
546
- if (this.overlay !== overlay) {
547
- this.notifyResize();
661
+ this.$.drawer.addEventListener('transitionend', resolve, { once: true });
662
+ });
663
+ }
664
+
665
+ /** @private */
666
+ async __trapFocusInDrawer() {
667
+ // Wait for the drawer CSS transition before focusing the drawer
668
+ // in order for VoiceOver to have a proper outline.
669
+ await this.__drawerTransitionComplete();
670
+
671
+ if (!this.drawerOpened) {
672
+ // The drawer has been closed during the CSS transition.
673
+ return;
674
+ }
675
+
676
+ this.$.drawer.setAttribute('tabindex', '0');
677
+ this.__focusTrapController.trapFocus(this.$.drawer);
678
+ }
679
+
680
+ /** @private */
681
+ async __releaseFocusFromDrawer() {
682
+ // Wait for the drawer CSS transition in order to restore focus to the toggle
683
+ // only after `visibility` becomes `hidden`, that is, the drawer becomes inaccessible by the tabbing navigation.
684
+ await this.__drawerTransitionComplete();
685
+
686
+ if (this.drawerOpened) {
687
+ // The drawer has been opened during the CSS transition.
688
+ return;
548
689
  }
549
690
 
550
- // TODO(jouni): ARIA attributes. The drawer should act similar to a modal dialog when in ”overlay” mode
691
+ this.__focusTrapController.releaseFocus();
692
+ this.$.drawer.removeAttribute('tabindex');
693
+
694
+ // Move focus to the drawer toggle when closing the drawer.
695
+ const toggle = this.querySelector('vaadin-drawer-toggle');
696
+ if (toggle) {
697
+ toggle.focus();
698
+ toggle.setAttribute('focus-ring', 'focus');
699
+ }
700
+ }
701
+
702
+ /**
703
+ * Closes the drawer on Escape press if it has been opened in the overlay mode.
704
+ *
705
+ * @param {KeyboardEvent} event
706
+ * @private
707
+ */
708
+ __onDrawerKeyDown(event) {
709
+ if (event.key === 'Escape' && this.overlay) {
710
+ this.drawerOpened = false;
711
+ }
551
712
  }
552
713
 
553
714
  /** @private */