Skip to content

feat(ui5-button): provide focus support for mobile #11236

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/fiori/test/specs/Wizard.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,15 +134,15 @@ describe("Wizard general interaction", () => {
assert.strictEqual(await step1InHeader.getAttribute("disabled"), null,
"First step in header is enabled.");

assert.ok(await firstFocusableElement.getProperty("focused"), "The First focusable element in the step content is focused.");
assert.ok(await firstFocusableElement.matches(":focus"), "The First focusable element in the step content is focused.");

await step1InHeader.keys(["Shift", "Tab"]);
await step2InHeader.keys("Space");
assert.ok(await firstFocusableElement.getProperty("focused"), "The First focusable element in the step content is focused.");
assert.ok(await firstFocusableElement.matches(":focus"), "The First focusable element in the step content is focused.");

await step1InHeader.keys(["Shift", "Tab"]);
await step2InHeader.keys("Enter");
assert.ok(await firstFocusableElement.getProperty("focused"), "The First focusable element in the step content is focused.");
assert.ok(await firstFocusableElement.matches(":focus"), "The First focusable element in the step content is focused.");

// assert - that second step in the content and in the header are not selected
assert.strictEqual(await step2.getAttribute("selected"), null,
Expand Down
25 changes: 5 additions & 20 deletions packages/main/src/Button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,6 @@ import { markEvent } from "@ui5/webcomponents-base/dist/MarkedEvents.js";
import { getIconAccessibleName } from "@ui5/webcomponents-base/dist/asset-registries/Icons.js";

import {
isPhone,
isTablet,
isCombi,
isDesktop,
isSafari,
} from "@ui5/webcomponents-base/dist/Device.js";
Expand Down Expand Up @@ -258,13 +255,6 @@ class Button extends UI5Element implements IFormElement, IButton {
@property({ type: Boolean })
iconOnly!: boolean;

/**
* Indicates if the elements is on focus
* @private
*/
@property({ type: Boolean })
focused!: boolean;

/**
* Indicates if the elements has a slotted icon
* @private
Expand All @@ -273,7 +263,7 @@ class Button extends UI5Element implements IFormElement, IButton {
hasIcon!: boolean;

/**
* Indicates if the element if focusable
* Indicates if the element is focusable
* @private
*/
@property({ type: Boolean })
Expand Down Expand Up @@ -356,7 +346,9 @@ class Button extends UI5Element implements IFormElement, IButton {
}

onEnterDOM() {
this._isTouch = (isPhone() || isTablet()) && !isCombi();
if (isDesktop()) {
this.setAttribute("desktop", "");
}
}

async onBeforeRendering() {
Expand Down Expand Up @@ -394,7 +386,7 @@ class Button extends UI5Element implements IFormElement, IButton {
}

_onmousedown(e: MouseEvent) {
if (this.nonInteractive || this._isTouch) {
if (this.nonInteractive) {
return;
}

Expand Down Expand Up @@ -453,10 +445,6 @@ class Button extends UI5Element implements IFormElement, IButton {
if (this.active) {
this._setActiveState(false);
}

if (isDesktop()) {
this.focused = false;
}
}

_onfocusin(e: FocusEvent) {
Expand All @@ -465,9 +453,6 @@ class Button extends UI5Element implements IFormElement, IButton {
}

markEvent(e, "button");
if (isDesktop()) {
this.focused = true;
}
}

_setActiveState(active: boolean) {
Expand Down
1 change: 0 additions & 1 deletion packages/main/src/SegmentedButton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,6 @@ class SegmentedButton extends UI5Element {
}

if (isTargetSegmentedButtonItem) {
eventTarget.focus();
this._itemNavigation.setCurrentItem(eventTarget);
this.hasPreviouslyFocusedItem = true;
}
Expand Down
34 changes: 17 additions & 17 deletions packages/main/src/SplitButton.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -20,29 +20,29 @@
@touchstart={{_textButtonPress}}
@mousedown={{_textButtonPress}}
@mouseup={{_textButtonRelease}}
@focusin={{_textButtonFocusIn}}
@focusin={{_onInnerButtonFocusIn}}
@focusout={{_onFocusOut}}
>
{{#if isTextButton}}
<slot></slot>
{{/if}}
</ui5-button>
<ui5-button
class="ui5-split-arrow-button"
design="{{design}}"
icon="slim-arrow-down"
tabindex="-1"
?disabled="{{disabled}}"
?active="{{effectiveActiveArrowButton}}"
tooltip="{{accInfo.arrowButton.title}}"
.accessibilityAttributes={{accInfo.arrowButton.accessibilityAttributes}}
@click="{{_handleArrowButtonAction}}"
@mousedown={{_arrowButtonPress}}
@mouseup={{_arrowButtonRelease}}
@focusin={{_setTabIndexValue}}
@ui5-_active-state-change={{_onArrowButtonActiveStateChange}}
>
</ui5-button>
<ui5-button
class="ui5-split-arrow-button"
design="{{design}}"
icon="slim-arrow-down"
tabindex="-1"
?disabled="{{disabled}}"
?active="{{effectiveActiveArrowButton}}"
tooltip="{{accInfo.arrowButton.title}}"
.accessibilityAttributes={{accInfo.arrowButton.accessibilityAttributes}}
@click="{{_handleArrowButtonAction}}"
@mousedown={{_arrowButtonPress}}
@mouseup={{_arrowButtonRelease}}
@focusin={{_onInnerButtonFocusIn}}
@ui5-_active-state-change={{_onArrowButtonActiveStateChange}}
>
</ui5-button>
<span id="{{_id}}-invisibleText" class="ui5-hidden-text">{{accInfo.root.description}} {{accInfo.root.keyboardHint}} {{accessibleName}}</span>
<span id="{{_id}}-invisibleTextDefault" class="ui5-hidden-text">{{textButtonAccText}}</span>

Expand Down
63 changes: 19 additions & 44 deletions packages/main/src/SplitButton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,14 +157,6 @@ class SplitButton extends UI5Element {
@property({ defaultValue: undefined })
accessibleName?: string;

/**
* Indicates if the elements is on focus
* @default false
* @private
*/
@property({ type: Boolean })
focused!: boolean;

/**
* Accessibility-related properties for inner elements of the Split Button
* @private
Expand Down Expand Up @@ -229,7 +221,7 @@ class SplitButton extends UI5Element {
@slot({ type: Node, "default": true })
text!: Array<Node>;

_textButtonPress: { handleEvent: () => void, passive: boolean };
_textButtonPress: { handleEvent: (e: MouseEvent) => void, passive: boolean };
_isDefaultActionPressed = false;
_isKeyDownOperation = false;

Expand All @@ -242,9 +234,9 @@ class SplitButton extends UI5Element {
constructor() {
super();

const handleTouchStartEvent = () => {
const handleTouchStartEvent = (e: MouseEvent) => {
e.stopPropagation();
this._textButtonActive = true;
this.focused = false;
this._tabIndex = "-1";
};

Expand All @@ -254,18 +246,6 @@ class SplitButton extends UI5Element {
};
}

/**
* Function that makes sure the focus is properly managed.
* @private
*/
_manageFocus(button?: Button | SplitButton) {
const buttons: Array<Button | SplitButton> = [this.textButton!, this.arrowButton!, this];

buttons.forEach(btn => {
btn.focused = btn === button;
});
}

onBeforeRendering() {
this._textButtonIcon = this.textButton && this.activeIcon !== "" && (this._textButtonActive) && !this._shiftOrEscapePressed ? this.activeIcon : this.icon;
if (this.disabled) {
Expand All @@ -274,9 +254,6 @@ class SplitButton extends UI5Element {
}

_handleMouseClick(e: MouseEvent) {
const target = e.target as Button;

this._manageFocus(target);
this._fireClick(e);
}

Expand All @@ -287,22 +264,20 @@ class SplitButton extends UI5Element {

this._shiftOrEscapePressed = false;
this._setTabIndexValue();
this._manageFocus();
}

_onFocusIn(e: FocusEvent) {
if (this.disabled || getEventMark(e)) {
return;
}
this._shiftOrEscapePressed = false;
this._manageFocus(this);
}

_textButtonFocusIn(e?: FocusEvent) {
e?.stopPropagation();
this._manageFocus(this.textButton!);

this._setTabIndexValue();
_onInnerButtonFocusIn(e: FocusEvent) {
e.stopPropagation();
this._setTabIndexValue(true);
const target = e.target as Button;
target.focus();
}

_onKeyDown(e: KeyboardEvent) {
Expand Down Expand Up @@ -374,8 +349,7 @@ class SplitButton extends UI5Element {
}

_arrowButtonPress(e: MouseEvent) {
e.preventDefault();
this.arrowButton!.focus();
e.stopPropagation();

this._tabIndex = "-1";
}
Expand All @@ -386,10 +360,10 @@ class SplitButton extends UI5Element {
this._tabIndex = "-1";
}

_setTabIndexValue() {
_setTabIndexValue(innerButtonPressed?: boolean) {
this._tabIndex = this.disabled ? "-1" : "0";

if (this._tabIndex === "-1" && (this.textButton?.focused || this.arrowButton?.focused)) {
if (this._tabIndex === "-1" && innerButtonPressed) {
this._tabIndex = "0";
}
}
Expand Down Expand Up @@ -450,20 +424,21 @@ class SplitButton extends UI5Element {
_handleDefaultAction(e: KeyboardEvent) {
e.preventDefault();
const wasSpacePressed = isSpace(e);
const target = e.target as Button;

if (this.focused || this.textButton?.focused) {
this._textButtonActive = true;
this._fireClick();
if (wasSpacePressed) {
this._spacePressed = true;
}
} else if (this.arrowButton && this.arrowButton.focused) {
if (this.arrowButton && target === this.arrowButton) {
this._activeArrowButton = true;
this._fireArrowClick();
if (wasSpacePressed) {
this._spacePressed = true;
this._textButtonActive = false;
}
} else {
this._textButtonActive = true;
this._fireClick();
if (wasSpacePressed) {
this._spacePressed = true;
}
}
}

Expand Down
36 changes: 24 additions & 12 deletions packages/main/src/themes/Button.css
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,12 @@
pointer-events: none;
}

:host([focused]:not([active])) .ui5-button-root:after,
:host([focused][active][design="Emphasized"]) .ui5-button-root:after,
:host([focused][active]) .ui5-button-root:before {
:host([desktop]:not([active])) .ui5-button-root:focus-within:after,
:host(:not([active])) .ui5-button-root:focus-visible:after,
:host([desktop][active][design="Emphasized"]) .ui5-button-root:focus-within:after,
:host([active][design="Emphasized"]) .ui5-button-root:focus-visible:after,
:host([desktop][active]) .ui5-button-root:focus-within:before,
:host([active]) .ui5-button-root:focus-visible:before {
content: "";
position: absolute;
box-sizing: border-box;
Expand All @@ -125,15 +128,18 @@
border-radius: var(--_ui5_button_focused_border_radius);
}

:host([focused][active]) .ui5-button-root:before {
:host([desktop][active]) .ui5-button-root:focus-within:before,
:host([active]) .ui5-button-root:focus-visible:before {
border-color: var(--_ui5_button_pressed_focused_border_color);
}

:host([design="Emphasized"][focused]) .ui5-button-root:after {
:host([design="Emphasized"][desktop]) .ui5-button-root:focus-within:after,
:host([design="Emphasized"]) .ui5-button-root:focus-visible:after {
border-color: var(--_ui5_button_emphasized_focused_border_color);
}

:host([design="Emphasized"][focused]) .ui5-button-root:before {
:host([design="Emphasized"][desktop]) .ui5-button-root:focus-within:before,
:host([design="Emphasized"]) .ui5-button-root:focus-visible:before {
content: "";
position: absolute;
box-sizing: border-box;
Expand Down Expand Up @@ -246,13 +252,15 @@ bdi {
color: var(--sapButton_Emphasized_Active_TextColor);
}

:host([design="Emphasized"][focused]) .ui5-button-root:after {
:host([design="Emphasized"][desktop]) .ui5-button-root:focus-within:after,
:host([design="Emphasized"]) .ui5-button-root:focus-visible:after {
border-color: var(--_ui5_button_emphasized_focused_border_color);
outline: none;
}

/* Belize related */
:host([design="Emphasized"][focused][active]:not([non-interactive])) .ui5-button-root:after {
:host([design="Emphasized"][desktop][active]:not([non-interactive])) .ui5-button-root:focus-within:after,
:host([design="Emphasized"][active]:not([non-interactive])) .ui5-button-root:focus-visible:after {
border-color: var(--_ui5_button_emphasized_focused_active_border_color);
}

Expand All @@ -277,18 +285,22 @@ bdi {
}

/* SegmentedButton and ToggleButton */
:host([ui5-segmented-button-item][active][focused]) .ui5-button-root:after,
:host([pressed][focused]) .ui5-button-root:after {
:host([ui5-segmented-button-item][active][desktop]) .ui5-button-root:focus-within:after,
:host([ui5-segmented-button-item][active]) .ui5-button-root:focus-visible:after,
:host([pressed][desktop]) .ui5-button-root:focus-within:after,
:host([pressed]) .ui5-button-root:focus-visible:after {
border-color: var(--_ui5_button_pressed_focused_border_color);
outline: none;
}

:host([ui5-segmented-button-item][focused]:not(:last-child)) .ui5-button-root:after {
:host([ui5-segmented-button-item][desktop]:not(:last-child)) .ui5-button-root:focus-within:after,
:host([ui5-segmented-button-item]:not(:last-child)) .ui5-button-root:focus-visible:after {
border-top-right-radius: var(--_ui5_button_focused_inner_border_radius);
border-bottom-right-radius: var(--_ui5_button_focused_inner_border_radius);
}

:host([ui5-segmented-button-item][focused]:not(:first-child)) .ui5-button-root:after {
:host([ui5-segmented-button-item][desktop]:not(:first-child)) .ui5-button-root:focus-within:after,
:host([ui5-segmented-button-item]:not(:first-child)) .ui5-button-root:focus-visible:after {
border-top-left-radius: var(--_ui5_button_focused_inner_border_radius);
border-bottom-left-radius: var(--_ui5_button_focused_inner_border_radius);
}
Loading