@vaadin/app-layout 22.0.1 → 23.0.0-alpha3

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": "22.0.1",
3
+ "version": "23.0.0-alpha3",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -32,19 +32,18 @@
32
32
  "polymer"
33
33
  ],
34
34
  "dependencies": {
35
- "@polymer/iron-resizable-behavior": "^3.0.0",
36
35
  "@polymer/polymer": "^3.0.0",
37
- "@vaadin/button": "^22.0.1",
38
- "@vaadin/component-base": "^22.0.1",
39
- "@vaadin/vaadin-lumo-styles": "^22.0.1",
40
- "@vaadin/vaadin-material-styles": "^22.0.1",
41
- "@vaadin/vaadin-themable-mixin": "^22.0.1"
36
+ "@vaadin/button": "23.0.0-alpha3",
37
+ "@vaadin/component-base": "23.0.0-alpha3",
38
+ "@vaadin/vaadin-lumo-styles": "23.0.0-alpha3",
39
+ "@vaadin/vaadin-material-styles": "23.0.0-alpha3",
40
+ "@vaadin/vaadin-themable-mixin": "23.0.0-alpha3"
42
41
  },
43
42
  "devDependencies": {
44
43
  "@esm-bundle/chai": "^4.3.4",
45
- "@vaadin/tabs": "^22.0.1",
44
+ "@vaadin/tabs": "23.0.0-alpha3",
46
45
  "@vaadin/testing-helpers": "^0.3.2",
47
46
  "sinon": "^9.2.1"
48
47
  },
49
- "gitHead": "2b0a2bff0369d6020f7cc33ad35506aa2d1f6f68"
48
+ "gitHead": "490037919a9e054cc002c1b3be0c94a1603e1a44"
50
49
  }
@@ -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);
@@ -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
  */
@@ -118,7 +123,29 @@ export type AppLayoutEventMap = HTMLElementEventMap & AppLayoutCustomEventMap;
118
123
  * @fires {CustomEvent} overlay-changed - Fired when the `overlay` property changes.
119
124
  * @fires {CustomEvent} primary-section-changed - Fired when the `primarySection` property changes.
120
125
  */
121
- 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
+
122
149
  /**
123
150
  * Defines whether navbar or drawer will come first visually.
124
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
  *
@@ -103,8 +107,9 @@ import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mix
103
107
  * @extends HTMLElement
104
108
  * @mixes ElementMixin
105
109
  * @mixes ThemableMixin
110
+ * @mixes ControllerMixin
106
111
  */
