diff --git a/.changeset/few-lands-feel.md b/.changeset/few-lands-feel.md new file mode 100644 index 0000000000..b0a82b285f --- /dev/null +++ b/.changeset/few-lands-feel.md @@ -0,0 +1,4 @@ +--- +"@patternfly/pfe-core": major +--- +Removed global `pfeLog` feature diff --git a/.changeset/mean-tires-ask.md b/.changeset/mean-tires-ask.md new file mode 100644 index 0000000000..cc3f34b6cb --- /dev/null +++ b/.changeset/mean-tires-ask.md @@ -0,0 +1,4 @@ +--- +"@patternfly/pfe-core": major +--- +Removed `window.PfeConfig` global config object diff --git a/.changeset/polite-rules-dress.md b/.changeset/polite-rules-dress.md new file mode 100644 index 0000000000..a2bfedd823 --- /dev/null +++ b/.changeset/polite-rules-dress.md @@ -0,0 +1,4 @@ +--- +"@patternfly/pfe-core": major +--- +Removed global `auto-reveal` feature diff --git a/.changeset/public-yaks-tickle.md b/.changeset/public-yaks-tickle.md new file mode 100644 index 0000000000..b500155e41 --- /dev/null +++ b/.changeset/public-yaks-tickle.md @@ -0,0 +1,4 @@ +--- +"@patternfly/pfe-core": patch +--- +Context: `makeContextRoot` no longer crashes SSR processes diff --git a/.changeset/thirty-hounds-know.md b/.changeset/thirty-hounds-know.md new file mode 100644 index 0000000000..37cfb7eb81 --- /dev/null +++ b/.changeset/thirty-hounds-know.md @@ -0,0 +1,4 @@ +--- +"@patternfly/pfe-core": major +--- +Removed global `trackPerformance` feature diff --git a/.github/workflows/bundle.yml b/.github/workflows/bundle.yml index 7d6949adad..416711a1ed 100644 --- a/.github/workflows/bundle.yml +++ b/.github/workflows/bundle.yml @@ -22,8 +22,8 @@ jobs: if: github.repository == 'patternfly/patternfly-elements' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: '20' cache: npm diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml index a382f20d84..d5e04f66e1 100644 --- a/.github/workflows/commitlint.yml +++ b/.github/workflows/commitlint.yml @@ -26,7 +26,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 915c5b4e41..6eaa98b20d 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -10,8 +10,8 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Repo - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: '20' cache: npm diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0685fc84c6..67b894866b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,8 +11,8 @@ jobs: if: github.repository == 'patternfly/patternfly-elements' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: '20' cache: npm diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d00b424a18..616db45655 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -26,6 +26,7 @@ env: # Bring color into the GitHub terminal FORCE_COLOR: true GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PLAYWRIGHT_REPORT_DIR: test-report # https://github.blog/changelog/2020-10-01-github-actions-deprecating-set-env-and-add-path-commands/ ACTIONS_ALLOW_UNSECURE_COMMANDS: "true" @@ -45,11 +46,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Configures the node version used on GitHub-hosted runners - name: Configure node version - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: '20' cache: npm @@ -61,17 +62,16 @@ jobs: id: lint run: npm run lint - test: - name: Run test suite (Web Test Runner) + name: Unit Tests (Web Test Runner) runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Configures the node version used on GitHub-hosted runners - name: Configure node version - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: '20' cache: npm @@ -91,6 +91,67 @@ jobs: report_paths: test-results/test-results.xml fail_on_failure: true # fail the actions run if the tests failed + ssr: + name: SSR Tests (Playwright) + runs-on: ubuntu-latest + container: + image: mcr.microsoft.com/playwright:v1.44.0-jammy + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: npm + - run: npm ci --prefer-offline + - run: npm run build + + - name: Run tests + run: npx playwright test -g ssr --update-snapshots + env: + HOME: /root + + - uses: actions/upload-artifact@v2 + if: always() + with: + name: ${{ env.PLAYWRIGHT_REPORT_DIR }} + path: ${{ env.PLAYWRIGHT_REPORT_DIR }}/ + retention-days: 30 + + publish_report: + name: Publish Playwright Report + # using always() is not ideal here, because it would also run if the workflow was cancelled + if: "success() || needs.ssr.result == 'failure'" + needs: + - ssr + runs-on: ubuntu-latest + continue-on-error: true + env: + HTML_REPORT_URL_PATH: reports/${{ github.ref_name }}/${{ github.run_id }}/${{ github.run_attempt }} + steps: + - uses: actions/checkout@v4 + - name: Download zipped HTML report + uses: actions/download-artifact@v2 + with: + name: ${{ env.PLAYWRIGHT_REPORT_DIR }} + path: ${{ env.PLAYWRIGHT_REPORT_DIR }}/ + - name: Upload to Github Pages + uses: JamesIves/github-pages-deploy-action@v4 + with: + folder: ${{ env.PLAYWRIGHT_REPORT_DIR }} + target-folder: ${{ env.HTML_REPORT_URL_PATH }} + - name: Add comment to PR + uses: marocchino/sticky-pull-request-comment@v2 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + number: ${{ github.event.inputs.issueNumber }} + append: true + header: "${{ github.sha }}" + hide: true + hide_details: true + message: | + **SSR Test Run for ${{ github.sha }}**: [Report](https://patternfly.github.io/patternfly-elements/${{ env.HTML_REPORT_URL_PATH }}) + + # Validate the build to main was successful; open an issue if not build: name: Compile project runs-on: ubuntu-latest @@ -116,13 +177,13 @@ jobs: ) steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 # Configures the node version used on GitHub-hosted runners - name: Configure node version - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} cache: npm @@ -138,7 +199,6 @@ jobs: id: release-dry run: npm run prepublishOnly -ws --if-present - # Validate the build to main was successful; open an issue if not validate: name: Validate successful build on main needs: diff --git a/.github/workflows/visual-regression.yml b/.github/workflows/visual-regression.yml index db55b2b814..3f3acfd848 100644 --- a/.github/workflows/visual-regression.yml +++ b/.github/workflows/visual-regression.yml @@ -26,8 +26,8 @@ jobs: e2e: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: '20' cache: npm @@ -42,7 +42,7 @@ jobs: lighthouse: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Generate URLs id: urls diff --git a/.gitignore b/.gitignore index a0146b7ebc..6f6ad9dc21 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ pfe.min.js **/*.LEGAL.txt *.tsbuildinfo test-results +test-report /elements/react diff --git a/core/pfe-core/controllers/floating-dom-controller.ts b/core/pfe-core/controllers/floating-dom-controller.ts index be84b89fed..4522c0ec94 100644 --- a/core/pfe-core/controllers/floating-dom-controller.ts +++ b/core/pfe-core/controllers/floating-dom-controller.ts @@ -1,5 +1,5 @@ import type { Placement } from '@floating-ui/dom'; -import type { ReactiveController, ReactiveControllerHost } from 'lit'; +import type { LitElement, ReactiveController, ReactiveControllerHost } from 'lit'; import type { StyleInfo } from 'lit/directives/style-map.js'; import type { OffsetOptions as Offset } from '@floating-ui/core'; @@ -100,7 +100,7 @@ export class FloatingDOMController implements ReactiveController { ) { host.addController(this); this.#options = { - invoker: (host instanceof HTMLElement ? () => host : () => undefined), + invoker: (() => host as LitElement), shift: true, ...options, }; diff --git a/core/pfe-core/controllers/internals-controller.ts b/core/pfe-core/controllers/internals-controller.ts index 3ebdbd842a..6e72138a3a 100644 --- a/core/pfe-core/controllers/internals-controller.ts +++ b/core/pfe-core/controllers/internals-controller.ts @@ -1,4 +1,9 @@ -import type { ReactiveController, ReactiveControllerHost } from 'lit'; +import { + isServer, + type ReactiveController, + type ReactiveControllerHost, + type LitElement, +} from 'lit'; function isARIAMixinProp(key: string): key is keyof ARIAMixin { return key === 'role' || key.startsWith('aria'); @@ -146,7 +151,11 @@ export class InternalsController implements ReactiveController, ARIAMixin { /** True when the control is disabled via it's containing fieldset element */ get formDisabled() { - return this.element?.matches(':disabled') || this._formDisabled; + if (isServer) { + return this._formDisabled; + } else { + return this.element?.matches(':disabled') || this._formDisabled; + } } get labels() { @@ -166,7 +175,13 @@ export class InternalsController implements ReactiveController, ARIAMixin { } private get element() { - return this.host instanceof HTMLElement ? this.host : this.options?.getHTMLElement?.(); + if (isServer) { + // FIXME(bennyp): a little white lie, which may break + // when the controller is applied to non-lit frameworks. + return this.host as LitElement; + } else { + return this.host instanceof HTMLElement ? this.host : this.options?.getHTMLElement?.(); + } } private internals!: ElementInternals; diff --git a/core/pfe-core/controllers/listbox-controller.ts b/core/pfe-core/controllers/listbox-controller.ts index 3dc85192f1..abcda1ec6f 100644 --- a/core/pfe-core/controllers/listbox-controller.ts +++ b/core/pfe-core/controllers/listbox-controller.ts @@ -1,4 +1,4 @@ -import type { ReactiveController, ReactiveControllerHost } from 'lit'; +import { isServer, type ReactiveController, type ReactiveControllerHost } from 'lit'; export interface ListboxAccessibilityController< Item extends HTMLElement @@ -56,7 +56,9 @@ export class ListboxController implements ReactiveCont if (!constructingAllowed) { throw new Error('ListboxController must be constructed with `ListboxController.of()`'); } - if (!(host instanceof HTMLElement) && typeof _options.getHTMLElement !== 'function') { + if (!isServer + && !(host instanceof HTMLElement) + && typeof _options.getHTMLElement !== 'function') { throw new Error( `ListboxController requires the host to be an HTMLElement, or for the initializer to include a \`getHTMLElement()\` function`, ); diff --git a/core/pfe-core/controllers/logger.ts b/core/pfe-core/controllers/logger.ts index ec3f313a96..4c4cb7bfd3 100644 --- a/core/pfe-core/controllers/logger.ts +++ b/core/pfe-core/controllers/logger.ts @@ -1,4 +1,4 @@ -import type { ReactiveController, ReactiveControllerHost } from 'lit'; +import { isServer, type ReactiveController, type ReactiveControllerHost } from 'lit'; export class Logger implements ReactiveController { private static logDebug: boolean; @@ -6,7 +6,7 @@ export class Logger implements ReactiveController { private static instances = new WeakMap(); private get prefix() { - if (this.host instanceof HTMLElement) { + if (!isServer && this.host instanceof HTMLElement) { return `[${this.host.localName}${this.host.id ? `#${this.host.id}` : ''}]`; } else { return `[${this.host.constructor.name}]`; diff --git a/core/pfe-core/controllers/overflow-controller.ts b/core/pfe-core/controllers/overflow-controller.ts index 5d4b888497..1b9e1d49e8 100644 --- a/core/pfe-core/controllers/overflow-controller.ts +++ b/core/pfe-core/controllers/overflow-controller.ts @@ -18,7 +18,7 @@ export class OverflowController implements ReactiveController { static { // on resize check for overflows to add or remove scroll buttons - window.addEventListener('resize', () => { + globalThis.addEventListener?.('resize', () => { for (const instance of this.#instances) { instance.onScroll(); } diff --git a/core/pfe-core/controllers/scroll-spy-controller.ts b/core/pfe-core/controllers/scroll-spy-controller.ts index 60c223d1ca..9468b3a547 100644 --- a/core/pfe-core/controllers/scroll-spy-controller.ts +++ b/core/pfe-core/controllers/scroll-spy-controller.ts @@ -44,7 +44,7 @@ export class ScrollSpyController implements ReactiveController { #rootMargin?: string; #threshold: number | number[]; - #rootNode: Node; + #getRootNode: () => Node; #getHash: (el: Element) => string | null; get #linkChildren(): Element[] { @@ -92,7 +92,7 @@ export class ScrollSpyController implements ReactiveController { this.#rootMargin = options.rootMargin; this.#activeAttribute = options.activeAttribute ?? 'active'; this.#threshold = options.threshold ?? 0.85; - this.#rootNode = options.rootNode ?? host.getRootNode(); + this.#getRootNode = () => options.rootNode ?? host.getRootNode(); this.#getHash = options?.getHash ?? ((el: Element) => el.getAttribute('href')); } @@ -101,7 +101,7 @@ export class ScrollSpyController implements ReactiveController { } #initIo() { - const rootNode = this.#rootNode; + const rootNode = this.#getRootNode(); if (rootNode instanceof Document || rootNode instanceof ShadowRoot) { const { rootMargin, threshold, root } = this; this.#io = new IntersectionObserver(r => this.#onIo(r), { root, rootMargin, threshold }); diff --git a/core/pfe-core/core.ts b/core/pfe-core/core.ts index 55519b5c93..21a3da623a 100644 --- a/core/pfe-core/core.ts +++ b/core/pfe-core/core.ts @@ -1,38 +1,9 @@ import type { ComplexAttributeConverter } from 'lit'; -/** PatternFly Elements global config object */ -export interface PfeConfig { - /** Set to false to disable client-side page load performance tracking */ - trackPerformance?: boolean; - /** Set to false to disable various debug logs */ - log?: boolean; - /** Set to false to disable automatically removing `unresolved` attr from body */ - autoReveal?: boolean; -} - export type RequireProps = T & { [P in Ps]-?: T[P]; }; -const noPref = Symbol(); - -/** Retrieve an HTML metadata item */ -function getMeta(name: string): string | undefined { - return document.head.querySelector(`meta[name="${name}"]`)?.content; -} - -/** - * A boolean value that indicates if the performance should be tracked. - * For use in a JS file or script tag; can also be added in the constructor of a component during development. - * @example trackPerformance(true); - */ -export function trackPerformance(preference: boolean | typeof noPref = noPref) { - if (preference !== noPref) { - window.PfeConfig.trackPerformance = !!preference; - } - return window.PfeConfig.trackPerformance; -} - function makeConverter( f: (x: string, type?: unknown) => T, ): ComplexAttributeConverter { @@ -78,36 +49,3 @@ export class ComposedEvent extends Event { } } -// ๐Ÿ‘‡ SIDE EFFECTS ๐Ÿ‘‡ - -declare global { - interface Window { - /** Global configuration settings for PatternFly Elements */ - PfeConfig: PfeConfig; - } -} - -const bodyNoAutoReveal = document.body.hasAttribute('no-auto-reveal'); - -/** Global patternfly elements config */ -window.PfeConfig = Object.assign(window.PfeConfig ?? {}, { - trackPerformance: window.PfeConfig?.trackPerformance - ?? getMeta('pf-track-performance') === 'true', - // if the body tag has `no-auto-reveal` attribute, reveal immediately - // if `` exists, and it's `content` is 'true', - // then auto-reveal the body - autoReveal: window.PfeConfig?.autoReveal ?? ( - bodyNoAutoReveal ? !bodyNoAutoReveal - : getMeta('pf-auto-reveal') === 'true' - ), - get log() { - return !!localStorage.pfeLog; - }, - set log(v: boolean) { - if (v) { - localStorage.setItem('pfeLog', `${true}`); - } else { - localStorage.removeItem('pfeLog'); - } - }, -}); diff --git a/core/pfe-core/decorators/time.ts b/core/pfe-core/decorators/time.ts index dca0b4ae31..aa0f964a8f 100644 --- a/core/pfe-core/decorators/time.ts +++ b/core/pfe-core/decorators/time.ts @@ -1,5 +1,6 @@ /** * Tracks the time a method takes to complete using the [performance API](https://developer.mozilla.org/en-US/docs/Web/API/Performance) + * @param tag - short string to identify the method name */ export function time(tag?: string) { return function(_: unknown, key: string, descriptor: PropertyDescriptor) { @@ -14,19 +15,15 @@ export function time(tag?: string) { const START_TAG = `start-${TAG}`; const END_TAG = `end-${TAG}`; - if (window.PfeConfig.trackPerformance) { - performance.mark(START_TAG); - } + performance.mark(START_TAG); const x = f.call(this, ...args); const ret = () => { - if (window.PfeConfig.trackPerformance) { - performance.mark(END_TAG); - performance.measure(TAG, START_TAG, END_TAG); - // eslint-disable-next-line no-console - console.log(Array.from(performance.getEntriesByName(TAG)).pop()); - } + performance.mark(END_TAG); + performance.measure(TAG, START_TAG, END_TAG); + // eslint-disable-next-line no-console + console.log(Array.from(performance.getEntriesByName(TAG)).pop()); return x; }; diff --git a/core/pfe-core/functions/context.ts b/core/pfe-core/functions/context.ts index ec97e4bcfb..baa8a1aeee 100644 --- a/core/pfe-core/functions/context.ts +++ b/core/pfe-core/functions/context.ts @@ -1,10 +1,11 @@ import { ContextRoot, createContext } from '@lit/context'; +import { isServer } from 'lit'; let root: ContextRoot; function makeContextRoot() { - root = new ContextRoot(); - root.attach(document.body); + const root = new ContextRoot(); + !isServer && root.attach(document.body); return root; } diff --git a/docs/components/demos.html b/docs/components/demos.html index 1f39a4d2c5..0d17a73eec 100644 --- a/docs/components/demos.html +++ b/docs/components/demos.html @@ -6,7 +6,7 @@ data: 'demos', alias: 'demo', size: 1, - before: xs => xs.filter(x => x.permalink ), + before: xs => xs.filter(x => x.permalink), }, preloads: [ '@lit/reactive-element@1.0.2/development/css-tag.js', diff --git a/elements/pf-accordion/BaseAccordion.ts b/elements/pf-accordion/BaseAccordion.ts new file mode 100644 index 0000000000..062ef9afef --- /dev/null +++ b/elements/pf-accordion/BaseAccordion.ts @@ -0,0 +1,327 @@ +import type { TemplateResult } from 'lit'; + +import { LitElement, html } from 'lit'; +import { property } from 'lit/decorators/property.js'; + +import { NumberListConverter, ComposedEvent } from '@patternfly/pfe-core'; +import { Logger } from '@patternfly/pfe-core/controllers/logger.js'; + +import { AccordionHeaderChangeEvent, BaseAccordionHeader } from './BaseAccordionHeader.js'; +import { BaseAccordionPanel } from './BaseAccordionPanel.js'; + +import { RovingTabindexController } from '@patternfly/pfe-core/controllers/roving-tabindex-controller.js'; + +export class AccordionExpandEvent extends ComposedEvent { + constructor( + public toggle: BaseAccordionHeader, + public panel: BaseAccordionPanel, + ) { + super('expand'); + } +} + +export class AccordionCollapseEvent extends ComposedEvent { + constructor( + public toggle: BaseAccordionHeader, + public panel: BaseAccordionPanel, + ) { + super('collapse'); + } +} + +export abstract class BaseAccordion extends LitElement { + static isAccordion(target: EventTarget | null): target is BaseAccordion { + return target instanceof BaseAccordion; + } + + static isHeader(target: EventTarget | null): target is BaseAccordionHeader { + return target instanceof BaseAccordionHeader; + } + + static isPanel(target: EventTarget | null): target is BaseAccordionPanel { + return target instanceof BaseAccordionPanel; + } + + static #isAccordionChangeEvent(event: Event): event is AccordionHeaderChangeEvent { + return event instanceof AccordionHeaderChangeEvent; + } + + #headerIndex = new RovingTabindexController(this, { + getItems: () => this.headers, + }); + + #expandedIndex: number[] = []; + + /** + * Sets and reflects the currently expanded accordion 0-based indexes. + * Use commas to separate multiple indexes. + * ```html + * + * ... + * + * ``` + */ + @property({ + attribute: 'expanded-index', + converter: NumberListConverter, + }) + get expandedIndex() { + return this.#expandedIndex; + } + + set expandedIndex(value) { + const old = this.#expandedIndex; + this.#expandedIndex = value; + if (JSON.stringify(old) !== JSON.stringify(value)) { + this.requestUpdate('expandedIndex', old); + this.collapseAll().then(async () => { + for (const i of this.expandedIndex) { + await this.expand(i, this); + } + }); + } + } + + get headers() { + return this.#allHeaders(); + } + + get panels() { + return this.#allPanels(); + } + + get #activeHeader() { + const { headers } = this; + const index = headers.findIndex(header => header.matches(':focus,:focus-within')); + return index > -1 ? headers.at(index) : undefined; + } + + protected expandedSets = new Set(); + + #logger = new Logger(this); + + // actually is read in #init, by the `||=` operator + // eslint-disable-next-line no-unused-private-class-members + #initialized = false; + + protected override async getUpdateComplete(): Promise { + const c = await super.getUpdateComplete(); + const results = await Promise.all([ + ...this.#allHeaders().map(x => x.updateComplete), + ...this.#allPanels().map(x => x.updateComplete), + ]); + return c && results.every(Boolean); + } + + #mo = new MutationObserver(() => this.#init()); + + connectedCallback() { + super.connectedCallback(); + this.addEventListener('change', this.#onChange as EventListener); + this.#mo.observe(this, { childList: true }); + this.#init(); + } + + render(): TemplateResult { + return html` + + `; + } + + async firstUpdated() { + const { headers } = this; + headers.forEach((header, index) => { + if (header.expanded) { + this.#expandHeader(header, index); + const panel = this.#panelForHeader(header); + if (panel) { + this.#expandPanel(panel); + } + } + }); + } + + /** + * Initialize the accordion by connecting headers and panels + * with aria controls and labels; set up the default disclosure + * state if not set by the author; and check the URL for default + * open + */ + async #init() { + this.#initialized ||= !!await this.updateComplete; + // Event listener to the accordion header after the accordion has been initialized to add the roving tabindex + this.addEventListener('focusin', this.#updateActiveHeader); + this.updateAccessibility(); + } + + #updateActiveHeader() { + if (this.#activeHeader !== this.#headerIndex.activeItem) { + this.#headerIndex.setActiveItem(this.#activeHeader); + } + } + + #panelForHeader(header: BaseAccordionHeader) { + const next = header.nextElementSibling; + if (!BaseAccordion.isPanel(next)) { + return void this.#logger.error('Sibling element to a header needs to be a panel'); + } else { + return next; + } + } + + #expandHeader(header: BaseAccordionHeader, index = this.#getIndex(header)) { + // If this index is not already listed in the expandedSets array, add it + this.expandedSets.add(index); + this.#expandedIndex = [...this.expandedSets as Set]; + header.expanded = true; + } + + #expandPanel(panel: BaseAccordionPanel) { + panel.expanded = true; + panel.hidden = false; + } + + async #collapseHeader(header: BaseAccordionHeader, index = this.#getIndex(header)) { + if (!this.expandedSets) { + await this.updateComplete; + } + this.expandedSets.delete(index); + header.expanded = false; + await header.updateComplete; + } + + async #collapsePanel(panel: BaseAccordionPanel) { + await panel.updateComplete; + if (!panel.expanded) { + return; + } + + panel.expanded = false; + panel.hidden = true; + } + + #onChange(event: AccordionHeaderChangeEvent) { + if (BaseAccordion.#isAccordionChangeEvent(event)) { + const index = this.#getIndex(event.target); + if (event.expanded) { + this.expand(index, event.accordion); + } else { + this.collapse(index); + } + } + } + + #allHeaders(accordion: BaseAccordion = this): BaseAccordionHeader[] { + return Array.from(accordion.children ?? []).filter(BaseAccordion.isHeader); + } + + #allPanels(accordion: BaseAccordion = this): BaseAccordionPanel[] { + return Array.from(accordion.children ?? []).filter(BaseAccordion.isPanel); + } + + #getIndex(el: Element | null) { + if (BaseAccordion.isHeader(el)) { + return this.headers.findIndex(header => header.id === el.id); + } + + if (BaseAccordion.isPanel(el)) { + return this.panels.findIndex(panel => panel.id === el.id); + } + + this.#logger.warn('The #getIndex method expects to receive a header or panel element.'); + return -1; + } + + public updateAccessibility() { + this.#headerIndex.updateItems(); + const { headers } = this; + + // For each header in the accordion, attach the aria connections + headers.forEach(header => { + const panel = this.#panelForHeader(header); + if (panel) { + header.setAttribute('aria-controls', panel.id); + panel.setAttribute('aria-labelledby', header.id); + panel.hidden = !panel.expanded; + } + }); + } + + /** + * Accepts a 0-based index value (integer) for the set of accordion items to expand or collapse. + */ + public async toggle(index: number) { + const { headers } = this; + const header = headers[index]; + + if (!header.expanded) { + await this.expand(index); + } else { + await this.collapse(index); + } + } + + /** + * Accepts a 0-based index value (integer) for the set of accordion items to expand. + * Accepts an optional parent accordion to search for headers and panels. + */ + public async expand(index: number, parentAccordion?: BaseAccordion) { + const allHeaders: BaseAccordionHeader[] = this.#allHeaders(parentAccordion); + + const header = allHeaders[index]; + if (!header) { + return; + } + + const panel = this.#panelForHeader(header); + if (!panel) { + return; + } + + // If the header and panel exist, open both + this.#expandHeader(header, index), + this.#expandPanel(panel), + + header.focus(); + + this.dispatchEvent(new AccordionExpandEvent(header, panel)); + + await this.updateComplete; + } + + /** + * Expands all accordion items. + */ + public async expandAll() { + this.headers.forEach(header => this.#expandHeader(header)); + this.panels.forEach(panel => this.#expandPanel(panel)); + await this.updateComplete; + } + + /** + * Accepts a 0-based index value (integer) for the set of accordion items to collapse. + */ + public async collapse(index: number) { + const header = this.headers.at(index); + const panel = this.panels.at(index); + + if (!header || !panel) { + return; + } + + this.#collapseHeader(header); + this.#collapsePanel(panel); + + this.dispatchEvent(new AccordionCollapseEvent(header, panel)); + await this.updateComplete; + } + + /** + * Collapses all accordion items. + */ + public async collapseAll() { + this.headers.forEach(header => this.#collapseHeader(header)); + this.panels.forEach(panel => this.#collapsePanel(panel)); + await this.updateComplete; + } +} diff --git a/elements/pf-accordion/pf-accordion.ts b/elements/pf-accordion/pf-accordion.ts index 173e1c77e3..729f17533c 100644 --- a/elements/pf-accordion/pf-accordion.ts +++ b/elements/pf-accordion/pf-accordion.ts @@ -293,12 +293,12 @@ export class PfAccordion extends LitElement { } #allHeaders(accordion: PfAccordion = this): PfAccordionHeader[] { - return Array.from(accordion.children).filter((x): x is PfAccordionHeader => + return Array.from(accordion.children ?? []).filter((x): x is PfAccordionHeader => x instanceof PfAccordionHeader); } #allPanels(accordion: PfAccordion = this): PfAccordionPanel[] { - return Array.from(accordion.children).filter((x): x is PfAccordionPanel => + return Array.from(accordion.children ?? []).filter((x): x is PfAccordionPanel => x instanceof PfAccordionPanel); } diff --git a/elements/pf-accordion/test/pf-accordion.e2e.ts b/elements/pf-accordion/test/pf-accordion.e2e.ts index 1fcec84cf6..82f3720c13 100644 --- a/elements/pf-accordion/test/pf-accordion.e2e.ts +++ b/elements/pf-accordion/test/pf-accordion.e2e.ts @@ -1,5 +1,6 @@ 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-accordion'; @@ -9,4 +10,16 @@ test.describe(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-avatar/test/pf-avatar.e2e.ts b/elements/pf-avatar/test/pf-avatar.e2e.ts index 17c15158a4..9ab4c74237 100644 --- a/elements/pf-avatar/test/pf-avatar.e2e.ts +++ b/elements/pf-avatar/test/pf-avatar.e2e.ts @@ -1,5 +1,6 @@ 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-avatar'; @@ -9,4 +10,16 @@ test.describe(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-back-to-top/demo/always-visible.html b/elements/pf-back-to-top/demo/always-visible.html index 427492d97f..a607050b23 100644 --- a/elements/pf-back-to-top/demo/always-visible.html +++ b/elements/pf-back-to-top/demo/always-visible.html @@ -1,5 +1,3 @@ - -

