From d81db390e86199658f28a0f44acdfd7d911f7747 Mon Sep 17 00:00:00 2001 From: Siyana Todorova Date: Wed, 3 Jun 2026 17:04:36 +0300 Subject: [PATCH 1/3] feat(ui5-dialog): focus goes on entire dialog for drag and resize operations JIRA: BGSOFUIRODOPI-3659 --- packages/main/src/Dialog.ts | 79 +++++++++++++++++-- packages/main/src/DialogTemplate.tsx | 42 ++++++---- .../main/src/i18n/messagebundle.properties | 25 ++++++ packages/main/src/themes/Dialog.css | 31 ++++++-- .../src/themes/base/Dialog-parameters.css | 2 + 5 files changed, 151 insertions(+), 28 deletions(-) diff --git a/packages/main/src/Dialog.ts b/packages/main/src/Dialog.ts index 89bd2b46ca4b..1f4e901dc82e 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_DRAGGABLE_RESIZABLE, + DIALOG_ARIA_DESCRIBEDBY_DRAGGABLE, + DIALOG_ARIA_DESCRIBEDBY_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, } from "./generated/i18n/i18n-defaults.js"; // Template @@ -252,12 +259,33 @@ 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_DRAGGABLE_RESIZABLE); + } + if (this.draggable) { + return Dialog.i18nBundle.getText(DIALOG_ARIA_DESCRIBEDBY_DRAGGABLE); + } + if (this.resizable) { + return Dialog.i18nBundle.getText(DIALOG_ARIA_DESCRIBEDBY_RESIZABLE); + } + + return ""; } get ariaDescribedByHeaderTextResizable() { @@ -284,13 +312,47 @@ class Dialog extends Popup { } get _headerTabIndex() { + return undefined; + } + + 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); @@ -470,7 +532,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 31bdd6f54300..2fa65078cdd8 100644 --- a/packages/main/src/DialogTemplate.tsx +++ b/packages/main/src/DialogTemplate.tsx @@ -18,11 +18,6 @@ function beforeContent(this: Dialog) {
{this.headerText} } - - {this.resizable ? - this.draggable ? - - : - - : - this.draggable && - - }
} @@ -62,9 +47,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 b43e02c4c878..f86c9b043522 100644 --- a/packages/main/src/i18n/messagebundle.properties +++ b/packages/main/src/i18n/messagebundle.properties @@ -840,6 +840,31 @@ DIALOG_HEADER_ARIA_DESCRIBEDBY_DRAGGABLE=Use Arrow keys to move #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_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_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_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 + #XFLD: A colon to separate the "label" from an input. In some languages there might be a different symbol used for such a colon LABEL_COLON=: diff --git a/packages/main/src/themes/Dialog.css b/packages/main/src/themes/Dialog.css index a99dfbbcb206..72defd5111fa 100644 --- a/packages/main/src/themes/Dialog.css +++ b/packages/main/src/themes/Dialog.css @@ -102,17 +102,34 @@ outline: none; } -:host([desktop]) .ui5-popup-header-root:focus:after, -.ui5-popup-header-root:focus-visible:after { +.ui5-popup-drag-resize-handler { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.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..7ce95a6c2f9e 100644 --- a/packages/main/src/themes/base/Dialog-parameters.css +++ b/packages/main/src/themes/base/Dialog-parameters.css @@ -5,4 +5,6 @@ --_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; } From 1ee962cc908f91f7acc2da77cc537993029a9fe6 Mon Sep 17 00:00:00 2001 From: Siyana Todorova Date: Thu, 4 Jun 2026 15:52:45 +0300 Subject: [PATCH 2/3] fix: cypress tests update --- packages/main/cypress/specs/Dialog.cy.tsx | 57 ++++++++++++----------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/packages/main/cypress/specs/Dialog.cy.tsx b/packages/main/cypress/specs/Dialog.cy.tsx index 686ad9cef953..3674bd78ca28 100644 --- a/packages/main/cypress/specs/Dialog.cy.tsx +++ b/packages/main/cypress/specs/Dialog.cy.tsx @@ -580,11 +580,11 @@ 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 using keyboard + cy.get("#draggable-dialog").shadow().find(".ui5-popup-drag-resize-handler").focus(); - cy.get("#header-slot").focused().realPress("{uparrow}"); - cy.get("#header-slot").focused().realPress("{uparrow}"); + cy.get("#draggable-dialog").shadow().find(".ui5-popup-drag-resize-handler").focused().realPress("{uparrow}"); + cy.get("#draggable-dialog").shadow().find(".ui5-popup-drag-resize-handler").focused().realPress("{uparrow}"); // Assert - Top position changes, left remains the same @@ -599,10 +599,10 @@ describe("Dialog general interaction", () => { }) // Act - Move dialog left using keyboard - cy.get("#header-slot").realClick(); + cy.get("#draggable-dialog").shadow().find(".ui5-popup-drag-resize-handler").focus(); - cy.get("#header-slot").focused().realPress("{leftarrow}"); - cy.get("#header-slot").focused().realPress("{leftarrow}"); + cy.get("#draggable-dialog").shadow().find(".ui5-popup-drag-resize-handler").focused().realPress("{leftarrow}"); + cy.get("#draggable-dialog").shadow().find(".ui5-popup-drag-resize-handler").focused().realPress("{leftarrow}"); // Assert - Left position changes, top remains the same cy.get("#draggable-dialog") @@ -776,8 +776,8 @@ 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(); + // Act - Focus the drag/resize handle and resize height using keyboard + cy.get("#resizable-dialog").shadow().find(".ui5-popup-drag-resize-handler").focus(); cy.get("#resizable-dialog").realPress(["Shift", "ArrowDown"]); // Assert - Height changes, width and position remain the same @@ -790,8 +790,8 @@ describe("Dialog general interaction", () => { expect(heightAfterResizeHeight).not.to.equal(initialHeight); expect(leftAfterResizeHeight).to.equal(initialLeft); - // Act - Resize width using keyboard - cy.get("#resizable-dialog").shadow().find(".ui5-popup-resize-handle").click(); + // Act - Focus the drag/resize handle and resize width using keyboard + cy.get("#resizable-dialog").shadow().find(".ui5-popup-drag-resize-handler").focus(); cy.get("#resizable-dialog").realPress(["Shift", "ArrowRight"]); // Assert - Width changes, height and position remain the same @@ -1078,30 +1078,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 +1122,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 +1133,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 +1161,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 +1172,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", () => { From c3dff78d2f8d5804771729260e47d453f5a0e440 Mon Sep 17 00:00:00 2001 From: Siyana Todorova Date: Fri, 5 Jun 2026 12:27:43 +0300 Subject: [PATCH 3/3] fix: cypress tests --- packages/main/cypress/specs/Dialog.cy.tsx | 26 ++++++++++------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/packages/main/cypress/specs/Dialog.cy.tsx b/packages/main/cypress/specs/Dialog.cy.tsx index 3674bd78ca28..a1022e4b243b 100644 --- a/packages/main/cypress/specs/Dialog.cy.tsx +++ b/packages/main/cypress/specs/Dialog.cy.tsx @@ -580,11 +580,10 @@ describe("Dialog general interaction", () => { const initialTop = parseInt(dialog.css("top")); const initialLeft = parseInt(dialog.css("left")); - // Act - Focus the drag/resize handle and move dialog up using keyboard - cy.get("#draggable-dialog").shadow().find(".ui5-popup-drag-resize-handler").focus(); - - cy.get("#draggable-dialog").shadow().find(".ui5-popup-drag-resize-handler").focused().realPress("{uparrow}"); - cy.get("#draggable-dialog").shadow().find(".ui5-popup-drag-resize-handler").focused().realPress("{uparrow}"); + // Act - Tab to the drag/resize handle and move dialog up + cy.realPress("Tab"); + cy.realPress("{uparrow}"); + cy.realPress("{uparrow}"); // Assert - Top position changes, left remains the same @@ -599,10 +598,8 @@ describe("Dialog general interaction", () => { }) // Act - Move dialog left using keyboard - cy.get("#draggable-dialog").shadow().find(".ui5-popup-drag-resize-handler").focus(); - - cy.get("#draggable-dialog").shadow().find(".ui5-popup-drag-resize-handler").focused().realPress("{leftarrow}"); - cy.get("#draggable-dialog").shadow().find(".ui5-popup-drag-resize-handler").focused().realPress("{leftarrow}"); + cy.realPress("{leftarrow}"); + cy.realPress("{leftarrow}"); // Assert - Left position changes, top remains the same cy.get("#draggable-dialog") @@ -776,9 +773,9 @@ describe("Dialog general interaction", () => { const initialTop = parseInt(dialog.css("top")); const initialLeft = parseInt(dialog.css("left")); - // Act - Focus the drag/resize handle and resize height using keyboard - cy.get("#resizable-dialog").shadow().find(".ui5-popup-drag-resize-handler").focus(); - cy.get("#resizable-dialog").realPress(["Shift", "ArrowDown"]); + // Act - Tab to the drag/resize handle and resize height + cy.realPress("Tab"); + cy.realPress(["Shift", "ArrowDown"]); // Assert - Height changes, width and position remain the same cy.get("#resizable-dialog").then(dialogAfterResizeHeight => { @@ -790,9 +787,8 @@ describe("Dialog general interaction", () => { expect(heightAfterResizeHeight).not.to.equal(initialHeight); expect(leftAfterResizeHeight).to.equal(initialLeft); - // Act - Focus the drag/resize handle and resize width using keyboard - cy.get("#resizable-dialog").shadow().find(".ui5-popup-drag-resize-handler").focus(); - cy.get("#resizable-dialog").realPress(["Shift", "ArrowRight"]); + // Act - Resize width using keyboard + cy.realPress(["Shift", "ArrowRight"]); // Assert - Width changes, height and position remain the same cy.get("#resizable-dialog").then(dialogAfterResizeWidth => {