+
Set the display status of the <omni-alert>
.
+
+ The status
attribute supports the following options:
+
+ 'success'
- Checkmark icon to indicate success.
+ 'warning'
- Yield icon to indicate warning.
+ 'error'
- Exclamation icon to indicate error.
+ 'info'
- Information icon to indicate info.
+ 'none'
- No icon. (Default)
+
+
+
+ `,
+ render: (args: Args) => html`
+
{
+ args.hide = false;
+ document.dispatchEvent(
+ new CustomEvent('story-renderer-interactive-update', {
+ bubbles: true,
+ composed: true
+ })
+ );
+ }}" label="Show Alert" >
+ ${args.hide ? nothing : alertHtml(args)}
+ `,
+ frameworkSources: [
+ {
+ framework: 'HTML',
+ sourceParts: {
+ htmlFragment: (args) => raw`
+ ${getSourceFromLit(alertHtml(args))}
+ `,
+ jsFragment: `document.querySelector('omni-alert').show();`
+ }
+ }
+ ],
+ name: 'Status',
+ args: {
+ 'status-indicator': '',
+ header: '',
+ '[Default Slot]': '',
+ primary: '',
+ secondary: '',
+
+ hide: true,
+
+ message: 'Message Alert',
+ description: 'Additional context for the alert.',
+
+ enableSecondary: undefined,
+
+ status: 'success',
+ headerAlign: undefined,
+ primaryAction: undefined,
+ secondaryAction: undefined,
+ actionAlign: undefined
+ }
+};
+
+export const Header_Align: ComponentStoryFormat
= {
+ description: 'Align header content horizontally.',
+ render: (args: Args) => html`
+ {
+ args.hide = false;
+ document.dispatchEvent(
+ new CustomEvent('story-renderer-interactive-update', {
+ bubbles: true,
+ composed: true
+ })
+ );
+ }}" label="Show Alert" >
+ ${args.hide ? nothing : alertHtml(args)}
+ `,
+ frameworkSources: [
+ {
+ framework: 'HTML',
+ sourceParts: {
+ htmlFragment: (args) => raw`
+ ${getSourceFromLit(alertHtml(args))}
+ `,
+ jsFragment: `document.querySelector('omni-alert').show();`
+ }
+ }
+ ],
+ name: 'Header Align',
+ args: {
+ 'status-indicator': '',
+ header: '',
+ '[Default Slot]': '',
+ primary: '',
+ secondary: '',
+
+ hide: true,
+
+ message: 'Message Alert',
+ description: 'Additional context for the alert.',
+
+ enableSecondary: undefined,
+
+ status: undefined,
+ headerAlign: 'left',
+ primaryAction: undefined,
+ secondaryAction: undefined,
+ actionAlign: undefined
+ }
+};
+
+export const Primary_Action: ComponentStoryFormat = {
+ description: 'Set the label for the primary action button.',
+ render: (args: Args) => html`
+ {
+ args.hide = false;
+ document.dispatchEvent(
+ new CustomEvent('story-renderer-interactive-update', {
+ bubbles: true,
+ composed: true
+ })
+ );
+ }}" label="Show Alert" >
+ ${args.hide ? nothing : alertHtml(args)}
+ `,
+ frameworkSources: [
+ {
+ framework: 'HTML',
+ sourceParts: {
+ htmlFragment: (args) => raw`
+ ${getSourceFromLit(alertHtml(args))}
+ `,
+ jsFragment: `document.querySelector('omni-alert').show();`
+ }
+ }
+ ],
+ name: 'Primary Action',
+ args: {
+ 'status-indicator': '',
+ header: '',
+ '[Default Slot]': '',
+ primary: '',
+ secondary: '',
+
+ hide: true,
+
+ message: 'Message Alert',
+ description: 'Additional context for the alert.',
+
+ enableSecondary: undefined,
+
+ status: undefined,
+ headerAlign: undefined,
+ primaryAction: 'Acknowledge',
+ secondaryAction: undefined,
+ actionAlign: undefined
+ }
+};
+
+export const Secondary_Action: ComponentStoryFormat = {
+ description: () => html`
+
+
+ - Set the label for the secondary action button with the
secondary-action
attribute.
+ - Enable the secondary action button with the
enable-secondary
attribute.
+
+
+
+ `,
+ render: (args: Args) => html`
+ {
+ args.hide = false;
+ document.dispatchEvent(
+ new CustomEvent('story-renderer-interactive-update', {
+ bubbles: true,
+ composed: true
+ })
+ );
+ }}" label="Show Alert" >
+ ${args.hide ? nothing : alertHtml(args)}
+ `,
+ frameworkSources: [
+ {
+ framework: 'HTML',
+ sourceParts: {
+ htmlFragment: (args) => raw`
+ ${getSourceFromLit(alertHtml(args))}
+ `,
+ jsFragment: `document.querySelector('omni-alert').show();`
+ }
+ }
+ ],
+ name: 'Secondary Action',
+ args: {
+ 'status-indicator': '',
+ header: '',
+ '[Default Slot]': '',
+ primary: '',
+ secondary: '',
+
+ hide: true,
+
+ message: 'Message Alert',
+ description: 'Additional context for the alert.',
+
+ enableSecondary: true,
+
+ status: undefined,
+ headerAlign: undefined,
+ primaryAction: undefined,
+ secondaryAction: 'Back',
+ actionAlign: undefined
+ }
+};
+
+export const Action_Align: ComponentStoryFormat = {
+ description: 'Align the action button(s) horizontally.',
+ render: (args: Args) => html`
+ {
+ args.hide = false;
+ document.dispatchEvent(
+ new CustomEvent('story-renderer-interactive-update', {
+ bubbles: true,
+ composed: true
+ })
+ );
+ }}" label="Show Alert" >
+ ${args.hide ? nothing : alertHtml(args)}
+ `,
+ frameworkSources: [
+ {
+ framework: 'HTML',
+ sourceParts: {
+ htmlFragment: (args) => raw`
+ ${getSourceFromLit(alertHtml(args))}
+ `,
+ jsFragment: `document.querySelector('omni-alert').show();`
+ }
+ }
+ ],
+ name: 'Action Align',
+ args: {
+ 'status-indicator': '',
+ header: '',
+ '[Default Slot]': '',
+ primary: '',
+ secondary: '',
+
+ hide: true,
+
+ message: 'Message Alert',
+ description: 'Additional context for the alert.',
+
+ enableSecondary: true,
+
+ status: undefined,
+ headerAlign: undefined,
+ primaryAction: 'Accept',
+ secondaryAction: 'Decline',
+ actionAlign: 'center'
+ }
+};
+
+export const Custom_Status_Indicator: ComponentStoryFormat = {
+ description: 'Render content as the status indicator instead of default status icons.',
+ render: (args: Args) => html`
+ {
+ args.hide = false;
+ document.dispatchEvent(
+ new CustomEvent('story-renderer-interactive-update', {
+ bubbles: true,
+ composed: true
+ })
+ );
+ }}" label="Show Alert" >
+ ${args.hide ? nothing : alertHtml(args)}
+ `,
+ frameworkSources: [
+ {
+ framework: 'HTML',
+ sourceParts: {
+ htmlFragment: (args) => raw`
+ ${getSourceFromLit(alertHtml(args))}
+ `,
+ jsFragment: `document.querySelector('omni-alert').show();`
+ }
+ }
+ ],
+ name: 'Custom Status Indicator',
+ args: {
+ 'status-indicator': raw`🔓`,
+ header: '',
+ '[Default Slot]': '',
+ primary: '',
+ secondary: '',
+
+ hide: true,
+
+ message: 'Message Alert',
+ description: 'Additional context for the alert.',
+
+ enableSecondary: undefined,
+
+ status: undefined,
+ headerAlign: undefined,
+ primaryAction: undefined,
+ secondaryAction: undefined,
+ actionAlign: undefined
+ }
+};
+
+export const Custom_Header: ComponentStoryFormat = {
+ description: () => html`Render content as the header message area. This overrides any text specified via the message
attribute.`,
+ render: (args: Args) => html`
+ {
+ args.hide = false;
+ document.dispatchEvent(
+ new CustomEvent('story-renderer-interactive-update', {
+ bubbles: true,
+ composed: true
+ })
+ );
+ }}" label="Show Alert" >
+ ${args.hide ? nothing : alertHtml(args)}
+ `,
+ frameworkSources: [
+ {
+ framework: 'HTML',
+ sourceParts: {
+ htmlFragment: (args) => raw`
+ ${getSourceFromLit(alertHtml(args))}
+ `,
+ jsFragment: `document.querySelector('omni-alert').show();`
+ }
+ }
+ ],
+ name: 'Custom Header',
+ args: {
+ 'status-indicator': '',
+ header: raw`Alert using the header
slot`,
+ '[Default Slot]': '',
+ primary: '',
+ secondary: '',
+
+ hide: true,
+
+ message: 'Message Alert',
+ description: 'Additional context for the alert.',
+
+ enableSecondary: undefined,
+
+ status: undefined,
+ headerAlign: undefined,
+ primaryAction: undefined,
+ secondaryAction: undefined,
+ actionAlign: undefined
+ }
+};
+
+export const Custom_Body: ComponentStoryFormat = {
+ description: () => html`Render rich html content in the description. This appends to text specified via the description
attribute.`,
+ render: (args: Args) => html`
+ {
+ args.hide = false;
+ document.dispatchEvent(
+ new CustomEvent('story-renderer-interactive-update', {
+ bubbles: true,
+ composed: true
+ })
+ );
+ }}" label="Show Alert" >
+ ${args.hide ? nothing : alertHtml(args)}
+ `,
+ frameworkSources: [
+ {
+ framework: 'HTML',
+ sourceParts: {
+ htmlFragment: (args) => raw`
+ ${getSourceFromLit(alertHtml(args))}
+ `,
+ jsFragment: `document.querySelector('omni-alert').show();`
+ }
+ }
+ ],
+ name: 'Custom Body',
+ args: {
+ 'status-indicator': '',
+ header: '',
+ '[Default Slot]': raw`Alert using the default slot and the description
attribute.`,
+ primary: '',
+ secondary: '',
+
+ hide: true,
+
+ message: 'Message Alert',
+ description: 'Additional context for the alert.',
+
+ enableSecondary: undefined,
+
+ status: undefined,
+ headerAlign: undefined,
+ primaryAction: undefined,
+ secondaryAction: undefined,
+ actionAlign: undefined
+ }
+};
+
+export const Custom_Primary_Action: ComponentStoryFormat = {
+ description: () =>
+ html`Render rich html content as the primary action. This replaces any text specified via the primary-action
attribute.`,
+ render: (args: Args) => html`
+ {
+ args.hide = false;
+ document.dispatchEvent(
+ new CustomEvent('story-renderer-interactive-update', {
+ bubbles: true,
+ composed: true
+ })
+ );
+ }}" label="Show Alert" >
+ ${args.hide ? nothing : alertHtml(args)}
+ `,
+ frameworkSources: [
+ {
+ framework: 'HTML',
+ sourceParts: {
+ htmlFragment: (args) => raw`
+ ${getSourceFromLit(alertHtml(args))}
+ `,
+ jsFragment: `document.querySelector('omni-alert').show();`
+ }
+ }
+ ],
+ name: 'Custom Primary Action',
+ args: {
+ 'status-indicator': '',
+ header: '',
+ '[Default Slot]': '',
+ primary: raw``,
+ secondary: '',
+
+ hide: true,
+
+ message: 'Message Alert',
+ description: 'Additional context for the alert.',
+
+ enableSecondary: undefined,
+
+ status: undefined,
+ headerAlign: undefined,
+ primaryAction: undefined,
+ secondaryAction: undefined,
+ actionAlign: undefined
+ }
+};
+
+export const Custom_Secondary_Action: ComponentStoryFormat = {
+ description: () =>
+ html`Render rich html content as the secondary action. This replaces any text specified via the secondary-action
attribute.`,
+ render: (args: Args) => html`
+ {
+ args.hide = false;
+ document.dispatchEvent(
+ new CustomEvent('story-renderer-interactive-update', {
+ bubbles: true,
+ composed: true
+ })
+ );
+ }}" label="Show Alert" >
+ ${args.hide ? nothing : alertHtml(args)}
+ `,
+ frameworkSources: [
+ {
+ framework: 'HTML',
+ sourceParts: {
+ htmlFragment: (args) => raw`
+ ${getSourceFromLit(alertHtml(args))}
+ `,
+ jsFragment: `document.querySelector('omni-alert').show();`
+ }
+ }
+ ],
+ name: 'Custom Secondary Action',
+ args: {
+ 'status-indicator': '',
+ header: '',
+ '[Default Slot]': '',
+ primary: '',
+ secondary: raw`↩`,
+
+ hide: true,
+
+ message: 'Message Alert',
+ description: 'Additional context for the alert.',
+
+ enableSecondary: true,
+
+ status: undefined,
+ headerAlign: undefined,
+ primaryAction: undefined,
+ secondaryAction: undefined,
+ actionAlign: undefined
+ }
+};
diff --git a/src/alert/Alert.ts b/src/alert/Alert.ts
new file mode 100644
index 000000000..7c3c12986
--- /dev/null
+++ b/src/alert/Alert.ts
@@ -0,0 +1,715 @@
+import { html, css, type TemplateResult, nothing } from 'lit';
+import { customElement, property, query } from 'lit/decorators.js';
+import { OmniElement } from '../core/OmniElement.js';
+import { Modal } from '../modal/Modal.js';
+import type { RenderFunction, RenderResult } from '../render-element/RenderElement.js';
+
+export type { RenderFunction, RenderResult } from '../render-element/RenderElement.js';
+
+import '../button/Button.js';
+import '../render-element/RenderElement.js';
+import '../modal/Modal.js';
+
+/**
+ * Component that displays an alert.
+ *
+ * @import
+ * ```js
+ * import '@capitec/omni-components/alert';
+ * ```
+ *
+ * @example
+ * ```html
+ *
+ *
+ * ```
+ *
+ * @element omni-alert
+ *
+ * Registry of all properties defined by the component.
+ *
+ * @fires close-click - Dispatched when the close button is clicked.
+ *
+ * @slot status-indicator - Content to render as the status indicator instead of default status icons.
+ * @slot header - Content to render inside the component message area.
+ * @slot - Content to render inside the component description body.
+ * @slot primary - Content to render as the primary action button.
+ * @slot secondary - Content to render as the secondary action button.
+ *
+ * @csspart modal - Internal `omni-modal` element instance.
+ * @csspart content - Internal `HTMLDivElement` instance for container of header and description content.
+ * @csspart header - Internal `HTMLDivElement` instance for header.
+ * @csspart actions - Internal `HTMLDivElement` instance for container of action button(s).
+ *
+ * @cssprop --omni-alert-min-width - Minimum width for alert.
+ * @cssprop --omni-alert-max-width - Maximum width for alert.
+ * @cssprop --omni-alert-border - Alert border.
+ * @cssprop --omni-alert-border-radius - Alert border radius.
+ * @cssprop --omni-alert-box-shadow - Alert box shadow.
+ *
+ * @cssprop --omni-alert-animation-duration - Alert fade in and out animation duration.
+ * @cssprop --omni-alert-padding-top - Alert content top padding.
+ * @cssprop --omni-alert-padding-bottom - Alert content bottom padding.
+ * @cssprop --omni-alert-padding-left - Alert content left padding.
+ * @cssprop --omni-alert-padding-right - Alert content right padding.
+ *
+ * @cssprop --omni-alert-header-font-color - Alert header font color.
+ * @cssprop --omni-alert-header-font-family - Alert header font family.
+ * @cssprop --omni-alert-header-font-size - Alert header font size.
+ * @cssprop --omni-alert-header-font-weight - Alert header font weight.
+ * @cssprop --omni-alert-header-line-height - Alert header line height.
+ * @cssprop --omni-alert-header-background - Alert header background.
+ *
+ * @cssprop --omni-alert-header-padding-top - Alert header top padding.
+ * @cssprop --omni-alert-header-padding-bottom - Alert header bottom padding.
+ * @cssprop --omni-alert-header-padding-left - Alert header left padding.
+ * @cssprop --omni-alert-header-padding-right - Alert header right padding.
+ *
+ * @cssprop --omni-alert-description-font-color - Alert description font color.
+ * @cssprop --omni-alert-description-font-family - Alert description font family.
+ * @cssprop --omni-alert-description-font-size - Alert description font size.
+ * @cssprop --omni-alert-description-font-weight - Alert description font weight.
+ * @cssprop --omni-alert-description-line-height - Alert description line height.
+ *
+ * @cssprop --omni-alert-description-padding-top - Alert description top padding.
+ * @cssprop --omni-alert-description-padding-bottom - Alert description bottom padding.
+ * @cssprop --omni-alert-description-padding-left - Alert description left padding.
+ * @cssprop --omni-alert-description-padding-right - Alert description right padding.
+ *
+ * @cssprop --omni-alert-action-button-padding-top - Alert action button(s) top padding.
+ * @cssprop --omni-alert-action-button-padding-bottom - Alert action button(s) bottom padding.
+ * @cssprop --omni-alert-action-button-padding-left - Alert action button(s) left padding.
+ * @cssprop --omni-alert-action-button-padding-right - Alert action button(s) right padding.
+ *
+ * @cssprop --omni-alert-action-button-internal-padding-top - Alert action button(s) internal top padding.
+ * @cssprop --omni-alert-action-button-internal-padding-bottom - Alert action button(s) internal bottom padding.
+ * @cssprop --omni-alert-action-button-internal-padding-left - Alert action button(s) internal left padding.
+ * @cssprop --omni-alert-action-button-internal-padding-right - Alert action button(s) internal right padding.
+ *
+ * @cssprop --omni-header-horizontal-gap - Alert header horizontal space between status indicator and header content.
+ *
+ * @cssprop --omni-header-status-size - Alert header status indicator symmetrical size.
+ */
+@customElement('omni-alert')
+export class Alert extends OmniElement {
+ /**
+ * Internal `omni-modal` instance.
+ * @no_attribute
+ * @ignore
+ */
+ @query('omni-modal') modal!: Modal;
+
+ /**
+ * The alert status.
+ * @attr
+ */
+ @property({ type: String, reflect: true }) status: 'success' | 'warning' | 'error' | 'info' | 'none' = 'none';
+
+ /**
+ * The alert header message.
+ * @attr
+ */
+ @property({ type: String, reflect: true }) message?: string;
+
+ /**
+ * Header content horizontal alignment:
+ * - `left` Align header to the left.
+ * - `center` Align header to the center.
+ * - `right` Align header to the right.
+ * @attr [header-align]
+ */
+ @property({ type: String, attribute: 'header-align', reflect: true }) headerAlign?: 'left' | 'center' | 'right';
+
+ /**
+ * The alert detail message.
+ * @attr
+ */
+ @property({ type: String, reflect: true }) description?: string;
+
+ /**
+ * The primary action button label.
+ * @attr [primary-action]
+ */
+ @property({ type: String, reflect: true, attribute: 'primary-action' }) primaryAction?: string;
+
+ /**
+ * The secondary action button label.
+ * @attr [secondary-action]
+ */
+ @property({ type: String, reflect: true, attribute: 'secondary-action' }) secondaryAction?: string;
+
+ /**
+ * If true, will provide a secondary action button.
+ * @attr [enable-secondary]
+ */
+ @property({ type: Boolean, reflect: true, attribute: 'enable-secondary' }) enableSecondary?: boolean;
+
+ /**
+ * Action button(s) horizontal alignment:
+ * - `left` Align action button(s) to the left.
+ * - `center` Align action button(s) to the center.
+ * - `right` Align action button(s) to the right.
+ * @attr [action-align]
+ */
+ @property({ type: String, attribute: 'action-align', reflect: true }) actionAlign?: 'left' | 'center' | 'right' | 'stretch';
+
+ /**
+ * Create a global notification element without showing it.
+ *
+ * @returns The alert instance.
+ */
+ static create(init: AlertInit) {
+ const element = document.body.appendChild(document.createElement('omni-alert'));
+ if (!init) {
+ init = {};
+ }
+
+ // Set the notification component values.
+ element.status = init.status ?? 'none';
+ element.message = init.message;
+ element.headerAlign = init.headerAlign;
+ element.description = init.description;
+ element.primaryAction = init.primaryAction;
+ element.secondaryAction = init.secondaryAction;
+ element.enableSecondary = init.enableSecondary;
+ element.actionAlign = init.actionAlign;
+ if (init.id) {
+ element.id = init.id;
+ }
+
+ // Setup optional renderers for slot(s)
+ if (init.statusIndicator) {
+ const renderElement = document.createElement('omni-render-element');
+ renderElement.slot = 'status-indicator';
+ renderElement.renderer = typeof init.statusIndicator === 'function' ? init.statusIndicator : () => init.statusIndicator as RenderResult;
+ element.appendChild(renderElement);
+ }
+ if (init.header) {
+ const renderElement = document.createElement('omni-render-element');
+ renderElement.slot = 'header';
+ renderElement.renderer = typeof init.header === 'function' ? init.header : () => init.header as RenderResult;
+ element.appendChild(renderElement);
+ }
+ if (init.body) {
+ const renderElement = document.createElement('omni-render-element');
+ renderElement.renderer = typeof init.body === 'function' ? init.body : () => init.body as RenderResult;
+ element.appendChild(renderElement);
+ }
+ if (init.primary) {
+ const renderElement = document.createElement('omni-render-element');
+ renderElement.slot = 'primary';
+ renderElement.renderer = typeof init.primary === 'function' ? init.primary : () => init.primary as RenderResult;
+ element.appendChild(renderElement);
+ }
+ if (init.secondary) {
+ const renderElement = document.createElement('omni-render-element');
+ renderElement.slot = 'secondary';
+ renderElement.renderer = typeof init.secondary === 'function' ? init.secondary : () => init.secondary as RenderResult;
+ element.appendChild(renderElement);
+ }
+
+ return element;
+ }
+
+ /**
+ * Show a global notification element.
+ *
+ * @returns The alert instance.
+ */
+ static show(
+ init: AlertInit & {
+ onClose?: (reason: 'auto' | 'primary' | 'secondary') => void;
+ }
+ ) {
+ const element = Alert.create(init);
+
+ if (init.onClose) {
+ let reason: 'auto' | 'primary' | 'secondary' = 'auto';
+ element.addEventListener('alert-action-click', (e: Event) => {
+ const actionClickEvent = e as CustomEvent<{ secondary: boolean }>;
+ reason = actionClickEvent.detail.secondary ? 'secondary' : 'primary';
+ });
+ element.addEventListener('alert-close', () => {
+ init.onClose?.apply(element, [reason]);
+ });
+ }
+
+ // Show the component as a modal dialog.
+ return element.show();
+ }
+
+ /**
+ * Show a global notification element asynchronously, waits for it to close and returns the reason for close.
+ *
+ * @returns The reason for the alert close.
+ */
+ static showAsync(init: AlertInit) {
+ const element = Alert.create(init);
+ return element.showAsync();
+ }
+
+ /**
+ * Show the notification asynchronously, waits for it to close and returns the reason for close.
+ *
+ * @returns The reason for the alert close.
+ */
+ showAsync() {
+ return new Promise<'auto' | 'primary' | 'secondary'>((resolve, reject) => {
+ try {
+ this.show();
+ let reason: 'auto' | 'primary' | 'secondary' = 'auto';
+ this.addEventListener('alert-action-click', (e: Event) => {
+ const actionClickEvent = e as CustomEvent<{ secondary: boolean }>;
+ reason = actionClickEvent.detail.secondary ? 'secondary' : 'primary';
+ });
+ this.addEventListener('alert-close', () => {
+ resolve?.apply(this, [reason]);
+ });
+ } catch (error) {
+ reject.apply(this, [error]);
+ }
+ });
+ }
+
+ /**
+ * Show the notification.
+ *
+ * @returns The alert instance.
+ */
+ show(): Alert {
+ // Show the component modal dialog, after the initial component render has completed.
+ this.updateComplete.then(() => {
+ this.modal.hide = false;
+ });
+ return this;
+ }
+
+ /**
+ * Hide the notification and remove the component from the DOM
+ */
+ hide(): void {
+ this.updateComplete.then(async () => {
+ const { matches: motionOK } = window.matchMedia('(prefers-reduced-motion: no-preference)');
+
+ // Animate the alert fading out if the user allows motion.
+ if (motionOK && document.timeline) {
+ // Get current opacity to cater for existing fade out of timed toasts.
+ const currentOpacity = Number(getComputedStyle(this.modal).getPropertyValue('opacity'));
+
+ const anim = this.modal.animate(
+ [
+ // key frames
+ { offset: 0, opacity: currentOpacity },
+ { offset: 1, opacity: 0 }
+ ],
+ {
+ // sync options
+ duration: 500,
+ easing: 'ease'
+ }
+ );
+ await anim.finished;
+ }
+
+ this.modal.hide = true;
+ this.dispatchEvent(new CustomEvent('alert-close'));
+ if (this.parentElement) {
+ this.remove();
+ }
+ });
+ }
+
+ private onActionClick(secondary?: boolean) {
+ this.dispatchEvent(
+ new CustomEvent('alert-action-click', {
+ detail: {
+ secondary: secondary ?? false
+ }
+ })
+ );
+
+ this.hide();
+ }
+
+ /**
+ * The element style template.
+ */
+ static override get styles() {
+ return [
+ super.styles,
+ css`
+ :host {
+ box-sizing: border-box;
+ }
+
+ omni-modal {
+ --omni-modal-body-padding: 0px;
+ }
+
+ omni-modal::part(container) {
+
+ min-width: var(--omni-alert-min-width, 20%);
+ max-width: var(--omni-alert-max-width, 80%);
+ }
+
+ /** Dialog */
+
+ .container {
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ justify-content: flex-start;
+
+ padding: 0px;
+
+ border: var(--omni-alert-border, none);
+
+ box-shadow: var(--omni-alert-box-shadow, 0px 0px 3px rgba(0, 0, 0, 0.1));
+ border-radius: var(--omni-alert-border-radius, 10px);
+ }
+
+ omni-modal:not([hide]) {
+ animation: fadein var(--omni-alert-animation-duration, 0.5s) ease-in-out;
+ animation-fill-mode: forwards;
+ }
+
+ @media (prefers-reduced-motion) {
+ /* styles to apply if a user's device settings are set to reduced motion */
+ omni-modal:not([hide]) {
+ animation: unset;
+ opacity: 1;
+ }
+ }
+
+ @keyframes fadein {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+ }
+
+ /* Content */
+
+ .content {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+
+ padding-top: var(--omni-alert-padding-top, 10px);
+ padding-bottom: var(--omni-alert-padding-bottom, 10px);
+ padding-left: var(--omni-alert-padding-left, 10px);
+ padding-right: var(--omni-alert-padding-right, 10px);
+ }
+
+ .header {
+ display: inline-flex;
+ align-items: center;
+ text-align: center;
+ justify-content: center;
+ color: var(--omni-alert-header-font-color,var(--omni-font-color));
+ background: var(--omni-alert-header-background, var(--omni-background-color));
+ font-family: var(--omni-alert-header-font-family, var(--omni-font-family));
+ font-size: var(--omni-alert-header-font-size, var(--omni-font-size));
+ line-height: var(--omni-alert-header-line-height, 1.2);
+ font-weight: var(--omni-alert-header-font-weight, bold);
+
+ position: relative;
+
+ margin-top: var(--omni-alert-header-padding-top, 10px);
+ margin-bottom: var(--omni-alert-header-padding-bottom, 0px);
+ margin-left: var(--omni-alert-header-padding-left, 0px);
+ margin-right: var(--omni-alert-header-padding-right, 0px);
+ }
+
+ .header.left {
+ justify-content: left;
+ }
+
+ .header.right {
+ justify-content: right;
+ text-align: right;
+ }
+
+ ::slotted(*:not([slot])),
+ .description {
+ font-family: var(--omni-alert-description-font-family, sans-serif);
+ font-size: var(--omni-alert-description-font-size, 16px);
+ font-weight: var(--omni-alert-description-font-weight, normal);
+ line-height: var(--omni-alert-description-line-height, 1.2);
+
+ color: var(--omni-alert-description-font-color, var(--omni-font-color));
+
+ text-align: center;
+
+ margin-top: var(--omni-alert-description-padding-top, 10px);
+ margin-bottom: var(--omni-alert-description-padding-bottom, 0px);
+ margin-left: var(--omni-alert-description-padding-left, 0px);
+ margin-right: var(--omni-alert-description-padding-right, 0px);
+ }
+
+ .actions {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-end;
+ align-items: center;
+ }
+
+ .actions.center {
+ justify-content: center;
+ text-align: center;
+ }
+
+ .actions.left {
+ flex-direction: row-reverse;
+ text-align: left;
+ }
+
+ .actions.left .action-btn {
+ padding-left: var(--omni-alert-action-button-padding-left, 4px);
+ padding-right: var(--omni-alert-action-button-padding-right);
+ }
+
+ .actions.stretch .action-btn,
+ .actions.stretch .clear-btn {
+ padding-left: var(--omni-alert-action-button-padding-left, 4px);
+ padding-right: var(--omni-alert-action-button-padding-right, 4px);
+ width: 100%;
+ }
+
+ .action-btn {
+ padding-top: var(--omni-alert-action-button-padding-top);
+ padding-bottom: var(--omni-alert-action-button-padding-bottom, 4px);
+ padding-left: var(--omni-alert-action-button-padding-left);
+ padding-right: var(--omni-alert-action-button-padding-right, 4px);
+
+ --omni-button-padding-top:var(--omni-alert-action-button-internal-padding-top, 0px);
+ --omni-button-padding-bottom:var(--omni-alert-action-button-internal-padding-bottom, 0px);
+ --omni-button-padding-left:var(--omni-alert-action-button-internal-padding-left, 4px);
+ --omni-button-padding-right:var(--omni-alert-action-button-internal-padding-right, 4px);
+ }
+
+ .clear-btn {
+ padding-top: var(--omni-alert-action-button-padding-top, 4px);
+ padding-bottom: var(--omni-alert-action-button-padding-bottom, 4px);
+ padding-left: var(--omni-alert-action-button-padding-left);
+ padding-right: var(--omni-alert-action-button-padding-right);
+
+ --omni-button-padding-top:var(--omni-alert-action-button-internal-padding-top);
+ --omni-button-padding-bottom:var(--omni-alert-action-button-internal-padding-bottom);
+ --omni-button-padding-left:var(--omni-alert-action-button-internal-padding-left);
+ --omni-button-padding-right:var(--omni-alert-action-button-internal-padding-right);
+ }
+
+ .status-icon {
+
+ margin-right: var(--omni-header-horizontal-gap, 10px);
+
+ width: var(--omni-header-status-size, 24px);
+ height: var(--omni-header-status-size, 24px);
+
+ min-width: var(--omni-header-status-size, 24px);
+ min-height: var(--omni-header-status-size, 24px);
+
+ max-width: var(--omni-header-status-size, 24px);
+ max-height: var(--omni-header-status-size, 24px);
+ }
+ `
+ ];
+ }
+
+ /**
+ * Generate the web component template.
+ *
+ * @returns The HTML component template.
+ */
+ override render(): TemplateResult {
+ // Determine the icon to show.
+ let iconTemplate: TemplateResult | typeof nothing = nothing;
+
+ // Derive the icon from the status.
+ switch (this.status) {
+ case 'info':
+ iconTemplate = html`
+
+ `;
+ break;
+
+ case 'success':
+ iconTemplate = html`
+
+ `;
+ break;
+
+ case 'error':
+ iconTemplate = html`
+
+ `;
+ break;
+
+ case 'warning':
+ iconTemplate = html`
+
+ `;
+ break;
+
+ case 'none':
+ default:
+ iconTemplate = nothing;
+ break;
+ }
+
+ // Generate the component template.
+ return html`
+
+
+
+ ${
+ this.description
+ ? this.description.split('\n').map((paragraph) => html`
${paragraph}
`)
+ : nothing
+ }
+
+
+
+
+ ${
+ this.enableSecondary
+ ? html`
+
this.onActionClick(true)}">
+
+
+
+
+ `
+ : nothing
+ }
+
this.onActionClick()}">
+
+
+
+
+
+
+ `;
+ }
+}
+
+/**
+ * Context for `Alert.show`/`Alert.showAsync` function(s) to programmatically render a new `` instance.
+ */
+export type AlertInit = {
+ /**
+ * The id to apply to the Alert element.
+ */
+ id?: string;
+
+ /**
+ * A function that returns, or an instance of content to render as the alert status indicator
+ */
+ statusIndicator?: RenderFunction | RenderResult;
+
+ /**
+ * A function that returns, or an instance of content to render in the alert header
+ */
+ header?: RenderFunction | RenderResult;
+
+ /**
+ * A function that returns, or an instance of content to render as alert body
+ */
+ body?: RenderFunction | RenderResult;
+
+ /**
+ * A function that returns, or an instance of content to render as the alert primary action
+ */
+ primary?: RenderFunction | RenderResult;
+
+ /**
+ * A function that returns, or an instance of content to render as the alert secondary action
+ */
+ secondary?: RenderFunction | RenderResult;
+
+ /**
+ * The alert status.
+ */
+ status?: 'success' | 'warning' | 'error' | 'info' | 'none';
+
+ /**
+ * The alert header message.
+ */
+ message?: string;
+
+ /**
+ * Header content horizontal alignment:
+ * - `left` Align header to the left.
+ * - `center` Align header to the center.
+ * - `right` Align header to the right.
+ */
+ headerAlign?: 'left' | 'center' | 'right';
+
+ /**
+ * The alert detail message.
+ */
+ description?: string;
+
+ /**
+ * The primary action button label.
+ */
+ primaryAction?: string;
+
+ /**
+ * The secondary action button label.
+ */
+ secondaryAction?: string;
+
+ /**
+ * If true, will provide a secondary action button.
+ */
+ enableSecondary?: boolean;
+
+ /**
+ * Action button(s) horizontal alignment:
+ * - `left` Align action button(s) to the left.
+ * - `center` Align action button(s) to the center.
+ * - `right` Align action button(s) to the right.
+ */
+ actionAlign?: 'left' | 'center' | 'right' | 'stretch';
+};
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'omni-alert': Alert;
+ }
+}
diff --git a/src/alert/index.ts b/src/alert/index.ts
new file mode 100644
index 000000000..315789b33
--- /dev/null
+++ b/src/alert/index.ts
@@ -0,0 +1 @@
+export * from './Alert.js';
diff --git a/src/modal/Modal.ts b/src/modal/Modal.ts
index b347e609d..c67e856f9 100644
--- a/src/modal/Modal.ts
+++ b/src/modal/Modal.ts
@@ -233,6 +233,8 @@ export class Modal extends OmniElement {
left: var(--omni-modal-dialog-left, 0px);
right: var(--omni-modal-dialog-right, 0px);
bottom: var(--omni-modal-dialog-bottom, 0px);
+
+ opacity: inherit;
}
::backdrop {
diff --git a/src/utils/LivePropertyEditor.ts b/src/utils/LivePropertyEditor.ts
index afb214101..208acb354 100644
--- a/src/utils/LivePropertyEditor.ts
+++ b/src/utils/LivePropertyEditor.ts
@@ -277,10 +277,13 @@ export class LivePropertyEditor extends OmniElement {
!attribute.type?.text?.replace('| undefined', '')?.trim().includes('Promise') &&
attribute.type?.text?.replace('| undefined', '')?.trim().includes("'")
) {
- const typesRaw = attribute.type?.text.split(' | ');
+ const typesRaw = attribute.type?.text?.replace('| undefined', '').split(' | ');
const types = [];
for (const type in typesRaw) {
- const typeValue = typesRaw[type].replaceAll('| ', '').replaceAll('\r\n', '').replaceAll(' ', '');
+ const typeValue = typesRaw[type]
+ .replaceAll('| ', '')
+ .replace(/(\r\n|\n|\r)/gm, '')
+ .replaceAll(' ', '');
types.push(typeValue.substring(1, typeValue.length - 1));
}
const startValue = this.data
From cf627d671c7bb00b5debc7ff432f94452c312c32 Mon Sep 17 00:00:00 2001
From: BOTLANNER <16349308+BOTLANNER@users.noreply.github.com>
Date: Wed, 19 Jul 2023 15:24:55 +0200
Subject: [PATCH 02/14] * Extended docs to display instance and static function
information * Updated custom elements plugin for type checking support (used
by docs)
---
.tooling/eleventy/components.njk | 60 ++++++++++++++++++++++++++++
.tooling/scripts/eleventy/filters.js | 59 ++++++++++++++++++++++++++-
custom-elements-manifest.config.js | 56 ++++++++++++++++++++++++++
3 files changed, 173 insertions(+), 2 deletions(-)
diff --git a/.tooling/eleventy/components.njk b/.tooling/eleventy/components.njk
index 3bcc08a33..2857ec4fe 100644
--- a/.tooling/eleventy/components.njk
+++ b/.tooling/eleventy/components.njk
@@ -127,6 +127,66 @@ eleventyComputed:
{% endif %}
+ {% set instanceFuncs = customElements | getInstanceFunctions(component.name) %}
+ {% if instanceFuncs.length > 0 %}
+
+ {#
+
+
#}
+
+
+
+
+ Name |
+ Parameters |
+ Returns |
+ Description |
+
+
+
+ {% for f in instanceFuncs %}
+
+ {{f.name}} |
+ {{ f.parameters }}
|
+ {{ f.returnType }}
|
+ {{ f.description | safe }} |
+
+ {% endfor %}
+
+
+
+ {% endif %}
+
+ {% set staticFuncs = customElements | getStaticFunctions(component.name) %}
+ {% if staticFuncs.length > 0 %}
+
+ {#
+
+
#}
+
+
+
+
+ Name |
+ Parameters |
+ Returns |
+ Description |
+
+
+
+ {% for f in staticFuncs %}
+
+ {{f.name}} |
+ {{ f.parameters }}
|
+ {{ f.returnType }}
|
+ {{ f.description | safe }} |
+
+ {% endfor %}
+
+
+
+ {% endif %}
+
{% set globalAttributes = customElements | getGlobalAttributes(component.name) %}
{% if globalAttributes.length > 0 %}
diff --git a/.tooling/scripts/eleventy/filters.js b/.tooling/scripts/eleventy/filters.js
index efacea1af..5cc132958 100644
--- a/.tooling/scripts/eleventy/filters.js
+++ b/.tooling/scripts/eleventy/filters.js
@@ -44,6 +44,61 @@ export function getProperties(value, componentName) {
});
}
+function convertToParameterString(parametersList) {
+ let parameters = '';
+
+ if (!parametersList || parametersList.length === 0) {
+ return parameters;
+ }
+
+ parametersList.forEach(p => {
+ if (parameters) {
+ parameters += ',\r\n';
+ }
+ if (!p.type) {
+ console.log(p);
+ }
+
+ parameters += `${p.name} - ${p.type.text}`
+ });
+
+ return parameters;
+}
+
+export function getInstanceFunctions(value, componentName) {
+ const declaration = getComponentDeclaration(value, componentName);
+ return declaration.members?.filter(m => m.kind === 'method' &&
+ m.privacy !== 'private' &&
+ m.privacy !== 'protected' &&
+ m.description &&
+ m.static?.toString() !== 'true' &&
+ !m.name.startsWith('_'))?.map(a => {
+ return {
+ ...a,
+ parameters: convertToParameterString(a.parameters),
+ description: transformFromJsdoc(a.description),
+ returnType: a.return?.type?.text ?? ''
+ };
+ });
+}
+
+export function getStaticFunctions(value, componentName) {
+ const declaration = getComponentDeclaration(value, componentName);
+ return declaration.members?.filter(m => m.kind === 'method' &&
+ m.privacy !== 'private' &&
+ m.privacy !== 'protected' &&
+ m.description &&
+ m.static?.toString() === 'true' &&
+ !m.name.startsWith('_'))?.map(a => {
+ return {
+ ...a,
+ parameters: convertToParameterString(a.parameters),
+ description: transformFromJsdoc(a.description),
+ returnType: a.return?.type?.text ?? ''
+ };
+ });
+}
+
export function getGlobalAttributes(value, componentName) {
const declaration = getComponentDeclaration(value, componentName);
return declaration.globalAttributes?.map(a => {
@@ -110,8 +165,8 @@ export function getCSSProperties(value, componentName) {
}
export function splitPascalCase(word) {
- var wordRe = /($[a-z])|[A-Z][^A-Z]+/g;
- return word.match(wordRe).join(' ');
+ var wordRe = /($[a-z])|[A-Z][^A-Z]+/g;
+ return word.match(wordRe).join(' ');
}
function distinct(value, index, self) {
diff --git a/custom-elements-manifest.config.js b/custom-elements-manifest.config.js
index ba47ce265..1d0a86eb8 100644
--- a/custom-elements-manifest.config.js
+++ b/custom-elements-manifest.config.js
@@ -1,6 +1,21 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable no-case-declarations */
+import { readFileSync } from 'fs';
+
+let typeChecker;
const plugins = {
+ // Setup our own module creation in order to track a global type checker for documentation enrichment
+ overrideModuleCreation: ({ ts, globs }) => {
+ const tsconfig = JSON.parse(readFileSync('./tsconfig.json'));
+ const program = ts.createProgram(globs, {
+ ...tsconfig.compilerOptions,
+ moduleResolution: undefined,
+ });
+
+ typeChecker = program.getTypeChecker();
+
+ return program.getSourceFiles().filter(sf => globs.find(glob => sf.fileName.includes(glob)));
+ },
plugins: [
function ignorePlugin() {
return {
@@ -362,6 +377,47 @@ const plugins = {
}
};
}(),
+ function inferMethodReturnTypesPlugin() {
+ return {
+ analyzePhase({ ts, node, moduleDoc }) {
+ switch (node.kind) {
+ case ts.SyntaxKind.MethodDeclaration:
+ {
+ const funcName = node.name.getText();
+ const isStatic = Boolean(node.modifiers?.find(m => m.kind === ts.SyntaxKind.StaticKeyword));
+ const classNode = findParentClass(ts, node);
+ if (classNode) {
+ const className = classNode.name.getText();
+ const classDeclaration = moduleDoc.declarations.find(declaration => declaration.name === className);
+
+ const method = classDeclaration.members.find(m => m.name === funcName && m.kind === 'method' && ((m.static && isStatic) || (!m.static && !isStatic)));
+ if (method && !method.return?.type?.text) {
+ const ret = {
+ ...(method.return || {}),
+ type: {
+ ...(method.return?.type || {}),
+ // Use Typescript type checker to read inferred type as text.
+ // https://stackoverflow.com/questions/75865839/extract-inferred-return-type-with-typescript-api
+ // https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API#using-the-type-checker
+ text: typeChecker.typeToString(typeChecker.getSignatureFromDeclaration(node).getReturnType())
+ }
+ };
+ method.return = ret;
+ }
+ }
+ }
+
+ break;
+ }
+ },
+ moduleLinkPhase({ moduleDoc }) {
+ // console.log(moduleDoc);
+ },
+ packageLinkPhase({ customElementsManifest }) {
+ // console.log(customElementsManifest);
+ }
+ };
+ }(),
function storyDescriptionPlugin() {
return {
// Runs for each module
From 36adeb34be2a17626631e105eabef7acf806a51d Mon Sep 17 00:00:00 2001
From: BOTLANNER <16349308+BOTLANNER@users.noreply.github.com>
Date: Wed, 19 Jul 2023 15:25:23 +0200
Subject: [PATCH 03/14] Added styles for code snippets on docs tab
---
.tooling/eleventy/assets/css/style.css | 2 ++
1 file changed, 2 insertions(+)
diff --git a/.tooling/eleventy/assets/css/style.css b/.tooling/eleventy/assets/css/style.css
index f96ad20ae..c3df1a5dd 100644
--- a/.tooling/eleventy/assets/css/style.css
+++ b/.tooling/eleventy/assets/css/style.css
@@ -600,6 +600,7 @@ h2 {
.static-article code:not([class]),
.story-description code:not([class]),
+.component-tab#docs code:not([class]),
.keyboard-showcase code:not([class]) {
font-family: ui-monospace, monospace;
font-size: 14px;
@@ -607,6 +608,7 @@ h2 {
.static-article code:not([class]),
.story-description code:not([class]),
+.component-tab#docs code:not([class]),
.keyboard-showcase code:not([class]) {
padding: 2px 6px;
margin: 0;
From 3b93ab5e81264662395d7f5d321542f173b68bb2 Mon Sep 17 00:00:00 2001
From: BOTLANNER <16349308+BOTLANNER@users.noreply.github.com>
Date: Wed, 19 Jul 2023 15:26:06 +0200
Subject: [PATCH 04/14] Improved docs JsDoc transformation for HTML encoding
---
src/utils/StoryUtils.ts | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/src/utils/StoryUtils.ts b/src/utils/StoryUtils.ts
index 06df49d5e..b259651af 100644
--- a/src/utils/StoryUtils.ts
+++ b/src/utils/StoryUtils.ts
@@ -482,12 +482,16 @@ function filterJsDocLinks(jsdoc: string) {
function transformFromJsdoc(jsdoc: string) {
if (!jsdoc) return jsdoc;
- const newline = '\r\n';
-
jsdoc = filterJsDocLinks(jsdoc);
- jsdoc = jsdoc.replace(new RegExp(newline, 'g'), raw`
`);
+
+ jsdoc = jsdoc.replace(new RegExp(/, 'g'), raw`<`);
+ jsdoc = jsdoc.replace(new RegExp(/>/, 'g'), raw`>`);
+
+ jsdoc = jsdoc.replace(/(\r\n|\n|\r)/gm, raw`
`);
jsdoc = jsdoc.replace(new RegExp(/\*/, 'g'), '•');
+ jsdoc = jsdoc.replace(/(`(.*?)`)/gi, raw`
$2
`);
+
return jsdoc;
}
From 7b9526a7aa3018af894c7ee2d76ed35d20768648 Mon Sep 17 00:00:00 2001
From: BOTLANNER <16349308+BOTLANNER@users.noreply.github.com>
Date: Wed, 19 Jul 2023 15:27:09 +0200
Subject: [PATCH 05/14] * Updated Alert documentation * Added
`description-align` attribute for Alert
---
src/alert/Alert.stories.ts | 92 +++++++++++++++++++++++++++++++++++---
src/alert/Alert.ts | 82 +++++++++++++++++++++++----------
2 files changed, 145 insertions(+), 29 deletions(-)
diff --git a/src/alert/Alert.stories.ts b/src/alert/Alert.stories.ts
index 211e3b18f..d647c5344 100644
--- a/src/alert/Alert.stories.ts
+++ b/src/alert/Alert.stories.ts
@@ -18,6 +18,7 @@ interface Args {
message?: string;
headerAlign?: 'left' | 'center' | 'right';
description?: string;
+ descriptionAlign?: 'left' | 'center' | 'right';
primaryAction?: string;
secondaryAction?: string;
enableSecondary?: boolean;
@@ -34,9 +35,14 @@ interface Args {
hide?: boolean;
}
-const alertHtml = (args: Args) => html`
+const alertHtml = (args: Args, onElement?: (a?: Alert) => void) => html`
(a as Alert)?.show())}
+ ${ref((a) => {
+ if (onElement) {
+ onElement(a as Alert | undefined);
+ }
+ (a as Alert)?.show();
+ })}
@alert-close="${() => {
args.hide = true;
document.dispatchEvent(
@@ -51,6 +57,7 @@ const alertHtml = (args: Args) => html`
message="${ifNotEmpty(args.message)}"
header-align="${ifNotEmpty(args.headerAlign)}"
description="${ifNotEmpty(args.description)}"
+ description-align="${ifNotEmpty(args.descriptionAlign)}"
primary-action="${ifNotEmpty(args.primaryAction)}"
secondary-action="${ifNotEmpty(args.secondaryAction)}"
?enable-secondary=${args.enableSecondary}
@@ -104,6 +111,7 @@ export const Interactive: ComponentStoryFormat = {
status: undefined,
headerAlign: undefined,
+ descriptionAlign: undefined,
primaryAction: undefined,
secondaryAction: undefined,
actionAlign: undefined
@@ -382,6 +390,7 @@ export const Status: ComponentStoryFormat = {
status: 'success',
headerAlign: undefined,
+ descriptionAlign: undefined,
primaryAction: undefined,
secondaryAction: undefined,
actionAlign: undefined
@@ -389,7 +398,7 @@ export const Status: ComponentStoryFormat = {
};
export const Header_Align: ComponentStoryFormat = {
- description: 'Align header content horizontally.',
+ description: () => html`Align header content horizontally (Defaults to 'center'
).`,
render: (args: Args) => html`
{
args.hide = false;
@@ -430,6 +439,69 @@ export const Header_Align: ComponentStoryFormat = {
status: undefined,
headerAlign: 'left',
+ descriptionAlign: undefined,
+ primaryAction: undefined,
+ secondaryAction: undefined,
+ actionAlign: undefined
+ }
+};
+
+export const Description_Align: ComponentStoryFormat = {
+ description: () => html`Align description content horizontally (Defaults to 'center'
).`,
+ render: (args: Args) => html`
+ {
+ args.hide = false;
+ document.dispatchEvent(
+ new CustomEvent('story-renderer-interactive-update', {
+ bubbles: true,
+ composed: true
+ })
+ );
+ }}" label="Show Alert" >
+ ${
+ args.hide
+ ? nothing
+ : alertHtml(args, (a) => {
+ if (a) {
+ a.description = `Additional context for the alert.
+Aligned to the ${args.descriptionAlign}.`;
+ }
+ })
+ }
+ `,
+ frameworkSources: [
+ {
+ framework: 'HTML',
+ sourceParts: {
+ htmlFragment: (args) => raw`
+ ${getSourceFromLit(alertHtml(args))}
+ `,
+ jsFragment: (args) => `const alert = document.querySelector('omni-alert');
+alert.description = \`Additional context for the alert.
+Aligned to the ${args.descriptionAlign}.\`;
+
+alert.show();`
+ }
+ }
+ ],
+ name: 'Header Align',
+ args: {
+ 'status-indicator': '',
+ header: '',
+ '[Default Slot]': '',
+ primary: '',
+ secondary: '',
+
+ hide: true,
+
+ message: 'Message Alert',
+ description: undefined,
+
+ enableSecondary: undefined,
+
+ status: undefined,
+ headerAlign: undefined,
+ descriptionAlign: 'right',
primaryAction: undefined,
secondaryAction: undefined,
actionAlign: undefined
@@ -437,7 +509,7 @@ export const Header_Align: ComponentStoryFormat = {
};
export const Primary_Action: ComponentStoryFormat = {
- description: 'Set the label for the primary action button.',
+ description: () => html`Set the label for the primary action button (Defaults to 'Ok'
).`,
render: (args: Args) => html`
{
args.hide = false;
@@ -478,6 +550,7 @@ export const Primary_Action: ComponentStoryFormat = {
status: undefined,
headerAlign: undefined,
+ descriptionAlign: undefined,
primaryAction: 'Acknowledge',
secondaryAction: undefined,
actionAlign: undefined
@@ -488,7 +561,7 @@ export const Secondary_Action: ComponentStoryFormat = {
description: () => html`
- - Set the label for the secondary action button with the
secondary-action
attribute.
+ - Set the label for the secondary action button with the
secondary-action
attribute (Defaults to 'Cancel'
).
- Enable the secondary action button with the
enable-secondary
attribute.
@@ -534,6 +607,7 @@ export const Secondary_Action: ComponentStoryFormat = {
status: undefined,
headerAlign: undefined,
+ descriptionAlign: undefined,
primaryAction: undefined,
secondaryAction: 'Back',
actionAlign: undefined
@@ -541,7 +615,7 @@ export const Secondary_Action: ComponentStoryFormat = {
};
export const Action_Align: ComponentStoryFormat = {
- description: 'Align the action button(s) horizontally.',
+ description: () => html`Align the action button(s) horizontally (Defaults to 'right'
).`,
render: (args: Args) => html`
{
args.hide = false;
@@ -582,6 +656,7 @@ export const Action_Align: ComponentStoryFormat = {
status: undefined,
headerAlign: undefined,
+ descriptionAlign: undefined,
primaryAction: 'Accept',
secondaryAction: 'Decline',
actionAlign: 'center'
@@ -630,6 +705,7 @@ export const Custom_Status_Indicator: ComponentStoryFormat = {
status: undefined,
headerAlign: undefined,
+ descriptionAlign: undefined,
primaryAction: undefined,
secondaryAction: undefined,
actionAlign: undefined
@@ -678,6 +754,7 @@ export const Custom_Header: ComponentStoryFormat = {
status: undefined,
headerAlign: undefined,
+ descriptionAlign: undefined,
primaryAction: undefined,
secondaryAction: undefined,
actionAlign: undefined
@@ -726,6 +803,7 @@ export const Custom_Body: ComponentStoryFormat = {
status: undefined,
headerAlign: undefined,
+ descriptionAlign: undefined,
primaryAction: undefined,
secondaryAction: undefined,
actionAlign: undefined
@@ -775,6 +853,7 @@ export const Custom_Primary_Action: ComponentStoryFormat = {
status: undefined,
headerAlign: undefined,
+ descriptionAlign: undefined,
primaryAction: undefined,
secondaryAction: undefined,
actionAlign: undefined
@@ -824,6 +903,7 @@ export const Custom_Secondary_Action: ComponentStoryFormat = {
status: undefined,
headerAlign: undefined,
+ descriptionAlign: undefined,
primaryAction: undefined,
secondaryAction: undefined,
actionAlign: undefined
diff --git a/src/alert/Alert.ts b/src/alert/Alert.ts
index 7c3c12986..e92e1c256 100644
--- a/src/alert/Alert.ts
+++ b/src/alert/Alert.ts
@@ -38,6 +38,7 @@ import '../modal/Modal.js';
*
* @csspart modal - Internal `omni-modal` element instance.
* @csspart content - Internal `HTMLDivElement` instance for container of header and description content.
+ * @csspart content - Internal `HTMLDivElement` instance for each line of description (does not include slotted description content).
* @csspart header - Internal `HTMLDivElement` instance for header.
* @csspart actions - Internal `HTMLDivElement` instance for container of action button(s).
*
@@ -100,7 +101,7 @@ export class Alert extends OmniElement {
@query('omni-modal') modal!: Modal;
/**
- * The alert status.
+ * The alert status (Defaults to 'none').
* @attr
*/
@property({ type: String, reflect: true }) status: 'success' | 'warning' | 'error' | 'info' | 'none' = 'none';
@@ -114,7 +115,7 @@ export class Alert extends OmniElement {
/**
* Header content horizontal alignment:
* - `left` Align header to the left.
- * - `center` Align header to the center.
+ * - `center` Align header to the center. (Default)
* - `right` Align header to the right.
* @attr [header-align]
*/
@@ -127,13 +128,22 @@ export class Alert extends OmniElement {
@property({ type: String, reflect: true }) description?: string;
/**
- * The primary action button label.
+ * Description content horizontal alignment:
+ * - `left` Align description content to the left.
+ * - `center` Align description content to the center. (Default)
+ * - `right` Align description content to the right.
+ * @attr [description-align]
+ */
+ @property({ type: String, attribute: 'description-align', reflect: true }) descriptionAlign?: 'left' | 'center' | 'right';
+
+ /**
+ * The primary action button label (Defaults to 'Ok').
* @attr [primary-action]
*/
@property({ type: String, reflect: true, attribute: 'primary-action' }) primaryAction?: string;
/**
- * The secondary action button label.
+ * The secondary action button label (Defaults to 'Cancel').
* @attr [secondary-action]
*/
@property({ type: String, reflect: true, attribute: 'secondary-action' }) secondaryAction?: string;
@@ -148,13 +158,14 @@ export class Alert extends OmniElement {
* Action button(s) horizontal alignment:
* - `left` Align action button(s) to the left.
* - `center` Align action button(s) to the center.
- * - `right` Align action button(s) to the right.
+ * - `right` Align action button(s) to the right. (Default)
+ * - `stretch` Align action button(s) stretched to fill the horizontal space.
* @attr [action-align]
*/
@property({ type: String, attribute: 'action-align', reflect: true }) actionAlign?: 'left' | 'center' | 'right' | 'stretch';
/**
- * Create a global notification element without showing it.
+ * Create a global `omni-alert` element without showing it.
*
* @returns The alert instance.
*/
@@ -164,10 +175,11 @@ export class Alert extends OmniElement {
init = {};
}
- // Set the notification component values.
+ // Set the `omni-alert` component values.
element.status = init.status ?? 'none';
element.message = init.message;
element.headerAlign = init.headerAlign;
+ element.descriptionAlign = init.descriptionAlign;
element.description = init.description;
element.primaryAction = init.primaryAction;
element.secondaryAction = init.secondaryAction;
@@ -208,11 +220,11 @@ export class Alert extends OmniElement {
element.appendChild(renderElement);
}
- return element;
+ return element as Alert;
}
/**
- * Show a global notification element.
+ * Show a global `omni-alert` element.
*
* @returns The alert instance.
*/
@@ -235,21 +247,21 @@ export class Alert extends OmniElement {
}
// Show the component as a modal dialog.
- return element.show();
+ return element.show() as Alert;
}
/**
- * Show a global notification element asynchronously, waits for it to close and returns the reason for close.
+ * Show a global `omni-alert` element asynchronously, waits for it to close and returns the reason for close.
*
* @returns The reason for the alert close.
*/
static showAsync(init: AlertInit) {
const element = Alert.create(init);
- return element.showAsync();
+ return element.showAsync() as Promise<'auto' | 'primary' | 'secondary'>;
}
/**
- * Show the notification asynchronously, waits for it to close and returns the reason for close.
+ * Show the `omni-alert` asynchronously, waits for it to close and returns the reason for close.
*
* @returns The reason for the alert close.
*/
@@ -272,7 +284,7 @@ export class Alert extends OmniElement {
}
/**
- * Show the notification.
+ * Show the `omni-alert`.
*
* @returns The alert instance.
*/
@@ -285,7 +297,7 @@ export class Alert extends OmniElement {
}
/**
- * Hide the notification and remove the component from the DOM
+ * Hide the `omni-alert` and remove the component from the DOM
*/
hide(): void {
this.updateComplete.then(async () => {
@@ -348,7 +360,7 @@ export class Alert extends OmniElement {
omni-modal::part(container) {
- min-width: var(--omni-alert-min-width, 20%);
+ min-width: var(--omni-alert-min-width, 10%);
max-width: var(--omni-alert-max-width, 80%);
}
@@ -449,6 +461,16 @@ export class Alert extends OmniElement {
margin-right: var(--omni-alert-description-padding-right, 0px);
}
+ .description.left {
+ justify-content: left;
+ text-align: left;
+ }
+
+ .description.right {
+ justify-content: right;
+ text-align: right;
+ }
+
.actions {
display: flex;
flex-direction: row;
@@ -491,8 +513,9 @@ export class Alert extends OmniElement {
}
.clear-btn {
+ line-height: normal;
padding-top: var(--omni-alert-action-button-padding-top, 4px);
- padding-bottom: var(--omni-alert-action-button-padding-bottom, 4px);
+ padding-bottom: var(--omni-alert-action-button-padding-bottom);
padding-left: var(--omni-alert-action-button-padding-left);
padding-right: var(--omni-alert-action-button-padding-right);
@@ -594,7 +617,11 @@ export class Alert extends OmniElement {
${
this.description
- ? this.description.split('\n').map((paragraph) => html`