diff --git a/.changeset/cold-cars-relate.md b/.changeset/cold-cars-relate.md new file mode 100644 index 0000000000..99092249cd --- /dev/null +++ b/.changeset/cold-cars-relate.md @@ -0,0 +1,8 @@ +--- +"@patternfly/pfe-core": minor +--- + +`SlotController`: + +- Add `isEmpty` method to check if a slot is empty. If no slot name is provided it will check the default slot. (#2603) +- `hasSlotted` method now returns default slot if no slot name is provided. (#2603) diff --git a/core/pfe-core/controllers/slot-controller.ts b/core/pfe-core/controllers/slot-controller.ts index 7e472ba1e6..bfbc8c7070 100644 --- a/core/pfe-core/controllers/slot-controller.ts +++ b/core/pfe-core/controllers/slot-controller.ts @@ -41,15 +41,16 @@ function isObjectConfigSpread(config: ([SlotsConfig] | (string | null)[])): conf * for the default slot, look for direct children not assigned to a slot */ const isSlot = - (n: string | typeof SlotController.anonymous) => + (n: string | typeof SlotController.default) => (child: Element): child is T => - n === SlotController.anonymous ? !child.hasAttribute('slot') + n === SlotController.default ? !child.hasAttribute('slot') : child.getAttribute('slot') === n; export class SlotController implements ReactiveController { - public static anonymous = Symbol('anonymous slot'); + public static default = Symbol('default slot'); + public static anonymous = this.default; - #nodes = new Map(); + #nodes = new Map(); #logger: Logger; @@ -105,22 +106,6 @@ export class SlotController implements ReactiveController { this.#mo.disconnect(); } - /** - * Returns a boolean statement of whether or not any of those slots exists in the light DOM. - * - * @param {String|Array} name The slot name. - * @example this.hasSlotted("header"); - */ - hasSlotted(...names: string[]): boolean { - if (!names.length) { - this.#logger.warn(`Please provide at least one slot name for which to search.`); - return false; - } else { - return names.some(x => - this.#nodes.get(x)?.hasContent ?? false); - } - } - /** * Given a slot name or slot names, returns elements assigned to the requested slots as an array. * If no value is provided, it returns all children not assigned to a slot (without a slot attribute). @@ -142,13 +127,40 @@ export class SlotController implements ReactiveController { */ getSlotted(...slotNames: string[]): T[] { if (!slotNames.length) { - return (this.#nodes.get(SlotController.anonymous)?.elements ?? []) as T[]; + return (this.#nodes.get(SlotController.default)?.elements ?? []) as T[]; } else { return slotNames.flatMap(slotName => this.#nodes.get(slotName)?.elements ?? []) as T[]; } } + /** + * Returns a boolean statement of whether or not any of those slots exists in the light DOM. + * + * @param names The slot names to check. + * @example this.hasSlotted('header'); + */ + hasSlotted(...names: (string | null | undefined)[]): boolean { + const { anonymous } = SlotController; + const slotNames = Array.from(names, x => x == null ? anonymous : x); + if (!slotNames.length) { + slotNames.push(anonymous); + } + return slotNames.some(x => this.#nodes.get(x)?.hasContent ?? false); + } + + /** + * Whether or not all the requested slots are empty. + * + * @param slots The slot name. If no value is provided, it returns the default slot. + * @example this.isEmpty('header', 'footer'); + * @example this.isEmpty(); + * @returns {Boolean} + */ + isEmpty(...names: (string | null | undefined)[]): boolean { + return !this.hasSlotted(...names); + } + #onSlotChange = (event: Event & { target: HTMLSlotElement }) => { const slotName = event.target.name; this.#initSlot(slotName); @@ -168,13 +180,13 @@ export class SlotController implements ReactiveController { this.host.requestUpdate(); }; - #getChildrenForSlot(name: string | typeof SlotController.anonymous): T[] { + #getChildrenForSlot(name: string | typeof SlotController.default): T[] { const children = Array.from(this.host.children) as T[]; return children.filter(isSlot(name)); } #initSlot = (slotName: string | null) => { - const name = slotName || SlotController.anonymous; + const name = slotName || SlotController.default; const elements = this.#nodes.get(name)?.slot?.assignedElements?.() ?? this.#getChildrenForSlot(name); const selector = slotName ? `slot[name="${slotName}"]` : 'slot:not([name])'; const slot = this.host.shadowRoot?.querySelector?.(selector) ?? null; diff --git a/elements/package.json b/elements/package.json index 0cd47fe88f..197c32786e 100644 --- a/elements/package.json +++ b/elements/package.json @@ -16,6 +16,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/BaseAvatar.js": "./pf-avatar/BaseAvatar.js", "./pf-avatar/pf-avatar.js": "./pf-avatar/pf-avatar.js", "./pf-badge/BaseBadge.js": "./pf-badge/BaseBadge.js", diff --git a/elements/pf-alert/README.md b/elements/pf-alert/README.md new file mode 100644 index 0000000000..9ffdc371cc --- /dev/null +++ b/elements/pf-alert/README.md @@ -0,0 +1,28 @@ +# PatternFly Elements Alert + +`` is a web component that provides a standard alert interface for displaying important messages to users. + +## Installation + +Load `` via CDN: + +```html + +``` + +Or, if you are using [NPM](https://npm.im), install it + +```bash +npm install @patternfly/elements +``` + +Then once installed, import it to your application: + +```js +import '@patternfly/elements/pf-alert/pf-alert.js'; +``` + + +```html + +``` diff --git a/elements/pf-alert/demo/custom-icons.html b/elements/pf-alert/demo/custom-icons.html new file mode 100644 index 0000000000..430be339fd --- /dev/null +++ b/elements/pf-alert/demo/custom-icons.html @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/elements/pf-alert/demo/demo.css b/elements/pf-alert/demo/demo.css new file mode 100644 index 0000000000..cd1ecdebf5 --- /dev/null +++ b/elements/pf-alert/demo/demo.css @@ -0,0 +1,15 @@ +pf-alert { + padding: 0.5rem; + display: block; +} + +pf-button { + display: block; + padding-block: 1em; +} + +#timeout-alerts { + min-height: 200px; + border: 1px solid #000; + margin: 0.5rem; +} diff --git a/elements/pf-alert/demo/inline.html b/elements/pf-alert/demo/inline.html new file mode 100644 index 0000000000..95bbf8dc14 --- /dev/null +++ b/elements/pf-alert/demo/inline.html @@ -0,0 +1,21 @@ + + +

+ Inline +

+ + + + + + + +

+ Inline Plain +

+ + + + + + diff --git a/elements/pf-alert/demo/kitchen-sink.html b/elements/pf-alert/demo/kitchen-sink.html new file mode 100644 index 0000000000..9a84450de1 --- /dev/null +++ b/elements/pf-alert/demo/kitchen-sink.html @@ -0,0 +1,123 @@ + + + + + + + + +

+ Alert Variations +

+ + +

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. This is a link.

+

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

+
+ + + + + + + + +

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

+

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

+ + + + + + +
+ + + +

+ Default Icons +

+ + + + + + + + + + + + + + + + + +

+ Plain +

+ + + + +

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

+ +
+ + + +

+ Inline +

+ + + + + + + +

+ Plain +

+ + + + + + + +

+ Inline Plain +

+ + + + + + + +

+ Truncated Title +

+ + + +

+ Timeout +

+ +Create default timeout alert +Create inline timeout alert + + + +Create Default timeout alert + +
diff --git a/elements/pf-alert/demo/pf-alert.html b/elements/pf-alert/demo/pf-alert.html new file mode 100644 index 0000000000..5439c06e0a --- /dev/null +++ b/elements/pf-alert/demo/pf-alert.html @@ -0,0 +1,3 @@ + + + diff --git a/elements/pf-alert/demo/pf-alert.js b/elements/pf-alert/demo/pf-alert.js new file mode 100644 index 0000000000..1ecbaeee26 --- /dev/null +++ b/elements/pf-alert/demo/pf-alert.js @@ -0,0 +1,3 @@ +import '@patternfly/elements/pf-alert/pf-alert.js'; +import '@patternfly/elements/pf-button/pf-button.js'; +import '@patternfly/elements/pf-icon/pf-icon.js'; diff --git a/elements/pf-alert/demo/plain.html b/elements/pf-alert/demo/plain.html new file mode 100644 index 0000000000..9e17e45c13 --- /dev/null +++ b/elements/pf-alert/demo/plain.html @@ -0,0 +1,24 @@ + + +

+ Plain +

+ + + + +

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

+ +
+ + + +

+ Inline Plain +

+ + + + + + diff --git a/elements/pf-alert/demo/timeout.html b/elements/pf-alert/demo/timeout.html new file mode 100644 index 0000000000..cceb2c46b0 --- /dev/null +++ b/elements/pf-alert/demo/timeout.html @@ -0,0 +1,15 @@ + + +

+ Timeout +

+ +Create default timeout alert +Create inline timeout alert +Create custom timeout alert + + + +Create Default timeout alert + +
diff --git a/elements/pf-alert/demo/timeout.js b/elements/pf-alert/demo/timeout.js new file mode 100644 index 0000000000..ceaa3a1df2 --- /dev/null +++ b/elements/pf-alert/demo/timeout.js @@ -0,0 +1,35 @@ +import '@patternfly/elements/pf-alert/pf-alert.js'; +import '@patternfly/elements/pf-button/pf-button.js'; +import '@patternfly/elements/pf-icon/pf-icon.js'; + +const createTimeoutAlert = document.getElementById('create-timeout-alert'); +const createInlintTimeoutAlert = document.getElementById('create-timeout-inline-alert'); +const timeoutRange = document.getElementById('timeout-range'); +const timeoutValue = document.getElementById('timeout-value'); +const createCustomTimeoutAlert = document.getElementById('create-custom-timeout-alert'); +const timeoutAlertsSection = document.getElementById('timeout-alerts'); + +createTimeoutAlert.addEventListener('click', () => { + const pfeAlert = document.createElement('pf-alert'); + pfeAlert.header = 'Default Timeout Alert 8000ms'; + pfeAlert.timeout = true; + timeoutAlertsSection.appendChild(pfeAlert); +}); + +createInlintTimeoutAlert.addEventListener('click', () => { + const pfeAlert = document.createElement('pf-alert'); + pfeAlert.header = 'Inline Timeout Alert 8000ms'; + pfeAlert.timeout = true; + timeoutAlertsSection.appendChild(pfeAlert); +}); + +createCustomTimeoutAlert.addEventListener('click', () => { + const pfeAlert = document.createElement('pf-alert'); + pfeAlert.header = `Custom Timeout Alert ${timeoutRange.value}ms`; + pfeAlert.timeout = timeoutRange.value; + timeoutAlertsSection.appendChild(pfeAlert); +}); + +timeoutRange.addEventListener('change', () => { + timeoutValue.innerText = timeoutRange.value; +}); diff --git a/elements/pf-alert/demo/truncated.html b/elements/pf-alert/demo/truncated.html new file mode 100644 index 0000000000..bc621cb540 --- /dev/null +++ b/elements/pf-alert/demo/truncated.html @@ -0,0 +1,7 @@ + + +

+ Truncated Title +

+ + diff --git a/elements/pf-alert/demo/variants.html b/elements/pf-alert/demo/variants.html new file mode 100644 index 0000000000..840365d64d --- /dev/null +++ b/elements/pf-alert/demo/variants.html @@ -0,0 +1,41 @@ + + + + + + + + +

+ Alert Variations +

+ + +

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. This is a link.

+

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

+
+ + + + + + + + +

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

+

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

+ + + + + + +
+ + diff --git a/elements/pf-alert/docs/pf-alert.md b/elements/pf-alert/docs/pf-alert.md new file mode 100644 index 0000000000..61f5ec0add --- /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..6b3c997894 --- /dev/null +++ b/elements/pf-alert/pf-alert.css @@ -0,0 +1,214 @@ +:host { + --_alert-title-color: var(--_alert-title-color, var(--pf-global--default-color--300, #003737)); + --_alert-icon-color: var(--_alert-icon-color, var(--pf-global--default-color--200, #009596)); + --_alert-inline-background-color: var(--pf-global--palette--cyan-50, #f2f9f9); +} + +:host([hidden]) { + display: none; +} + +:host([variant="info"]) { + --_alert-border-top-color: var(--pf-global--info-color--100, #2b9af3); + --_alert-icon-color: var(--pf-global--info-color--100, #2b9af3); + --_alert-title-color: var(--pf-global--info-color--200, #002952); +} + +:host([variant="info"][inline]) { + --_alert-inline-background-color: var(--pf-global--palette--blue-50, #e7f1fa); +} + +:host([variant="success"]) { + --_alert-border-top-color: var(--pf-global--success-color--100, #3e8635); + --_alert-icon-color: var(--pf-global--success-color--100, #3e8635); + --_alert-title-color: var(--pf-global--success-color--200, #1e4f18); +} + +:host([variant="success"][inline]) { + --_alert-inline-background-color: var(--pf-global--palette--green-50, #f3faf2); +} + +:host([variant="warning"]) { + --_alert-border-top-color: var(--pf-global--warning-color--100, #f0ab00); + --_alert-icon-color: var(--pf-global--warning-color--100, #f0ab00); + --_alert-title-color: var(--pf-global--warning-color--200, #795600); +} + +:host([variant="warning"][inline]) { + --_alert-inline-background-color: var(--pf-global--palette--gold-50, #fdf7e7); +} + +:host([variant="danger"]) { + --_alert-border-top-color: var(--pf-global--danger-color--100, #c9190b); + --_alert-icon-color: var(--pf-global--danger-color--100, #c9190b); + --_alert-title-color: var(--pf-global--danger-color--200, #7d1007); +} + +:host([variant="danger"][inline]) { + --_alert-inline-background-color: var(--pf-global--palette--blue-50, #faeae8); +} + +:host([inline]) { + --_alert-background-color: var(--_alert-inline-background-color); + --_alert-box-shadow: 0; +} + +:host([plain]) { + --_alert-padding-top: 0; + --_alert-padding-left: 0; + --_alert-padding-right: 0; + --_alert-padding-bottom: 0; + --_alert-background-color: transparent; + --_alert-border-top-color: transparent; +} + +#container.truncateTitle { + display: block; +} + +.truncateTitle #middle-column { + display: block; +} + +.truncateTitle #header { + overflow-x: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +#container { + color: var(--pf-global--Color--100, #151515); + position: relative; + display: grid; + box-sizing: border-box; + grid-template-columns: min-content 1fr; + gap: var(--pf-global--spacer--xs, 0.25rem); + padding: var(--_alert-padding-top, 1rem) var(--_alert-padding-right, 1rem) var(--_alert-padding-bottom, 1rem) var(--_alert-padding-left, 1rem); + font-size: var(--_alert-font-size, 0.875rem) !important; + background-color: var(--_alert-background-color, #ffffff); + border-width: 0; + border-block: solid var(--_alert-border-top-color, #009596); + border-block-width: var(--_alert-border-top-width, 2px) 0; + box-shadow: var(--_alert-box-shadow, 0 0.5rem 1rem 0 rgba(3, 3, 3, 0.16), 0 0 0.375rem 0 rgba(3, 3, 3, 0.08)); + + font-family: + var(--pf-global--FontFamily--redhat-updated--heading--sans-serif, + "RedHatTextUpdated", + helvetica, + arial, + sans-serif); +} + +#left-column { + display: inline-block; + vertical-align: top; +} + +#middle-column { + display: flex; + flex-flow: column; + justify-content: center; +} + +header { + display: flex; + align-items: center; + justify-content: space-between; + font-weight: var(--pf-global--FontWeight--bold, 700); +} + +header ::slotted(*) { + font-family: var(--_font-family) !important; + font-size: var(--_alert-font-size, var(--pf-global--font-size--sm, 0.875rem)) !important; + font-weight: var(--pf-global--FontWeight--bold, 700) !important; + margin: var(--_alert-header-slotted-margin, 0) !important; + padding-block: var(--_alert-header-slotted-padding-block, 2px 4px) !important; +} + +#header-actions { + margin-right: var(--pf-global--spacer--xs, 0.25rem); +} + +#header { + color: var(--_alert-title-color); +} + +#icon { + display: flex; + align-items: center; + justify-content: center; + width: var(--pf-global--spacer--lg, 1.5rem); + height: var(--pf-global--spacer--lg, 1.5rem); + color: var(--_alert-icon-color, var(--pf-global--default-color--200, #009596)); +} + +#icon, +#icon > svg { + font-size: var(--_alert-icon-font-size, var(--pf-global--icon--font-size--md, 1.125rem)); + --pf-global--icon--font-size--sm: var(--_alert-icon-font-size, var(--pf-global--icon--font-size--md, 1.125rem)); +} + +#icon ::slotted(pf-icon), +#icon > pf-icon { + --pf-global--icon--FontSize--md: 18.5px; +} + +#close-button { + color: var(--pf-global--Color--dark-200, #6a6e73); + background-color: transparent; + border: none; + height: var(--pf-global--spacer--lg, 1.5rem); + width: var(--pf-global--spacer--lg, 1.5rem); + cursor: pointer; +} + +#close-button:hover { + color: var(--pf-global--Color--100, var(--pf-global--Color--dark-100, #151515)); +} + +#description { + font-size: var(--_alert-font-size, var(--pf-global--font-size--sm, 0.875rem)); +} + +#description > ::slotted(*) { + margin-block: var(--_alert-description-slotted-margin-block, 0) !important; + padding: var(--_alert-description-slotted-padding, 0) !important; +} + +#description.hasDescriptionContent { + padding-top: var(--pf-global--spacer--xs, 0.25rem) !important; +} + +footer.hasActions { + margin-top: var(--_alert-description-action-group-padding-top, 1rem); +} + +footer ::slotted([slot="actions"]) { + padding: var(--_alert-footer-slotted-actions-padding, 0) !important; + border: var(--_alert-footer-slotted-actions-border, none) !important; + background-color: var(--_alert-footer-slotted-actions-background-color, transparent) !important; + color: var(--pf-global--link--Color, var(--pf-global--link--Color--dark, #0066cc)) !important; + font-size: var(--_alert-font-size, var(--pf-global--font-size--sm, 0.875rem)) !important; + font-family: var(--_font-family) !important; +} + +footer ::slotted(*:not(:last-child)) { + margin-inline-end: var(--_alert-action-group-not-last-child-margin-right, var(--pf-global--spacer--lg, 1.5rem)) !important; +} + +footer ::slotted([slot="actions"]:focus) { + text-decoration: var(--_alert-footer-slotted-actions-text-decoration-focus, underline) !important; + color: var(--pf-global--link--Color--hover, var(--pf-global--link--Color--dark--hover, #004080)) !important; +} + +footer ::slotted([slot="actions"]:hover) { + cursor: var(--_alert-footer-slotted-actions-cursor-hover, pointer) !important; + text-decoration: var(--_alert-footer-slotted-actions-text-decoration-hover, underline) !important; + color: var(--pf-global--link--Color--hover, var(--pf-global--link--Color--dark--hover, #004080)) !important; +} + +:host(:not([variant])) #container { + border-left: 0; + border-bottom: none; + border-right: 0; +} diff --git a/elements/pf-alert/pf-alert.ts b/elements/pf-alert/pf-alert.ts new file mode 100644 index 0000000000..a01f16a292 --- /dev/null +++ b/elements/pf-alert/pf-alert.ts @@ -0,0 +1,247 @@ +import { SlotController } from '@patternfly/pfe-core/controllers/slot-controller.js'; + +import { LitElement, html, type ComplexAttributeConverter, type PropertyValues } 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 styles from './pf-alert.css'; +import { ComposedEvent } from '@patternfly/pfe-core'; + +import '@patternfly/elements/pf-icon/pf-icon.js'; + +const ICONS = { + default: { set: 'patternfly', icon: 'bell' }, + success: { set: 'fas', icon: 'circle-check' }, + warning: { set: 'fas', icon: 'exclamation-triangle' }, + danger: { set: 'fas', icon: 'exclamation-circle' }, + info: { set: 'fas', icon: 'info-circle' }, + close: { set: 'patternfly', icon: 'close' }, + get(name: 'default' | 'success' | 'warning' | 'danger' | 'info' | 'close') { + const { set, icon } = ICONS[name]; + return html` + `; + } +}; + +export class AlertCloseEvent extends ComposedEvent { + constructor() { + super('close', { + cancelable: true + }); + } +} + +const BooleanNumberConverter: ComplexAttributeConverter = { + toAttribute(value: boolean | number) { + if (!value) { + return null; + } else if (typeof value === 'boolean' && value) { + return 8000; + } else { + return Number(value); + } + } +}; + +/** + * An **alert** is a message that communicates a change in state or condition that might affect the user's experience on a page. + * + * @summary Communicates a change in state or condition that might affect the user's experience on a page. + * + * @fires {AlertCloseEvent} close - when the alert is closed + * + * @slot header - Place the alert header here + * @slot - Place the alert content here + * @slot actions - Place the alert actions here + * @slot icon - Place the alert icon here + * + * @cssproperty {} --alert-title-color + * Color of the alert title + * {@default `var(--pf-global--default-color--300, #003737)`} + * @cssproperty {} --alert-icon-color + * Color of the alert icon + * {@default `var(--pf-global--default-color--200, #009596)`} + * @cssproperty {} --alert-inline-background-color + * Background color of the alert when inline + * {@default `var(--pf-global--palette--cyan-50, #f2f9f9)`} + * @cssproperty {} --alert-border-top-color + * Color of the top border of the alert + * {@default `#009596`} + * @cssproperty {} --alert-background-color + * Background color of the alert + * {@default `#ffffff`} + * @cssproperty {} --alert-box-shadow + * Box shadow of the alert + * {@default `0 0.5rem 1rem 0 rgba(3, 3, 3, 0.16), 0 0 0.375rem 0 rgba(3, 3, 3, 0.08)`} + * @cssproperty {} --alert-padding-top + * Padding top of the alert + * {@default `1rem`} + * @cssproperty {} --alert-padding-left + * Padding left of the alert + * {@default `1rem`} + * @cssproperty {} --alert-padding-right + * Padding right of the alert + * {@default `1rem`} + * @cssproperty {} --alert-padding-bottom + * Padding bottom of the alert + * {@default `1rem`} + * @cssproperty {} --alert-font-size + * Font size of the alert + * {@default `0.875rem`} + * @cssproperty {} --alert-border-top-width + * Width of the top border of the alert + * {@default `2px`} + * @cssproperty {} --alert-header-slotted-margin + * Margin of the slotted header + * {@default `0`} + * @cssproperty {} --alert-header-slotted-padding-block + * Padding block of the slotted header + * {@default `2px 4px`} + * @cssproperty {} --alert-icon-font-size + * Font size of the alert icon + * {@default `var(--pf-global--icon--font-size--md, 1.125rem)`} + * @cssproperty {} --alert-description-slotted-margin-block + * Margin block of the slotted description + * {@default `0`} + * @cssproperty {} --alert-description-slotted-padding + * Padding of the slotted description + * {@default `0`} + * @cssproperty {} --alert-description-action-group-padding-top + * Padding top of the slotted description action group + * {@default `1rem`} + * @cssproperty {} --alert-footer-slotted-actions-padding + * Padding of the slotted footer actions + * {@default `0`} + * @cssproperty {} --alert-footer-slotted-actions-border + * Border of the slotted footer actions + * {@default `none`} + * @cssproperty {} --alert-footer-slotted-actions-background-color + * Background color of the slotted footer actions + * {@default `transparent`} + * @cssproperty {} --alert-action-group-not-last-child-margin-right + * Margin right of the slotted action group when it is not the last child + * {@default `var(--pf-global--spacer--lg, 1.5rem)`} + * @cssproperty {} --alert-footer-slotted-actions-text-decoration-focus + * Text decoration of the slotted footer actions when focused + * {@default `underline`} + * @cssproperty {} --alert-footer-slotted-actions-cursor-hover + * Cursor of the slotted footer actions when hovered + * {@default `pointer`} + * @cssproperty {} --alert-footer-slotted-actions-text-decoration-hover + * Text decoration of the slotted footer actions when hovered + * {@default `underline`} + */ +@customElement('pf-alert') +export class PfAlert extends LitElement { + static readonly styles = [styles]; + + /** Header or title of the alert. */ + @property({ reflect: true }) header = ''; + + /** + * The amount of time, in milliseconds, the alert will be visible before automatically closing. + * If set to `undefined`, the alert will not automatically close. + * If set to `true`, the alert will close after 8 seconds. + */ + @property({ reflect: true, converter: BooleanNumberConverter }) timeout: boolean | number = false; + + /** + * The variant of the alert. + * @type {('success' | 'danger' | 'warning' | 'info' | 'default')} + */ + @property({ reflect: true }) + variant: 'success' | 'danger' | 'warning' | 'info' | 'default' = 'default'; + + /** Whether the alert should be displayed inline. */ + @property({ reflect: true, type: Boolean }) inline = false; + + /** Whether the alert should be displayed as plain text. */ + @property({ reflect: true, type: Boolean }) plain = false; + + /** Whether the alert should be dismissable. */ + @property({ reflect: true, type: Boolean }) dismissable = false; + + /** Whether the alert should have a truncated title with a tooltip. */ + @property({ + type: Boolean, + reflect: true, + attribute: 'truncate-title', + }) truncateTitle = false; + + #slots = new SlotController(this, 'header', null, 'actions'); + + override willUpdate() { + if (this.truncateTitle) { + import('@patternfly/elements/pf-tooltip/pf-tooltip.js'); + } + } + + firstUpdated(_changedProperties: PropertyValues) { + if (_changedProperties.has('timeout')) { + if (this.timeout) { + const parsed = typeof this.timeout === 'boolean' ? 8000 : this.timeout; + setTimeout(() => { + this.remove(); + }, parsed); + } + } + } + + #closeHandler() { + const event = new AlertCloseEvent(); + if (this.dispatchEvent(event)) { + this.remove(); + } + } + + render() { + const { truncateTitle, header, dismissable, variant } = this; + const hasActions = this.#slots.hasSlotted('actions'); + const hasDescriptionContent = this.#slots.hasSlotted(); + + return html` + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'pf-alert': PfAlert; + } +} 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..179e90fd87 --- /dev/null +++ b/elements/pf-alert/test/pf-alert.e2e.ts @@ -0,0 +1,12 @@ +import { test } from '@playwright/test'; +import { PfeDemoPage } from '@patternfly/pfe-tools/test/playwright/PfeDemoPage.js'; + +const tagName = 'pf-alert'; + +test.describe(tagName, () => { + test('snapshot', async ({ page }) => { + const componentPage = new PfeDemoPage(page, tagName); + await componentPage.navigate(); + await componentPage.snapshot(); + }); +}); 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..23972ec599 --- /dev/null +++ b/elements/pf-alert/test/pf-alert.spec.ts @@ -0,0 +1,21 @@ +import { expect, html } from '@open-wc/testing'; +import { createFixture } from '@patternfly/pfe-tools/test/create-fixture.js'; +import { PfAlert } from '@patternfly/elements/pf-alert/pf-alert.js'; + +describe('', function() { + describe('simply instantiating', function() { + let element: PfAlert; + it('imperatively instantiates', function() { + expect(document.createElement('pf-alert')).to.be.an.instanceof(PfAlert); + }); + + it('should upgrade', async function() { + element = await createFixture(html``); + const klass = customElements.get('pf-alert'); + expect(element) + .to.be.an.instanceOf(klass) + .and + .to.be.an.instanceOf(PfAlert); + }); + }); +});