Always visible

Focusable element (top) @@ -10,3 +8,21 @@

Always visible

+ + diff --git a/elements/pf-back-to-top/demo/button-no-text.html b/elements/pf-back-to-top/demo/button-no-text.html index 09728fc299..0f6de199a2 100644 --- a/elements/pf-back-to-top/demo/button-no-text.html +++ b/elements/pf-back-to-top/demo/button-no-text.html @@ -1,5 +1,3 @@ - -

Button No Text

@@ -20,3 +18,26 @@

Button No Text

target.focus(); }); + + diff --git a/elements/pf-back-to-top/demo/button.html b/elements/pf-back-to-top/demo/button.html index a05dd33e70..3a2806507f 100644 --- a/elements/pf-back-to-top/demo/button.html +++ b/elements/pf-back-to-top/demo/button.html @@ -1,5 +1,3 @@ - - Accessibility Warning Using the Button/JS variant, implementation must apply click event and focus to the element that is scrolled to. @@ -25,3 +23,30 @@

Button

target.focus(); }); + + diff --git a/elements/pf-back-to-top/demo/demo.css b/elements/pf-back-to-top/demo/demo.css deleted file mode 100644 index 39ac723ffd..0000000000 --- a/elements/pf-back-to-top/demo/demo.css +++ /dev/null @@ -1,25 +0,0 @@ -:root { - --_scroll-distance: 400px; -} - -main { - scroll-behavior: smooth; -} - -.scroll-distance { - --_scroll-distance: 200px; -} - -.outer-container { - height: calc(100vh - var(--pf-demo-header-height) + var(--_scroll-distance)); -} - -.padded { - padding: var(--pf-global--spacer--md, 1rem); -} - -.scroll-indicator { - height: var(--_scroll-distance); - background-color: var(--pf-global--palette--cyan-50, #f2f9f9) !important; -} - diff --git a/elements/pf-back-to-top/demo/label.html b/elements/pf-back-to-top/demo/label.html index 51db65dd2f..0fa9c01840 100644 --- a/elements/pf-back-to-top/demo/label.html +++ b/elements/pf-back-to-top/demo/label.html @@ -1,5 +1,3 @@ - -

Default

@@ -14,3 +12,30 @@

Default

+ + diff --git a/elements/pf-back-to-top/demo/no-text.html b/elements/pf-back-to-top/demo/no-text.html index 66e8d4bbfa..c2a4ed1db3 100644 --- a/elements/pf-back-to-top/demo/no-text.html +++ b/elements/pf-back-to-top/demo/no-text.html @@ -1,5 +1,3 @@ - -

No Text

@@ -14,3 +12,30 @@

No Text

+ + diff --git a/elements/pf-back-to-top/demo/pf-back-to-top.html b/elements/pf-back-to-top/demo/pf-back-to-top.html index f22fb4277d..521bb16603 100644 --- a/elements/pf-back-to-top/demo/pf-back-to-top.html +++ b/elements/pf-back-to-top/demo/pf-back-to-top.html @@ -1,5 +1,3 @@ - -

Default

@@ -14,3 +12,30 @@

Default

+ + diff --git a/elements/pf-back-to-top/demo/scroll-distance.html b/elements/pf-back-to-top/demo/scroll-distance.html index b7d97a6015..15c65c6a99 100644 --- a/elements/pf-back-to-top/demo/scroll-distance.html +++ b/elements/pf-back-to-top/demo/scroll-distance.html @@ -1,5 +1,3 @@ - -

Default

@@ -14,3 +12,30 @@

Default

+ + diff --git a/elements/pf-back-to-top/pf-back-to-top.ts b/elements/pf-back-to-top/pf-back-to-top.ts index e45e0f6e8b..b436bfd7e6 100644 --- a/elements/pf-back-to-top/pf-back-to-top.ts +++ b/elements/pf-back-to-top/pf-back-to-top.ts @@ -1,4 +1,4 @@ -import { LitElement, html, type PropertyValues } from 'lit'; +import { LitElement, html, isServer, type PropertyValues } 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'; @@ -66,9 +66,11 @@ export class PfBackToTop extends LitElement { #logger = new Logger(this); - get #rootNode(): Document | ShadowRoot { - const root = this.getRootNode(); - if (root instanceof Document || root instanceof ShadowRoot) { + get #rootNode(): Document | ShadowRoot | null { + let root = null; + if (isServer) { + return null; + } else if ((root = this.getRootNode()) instanceof Document || root instanceof ShadowRoot) { return root; } else { return document; @@ -157,7 +159,7 @@ export class PfBackToTop extends LitElement { this.#scrollSpy = !!this.scrollableSelector; if (this.#scrollSpy && this.scrollableSelector) { - const scrollableElement = this.#rootNode.querySelector(this.scrollableSelector); + const scrollableElement = this.#rootNode?.querySelector?.(this.scrollableSelector); if (!scrollableElement) { this.#logger.error(`unable to find element with selector ${this.scrollableSelector}`); return; diff --git a/elements/pf-back-to-top/test/pf-back-to-top.e2e.ts b/elements/pf-back-to-top/test/pf-back-to-top.e2e.ts index 708611d7a3..f1c0a0ddde 100644 --- a/elements/pf-back-to-top/test/pf-back-to-top.e2e.ts +++ b/elements/pf-back-to-top/test/pf-back-to-top.e2e.ts @@ -1,5 +1,6 @@ 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-back-to-top'; @@ -9,4 +10,16 @@ test.describe(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-background-image/demo/filter-override.html b/elements/pf-background-image/demo/filter-override.html index aec2f02c8c..5b874ccedd 100644 --- a/elements/pf-background-image/demo/filter-override.html +++ b/elements/pf-background-image/demo/filter-override.html @@ -1,9 +1,9 @@ + src="pfbg.jpg" + src-2x="pfbg_576.jpg" + src-sm="pfbg_768.jpg" + src-sm-2x="pfbg_768@2x.jpg" + src-lg="pfbg_1200.jpg"> diff --git a/elements/pf-background-image/demo/sibling-content.html b/elements/pf-background-image/demo/sibling-content.html index c27647c14b..0bfc15f2b5 100644 --- a/elements/pf-background-image/demo/sibling-content.html +++ b/elements/pf-background-image/demo/sibling-content.html @@ -1,10 +1,10 @@
diff --git a/elements/pf-background-image/test/pf-background-image.e2e.ts b/elements/pf-background-image/test/pf-background-image.e2e.ts index 591f19a641..c6b83ffb54 100644 --- a/elements/pf-background-image/test/pf-background-image.e2e.ts +++ b/elements/pf-background-image/test/pf-background-image.e2e.ts @@ -1,5 +1,6 @@ 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-background-image'; @@ -9,4 +10,16 @@ test.describe(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-badge/test/pf-badge.e2e.ts b/elements/pf-badge/test/pf-badge.e2e.ts index 4a2d5498f2..d2723a0326 100644 --- a/elements/pf-badge/test/pf-badge.e2e.ts +++ b/elements/pf-badge/test/pf-badge.e2e.ts @@ -1,5 +1,6 @@ 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-badge'; @@ -9,4 +10,16 @@ test.describe(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-banner/test/pf-banner.e2e.ts b/elements/pf-banner/test/pf-banner.e2e.ts index 03506dda94..e45c6deb82 100644 --- a/elements/pf-banner/test/pf-banner.e2e.ts +++ b/elements/pf-banner/test/pf-banner.e2e.ts @@ -1,5 +1,6 @@ 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-banner'; @@ -9,4 +10,16 @@ test.describe(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-button/test/pf-button.e2e.ts b/elements/pf-button/test/pf-button.e2e.ts index d139085abd..5072a7e6d7 100644 --- a/elements/pf-button/test/pf-button.e2e.ts +++ b/elements/pf-button/test/pf-button.e2e.ts @@ -1,5 +1,6 @@ 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-button'; @@ -9,4 +10,16 @@ test.describe(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-card/test/pf-card.e2e.ts b/elements/pf-card/test/pf-card.e2e.ts index 0138284760..0d4aac7d7f 100644 --- a/elements/pf-card/test/pf-card.e2e.ts +++ b/elements/pf-card/test/pf-card.e2e.ts @@ -1,5 +1,6 @@ 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-card'; @@ -9,4 +10,16 @@ test.describe(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-chip/test/pf-chip.e2e.ts b/elements/pf-chip/test/pf-chip.e2e.ts index fb26e51cc2..77e5e203e5 100644 --- a/elements/pf-chip/test/pf-chip.e2e.ts +++ b/elements/pf-chip/test/pf-chip.e2e.ts @@ -1,5 +1,6 @@ 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-chip'; @@ -9,4 +10,16 @@ test.describe(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-clipboard-copy/test/pf-clipboard-copy.e2e.ts b/elements/pf-clipboard-copy/test/pf-clipboard-copy.e2e.ts index d7e145eab2..0d2f1f8da4 100644 --- a/elements/pf-clipboard-copy/test/pf-clipboard-copy.e2e.ts +++ b/elements/pf-clipboard-copy/test/pf-clipboard-copy.e2e.ts @@ -1,5 +1,6 @@ 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-clipboard-copy'; @@ -9,4 +10,16 @@ test.describe(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-code-block/pf-code-block.ts b/elements/pf-code-block/pf-code-block.ts index 4f2d2482c8..d5e920e7eb 100644 --- a/elements/pf-code-block/pf-code-block.ts +++ b/elements/pf-code-block/pf-code-block.ts @@ -40,11 +40,11 @@ export class PfCodeBlock extends LitElement { @property({ type: Boolean, reflect: true }) expanded = false; get #expandedContent(): string { - return this.querySelector('script[data-expand]')?.textContent ?? ''; + return this.querySelector?.('script[data-expand]')?.textContent ?? ''; } get #content() { - const script = this.querySelector('script[type]'); + const script = this.querySelector?.('script[type]'); if (script?.type !== 'text/javascript-sample' && !!script?.type.match(/(j(ava)?|ecma|live)script/)) { return ''; diff --git a/elements/pf-code-block/test/pf-code-block.e2e.ts b/elements/pf-code-block/test/pf-code-block.e2e.ts index 692e8f317c..5df986cfe4 100644 --- a/elements/pf-code-block/test/pf-code-block.e2e.ts +++ b/elements/pf-code-block/test/pf-code-block.e2e.ts @@ -1,5 +1,6 @@ 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-code-block'; @@ -9,4 +10,16 @@ test.describe(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-dropdown/test/pf-dropdown.e2e.ts b/elements/pf-dropdown/test/pf-dropdown.e2e.ts index 0bd90ef8b7..97435b0d68 100644 --- a/elements/pf-dropdown/test/pf-dropdown.e2e.ts +++ b/elements/pf-dropdown/test/pf-dropdown.e2e.ts @@ -1,5 +1,6 @@ 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-dropdown'; @@ -9,4 +10,16 @@ test.describe(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-icon/pf-icon.ts b/elements/pf-icon/pf-icon.ts index f82646adce..64943bc3c5 100644 --- a/elements/pf-icon/pf-icon.ts +++ b/elements/pf-icon/pf-icon.ts @@ -1,4 +1,4 @@ -import { LitElement, html, type PropertyValues } from 'lit'; +import { LitElement, html, isServer, type PropertyValues } from 'lit'; import { customElement } from 'lit/decorators/custom-element.js'; import { property } from 'lit/decorators/property.js'; import { state } from 'lit/decorators/state.js'; @@ -13,7 +13,10 @@ export type IconResolverFunction = (set: string, icon: string) => Renderable | Promise; /** requestIdleCallback when available, requestAnimationFrame when not */ -const ric = window.requestIdleCallback ?? window.requestAnimationFrame; +const ric: typeof globalThis.requestIdleCallback = + globalThis.requestIdleCallback + ?? globalThis.requestAnimationFrame + ?? (async (f: () => void) => Promise.resolve().then(f)); /** Fired when an icon fails to load */ export class IconResolveError extends ErrorEvent { @@ -212,9 +215,8 @@ export class PfIcon extends LitElement { this.content = await resolver(set, icon); this.#contentChanged(); } catch (error: unknown) { - const event = new IconResolveError(set, icon, error as Error); this.#logger.error((error as IconResolveError).message); - this.dispatchEvent(event); + this.dispatchEvent(new IconResolveError(set, icon, error as Error)); } } } diff --git a/elements/pf-icon/test/pf-icon.e2e.ts b/elements/pf-icon/test/pf-icon.e2e.ts index 3a8111887e..54a8cca6c8 100644 --- a/elements/pf-icon/test/pf-icon.e2e.ts +++ b/elements/pf-icon/test/pf-icon.e2e.ts @@ -1,5 +1,6 @@ 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-icon'; @@ -9,4 +10,16 @@ test.describe(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-jump-links/pf-jump-links.ts b/elements/pf-jump-links/pf-jump-links.ts index 83725a956f..f352ff1031 100644 --- a/elements/pf-jump-links/pf-jump-links.ts +++ b/elements/pf-jump-links/pf-jump-links.ts @@ -75,7 +75,7 @@ export class PfJumpLinks extends LitElement { /** Label to add to nav element. */ @property() label?: string; - #kids = this.querySelectorAll(':is(pf-jump-links-item, pf-jump-links-list)'); + #kids = this.querySelectorAll?.(':is(pf-jump-links-item, pf-jump-links-list)'); #tabindex?: RovingTabindexController; @@ -101,13 +101,13 @@ export class PfJumpLinks extends LitElement { getItems: () => { const items = Array.from(this.#kids) .flatMap(i => [ - ...i.shadowRoot?.querySelectorAll('a') ?? [], - ...i.querySelectorAll('a') ?? [], + ...i.shadowRoot?.querySelectorAll?.('a') ?? [], + ...i.querySelectorAll?.('a') ?? [], ]); return items; }, }); - const active = this.querySelector('pf-jump-links-item[active]'); + const active = this.querySelector?.('pf-jump-links-item[active]'); if (active) { this.#setActiveItem(active); } @@ -150,7 +150,7 @@ export class PfJumpLinks extends LitElement { } #setActiveItem(item: PfJumpLinksItem) { - this.#tabindex?.setActiveItem(item.shadowRoot?.querySelector('a') ?? undefined); + this.#tabindex?.setActiveItem(item.shadowRoot?.querySelector?.('a') ?? undefined); this.#spy.setActive(item); } diff --git a/elements/pf-jump-links/test/pf-jump-links.e2e.ts b/elements/pf-jump-links/test/pf-jump-links.e2e.ts index 4b25956d0c..739c64c4e8 100644 --- a/elements/pf-jump-links/test/pf-jump-links.e2e.ts +++ b/elements/pf-jump-links/test/pf-jump-links.e2e.ts @@ -1,5 +1,6 @@ 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-jump-links'; @@ -9,4 +10,16 @@ test.describe(tagName, () => { await componentPage.navigate({ selector: 'pf-jump-links-nav' }); 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-label/test/pf-label.e2e.ts b/elements/pf-label/test/pf-label.e2e.ts index 0e9c8fd44a..b0e512b6dd 100644 --- a/elements/pf-label/test/pf-label.e2e.ts +++ b/elements/pf-label/test/pf-label.e2e.ts @@ -1,5 +1,6 @@ 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-label'; @@ -9,4 +10,16 @@ test.describe(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-modal/test/pf-modal.e2e.ts b/elements/pf-modal/test/pf-modal.e2e.ts index 715f5097cc..391383f3cf 100644 --- a/elements/pf-modal/test/pf-modal.e2e.ts +++ b/elements/pf-modal/test/pf-modal.e2e.ts @@ -1,5 +1,6 @@ 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-modal'; @@ -13,4 +14,16 @@ test.describe(tagName, () => { 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-panel/test/pf-panel.e2e.ts b/elements/pf-panel/test/pf-panel.e2e.ts index 3d15e31a82..3b1afc3428 100644 --- a/elements/pf-panel/test/pf-panel.e2e.ts +++ b/elements/pf-panel/test/pf-panel.e2e.ts @@ -1,5 +1,6 @@ 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-panel'; @@ -9,4 +10,16 @@ test.describe(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-popover/pf-popover.ts b/elements/pf-popover/pf-popover.ts index c739d4410b..0a1b22684d 100644 --- a/elements/pf-popover/pf-popover.ts +++ b/elements/pf-popover/pf-popover.ts @@ -1,4 +1,6 @@ -import { LitElement, nothing, html, type PropertyValues } from 'lit'; +import type { Placement } from '@patternfly/pfe-core/controllers/floating-dom-controller.js'; + +import { LitElement, nothing, html, type PropertyValues, isServer } from 'lit'; import { customElement } from 'lit/decorators/custom-element.js'; import { property } from 'lit/decorators/property.js'; import { query } from 'lit/decorators/query.js'; @@ -7,12 +9,13 @@ import { classMap } from 'lit/directives/class-map.js'; import { ifDefined } from 'lit/directives/if-defined.js'; import { FloatingDOMController } from '@patternfly/pfe-core/controllers/floating-dom-controller.js'; import { SlotController } from '@patternfly/pfe-core/controllers/slot-controller.js'; +import { deprecation } from '@patternfly/pfe-core/decorators/deprecation.js'; import { bound } from '@patternfly/pfe-core/decorators/bound.js'; import { ComposedEvent, StringListConverter } from '@patternfly/pfe-core/core.js'; -import type { Placement } from '@patternfly/pfe-core/controllers/floating-dom-controller.js'; + import '@patternfly/elements/pf-button/pf-button.js'; + import styles from './pf-popover.css'; -import { deprecation } from '@patternfly/pfe-core/decorators/deprecation.js'; const headingLevels = [2, 3, 4, 5, 6] as const; @@ -189,7 +192,7 @@ export class PfPopover extends LitElement { } satisfies Record) as [AlertSeverity, string][]); static { - document.addEventListener('click', function(event) { + !isServer && document.addEventListener('click', function(event) { for (const instance of PfPopover.instances) { if (!instance.noOutsideClick) { instance.#outsideClick(event); @@ -330,7 +333,7 @@ export class PfPopover extends LitElement { constructor() { super(); - this.addEventListener('keydown', this.#onKeydown); + !isServer && this.addEventListener('keydown', this.#onKeydown); } render() { @@ -412,8 +415,11 @@ export class PfPopover extends LitElement { } #getReferenceTrigger() { - const root = this.getRootNode() as Document | ShadowRoot; - return !this.trigger ? null : root.getElementById(this.trigger); + if (isServer || !this.trigger) { + return null; + } else { + return (this.getRootNode() as Document | ShadowRoot).getElementById(this.trigger); + } } #triggerChanged() { diff --git a/elements/pf-popover/test/pf-popover.e2e.ts b/elements/pf-popover/test/pf-popover.e2e.ts index 1c1c65c44b..78709c9997 100644 --- a/elements/pf-popover/test/pf-popover.e2e.ts +++ b/elements/pf-popover/test/pf-popover.e2e.ts @@ -1,5 +1,6 @@ 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-popover'; @@ -9,4 +10,16 @@ test.describe(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-progress-stepper/pf-progress-step.ts b/elements/pf-progress-stepper/pf-progress-step.ts index d2c97189f6..c6cfc5f73e 100644 --- a/elements/pf-progress-stepper/pf-progress-step.ts +++ b/elements/pf-progress-stepper/pf-progress-step.ts @@ -57,7 +57,7 @@ export class PfProgressStep extends LitElement { const icon = this.icon ?? ICONS.get(this.variant ?? 'default')?.icon; const set = this.iconSet ?? ICONS.get(this.variant ?? 'default')?.set; const { parentTagName } = (this.constructor as typeof PfProgressStep); - const { compact = false } = this.closest(parentTagName) ?? {}; + const { compact = false } = this.closest?.(parentTagName) ?? {}; return html`
@@ -74,7 +74,6 @@ export class PfProgressStep extends LitElement { } updated(changed: PropertyValues) { - super.updated?.(changed); if (changed.has('current')) { this.#internals.ariaCurrent = String(!!this.current); } diff --git a/elements/pf-progress-stepper/pf-progress-stepper.ts b/elements/pf-progress-stepper/pf-progress-stepper.ts index 29331e5c12..8333d3a072 100644 --- a/elements/pf-progress-stepper/pf-progress-stepper.ts +++ b/elements/pf-progress-stepper/pf-progress-stepper.ts @@ -1,4 +1,4 @@ -import { LitElement, html } from 'lit'; +import { LitElement, html, type PropertyValues } from 'lit'; import { customElement } from 'lit/decorators/custom-element.js'; import { property } from 'lit/decorators/property.js'; import { observed } from '@patternfly/pfe-core/decorators/observed.js'; @@ -28,9 +28,6 @@ export class PfProgressStepper extends LitElement { @property({ type: Boolean, reflect: true }) center = false; /** Whether to use the compact layout */ - @observed(function(this: PfProgressStepper) { - this.querySelectorAll('pf-progress-step').forEach(step => step.requestUpdate()); - }) @property({ type: Boolean, reflect: true }) compact = false; #internals = InternalsController.of(this, { @@ -42,8 +39,8 @@ export class PfProgressStepper extends LitElement { get value() { const { childTagName } = (this.constructor as typeof PfProgressStepper); - const steps = this.querySelectorAll(childTagName); - const current = this.querySelector(`${childTagName}[current]`); + const steps = this.querySelectorAll?.(childTagName) ?? []; + const current = this.querySelector?.(`${childTagName}[current]`); const n = Array.from(steps).indexOf(current as PfProgressStep) + 1; return (n / steps.length) * 100; } @@ -62,6 +59,12 @@ export class PfProgressStepper extends LitElement { // eslint-disable-next-line lit-a11y/accessible-name return html`
`; } + + updated(changed: PropertyValues) { + if (changed.has('compact')) { + this.querySelectorAll?.('pf-progress-step').forEach(step => step.requestUpdate()); + } + } } declare global { diff --git a/elements/pf-progress-stepper/test/pf-progress-stepper.e2e.ts b/elements/pf-progress-stepper/test/pf-progress-stepper.e2e.ts index baebac6716..c218f62249 100644 --- a/elements/pf-progress-stepper/test/pf-progress-stepper.e2e.ts +++ b/elements/pf-progress-stepper/test/pf-progress-stepper.e2e.ts @@ -1,5 +1,6 @@ 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-progress-stepper'; @@ -9,4 +10,16 @@ test.describe(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-progress/test/pf-progress.e2e.ts b/elements/pf-progress/test/pf-progress.e2e.ts index e19cba99d9..2da4cdda68 100644 --- a/elements/pf-progress/test/pf-progress.e2e.ts +++ b/elements/pf-progress/test/pf-progress.e2e.ts @@ -1,5 +1,6 @@ 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-progress'; @@ -9,4 +10,16 @@ test.describe(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-select/pf-select.ts b/elements/pf-select/pf-select.ts index 8b1a265ab2..076a8e0df9 100644 --- a/elements/pf-select/pf-select.ts +++ b/elements/pf-select/pf-select.ts @@ -1,6 +1,6 @@ import type { PfChipRemoveEvent } from '@patternfly/elements/pf-chip/pf-chip.js'; -import { LitElement, html, type PropertyValues } from 'lit'; +import { LitElement, html, isServer, type PropertyValues } from 'lit'; import { customElement } from 'lit/decorators/custom-element.js'; import { property } from 'lit/decorators/property.js'; import { query } from 'lit/decorators/query.js'; @@ -144,12 +144,16 @@ export class PfSelect extends LitElement { * array of slotted options */ get options(): PfOption[] { - const opts = Array.from(this.querySelectorAll('pf-option')); - const placeholder = this.shadowRoot?.getElementById('placeholder') as PfOption | null; - if (placeholder) { - return [placeholder, ...opts]; + if (isServer) { + return []; // TODO: expose a DOM property to allow setting options in SSR scenarios } else { - return opts; + const opts = Array.from(this.querySelectorAll('pf-option')); + const placeholder = this.shadowRoot?.getElementById('placeholder') as PfOption | null; + if (placeholder) { + return [placeholder, ...opts]; + } else { + return opts; + } } } @@ -216,7 +220,7 @@ export class PfSelect extends LitElement { const { disabled, expanded, variant } = this; const { anchor = 'bottom', alignment = 'start', styles = {} } = this.#float; const { computedLabelText } = this.#internals; - const { height, width } = this.getBoundingClientRect() || {}; + const { height, width } = this.getBoundingClientRect?.() || {}; const buttonLabel = this.#buttonLabel; const hasBadge = this.#hasBadge; const selectedOptions = this.#listbox?.selectedOptions ?? []; @@ -458,7 +462,7 @@ export class PfSelect extends LitElement { #computePlaceholderText() { return this.placeholder - || this.querySelector('[slot=placeholder]') + || this.querySelector?.('[slot=placeholder]') ?.assignedNodes() ?.reduce((acc, node) => `${acc}${node.textContent}`, '')?.trim() || this.#listbox?.options diff --git a/elements/pf-select/test/pf-select.e2e.ts b/elements/pf-select/test/pf-select.e2e.ts index 4bbc30693f..b537e0eff7 100644 --- a/elements/pf-select/test/pf-select.e2e.ts +++ b/elements/pf-select/test/pf-select.e2e.ts @@ -1,5 +1,6 @@ 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-select'; @@ -9,4 +10,16 @@ test.describe(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-spinner/test/pf-spinner.e2e.ts b/elements/pf-spinner/test/pf-spinner.e2e.ts index 6dcfc50a34..e034c6cc06 100644 --- a/elements/pf-spinner/test/pf-spinner.e2e.ts +++ b/elements/pf-spinner/test/pf-spinner.e2e.ts @@ -1,5 +1,6 @@ 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-spinner'; @@ -9,4 +10,16 @@ test.describe(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-switch/test/pf-switch.e2e.ts b/elements/pf-switch/test/pf-switch.e2e.ts index b62ba1c65c..e06cc70125 100644 --- a/elements/pf-switch/test/pf-switch.e2e.ts +++ b/elements/pf-switch/test/pf-switch.e2e.ts @@ -1,5 +1,6 @@ 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-switch'; @@ -9,4 +10,16 @@ test.describe(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-table/pf-table.ts b/elements/pf-table/pf-table.ts index 84be0cd5d1..0e07d9c47c 100644 --- a/elements/pf-table/pf-table.ts +++ b/elements/pf-table/pf-table.ts @@ -666,7 +666,7 @@ export class PfTable extends LitElement { static readonly styles = [styles]; get rows() { - return this.querySelectorAll(rowQuery); + return this.querySelectorAll?.(rowQuery); } @state() private columns = 0; @@ -678,7 +678,7 @@ export class PfTable extends LitElement { } render() { - const hasExpandableRow = !!this.querySelector('pf-tr[expandable]'); + const hasExpandableRow = !!this.querySelector?.('pf-tr[expandable]'); const coeffRows = hasExpandableRow ? '1' : '0'; return html` { 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-tabs/test/pf-tabs.e2e.ts b/elements/pf-tabs/test/pf-tabs.e2e.ts index 27b9224c52..1f37415eb2 100644 --- a/elements/pf-tabs/test/pf-tabs.e2e.ts +++ b/elements/pf-tabs/test/pf-tabs.e2e.ts @@ -1,5 +1,6 @@ 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-tabs'; @@ -11,4 +12,16 @@ test.describe(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-text-area/pf-text-area.ts b/elements/pf-text-area/pf-text-area.ts index c39e160cb0..04dde1eb0b 100644 --- a/elements/pf-text-area/pf-text-area.ts +++ b/elements/pf-text-area/pf-text-area.ts @@ -1,4 +1,4 @@ -import { LitElement, html } from 'lit'; +import { LitElement, html, isServer } 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'; @@ -186,6 +186,10 @@ export class PfTextArea extends LitElement { #derivedLabel = ''; + get #disabled() { + return (!isServer && this.matches(':disabled')) || this.disabled; + } + get #input() { return this.shadowRoot?.getElementById('textarea') as HTMLTextAreaElement ?? null; } @@ -201,7 +205,7 @@ export class PfTextArea extends LitElement {