@vaadin/app-layout 23.0.0-alpha1 → 23.0.0-alpha5

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-alpha5",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -18,6 +18,7 @@
18
18
  },
19
19
  "main": "vaadin-app-layout.js",
20
20
  "module": "vaadin-app-layout.js",
21
+ "type": "module",
21
22
  "files": [
22
23
  "src",
23
24
  "theme",
@@ -32,19 +33,18 @@
32
33
  "polymer"
33
34
  ],
34
35
  "dependencies": {
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-alpha5",
38
+ "@vaadin/component-base": "23.0.0-alpha5",
39
+ "@vaadin/vaadin-lumo-styles": "23.0.0-alpha5",
40
+ "@vaadin/vaadin-material-styles": "23.0.0-alpha5",
41
+ "@vaadin/vaadin-themable-mixin": "23.0.0-alpha5"
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-alpha5",
46
46
  "@vaadin/testing-helpers": "^0.3.2",
47
47
  "sinon": "^9.2.1"
48
48
  },
49
- "gitHead": "fbcb07328fdf88260e3b461088d207426b21c710"
49
+ "gitHead": "74f9294964eb8552d96578c14af6ad214f5257bc"
50
50
  }
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * @license
3
- * Copyright (c) 2021 Vaadin Ltd.
3
+ * Copyright (c) 2018 - 2022 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 { 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
  `;
@@ -1,11 +1,16 @@
1
1
  /**
2
2
  * @license
3
- * Copyright (c) 2021 Vaadin Ltd.
3
+ * Copyright (c) 2018 - 2022 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.
@@ -1,18 +1,22 @@
1
1
  /**
2
2
  * @license
3
- * Copyright (c) 2021 Vaadin Ltd.
3
+ * Copyright (c) 2018 - 2022 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 './safe-area-inset.js';
7
7
  import './detect-ios-navbar.js';
8
- import { IronResizableBehavior } from '@polymer/iron-resizable-behavior/iron-resizable-behavior.js';
9
- import { mixinBehaviors } from '@polymer/polymer/lib/legacy/class.js';
10
8
  import { FlattenedNodesObserver } from '@polymer/polymer/lib/utils/flattened-nodes-observer.js';
11
9
  import { afterNextRender, beforeNextRender } from '@polymer/polymer/lib/utils/render-status.js';
12
10
  import { html, PolymerElement } from '@polymer/polymer/polymer-element.js';
11
+ import { ControllerMixin } from '@vaadin/component-base/src/controller-mixin.js';
13
12
  import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
13
+ import { FocusTrapController } from '@vaadin/component-base/src/focus-trap-controller.js';
14
14
  import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
15
15
 
16
+ /**
17
+ * @typedef {import('./vaadin-app-layout.js').AppLayoutI18n} AppLayoutI18n
18
+ */
19
+
16
20
  /**
17
21
  * `<vaadin-app-layout>` is a Web Component providing a quick and easy way to get a common application layout structure done.
18
22
  *
@@ -54,7 +58,6 @@ import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mix
54
58
  * --------------|---------------------------------------------------------|
55
59
  * `navbar` | Container for the navigation bar
56
60
  * `drawer` | Container for the drawer area
57
- * `content` | Container for page content.
58
61
  *
59
62
  * See [Styling Components](https://vaadin.com/docs/latest/ds/customization/styling-components) documentation.
60
63
  *
@@ -104,8 +107,9 @@ import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mix
104
107
  * @extends HTMLElement
105
108
  * @mixes ElementMixin
106
109
  * @mixes ThemableMixin
110
+ * @mixes ControllerMixin
107
111
  */
108
- class AppLayout extends ElementMixin(ThemableMixin(mixinBehaviors([IronResizableBehavior], PolymerElement))) {
112
+ class AppLayout extends ElementMixin(ThemableMixin(ControllerMixin(PolymerElement))) {
109
113
  static get template() {
110
114
  return html`
111
115
  <style>
@@ -148,7 +152,6 @@ class AppLayout extends ElementMixin(ThemableMixin(mixinBehaviors([IronResizable
148
152
 
149
153
  :host(:not([no-scroll])) [content] {
150
154
  overflow: auto;
151
- -webkit-overflow-scrolling: touch;
152
155
  }
153
156
 
154
157
  [content] {
@@ -201,16 +204,20 @@ class AppLayout extends ElementMixin(ThemableMixin(mixinBehaviors([IronResizable
201
204
  right: auto;
202
205
  bottom: var(--vaadin-app-layout-navbar-offset-bottom, var(--vaadin-viewport-offset-bottom, 0));
203
206
  left: var(--vaadin-app-layout-navbar-offset-left, 0);
204
- transition: transform var(--vaadin-app-layout-transition);
207
+ transition: transform var(--vaadin-app-layout-transition), visibility var(--vaadin-app-layout-transition);
205
208
  transform: translateX(-100%);
206
209
  max-width: 90%;
207
210
  width: 16em;
208
211
  box-sizing: border-box;
209
212
  padding: var(--safe-area-inset-top) 0 var(--safe-area-inset-bottom) var(--safe-area-inset-left);
210
213
  outline: none;
214
+ /* The drawer should be inaccessible by the tabbing navigation when it is closed. */
215
+ visibility: hidden;
211
216
  }
212
217
 
213
218
  :host([drawer-opened]) [part='drawer'] {
219
+ /* The drawer should be accessible by the tabbing navigation when it is opened. */
220
+ visibility: visible;
214
221
  transform: translateX(0%);
215
222
  touch-action: manipulation;
216
223
  }
@@ -287,7 +294,7 @@ class AppLayout extends ElementMixin(ThemableMixin(mixinBehaviors([IronResizable
287
294
  <slot name="navbar"></slot>
288
295
  </div>
289
296
  <div part="backdrop" on-click="_close" on-touchstart="_close"></div>
290
- <div part="drawer" id="drawer">
297
+ <div part="drawer" id="drawer" on-keydown="__onDrawerKeyDown">
291
298
  <slot name="drawer" id="drawerSlot"></slot>
292
299
  </div>
293
300
  <div content>
@@ -306,6 +313,39 @@ class AppLayout extends ElementMixin(ThemableMixin(mixinBehaviors([IronResizable
306
313
 
307
314
  static get properties() {
308
315
  return {
316
+ /**
317
+ * The object used to localize this component.
318
+ * To change the default localization, replace the entire
319
+ * `i18n` object with a custom one.
320
+ *
321
+ * To update individual properties, extend the existing i18n object as follows:
322
+ * ```js
323
+ * appLayout.i18n = {
324
+ * ...appLayout.i18n,
325
+ * drawer: 'Drawer'
326
+ * }
327
+ * ```
328
+ *
329
+ * The object has the following structure and default values:
330
+ * ```
331
+ * {
332
+ * drawer: 'Drawer'
333
+ * }
334
+ * ```
335
+ *
336
+ * @type {AppLayoutI18n}
337
+ * @default {English/US}
338
+ */
339
+ i18n: {
340
+ type: Object,
341
+ observer: '__i18nChanged',
342
+ value: () => {
343
+ return {
344
+ drawer: 'Drawer'
345
+ };
346
+ }
347
+ },
348
+
309
349
  /**
310
350
  * Defines whether navbar or drawer will come first visually.
311
351
  * - By default (`primary-section="navbar"`), the navbar takes the full available width and moves the drawer down.
@@ -318,7 +358,7 @@ class AppLayout extends ElementMixin(ThemableMixin(mixinBehaviors([IronResizable
318
358
  value: 'navbar',
319
359
  notify: true,
320
360
  reflectToAttribute: true,
321
- observer: '_primarySectionObserver'
361
+ observer: '__primarySectionChanged'
322
362
  },
323
363
 
324
364
  /**
@@ -334,7 +374,7 @@ class AppLayout extends ElementMixin(ThemableMixin(mixinBehaviors([IronResizable
334
374
  notify: true,
335
375
  value: true,
336
376
  reflectToAttribute: true,
337
- observer: '_drawerOpenedObserver'
377
+ observer: '__drawerOpenedChanged'
338
378
  },
339
379
 
340
380
  /**
@@ -371,6 +411,9 @@ class AppLayout extends ElementMixin(ThemableMixin(mixinBehaviors([IronResizable
371
411
  this.__boundResizeListener = this._resize.bind(this);
372
412
  this.__drawerToggleClickListener = this._drawerToggleClick.bind(this);
373
413
  this.__closeOverlayDrawerListener = this.__closeOverlayDrawer.bind(this);
414
+ this.__trapFocusInDrawer = this.__trapFocusInDrawer.bind(this);
415
+ this.__releaseFocusFromDrawer = this.__releaseFocusFromDrawer.bind(this);
416
+ this.__focusTrapController = new FocusTrapController(this);
374
417
  }
375
418
 
376
419
  /** @protected */
@@ -404,6 +447,12 @@ class AppLayout extends ElementMixin(ThemableMixin(mixinBehaviors([IronResizable
404
447
  window.addEventListener('close-overlay-drawer', this.__closeOverlayDrawerListener);
405
448
  }
406
449
 
450
+ /** @protected */
451
+ ready() {
452
+ super.ready();
453
+ this.addController(this.__focusTrapController);
454
+ }
455
+
407
456
  /** @protected */
408
457
  disconnectedCallback() {
409
458
  super.disconnectedCallback();
@@ -423,29 +472,55 @@ class AppLayout extends ElementMixin(ThemableMixin(mixinBehaviors([IronResizable
423
472
  window.dispatchEvent(new CustomEvent('close-overlay-drawer'));
424
473
  }
425
474
 
426
- /** @private */
427
- _primarySectionObserver(value) {
428
- const isValid = ['navbar', 'drawer'].indexOf(value) !== -1;
475
+ /**
476
+ * A callback for the `primarySection` property observer.
477
+ *
478
+ * Ensures the property is set to its default value `navbar`
479
+ * whenever the new value is not one of the valid values: `navbar`, `drawer`.
480
+ *
481
+ * @param {string} value
482
+ * @private
483
+ */
484
+ __primarySectionChanged(value) {
485
+ const isValid = ['navbar', 'drawer'].includes(value);
429
486
  if (!isValid) {
430
487
  this.set('primarySection', 'navbar');
431
488
  }
432
489
  }
433
490
 
434
- /** @private */
435
- _drawerOpenedObserver() {
436
- const drawer = this.$.drawer;
437
-
438
- drawer.removeAttribute('tabindex');
439
-
491
+ /**
492
+ * A callback for the `drawerOpened` property observer.
493
+ *
494
+ * When the drawer opens, the method ensures the drawer has a proper height and sets focus on it.
495
+ * As long as the drawer is open, the focus is trapped within the drawer.
496
+ *
497
+ * When the drawer closes, the method releases focus from the drawer, setting focus on the drawer toggle.
498
+ *
499
+ * @param {boolean} drawerOpened
500
+ * @param {boolean} oldDrawerOpened
501
+ * @private
502
+ */
503
+ __drawerOpenedChanged(drawerOpened, oldDrawerOpened) {
440
504
  if (this.overlay) {
441
- if (this.drawerOpened) {
442
- drawer.setAttribute('tabindex', 0);
443
- drawer.focus();
505
+ if (drawerOpened) {
444
506
  this._updateDrawerHeight();
507
+ this.__trapFocusInDrawer();
508
+ } else if (oldDrawerOpened) {
509
+ this.__releaseFocusFromDrawer();
445
510
  }
446
511
  }
512
+ }
447
513
 
448
- this.notifyResize();
514
+ /**
515
+ * A callback for the `i18n` property observer.
516
+ *
517
+ * The method ensures the drawer has ARIA attributes updated
518
+ * once the `i18n` property changes.
519
+ *
520
+ * @private
521
+ */
522
+ __i18nChanged() {
523
+ this.__updateDrawerAriaAttributes();
449
524
  }
450
525
 
451
526
  /** @protected */
@@ -502,8 +577,6 @@ class AppLayout extends ElementMixin(ThemableMixin(mixinBehaviors([IronResizable
502
577
  const drawerRect = drawer.getBoundingClientRect();
503
578
 
504
579
  this.style.setProperty('--_vaadin-app-layout-drawer-offset-size', drawerRect.width + 'px');
505
-
506
- this.notifyResize();
507
580
  }
508
581
 
509
582
  /** @protected */
@@ -515,8 +588,7 @@ class AppLayout extends ElementMixin(ThemableMixin(mixinBehaviors([IronResizable
515
588
 
516
589
  /** @protected */
517
590
  _updateOverlayMode() {
518
- const overlay = this._getCustomPropertyValue('--vaadin-app-layout-drawer-overlay') == 'true';
519
- const drawer = this.$.drawer;
591
+ const overlay = this._getCustomPropertyValue('--vaadin-app-layout-drawer-overlay') === 'true';
520
592
 
521
593
  if (!this.overlay && overlay) {
522
594
  // Changed from not overlay to overlay
@@ -526,28 +598,105 @@ class AppLayout extends ElementMixin(ThemableMixin(mixinBehaviors([IronResizable
526
598
 
527
599
  this._setOverlay(overlay);
528
600
 
601
+ if (!this.overlay && this._drawerStateSaved) {
602
+ this.drawerOpened = this._drawerStateSaved;
603
+ this._drawerStateSaved = null;
604
+ }
605
+
606
+ this._updateDrawerHeight();
607
+ this.__updateDrawerAriaAttributes();
608
+ }
609
+
610
+ /**
611
+ * Updates ARIA attributes on the drawer depending on the drawer mode.
612
+ *
613
+ * - In the overlay mode, the method marks the drawer with ARIA attributes as a dialog
614
+ * labelled with the `i18n.drawer` property.
615
+ * - In the normal mode, the method removes the ARIA attributes that has been set for the overlay mode.
616
+ *
617
+ * @private
618
+ */
619
+ __updateDrawerAriaAttributes() {
620
+ const drawer = this.$.drawer;
529
621
  if (this.overlay) {
530
622
  drawer.setAttribute('role', 'dialog');
531
623
  drawer.setAttribute('aria-modal', 'true');
532
- drawer.setAttribute('aria-label', 'drawer');
624
+ drawer.setAttribute('aria-label', this.i18n.drawer);
533
625
  } else {
534
- if (this._drawerStateSaved) {
535
- this.drawerOpened = this._drawerStateSaved;
536
- this._drawerStateSaved = null;
537
- }
538
-
539
626
  drawer.removeAttribute('role');
540
627
  drawer.removeAttribute('aria-modal');
541
628
  drawer.removeAttribute('aria-label');
542
629
  }
630
+ }
543
631
 
544
- this._updateDrawerHeight();
632
+ /**
633
+ * Returns a promise that resolves when the drawer opening/closing CSS transition ends.
634
+ *
635
+ * The method relies on the `--vaadin-app-layout-transition` CSS variable to detect whether
636
+ * the drawer has a CSS transition that needs to be awaited. If the CSS variable equals `none`,
637
+ * the promise resolves immediately.
638
+ *
639
+ * @return {Promise}
640
+ * @private
641
+ */
642
+ __drawerTransitionComplete() {
643
+ return new Promise((resolve) => {
644
+ if (this._getCustomPropertyValue('--vaadin-app-layout-transition') === 'none') {
645
+ resolve();
646
+ return;
647
+ }
545
648
 
546
- if (this.overlay !== overlay) {
547
- this.notifyResize();
649
+ this.$.drawer.addEventListener('transitionend', resolve, { once: true });
650
+ });
651
+ }
652
+
653
+ /** @private */
654
+ async __trapFocusInDrawer() {
655
+ // Wait for the drawer CSS transition before focusing the drawer
656
+ // in order for VoiceOver to have a proper outline.
657
+ await this.__drawerTransitionComplete();
658
+
659
+ if (!this.drawerOpened) {
660
+ // The drawer has been closed during the CSS transition.
661
+ return;
662
+ }
663
+
664
+ this.$.drawer.setAttribute('tabindex', '0');
665
+ this.__focusTrapController.trapFocus(this.$.drawer);
666
+ }
667
+
668
+ /** @private */
669
+ async __releaseFocusFromDrawer() {
670
+ // Wait for the drawer CSS transition in order to restore focus to the toggle
671
+ // only after `visibility` becomes `hidden`, that is, the drawer becomes inaccessible by the tabbing navigation.
672
+ await this.__drawerTransitionComplete();
673
+
674
+ if (this.drawerOpened) {
675
+ // The drawer has been opened during the CSS transition.
676
+ return;
548
677
  }
549
678
 
550
- // TODO(jouni): ARIA attributes. The drawer should act similar to a modal dialog when in ”overlay” mode
679
+ this.__focusTrapController.releaseFocus();
680
+ this.$.drawer.removeAttribute('tabindex');
681
+
682
+ // Move focus to the drawer toggle when closing the drawer.
683
+ const toggle = this.querySelector('vaadin-drawer-toggle');
684
+ if (toggle) {
685
+ toggle.focus();
686
+ toggle.setAttribute('focus-ring', 'focus');
687
+ }
688
+ }
689
+
690
+ /**
691
+ * Closes the drawer on Escape press if it has been opened in the overlay mode.
692
+ *
693
+ * @param {KeyboardEvent} event
694
+ * @private
695
+ */
696
+ __onDrawerKeyDown(event) {
697
+ if (event.key === 'Escape' && this.overlay) {
698
+ this.drawerOpened = false;
699
+ }
551
700
  }
552
701
 
553
702
  /** @private */
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @license
3
- * Copyright (c) 2021 Vaadin Ltd.
3
+ * Copyright (c) 2018 - 2022 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 { Button } from '@vaadin/button/src/vaadin-button.js';
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @license
3
- * Copyright (c) 2021 Vaadin Ltd.
3
+ * Copyright (c) 2018 - 2022 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 } from '@polymer/polymer/lib/utils/html-tag.js';