diff --git a/packages/main/cypress/specs/Dialog.cy.tsx b/packages/main/cypress/specs/Dialog.cy.tsx index a9ded9140d87..ea218a940f41 100644 --- a/packages/main/cypress/specs/Dialog.cy.tsx +++ b/packages/main/cypress/specs/Dialog.cy.tsx @@ -580,11 +580,13 @@ describe("Dialog general interaction", () => { const initialTop = parseInt(dialog.css("top")); const initialLeft = parseInt(dialog.css("left")); - // Act - Move dialog up using keyboard - cy.get("#header-slot").realClick(); + // Act - Focus the drag/resize handle and move dialog up + cy.get("#draggable-dialog").shadow().find(".ui5-popup-drag-resize-handler") + .focus() + .should("be.focused"); - cy.get("#header-slot").focused().realPress("{uparrow}"); - cy.get("#header-slot").focused().realPress("{uparrow}"); + cy.realPress("{uparrow}"); + cy.realPress("{uparrow}"); // Assert - Top position changes, left remains the same @@ -599,10 +601,8 @@ describe("Dialog general interaction", () => { }) // Act - Move dialog left using keyboard - cy.get("#header-slot").realClick(); - - cy.get("#header-slot").focused().realPress("{leftarrow}"); - cy.get("#header-slot").focused().realPress("{leftarrow}"); + cy.realPress("{leftarrow}"); + cy.realPress("{leftarrow}"); // Assert - Left position changes, top remains the same cy.get("#draggable-dialog") @@ -776,9 +776,12 @@ describe("Dialog general interaction", () => { const initialTop = parseInt(dialog.css("top")); const initialLeft = parseInt(dialog.css("left")); - // Act - Resize height using keyboard - cy.get("#resizable-dialog").shadow().find(".ui5-popup-resize-handle").click(); - cy.get("#resizable-dialog").realPress(["Shift", "ArrowDown"]); + // Act - Focus the drag/resize handle and resize height + cy.get("#resizable-dialog").shadow().find(".ui5-popup-drag-resize-handler") + .focus() + .should("be.focused"); + + cy.realPress(["Shift", "ArrowDown"]); // Assert - Height changes, width and position remain the same cy.get("#resizable-dialog").then(dialogAfterResizeHeight => { @@ -791,8 +794,7 @@ describe("Dialog general interaction", () => { expect(leftAfterResizeHeight).to.equal(initialLeft); // Act - Resize width using keyboard - cy.get("#resizable-dialog").shadow().find(".ui5-popup-resize-handle").click(); - cy.get("#resizable-dialog").realPress(["Shift", "ArrowRight"]); + cy.realPress(["Shift", "ArrowRight"]); // Assert - Width changes, height and position remain the same cy.get("#resizable-dialog").then(dialogAfterResizeWidth => { @@ -1078,30 +1080,33 @@ describe("Acc", () => { cy.get("#draggable-dialog").invoke("attr", "open", true); cy.get("#draggable-dialog").ui5DialogOpened(); - // Assert aria-labelledby and aria attributes + // Assert aria-label on the dialog root cy.get("#draggable-dialog") .shadow() .find(".ui5-popup-root") .should("have.attr", "aria-label", "Draggable"); + // Assert aria-describedby is on the drag/resize handle, not the header cy.get("#draggable-dialog") .shadow() - .find(".ui5-popup-header-root") + .find(".ui5-popup-drag-resize-handler") .should("have.attr", "aria-describedby"); + // Assert hidden text contains keyboard instructions cy.get("#draggable-dialog") .shadow() .find(".ui5-hidden-text") .should("exist") .then(hiddenText => { - const valueOfTheHiddenText = hiddenText.text(); + const valueOfTheHiddenText = hiddenText.first().text(); cy.wrap(valueOfTheHiddenText).should("equal", "Use Arrow keys to move"); }); + // Assert aria-roledescription on the drag/resize handle cy.get("#draggable-dialog") .shadow() - .find(".ui5-popup-header-root") - .should("have.attr", "aria-roledescription", "Interactive Header"); + .find(".ui5-popup-drag-resize-handler") + .should("have.attr", "aria-roledescription", "Handle"); }); it("tests aria-describedby for default header", () => { @@ -1119,10 +1124,10 @@ describe("Acc", () => { cy.get("#resizable-dialog").invoke("attr", "open", true); cy.get("#resizable-dialog").ui5DialogOpened(); - // Assert aria-describedby and aria-roledescription attributes + // Assert aria-describedby is on the drag/resize handle cy.get("#resizable-dialog") .shadow() - .find(".ui5-popup-header-root") + .find(".ui5-popup-drag-resize-handler") .should("have.attr", "aria-describedby") .then($el => { cy.get("#resizable-dialog") @@ -1130,15 +1135,16 @@ describe("Acc", () => { .find(".ui5-hidden-text") .should("exist") .then(hiddenText => { - const valueOfTheHiddenText = hiddenText.text(); + const valueOfTheHiddenText = hiddenText.first().text(); cy.wrap(valueOfTheHiddenText).should("equal", "Use Shift+Arrow keys to resize"); }); }); + // Assert aria-roledescription on the drag/resize handle cy.get("#resizable-dialog") .shadow() - .find(".ui5-popup-header-root") - .should("have.attr", "aria-roledescription", "Interactive Header"); + .find(".ui5-popup-drag-resize-handler") + .should("have.attr", "aria-roledescription", "Handle"); }); @@ -1157,10 +1163,10 @@ describe("Acc", () => { cy.get("#resizable-dialog-custom-header").invoke("attr", "open", true); cy.get("#resizable-dialog-custom-header").ui5DialogOpened(); - // Assert aria-describedby and aria-roledescription attributes + // Assert aria-describedby is on the drag/resize handle cy.get("#resizable-dialog-custom-header") .shadow() - .find(".ui5-popup-header-root") + .find(".ui5-popup-drag-resize-handler") .should("have.attr", "aria-describedby") .then($el => { cy.get("#resizable-dialog-custom-header") @@ -1168,15 +1174,16 @@ describe("Acc", () => { .find(".ui5-hidden-text") .should("exist") .then(hiddenText => { - const valueOfTheHiddenText = hiddenText.text(); + const valueOfTheHiddenText = hiddenText.first().text(); cy.wrap(valueOfTheHiddenText).should("equal", "Use Shift+Arrow keys to resize"); }); }); + // Assert aria-roledescription on the drag/resize handle cy.get("#resizable-dialog-custom-header") .shadow() - .find(".ui5-popup-header-root") - .should("have.attr", "aria-roledescription", "Interactive Header"); + .find(".ui5-popup-drag-resize-handler") + .should("have.attr", "aria-roledescription", "Handle"); }); it("tests accessibleName-ref", () => { diff --git a/packages/main/src/Dialog.ts b/packages/main/src/Dialog.ts index 9596e0562044..435de4aaa553 100644 --- a/packages/main/src/Dialog.ts +++ b/packages/main/src/Dialog.ts @@ -18,10 +18,17 @@ import "@ui5/webcomponents-icons/dist/sys-enter-2.js"; import "@ui5/webcomponents-icons/dist/information.js"; import { - DIALOG_HEADER_ARIA_ROLE_DESCRIPTION, - DIALOG_HEADER_ARIA_DESCRIBEDBY_RESIZABLE, - DIALOG_HEADER_ARIA_DESCRIBEDBY_DRAGGABLE, - DIALOG_HEADER_ARIA_DESCRIBEDBY_DRAGGABLE_RESIZABLE, + DIALOG_ARIA_DESCRIBEDBY_RESIZABLE, + DIALOG_ARIA_DESCRIBEDBY_DRAGGABLE, + DIALOG_ARIA_DESCRIBEDBY_DRAGGABLE_RESIZABLE, + DIALOG_ARIA_DESCRIBEDBY_REACH_DRAGGABLE_RESIZABLE, + DIALOG_ARIA_DESCRIBEDBY_REACH_DRAGGABLE, + DIALOG_ARIA_DESCRIBEDBY_REACH_RESIZABLE, + DIALOG_RESIZE_HANDLE_TOOLTIP, + DIALOG_DRAG_AND_RESIZE_HANDLE_ARIA_LABEL, + DIALOG_DRAG_HANDLE_ARIA_LABEL, + DIALOG_RESIZE_HANDLE_ARIA_LABEL, + DIALOG_HANDLE_ARIA_ROLEDESCRIPTION, DIALOG_HEADER_ARIA_LABEL, DIALOG_CONTENT_ARIA_LABEL, DIALOG_FOOTER_ARIA_LABEL, @@ -81,14 +88,14 @@ const ICON_PER_STATE: Record = { * ### Keyboard Handling * * #### Basic Navigation - * When the `ui5-dialog` has the `draggable` property set to `true` and the header is focused, the user can move the dialog + * When the `ui5-dialog` has the `draggable` property set to `true`, the user can move the dialog * with the following keyboard shortcuts: * * - [Up] or [Down] arrow keys - Move the dialog up/down. * - [Left] or [Right] arrow keys - Move the dialog left/right. * * #### Resizing - * When the `ui5-dialog` has the `resizable` property set to `true` and the header is focused, the user can change the size of the dialog + * When the `ui5-dialog` has the `resizable` property set to `true`, the user can change the size of the dialog * with the following keyboard shortcuts: * * - [Shift] + [Up] or [Down] - Decrease/Increase the height of the dialog. @@ -255,24 +262,45 @@ class Dialog extends Popup { return ariaLabelledById; } - get ariaRoleDescriptionHeaderText() { - return (this.resizable || this.draggable) ? Dialog.i18nBundle.getText(DIALOG_HEADER_ARIA_ROLE_DESCRIPTION) : undefined; + get effectiveAriaDescribedBy() { + return this._movable ? `${this._id}-dialog-descr` : undefined; } - get effectiveAriaDescribedBy() { - return (this.resizable || this.draggable) ? `${this._id}-descr` : undefined; + get ariaDescribedByIds() { + return [ + this.ariaDescriptionTextId, + this.effectiveAriaDescribedBy, + ].filter(Boolean).join(" "); + } + + get dialogAriaDescribedByText() { + if (!this._movable) { + return ""; + } + + if (this.resizable && this.draggable) { + return Dialog.i18nBundle.getText(DIALOG_ARIA_DESCRIBEDBY_REACH_DRAGGABLE_RESIZABLE); + } + if (this.draggable) { + return Dialog.i18nBundle.getText(DIALOG_ARIA_DESCRIBEDBY_REACH_DRAGGABLE); + } + if (this.resizable) { + return Dialog.i18nBundle.getText(DIALOG_ARIA_DESCRIBEDBY_REACH_RESIZABLE); + } + + return ""; } - get ariaDescribedByHeaderTextResizable() { - return Dialog.i18nBundle.getText(DIALOG_HEADER_ARIA_DESCRIBEDBY_RESIZABLE); + get ariaDescribedByTextResizable() { + return Dialog.i18nBundle.getText(DIALOG_ARIA_DESCRIBEDBY_RESIZABLE); } - get ariaDescribedByHeaderTextDraggable() { - return Dialog.i18nBundle.getText(DIALOG_HEADER_ARIA_DESCRIBEDBY_DRAGGABLE); + get ariaDescribedByTextDraggable() { + return Dialog.i18nBundle.getText(DIALOG_ARIA_DESCRIBEDBY_DRAGGABLE); } - get ariaDescribedByHeaderTextDraggableAndResizable() { - return Dialog.i18nBundle.getText(DIALOG_HEADER_ARIA_DESCRIBEDBY_DRAGGABLE_RESIZABLE); + get ariaDescribedByTextDraggableAndResizable() { + return Dialog.i18nBundle.getText(DIALOG_ARIA_DESCRIBEDBY_DRAGGABLE_RESIZABLE); } /** @@ -286,14 +314,44 @@ class Dialog extends Popup { return !this.stretch && this.onDesktop && (this.draggable || this.resizable); } - get _headerTabIndex() { + get _dragResizeHandleTabIndex() { return this._movable ? 0 : undefined; } + get _dragResizeHandleAriaLabel() { + if (!this._movable) { + return ""; + } + + if (this.resizable && this.draggable) { + return Dialog.i18nBundle.getText(DIALOG_DRAG_AND_RESIZE_HANDLE_ARIA_LABEL); + } + if (this.draggable) { + return Dialog.i18nBundle.getText(DIALOG_DRAG_HANDLE_ARIA_LABEL); + } + if (this.resizable) { + return Dialog.i18nBundle.getText(DIALOG_RESIZE_HANDLE_ARIA_LABEL); + } + + return ""; + } + + get _dragResizeHandleAriaRoleDescription() { + return this._movable ? Dialog.i18nBundle.getText(DIALOG_HANDLE_ARIA_ROLEDESCRIPTION) : undefined; + } + + get _dragResizeHandleAriaDescribedBy() { + return this._movable ? `${this._id}-descr` : undefined; + } + get _showResizeHandle() { return this.resizable && this.onDesktop; } + get _resizeHandleTooltip() { + return this._showResizeHandle ? Dialog.i18nBundle.getText(DIALOG_RESIZE_HANDLE_TOOLTIP) : undefined; + } + get _minHeight() { let minHeight = Number.parseInt(window.getComputedStyle(this.contentDOM).minHeight); @@ -489,7 +547,12 @@ class Dialog extends Popup { } _onDragOrResizeKeyDown(e: KeyboardEvent) { - if (!this._movable || !Dialog._isHeader(e.target as HTMLElement)) { + if (!this._movable) { + return; + } + + const target = e.target as HTMLElement; + if (!target || target.id !== `${this._id}-dragResizeHandler`) { return; } diff --git a/packages/main/src/DialogTemplate.tsx b/packages/main/src/DialogTemplate.tsx index 3575a770d021..4abfddd66fc2 100644 --- a/packages/main/src/DialogTemplate.tsx +++ b/packages/main/src/DialogTemplate.tsx @@ -19,10 +19,6 @@ function beforeContent(this: Dialog) { id="ui5-popup-header" role="region" aria-label={this._headerAriaLabel} - aria-describedby={this.effectiveAriaDescribedBy} - aria-roledescription={this.ariaRoleDescriptionHeaderText} - tabIndex={this._headerTabIndex} - onKeyDown={this._onDragOrResizeKeyDown} onMouseDown={this._onDragMouseDown} part="header" // state={this.state} @@ -35,16 +31,6 @@ function beforeContent(this: Dialog) { : {this.headerText} } - - {this.resizable ? - this.draggable ? - - : - - : - this.draggable && - - } } ); @@ -66,9 +52,36 @@ function afterContent(this: Dialog) {
} + {this._movable && + <> + + {this.resizable ? + this.draggable ? + + : + + : + this.draggable && + + } + {this.dialogAriaDescribedByText && + + } + + } ); } diff --git a/packages/main/src/i18n/messagebundle.properties b/packages/main/src/i18n/messagebundle.properties index a151c7fc8801..6176393b46c1 100644 --- a/packages/main/src/i18n/messagebundle.properties +++ b/packages/main/src/i18n/messagebundle.properties @@ -832,17 +832,38 @@ MENU_ITEM_END_CONTENT_ACCESSIBLE_NAME=Additional Actions #XACT: ARIA information for the Menu Item loading state MENU_ITEM_LOADING=Loading -#XACT: ARIA announcement for roldesecription attribute of Dialog header -DIALOG_HEADER_ARIA_ROLE_DESCRIPTION=Interactive Header +#XACT: ARIA announcement for describedby attribute of resizable Dialog +DIALOG_ARIA_DESCRIBEDBY_RESIZABLE=Use Shift+Arrow keys to resize -#XACT: ARIA announcement for describedby attribute of resizable Dialog header -DIALOG_HEADER_ARIA_DESCRIBEDBY_RESIZABLE=Use Shift+Arrow keys to resize +#XACT: ARIA announcement for describedby attribute of draggable Dialog +DIALOG_ARIA_DESCRIBEDBY_DRAGGABLE=Use Arrow keys to move -#XACT: ARIA announcement for describedby attribute of draggable Dialog header -DIALOG_HEADER_ARIA_DESCRIBEDBY_DRAGGABLE=Use Arrow keys to move +#XACT: ARIA announcement for describedby attribute of draggable and resizable Dialog +DIALOG_ARIA_DESCRIBEDBY_DRAGGABLE_RESIZABLE=Use Arrow keys to move, Shift+Arrow keys to resize -#XACT: ARIA announcement for describedby attribute of draggable and resizable Dialog header -DIALOG_HEADER_ARIA_DESCRIBEDBY_DRAGGABLE_RESIZABLE=Use Arrow keys to move, Shift+Arrow keys to resize +#XACT: ARIA announcement for describedby attribute informing users how to reach drag and resize handle +DIALOG_ARIA_DESCRIBEDBY_REACH_DRAGGABLE_RESIZABLE=Tab to the end of dialog to reach drag and resize handle. + +#XACT: ARIA announcement for describedby attribute informing users how to reach drag handle +DIALOG_ARIA_DESCRIBEDBY_REACH_DRAGGABLE=Tab to the end of dialog to reach drag handle. + +#XACT: ARIA announcement for describedby attribute informing users how to reach resize handle +DIALOG_ARIA_DESCRIBEDBY_REACH_RESIZABLE=Tab to the end of dialog to reach resize handle. + +#XACT: Tooltip for the resize handle icon in the Dialog +DIALOG_RESIZE_HANDLE_TOOLTIP=Drag handle to resize (Shift+Arrow) + +#XACT: ARIA label for drag and resize handle +DIALOG_DRAG_AND_RESIZE_HANDLE_ARIA_LABEL=Drag and resize dialog + +#XACT: ARIA label for drag handle +DIALOG_DRAG_HANDLE_ARIA_LABEL=Drag dialog + +#XACT: ARIA label for resize handle +DIALOG_RESIZE_HANDLE_ARIA_LABEL=Resize dialog + +#XACT: ARIA role description for drag/resize handle +DIALOG_HANDLE_ARIA_ROLEDESCRIPTION=Handle #XACT: ARIA label for the Dialog header region DIALOG_HEADER_ARIA_LABEL=Header diff --git a/packages/main/src/themes/Dialog.css b/packages/main/src/themes/Dialog.css index a99dfbbcb206..bd4001653e57 100644 --- a/packages/main/src/themes/Dialog.css +++ b/packages/main/src/themes/Dialog.css @@ -102,17 +102,22 @@ outline: none; } -:host([desktop]) .ui5-popup-header-root:focus:after, -.ui5-popup-header-root:focus-visible:after { +.ui5-popup-drag-resize-handler:focus { + outline: none; +} + +.ui5-popup-root:has(.ui5-popup-drag-resize-handler:focus)::before { content: ''; position: absolute; - left: var(--_ui5_dialog_header_focus_left_offset); - bottom: var(--_ui5_dialog_header_focus_bottom_offset); - right: var(--_ui5_dialog_header_focus_right_offset); - top: var(--_ui5_dialog_header_focus_top_offset); + inset: var(--_ui5_dialog_focus_outline_offset); border: var(--sapContent_FocusWidth) var(--sapContent_FocusStyle) var(--sapContent_FocusColor); - border-radius: var(--_ui5_dialog_header_border_radius) var(--_ui5_dialog_header_border_radius) 0 0; + border-radius: var(--sapElement_BorderCornerRadius); pointer-events: none; + z-index: 5; +} + +:host([resizable]) .ui5-popup-root:has(.ui5-popup-drag-resize-handler:focus)::before { + border-end-end-radius: var(--_ui5_dialog_resizable_bottom_right_radius); } :host([stretch]) .ui5-popup-content { diff --git a/packages/main/src/themes/base/Dialog-parameters.css b/packages/main/src/themes/base/Dialog-parameters.css index c001a627e2db..1fe83302ddfc 100644 --- a/packages/main/src/themes/base/Dialog-parameters.css +++ b/packages/main/src/themes/base/Dialog-parameters.css @@ -1,8 +1,5 @@ :host { - --_ui5_dialog_header_focus_bottom_offset: 3px; - --_ui5_dialog_header_focus_top_offset: 2px; - --_ui5_dialog_header_focus_left_offset: 2px; - --_ui5_dialog_header_focus_right_offset: 2px; - --_ui5_dialog_header_border_radius: 0px; --_ui5_dialog_header_state_line_height: 0.0625rem; + --_ui5_dialog_focus_outline_offset: 0.0625rem; + --_ui5_dialog_resizable_bottom_right_radius: 1.75rem; } diff --git a/packages/main/src/themes/sap_horizon/Dialog-parameters.css b/packages/main/src/themes/sap_horizon/Dialog-parameters.css index 460e8d806f59..e2dca7790d2f 100644 --- a/packages/main/src/themes/sap_horizon/Dialog-parameters.css +++ b/packages/main/src/themes/sap_horizon/Dialog-parameters.css @@ -1,9 +1 @@ @import "../base/Dialog-parameters.css"; - -:host { - --_ui5_dialog_header_focus_bottom_offset: 2px; - --_ui5_dialog_header_focus_top_offset: 1px; - --_ui5_dialog_header_focus_left_offset: 1px; - --_ui5_dialog_header_focus_right_offset: 1px; - --_ui5_dialog_header_border_radius: var(--sapElement_BorderCornerRadius); -} \ No newline at end of file diff --git a/packages/main/src/themes/sap_horizon_dark/Dialog-parameters.css b/packages/main/src/themes/sap_horizon_dark/Dialog-parameters.css index 460e8d806f59..e2dca7790d2f 100644 --- a/packages/main/src/themes/sap_horizon_dark/Dialog-parameters.css +++ b/packages/main/src/themes/sap_horizon_dark/Dialog-parameters.css @@ -1,9 +1 @@ @import "../base/Dialog-parameters.css"; - -:host { - --_ui5_dialog_header_focus_bottom_offset: 2px; - --_ui5_dialog_header_focus_top_offset: 1px; - --_ui5_dialog_header_focus_left_offset: 1px; - --_ui5_dialog_header_focus_right_offset: 1px; - --_ui5_dialog_header_border_radius: var(--sapElement_BorderCornerRadius); -} \ No newline at end of file