diff --git a/core/pfe-core/package.json b/core/pfe-core/package.json index 578aee7e8c..aad068f41f 100644 --- a/core/pfe-core/package.json +++ b/core/pfe-core/package.json @@ -66,6 +66,7 @@ }, "dependencies": { "@lit/context": "^1.1.5", + "@pwrs/cem": "^0.6.5", "lit": "^3.3.0" }, "repository": { diff --git a/elements/package.json b/elements/package.json index 94dadf7fc8..a3e01fbf70 100644 --- a/elements/package.json +++ b/elements/package.json @@ -31,6 +31,8 @@ "./pf-dropdown/pf-dropdown-item.js": "./pf-dropdown/pf-dropdown-item.js", "./pf-dropdown/pf-dropdown-menu.js": "./pf-dropdown/pf-dropdown-menu.js", "./pf-dropdown/pf-dropdown.js": "./pf-dropdown/pf-dropdown.js", + "./pf-helper-text/pf-helper-text.js": "./pf-helper-text/pf-helper-text.js", + "./pf-helper-text/pf-helper-text-item.js": "./pf-helper-text/pf-helper-text-item.js", "./pf-icon/pf-icon.js": "./pf-icon/pf-icon.js", "./pf-jazz-hands/pf-jazz-hands.js": "./pf-jazz-hands/pf-jazz-hands.js", "./pf-jump-links/pf-jump-links-item.js": "./pf-jump-links/pf-jump-links-item.js", @@ -47,6 +49,7 @@ "./pf-progress-stepper/pf-progress-stepper.js": "./pf-progress-stepper/pf-progress-stepper.js", "./pf-progress/pf-progress.js": "./pf-progress/pf-progress.js", "./pf-search-input/pf-search-input.js": "./pf-search-input/pf-search-input.js", + "./pf-sidebar/pf-sidebar.js": "./pf-sidebar/pf-sidebar.js", "./pf-spinner/pf-spinner.js": "./pf-spinner/pf-spinner.js", "./pf-switch/pf-switch.js": "./pf-switch/pf-switch.js", "./pf-table/context.js": "./pf-table/context.js", diff --git a/elements/pf-helper-text/README.md b/elements/pf-helper-text/README.md new file mode 100644 index 0000000000..63a3e4bc96 --- /dev/null +++ b/elements/pf-helper-text/README.md @@ -0,0 +1,151 @@ +# `` + +The **pf-helper-text** component provides contextual feedback messages for form fields or other UI elements. +It visually communicates states such as success, warning, error, or informational messages. +The component can display a default status icon or a fully custom icon provided through a slot or property. + +Read more about dropdown in the [PatternFly Elements Dropdown +documentation](https://patternflyelements.org/components/dropdown) + +--- +## 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-helper-text/pf-helper-text.js'; +import '@patternfly/elements/pf-icon/pf-icon.js'; + +``` + +## Usage + +```html + + + + This is default helper text + + + + This is indeterminate helper text + + + + This is warning helper text + + + + This is success helper text + + + + This is error helper text + + + + + +``` +## Slots + +| Name | Description | +|------------|----------------------------------------------------------------------------------------| +| `icon` | Optional named slot for providing a custom icon.
Overrides the default status icon. | +| (default) | Default unnamed slot for the helper text content (text or other inline elements). | + +Properties + +|Name | Type | Default | Description| +| ----- |-----------------------------------------------------|---------|-------------------------- +|status |'default','success','warning','error','indeterminate'|'default'| Defines the visual state of the helper text and determines which default icon is shown if no custom icon is provided.| +|icon | string |undefined| Custom icon name to display. Overrides the default icon derived from status. Requires pf-icon to be imported.| +|iconSet| string |undefined| Icon set to use when specifying a custom icon (e.g., 'fas').| + +## Events + +| Event Name | Description | +| ------------ | -------------------------------------------------------------------------------- | +| `icon-load` | Fired when a custom or default icon successfully loads. | +| `icon-error` | Fired if the icon fails to load. The event’s `detail` contains the error object. | + +## Default Icons by Status + +| Status | Default Icon | +| --------------- | ---------------------- | +| `default` | *(none)* | +| `indeterminate` | `minus-circle` | +| `warning` | `exclamation-triangle` | +| `success` | `circle-check` | +| `error` | `exclamation-circle` | + +## Notes + +Providing a custom overrides the default icon. + +Icons are lazy-loaded (loading="lazy") to optimize performance. + +The pf-icon element must be imported if using built-in icons. + +The component emits icon-load and icon-error events to track icon state. + + + + + + + + + + + + diff --git a/elements/pf-helper-text/demo/dynamic-list.html b/elements/pf-helper-text/demo/dynamic-list.html new file mode 100644 index 0000000000..3e098590d2 --- /dev/null +++ b/elements/pf-helper-text/demo/dynamic-list.html @@ -0,0 +1,40 @@ + + + + + pf-helper-text — Dynamic list + + + + + + +

Dynamic list

+ + + Must be at least 14 characters + + + Cannot contain any variation of the word "redhat" + + + Must include at least 3 of the following: lowercase letter, uppercase letters, numbers, symbols + + +
+ + + + diff --git a/elements/pf-helper-text/demo/dynamic.html b/elements/pf-helper-text/demo/dynamic.html new file mode 100644 index 0000000000..32ebc413d0 --- /dev/null +++ b/elements/pf-helper-text/demo/dynamic.html @@ -0,0 +1,107 @@ + + + + + + + pf-helper-text — Dynamic + + + + + + + + + + + + +

Dynamic

+ + + + This is default helper text + + + + This is indeterminate helper text + + + + This is warning helper text + + + + This is success helper text + + + + This is error helper text + + +
+ + + + diff --git a/elements/pf-helper-text/demo/index.html b/elements/pf-helper-text/demo/index.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/elements/pf-helper-text/demo/multiple.html b/elements/pf-helper-text/demo/multiple.html new file mode 100644 index 0000000000..4e1a5ac2ac --- /dev/null +++ b/elements/pf-helper-text/demo/multiple.html @@ -0,0 +1,83 @@ + + + + + + + pf-helper-text — Multiple static + + + + + + + + + + + + +

Multiple static

+ + + + This is default helper text + + + + This is another default helper text in the same block + + + + And this is more default text in the same block + + +
+ + + + diff --git a/elements/pf-helper-text/demo/static-icons.html b/elements/pf-helper-text/demo/static-icons.html new file mode 100644 index 0000000000..a145b1709d --- /dev/null +++ b/elements/pf-helper-text/demo/static-icons.html @@ -0,0 +1,139 @@ + + + + + + pf-helper-text — Static with icons + + + + + + + + + + + + +

Static helper text with default icons

+ + + + This is default helper text + + + + This is indeterminate helper text + + + + This is warning helper text + + + + This is success helper text + + + + This is error helper text + + +
+ + + + diff --git a/elements/pf-helper-text/demo/static.html b/elements/pf-helper-text/demo/static.html new file mode 100644 index 0000000000..f090f4f179 --- /dev/null +++ b/elements/pf-helper-text/demo/static.html @@ -0,0 +1,134 @@ + + + + + + pf-helper-text — Static (no icons) + + + + + + + + + + + + +

Static helper text

+ + + + This is default helper text + + + + This is indeterminate helper text + + + + This is warning helper text + + + + This is success helper text + + + + This is error helper text + + +
+ + + + diff --git a/elements/pf-helper-text/docs/pf-helper-text.md b/elements/pf-helper-text/docs/pf-helper-text.md new file mode 100644 index 0000000000..3e83d2c20a --- /dev/null +++ b/elements/pf-helper-text/docs/pf-helper-text.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-helper-text/pf-helper-text-item.ts b/elements/pf-helper-text/pf-helper-text-item.ts new file mode 100644 index 0000000000..546fd48504 --- /dev/null +++ b/elements/pf-helper-text/pf-helper-text-item.ts @@ -0,0 +1,169 @@ +import '@patternfly/elements/pf-icon/pf-icon.js'; +import { LitElement, html, css, type TemplateResult, type CSSResultGroup } from 'lit'; +import { customElement } from 'lit/decorators/custom-element.js'; +import { property } from 'lit/decorators/property.js'; +import { ifDefined } from 'lit/directives/if-defined.js'; + +/** + * Status types for helper text items. + */ +export type HelperTextStatus = + | 'default' + | 'success' + | 'warning' + | 'error' + | 'indeterminate'; + +/** + * `` + * + * Represents a single helper text line, including optional icon and status color. + * Can be used inside `` to display contextual feedback for form fields. + * + * @slot icon - Optional custom icon to override the default icon. + * @slot - Default slot for the helper text content. + * + * @fires icon-load - Fired when the icon successfully loads. + * @fires icon-error - Fired if loading the icon fails. + * + * @csspart icon - The container for the icon. + * @csspart text - The container for the text. + * @csspart item - The wrapper for both icon and text. + */ +@customElement('pf-helper-text-item') +export class PfHelperTextItem extends LitElement { + /** + * Defines the helper text status and its corresponding color and icon. + */ + @property({ reflect: true }) + status: HelperTextStatus = 'default'; + + /** + * Custom icon name to override the default icon. + * Requires `` to be imported. + */ + @property({ type: String }) + icon?: string; + + /** + * Icon set for custom icons (e.g., 'fas', 'patternfly'). + */ + @property({ type: String, attribute: 'icon-set' }) + iconSet?: string; + + /** + * When true, indicates the item is dynamic (updates visually in response to validation). + */ + @property({ type: Boolean, attribute: 'is-dynamic' }) + isDynamic = false; + + /** + * Controls whether the item should display an icon. + */ + @property({ type: Boolean, attribute: 'has-icon' }) + hasIcon = true; + + /** + * Map of status to default icons (Font Awesome solid set). + */ + private readonly _statusIconMap: Record< + Exclude, + string + > = { + success: 'check-circle', + warning: 'exclamation-triangle', + error: 'exclamation-circle', + indeterminate: 'info-circle', + }; + + /** + * Determine the effective icon to display. + */ + private get _resolvedIcon(): string | undefined { + if (this.icon) { + return this.icon; + } + if (this.status !== 'default') { + return this._statusIconMap[this.status]; + } + return undefined; + } + + /** + * Base styles adapted from PatternFly helper text design tokens. + */ + static readonly styles: CSSResultGroup = css` + :host { + display: flex; + align-items: center; + gap: var(--pf-c-helper-text--Gap, 0.25rem); + font-size: var(--pf-c-helper-text--FontSize, 0.875rem); + color: var(--pf-c-helper-text__item-text--Color, #151515); + line-height: 1.4; + } + + /* Color variants */ + :host([status='indeterminate']) { + color: var(--pf-c-helper-text__item-text--m-indeterminate--Color, #6a6e73); + } + + :host([status='warning']) { + color: var(--pf-c-helper-text__item-text--m-warning--Color, #795600); + } + + :host([status='success']) { + color: var(--pf-c-helper-text__item-text--m-success--Color, #1e4f18); + } + + :host([status='error']) { + color: var(--pf-c-helper-text__item-text--m-error--Color, #a30000); + } + + pf-icon { + fill: currentColor; + } + `; + + protected render(): TemplateResult { + const iconName = this._resolvedIcon; + const showIcon = this.hasIcon && iconName; + + return html` +
+ ${showIcon ? + html` + + + + + + ` + : ''} + + + +
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'pf-helper-text-item': PfHelperTextItem; + } +} diff --git a/elements/pf-helper-text/pf-helper-text.css b/elements/pf-helper-text/pf-helper-text.css new file mode 100644 index 0000000000..7761f97001 --- /dev/null +++ b/elements/pf-helper-text/pf-helper-text.css @@ -0,0 +1,29 @@ +:host { + display: inline-flex; + align-items: center; + gap: 0.4rem; + font-size: 14px; + line-height: 1.5; +} + +.helper-container { + display: inline-flex; + align-items: center; + gap: 0.4rem; +} + +:host([status="error"]) { + color: var(--pf-global--danger-color--100, #c9190b); +} + +:host([status="warning"]) { + color: var(--pf-global--warning-color--100, #f0ab00); +} + +:host([status="success"]) { + color: var(--pf-global--success-color--100, #3e8635); +} + +:host([status="indeterminate"]) { + color: var(--pf-global--palette--black-600, #6a6e73); +} diff --git a/elements/pf-helper-text/pf-helper-text.ts b/elements/pf-helper-text/pf-helper-text.ts new file mode 100644 index 0000000000..4fd7942661 --- /dev/null +++ b/elements/pf-helper-text/pf-helper-text.ts @@ -0,0 +1,40 @@ +import { LitElement, html, css, type TemplateResult, type CSSResultGroup } from 'lit'; +import { customElement } from 'lit/decorators/custom-element.js'; + + +/** + * `` + * + * Container for one or more `` elements. + * Provides accessibility structure and optional live-region behavior. + * + * @slot - Contains one or more `` elements. + */ +@customElement('pf-helper-text') +export class PfHelperText extends LitElement { + /** + * Styles controlling spacing and layout. + */ + static readonly styles: CSSResultGroup = css` + :host { + display: block; + font-size: var(--pf-c-helper-text--FontSize, 0.875rem); + gap: var(--pf-c-helper-text--Gap, 0.25rem); + } + + ::slotted(pf-helper-text-item) { + display: block; + margin-block: 0.25rem; + } + `; + + protected render(): TemplateResult { + return html``; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'pf-helper-text': PfHelperText; + } +} diff --git a/elements/pf-helper-text/test/pf-helper-text.e2e.ts b/elements/pf-helper-text/test/pf-helper-text.e2e.ts new file mode 100644 index 0000000000..3eec449fd4 --- /dev/null +++ b/elements/pf-helper-text/test/pf-helper-text.e2e.ts @@ -0,0 +1,25 @@ +import { test } from '@playwright/test'; +import { PfeDemoPage } from '@patternfly/pfe-tools/test/playwright/PfeDemoPage.js'; +import { SSRPage } from '@patternfly/pfe-tools/test/playwright/SSRPage.js'; + +const tagName = 'pf-helper-text'; + +test.describe(tagName, () => { + test('snapshot', async ({ page }) => { + const componentPage = new PfeDemoPage(page, tagName); + await componentPage.navigate(); + await componentPage.snapshot(); + }); + + test('ssr', async ({ browser }) => { + const fixture = new SSRPage({ + tagName, + browser, + demoDir: new URL('../demo/', import.meta.url), + importSpecifiers: [ + `@patternfly/elements/${tagName}/${tagName}.js`, + ], + }); + await fixture.snapshots(); + }); +}); diff --git a/elements/pf-helper-text/test/pf-helper-text.spec.ts b/elements/pf-helper-text/test/pf-helper-text.spec.ts new file mode 100644 index 0000000000..bf985246fc --- /dev/null +++ b/elements/pf-helper-text/test/pf-helper-text.spec.ts @@ -0,0 +1,21 @@ +import { expect, html } from '@open-wc/testing'; +import { createFixture } from '@patternfly/pfe-tools/test/create-fixture.js'; +import { PfHelperText } from '@patternfly/elements/pf-helper-text/pf-helper-text.js'; + +describe('', function() { + describe('simply instantiating', function() { + let element: PfHelperText; + it('imperatively instantiates', function() { + expect(document.createElement('pf-helper-text')).to.be.an.instanceof(PfHelperText); + }); + + it('should upgrade', async function() { + element = await createFixture(html``); + const klass = customElements.get('pf-helper-text'); + expect(element) + .to.be.an.instanceOf(klass) + .and + .to.be.an.instanceOf(PfHelperText); + }); + }); +}); diff --git a/package.json b/package.json index 745f339923..08f22dbadc 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "🚧-BUILD": "❓ Make packages and artifacts", "build": "wireit", "build:create": "wireit", + "build:elements": "wireit", "analyze": "wireit", "docs": "wireit", "site": "npm run build", @@ -290,6 +291,7 @@ "@octokit/core": "^6.1.2", "@patternfly/patternfly": "^4.224.5", "@pwrs/cem": "^0.6.5", + "@pwrs/cem-linux-x64": "^0.6.5", "@rhds/elements": "^1.4.5", "@types/koa__router": "^12.0.5", "@types/node": "^22.19.0",