Skip to content
Merged
62 changes: 58 additions & 4 deletions packages/crud/src/vaadin-crud-dialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,24 @@ class CrudDialogOverlay extends OverlayMixin(DirMixin(ThemableMixin(PolylitMixin
return crudDialogOverlayStyles;
}

/**
* Override method from OverlayFocusMixin to use the owner (CRUD element) as modal root
* @protected
* @override
*/
get _modalRoot() {
return this.owner;
}

/** @protected */
render() {
return html`
<div part="backdrop" id="backdrop" ?hidden="${!this.withBackdrop}"></div>
<div part="overlay" id="overlay" tabindex="0">
<section id="resizerContainer" class="resizer-container">
<header part="header"><slot name="header"></slot></header>
<header part="header">
<slot name="header"></slot>
</header>
<div part="content" id="content">
<slot name="form"></slot>
</div>
Expand All @@ -71,6 +82,22 @@ class CrudDialogOverlay extends OverlayMixin(DirMixin(ThemableMixin(PolylitMixin
this.setAttribute('has-header', '');
this.setAttribute('has-footer', '');
}

/**
* @protected
* @override
*/
_attachOverlay() {
this.showPopover();
}

/**
* @protected
* @override
*/
_detachOverlay() {
this.hidePopover();
}
}

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

static get styles() {
return css`
:host {
display: none;
:host,
[hidden] {
display: none !important;
}

:host([opened]),
:host([opening]),
:host([closing]) {
display: contents !important;
}
`;
}
Expand All @@ -101,6 +135,10 @@ class CrudDialog extends DialogBaseMixin(OverlayClassMixin(ThemePropertyMixin(Po
fullscreen: {
type: Boolean,
},

crudElement: {
type: Object,
},
};
}

Expand All @@ -109,20 +147,36 @@ class CrudDialog extends DialogBaseMixin(OverlayClassMixin(ThemePropertyMixin(Po
return html`
<vaadin-crud-dialog-overlay
id="overlay"
popover="manual"
.owner="${this.crudElement}"
.opened="${this.opened}"
aria-label="${ifDefined(this.ariaLabel)}"
@opened-changed="${this._onOverlayOpened}"
@mousedown="${this._bringOverlayToFront}"
@touchstart="${this._bringOverlayToFront}"
@vaadin-overlay-outside-click="${this.__cancel}"
@vaadin-overlay-escape-press="${this.__cancel}"
theme="${ifDefined(this._theme)}"
.modeless="${this.modeless}"
.withBackdrop="${!this.modeless}"
?fullscreen="${this.fullscreen}"
role="dialog"
focus-trap
></vaadin-crud-dialog-overlay>
exportparts="backdrop, overlay, header, content, footer"
>
<slot name="header" slot="header"></slot>
<slot name="form" slot="form"></slot>
<slot name="save-button" slot="save-button"></slot>
<slot name="cancel-button" slot="cancel-button"></slot>
<slot name="delete-button" slot="delete-button"></slot>
</vaadin-crud-dialog-overlay>
`;
}

/** @private **/
__cancel() {
this.dispatchEvent(new CustomEvent('cancel'));
}
}

defineCustomElement(CrudDialog);
133 changes: 55 additions & 78 deletions packages/crud/src/vaadin-crud-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -377,13 +377,25 @@ export const CrudMixin = (superClass) =>
return this.__fields;
}

/** @private */
get _editor() {
return this.shadowRoot.querySelector('#editor');
}

/** @private */
get _scroller() {
return this.shadowRoot.querySelector('#scroller');
}

/** @private */
get _dialogMode() {
return this.editorPosition === '' || this._fullscreen;
}

/** @protected */
ready() {
super.ready();

this.$.dialog.$.overlay.addEventListener('vaadin-overlay-outside-click', this.__cancel);
this.$.dialog.$.overlay.addEventListener('vaadin-overlay-escape-press', this.__cancel);

this._gridController = new GridSlotController(this);
this.addController(this._gridController);

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

/** @protected */
updated(props) {
super.updated(props);

// When using dialog mode, hide elements not slotted into the dialog from accessibility tree
if (
props.has('_grid') ||
props.has('_newButton') ||
props.has('editorOpened') ||
props.has('editorPosition') ||
props.has('_fullscreen')
) {
const hide = this.editorOpened && this._dialogMode;
this.__hideElement(this._grid, hide);
this.__hideElement(this._newButton, hide);
}
}
Comment on lines +436 to +452
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The aria-hidden logic needs some customization, as we need to hide all elements that are not in the dialog. So the solution now is:

  • Use CRUD itself as modal root
  • Then add this custom logic to also hide elements that are not slotted into the dialog when it is opened


/**
* @param {boolean} isDirty
* @private
Expand Down Expand Up @@ -477,34 +507,31 @@ export const CrudMixin = (superClass) =>
}

if (opened) {
this.__ensureChildren();

// When using bottom / aside editor position,
// auto-focus the editor element on open.
if (this._form.parentElement === this) {
this.$.editor.setAttribute('tabindex', '0');
this.$.editor.focus();
} else {
this.$.editor.removeAttribute('tabindex');
if (this._editor) {
this._editor.focus();
}
} else if (oldOpened) {
// Teleport form and buttons back to light DOM when closing overlay
this.__moveChildNodes(this);

// Wait to set label until header node has updated (observer seems to run after this one)
setTimeout(() => {
this.__dialogAriaLabel = this._headerNode.textContent.trim();
});
Comment on lines +516 to +519
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be better, but I couldn't find a simple solution.

Updating the label here is easiest as it also handles text content of custom header nodes.The timeout could be avoided if all observer logic were moved into updated. Then we could control the order of updates, so that the default header node would be updated before running this opened update logic. But that would also require several changes.

}

this.__toggleToolbar();

// Make sure to reset scroll position
this.$.scroller.scrollTop = 0;
if (this._scroller) {
this._scroller.scrollTop = 0;
}
}

/** @private */
__fullscreenChanged(fullscreen, oldFullscreen) {
if (fullscreen || oldFullscreen) {
this.__toggleToolbar();

this.__ensureChildren();

this.toggleAttribute('fullscreen', fullscreen);
}
}
Expand All @@ -517,66 +544,6 @@ export const CrudMixin = (superClass) =>
}
}

/** @private */
__moveChildNodes(target) {
const nodes = [this._headerNode, this._form];
const buttons = [this._saveButton, this._cancelButton, this._deleteButton].filter(Boolean);
if (!nodes.every((node) => node instanceof HTMLElement)) {
return;
}

// Teleport header node, form, and the buttons to corresponding slots.
// NOTE: order in which buttons are moved matches the order of slots.
[...nodes, ...buttons].forEach((node) => {
// Do not move nodes if the editor position has not changed
if (node.parentNode !== target) {
target.appendChild(node);
}
});

// Wait to set label until slotted element has been moved.
setTimeout(() => {
this.__dialogAriaLabel = this._headerNode.textContent.trim();
});
}

/** @private */
__shouldOpenDialog(fullscreen, editorPosition) {
return editorPosition === '' || fullscreen;
}

/** @private */
__ensureChildren() {
if (this.__shouldOpenDialog(this._fullscreen, this.editorPosition)) {
// Move form to dialog
this.__moveChildNodes(this.$.dialog.$.overlay);
} else {
// Move form to crud
this.__moveChildNodes(this);
}
}

/** @private */
__computeDialogOpened(opened, fullscreen, editorPosition) {
// Only open dialog when editorPosition is "" or fullscreen is set
return this.__shouldOpenDialog(fullscreen, editorPosition) ? opened : false;
}

/** @private */
__computeEditorHidden(opened, fullscreen, editorPosition) {
// Only show editor when editorPosition is "bottom" or "aside"
if (['aside', 'bottom'].includes(editorPosition) && !fullscreen) {
return !opened;
}

return true;
}

/** @private */
__onDialogOpened(event) {
this.editorOpened = event.detail.value;
}

/** @private */
__onGridEdit(event) {
event.stopPropagation();
Expand Down Expand Up @@ -620,8 +587,7 @@ export const CrudMixin = (superClass) =>
* @private
*/
__formChanged(form, oldForm) {
if (oldForm && oldForm.parentElement) {
oldForm.parentElement.removeChild(oldForm);
if (oldForm) {
oldForm.removeEventListener('change', this.__onFormChange);
oldForm.removeEventListener('input', this.__onFormChange);
}
Expand Down Expand Up @@ -1023,6 +989,17 @@ export const CrudMixin = (superClass) =>
}
}

/** @private */
__hideElement(element, value) {
if (!element) return;

if (value) {
element.setAttribute('aria-hidden', 'true');
} else {
element.removeAttribute('aria-hidden');
}
}

/**
* Fired when user wants to edit an existing item. If the default is prevented, then
* a new item is not assigned to the form, giving that responsibility to the app, though
Expand Down
19 changes: 17 additions & 2 deletions packages/crud/src/vaadin-crud.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,11 +132,26 @@ export * from './vaadin-crud-mixin.js';
*
* ### Styling
*
* The following shadow DOM parts are available for styling:
* The following shadow DOM parts are available for styling when the editor is rendered next to, or below, the grid:
*
* Part name | Description
* ----------------|----------------
* `toolbar` | Toolbar container at the bottom. By default it contains the the `new` button
* `toolbar` | Toolbar container at the bottom of the grid. By default, it contains the `new` button
* `editor` | The editor container
* `scroller` | The wrapper for the header and the form
* `header` | The header of the editor
* `footer` | The footer of the editor
*
* The following shadow DOM parts are available for styling when the editor renders as a dialog:
*
* Part name | Description
* ----------------|----------------
* `toolbar` | Toolbar container at the bottom of the grid. By default, it contains the `new` button
* `overlay` | The dialog overlay
* `backdrop` | The dialog backdrop
* `header` | The header of the dialog
* `footer` | The footer of the dialog
* `content` | The wrapper for the form
*
* The following custom properties are available:
*
Expand Down
Loading