diff --git a/src/elements/alert/alert-group/__snapshots__/alert-group.snapshot.spec.snap.js b/src/elements/alert/alert-group/__snapshots__/alert-group.snapshot.spec.snap.js index dfb3a4d562..6dff5f3b84 100644 --- a/src/elements/alert/alert-group/__snapshots__/alert-group.snapshot.spec.snap.js +++ b/src/elements/alert/alert-group/__snapshots__/alert-group.snapshot.spec.snap.js @@ -8,7 +8,7 @@ snapshots["sbb-alert-group renders DOM"] = role="status" > { .shadowRoot!.querySelector( '.sbb-alert__close-button-wrapper sbb-transparent-button', )!; - closeButton.focus(); closeButton.click(); await waitForLitRender(element); @@ -73,6 +73,8 @@ describe(`sbb-alert-group`, () => { // Then the alert should be removed from sbb-alert-group, tabindex should be set to 0, // focus should be on sbb-alert-group, accessibility title should be removed and empty event should be fired. + await waitForCondition(() => didDismissAlertSpy.events.length === 2); + expect(didDismissAlertSpy.count).to.be.equal(2); expect(element.querySelectorAll('sbb-alert').length).to.be.equal(0); expect(element.tabIndex).to.be.equal(0); expect(document.activeElement!.id).to.be.equal(alertGroupId); @@ -81,8 +83,8 @@ describe(`sbb-alert-group`, () => { expect(didDismissAlertSpy.count).to.be.equal(2); expect(emptySpy.count).to.be.greaterThan(0); - // When clicking away (simulated by blur event) - element.dispatchEvent(new CustomEvent('blur')); + // When clicking away + await sendMouse({ type: 'click', position: [0, 0] }); await waitForLitRender(element); // Then the active element id should be unset and tabindex should be removed diff --git a/src/elements/alert/alert-group/alert-group.ts b/src/elements/alert/alert-group/alert-group.ts index 26d926862a..cd48a7b026 100644 --- a/src/elements/alert/alert-group/alert-group.ts +++ b/src/elements/alert/alert-group/alert-group.ts @@ -57,11 +57,24 @@ export class SbbAlertGroupElement extends SbbHydrationMixin(LitElement) { private _abort = new SbbConnectedAbortController(this); - private _removeAlert(event: Event): void { + public override connectedCallback(): void { + super.connectedCallback(); + const signal = this._abort.signal; + this.addEventListener( + SbbAlertElement.events.dismissalRequested, + (e) => (e.target as SbbAlertElement).close(), + { + signal, + }, + ); + this.addEventListener(SbbAlertElement.events.didClose, (e) => this._alertClosed(e), { + signal, + }); + } + + private _alertClosed(event: Event): void { const target = event.target as SbbAlertElement; const hasFocusInsideAlertGroup = document.activeElement === target; - - target.parentNode?.removeChild(target); this._didDismissAlert.emit(target); // Restore focus @@ -77,14 +90,6 @@ export class SbbAlertGroupElement extends SbbHydrationMixin(LitElement) { } } - public override connectedCallback(): void { - super.connectedCallback(); - const signal = this._abort.signal; - this.addEventListener(SbbAlertElement.events.dismissalRequested, (e) => this._removeAlert(e), { - signal, - }); - } - private _slotChanged(event: Event): void { const hadAlerts = this._hasAlerts; this._hasAlerts = diff --git a/src/elements/alert/alert/__snapshots__/alert.snapshot.spec.snap.js b/src/elements/alert/alert/__snapshots__/alert.snapshot.spec.snap.js index d8f2d7bd65..9635bb92e5 100644 --- a/src/elements/alert/alert/__snapshots__/alert.snapshot.spec.snap.js +++ b/src/elements/alert/alert/__snapshots__/alert.snapshot.spec.snap.js @@ -3,7 +3,7 @@ export const snapshots = {}; snapshots["sbb-alert should render default properties DOM"] = ` { it('should fire animation events', async () => { const willOpenSpy = new EventSpy(SbbAlertElement.events.willOpen); const didOpenSpy = new EventSpy(SbbAlertElement.events.didOpen); + const willCloseSpy = new EventSpy(SbbAlertElement.events.willClose); + const didCloseSpy = new EventSpy(SbbAlertElement.events.didClose); + const dismissalSpy = new EventSpy(SbbAlertElement.events.dismissalRequested); - await fixture(html`Interruption`); + const alert: SbbAlertElement = await fixture( + html`Interruption`, + ); await waitForCondition(() => willOpenSpy.events.length === 1); expect(willOpenSpy.count).to.be.equal(1); await waitForCondition(() => didOpenSpy.events.length === 1); expect(didOpenSpy.count).to.be.equal(1); + + alert.requestDismissal(); + expect(dismissalSpy.count).to.be.equal(1); + + alert.close(); + + await waitForCondition(() => didCloseSpy.events.length === 1); + expect(willCloseSpy.count).to.be.equal(1); + expect(didCloseSpy.count).to.be.equal(1); }); it('should hide close button in readonly mode', async () => { diff --git a/src/elements/alert/alert/alert.stories.ts b/src/elements/alert/alert/alert.stories.ts index 334561110d..f3db653536 100644 --- a/src/elements/alert/alert/alert.stories.ts +++ b/src/elements/alert/alert/alert.stories.ts @@ -10,7 +10,11 @@ import { SbbAlertElement } from './alert.js'; import readme from './readme.md?raw'; const Default = ({ 'content-slot-text': contentSlotText, ...args }: Args): TemplateResult => html` - ${contentSlotText} + (e.target! as SbbAlertElement).close()} + >${contentSlotText} `; const DefaultWithOtherContent = (args: Args): TemplateResult => { @@ -134,7 +138,7 @@ const animation: InputType = { control: { type: 'inline-radio', }, - options: ['open', 'none'], + options: ['all', 'open', 'close', 'none'], }; const defaultArgTypes: ArgTypes = { diff --git a/src/elements/alert/alert/alert.ts b/src/elements/alert/alert/alert.ts index fd1866d8e9..5b3782270f 100644 --- a/src/elements/alert/alert/alert.ts +++ b/src/elements/alert/alert/alert.ts @@ -1,18 +1,10 @@ -import { - type CSSResultGroup, - html, - LitElement, - nothing, - type PropertyValues, - type TemplateResult, -} from 'lit'; +import { type CSSResultGroup, html, nothing, type PropertyValues, type TemplateResult } from 'lit'; import { customElement, property } from 'lit/decorators.js'; -import type { LinkTargetType } from '../../core/base-elements.js'; +import { type LinkTargetType, SbbOpenCloseBaseElement } from '../../core/base-elements.js'; import { SbbLanguageController } from '../../core/controllers.js'; import { EventEmitter } from '../../core/eventing.js'; import { i18nCloseAlert, i18nFindOutMore } from '../../core/i18n.js'; -import type { SbbOpenedClosedState } from '../../core/interfaces.js'; import { SbbIconNameMixin } from '../../icon.js'; import type { SbbTitleLevel } from '../../title.js'; @@ -23,24 +15,26 @@ import '../../divider.js'; import '../../link.js'; import '../../title.js'; -type SbbAlertState = Exclude; - /** * It displays messages which require user's attention. * * @slot - Use the unnamed slot to add content to the `sbb-alert`. * @slot icon - Should be a `sbb-icon` which is displayed next to the title. Styling is optimized for icons of type HIM-CUS. * @slot title - Title content. - * @event {CustomEvent} willOpen - Emits when the fade in animation starts. - * @event {CustomEvent} didOpen - Emits when the fade in animation ends and the button is displayed. + * @event {CustomEvent} willOpen - Emits when the opening animation starts. + * @event {CustomEvent} didOpen - Emits when the opening animation ends. + * @event {CustomEvent} willClose - Emits when the closing animation starts. Can be canceled. + * @event {CustomEvent} didClose - Emits when the closing animation ends. * @event {CustomEvent} dismissalRequested - Emits when dismissal of an alert was requested. */ @customElement('sbb-alert') -export class SbbAlertElement extends SbbIconNameMixin(LitElement) { +export class SbbAlertElement extends SbbIconNameMixin(SbbOpenCloseBaseElement) { public static override styles: CSSResultGroup = style; - public static readonly events = { + public static override readonly events = { willOpen: 'willOpen', didOpen: 'didOpen', + willClose: 'willClose', + didClose: 'didClose', dismissalRequested: 'dismissalRequested', } as const; @@ -82,23 +76,12 @@ export class SbbAlertElement extends SbbIconNameMixin(LitElement) { @property({ attribute: 'accessibility-label' }) public accessibilityLabel: string | undefined; /** The enabled animations. */ - @property({ reflect: true }) public animation: 'open' | 'none' = 'open'; - - /** The state of the alert. */ - private get _state(): SbbAlertState { - return (this.getAttribute('data-state') as SbbAlertState | null) ?? 'closed'; - } - private set _state(value: SbbAlertState) { - this.setAttribute('data-state', value); - } + @property({ reflect: true }) public animation: 'open' | 'close' | 'all' | 'none' = 'all'; - /** Emits when the fade in animation starts. */ - private _willOpen: EventEmitter = new EventEmitter(this, SbbAlertElement.events.willOpen); - - /** Emits when the fade in animation ends and the button is displayed. */ - private _didOpen: EventEmitter = new EventEmitter(this, SbbAlertElement.events.didOpen); - - /** Emits when dismissal of an alert was requested. */ + /** + * Emits when dismissal of an alert was requested. + * @deprecated + */ private _dismissalRequested: EventEmitter = new EventEmitter( this, SbbAlertElement.events.dismissalRequested, @@ -109,27 +92,48 @@ export class SbbAlertElement extends SbbIconNameMixin(LitElement) { protected override async firstUpdated(changedProperties: PropertyValues): Promise { super.firstUpdated(changedProperties); - this._open(); + this.open(); } - /** Requests dismissal of the alert. */ + /** Requests dismissal of the alert. + * @deprecated in favour of 'willClose' and 'didClose' events + */ public requestDismissal(): void { this._dismissalRequested.emit(); } /** Open the alert. */ - private _open(): void { - this._state = 'opening'; - this._willOpen.emit(); + public open(): void { + this.willOpen.emit(); + this.state = 'opening'; + } + + /** Close the alert. */ + public close(): void { + if (this.willClose.emit()) { + this.state = 'closing'; + } } private _onAnimationEnd(event: AnimationEvent): void { - if (this._state === 'opening' && event.animationName === 'open-opacity') { - this._state = 'opened'; - this._didOpen.emit(); + if (this.state === 'opening' && event.animationName === 'open-opacity') { + this._handleOpening(); + } else if (this.state === 'closing' && event.animationName === 'close') { + this._handleClosing(); } } + private _handleOpening(): void { + this.state = 'opened'; + this.didOpen.emit(); + } + + private _handleClosing(): void { + this.state = 'closed'; + this.didClose.emit(); + setTimeout(() => this.remove()); + } + protected override render(): TemplateResult { return html`
diff --git a/src/elements/alert/alert/readme.md b/src/elements/alert/alert/readme.md index 8f4a8b2f9a..7e38f31594 100644 --- a/src/elements/alert/alert/readme.md +++ b/src/elements/alert/alert/readme.md @@ -85,7 +85,7 @@ As a base rule, opening animations should be active if an alert arrives after th | Name | Attribute | Privacy | Type | Default | Description | | -------------------- | --------------------- | ------- | --------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `accessibilityLabel` | `accessibility-label` | public | `string \| undefined` | | This will be forwarded as aria-label to the relevant nested element. | -| `animation` | `animation` | public | `'open' \| 'none'` | `'open'` | The enabled animations. | +| `animation` | `animation` | public | `'open' \| 'close' \| 'all' \| 'none'` | `'all'` | The enabled animations. | | `href` | `href` | public | `string \| undefined` | | The href value you want to link to. | | `iconName` | `icon-name` | public | `string \| undefined` | `'info'` | Name of the icon which will be forward to the nested `sbb-icon`. Choose the icons from https://icons.app.sbb.ch. Styling is optimized for icons of type HIM-CUS. | | `linkContent` | `link-content` | public | `string \| undefined` | | Content of the link. | @@ -98,17 +98,21 @@ As a base rule, opening animations should be active if an alert arrives after th ## Methods -| Name | Privacy | Description | Parameters | Return | Inherited From | -| ------------------ | ------- | -------------------------------- | ---------- | ------ | -------------- | -| `requestDismissal` | public | Requests dismissal of the alert. | | `void` | | +| Name | Privacy | Description | Parameters | Return | Inherited From | +| ------------------ | ------- | -------------------------------- | ---------- | ------ | ----------------------- | +| `close` | public | Close the alert. | | `void` | SbbOpenCloseBaseElement | +| `open` | public | Open the alert. | | `void` | SbbOpenCloseBaseElement | +| `requestDismissal` | public | Requests dismissal of the alert. | | `void` | | ## Events -| Name | Type | Description | Inherited From | -| -------------------- | ------------------- | ------------------------------------------------------------------ | -------------- | -| `didOpen` | `CustomEvent` | Emits when the fade in animation ends and the button is displayed. | | -| `dismissalRequested` | `CustomEvent` | Emits when dismissal of an alert was requested. | | -| `willOpen` | `CustomEvent` | Emits when the fade in animation starts. | | +| Name | Type | Description | Inherited From | +| -------------------- | ------------------- | --------------------------------------------------------- | ----------------------- | +| `didClose` | `CustomEvent` | Emits when the closing animation ends. | SbbOpenCloseBaseElement | +| `didOpen` | `CustomEvent` | Emits when the opening animation ends. | SbbOpenCloseBaseElement | +| `dismissalRequested` | `CustomEvent` | Emits when dismissal of an alert was requested. | | +| `willClose` | `CustomEvent` | Emits when the closing animation starts. Can be canceled. | SbbOpenCloseBaseElement | +| `willOpen` | `CustomEvent` | Emits when the opening animation starts. | SbbOpenCloseBaseElement | ## Slots diff --git a/src/elements/notification/notification.ts b/src/elements/notification/notification.ts index 153c6427f0..624147a741 100644 --- a/src/elements/notification/notification.ts +++ b/src/elements/notification/notification.ts @@ -31,10 +31,10 @@ const DEBOUNCE_TIME = 150; * * @slot - Use the unnamed slot to add content to the notification message. * @slot title - Use this to provide a notification title (optional). - * @event {CustomEvent} willOpen - Emits whenever the `sbb-notification` starts the opening transition. - * @event {CustomEvent} didOpen - Emits whenever the `sbb-notification` is opened. - * @event {CustomEvent} willClose - Emits whenever the `sbb-notification` begins the closing transition. - * @event {CustomEvent} didClose - Emits whenever the `sbb-notification` is closed. + * @event {CustomEvent} willOpen - Emits when the opening animation starts. + * @event {CustomEvent} didOpen - Emits when the opening animation ends. + * @event {CustomEvent} willClose - Emits when the closing animation starts. + * @event {CustomEvent} didClose - Emits when the closing animation ends. * @cssprop [--sbb-notification-margin=0] - Can be used to modify the margin in order to get a smoother animation. * See style section for more information. */ diff --git a/src/elements/notification/readme.md b/src/elements/notification/readme.md index 1514f12e88..dbd74addc4 100644 --- a/src/elements/notification/readme.md +++ b/src/elements/notification/readme.md @@ -97,12 +97,12 @@ As a base rule, opening animations should be active if a notification arrives af ## Events -| Name | Type | Description | Inherited From | -| ----------- | ------------------- | -------------------------------------------------------------------- | -------------- | -| `didClose` | `CustomEvent` | Emits whenever the `sbb-notification` is closed. | | -| `didOpen` | `CustomEvent` | Emits whenever the `sbb-notification` is opened. | | -| `willClose` | `CustomEvent` | Emits whenever the `sbb-notification` begins the closing transition. | | -| `willOpen` | `CustomEvent` | Emits whenever the `sbb-notification` starts the opening transition. | | +| Name | Type | Description | Inherited From | +| ----------- | ------------------- | ---------------------------------------- | -------------- | +| `didClose` | `CustomEvent` | Emits when the closing animation ends. | | +| `didOpen` | `CustomEvent` | Emits when the opening animation ends. | | +| `willClose` | `CustomEvent` | Emits when the closing animation starts. | | +| `willOpen` | `CustomEvent` | Emits when the opening animation starts. | | ## CSS Properties