107
- class AppLayout extends ElementMixin(ThemableMixin(mixinBehaviors([IronResizableBehavior], PolymerElement))) {
112
+ class AppLayout extends ElementMixin(ThemableMixin(ControllerMixin(PolymerElement))) {
108
113
  static get template() {
109
114
  return html`
110
115
  <style>
@@ -199,16 +204,20 @@ class AppLayout extends ElementMixin(ThemableMixin(mixinBehaviors([IronResizable
199
204
  right: auto;
200
205
  bottom: var(--vaadin-app-layout-navbar-offset-bottom, var(--vaadin-viewport-offset-bottom, 0));
201
206
  left: var(--vaadin-app-layout-navbar-offset-left, 0);
202
- transition: transform var(--vaadin-app-layout-transition);
207
+ transition: transform var(--vaadin-app-layout-transition), visibility var(--vaadin-app-layout-transition);
203
208
  transform: translateX(-100%);
204
209
  max-width: 90%;
205
210
  width: 16em;
206
211
  box-sizing: border-box;
207
212
  padding: var(--safe-area-inset-top) 0 var(--safe-area-inset-bottom) var(--safe-area-inset-left);
208
213
  outline: none;
214
+ /* The drawer should be inaccessible by the tabbing navigation when it is closed. */
215
+ visibility: hidden;
209
216
  }
210
217
 
211
218
  :host([drawer-opened]) [part='drawer'] {
219
+ /* The drawer should be accessible by the tabbing navigation when it is opened. */
220
+ visibility: visible;
212
221
  transform: translateX(0%);
213
222
  touch-action: manipulation;
214
223
  }
@@ -285,7 +294,7 @@ class AppLayout extends ElementMixin(ThemableMixin(mixinBehaviors([IronResizable
285
294
  <slot name="navbar"></slot>
286
295
  </div>
287
296
  <div part="backdrop" on-click="_close" on-touchstart="_close"></div>
288
- <div part="drawer" id="drawer">
297
+ <div part="drawer" id="drawer" on-keydown="__onDrawerKeyDown">
289
298
  <slot name="drawer" id="drawerSlot"></slot>
290
299
  </div>
291
300
  <div content>
@@ -304,6 +313,39 @@ class AppLayout extends ElementMixin(ThemableMixin(mixinBehaviors([IronResizable
304
313
 
305
314
  static get properties() {
306
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
+
307
349
  /**
308
350
  * Defines whether navbar or drawer will come first visually.
309
351
  * - By default (`primary-section="navbar"`), the navbar takes the full available width and moves the drawer down.
@@ -316,7 +358,7 @@ class AppLayout extends ElementMixin(ThemableMixin(mixinBehaviors([IronResizable
316
358
  value: 'navbar',
317
359
  notify: true,
318
360
  reflectToAttribute: true,
319
- observer: '_primarySectionObserver'
361
+ observer: '__primarySectionChanged'
320
362
  },
321
363
 
322
364
  /**
@@ -332,7 +374,7 @@ class AppLayout extends ElementMixin(ThemableMixin(mixinBehaviors([IronResizable
332
374
  notify: true,
333
375
  value: true,
334
376
  reflectToAttribute: true,
335
- observer: '_drawerOpenedObserver'
377
+ observer: '__drawerOpenedChanged'
336
378
  },
337
379
 
338
380
  /**
@@ -369,6 +411,9 @@ class AppLayout extends ElementMixin(ThemableMixin(mixinBehaviors([IronResizable
369
411
  this.__boundResizeListener = this._resize.bind(this);
370
412
  this.__drawerToggleClickListener = this._drawerToggleClick.bind(this);
371
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);
372
417
  }
373
418
 
374
419
  /** @protected */
@@ -402,6 +447,12 @@ class AppLayout extends ElementMixin(ThemableMixin(mixinBehaviors([IronResizable
402
447
  window.addEventListener('close-overlay-drawer', this.__closeOverlayDrawerListener);
403
448
  }
404
449
 
450
+ /** @protected */
451
+ ready() {
452
+ super.ready();
453
+ this.addController(this.__focusTrapController);
454
+ }
455
+
405
456
  /** @protected */
406
457
  disconnectedCallback() {
407
458
  super.disconnectedCallback();
@@ -421,29 +472,55 @@ class AppLayout extends ElementMixin(ThemableMixin(mixinBehaviors([IronResizable
421
472
  window.dispatchEvent(new CustomEvent('close-overlay-drawer'));
422
473
  }
423
474
 
424
- /** @private */
425
- _primarySectionObserver(value) {
426
- 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);
427
486
  if (!isValid) {
428
487
  this.set('primarySection', 'navbar');
429
488
  }
430
489
  }
431
490
 
432
- /** @private */
433
- _drawerOpenedObserver() {
434
- const drawer = this.$.drawer;
435
-
436
- drawer.removeAttribute('tabindex');
437
-
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) {
438
504
  if (this.overlay) {
439
- if (this.drawerOpened) {
440
- drawer.setAttribute('tabindex', 0);
441
- drawer.focus();
505
+ if (drawerOpened) {
442
506
  this._updateDrawerHeight();
507
+ this.__trapFocusInDrawer();
508
+ } else if (oldDrawerOpened) {
509
+ this.__releaseFocusFromDrawer();
443
510
  }
444
511
  }
512
+ }
445
513
 
446
- 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();
447
524
  }
448
525
 
449
526
  /** @protected */
@@ -500,8 +577,6 @@ class AppLayout extends ElementMixin(ThemableMixin(mixinBehaviors([IronResizable
500
577
  const drawerRect = drawer.getBoundingClientRect();
501
578
 
502
579
  this.style.setProperty('--_vaadin-app-layout-drawer-offset-size', drawerRect.width + 'px');
503
-
504
- this.notifyResize();
505
580
  }
506
581
 
507
582
  /** @protected */
@@ -513,8 +588,7 @@ class AppLayout extends ElementMixin(ThemableMixin(mixinBehaviors([IronResizable
513
588
 
514
589
  /** @protected */
515
590
  _updateOverlayMode() {
516
- const overlay = this._getCustomPropertyValue('--vaadin-app-layout-drawer-overlay') == 'true';
517
- const drawer = this.$.drawer;
591
+ const overlay = this._getCustomPropertyValue('--vaadin-app-layout-drawer-overlay') === 'true';
518
592
 
519
593
  if (!this.overlay && overlay) {
520
594
  // Changed from not overlay to overlay
@@ -524,28 +598,105 @@ class AppLayout extends ElementMixin(ThemableMixin(mixinBehaviors([IronResizable
524
598
 
525
599
  this._setOverlay(overlay);
526
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;
527
621
  if (this.overlay) {
528
622
  drawer.setAttribute('role', 'dialog');
529
623
  drawer.setAttribute('aria-modal', 'true');
530
- drawer.setAttribute('aria-label', 'drawer');
624
+ drawer.setAttribute('aria-label', this.i18n.drawer);
531
625
  } else {
532
- if (this._drawerStateSaved) {
533
- this.drawerOpened = this._drawerStateSaved;
534
- this._drawerStateSaved = null;
535
- }
536
-
537
626
  drawer.removeAttribute('role');
538
627
  drawer.removeAttribute('aria-modal');
539
628
  drawer.removeAttribute('aria-label');
540
629
  }
630
+ }
541
631
 
542
- 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
+ }
543
648
 
544
- if (this.overlay !== overlay) {
545
- 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;
546
677
  }
547
678
 
548
- // 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
+ }
549
700
  }
550
701
 
551
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';