Skip to content

Commit 8ff92f5

Browse files
authored
refactor!: update crud editor dialog to use native popover (#9790)
* use native popover for CRUD editor * replace teleporting with slots * update tests * cleanup * add export parts test * hide elements outside of modal dialog * update snapshots * cleanup member order * cleanup test suite * reuse owner property for passing modal root * use default event options * render nothing instead of empty string * update snapshot * update documented part names
1 parent ebf5367 commit 8ff92f5

File tree

12 files changed

+480
-249
lines changed

12 files changed

+480
-249
lines changed

packages/crud/src/vaadin-crud-dialog.js

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,24 @@ class CrudDialogOverlay extends OverlayMixin(DirMixin(ThemableMixin(PolylitMixin
4040
return crudDialogOverlayStyles;
4141
}
4242

43+
/**
44+
* Override method from OverlayFocusMixin to use the owner (CRUD element) as modal root
45+
* @protected
46+
* @override
47+
*/
48+
get _modalRoot() {
49+
return this.owner;
50+
}
51+
4352
/** @protected */
4453
render() {
4554
return html`
4655
<div part="backdrop" id="backdrop" ?hidden="${!this.withBackdrop}"></div>
4756
<div part="overlay" id="overlay" tabindex="0">
4857
<section id="resizerContainer" class="resizer-container">
49-
<header part="header"><slot name="header"></slot></header>
58+
<header part="header">
59+
<slot name="header"></slot>
60+
</header>
5061
<div part="content" id="content">
5162
<slot name="form"></slot>
5263
</div>
@@ -71,6 +82,22 @@ class CrudDialogOverlay extends OverlayMixin(DirMixin(ThemableMixin(PolylitMixin
7182
this.setAttribute('has-header', '');
7283
this.setAttribute('has-footer', '');
7384
}
85+
86+
/**
87+
* @protected
88+
* @override
89+
*/
90+
_attachOverlay() {
91+
this.showPopover();
92+
}
93+
94+
/**
95+
* @protected
96+
* @override
97+
*/
98+
_detachOverlay() {
99+
this.hidePopover();
100+
}
74101
}
75102

76103
defineCustomElement(CrudDialogOverlay);
@@ -86,8 +113,15 @@ class CrudDialog extends DialogBaseMixin(OverlayClassMixin(ThemePropertyMixin(Po
86113

87114
static get styles() {
88115
return css`
89-
:host {
90-
display: none;
116+
:host,
117+
[hidden] {
118+
display: none !important;
119+
}
120+
121+
:host([opened]),
122+
:host([opening]),
123+
:host([closing]) {
124+
display: contents !important;
91125
}
92126
`;
93127
}
@@ -101,6 +135,10 @@ class CrudDialog extends DialogBaseMixin(OverlayClassMixin(ThemePropertyMixin(Po
101135
fullscreen: {
102136
type: Boolean,
103137
},
138+
139+
crudElement: {
140+
type: Object,
141+
},
104142
};
105143
}
106144

@@ -109,20 +147,36 @@ class CrudDialog extends DialogBaseMixin(OverlayClassMixin(ThemePropertyMixin(Po
109147
return html`
110148
<vaadin-crud-dialog-overlay
111149
id="overlay"
150+
popover="manual"
151+
.owner="${this.crudElement}"
112152
.opened="${this.opened}"
113153
aria-label="${ifDefined(this.ariaLabel)}"
114154
@opened-changed="${this._onOverlayOpened}"
115155
@mousedown="${this._bringOverlayToFront}"
116156
@touchstart="${this._bringOverlayToFront}"
157+
@vaadin-overlay-outside-click="${this.__cancel}"
158+
@vaadin-overlay-escape-press="${this.__cancel}"
117159
theme="${ifDefined(this._theme)}"
118160
.modeless="${this.modeless}"
119161
.withBackdrop="${!this.modeless}"
120162
?fullscreen="${this.fullscreen}"
121163
role="dialog"
122164
focus-trap
123-
></vaadin-crud-dialog-overlay>
165+
exportparts="backdrop, overlay, header, content, footer"
166+
>
167+
<slot name="header" slot="header"></slot>
168+
<slot name="form" slot="form"></slot>
169+
<slot name="save-button" slot="save-button"></slot>
170+
<slot name="cancel-button" slot="cancel-button"></slot>
171+
<slot name="delete-button" slot="delete-button"></slot>
172+
</vaadin-crud-dialog-overlay>
124173
`;
125174
}
175+
176+
/** @private **/
177+
__cancel() {
178+
this.dispatchEvent(new CustomEvent('cancel'));
179+
}
126180
}
127181

128182
defineCustomElement(CrudDialog);

packages/crud/src/vaadin-crud-mixin.js

Lines changed: 55 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -377,13 +377,25 @@ export const CrudMixin = (superClass) =>
377377
return this.__fields;
378378
}
379379

380+
/** @private */
381+
get _editor() {
382+
return this.shadowRoot.querySelector('#editor');
383+
}
384+
385+
/** @private */
386+
get _scroller() {
387+
return this.shadowRoot.querySelector('#scroller');
388+
}
389+
390+
/** @private */
391+
get _dialogMode() {
392+
return this.editorPosition === '' || this._fullscreen;
393+
}
394+
380395
/** @protected */
381396
ready() {
382397
super.ready();
383398

384-
this.$.dialog.$.overlay.addEventListener('vaadin-overlay-outside-click', this.__cancel);
385-
this.$.dialog.$.overlay.addEventListener('vaadin-overlay-escape-press', this.__cancel);
386-
387399
this._gridController = new GridSlotController(this);
388400
this.addController(this._gridController);
389401

@@ -421,6 +433,24 @@ export const CrudMixin = (superClass) =>
421433
this._confirmDeleteDialog = this.querySelector('vaadin-confirm-dialog[slot="confirm-delete"]');
422434
}
423435

436+
/** @protected */
437+
updated(props) {
438+
super.updated(props);
439+
440+
// When using dialog mode, hide elements not slotted into the dialog from accessibility tree
441+
if (
442+
props.has('_grid') ||
443+
props.has('_newButton') ||
444+
props.has('editorOpened') ||
445+
props.has('editorPosition') ||
446+
props.has('_fullscreen')
447+
) {
448+
const hide = this.editorOpened && this._dialogMode;
449+
this.__hideElement(this._grid, hide);
450+
this.__hideElement(this._newButton, hide);
451+
}
452+
}
453+
424454
/**
425455
* @param {boolean} isDirty
426456
* @private
@@ -477,34 +507,31 @@ export const CrudMixin = (superClass) =>
477507
}
478508

479509
if (opened) {
480-
this.__ensureChildren();
481-
482510
// When using bottom / aside editor position,
483511
// auto-focus the editor element on open.
484-
if (this._form.parentElement === this) {
485-
this.$.editor.setAttribute('tabindex', '0');
486-
this.$.editor.focus();
487-
} else {
488-
this.$.editor.removeAttribute('tabindex');
512+
if (this._editor) {
513+
this._editor.focus();
489514
}
490-
} else if (oldOpened) {
491-
// Teleport form and buttons back to light DOM when closing overlay
492-
this.__moveChildNodes(this);
515+
516+
// Wait to set label until header node has updated (observer seems to run after this one)
517+
setTimeout(() => {
518+
this.__dialogAriaLabel = this._headerNode.textContent.trim();
519+
});
493520
}
494521

495522
this.__toggleToolbar();
496523

497524
// Make sure to reset scroll position
498-
this.$.scroller.scrollTop = 0;
525+
if (this._scroller) {
526+
this._scroller.scrollTop = 0;
527+
}
499528
}
500529

501530
/** @private */
502531
__fullscreenChanged(fullscreen, oldFullscreen) {
503532
if (fullscreen || oldFullscreen) {
504533
this.__toggleToolbar();
505534

506-
this.__ensureChildren();
507-
508535
this.toggleAttribute('fullscreen', fullscreen);
509536
}
510537
}
@@ -517,66 +544,6 @@ export const CrudMixin = (superClass) =>
517544
}
518545
}
519546

520-
/** @private */
521-
__moveChildNodes(target) {
522-
const nodes = [this._headerNode, this._form];
523-
const buttons = [this._saveButton, this._cancelButton, this._deleteButton].filter(Boolean);
524-
if (!nodes.every((node) => node instanceof HTMLElement)) {
525-
return;
526-
}
527-
528-
// Teleport header node, form, and the buttons to corresponding slots.
529-
// NOTE: order in which buttons are moved matches the order of slots.
530-
[...nodes, ...buttons].forEach((node) => {
531-
// Do not move nodes if the editor position has not changed
532-
if (node.parentNode !== target) {
533-
target.appendChild(node);
534-
}
535-
});
536-
537-
// Wait to set label until slotted element has been moved.
538-
setTimeout(() => {
539-
this.__dialogAriaLabel = this._headerNode.textContent.trim();
540-
});
541-
}
542-
543-
/** @private */
544-
__shouldOpenDialog(fullscreen, editorPosition) {
545-
return editorPosition === '' || fullscreen;
546-
}
547-
548-
/** @private */
549-
__ensureChildren() {
550-
if (this.__shouldOpenDialog(this._fullscreen, this.editorPosition)) {
551-
// Move form to dialog
552-
this.__moveChildNodes(this.$.dialog.$.overlay);
553-
} else {
554-
// Move form to crud
555-
this.__moveChildNodes(this);
556-
}
557-
}
558-
559-
/** @private */
560-
__computeDialogOpened(opened, fullscreen, editorPosition) {
561-
// Only open dialog when editorPosition is "" or fullscreen is set
562-
return this.__shouldOpenDialog(fullscreen, editorPosition) ? opened : false;
563-
}
564-
565-
/** @private */
566-
__computeEditorHidden(opened, fullscreen, editorPosition) {
567-
// Only show editor when editorPosition is "bottom" or "aside"
568-
if (['aside', 'bottom'].includes(editorPosition) && !fullscreen) {
569-
return !opened;
570-
}
571-
572-
return true;
573-
}
574-
575-
/** @private */
576-
__onDialogOpened(event) {
577-
this.editorOpened = event.detail.value;
578-
}
579-
580547
/** @private */
581548
__onGridEdit(event) {
582549
event.stopPropagation();
@@ -620,8 +587,7 @@ export const CrudMixin = (superClass) =>
620587
* @private
621588
*/
622589
__formChanged(form, oldForm) {
623-
if (oldForm && oldForm.parentElement) {
624-
oldForm.parentElement.removeChild(oldForm);
590+
if (oldForm) {
625591
oldForm.removeEventListener('change', this.__onFormChange);
626592
oldForm.removeEventListener('input', this.__onFormChange);
627593
}
@@ -1023,6 +989,17 @@ export const CrudMixin = (superClass) =>
1023989
}
1024990
}
1025991

992+
/** @private */
993+
__hideElement(element, value) {
994+
if (!element) return;
995+
996+
if (value) {
997+
element.setAttribute('aria-hidden', 'true');
998+
} else {
999+
element.removeAttribute('aria-hidden');
1000+
}
1001+
}
1002+
10261003
/**
10271004
* Fired when user wants to edit an existing item. If the default is prevented, then
10281005
* a new item is not assigned to the form, giving that responsibility to the app, though

packages/crud/src/vaadin-crud.d.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,11 +132,26 @@ export * from './vaadin-crud-mixin.js';
132132
*
133133
* ### Styling
134134
*
135-
* The following shadow DOM parts are available for styling:
135+
* The following shadow DOM parts are available for styling when the editor is rendered next to, or below, the grid:
136136
*
137137
* Part name | Description
138138
* ----------------|----------------
139-
* `toolbar` | Toolbar container at the bottom. By default it contains the the `new` button
139+
* `toolbar` | Toolbar container at the bottom of the grid. By default, it contains the `new` button
140+
* `editor` | The editor container
141+
* `scroller` | The wrapper for the header and the form
142+
* `header` | The header of the editor
143+
* `footer` | The footer of the editor
144+
*
145+
* The following shadow DOM parts are available for styling when the editor renders as a dialog:
146+
*
147+
* Part name | Description
148+
* ----------------|----------------
149+
* `toolbar` | Toolbar container at the bottom of the grid. By default, it contains the `new` button
150+
* `overlay` | The dialog overlay
151+
* `backdrop` | The dialog backdrop
152+
* `header` | The header of the dialog
153+
* `footer` | The footer of the dialog
154+
* `content` | The wrapper for the form
140155
*
141156
* The following custom properties are available:
142157
*

0 commit comments

Comments
 (0)