Skip to content

Commit

Permalink
feat(components): add alert component (#1085)
Browse files Browse the repository at this point in the history
Co-authored-by: Philipp Gfeller <[email protected]>
Co-authored-by: Loïc Fürhoff <[email protected]>
  • Loading branch information
3 people authored Sep 29, 2023
1 parent 43b6ac2 commit d487840
Show file tree
Hide file tree
Showing 32 changed files with 1,044 additions and 383 deletions.
5 changes: 5 additions & 0 deletions .changeset/fifty-apricots-film.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@swisspost/design-system-styles': patch
---

Reduced the gap between the alert body and action buttons.
6 changes: 6 additions & 0 deletions .changeset/sweet-singers-trade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@swisspost/design-system-documentation': minor
'@swisspost/design-system-components': minor
---

Created the web component variant for the alert component.
9 changes: 7 additions & 2 deletions packages/components/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,8 @@
}
],
"@stencil-community/strict-boolean-conditions": "off",
"@stencil-community/required-prefix": ["error", ["post"]],
"@stencil-community/required-prefix": ["error", ["post-"]],
"@stencil-community/async-methods": "error",
"@stencil-community/ban-prefix": ["error", ["stencil", "stnl", "st"]],
"@stencil-community/decorators-context": "error",
"@stencil-community/decorators-style": [
"error",
Expand All @@ -51,6 +50,12 @@
"listen": "multiline"
}
],
"@stencil-community/class-pattern": [
"error",
{
"pattern": "^Post.*(?!Component)$"
}
],
"@stencil-community/element-type": "error",
"@stencil-community/host-data-deprecated": "error",
"@stencil-community/methods-must-be-public": "error",
Expand Down
30 changes: 30 additions & 0 deletions packages/components/cypress/e2e/alert.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
describe('alert', () => {
describe('default', () => {
beforeEach(() => {
cy.getComponent('post-alert');
});

it('should render', () => {
cy.get('@alert').should('exist');
});

it('should not have a close button', () => {
cy.get('@alert').find('.btn-close').should('not.exist');
});
});

describe('dismissible', () => {
beforeEach(() => {
cy.getComponent('post-alert', 'dismissible');
});

it('should have a close button', () => {
cy.get('@alert').find('.btn-close').should('be.visible');
});

it('should be removed after the dismiss button is clicked', () => {
cy.get('@alert').find('.btn-close').click();
cy.get('@alert').should('not.exist');
});
});
});
4 changes: 3 additions & 1 deletion packages/components/cypress/support/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ chai.use(isInViewport);

Cypress.Commands.add('getComponent', (component: string, story = 'default') => {
cy.visit(`/iframe.html?id=components-${component}--${story}`);
cy.get(`post-${component}`).as(component);

const alias = component.replace(/^post-/, '');
cy.get(`post-${alias}`).as(alias);
});

