Skip to content

feat(ui5-button): make click button preventable #11318

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

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
11 changes: 6 additions & 5 deletions packages/fiori/src/ShellBar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import type {
ClassMap,
AccessibilityAttributes,
AriaRole,
UI5CustomEvent,
} from "@ui5/webcomponents-base";
import type ListItemBase from "@ui5/webcomponents/dist/ListItemBase.js";
import type PopoverHorizontalAlign from "@ui5/webcomponents/dist/types/PopoverHorizontalAlign.js";
Expand Down Expand Up @@ -127,7 +128,7 @@ interface IShelBarItemInfo extends IShellBarHidableItem {
title?: string,
stableDomRef?: string,
refItemid?: string,
press: (e: MouseEvent) => void,
press: (e: UI5CustomEvent<Button, "click">) => void,
order?: number,
profile?: boolean,
tooltip?: string,
Expand Down Expand Up @@ -897,7 +898,7 @@ class ShellBar extends UI5Element {
this._defaultItemPressPrevented = false;
}

_handleCustomActionPress(e: MouseEvent) {
_handleCustomActionPress(e: UI5CustomEvent<Button, "click">) {
const target = e.target as HTMLElement;
const refItemId = target.getAttribute("data-ui5-external-action-item-id");

Expand All @@ -916,7 +917,7 @@ class ShellBar extends UI5Element {
this._toggleActionPopover();
}

_handleNotificationsPress(e: MouseEvent) {
_handleNotificationsPress(e: UI5CustomEvent<Button, "click">) {
const notificationIconRef = this.shadowRoot!.querySelector<Button>(".ui5-shellbar-bell-button")!,
target = e.target as HTMLElement;

Expand All @@ -935,7 +936,7 @@ class ShellBar extends UI5Element {
this.showSearchField = false;
}

_handleProductSwitchPress(e: MouseEvent) {
_handleProductSwitchPress(e: UI5CustomEvent<Button, "click">) {
const buttonRef = this.shadowRoot!.querySelector<Button>(".ui5-shellbar-button-product-switch")!,
target = e.target as HTMLElement;

Expand Down Expand Up @@ -1426,7 +1427,7 @@ class ShellBar extends UI5Element {
return this.contentItems.length > 0;
}

get hidableDomElements(): HTMLElement [] {
get hidableDomElements(): HTMLElement[] {
const items = Array.from(this.shadowRoot!.querySelectorAll<HTMLElement>(".ui5-shellbar-button:not(.ui5-shellbar-search-button):not(.ui5-shellbar-overflow-button):not(.ui5-shellbar-cancel-button):not(.ui5-shellbar-no-overflow-button)"));
const assistant = this.shadowRoot!.querySelector<HTMLElement>(".ui5-shellbar-assistant-button");
const searchButton = this.shadowRoot!.querySelector<HTMLElement>(".ui5-shellbar-search-button");
Expand Down
9 changes: 5 additions & 4 deletions packages/fiori/src/ShellBarItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
import property from "@ui5/webcomponents-base/dist/decorators/property.js";
import event from "@ui5/webcomponents-base/dist/decorators/event-strict.js";
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
import type { AccessibilityAttributes } from "@ui5/webcomponents-base";
import type { AccessibilityAttributes, UI5CustomEvent } from "@ui5/webcomponents-base";
import type Button from "@ui5/webcomponents/dist/Button.js";

type ShellBarItemClickEventDetail = {
targetRef: HTMLElement,
Expand Down Expand Up @@ -52,8 +53,8 @@ class ShellBarItem extends UI5Element {

/**
* Defines the item text.
*
* **Note:** The text is only displayed inside the overflow popover list view.
*
* **Note:** The text is only displayed inside the overflow popover list view.
* @default undefined
* @public
*/
Expand Down Expand Up @@ -97,7 +98,7 @@ class ShellBarItem extends UI5Element {
return this.getAttribute("stable-dom-ref") || `${this._id}-stable-dom-ref`;
}

fireClickEvent(e: MouseEvent) {
fireClickEvent(e: UI5CustomEvent<Button, "click">) {
return this.fireDecoratorEvent("click", {
targetRef: (e.target as HTMLElement),
});
Expand Down
3 changes: 2 additions & 1 deletion packages/fiori/src/UploadCollectionItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type Input from "@ui5/webcomponents/dist/Input.js";
import ListItem from "@ui5/webcomponents/dist/ListItem.js";
import getFileExtension from "@ui5/webcomponents-base/dist/util/getFileExtension.js";
import { renderFinished } from "@ui5/webcomponents-base/dist/Render.js";
import type { UI5CustomEvent } from "@ui5/webcomponents-base";
import {
isDelete,
isEnter,
Expand Down Expand Up @@ -303,7 +304,7 @@ class UploadCollectionItem extends ListItem {
}
}

async _onRenameCancel(e: KeyboardEvent | MouseEvent) {
async _onRenameCancel(e: KeyboardEvent | UI5CustomEvent<Button, "click">) {
this._editing = false;

if (isEscape(e as KeyboardEvent)) {
Expand Down
4 changes: 3 additions & 1 deletion packages/fiori/src/Wizard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ import ItemNavigation from "@ui5/webcomponents-base/dist/delegate/ItemNavigation
import NavigationMode from "@ui5/webcomponents-base/dist/types/NavigationMode.js";
import clamp from "@ui5/webcomponents-base/dist/util/clamp.js";
import ResizeHandler from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js";
import type { UI5CustomEvent } from "@ui5/webcomponents-base";
import type { ResizeObserverCallback } from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js";
import debounce from "@ui5/webcomponents-base/dist/util/debounce.js";
import { getFirstFocusableElement } from "@ui5/webcomponents-base/dist/util/FocusableElements.js";
import type ResponsivePopover from "@ui5/webcomponents/dist/ResponsivePopover.js";
import type Button from "@ui5/webcomponents/dist/Button.js";
import type WizardContentLayout from "./types/WizardContentLayout.js";
import "./WizardStep.js";

Expand Down Expand Up @@ -602,7 +604,7 @@ class Wizard extends UI5Element {
}
}

_onOverflowStepButtonClick(e: MouseEvent) {
_onOverflowStepButtonClick(e: UI5CustomEvent<Button, "click">) {
const tabs = Array.from(this.stepsInHeaderDOM);
const eTarget = e.target as HTMLElement;
const stepRefId = eTarget.getAttribute("data-ui5-header-tab-ref-id");
Expand Down
8 changes: 8 additions & 0 deletions packages/main/cypress/specs/Card.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,14 @@ describe("Card general interaction", () => {
</Card>
);

cy.get("#actionBtn").then($header => {
// Buttons have the same button which bubbles
$header.get(0).addEventListener("ui5-click", (e) => {
e.stopImmediatePropagation();
e.stopPropagation();
});
});

cy.get("#cardHeader").then($header => {
$header.get(0).addEventListener("ui5-click", cy.stub().as("headerClick"));
});
Expand Down
33 changes: 33 additions & 0 deletions packages/main/cypress/specs/FormSupport.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -875,6 +875,39 @@ describe("Form support", () => {
.should("be.equal", "time_picker3=ok&time_picker4=1:10:10 PM");
});

it("Button's click doesn't submit form on prevent default", () => {
cy.mount(<form method="get">
<Button id="b1" type="Submit">Preventable button</Button>
</form>);

cy.get("#b1")
.then($item => {
$item.get(0).addEventListener("ui5-click", e => e.preventDefault());
$item.get(0).addEventListener("ui5-click", cy.stub().as("click"));
});

cy.get("form")
.then($item => {
$item.get(0).addEventListener("submit", e => e.preventDefault());
$item.get(0).addEventListener("submit", cy.stub().as("submit"));
});

cy.get("#b1")
.realClick();

cy.get("#b1")
.realPress("Enter");

cy.get("#b1")
.realPress("Space");

cy.get("@click")
.should("have.been.calledThrice");

cy.get("@submit")
.should("have.not.been.called");
});

it("Normal button does not submit forms", () => {
cy.mount(<form method="get">
<Button id="b1">Does not submit forms</Button>
Expand Down
7 changes: 4 additions & 3 deletions packages/main/src/AvatarGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js";
import ResizeHandler from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js";
import ItemNavigation from "@ui5/webcomponents-base/dist/delegate/ItemNavigation.js";
import type { ITabbable } from "@ui5/webcomponents-base/dist/delegate/ItemNavigation.js";
import type { UI5CustomEvent } from "@ui5/webcomponents-base";
import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
import property from "@ui5/webcomponents-base/dist/decorators/property.js";
Expand Down Expand Up @@ -190,8 +191,8 @@ class AvatarGroup extends UI5Element {
* @since 2.0.0
* @default {}
*/
@property({ type: Object })
accessibilityAttributes: AvatarGroupAccessibilityAttributes = {};
@property({ type: Object })
accessibilityAttributes: AvatarGroupAccessibilityAttributes = {};

/**
* @private
Expand Down Expand Up @@ -433,7 +434,7 @@ class AvatarGroup extends UI5Element {
e.stopPropagation();
}

onOverflowButtonClick(e: MouseEvent) {
onOverflowButtonClick(e: UI5CustomEvent<Button, "click">) {
e.stopPropagation();

this.fireDecoratorEvent("click", {
Expand Down
1 change: 1 addition & 0 deletions packages/main/src/AvatarGroupTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export default function AvatarGroupTemplate(this: AvatarGroup) {
<slot onClick={this.onAvatarClick} onui5-click={this.onAvatarUI5Click}></slot>

{this._customOverflowButton ?
// @ts-expect-error
<slot onClick={this.onOverflowButtonClick} name="overflowButton"></slot>
:
<Button
Expand Down
50 changes: 49 additions & 1 deletion packages/main/src/Button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ let activeButton: Button | null = null;

type ButtonAccessibilityAttributes = Pick<AccessibilityAttributes, "expanded" | "hasPopup" | "controls">;

type ButtonClickEventDetail = {
originalEvent: MouseEvent,
altKey: boolean;
ctrlKey: boolean;
metaKey: boolean;
shiftKey: boolean;
}

/**
* @class
*
Expand Down Expand Up @@ -92,6 +100,23 @@ type ButtonAccessibilityAttributes = Pick<AccessibilityAttributes, "expanded" |
styles: buttonCss,
shadowRootOptions: { delegatesFocus: true },
})
/**
* Fired when the component is activated either with a mouse/tap or by using the Enter or Space key.
*
* **Note:** The event will not be fired if the `disabled` property is set to `true`.
*
* @since 2.10.0
* @public
* @param {Event} originalEvent Returns original event that comes from user's **click** interaction
* @param {boolean} altKey Returns whether the "ALT" key was pressed when the event was triggered.
* @param {boolean} ctrlKey Returns whether the "CTRL" key was pressed when the event was triggered.
* @param {boolean} metaKey Returns whether the "META" key was pressed when the event was triggered.
* @param {boolean} shiftKey Returns whether the "SHIFT" key was pressed when the event was triggered.
*/
@event("click", {
bubbles: true,
cancelable: true,
})
/**
* Fired whenever the active state of the component changes.
* @private
Expand All @@ -102,6 +127,7 @@ type ButtonAccessibilityAttributes = Pick<AccessibilityAttributes, "expanded" |
})
class Button extends UI5Element implements IButton {
eventDetails!: {
"click": ButtonClickEventDetail,
"active-state-change": void,
};

Expand Down Expand Up @@ -380,11 +406,32 @@ class Button extends UI5Element implements IButton {
}
}

_onclick() {
_onclick(e: MouseEvent) {
e.stopImmediatePropagation();

if (this.nonInteractive) {
return;
}

const {
altKey,
ctrlKey,
metaKey,
shiftKey,
} = e;

const prevented = !this.fireDecoratorEvent("click", {
originalEvent: e,
altKey,
ctrlKey,
metaKey,
shiftKey,
});

if (prevented) {
return;
}

if (this._isSubmit) {
submitForm(this);
}
Expand Down Expand Up @@ -548,5 +595,6 @@ Button.define();
export default Button;
export type {
ButtonAccessibilityAttributes,
ButtonClickEventDetail,
IButton,
};
3 changes: 2 additions & 1 deletion packages/main/src/Carousel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
isUp,
isF7,
} from "@ui5/webcomponents-base/dist/Keys.js";
import type { UI5CustomEvent } from "@ui5/webcomponents-base";
import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js";
import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
import ScrollEnablement from "@ui5/webcomponents-base/dist/delegate/ScrollEnablement.js";
Expand Down Expand Up @@ -496,7 +497,7 @@ class Carousel extends UI5Element {
}
}

_navButtonClick(e: MouseEvent) {
_navButtonClick(e: UI5CustomEvent<Button, "click">) {
const button = e.target as Button;
if (button.hasAttribute("data-ui5-arrow-forward")) {
this.navigateRight();
Expand Down
4 changes: 3 additions & 1 deletion packages/main/src/ExpandableText.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js";
import jsxRender from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js";
import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
import { isPhone } from "@ui5/webcomponents-base/dist/Device.js";
import type { UI5CustomEvent } from "@ui5/webcomponents-base";
import type { LinkAccessibilityAttributes } from "./Link.js";
import ExpandableTextOverflowMode from "./types/ExpandableTextOverflowMode.js";
import type Button from "./Button.js";
import type TextEmptyIndicatorMode from "./types/TextEmptyIndicatorMode.js";
import {
EXPANDABLE_TEXT_SHOW_LESS,
Expand Down Expand Up @@ -168,7 +170,7 @@ class ExpandableText extends UI5Element {
this._expanded = !this._expanded;
}

_handleCloseButtonClick(e: MouseEvent) {
_handleCloseButtonClick(e: UI5CustomEvent<Button, "click">) {
this._expanded = false;
e.stopPropagation();
}
Expand Down
3 changes: 2 additions & 1 deletion packages/main/src/MultiComboBox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import jsxRender from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js";
import ResizeHandler from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js";
import type { ResizeObserverCallback } from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js";
import ValueState from "@ui5/webcomponents-base/dist/types/ValueState.js";
import type { UI5CustomEvent } from "@ui5/webcomponents-base";
import {
isShow,
isDown,
Expand Down Expand Up @@ -638,7 +639,7 @@ class MultiComboBox extends UI5Element implements IFormInputElement {
this._toggleTokenizerPopover();
}

filterSelectedItems(e: MouseEvent) {
filterSelectedItems(e: UI5CustomEvent<ToggleButton, "click">) {
this.filterSelected = (e.target as ToggleButton).pressed;
const selectedItems = this._filteredItems.filter(item => item.selected);

Expand Down
6 changes: 4 additions & 2 deletions packages/main/src/Panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import AnimationMode from "@ui5/webcomponents-base/dist/types/AnimationMode.js";
import { getAnimationMode } from "@ui5/webcomponents-base/dist/config/AnimationMode.js";
import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js";
import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
import type { UI5CustomEvent } from "@ui5/webcomponents-base";
import type TitleLevel from "./types/TitleLevel.js";
import type Button from "./Button.js";
import type PanelAccessibleRole from "./types/PanelAccessibleRole.js";
import PanelTemplate from "./PanelTemplate.js";
import { PANEL_ICON } from "./generated/i18n/i18n-defaults.js";
Expand Down Expand Up @@ -234,8 +236,8 @@ class Panel extends UI5Element {
this._toggleOpen();
}

_toggleButtonClick(e: MouseEvent) {
if (e.x === 0 && e.y === 0) {
_toggleButtonClick(e: UI5CustomEvent<Button, "click">) {
if (e.detail.originalEvent.x === 0 && e.detail.originalEvent.y === 0) {
e.stopImmediatePropagation();
}
}
Expand Down
Loading
Loading