Skip to content
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

feat(sbb-alert): add close fade-out animation #2943

Merged
merged 5 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ snapshots["sbb-alert-group renders DOM"] =
role="status"
>
<sbb-alert
animation="open"
animation="all"
data-state="opening"
href="https://www.sbb.ch"
size="m"
Expand Down Expand Up @@ -42,7 +42,7 @@ snapshots["sbb-alert-group renders with slotted DOM"] =
Interruptions
</span>
<sbb-alert
animation="open"
animation="all"
data-state="opening"
href="https://www.sbb.ch"
size="m"
Expand Down
8 changes: 5 additions & 3 deletions src/elements/alert/alert-group/alert-group.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect } from '@open-wc/testing';
import { sendMouse } from '@web/test-runner-commands';
import { html } from 'lit/static-html.js';

import type { SbbTransparentButtonElement } from '../../button.js';
Expand Down Expand Up @@ -44,7 +45,6 @@ describe(`sbb-alert-group`, () => {
.shadowRoot!.querySelector<SbbTransparentButtonElement>(
'.sbb-alert__close-button-wrapper sbb-transparent-button',
)!;

closeButton.focus();
closeButton.click();
await waitForLitRender(element);
Expand Down Expand Up @@ -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);
Expand All @@ -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
Expand Down
27 changes: 16 additions & 11 deletions src/elements/alert/alert-group/alert-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export const snapshots = {};

snapshots["sbb-alert should render default properties DOM"] =
`<sbb-alert
animation="open"
animation="all"
data-state="opening"
size="m"
title-content="Interruption"
Expand Down Expand Up @@ -79,7 +79,7 @@ snapshots["sbb-alert should render default properties Shadow DOM"] =
snapshots["sbb-alert should render customized properties DOM"] =
`<sbb-alert
accessibility-label="label"
animation="open"
animation="all"
data-state="opening"
href="https://www.sbb.ch"
icon-name="disruption"
Expand Down
42 changes: 39 additions & 3 deletions src/elements/alert/alert/alert.scss
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ $open-anim-opacity-to: 1;
--sbb-disable-animation-zero-time,
var(--sbb-animation-duration-6x)
);
--sbb-alert-timing-function: ease-in;

@include sbb.mq($from: medium) {
--sbb-alert-icon-size: #{sbb.px-to-rem-build(28)};
Expand All @@ -38,7 +39,12 @@ $open-anim-opacity-to: 1;
display: block;
}

:host([animation='none']) {
// By default, the open animation is disabled
:host([data-state='opening']:not([animation='open'], [animation='all'])) {
@include sbb.disable-animation;
}

:host([data-state='closing']:not([animation='close'], [animation='all'])) {
@include sbb.disable-animation;
}

Expand All @@ -60,7 +66,7 @@ $open-anim-opacity-to: 1;
grid-template-rows: #{$open-anim-rows-from};
opacity: #{$open-anim-opacity-from};

:host([data-state='opened']) & {
:host(:is([data-state='opened'], [data-state='closing'])) & {
grid-template-rows: #{$open-anim-rows-to};
opacity: #{$open-anim-opacity-to};
}
Expand All @@ -69,9 +75,19 @@ $open-anim-opacity-to: 1;
animation-name: open, open-opacity;
animation-fill-mode: forwards;
animation-duration: var(--sbb-alert-animation-duration);
animation-timing-function: ease-in;
animation-timing-function: var(--sbb-alert-timing-function);
animation-delay: 0s, var(--sbb-alert-animation-duration);
}

:host([data-state='closing']) & {
animation: {
name: close-opacity, close;
fill-mode: forwards;
duration: var(--sbb-alert-animation-duration);
timing-function: var(--sbb-alert-timing-function);
delay: 0s, var(--sbb-disable-animation-zero-time, var(--sbb-animation-duration-2x));
}
}
}

.sbb-alert__transition-sub-wrapper {
Expand Down Expand Up @@ -172,3 +188,23 @@ $open-anim-opacity-to: 1;
opacity: #{$open-anim-opacity-to};
}
}

@keyframes close {
from {
grid-template-rows: #{$open-anim-rows-to};
}

to {
grid-template-rows: #{$open-anim-rows-from};
}
}

@keyframes close-opacity {
from {
opacity: #{$open-anim-opacity-to};
}

to {
opacity: #{$open-anim-opacity-from};
}
}
16 changes: 15 additions & 1 deletion src/elements/alert/alert/alert.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,27 @@ describe(`sbb-alert`, () => {
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`<sbb-alert title-content="disruption">Interruption</sbb-alert>`);
const alert: SbbAlertElement = await fixture(
html`<sbb-alert title-content="disruption">Interruption</sbb-alert>`,
);

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 () => {
Expand Down
8 changes: 6 additions & 2 deletions src/elements/alert/alert/alert.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
<sbb-alert ${sbbSpread(args)}>${contentSlotText}</sbb-alert>
<sbb-alert
${sbbSpread(args)}
@dismissalRequested=${(e: Event) => (e.target! as SbbAlertElement).close()}
>${contentSlotText}</sbb-alert
>
`;

const DefaultWithOtherContent = (args: Args): TemplateResult => {
Expand Down Expand Up @@ -134,7 +138,7 @@ const animation: InputType = {
control: {
type: 'inline-radio',
},
options: ['open', 'none'],
options: ['all', 'open', 'close', 'none'],
};

const defaultArgTypes: ArgTypes = {
Expand Down
86 changes: 46 additions & 40 deletions src/elements/alert/alert/alert.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -23,24 +15,26 @@ import '../../divider.js';
import '../../link.js';
import '../../title.js';

type SbbAlertState = Exclude<SbbOpenedClosedState, 'closing'>;

/**
* 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<void>} willOpen - Emits when the fade in animation starts.
* @event {CustomEvent<void>} didOpen - Emits when the fade in animation ends and the button is displayed.
* @event {CustomEvent<void>} willOpen - Emits when the opening animation starts.
* @event {CustomEvent<void>} didOpen - Emits when the opening animation ends.
* @event {CustomEvent<void>} willClose - Emits when the closing animation starts. Can be canceled.
* @event {CustomEvent<void>} didClose - Emits when the closing animation ends.
* @event {CustomEvent<void>} 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',
TomMenga marked this conversation as resolved.
Show resolved Hide resolved
dismissalRequested: 'dismissalRequested',
} as const;

Expand Down Expand Up @@ -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);
}

/** Emits when the fade in animation starts. */
private _willOpen: EventEmitter<void> = new EventEmitter(this, SbbAlertElement.events.willOpen);
@property({ reflect: true }) public animation: 'open' | 'close' | 'all' | 'none' = 'all';

/** Emits when the fade in animation ends and the button is displayed. */
private _didOpen: EventEmitter<void> = 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<void> = new EventEmitter(
this,
SbbAlertElement.events.dismissalRequested,
Expand All @@ -109,27 +92,50 @@ export class SbbAlertElement extends SbbIconNameMixin(LitElement) {
protected override async firstUpdated(changedProperties: PropertyValues<this>): Promise<void> {
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();
}

if (this.state === 'closing' && event.animationName === 'close') {
this._handleClosing();
TomMenga marked this conversation as resolved.
Show resolved Hide resolved
}
}

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`
<div class="sbb-alert__transition-wrapper" @animationend=${this._onAnimationEnd}>
Expand Down
Loading
Loading