Cypress.Commands.add('checkVisibility', (visibility: 'visible' | 'hidden') => {
Expand Down
67 changes: 67 additions & 0 deletions packages/components/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,39 @@
* It contains typing information for all components that exist in this project.
*/
import { HTMLStencilElement, JSXBase } from "@stencil/core/internal";
import { AlertType } from "./components/post-alert/alert-types";
import { BackgroundColor } from "./components/post-tooltip/types";
import { Placement } from "@floating-ui/dom";
export { AlertType } from "./components/post-alert/alert-types";
export { BackgroundColor } from "./components/post-tooltip/types";
export { Placement } from "@floating-ui/dom";
export namespace Components {
interface PostAlert {
/**
* Triggers alert dismissal programmatically (same as clicking on the close button (×)).
*/
"dismiss": () => Promise<void>;
/**
* The label to use for the close button of a dismissible alert.
*/
"dismissLabel": string;
/**
* If `true`, a close button (×) is displayed and the alert can be dismissed by the user.
*/
"dismissible": boolean;
/**
* If `true`, the alert is positioned at the bottom of the window, from edge to edge.
*/
"fixed": boolean;
/**
* The icon to display in the alert. By default, the icon depends on the alert type. If `none`, no icon is displayed.
*/
"icon": string;
/**
* The type of the alert.
*/
"type": AlertType;
}
interface PostCollapsible {
/**
* If `true`, the element is initially collapsed otherwise it is displayed.
Expand Down Expand Up @@ -105,11 +133,21 @@ export namespace Components {
"toggle": (target: HTMLElement, force?: boolean) => Promise<void>;
}
}
export interface PostAlertCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLPostAlertElement;
}
export interface PostTabsCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLPostTabsElement;
}
declare global {
interface HTMLPostAlertElement extends Components.PostAlert, HTMLStencilElement {
}
var HTMLPostAlertElement: {
prototype: HTMLPostAlertElement;
new (): HTMLPostAlertElement;
};
interface HTMLPostCollapsibleElement extends Components.PostCollapsible, HTMLStencilElement {
}
var HTMLPostCollapsibleElement: {
Expand Down Expand Up @@ -150,6 +188,7 @@ declare global {
new (): HTMLPostTooltipElement;
};
interface HTMLElementTagNameMap {
"post-alert": HTMLPostAlertElement;
"post-collapsible": HTMLPostCollapsibleElement;
"post-icon": HTMLPostIconElement;
"post-tab-header": HTMLPostTabHeaderElement;
Expand All @@ -159,6 +198,32 @@ declare global {
}
}
declare namespace LocalJSX {
interface PostAlert {
/**
* The label to use for the close button of a dismissible alert.
*/
"dismissLabel"?: string;
/**
* If `true`, a close button (×) is displayed and the alert can be dismissed by the user.
*/
"dismissible"?: boolean;
/**
* If `true`, the alert is positioned at the bottom of the window, from edge to edge.
*/
"fixed"?: boolean;
/**
* The icon to display in the alert. By default, the icon depends on the alert type. If `none`, no icon is displayed.
*/
"icon"?: string;
/**
* An event emitted when the alert element is dismissed, after the transition. It has no payload and only relevant for dismissible alerts.
*/
"onDismissed"?: (event: PostAlertCustomEvent<void>) => void;
/**
* The type of the alert.
*/
"type"?: AlertType;
}
interface PostCollapsible {
/**
* If `true`, the element is initially collapsed otherwise it is displayed.
Expand Down Expand Up @@ -235,6 +300,7 @@ declare namespace LocalJSX {
"placement"?: Placement;
}
interface IntrinsicElements {
"post-alert": PostAlert;
"post-collapsible": PostCollapsible;
"post-icon": PostIcon;
"post-tab-header": PostTabHeader;
Expand All @@ -247,6 +313,7 @@ export { LocalJSX as JSX };
declare module "@stencil/core" {
export namespace JSX {
interface IntrinsicElements {
"post-alert": LocalJSX.PostAlert & JSXBase.HTMLAttributes<HTMLPostAlertElement>;
"post-collapsible": LocalJSX.PostCollapsible & JSXBase.HTMLAttributes<HTMLPostCollapsibleElement>;
/**
* @class PostIcon - representing a stencil component
Expand Down
3 changes: 3 additions & 0 deletions packages/components/src/components/post-alert/alert-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const ALERT_TYPES = ['primary', 'success', 'danger', 'warning', 'info', 'gray'] as const;

export type AlertType = typeof ALERT_TYPES[number];
22 changes: 22 additions & 0 deletions packages/components/src/components/post-alert/post-alert.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
@use '@swisspost/design-system-styles/components/alert';
@use '@swisspost/design-system-styles/components/close';
@use '@swisspost/design-system-styles/core' as post;

:host {
display: block;

::slotted(*) {
margin: 0 !important;
}
}

.visually-hidden {
@include post.visually-hidden();
}

@for $i from 1 through 6 {
.alert-heading > ::slotted(h#{$i}) {
font-size: inherit !important;
font-weight: inherit !important;
}
}
153 changes: 153 additions & 0 deletions packages/components/src/components/post-alert/post-alert.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { Component, Element, Event, EventEmitter, h, Host, Method, Prop, State, Watch } from '@stencil/core';
import { version } from '../../../package.json';
import { fadeOut } from '../../animations';
import { checkEmptyOrOneOf, checkEmptyOrPattern, checkNonEmpty, checkType } from '../../utils';
import { ALERT_TYPES, AlertType } from './alert-types';

@Component({
tag: 'post-alert',
styleUrl: 'post-alert.scss',
shadow: true,
})
export class PostAlert {
@Element() host: HTMLPostAlertElement;

@State() alertId = crypto.randomUUID();
@State() classes: string;
@State() hasActions: boolean;
@State() hasHeading: boolean;
@State() onDismissButtonClick = () => this.dismiss();

/**
* If `true`, a close button (×) is displayed and the alert can be dismissed by the user.
*/
@Prop() readonly dismissible: boolean = false;

@Watch('dismissible')
validateDismissible(isDismissible = this.dismissible) {
checkType(isDismissible, 'boolean', 'The post-alert "dismissible" prop should be a boolean.');
setTimeout(() => this.validateDismissLabel());
}

/**
* The label to use for the close button of a dismissible alert.
*/
@Prop() readonly dismissLabel: string;

@Watch('dismissLabel')
validateDismissLabel(dismissLabel = this.dismissLabel) {
if (this.dismissible) {
checkNonEmpty(dismissLabel, 'Dismissible post-alert\'s require a "dismiss-label" prop.');
}
}

/**
* If `true`, the alert is positioned at the bottom of the window, from edge to edge.
*/
@Prop() readonly fixed: boolean = false;

@Watch('fixed')
validateFixed(isFixed = this.fixed) {
checkType(isFixed, 'boolean', 'The post-alert "fixed" prop should be a boolean.');
}

/**
* The icon to display in the alert. By default, the icon depends on the alert type.
*
* If `none`, no icon is displayed.
*/
@Prop() readonly icon: string;

@Watch('icon')
validateIcon(icon = this.icon) {
checkEmptyOrPattern(icon, /\d{4}|none/, 'The post-alert "icon" prop should be a 4-digits string.');
}

/**
* The type of the alert.
*/
@Prop() readonly type: AlertType = 'primary';

@Watch('type')
validateType(type = this.type) {
checkEmptyOrOneOf(type, ALERT_TYPES, `The post-alert requires a type form: ${ALERT_TYPES.join(', ')}`);
}

/**
* An event emitted when the alert element is dismissed, after the transition.
* It has no payload and only relevant for dismissible alerts.
*/
@Event() dismissed: EventEmitter<void>;

connectedCallback() {
this.validateDismissible();
this.validateFixed();
this.validateIcon();
this.validateType();
}

componentWillRender() {
this.hasHeading = this.host.querySelectorAll('[slot=heading]').length > 0;
this.hasActions = this.host.querySelectorAll('[slot=actions]').length > 0;

this.classes = `alert alert-${this.type ?? 'primary'}`;
if (this.dismissible) this.classes += ' alert-dismissible';
if (this.hasActions) this.classes += ' alert-action';
if (this.fixed) this.classes += ' alert-fixed-bottom';
if (this.icon === 'none') this.classes += ' no-icon';
}

/**
* Triggers alert dismissal programmatically (same as clicking on the close button (×)).
*/
@Method()
async dismiss() {
const dismissal = fadeOut(this.host);

await dismissal.finished;

this.host.remove();
this.dismissed.emit();
}

render() {
const defaultAlertContent = [
this.icon && this.icon !== 'none' && (
<post-icon key={`${this.alertId}-icon`} name={this.icon} />
),
this.hasHeading && (
<div key={`${this.alertId}-heading`} class="alert-heading">
<slot name="heading"/>
</div>
),
<slot key={`${this.alertId}-message`}/>
];

const actionAlertContent = [
<div key={`${this.alertId}-content`} class="alert-content">
{defaultAlertContent}
</div>,
<div key={`${this.alertId}-buttons`} class="alert-buttons">
<slot name="actions"/>
</div>,
];

return (
<Host data-version={version}>
<div role="alert" class={this.classes}>
{this.dismissible && (
<button class="btn-close" onClick={this.onDismissButtonClick}>
<span class="visually-hidden">{this.dismissLabel}</span>
</button>
)}

{this.hasActions
? actionAlertContent
: defaultAlertContent
}
</div>
</Host>
);
}

}
Loading

0 comments on commit d487840

Please sign in to comment.