diff --git a/src/components/autocomplete/autocomplete.e2e.ts b/src/components/autocomplete/autocomplete.e2e.ts index 81434ef31b4..1b5027a0948 100644 --- a/src/components/autocomplete/autocomplete.e2e.ts +++ b/src/components/autocomplete/autocomplete.e2e.ts @@ -176,4 +176,34 @@ describe('sbb-autocomplete', () => { await waitForLitRender(element); expect(input).to.have.attribute('aria-expanded', 'false'); }); + + it('does not open if prevented', async () => { + const willOpenEventSpy = new EventSpy(SbbAutocomplete.events.willOpen); + + element.addEventListener(SbbAutocomplete.events.willOpen, (ev) => ev.preventDefault()); + element.open(); + + await waitForCondition(() => willOpenEventSpy.events.length === 1); + expect(willOpenEventSpy.count).to.be.equal(1); + await waitForLitRender(element); + + expect(element).to.have.attribute('data-state', 'closed'); + }); + + it('does not close if prevented', async () => { + const didOpenEventSpy = new EventSpy(SbbAutocomplete.events.didOpen); + const willCloseEventSpy = new EventSpy(SbbAutocomplete.events.willClose); + + element.open(); + await waitForCondition(() => didOpenEventSpy.events.length === 1); + await waitForLitRender(element); + + element.addEventListener(SbbAutocomplete.events.willClose, (ev) => ev.preventDefault()); + element.close(); + + await waitForCondition(() => willCloseEventSpy.events.length === 1); + await waitForLitRender(element); + + expect(element).to.have.attribute('data-state', 'opened'); + }); }); diff --git a/src/components/autocomplete/autocomplete.ts b/src/components/autocomplete/autocomplete.ts index fad72f4ee4d..336ffcb9d79 100644 --- a/src/components/autocomplete/autocomplete.ts +++ b/src/components/autocomplete/autocomplete.ts @@ -31,9 +31,9 @@ let nextId = 0; * Combined with a native input, it displays a panel with a list of available options. * * @slot - Use the unnamed slot to add `sbb-option` or `sbb-optgroup` elements to the `sbb-autocomplete`. - * @event {CustomEvent} willOpen - Emits whenever the `sbb-autocomplete` starts the opening transition. + * @event {CustomEvent} willOpen - Emits whenever the `sbb-autocomplete` starts the opening transition. Can be canceled. * @event {CustomEvent} didOpen - Emits whenever the `sbb-autocomplete` is opened. - * @event {CustomEvent} willClose - Emits whenever the `sbb-autocomplete` begins the closing transition. + * @event {CustomEvent} willClose - Emits whenever the `sbb-autocomplete` begins the closing transition. Can be canceled. * @event {CustomEvent} didClose - Emits whenever the `sbb-autocomplete` is closed. */ @customElement('sbb-autocomplete') @@ -123,9 +123,10 @@ export class SbbAutocomplete extends LitElement { return; } - this._state = 'opening'; - this._willOpen.emit(); - this._setOverlayPosition(); + if (this._willOpen.emit()) { + this._state = 'opening'; + this._setOverlayPosition(); + } } /** Closes the autocomplete. */ @@ -134,9 +135,10 @@ export class SbbAutocomplete extends LitElement { return; } - this._state = 'closing'; - this._willClose.emit(); - this._openPanelEventsController.abort(); + if (this._willClose.emit()) { + this._state = 'closing'; + this._openPanelEventsController.abort(); + } } /** Removes trigger click listener on trigger change. */ diff --git a/src/components/dialog/dialog.e2e.ts b/src/components/dialog/dialog.e2e.ts index 991af565378..174c9ef9098 100644 --- a/src/components/dialog/dialog.e2e.ts +++ b/src/components/dialog/dialog.e2e.ts @@ -50,6 +50,23 @@ describe('sbb-dialog', () => { await openDialog(element); }); + it('does not open the dialog if prevented', async () => { + const willOpen = new EventSpy(SbbDialog.events.willOpen); + const didOpen = new EventSpy(SbbDialog.events.didOpen); + + element.addEventListener(SbbDialog.events.willOpen, (ev) => ev.preventDefault()); + + element.open(); + await waitForLitRender(element); + + await waitForCondition(() => willOpen.events.length === 1); + expect(willOpen.count).to.be.equal(1); + await waitForLitRender(element); + + expect(didOpen.count).to.be.equal(0); + expect(element).to.have.attribute('data-state', 'closed'); + }); + it('closes the dialog', async () => { const willClose = new EventSpy(SbbDialog.events.willClose); const didClose = new EventSpy(SbbDialog.events.didClose); @@ -73,6 +90,25 @@ describe('sbb-dialog', () => { expect(ariaLiveRef.textContent).to.be.equal(''); }); + it('does not close the dialog if prevented', async () => { + const willClose = new EventSpy(SbbDialog.events.willClose); + const didClose = new EventSpy(SbbDialog.events.didClose); + + await openDialog(element); + + element.addEventListener(SbbDialog.events.willClose, (ev) => ev.preventDefault()); + + element.close(); + await waitForLitRender(element); + + await waitForCondition(() => willClose.events.length === 1); + expect(willClose.count).to.be.equal(1); + await waitForLitRender(element); + + expect(didClose.count).to.be.equal(0); + expect(element).to.have.attribute('data-state', 'opened'); + }); + it('closes the dialog on backdrop click', async () => { const willClose = new EventSpy(SbbDialog.events.willClose); const didClose = new EventSpy(SbbDialog.events.didClose); diff --git a/src/components/dialog/dialog.ts b/src/components/dialog/dialog.ts index 72d9fcb9708..c20a75db491 100644 --- a/src/components/dialog/dialog.ts +++ b/src/components/dialog/dialog.ts @@ -37,9 +37,9 @@ let nextId = 0; * @slot - Use the unnamed slot to add content to the `sbb-dialog`. * @slot title - Use this slot to provide a title. * @slot action-group - Use this slot to display a `sbb-action-group` in the footer. - * @event {CustomEvent} willOpen - Emits whenever the `sbb-dialog` starts the opening transition. + * @event {CustomEvent} willOpen - Emits whenever the `sbb-dialog` starts the opening transition. Can be canceled. * @event {CustomEvent} didOpen - Emits whenever the `sbb-dialog` is opened. - * @event {CustomEvent} willClose - Emits whenever the `sbb-dialog` begins the closing transition. + * @event {CustomEvent} willClose - Emits whenever the `sbb-dialog` begins the closing transition. Can be canceled. * @event {CustomEvent} didClose - Emits whenever the `sbb-dialog` is closed. * @event {CustomEvent} requestBackAction - Emits whenever the back button is clicked. */ @@ -184,13 +184,17 @@ export class SbbDialog extends LitElement { return; } this._lastFocusedElement = document.activeElement as HTMLElement; - this._willOpen.emit(); - this._state = 'opening'; - // Add this dialog to the global collection - dialogRefs.push(this as SbbDialog); - this._setOverflowAttribute(); - // Disable scrolling for content below the dialog - this._scrollHandler.disableScroll(); + + if (this._willOpen.emit()) { + this._state = 'opening'; + + // Add this dialog to the global collection + dialogRefs.push(this as SbbDialog); + this._setOverflowAttribute(); + + // Disable scrolling for content below the dialog + this._scrollHandler.disableScroll(); + } } /** @@ -203,9 +207,15 @@ export class SbbDialog extends LitElement { this._returnValue = result; this._dialogCloseElement = target; - this._willClose.emit({ returnValue: this._returnValue, closeTarget: this._dialogCloseElement }); - this._state = 'closing'; - this._removeAriaLiveRefContent(); + if ( + this._willClose.emit({ + returnValue: this._returnValue, + closeTarget: this._dialogCloseElement, + }) + ) { + this._state = 'closing'; + this._removeAriaLiveRefContent(); + } } // Closes the dialog on "Esc" key pressed. diff --git a/src/components/menu/menu/menu.e2e.ts b/src/components/menu/menu/menu.e2e.ts index 8d3ea9b820f..f69db439c39 100644 --- a/src/components/menu/menu/menu.e2e.ts +++ b/src/components/menu/menu/menu.e2e.ts @@ -243,4 +243,34 @@ describe('sbb-menu', () => { await waitForLitRender(element); expect(document.activeElement.id).to.be.equal('menu-link'); }); + + it('does not open if prevented', async () => { + const willOpenEventSpy = new EventSpy(SbbMenu.events.willOpen); + + element.addEventListener(SbbMenu.events.willOpen, (ev) => ev.preventDefault()); + element.open(); + + await waitForCondition(() => willOpenEventSpy.events.length === 1); + expect(willOpenEventSpy.count).to.be.equal(1); + await waitForLitRender(element); + + expect(element).to.have.attribute('data-state', 'closed'); + }); + + it('does not close if prevented', async () => { + const didOpenEventSpy = new EventSpy(SbbMenu.events.didOpen); + const willCloseEventSpy = new EventSpy(SbbMenu.events.willClose); + + element.open(); + await waitForCondition(() => didOpenEventSpy.events.length === 1); + await waitForLitRender(element); + + element.addEventListener(SbbMenu.events.willClose, (ev) => ev.preventDefault()); + element.close(); + + await waitForCondition(() => willCloseEventSpy.events.length === 1); + await waitForLitRender(element); + + expect(element).to.have.attribute('data-state', 'opened'); + }); }); diff --git a/src/components/menu/menu/menu.ts b/src/components/menu/menu/menu.ts index 6c3e60018f8..eea9051c52a 100644 --- a/src/components/menu/menu/menu.ts +++ b/src/components/menu/menu/menu.ts @@ -42,9 +42,9 @@ let nextId = 0; * It displays a contextual menu with one or more action element. * * @slot - Use the unnamed slot to add `sbb-menu-action` or other elements to the menu. - * @event {CustomEvent} willOpen - Emits whenever the `sbb-menu` starts the opening transition. + * @event {CustomEvent} willOpen - Emits whenever the `sbb-menu` starts the opening transition. Can be canceled. * @event {CustomEvent} didOpen - Emits whenever the `sbb-menu` is opened. - * @event {CustomEvent} willClose - Emits whenever the `sbb-menu` begins the closing transition. + * @event {CustomEvent} willClose - Emits whenever the `sbb-menu` begins the closing transition. Can be canceled. * @event {CustomEvent} didClose - Emits whenever the `sbb-menu` is closed. */ @customElement('sbb-menu') @@ -93,28 +93,16 @@ export class SbbMenu extends SlotChildObserver(LitElement) { @state() private _actions: SbbMenuAction[]; /** Emits whenever the `sbb-menu` starts the opening transition. */ - private _willOpen: EventEmitter = new EventEmitter(this, SbbMenu.events.willOpen, { - bubbles: true, - composed: true, - }); + private _willOpen: EventEmitter = new EventEmitter(this, SbbMenu.events.willOpen); /** Emits whenever the `sbb-menu` is opened. */ - private _didOpen: EventEmitter = new EventEmitter(this, SbbMenu.events.didOpen, { - bubbles: true, - composed: true, - }); + private _didOpen: EventEmitter = new EventEmitter(this, SbbMenu.events.didOpen); /** Emits whenever the `sbb-menu` begins the closing transition. */ - private _willClose: EventEmitter = new EventEmitter(this, SbbMenu.events.willClose, { - bubbles: true, - composed: true, - }); + private _willClose: EventEmitter = new EventEmitter(this, SbbMenu.events.willClose); /** Emits whenever the `sbb-menu` is closed. */ - private _didClose: EventEmitter = new EventEmitter(this, SbbMenu.events.didClose, { - bubbles: true, - composed: true, - }); + private _didClose: EventEmitter = new EventEmitter(this, SbbMenu.events.didClose); private _menu: HTMLDivElement; private _triggerElement: HTMLElement; @@ -135,14 +123,15 @@ export class SbbMenu extends SlotChildObserver(LitElement) { return; } - this._willOpen.emit(); - this._state = 'opening'; - this._setMenuPosition(); - this._triggerElement?.setAttribute('aria-expanded', 'true'); + if (this._willOpen.emit()) { + this._state = 'opening'; + this._setMenuPosition(); + this._triggerElement?.setAttribute('aria-expanded', 'true'); - // Starting from breakpoint medium, disable scroll - if (!isBreakpoint('medium')) { - this._scrollHandler.disableScroll(); + // Starting from breakpoint medium, disable scroll + if (!isBreakpoint('medium')) { + this._scrollHandler.disableScroll(); + } } } @@ -154,9 +143,10 @@ export class SbbMenu extends SlotChildObserver(LitElement) { return; } - this._willClose.emit(); - this._state = 'closing'; - this._triggerElement?.setAttribute('aria-expanded', 'false'); + if (this._willClose.emit()) { + this._state = 'closing'; + this._triggerElement?.setAttribute('aria-expanded', 'false'); + } } /** diff --git a/src/components/navigation/navigation/navigation.e2e.ts b/src/components/navigation/navigation/navigation.e2e.ts index 23bcffdae66..49d946c7f75 100644 --- a/src/components/navigation/navigation/navigation.e2e.ts +++ b/src/components/navigation/navigation/navigation.e2e.ts @@ -323,4 +323,34 @@ describe('sbb-navigation', () => { expect(element).to.have.attribute('data-state', 'opened'); expect(section).to.have.attribute('data-state', 'closed'); }); + + it('does not open if prevented', async () => { + const willOpenEventSpy = new EventSpy(SbbNavigation.events.willOpen); + + element.addEventListener(SbbNavigation.events.willOpen, (ev) => ev.preventDefault()); + element.open(); + + await waitForCondition(() => willOpenEventSpy.events.length === 1); + expect(willOpenEventSpy.count).to.be.equal(1); + await waitForLitRender(element); + + expect(element).to.have.attribute('data-state', 'closed'); + }); + + it('does not close if prevented', async () => { + const didOpenEventSpy = new EventSpy(SbbNavigation.events.didOpen); + const willCloseEventSpy = new EventSpy(SbbNavigation.events.willClose); + + element.open(); + await waitForCondition(() => didOpenEventSpy.events.length === 1); + await waitForLitRender(element); + + element.addEventListener(SbbNavigation.events.willClose, (ev) => ev.preventDefault()); + element.close(); + + await waitForCondition(() => willCloseEventSpy.events.length === 1); + await waitForLitRender(element); + + expect(element).to.have.attribute('data-state', 'opened'); + }); }); diff --git a/src/components/navigation/navigation/navigation.ts b/src/components/navigation/navigation/navigation.ts index 00d642e73ee..6a0a487f082 100644 --- a/src/components/navigation/navigation/navigation.ts +++ b/src/components/navigation/navigation/navigation.ts @@ -43,9 +43,9 @@ let nextId = 0; * It displays a navigation menu, wrapping one or more `sbb-navigation-*` components. * * @slot - Use the unnamed slot to add `sbb-navigation-action` elements into the sbb-navigation menu. - * @event {CustomEvent} willOpen - Emits whenever the `sbb-navigation` begins the opening transition. + * @event {CustomEvent} willOpen - Emits whenever the `sbb-navigation` begins the opening transition. Can be canceled. * @event {CustomEvent} didOpen - Emits whenever the `sbb-navigation` is opened. - * @event {CustomEvent} willClose - Emits whenever the `sbb-navigation` begins the closing transition. + * @event {CustomEvent} willClose - Emits whenever the `sbb-navigation` begins the closing transition. Can be canceled. * @event {CustomEvent} didClose - Emits whenever the `sbb-navigation` is closed. */ @customElement('sbb-navigation') @@ -137,13 +137,14 @@ export class SbbNavigation extends UpdateScheduler(LitElement) { return; } - this._willOpen.emit(); - this._state = 'opening'; - this.startUpdate(); + if (this._willOpen.emit()) { + this._state = 'opening'; + this.startUpdate(); - // Disable scrolling for content below the navigation - this._scrollHandler.disableScroll(); - this._triggerElement?.setAttribute('aria-expanded', 'true'); + // Disable scrolling for content below the navigation + this._scrollHandler.disableScroll(); + this._triggerElement?.setAttribute('aria-expanded', 'true'); + } } /** @@ -154,10 +155,11 @@ export class SbbNavigation extends UpdateScheduler(LitElement) { return; } - this._willClose.emit(); - this._state = 'closing'; - this.startUpdate(); - this._triggerElement?.setAttribute('aria-expanded', 'false'); + if (this._willClose.emit()) { + this._state = 'closing'; + this.startUpdate(); + this._triggerElement?.setAttribute('aria-expanded', 'false'); + } } // Removes trigger click listener on trigger change. diff --git a/src/components/select/select.e2e.ts b/src/components/select/select.e2e.ts index a1da786cad6..252c5a692dc 100644 --- a/src/components/select/select.e2e.ts +++ b/src/components/select/select.e2e.ts @@ -366,4 +366,34 @@ describe('sbb-select', () => { await waitForLitRender(element); expect(document.activeElement).not.to.have.attribute('role', 'combobox'); }); + + it('does not open if prevented', async () => { + const willOpenEventSpy = new EventSpy(SbbSelect.events.willOpen); + + element.addEventListener(SbbSelect.events.willOpen, (ev) => ev.preventDefault()); + element.open(); + + await waitForCondition(() => willOpenEventSpy.events.length === 1); + expect(willOpenEventSpy.count).to.be.equal(1); + await waitForLitRender(element); + + expect(element).to.have.attribute('data-state', 'closed'); + }); + + it('does not close if prevented', async () => { + const didOpenEventSpy = new EventSpy(SbbSelect.events.didOpen); + const willCloseEventSpy = new EventSpy(SbbSelect.events.willClose); + + element.open(); + await waitForCondition(() => didOpenEventSpy.events.length === 1); + await waitForLitRender(element); + + element.addEventListener(SbbSelect.events.willClose, (ev) => ev.preventDefault()); + element.close(); + + await waitForCondition(() => willCloseEventSpy.events.length === 1); + await waitForLitRender(element); + + expect(element).to.have.attribute('data-state', 'opened'); + }); }); diff --git a/src/components/select/select.ts b/src/components/select/select.ts index 16149bcb3d9..03f22d63eae 100644 --- a/src/components/select/select.ts +++ b/src/components/select/select.ts @@ -36,9 +36,9 @@ export interface SelectChange { * @event {CustomEvent} didChange - Deprecated. used for React. Will probably be removed once React 19 is available. * @event {CustomEvent} change - Notifies that the component's value has changed. * @event {CustomEvent} input - Notifies that an option value has been selected. - * @event {CustomEvent} willOpen - Emits whenever the `sbb-select` starts the opening transition. + * @event {CustomEvent} willOpen - Emits whenever the `sbb-select` starts the opening transition. Can be canceled. * @event {CustomEvent} didOpen - Emits whenever the `sbb-select` is opened. - * @event {CustomEvent} willClose - Emits whenever the `sbb-select` begins the closing transition. + * @event {CustomEvent} willClose - Emits whenever the `sbb-select` begins the closing transition. Can be canceled. * @event {CustomEvent} didClose - Emits whenever the `sbb-select` is closed. */ @customElement('sbb-select') @@ -162,9 +162,10 @@ export class SbbSelect extends UpdateScheduler(LitElement) { return; } - this._state = 'opening'; - this._willOpen.emit(); - this._setOverlayPosition(); + if (this._willOpen.emit()) { + this._state = 'opening'; + this._setOverlayPosition(); + } } /** Closes the selection panel. */ @@ -173,9 +174,10 @@ export class SbbSelect extends UpdateScheduler(LitElement) { return; } - this._state = 'closing'; - this._willClose.emit(); - this._openPanelEventsController.abort(); + if (this._willClose.emit()) { + this._state = 'closing'; + this._openPanelEventsController.abort(); + } } /** Gets the current displayed value. */ diff --git a/src/components/toast/toast.e2e.ts b/src/components/toast/toast.e2e.ts index 853c17ea898..64139cf0316 100644 --- a/src/components/toast/toast.e2e.ts +++ b/src/components/toast/toast.e2e.ts @@ -162,4 +162,34 @@ describe('sbb-toast', () => { expect(toast1).to.have.attribute('data-state', 'closed'); expect(toast2).to.have.attribute('data-state', 'opened'); }); + + it('does not open if prevented', async () => { + const willOpenEventSpy = new EventSpy(SbbToast.events.willOpen); + + element.addEventListener(SbbToast.events.willOpen, (ev) => ev.preventDefault()); + element.open(); + + await waitForCondition(() => willOpenEventSpy.events.length === 1); + expect(willOpenEventSpy.count).to.be.equal(1); + await waitForLitRender(element); + + expect(element).to.have.attribute('data-state', 'closed'); + }); + + it('does not close if prevented', async () => { + const didOpenEventSpy = new EventSpy(SbbToast.events.didOpen); + const willCloseEventSpy = new EventSpy(SbbToast.events.willClose); + + element.open(); + await waitForCondition(() => didOpenEventSpy.events.length === 1); + await waitForLitRender(element); + + element.addEventListener(SbbToast.events.willClose, (ev) => ev.preventDefault()); + element.close(); + + await waitForCondition(() => willCloseEventSpy.events.length === 1); + await waitForLitRender(element); + + expect(element).to.have.attribute('data-state', 'opened'); + }); }); diff --git a/src/components/toast/toast.ts b/src/components/toast/toast.ts index 60fd5416f74..5815250b8cc 100644 --- a/src/components/toast/toast.ts +++ b/src/components/toast/toast.ts @@ -35,9 +35,9 @@ const toastRefs = new Set(); * @slot - Use the unnamed slot to add content to the `sbb-toast`. * @slot icon - Assign a custom icon via slot. * @slot action - Provide a custom action for this toast. - * @event {CustomEvent} willOpen - Emits whenever the `sbb-toast` starts the opening transition. + * @event {CustomEvent} willOpen - Emits whenever the `sbb-toast` starts the opening transition. Can be canceled. * @event {CustomEvent} didOpen - Emits whenever the `sbb-toast` is opened. - * @event {CustomEvent} willClose - Emits whenever the `sbb-toast` begins the closing transition. + * @event {CustomEvent} willClose - Emits whenever the `sbb-toast` begins the closing transition. Can be canceled. * @event {CustomEvent} didClose - Emits whenever the `sbb-toast` is closed. */ @customElement('sbb-toast') @@ -87,28 +87,16 @@ export class SbbToast extends LitElement { @state() private _currentLanguage = documentLanguage(); /** Emits whenever the `sbb-toast` starts the opening transition. */ - private _willOpen: EventEmitter = new EventEmitter(this, SbbToast.events.willOpen, { - bubbles: true, - composed: true, - }); + private _willOpen: EventEmitter = new EventEmitter(this, SbbToast.events.willOpen); /** Emits whenever the `sbb-toast` is opened. */ - private _didOpen: EventEmitter = new EventEmitter(this, SbbToast.events.didOpen, { - bubbles: true, - composed: true, - }); + private _didOpen: EventEmitter = new EventEmitter(this, SbbToast.events.didOpen); /** Emits whenever the `sbb-toast` begins the closing transition. */ - private _willClose: EventEmitter = new EventEmitter(this, SbbToast.events.willClose, { - bubbles: true, - composed: true, - }); + private _willClose: EventEmitter = new EventEmitter(this, SbbToast.events.willClose); /** Emits whenever the `sbb-toast` is closed. */ - private _didClose: EventEmitter = new EventEmitter(this, SbbToast.events.didClose, { - bubbles: true, - composed: true, - }); + private _didClose: EventEmitter = new EventEmitter(this, SbbToast.events.didClose); private _handlerRepository = new HandlerRepository( this, @@ -144,10 +132,10 @@ export class SbbToast extends LitElement { return; } - this._closeOtherToasts(); - - this._state = 'opening'; - this._willOpen.emit(); + if (this._willOpen.emit()) { + this._state = 'opening'; + this._closeOtherToasts(); + } } /** @@ -158,10 +146,10 @@ export class SbbToast extends LitElement { return; } - clearTimeout(this._closeTimeout); - - this._state = 'closing'; - this._willClose.emit(); + if (this._willClose.emit()) { + clearTimeout(this._closeTimeout); + this._state = 'closing'; + } } // Close the tooltip on click of any element that has the 'sbb-toast-close' attribute. diff --git a/src/components/tooltip/tooltip/tooltip.e2e.ts b/src/components/tooltip/tooltip/tooltip.e2e.ts index 14fa2785e48..700afdb0116 100644 --- a/src/components/tooltip/tooltip/tooltip.e2e.ts +++ b/src/components/tooltip/tooltip/tooltip.e2e.ts @@ -373,4 +373,34 @@ describe('sbb-tooltip', () => { expect(didOpenEventSpy.count).to.be.equal(2); expect(secondElement).to.have.attribute('data-state', 'opened'); }); + + it('does not open if prevented', async () => { + const willOpenEventSpy = new EventSpy(SbbTooltip.events.willOpen); + + element.addEventListener(SbbTooltip.events.willOpen, (ev) => ev.preventDefault()); + element.open(); + + await waitForCondition(() => willOpenEventSpy.events.length === 1); + expect(willOpenEventSpy.count).to.be.equal(1); + await waitForLitRender(element); + + expect(element).to.have.attribute('data-state', 'closed'); + }); + + it('does not close if prevented', async () => { + const didOpenEventSpy = new EventSpy(SbbTooltip.events.didOpen); + const willCloseEventSpy = new EventSpy(SbbTooltip.events.willClose); + + element.open(); + await waitForCondition(() => didOpenEventSpy.events.length === 1); + await waitForLitRender(element); + + element.addEventListener(SbbTooltip.events.willClose, (ev) => ev.preventDefault()); + element.close(); + + await waitForCondition(() => willCloseEventSpy.events.length === 1); + await waitForLitRender(element); + + expect(element).to.have.attribute('data-state', 'opened'); + }); }); diff --git a/src/components/tooltip/tooltip/tooltip.ts b/src/components/tooltip/tooltip/tooltip.ts index ccca68532f1..adb06d91d4d 100644 --- a/src/components/tooltip/tooltip/tooltip.ts +++ b/src/components/tooltip/tooltip/tooltip.ts @@ -41,9 +41,9 @@ const tooltipsRef = new Set(); * It displays contextual information within a tooltip. * * @slot - Use the unnamed slot to add content into the tooltip. - * @event {CustomEvent} willOpen - Emits whenever the `sbb-tooltip` starts the opening transition. + * @event {CustomEvent} willOpen - Emits whenever the `sbb-tooltip` starts the opening transition. Can be canceled. * @event {CustomEvent} didOpen - Emits whenever the `sbb-tooltip` is opened. - * @event {CustomEvent<{ closeTarget: HTMLElement }>} willClose - Emits whenever the `sbb-tooltip` begins the closing transition. + * @event {CustomEvent<{ closeTarget: HTMLElement }>} willClose - Emits whenever the `sbb-tooltip` begins the closing transition. Can be canceled. * @event {CustomEvent<{ closeTarget: HTMLElement }>} didClose - Emits whenever the `sbb-tooltip` is closed. */ @customElement('sbb-tooltip') @@ -161,19 +161,21 @@ export class SbbTooltip extends LitElement { return; } - for (const tooltip of Array.from(tooltipsRef)) { - const state = tooltip.getAttribute('data-state') as SbbOverlayState; - if (state && (state === 'opened' || state === 'opening')) { - tooltip.close(); + if (this._willOpen.emit()) { + // Close the other tooltips + for (const tooltip of Array.from(tooltipsRef)) { + const state = tooltip.getAttribute('data-state') as SbbOverlayState; + if (state && (state === 'opened' || state === 'opening')) { + tooltip.close(); + } } - } - this._willOpen.emit(); - this._state = 'opening'; - this.inert = true; - this._setTooltipPosition(); - this._triggerElement?.setAttribute('aria-expanded', 'true'); - this._nextFocusedElement = undefined; + this._state = 'opening'; + this.inert = true; + this._setTooltipPosition(); + this._triggerElement?.setAttribute('aria-expanded', 'true'); + this._nextFocusedElement = undefined; + } } /** @@ -185,10 +187,11 @@ export class SbbTooltip extends LitElement { } this._tooltipCloseElement = target; - this._willClose.emit({ closeTarget: this._tooltipCloseElement }); - this._state = 'closing'; - this.inert = true; - this._triggerElement?.setAttribute('aria-expanded', 'false'); + if (this._willClose.emit({ closeTarget: this._tooltipCloseElement })) { + this._state = 'closing'; + this.inert = true; + this._triggerElement?.setAttribute('aria-expanded', 'false'); + } } // Closes the tooltip on "Esc" key pressed and traps focus within the tooltip.