@rhds/elements
Version:
Red Hat Design System Elements
300 lines • 17.3 kB
JavaScript
var _RhAlert_instances, _RhAlert_icon_get, _RhAlert_slots, _RhAlert_onClose, _RhAlert_aliasState, _RhAlert_close, _RhAlert_onActionsClick;
import { __classPrivateFieldGet, __decorate } from "tslib";
import { LitElement, html, isServer, render } from 'lit';
import { customElement } from 'lit/decorators/custom-element.js';
import { property } from 'lit/decorators/property.js';
import { repeat } from 'lit/directives/repeat.js';
import { classMap } from 'lit/directives/class-map.js';
import { getRandomId } from '@patternfly/pfe-core/functions/random.js';
import { themable } from '@rhds/elements/lib/themable.js';
import { SlotController } from '@patternfly/pfe-core/controllers/slot-controller.js';
import '@rhds/elements/rh-surface/rh-surface.js';
import '@rhds/elements/rh-button/rh-button.js';
import '@rhds/elements/rh-icon/rh-icon.js';
import { css } from "lit";
const styles = css `:host{display:block;max-width:var(--_max-width,initial)}[hidden]{display:none!important}header{display:flex;align-items:center;justify-content:flex-start}.hasBody header{margin-block-end:var(--rh-space-xs,4px)}footer.hasActions{margin-block-start:var(--rh-space-lg,16px)}footer ::slotted([slot=actions]:not(rh-button[variant=link i])){margin-inline-end:var(--rh-space-xl,24px)!important;padding:0!important;border:none!important;background-color:initial!important;color:var(--rh-color-interactive-primary-default)!important;font-size:var(--rh-font-size-body-text-sm,.875rem)!important;font-family:var(--_font-family)!important}footer ::slotted([slot=actions]:focus){text-decoration:underline!important;color:var(--rh-color-interactive-primary-hover)!important}footer ::slotted([slot=actions]:hover){cursor:pointer!important;text-decoration:underline!important;color:var(--rh-color-interactive-primary-hover)!important}footer ::slotted(rh-button[variant=link i]){display:inline-block;translate:calc(var(--rh-space-lg, 16px)*-1)}#container{--_background-color:var(--rh-color-teal-10,#daf2f2);--_border-color:var(--rh-color-teal-50,#37a3a3);--_icon-color:var(--rh-color-teal-50,#37a3a3);border-width:var(--rh-border-width-md,2px);border-style:solid;border-color:var(--_border-color);border-inline-start-color:#0000;border-block-end-color:#0000;border-inline-end-color:#0000;background-color:light-dark(var(--_background-color),var(--rh-color-surface-dark,#383838));box-shadow:var(--_box-shadow,none);padding:var(--rh-space-lg,16px);display:grid;grid-template-columns:min-content 1fr;gap:var(--rh-space-xs,4px);font-family:var(--rh-font-family-body-text,RedHatText,"Red Hat Text",Helvetica,Arial,sans-serif);font-weight:var(--rh-font-weight-body-text-regular,400);font-size:var(--rh-font-size-body-text-sm,.875rem);line-height:var(--rh-line-height-body-text,1.5)}#container header ::slotted(*){font-family:var(--rh-font-family-body-text,RedHatText,"Red Hat Text",Helvetica,Arial,sans-serif)!important;font-weight:var(--rh-font-weight-body-text-medium,500)!important;font-size:var(--rh-font-size-body-text-sm,.875rem)!important;line-height:var(--rh-line-height-body-text,1.5)!important;margin:0!important}#container header ::slotted(:is(h1,h2,h3,h4,h5,h6)){padding-block:2px var(--rh-space-xs,4px)!important}:is(#container #description)>::slotted(*){font-size:var(--rh-font-size-body-text-sm,.875rem)!important;margin-block:0!important;padding:0!important}#container.neutral{--_border-color:var(--rh-color-status-neutral);--_icon-color:var(--rh-color-icon-status-neutral);--_background-color:var(--rh-color-surface-status-neutral)}#container.info{--_border-color:var(--rh-color-status-info);--_icon-color:var(--rh-color-icon-status-info);--_background-color:var(--rh-color-surface-status-info)}#container.success{--_border-color:var(--rh-color-status-success);--_icon-color:var(--rh-color-icon-status-success);--_background-color:var(--rh-color-surface-status-success)}#container.caution{--_border-color:var(--rh-color-status-caution);--_icon-color:var(--rh-color-icon-status-caution);--_background-color:var(--rh-color-surface-status-caution)}#container.warning{--_border-color:var(--rh-color-status-warning);--_icon-color:var(--rh-color-icon-status-warning);--_background-color:var(--rh-color-surface-status-warning)}#container.danger{--_border-color:var(--rh-color-status-danger);--_icon-color:var(--rh-color-icon-status-danger);--_background-color:var(--rh-color-surface-status-danger)}#container.toast{--_background-color:var(--rh-color-surface-lightest,#fff);--_max-width:550px;--_box-shadow:var(--rh-box-shadow-xl,0 8px 24px 3px #15151559)}#container.alternate{border-inline-start-color:var(--_border-color);border-block-end-color:var(--_border-color);border-inline-end-color:var(--_border-color)}#left-column,#middle-column{display:inline-block;vertical-align:top}#header-actions{margin-inline-end:var(--rh-space-xs,4px)}#header{color:var(--rh-color-text-primary);flex:1 1 auto}#icon{display:flex;align-items:center;justify-content:center;width:var(--rh-size-icon-02,24px);height:var(--rh-size-icon-02,24px);color:var(--_icon-color)}#close-button{color:var(--rh-color-text-secondary);background-color:initial;border:none;height:var(--rh-length-xl,24px);width:var(--rh-length-xl,24px);cursor:pointer}#close-button:hover{color:var(--rh-color-text-primary)}`;
const toastStyles = css `.rh-alert-toast-group{position:fixed;z-index:1000;inset-block-start:var(--rh-space-lg,16px);inset-inline-end:var(--rh-space-lg,16px);padding-block-end:var(--rh-space-4xl,64px);display:grid;justify-items:end;justify-content:center;gap:var(--rh-space-lg,16px);pointer-events:none}.rh-alert-toast-group rh-alert{--_duration:8s;--_travel-distance:0;pointer-events:all;will-change:transform;animation:fade-in .3s ease,slide-in .3s ease,fade-out .3s ease var(--_duration)}.persistent:is(.rh-alert-toast-group rh-alert){animation:slide-in .3s ease} (prefers-reduced-motion:no-preference){.rh-alert-toast-group rh-alert{--_travel-distance:var(--rh-space-4xl,64px)}} fade-in{0%{opacity:0}} fade-out{to{opacity:0}} slide-in{0%{transform:translateY(var(--_travel-distance,var(--rh-space-lg,16px)))}}`;
const ICONS = new Map(Object.entries({
neutral: 'minus-circle-fill',
info: 'information-fill',
success: 'check-circle-fill',
caution: 'error-fill',
warning: 'warning-fill',
danger: 'ban-fill',
}));
export class AlertCloseEvent extends Event {
constructor(action) {
super('close', { bubbles: true, cancelable: true });
this.action = action;
}
}
let toaster;
const toasts = new Set();
/**
* An alert is a banner used to notify a user about a change in status
* or communicate other information. It can be generated with or without
* a user triggering an action first.
*
* @summary Notifies a user without blocking their workflow
*
* @alias alert
*
* @fires {AlertCloseEvent} close - when the dismissable alert closes
*/
let RhAlert = class RhAlert extends LitElement {
constructor() {
super(...arguments);
_RhAlert_instances.add(this);
/**
* Communicates the urgency of a message and is denoted by various styling configurations.
*
* - `neutral` - Indicates generic information or a message with no severity.
* - `danger` - Indicates a danger state, like an error that is blocking a user from completing a task.
* - `warning` - Indicates a warning state, like a non-blocking error that might need to be fixed.
* - `caution` - Indicates an action or notice which should immediately draw the attention
* - `info` - Indicates helpful information or a message with very little to no severity.
* - `success` - Indicates a success state, like if a process was completed without errors.
*
* Note: 'note', 'default', and 'error' will also work, but are deprecated
*/
this.state = 'neutral';
/**
* Alert variants have different rules regarding their ability to be dismissed by a user.
* Default, Info, and Success Inline alerts can be dismissed by a user selecting the close button.
* Warning and Danger Inline alerts can be dismissed by a user resolving the issues caused by the alert.
* All Toast alerts can be dismissed by a user selecting the close button or waiting for them to time out.
*/
this.dismissable = false;
_RhAlert_slots.set(this, new SlotController(this, 'header', null, 'actions'));
}
/**
* Toast a message with an rh-alert
* @param options
* @param options.message alert text
* @param [options.actions] optional array of actions
* @param [options.heading="Success"] alert heading
* @param [options.state="info"] `<rh-alert state="...">`
* @param [options.persistent=false] when true, toast remains on screen until dismissed
*/
static async toast({ message, persistent = false, heading = 'Success', state = 'info', actions: _actions, }) {
const actions = _actions ?? [];
toaster ?? (toaster = initToaster());
const id = getRandomId();
const toast = { actions, heading, message, state, id, persistent };
toasts.add(toast);
const { matches: motionOK } = window.matchMedia('(prefers-reduced-motion: no-preference)');
renderToasts();
const alert = toaster.querySelector(`#${id}`);
if (toaster.children.length && motionOK) {
flip(toaster);
}
await Promise.all(toaster.getAnimations().map(x => x.finished));
if (!persistent) {
await Promise.all(alert?.getAnimations().map(x => x.finished) ?? []);
toasts.delete(toast);
}
renderToasts();
}
;
connectedCallback() {
super.connectedCallback();
if (!isServer) {
this.requestUpdate();
}
}
render() {
const _isServer = isServer && !this.hasUpdated;
const hasActions = _isServer || __classPrivateFieldGet(this, _RhAlert_slots, "f").hasSlotted('actions');
const hasBody = _isServer || __classPrivateFieldGet(this, _RhAlert_slots, "f").hasSlotted(SlotController.default);
const { variant = 'inline' } = this;
const state = __classPrivateFieldGet(this, _RhAlert_instances, "m", _RhAlert_aliasState).call(this, this.state);
// this click listener delegates events from the footer slot
// as such it doest not require a key listener.
// eslint-disable-next-line lit-a11y/click-events-have-key-events
const footer = html `<footer class="${classMap({ hasActions })}"
="${__classPrivateFieldGet(this, _RhAlert_instances, "m", _RhAlert_onActionsClick)}">
<!-- Provide actions that the user can take for the alert -->
<slot name="actions"></slot>
</footer>`;
return html `
<rh-surface id="container"
class="${classMap({
hasBody,
light: true,
[state]: true,
[variant]: !!variant,
})}"
role="alert"
aria-hidden="false">
<div id="left-column">
<rh-icon id="icon" set="ui" icon="${__classPrivateFieldGet(this, _RhAlert_instances, "a", _RhAlert_icon_get)}"></rh-icon>
</div>
<div id="middle-column">
<header ?hidden="${!_isServer && __classPrivateFieldGet(this, _RhAlert_slots, "f").isEmpty('header')}">
<div id="header">
<!-- Provide a header for the alert message. -->
<slot name="header"></slot>
</div>${!this.dismissable && this.variant !== 'toast' ? '' : html `
<div id="header-actions">
<rh-button id="close-button"
variant="close"
label="Close"
confirm
="${__classPrivateFieldGet(this, _RhAlert_instances, "m", _RhAlert_onClose)}"></rh-button>
</div>`}
</header>
<div id="description">
<!-- Provide a description for the alert message -->
<slot></slot>
</div>
${footer}
</div>
</rh-surface>
`;
}
};
_RhAlert_slots = new WeakMap();
_RhAlert_instances = new WeakSet();
_RhAlert_icon_get = function _RhAlert_icon_get() {
const state = this.state.toLowerCase();
switch (state) {
// @ts-expect-error: support for deprecated props
case 'note': return ICONS.get('info');
// @ts-expect-error: support for deprecated props
case 'default': return ICONS.get('neutral');
// @ts-expect-error: support for deprecated props
case 'error': return ICONS.get('danger');
default: return ICONS.get(state);
}
};
_RhAlert_onClose = function _RhAlert_onClose() {
if (this.dispatchEvent(new AlertCloseEvent('close'))) {
__classPrivateFieldGet(this, _RhAlert_instances, "m", _RhAlert_close).call(this);
}
};
_RhAlert_aliasState = function _RhAlert_aliasState(state) {
switch (state.toLowerCase()) {
// the first three are deprecated pre-DPO status names
case 'note': return 'info';
case 'default': return 'neutral';
case 'error': return 'danger';
// the following are DPO-approved status names
case 'danger':
case 'warning':
case 'caution':
case 'neutral':
case 'info':
case 'success':
return state.toLowerCase();
default:
return 'neutral';
}
};
_RhAlert_close = async function _RhAlert_close() {
await this.updateComplete;
await Promise.all(this.getAnimations().map(x => {
x.finish();
return x.finished;
}));
this.remove();
};
_RhAlert_onActionsClick = async function _RhAlert_onActionsClick(event) {
if (event.target instanceof HTMLElement
&& event.target?.slot === 'actions'
&& typeof event.target.dataset.action === 'string'
&& this.dispatchEvent(new AlertCloseEvent(event.target?.dataset.action.toLowerCase()))) {
__classPrivateFieldGet(this, _RhAlert_instances, "m", _RhAlert_close).call(this);
}
};
RhAlert.styles = [styles];
__decorate([
property({ reflect: true })
], RhAlert.prototype, "state", void 0);
__decorate([
property({ reflect: true })
], RhAlert.prototype, "variant", void 0);
__decorate([
property({ reflect: true, type: Boolean })
], RhAlert.prototype, "dismissable", void 0);
RhAlert = __decorate([
customElement('rh-alert'),
themable
], RhAlert);
export { RhAlert };
function initToaster() {
const node = document.createElement('section');
node.classList.add('rh-alert-toast-group');
// TODO: possibly allow other roots
const styles = toastStyles instanceof CSSStyleSheet ? toastStyles
: toastStyles.styleSheet;
document.adoptedStyleSheets = [
...document.adoptedStyleSheets ?? [],
styles,
];
document.body.append(node);
return node;
}
function renderToasts() {
render(repeat(toasts, x => x.id, ({ actions, id, state, heading, message, persistent, }) => {
const [firstAction, secondAction] = actions ?? [];
return html `
<rh-alert id="${id}"
state="${state}"
class="${classMap({ persistent })}"
variant="toast"
role="status"
aria-live="polite"
="${manageAlertAnimation}"
="${manageAlertAnimation}"
="${manageAlertAnimation}"
="${manageAlertAnimation}">
<h3 slot="header">${heading}</h3>
${!message ? '' : typeof message !== 'string' ? message : html `
<p class="text" ?hidden="${!message}">${message}</p>`}
${[firstAction, secondAction].filter(x => !!x).map(action => html `
<rh-button slot="actions"
variant="${action === firstAction ? 'secondary' : 'link'}"
data-action="${action.action}">${action.text}</rh-button>
`) ?? []}
</rh-alert>
`;
}), toaster);
}
async function manageAlertAnimation(event) {
const alert = event.target instanceof RhAlert ? event.target
: event.target instanceof Element ? event.target.closest('rh-alert')
: null;
if (!alert) {
return;
}
for (const animation of alert.getAnimations() ?? []) {
switch (event.type) {
case 'focusin':
case 'mouseenter':
return animation.pause();
case 'focusout':
case 'mouseleave':
if (!alert.matches(':focus-within')) {
return animation.play();
}
}
}
}
/**
* @see https://aerotwist.com/blog/flip-your-animations/
* @param toaster container for toasted alerts
*/
function flip(toaster) {
const first = toaster.offsetHeight;
const last = toaster.offsetHeight;
const invert = last - first;
const animation = toaster.animate([
{ transform: `translateY(${invert}px)` },
{ transform: 'translateY(0)' },
], {
duration: 150,
easing: 'ease-out',
});
animation.startTime = document.timeline.currentTime;
}
//# sourceMappingURL=rh-alert.js.map