diff --git a/.changeset/pf-alert.md b/.changeset/pf-alert.md new file mode 100644 index 0000000000..c2e83f1ab2 --- /dev/null +++ b/.changeset/pf-alert.md @@ -0,0 +1,15 @@ +--- +"@patternfly/elements": minor +--- + +✨ Added `` component + +An **alert** is a notification that provides brief information to the user without blocking their workflow. + +```html + +

Custom alert title

+ This is the alert description. + Action 1 + Action 2 +
diff --git a/elements/package.json b/elements/package.json index 2decacb2a1..6ba8a885d2 100644 --- a/elements/package.json +++ b/elements/package.json @@ -15,6 +15,7 @@ "./pf-accordion/pf-accordion-header.js": "./pf-accordion/pf-accordion-header.js", "./pf-accordion/pf-accordion-panel.js": "./pf-accordion/pf-accordion-panel.js", "./pf-accordion/pf-accordion.js": "./pf-accordion/pf-accordion.js", + "./pf-alert/pf-alert.js": "./pf-alert/pf-alert.js", "./pf-avatar/pf-avatar.js": "./pf-avatar/pf-avatar.js", "./pf-back-to-top/pf-back-to-top.js": "./pf-back-to-top/pf-back-to-top.js", "./pf-background-image/pf-background-image.js": "./pf-background-image/pf-background-image.js", diff --git a/elements/pf-alert/README.md b/elements/pf-alert/README.md new file mode 100644 index 0000000000..19d47e3dfb --- /dev/null +++ b/elements/pf-alert/README.md @@ -0,0 +1,38 @@ +# pf-alert + +The `pf-alert` web component displays PatternFly-styled alerts. It can be used inline in pages or as a toast notification (if a static helper is provided separately). Alerts support several visual **variants** (for example: `info`, `success`, `warning`, `danger`), an optional title slot, body content, and an **action links** slot for interactive controls. Alerts can also be **closable** and **expandable**. + +## Installation + +Import the element in your page or application as an ES module: + +```html + +``` + +## Basic usage + +Inline alert example: + +```html + + Operation Success + + The operation completed successfully. + + + + System Update + + A new system update is available. + +
+ Update Now + Later +
+
+``` + + diff --git a/elements/pf-alert/demo/custom-icons.html b/elements/pf-alert/demo/custom-icons.html new file mode 100644 index 0000000000..856ef7bc6c --- /dev/null +++ b/elements/pf-alert/demo/custom-icons.html @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/elements/pf-alert/demo/expandable.html b/elements/pf-alert/demo/expandable.html new file mode 100644 index 0000000000..f53b7ce423 --- /dev/null +++ b/elements/pf-alert/demo/expandable.html @@ -0,0 +1,27 @@ + +

Success alert description. This should tell the user more information about the alert.

+
+ + +

Success alert description. This should tell the user more information about the alert.

+ View details + Ignore +
+ + + + diff --git a/elements/pf-alert/demo/index.html b/elements/pf-alert/demo/index.html new file mode 100644 index 0000000000..3c3ae5e2eb --- /dev/null +++ b/elements/pf-alert/demo/index.html @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/elements/pf-alert/demo/inline.html b/elements/pf-alert/demo/inline.html new file mode 100644 index 0000000000..9798050f4b --- /dev/null +++ b/elements/pf-alert/demo/inline.html @@ -0,0 +1,24 @@ + + + + + + + + + diff --git a/elements/pf-alert/demo/plain.html b/elements/pf-alert/demo/plain.html new file mode 100644 index 0000000000..6510c7c6be --- /dev/null +++ b/elements/pf-alert/demo/plain.html @@ -0,0 +1,29 @@ + + + + + + + + + diff --git a/elements/pf-alert/demo/timeout.html b/elements/pf-alert/demo/timeout.html new file mode 100644 index 0000000000..821323f345 --- /dev/null +++ b/elements/pf-alert/demo/timeout.html @@ -0,0 +1,66 @@ +Add alert +Remove all alerts + +
+
+ + + + diff --git a/elements/pf-alert/demo/title-slot.html b/elements/pf-alert/demo/title-slot.html new file mode 100644 index 0000000000..d1464f1c53 --- /dev/null +++ b/elements/pf-alert/demo/title-slot.html @@ -0,0 +1,29 @@ + +

Custom alert title

+
+ + +

Info alert title

+
+ + +

Success alert title

+
+ + +

Warning alert title

+
+ + +

Danger alert title

+
+ + + + diff --git a/elements/pf-alert/demo/variations.html b/elements/pf-alert/demo/variations.html new file mode 100644 index 0000000000..1baf997dc6 --- /dev/null +++ b/elements/pf-alert/demo/variations.html @@ -0,0 +1,37 @@ + + Success alert description. This should tell the user more information about the alert. + View details + Ignore + + + + + Success alert description. + This should tell the user more information about the alert. + This is a link. + + + + Short alert description. + + + + Short alert description. + + + + + diff --git a/elements/pf-alert/docs/pf-alert.md b/elements/pf-alert/docs/pf-alert.md new file mode 100644 index 0000000000..db0de61f04 --- /dev/null +++ b/elements/pf-alert/docs/pf-alert.md @@ -0,0 +1,17 @@ +{% renderOverview %} + +{% endrenderOverview %} + +{% band header="Usage" %}{% endband %} + +{% renderSlots %}{% endrenderSlots %} + +{% renderAttributes %}{% endrenderAttributes %} + +{% renderMethods %}{% endrenderMethods %} + +{% renderEvents %}{% endrenderEvents %} + +{% renderCssCustomProperties %}{% endrenderCssCustomProperties %} + +{% renderCssParts %}{% endrenderCssParts %} diff --git a/elements/pf-alert/pf-alert.css b/elements/pf-alert/pf-alert.css new file mode 100644 index 0000000000..e6cb4bbfb5 --- /dev/null +++ b/elements/pf-alert/pf-alert.css @@ -0,0 +1,193 @@ +[hidden] { + display: none !important; +} + +:host { + --pf-c-alert--BoxShadow: var(--pf-global--BoxShadow--lg, 0 0.5rem 1rem 0 rgba(3, 3, 3, 0.16), 0 0 0.375rem 0 rgba(3, 3, 3, 0.08)); + --pf-c-alert--BackgroundColor: var(--pf-global--BackgroundColor--100, #fff); + --pf-c-alert--GridTemplateColumns: max-content 1fr max-content; + --pf-c-alert--GridTemplateAreas: "icon title close" ". description description" ". action action"; + --pf-c-alert--BorderTopWidth: var(--pf-global--BorderWidth--md, 2px); + --pf-c-alert--BorderTopColor: var(--pf-global--default-color--200, #009596); + --pf-c-alert--PaddingTop: var(--pf-global--spacer--md, 1rem); + --pf-c-alert--PaddingRight: var(--pf-global--spacer--md, 1rem); + --pf-c-alert--PaddingBottom: var(--pf-global--spacer--md, 1rem); + --pf-c-alert--PaddingLeft: var(--pf-global--spacer--md, 1rem); + --pf-c-alert__FontSize: var(--pf-global--FontSize--sm, 0.875rem); + --pf-c-alert__toggle--MarginTop: calc(-1 * var(--pf-global--spacer--form-element, 0.375rem) - 0.0625rem); + --pf-c-alert__toggle--MarginBottom: calc(-1 * var(--pf-global--spacer--form-element, 0.375rem)); + --pf-c-alert__toggle--MarginLeft: calc(-1 * var(--pf-global--spacer--md, 1rem)); + --pf-c-alert__toggle-icon--Rotate: 0; + --pf-c-alert__toggle-icon--Transition: var(--pf-global--Transition, all 250ms cubic-bezier(0.42, 0, 0.58, 1)); + --pf-c-alert__icon--Color: var(--pf-global--default-color--200, #009596); + --pf-c-alert__icon--MarginTop: 0.0625rem; + --pf-c-alert__icon--MarginRight: var(--pf-global--spacer--sm, 0.5rem); + --pf-c-alert__icon--FontSize: var(--pf-global--icon--FontSize--md, 1.125rem); + --pf-c-alert__title--FontWeight: var(--pf-global--FontWeight--bold, 700); + --pf-c-alert__title--Color: var(--pf-global--default-color--300, #003737); + --pf-c-alert__title--max-lines: 1; + --pf-c-alert__action--MarginTop: calc(var(--pf-global--spacer--form-element, 0.375rem) * -1); + --pf-c-alert__action--MarginBottom: calc(var(--pf-global--spacer--form-element, 0.375rem) * -1); + --pf-c-alert__action--TranslateY: 0.125rem; + --pf-c-alert__action--MarginRight: calc(var(--pf-global--spacer--sm, 0.5rem) * -1); + --pf-c-alert__description--PaddingTop: var(--pf-global--spacer--xs, 0.25rem); + --pf-c-alert__action-group--PaddingTop-base: var(--pf-global--spacer--xs, 0.25rem); + --pf-c-alert__action-group--PaddingTop: var(--pf-c-alert__action-group--PaddingTop-base); + --pf-c-alert__description--action-group--PaddingTop-base: var(--pf-global--spacer--md, 1rem); + --pf-c-alert__description--action-group--PaddingTop: var(--pf-c-alert__description--action-group--PaddingTop-base); + --pf-c-alert__action-group__c-button--not-last-child--MarginRight: var(--pf-global--spacer--lg, 1.5rem); + --pf-c-alert--m-success--BorderTopColor: var(--pf-global--success-color--100, #3e8635); + --pf-c-alert--m-success__icon--Color: var(--pf-global--success-color--100, #3e8635); + --pf-c-alert--m-success__title--Color: var(--pf-global--success-color--200, #1e4f18); + --pf-c-alert--m-danger--BorderTopColor: var(--pf-global--danger-color--100, #c9190b); + --pf-c-alert--m-danger__icon--Color: var(--pf-global--danger-color--100, #c9190b); + --pf-c-alert--m-danger__title--Color: var(--pf-global--danger-color--200, #a30000); + --pf-c-alert--m-warning--BorderTopColor: var(--pf-global--warning-color--100, #f0ab00); + --pf-c-alert--m-warning__icon--Color: var(--pf-global--warning-color--100, #f0ab00); + --pf-c-alert--m-warning__title--Color: var(--pf-global--warning-color--200, #795600); + --pf-c-alert--m-info--BorderTopColor: var(--pf-global--info-color--100, #2b9af3); + --pf-c-alert--m-info__icon--Color: var(--pf-global--info-color--100, #2b9af3); + --pf-c-alert--m-info__title--Color: var(--pf-global--info-color--200, #002952); + --pf-c-alert--m-inline--BoxShadow: none; + --pf-c-alert--m-inline--BackgroundColor: var(--pf-global--palette--cyan-50, #f2f9f9); + --pf-c-alert--m-inline--m-success--BackgroundColor: var(--pf-global--palette--green-50, #f3faf2); + --pf-c-alert--m-inline--m-danger--BackgroundColor: var(--pf-global--palette--red-50, #faeae8); + --pf-c-alert--m-inline--m-warning--BackgroundColor: var(--pf-global--palette--gold-50, #fdf7e7); + --pf-c-alert--m-inline--m-info--BackgroundColor: var(--pf-global--palette--blue-50, #e7f1fa); + --pf-c-alert--m-inline--m-plain--BorderTopWidth: 0; + --pf-c-alert--m-inline--m-plain--BackgroundColor: transparent; + --pf-c-alert--m-inline--m-plain--PaddingTop: 0; + --pf-c-alert--m-inline--m-plain--PaddingRight: 0; + --pf-c-alert--m-inline--m-plain--PaddingBottom: 0; + --pf-c-alert--m-inline--m-plain--PaddingLeft: 0; + --pf-c-alert--m-expandable--GridTemplateColumns: auto max-content 1fr max-content; + --pf-c-alert--m-expandable--GridTemplateAreas: "toggle icon title close" ". . description description" ". . action action"; + --pf-c-alert--m-expandable__description--action-group--PaddingTop: var(--pf-c-alert__action-group--PaddingTop-base); + --pf-c-alert--m-expanded__toggle-icon--Rotate: 90deg; + --pf-c-alert--m-expanded__description--action-group--PaddingTop: var(--pf-c-alert__description--action-group--PaddingTop-base); + color: var(--pf-global--Color--100, #151515); + position: relative; + display: grid; + padding: var(--pf-c-alert--PaddingTop) var(--pf-c-alert--PaddingRight) var(--pf-c-alert--PaddingBottom) var(--pf-c-alert--PaddingLeft); + font-size: var(--pf-c-alert__FontSize); + background-color: var(--pf-c-alert--BackgroundColor); + border-top: var(--pf-c-alert--BorderTopWidth) solid var(--pf-c-alert--BorderTopColor); + box-shadow: var(--pf-c-alert--BoxShadow); + grid-template-columns: var(--pf-c-alert--GridTemplateColumns); + grid-template-areas: var(--pf-c-alert--GridTemplateAreas); +} + +:host([variant="success"]) { + --pf-c-alert--BorderTopColor: var(--pf-c-alert--m-success--BorderTopColor); + --pf-c-alert__icon--Color: var(--pf-c-alert--m-success__icon--Color); + --pf-c-alert__title--Color: var(--pf-c-alert--m-success__title--Color); + --pf-c-alert--m-inline--BackgroundColor: var(--pf-c-alert--m-inline--m-success--BackgroundColor); +} + +:host([variant="danger"]) { + --pf-c-alert--BorderTopColor: var(--pf-c-alert--m-danger--BorderTopColor); + --pf-c-alert__icon--Color: var(--pf-c-alert--m-danger__icon--Color); + --pf-c-alert__title--Color: var(--pf-c-alert--m-danger__title--Color); + --pf-c-alert--m-inline--BackgroundColor: var(--pf-c-alert--m-inline--m-danger--BackgroundColor); +} + +:host([variant="warning"]) { + --pf-c-alert--BorderTopColor: var(--pf-c-alert--m-warning--BorderTopColor); + --pf-c-alert__icon--Color: var(--pf-c-alert--m-warning__icon--Color); + --pf-c-alert__title--Color: var(--pf-c-alert--m-warning__title--Color); + --pf-c-alert--m-inline--BackgroundColor: var(--pf-c-alert--m-inline--m-warning--BackgroundColor); +} + +:host([variant="info"]) { + --pf-c-alert--BorderTopColor: var(--pf-c-alert--m-info--BorderTopColor); + --pf-c-alert__icon--Color: var(--pf-c-alert--m-info__icon--Color); + --pf-c-alert__title--Color: var(--pf-c-alert--m-info__title--Color); + --pf-c-alert--m-inline--BackgroundColor: var(--pf-c-alert--m-inline--m-info--BackgroundColor); +} + +:host([inline]) { + --pf-c-alert--BoxShadow: var(--pf-c-alert--m-inline--BoxShadow); + --pf-c-alert--BackgroundColor: var(--pf-c-alert--m-inline--BackgroundColor); +} + +:host([plain]) { + --pf-c-alert--BorderTopWidth: var(--pf-c-alert--m-inline--m-plain--BorderTopWidth); + --pf-c-alert--BackgroundColor: var(--pf-c-alert--m-inline--m-plain--BackgroundColor); + --pf-c-alert--PaddingTop: var(--pf-c-alert--m-inline--m-plain--PaddingTop); + --pf-c-alert--PaddingRight: var(--pf-c-alert--m-inline--m-plain--PaddingRight); + --pf-c-alert--PaddingBottom: var(--pf-c-alert--m-inline--m-plain--PaddingBottom); + --pf-c-alert--PaddingLeft: var(--pf-c-alert--m-inline--m-plain--PaddingLeft); +} + +:host([expandable]) { + --pf-c-alert--GridTemplateColumns: var(--pf-c-alert--m-expandable--GridTemplateColumns); + --pf-c-alert--GridTemplateAreas: var(--pf-c-alert--m-expandable--GridTemplateAreas); + --pf-c-alert__description--action-group--PaddingTop: var(--pf-c-alert--m-expandable__description--action-group--PaddingTop); +} + +:host([expanded]) { + --pf-c-alert__toggle-icon--Rotate: var(--pf-c-alert--m-expanded__toggle-icon--Rotate); + --pf-c-alert__description--action-group--PaddingTop: var(--pf-c-alert--m-expanded__description--action-group--PaddingTop); +} + +#toggle { + margin-top: var(--pf-c-alert__toggle--MarginTop); + margin-bottom: var(--pf-c-alert__toggle--MarginBottom); + margin-left: var(--pf-c-alert__toggle--MarginLeft); +} + +#icon { + grid-area: icon; + display: flex; + margin-top: var(--pf-c-alert__icon--MarginTop); + margin-right: var(--pf-c-alert__icon--MarginRight); + font-size: var(--pf-c-alert__icon--FontSize); + --pf-icon--size: var(--pf-c-alert__icon--FontSize); + color: var(--pf-c-alert__icon--Color); + pf-icon, + ::slotted(pf-icon) { + translate: 0 0.125em; + } +} + +#title { + grid-area: title; + font-weight: var(--pf-c-alert__title--FontWeight); + color: var(--pf-c-alert__title--Color); + word-break: break-word; + ::slotted(*) { + color: inherit; + font-weight: inherit; + } + :is(h1,h2,h3,h4,h5,h6), + ::slotted(:is(h1,h2,h3,h4,h5,h6)) { + margin-block: 0 !important; + } +} + +#close { + grid-area: close; +} + +#description { + grid-area: description; + padding-top: var(--pf-c-alert__description--PaddingTop); + word-break: break-word; +} + +#actions { + grid-area: action; + --pf-c-alert__action-group--PaddingTop: var(--pf-c-alert__description--action-group--PaddingTop); + + & ::slotted(a) { + text-decoration: none; + color: #06c; + margin-inline-end: 1rem; + } + + & ::slotted(a:hover), + & ::slotted(a:focus), + & ::slotted(a:active) { + color: #004080; + } +} diff --git a/elements/pf-alert/pf-alert.ts b/elements/pf-alert/pf-alert.ts new file mode 100644 index 0000000000..cef2c34d99 --- /dev/null +++ b/elements/pf-alert/pf-alert.ts @@ -0,0 +1,223 @@ +import { LitElement, html, type TemplateResult } from 'lit'; +import { customElement } from 'lit/decorators/custom-element.js'; +import { property } from 'lit/decorators/property.js'; +import { classMap } from 'lit/directives/class-map.js'; +import { ifDefined } from 'lit/directives/if-defined.js'; + +import { observes } from '@patternfly/pfe-core/decorators.js'; + +import '@patternfly/elements/pf-icon/pf-icon.js'; +import '@patternfly/elements/pf-button/pf-button.js'; + +import styles from './pf-alert.css'; + +const VariantIconMap = new Map(Object.entries({ + info: 'info-circle', + success: 'check-circle', + warning: 'exclamation-triangle', + danger: 'exclamation-circle', + neutral: 'bell', +})); + +export class PfAlertCloseEvent extends Event { + constructor(public reason: 'closed' | 'timeout' = 'closed') { + super('close', { bubbles: true, cancelable: true }); + } +} + +/** + * An **alert** is a notification that provides brief information to the user + * without blocking their workflow. + * + * @fires close - When an alert is closed e.g. when close button is clicked or when the alert times + * out. Cancel the event to prevent the alert from being removed. + */ +@customElement('pf-alert') +export class PfAlert extends LitElement { + static readonly styles: CSSStyleSheet[] = [styles]; + + /** + * Use the `timeout` property to automatically dismiss an alert after a period + * of time. If set to `true`, the timeout will be 8000 milliseconds. Provide a + * specific value to dismiss the alert after a different number of + * milliseconds. + */ + @property({ type: Number }) timeout: number | true = 0; + + /** + * PatternFly supports several alert variants for different scenarios. + * Each variant has an associated status icon, background, and alert title + * coded to communicate the severity of an alert. Use the variant property to + * apply the following styling options. If no variant is specified, then the + * variant will be set to "default". + * + * - **Default** - Use for generic messages with no associated severity + * - **Info** - Use for general informational messages + * - **Success** - Use to indicate that a task or process has been completed successfully + * - **Warning** - Use to indicate that a non-critical error has occurred + * - **Danger** - Use to indicate that a critical or blocking error has occurred + */ + @property({ reflect: true }) + variant: + | 'warning' + | 'custom' + | 'neutral' + | 'info' + | 'success' + | 'danger' = 'neutral'; + + /** + * Use the `icon` attribute to replace a default alert icon with a custom icon. + * The `icon` attribute is overridden by the `icon` slot. + */ + @property() icon?: string; + + /** + * The title of the alert. Overridden by the title slot. + */ + @property({ attribute: 'title-text', reflect: true }) titleText?: string; + + /** + * The heading level of the alert's title. Overridden by the title slot. + * Default: 3 + */ + @property({ attribute: 'title-level', reflect: true }) titleLevel?: 1 | 2 | 3 | 4 | 5 | 6; + + /** + * Use inline alerts to display an alert inline with content. All alert + * variants may use the `inline` attribute to position alerts in content-heavy + * areas, such as within forms, wizards, or drawers. + */ + @property({ type: Boolean, reflect: true }) inline = false; + + /** + * Use the `plain` attribute to make any inline alert plain. Plain styling + * removes the colored background but keeps colored text and icons. + */ + @property({ type: Boolean, reflect: true }) plain = false; + + /** + * An alert can contain additional, hidden information that is made visible + * when users click a caret icon. This information can be expanded and + * collapsed each time the icon is clicked. + * + * It is not recommended to use an expandable alert with a timeout in a toast + * alert group because the alert could timeout before users have time to + * interact with and view the entire alert. + * + * See the toast alert considerations section of the alert accessibility + * documentation to understand the accessibility risks associated with using + * toast alerts. + */ + @property({ reflect: true, type: Boolean }) expandable = false; + + /** + * True when an expandable alert is expanded + */ + @property({ reflect: true, type: Boolean }) expanded = false; + + /** + * When true, the alert displays a close button + * Clicking the close button removes the alert + */ + @property({ reflect: true, type: Boolean }) dismissable = false; + + #timeoutId?: number; + + override disconnectedCallback(): void { + super.disconnectedCallback(); + clearTimeout(this.#timeoutId); + } + + override render(): TemplateResult<1> { + const { expandable, expanded, variant } = this; + const icon = this.icon ?? VariantIconMap.get(variant); + return html` + + + +
+ + + +
+ +
+ ${this.#renderDefaultTitle()} +
+ +
+ +
+ +
+ +
+ + + + `; + } + + @observes('timeout') + protected timeoutChanged(): void { + clearTimeout(this.#timeoutId); + let { timeout } = this; + if (timeout === true) { + timeout = 8000; + } + if (timeout > 0) { + this.#timeoutId = setTimeout(() => { + if (this.isConnected && this.dispatchEvent(new PfAlertCloseEvent('timeout'))) { + this.remove(); + } + }, timeout) as unknown as number; + } + } + + #renderDefaultTitle() { + if (!this.titleText) { + return ''; + } + switch (this.titleLevel ?? 3) { + case 1: return html`

${this.titleText}

`; + case 2: return html`

${this.titleText}

`; + case 4: return html`

${this.titleText}

`; + case 5: return html`
${this.titleText}
`; + case 6: return html`
${this.titleText}
`; + case 3: + default: return html`

${this.titleText}

`; + } + } + + #onCloseClick() { + if (this.isConnected && this.dispatchEvent(new PfAlertCloseEvent())) { + clearTimeout(this.#timeoutId); + this.remove(); + } + } + + #onToggleClick() { + this.expanded = !this.expanded; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'pf-alert': PfAlert; + } +} diff --git a/elements/pf-alert/test/MANUAL_TESTS.md b/elements/pf-alert/test/MANUAL_TESTS.md new file mode 100644 index 0000000000..fb53539a30 --- /dev/null +++ b/elements/pf-alert/test/MANUAL_TESTS.md @@ -0,0 +1,80 @@ +# pf-alert — Manual Tests + +This document outlines manual testing procedures for the `pf-alert` component, based on its functionality, attributes, and slots. + +## Setup +1. Start the development server (if applicable): +```bash +npm run start +``` +2. Open the demo page where the pf-alert component is displayed in your browser. +3. Have a screen reader ready (e.g., NVDA, VoiceOver) for accessibility tests. + +## Visual Tests + +### Base Alert Structure +- [ ] Renders with correct default styling +- [ ] Alert renders with the default Neutral variant and the Bell icon (default fallback). +- [ ] Title content (slotted) is clearly visible in the #title-area. +- [ ] Description content (default slot) is visible in the #description area. +- [ ] The pf-icon element is present in the #icon-container. + +### Variants and Icon Mapping +Test the component with various variant attributes and verify the appearance and correct icon display (as defined in variantToIcon): +- [ ] variant="info" (Blue, info-circle icon). +- [ ] variant="success" (Green, check-circle icon). +- [ ] variant="warning" (Yellow/Orange, exclamation-triangle icon). +- [ ] variant="danger" (Red, exclamation-circle icon). +- [ ] variant="neutral" (Gray, bell icon). +- [ ] variant="custom" (Uses default styling, no icon unless specified). + +### Custom Overrides +- [ ] Set variant="danger" and specify icon="star". The alert should be red but display the star icon (custom icon overrides variant default). + +## Interaction & Functional Tests + +### Closable Alert (onClose and #close-button) +- [ ] Render the alert with onClose. Verify the close button is visible (not hidden). +- [ ] Click the close button. Verify the alert component is removed from the DOM. +- [ ] Focus the close button via keyboard (Tab) and press Enter or Space. Verify the alert component is removed from the DOM. + +### Timed Dismissal (timeout) +- [ ] Render the alert with timeout="1000" (1 second). Wait 1.5 seconds. + [ ] Verify the alert component is removed from the DOM. + + [ ] Verify that the custom event pf-alert:timeout was dispatched before removal. + +- [ ] Render the alert with onClose and timeout="5000". Click the close button immediately. + [ ] Verify the alert is removed immediately, and the timeout function is canceled (it does not attempt to remove itself again after 5 seconds). + + +### Expandable Alert (isExpandable and #toggle-button) +- [ ] Render the alert with slot name="isExpandable" (collapsed state). + [ ] Verify the toggle button is visible. + [ ] Verify the expandable content slot is hidden (#expandable-description has ?hidden). + [ ] Verify the toggle icon is angle-right. +- [ ] Click the toggle button. + [ ] Verify the expandable content slot is now visible. + [ ] Verify the toggle icon changes to angle-down. +- [ ] Click the toggle button again. + [ ] Verify the expandable content slot is hidden again. + [ ] Verify the toggle icon changes back to angle-right. + +## Accessibility Tests + +### ARIA Roles and Attributes +- [ ] Inspect the main internal #container element. Verify it has the attribute role="alert". +- [ ] Inspect the toggle button (#toggle-button) in the collapsed state. + [ ] Verify aria-expanded="false". + [ ] Verify aria-label="Expand Alert". +- [ ] Inspect the toggle button in the expanded state. + [ ] Verify aria-expanded="true". + [ ] Verify aria-label="Collapse Alert". + + +### Keyboard Navigation +- [ ] Without isExpandable: Tabbing should move through action links (if present) and the close button in sequence. +- [ ] With isExpandable: Tabbing should move to the toggle button, then action links (if present), and then the close button in sequence. + +### General +- [ ] Verify the component's root element correctly reflects the ouia-id attribute when set (e.g., ). diff --git a/elements/pf-alert/test/pf-alert.e2e.ts b/elements/pf-alert/test/pf-alert.e2e.ts new file mode 100644 index 0000000000..d89e81d7b4 --- /dev/null +++ b/elements/pf-alert/test/pf-alert.e2e.ts @@ -0,0 +1,50 @@ +import { test, expect } from '@playwright/test'; + +test.describe('pf-alert E2E Tests', () => { + test.beforeEach(async ({ page }) => { + await page.setContent(` + + Alert Title + Alert description + + `); + }); + + test('should render title and description', async ({ page }) => { + const title = page.locator('pf-alert >>> #title-area slot[name="title"]'); + const desc = page.locator('pf-alert >>> #description slot'); + await expect(title).toHaveText('Alert Title'); + await expect(desc).toHaveText('Alert description'); + }); + + test('close button removes alert', async ({ page }) => { + await page.locator('pf-alert >>> #close-button').click(); + const alert = page.locator('pf-alert'); + await expect(alert).toHaveCount(0); + }); + + test('toggle button expands/collapses content', async ({ page }) => { + await page.setContent(` + + Extra content + + `); + const toggle = page.locator('pf-alert >>> #toggle-button'); + await toggle.click(); + const expandedContent = page.locator('pf-alert >>> #expandable-description'); + await expect(expandedContent).toBeVisible(); + }); + + test('keyboard navigation works', async ({ page }) => { + const btn = page.locator('pf-alert >>> #close-button'); + await btn.focus(); + await page.keyboard.press('Enter'); + const alert = page.locator('pf-alert'); + await expect(alert).toHaveCount(0); + }); + + test('accessibility checks', async ({ page }) => { + const container = page.locator('pf-alert >>> #container'); + await expect(container).toHaveAttribute('role', 'alert'); // אם הוספת role + }); +}); diff --git a/elements/pf-alert/test/pf-alert.spec.ts b/elements/pf-alert/test/pf-alert.spec.ts new file mode 100644 index 0000000000..2c5a005ca6 --- /dev/null +++ b/elements/pf-alert/test/pf-alert.spec.ts @@ -0,0 +1,46 @@ +import { html, fixture, expect } from '@open-wc/testing'; +import '../pf-alert.js'; + +describe('pf-alert Unit Tests', () => { + it('should create the component', async () => { + const el = await fixture(html``) as any; + expect(el).to.exist; + expect(el.variant).to.equal('neutral'); + }); + + it('should render a title slot', async () => { + const el = await fixture(html` + My Title + `); + const titleSlot = el.shadowRoot!.querySelector('#title slot[name="title"]'); + expect(titleSlot).to.exist; + }); + + it('close button should appear when dismissable=true', async () => { + const el = await fixture(html``); + const btn = el.shadowRoot!.querySelector('#close')!; + expect(btn.hasAttribute('hidden')).to.be.false; + }); + + it('should remove itself after timeout', async () => { + const el = await fixture(html``); + const removed = new Promise(resolve => el.addEventListener('close', () => resolve())); + await removed; + expect(document.body.contains(el)).to.be.false; + }); + + it('should toggle expanded when toggle button clicked', async () => { + const el = await fixture(html` + Content + `) as any; + await el.updateComplete; + const toggle = el.shadowRoot!.querySelector('#toggle') as HTMLElement; + const initial = el.expanded; + toggle.click(); + await el.updateComplete; + expect(el.expanded).to.equal(!initial); + toggle.click(); + await el.updateComplete; + expect(el.expanded).to.equal(initial); + }); +});