diff --git a/.changeset/clever-places-happen.md b/.changeset/clever-places-happen.md new file mode 100644 index 0000000000..008556e53f --- /dev/null +++ b/.changeset/clever-places-happen.md @@ -0,0 +1,8 @@ +--- +"@patternfly/eslint-config": patch +"@patternfly/create-element": patch +"@patternfly/pfe-core": patch +"@patternfly/pfe-tools": patch +"@patternfly/elements": patch +--- +updated dependencies diff --git a/.changeset/clever-yaks-thank.md b/.changeset/clever-yaks-thank.md new file mode 100644 index 0000000000..b10af4b792 --- /dev/null +++ b/.changeset/clever-yaks-thank.md @@ -0,0 +1,4 @@ +--- +"@patternfly/elements": minor +--- +``: added `title` slot, for when the title is not inline with any slotted header actions diff --git a/.changeset/dirty-bears-win.md b/.changeset/dirty-bears-win.md new file mode 100644 index 0000000000..fe7f079524 --- /dev/null +++ b/.changeset/dirty-bears-win.md @@ -0,0 +1,34 @@ +--- +"@patternfly/elements": major +--- +``: removed the `getIconUrl` static method, and replaced it with the +`resolve` static method + +The steps for overriding icon loading behaviour have changed. Before, you had to +return a string from the `getIconUrl` method, or the second argument to +`addIconSet`. Now, both of those functions must return a Node, or any lit-html +renderable value, or a Promise thereof. + +BEFORE: + +```js +PfIcon.addIconSet('local', (set, icon) => + new URL(`/assets/icons/${set}-${icon}.js`)); + +// or +PfIcon.getIconUrl = (set, icon) => + new URL(`/assets/icons/${set}-${icon}.js`)) +``` + +AFTER +```js +PfIcon.addIconSet('local', (set, icon) => + import(`/assets/icons/${set}-${icon}.js`)) + .then(mod => mod.default); + +// or +PfIcon.resolve = (set, icon) => + import(`/assets/icons/${set}-${icon}.js`)) + .then(mod => mod.default); +``` + 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/free-ideas-fry.md b/.changeset/free-ideas-fry.md new file mode 100644 index 0000000000..f73d88cf6f --- /dev/null +++ b/.changeset/free-ideas-fry.md @@ -0,0 +1,4 @@ +--- +"@patternfly/pfe-core": patch +--- +`InternalsController`: corrected the types for aria IDL list attributes diff --git a/.changeset/fresh-shrimps-work.md b/.changeset/fresh-shrimps-work.md new file mode 100644 index 0000000000..0e550d4716 --- /dev/null +++ b/.changeset/fresh-shrimps-work.md @@ -0,0 +1,4 @@ +--- +"@patternfly/elements": major +--- +``: removed the `defaultIconSet` static field. diff --git a/.changeset/heavy-peas-appear.md b/.changeset/heavy-peas-appear.md new file mode 100644 index 0000000000..0df3aec13c --- /dev/null +++ b/.changeset/heavy-peas-appear.md @@ -0,0 +1,18 @@ +--- +"@patternfly/pfe-tools": minor +--- +Added `querySnapshot` accessibility testing helper + +```ts + +describe('then clicking the toggle', function() { + beforeEach(async function() { + await clickElementAtCenter(toggle); + }); + it('expands the disclosure panel', async function() { + const snapshot = await a11ySnapshot(); + const expanded = querySnapshot(snapshot, { expanded: true }); + expect(expanded).to.be.ok; + }); +}); +``` diff --git a/.changeset/hip-coins-prove.md b/.changeset/hip-coins-prove.md new file mode 100644 index 0000000000..861aa6a252 --- /dev/null +++ b/.changeset/hip-coins-prove.md @@ -0,0 +1,32 @@ +--- +"@patternfly/elements": major +--- +``: Removed `BaseAccordion*` classes, as well as static `isPanel`, `isHeader`, and `isAccordion` methods. Removed the optional `parentAccordion` parameter to `PfAccordion#expand(index)`. Renamed accordion event classes by adding the `Pf` prefix: + +**Before**: + +```js +import { + AccordionHeaderChangeEvent +} from '@patternfly/elements/pf-accordion/pf-accordion.js'; + +addEventListener('change', function(event) { + if (event instanceof AccordionHeaderChangeEvent) { + // ... + } +}); +``` + +**After**: + +```js +import { + PfAccordionHeaderChangeEvent +} from '@patternfly/elements/pf-accordion/pf-accordion.js'; + +addEventListener('change', function(event) { + if (event instanceof PfAccordionHeaderChangeEvent) { + // ... + } +}); +``` diff --git a/.changeset/khaki-regions-play.md b/.changeset/khaki-regions-play.md new file mode 100644 index 0000000000..6f278a03a6 --- /dev/null +++ b/.changeset/khaki-regions-play.md @@ -0,0 +1,4 @@ +--- +"@patternfly/elements": major +--- +``: removed svg files, use `@patternfly/icons` instead diff --git a/.changeset/label-close-remove.md b/.changeset/label-close-remove.md new file mode 100644 index 0000000000..063b1d7797 --- /dev/null +++ b/.changeset/label-close-remove.md @@ -0,0 +1,17 @@ +--- +"@patternfly/elements": major +--- +``: when clicking close button, `close` event is fired. +Now, if that event is not cancelled, the label will remove itself from the document. + +To restore previous behaviour: + +```js +import { LabelCloseEvent } from '@patternfly/elements/pf-label/pf-label.js'; +label.addEventListener('close', function(event) { + if (event instanceof LabelCloseEvent) { + event.preventDefault(); + return false; + } +}); +``` 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/remove-base-clipboard-copy.md b/.changeset/remove-base-clipboard-copy.md new file mode 100644 index 0000000000..f121291d7d --- /dev/null +++ b/.changeset/remove-base-clipboard-copy.md @@ -0,0 +1,35 @@ +--- +"@patternfly/elements": major +--- +``: Removed `BaseClipboardCopy` class. +Reimplement (recommended) or extend `PfClipboardCopy`. +Renames `AvatarLoadEvent` to `PfAvatarLoadEvent` and moves it to `pf-avatar.js`. + +**Before**: + +```js +import { + ClipboardCopyCopiedEvent +} from '@patternfly/elements/pf-clipboard-copy/BaseClipboardCopy.js'; + +addEventListener('copy', function(event) { + if (event instanceof ClipboardCopyCopiedEvent) { + // ... + } +}); +``` + +**After**: + +```js +import { + PfClipboardCopyCopiedEvent +} from '@patternfly/elements/pf-clipboard-copy/pf-clipboard-copy.js'; + +addEventListener('copy', function(event) { + if (event instanceof PfClipboardCopyCopiedEvent) { + // ... + } +}); +``` + diff --git a/.changeset/remove-base-icon.md b/.changeset/remove-base-icon.md new file mode 100644 index 0000000000..4ec3813241 --- /dev/null +++ b/.changeset/remove-base-icon.md @@ -0,0 +1,4 @@ +--- +"@patternfly/elements": major +--- +``: Removed `BaseIcon` class. Reimplement (recommended) or extend `PfIcon`. diff --git a/.changeset/remove-base-label.md b/.changeset/remove-base-label.md new file mode 100644 index 0000000000..bc79ff2729 --- /dev/null +++ b/.changeset/remove-base-label.md @@ -0,0 +1,4 @@ +--- +"@patternfly/elements": major +--- +``: Removed `BaseLabel` class. Reimplement (recommended) or extend `PfLabel`. diff --git a/.changeset/remove-base-switch.md b/.changeset/remove-base-switch.md new file mode 100644 index 0000000000..3cf68ab755 --- /dev/null +++ b/.changeset/remove-base-switch.md @@ -0,0 +1,4 @@ +--- +"@patternfly/elements": major +--- +``: Removed `BaseSwitch` class. Reimplement (recommended) or extend `PfSwitch`. diff --git a/.changeset/remove-baseavatar.md b/.changeset/remove-baseavatar.md new file mode 100644 index 0000000000..b489d1c261 --- /dev/null +++ b/.changeset/remove-baseavatar.md @@ -0,0 +1,29 @@ +--- +"@patternfly/elements": major +--- +``: Removed `BaseAvatar` class. Reimplement (recommended) or extend `PfAvatar`. +Renames `AvatarLoadEvent` to `PfAvatarLoadEvent` and moves it to `pf-avatar.js`. + +**Before**: + +```js +import { AvatarLoadEvent } from '@patternfly/elements/pf-avatar/BaseAvatar.js'; + +addEventListener('load', function(event) { + if (event instanceof AvatarLoadEvent) { + // ... + } +}); +``` + +**After**: + +```js +import { PfAvatarLoadEvent } from '@patternfly/elements/pf-avatar/pf-avatar.js'; + +addEventListener('load', function(event) { + if (event instanceof PfAvatarLoadEvent) { + // ... + } +}); +``` diff --git a/.changeset/remove-basebadge.md b/.changeset/remove-basebadge.md new file mode 100644 index 0000000000..9aa1f1c36b --- /dev/null +++ b/.changeset/remove-basebadge.md @@ -0,0 +1,4 @@ +--- +"@patternfly/elements": major +--- +``: Removed `BaseBadge` class. Reimplement (recommended) or extend `PfBadge`. diff --git a/.changeset/remove-basebutton.md b/.changeset/remove-basebutton.md new file mode 100644 index 0000000000..f674ea21a6 --- /dev/null +++ b/.changeset/remove-basebutton.md @@ -0,0 +1,4 @@ +--- +"@patternfly/elements": major +--- +``: Removed `BaseButton` class. Reimplement (recommended) or extend `PfButton`. diff --git a/.changeset/remove-basecodeblock.md b/.changeset/remove-basecodeblock.md new file mode 100644 index 0000000000..f135bcf8c1 --- /dev/null +++ b/.changeset/remove-basecodeblock.md @@ -0,0 +1,4 @@ +--- +"@patternfly/elements": major +--- +``: Removed `BaseCodeBlock` class. Reimplement (recommended) or extend `PfCodeBlock`. diff --git a/.changeset/remove-basespinner.md b/.changeset/remove-basespinner.md new file mode 100644 index 0000000000..5b4a325b4e --- /dev/null +++ b/.changeset/remove-basespinner.md @@ -0,0 +1,4 @@ +--- +"@patternfly/elements": major +--- +``: Removed `BaseSpinner` class. Reimplement (recommended) or extend `PfSpinner`. diff --git a/.changeset/remove-basetabs.md b/.changeset/remove-basetabs.md new file mode 100644 index 0000000000..36215b7f53 --- /dev/null +++ b/.changeset/remove-basetabs.md @@ -0,0 +1,5 @@ +--- +"@patternfly/elements": major +--- +``: Remove `BaseTabs`. Use `TabsAriaController`, etc. to reimplement +your elements which extend it, or extend from `PfTabs` instead. diff --git a/.changeset/remove-basetile.md b/.changeset/remove-basetile.md new file mode 100644 index 0000000000..cb6f2713cb --- /dev/null +++ b/.changeset/remove-basetile.md @@ -0,0 +1,4 @@ +--- +"@patternfly/elements": major +--- +``: Removed `BaseTile` class. Reimplement (recommended) or extend `PfTile`. diff --git a/.changeset/remove-basetooltip.md b/.changeset/remove-basetooltip.md new file mode 100644 index 0000000000..e60835d22f --- /dev/null +++ b/.changeset/remove-basetooltip.md @@ -0,0 +1,4 @@ +--- +"@patternfly/elements": major +--- +``: Removed `BaseTooltip` class. Reimplement (recommended) or extend `PfTooltip`. diff --git a/.changeset/sharp-spiders-float.md b/.changeset/sharp-spiders-float.md new file mode 100644 index 0000000000..373def2e63 --- /dev/null +++ b/.changeset/sharp-spiders-float.md @@ -0,0 +1,5 @@ +--- +"@patternfly/elements": major +--- + +``: Removes `BaseCard` base class. If your project extends `BaseCard`, we recommend extending `LitElement` instead and re-implementing card's properties. Alternately, extend from `PfCard`. diff --git a/.changeset/slick-bats-brake.md b/.changeset/slick-bats-brake.md new file mode 100644 index 0000000000..6e09899203 --- /dev/null +++ b/.changeset/slick-bats-brake.md @@ -0,0 +1,12 @@ +--- +"@patternfly/pfe-tools": minor +--- +**TypeScript**: Add static version transformer. This adds a runtime-only +static `version` field to custom element classes. + +```js +import '@patternfly/elements/pf-button/pf-button.js'; +const PFE_VERSION = + await customElements.whenDefined('pf-button') + .then(PfButton => PfButton.version); +``` 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/.changeset/weak-turtles-act.md b/.changeset/weak-turtles-act.md new file mode 100644 index 0000000000..80deca10d9 --- /dev/null +++ b/.changeset/weak-turtles-act.md @@ -0,0 +1,5 @@ +--- +"@patternfly/pfe-tools": patch +--- +**Test Runner Config**: import the production version of Lit for tests, reducing +console chatter during test runs diff --git a/.changeset/wide-guests-speak.md b/.changeset/wide-guests-speak.md new file mode 100644 index 0000000000..bbda4244f3 --- /dev/null +++ b/.changeset/wide-guests-speak.md @@ -0,0 +1,4 @@ +--- +"@patternfly/eslint-config-elements": major +--- +Require `@typescript-eslint` ^8.0.0 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 6761cfe594..507f1ec8ce 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.45.1-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/@types/colorjs.io/index.d.ts b/@types/colorjs.io/index.d.ts deleted file mode 100644 index c23d572828..0000000000 --- a/@types/colorjs.io/index.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -declare module 'colorjs.io' { - - export default class Color { - constructor(str: string); - toString(options?: { format: 'hex'|'rgb'|'rgba'|'hsl'|'hsla' }): string; - } - -} diff --git a/core/pfe-core/controllers/cascade-controller.ts b/core/pfe-core/controllers/cascade-controller.ts index c7a285c033..bde105cb07 100644 --- a/core/pfe-core/controllers/cascade-controller.ts +++ b/core/pfe-core/controllers/cascade-controller.ts @@ -5,7 +5,7 @@ import { debounce } from '../functions/debounce.js'; import { Logger } from './logger.js'; /** - * @deprecated: use context, especially via `@patternfly/pfe-core/functions/context.js`; + * @deprecated use context, especially via `@patternfly/pfe-core/functions/context.js`; */ export interface Options { properties: Partial>; @@ -13,20 +13,21 @@ export interface Options { } /** - * @deprecated: use context, especially via `@patternfly/pfe-core/functions/context.js`; + * @deprecated use context, especially via `@patternfly/pfe-core/functions/context.js`; */ export class CascadeController implements ReactiveController { private class: typeof ReactiveElement; private logger: Logger; - static instances = new WeakMap>(); + static instances: WeakMap> = + new WeakMap>(); - mo = new MutationObserver(this.parse); + mo: MutationObserver = new MutationObserver(this.parse); - cache = new Map(); + cache: Map = new Map(); - constructor(public host: E, public options?: Options) { + constructor(public host: E, public options?: Options | undefined) { this.class = host.constructor as typeof ReactiveElement; this.logger = new Logger(this.host); CascadeController.instances.set(host, this); @@ -38,24 +39,25 @@ export class CascadeController implements ReactiveCon this.cascadeProperties = debounce(this.cascadeProperties, 1); } - hostUpdated() { + hostUpdated(): void { this.cascadeProperties(); } - hostConnected() { + hostConnected(): void { this.mo.observe(this.host, { attributes: true, childList: true }); this.cascadeProperties(); } - hostDisconnected() { + hostDisconnected(): void { this.mo.disconnect(); } /** * Handles the cascading of properties to nested components when new elements are added * Attribute updates/additions are handled by the attribute callback + * @param [nodeList=this.host.children] */ - cascadeProperties(nodeList: HTMLCollection | NodeList = this.host.children) { + cascadeProperties(nodeList: HTMLCollection | NodeList = this.host.children): void { if (this.host.isConnected) { const selectors = this.cache.keys(); @@ -89,8 +91,10 @@ export class CascadeController implements ReactiveCon * Gets the configured attribute name for the decorated property, * falling back to the lowercased property name, and caches the attribute name * with it's designated child selectors for value-propagation on change + * @param propName + * @param cascade */ - initProp(propName: string, cascade: string | string[]) { + initProp(propName: string, cascade: string | string[]): void { for (const nodeItem of [cascade].flat(Infinity).filter(Boolean) as string[]) { const { attribute } = this.class.getPropertyOptions(propName); @@ -122,6 +126,8 @@ export class CascadeController implements ReactiveCon /** * Copy the named attribute to a target element. + * @param name attr name + * @param el element */ private async _copyAttribute(name: string, el: Element) { this.logger.log(`copying ${name} to ${el}`); diff --git a/core/pfe-core/controllers/css-variable-controller.ts b/core/pfe-core/controllers/css-variable-controller.ts index 4070f5a57e..ba21f4b9d8 100644 --- a/core/pfe-core/controllers/css-variable-controller.ts +++ b/core/pfe-core/controllers/css-variable-controller.ts @@ -8,10 +8,10 @@ export class CssVariableController implements ReactiveController { } private parseProperty(name: string) { - return name.substr(0, 2) !== '--' ? `--${name}` : name; + return name.substring(0, 2) !== '--' ? `--${name}` : name; } - getVariable(name: string) { + getVariable(name: string): string | null { return this.style.getPropertyValue(this.parseProperty(name)).trim() || null; } diff --git a/core/pfe-core/controllers/floating-dom-controller.ts b/core/pfe-core/controllers/floating-dom-controller.ts index be84b89fed..3bb9c7602a 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'; @@ -64,19 +64,19 @@ export class FloatingDOMController implements ReactiveController { } /** The crosswise alignment of the invoker on which to display the floating DOM */ - get alignment() { + get alignment(): Alignment { return this.#alignment ?? 'center'; } /** The side of the invoker on which to display the floating DOM */ - get anchor() { + get anchor(): Anchor { return this.#anchor ?? ''; } /** * When true, the floating DOM is visible */ - get open() { + get open(): boolean { return this.#open; } @@ -100,13 +100,13 @@ export class FloatingDOMController implements ReactiveController { ) { host.addController(this); this.#options = { - invoker: (host instanceof HTMLElement ? () => host : () => undefined), + invoker: (() => host as LitElement), shift: true, ...options, }; } - hostDisconnected() { + hostDisconnected(): void { this.#cleanup?.(); } @@ -167,8 +167,15 @@ export class FloatingDOMController implements ReactiveController { this.host.requestUpdate(); } - /** Show the floating DOM */ - async show({ offset, placement, flip, fallbackPlacements }: ShowOptions = {}) { + /** + * Show the floating DOM + * @param [options={}] + * @param options.offset + * @param options.placement + * @param options.flip + * @param options.fallbackPlacements + * */ + async show({ offset, placement, flip, fallbackPlacements }: ShowOptions = {}): Promise { const invoker = this.#invoker; const content = this.#content; if (!invoker || !content) { @@ -187,7 +194,7 @@ export class FloatingDOMController implements ReactiveController { } /** Hide the floating DOM */ - async hide() { + async hide(): Promise { await this.host.updateComplete; while (this.#opening && !this.open) { await new Promise(requestAnimationFrame); diff --git a/core/pfe-core/controllers/internals-controller.ts b/core/pfe-core/controllers/internals-controller.ts index 3ebdbd842a..1636f91320 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'); @@ -16,7 +21,11 @@ interface InternalsControllerOptions extends Partial { getHTMLElement?(): HTMLElement; } -/** reactively forward the internals object's aria mixin prototype */ +/** + * reactively forward the internals object's aria mixin prototype + * @param target + * @param key + */ function aria( target: InternalsController, key: keyof InternalsController, @@ -36,11 +45,13 @@ function aria( configurable: false, get(this: InternalsController) { // @ts-expect-error: because i'm bad, i'm bad - return this.attach()[key]; + const internals = this.attachOrRetrieveInternals(); + return internals[key]; }, set(this: InternalsController, value: string | null) { // @ts-expect-error: shamone! - this.attach()[key] = value; + const internals = this.attachOrRetrieveInternals(); + internals[key] = value; this.host.requestUpdate(); }, }); @@ -89,6 +100,8 @@ export class InternalsController implements ReactiveController, ARIAMixin { @aria ariaAtomic: string | null = null; @aria ariaAutoComplete: string | null = null; @aria ariaBusy: string | null = null; + @aria ariaBrailleLabel: string | null = null; + @aria ariaBrailleRoleDescription: string | null = null; @aria ariaChecked: string | null = null; @aria ariaColCount: string | null = null; @aria ariaColIndex: string | null = null; @@ -130,35 +143,39 @@ export class InternalsController implements ReactiveController, ARIAMixin { /** WARNING: be careful of cross-root ARIA browser support */ @aria ariaActiveDescendantElement: Element | null = null; /** WARNING: be careful of cross-root ARIA browser support */ - @aria ariaControlsElements: Element | null = null; + @aria ariaControlsElements: Element[] | null = null; /** WARNING: be careful of cross-root ARIA browser support */ - @aria ariaDescribedByElements: Element | null = null; + @aria ariaDescribedByElements: Element[] | null = null; /** WARNING: be careful of cross-root ARIA browser support */ - @aria ariaDetailsElements: Element | null = null; + @aria ariaDetailsElements: Element[] | null = null; /** WARNING: be careful of cross-root ARIA browser support */ - @aria ariaErrorMessageElements: Element | null = null; + @aria ariaErrorMessageElements: Element[] | null = null; /** WARNING: be careful of cross-root ARIA browser support */ - @aria ariaFlowToElements: Element | null = null; + @aria ariaFlowToElements: Element[] | null = null; /** WARNING: be careful of cross-root ARIA browser support */ - @aria ariaLabelledByElements: Element | null = null; + @aria ariaLabelledByElements: Element[] | null = null; /** WARNING: be careful of cross-root ARIA browser support */ - @aria ariaOwnsElements: Element | null = null; + @aria ariaOwnsElements: Element[] | null = null; /** True when the control is disabled via it's containing fieldset element */ - get formDisabled() { - return this.element?.matches(':disabled') || this._formDisabled; + get formDisabled(): boolean { + if (isServer) { + return this._formDisabled; + } else { + return this.element?.matches(':disabled') || this._formDisabled; + } } - get labels() { + get labels(): NodeList { return this.internals.labels; } - get validity() { + get validity(): ValidityState { return this.internals.validity; } /** A best-attempt based on observed behaviour in FireFox 115 on fedora 38 */ - get computedLabelText() { + get computedLabelText(): string { return this.internals.ariaLabel || Array.from(this.internals.labels as NodeListOf) .reduce((acc, label) => @@ -166,7 +183,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; @@ -185,7 +208,7 @@ export class InternalsController implements ReactiveController, ARIAMixin { `InternalsController must be instantiated with an HTMLElement or a \`getHTMLElement\` function`, ); } - this.attach(); + this.attachOrRetrieveInternals(); this.initializeOptions(options); InternalsController.instances.set(host, this); this.#polyfillDisabledPseudo(); @@ -216,7 +239,7 @@ export class InternalsController implements ReactiveController, ARIAMixin { * Because of that, `this.internals` may not be available in the decorator setter * so we cheat here with nullish coalescing assignment operator `??=`; */ - private attach() { + private attachOrRetrieveInternals() { this.internals ??= this.element!.attachInternals(); return this.internals; } @@ -234,27 +257,27 @@ export class InternalsController implements ReactiveController, ARIAMixin { hostConnected?(): void; - setFormValue(...args: Parameters) { + setFormValue(...args: Parameters): void { return this.internals.setFormValue(...args); } - setValidity(...args: Parameters) { + setValidity(...args: Parameters): void { return this.internals.setValidity(...args); } - checkValidity(...args: Parameters) { + checkValidity(...args: Parameters): boolean { return this.internals.checkValidity(...args); } - reportValidity(...args: Parameters) { + reportValidity(...args: Parameters): boolean { return this.internals.reportValidity(...args); } - submit() { + submit(): void { this.internals.form?.requestSubmit(); } - reset() { + reset(): void { this.internals.form?.reset(); } } diff --git a/core/pfe-core/controllers/light-dom-controller.ts b/core/pfe-core/controllers/light-dom-controller.ts index 3838180431..e53d11cd5c 100644 --- a/core/pfe-core/controllers/light-dom-controller.ts +++ b/core/pfe-core/controllers/light-dom-controller.ts @@ -12,14 +12,18 @@ export class LightDOMController implements ReactiveController { private logger: Logger; private initializer: () => void; - constructor(private host: ReactiveElement, initializer: () => void, private options?: Options) { + constructor( + private host: ReactiveElement, + initializer: () => void, + private options?: Options | undefined, + ) { this.initializer = initializer.bind(host); this.mo = new MutationObserver(this.initializer); this.logger = new Logger(this.host); host.addController(this); } - hostConnected() { + hostConnected(): void { if (this.hasLightDOM()) { this.initializer(); } else if (this.options?.emptyWarning) { @@ -29,7 +33,7 @@ export class LightDOMController implements ReactiveController { this.initObserver(); } - hostDisconnected() { + hostDisconnected(): void { this.mo.disconnect(); } diff --git a/core/pfe-core/controllers/listbox-controller.ts b/core/pfe-core/controllers/listbox-controller.ts index 3dc85192f1..b89efe79d9 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 @@ -33,17 +33,17 @@ let constructingAllowed = false; * ActiveDescendantController) to complete the implementation. */ export class ListboxController implements ReactiveController { - private static instances = new WeakMap>(); + private static instances = new WeakMap>(); public static of( host: ReactiveControllerHost, options: ListboxConfigOptions, ): ListboxController { constructingAllowed = true; - const instance: ListboxController = + const instance = ListboxController.instances.get(host) ?? new ListboxController(host, options); constructingAllowed = false; - return instance; + return instance as ListboxController; } private constructor( @@ -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`, ); @@ -85,27 +87,27 @@ export class ListboxController implements ReactiveCont disabled = false; /** Current active descendant in listbox */ - get activeItem() { + get activeItem(): Item | undefined { return this.options.find(option => option === this._options.a11yController.activeItem) || this._options.a11yController.firstItem; } - get nextItem() { + get nextItem(): Item | undefined { return this._options.a11yController.nextItem; } - get options() { + get options(): Item[] { return this.#items; } /** * array of options which are selected */ - get selectedOptions() { + get selectedOptions(): Item[] { return this.options.filter(option => this._options.isSelected(option)); } - get value() { + get value(): Item | Item[] { const [firstItem] = this.selectedOptions; return this._options.multi ? this.selectedOptions : firstItem; } @@ -114,7 +116,7 @@ export class ListboxController implements ReactiveCont return this._options.getHTMLElement(); } - async hostConnected() { + async hostConnected(): Promise { if (!this.#listening) { await this.host.updateComplete; this.element?.addEventListener('click', this.#onClick); @@ -125,7 +127,7 @@ export class ListboxController implements ReactiveCont } } - hostUpdated() { + hostUpdated(): void { this.element?.setAttribute('role', 'listbox'); this.element?.setAttribute('aria-disabled', String(!!this.disabled)); this.element?.setAttribute('aria-multi-selectable', String(!!this._options.multi)); @@ -138,7 +140,7 @@ export class ListboxController implements ReactiveCont } } - hostDisconnected() { + hostDisconnected(): void { this.element?.removeEventListener('click', this.#onClick); this.element?.removeEventListener('focus', this.#onFocus); this.element?.removeEventListener('keydown', this.#onKeydown); @@ -160,6 +162,7 @@ export class ListboxController implements ReactiveCont /** * handles focusing on an option: * updates roving tabindex and active descendant + * @param event focus event */ #onFocus = (event: FocusEvent) => { const target = this.#getEventOption(event); @@ -172,6 +175,7 @@ export class ListboxController implements ReactiveCont * handles clicking on a listbox option: * which selects an item by default * or toggles selection if multiselectable + * @param event click event */ #onClick = (event: MouseEvent) => { const target = this.#getEventOption(event); @@ -197,8 +201,8 @@ export class ListboxController implements ReactiveCont }; /** - * handles keyup: * track whether shift key is being used for multiselectable listbox + * @param event keyup event */ #onKeyup = (event: KeyboardEvent) => { const target = this.#getEventOption(event); @@ -213,9 +217,9 @@ export class ListboxController implements ReactiveCont }; /** - * handles keydown: * filters listbox by keyboard event when slotted option has focus, * or by external element such as a text field + * @param event keydown event */ #onKeydown = (event: KeyboardEvent) => { const target = this.#getEventOption(event); @@ -263,6 +267,7 @@ export class ListboxController implements ReactiveCont /** * handles change to options given previous options array + * @param oldOptions previous options list */ #optionsChanged(oldOptions: Item[]) { const setSize = this.#items.length; @@ -289,6 +294,9 @@ export class ListboxController implements ReactiveCont /** * updates option selections for multiselectable listbox: * toggles all options between active descendant and target + * @param currentItem item being added + * @param referenceItem item already selected. + * @param ctrlA is ctrl-a held down? */ #updateMultiselect( currentItem?: Item, @@ -320,8 +328,9 @@ export class ListboxController implements ReactiveCont /** * sets the listbox value based on selected options + * @param value item or items */ - setValue(value: Item | Item[]) { + setValue(value: Item | Item[]): void { const selected = Array.isArray(value) ? value : [value]; const [firstItem = null] = selected; for (const option of this.options) { @@ -334,8 +343,9 @@ export class ListboxController implements ReactiveCont /** * register's the host's Item elements as listbox controller items + * @param options items */ - setOptions(options: Item[]) { + setOptions(options: Item[]): void { const oldOptions = [...this.#items]; this.#items = options; this.#optionsChanged(oldOptions); diff --git a/core/pfe-core/controllers/logger.ts b/core/pfe-core/controllers/logger.ts index ec3f313a96..d7cf7e95e8 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}]`; @@ -17,9 +17,9 @@ export class Logger implements ReactiveController { * A boolean value that indicates if the logging should be printed to the console; used for debugging. * For use in a JS file or script tag; can also be added in the constructor of a component during development. * @example Logger.debugLog(true); - * @tags debug + * @param [preference=null] */ - static debugLog(preference = null) { + static debugLog(preference = null): boolean { // wrap localStorage references in a try/catch; merely referencing it can // throw errors in some locked down environments try { @@ -28,7 +28,7 @@ export class Logger implements ReactiveController { localStorage.pfeLog = !!preference; } return localStorage.pfeLog === 'true'; - } catch (e) { + } catch { return Logger.logDebug; } } @@ -38,8 +38,9 @@ export class Logger implements ReactiveController { /** * A logging wrapper which checks the debugLog boolean and prints to the console if true. * @example Logger.debug("Hello"); + * @param msgs console.log params */ - static debug(...msgs: unknown[]) { + static debug(...msgs: unknown[]): void { if (Logger.debugLog()) { console.debug(...msgs); } @@ -48,8 +49,9 @@ export class Logger implements ReactiveController { /** * A logging wrapper which checks the debugLog boolean and prints to the console if true. * @example Logger.info("Hello"); + * @param msgs console.log params */ - static info(...msgs: unknown[]) { + static info(...msgs: unknown[]): void { if (Logger.debugLog()) { console.info(...msgs); } @@ -58,8 +60,9 @@ export class Logger implements ReactiveController { /** * A logging wrapper which checks the debugLog boolean and prints to the console if true. * @example Logger.log("Hello"); + * @param msgs console.log params */ - static log(...msgs: unknown[]) { + static log(...msgs: unknown[]): void { if (Logger.debugLog()) { console.log(...msgs); } @@ -68,8 +71,9 @@ export class Logger implements ReactiveController { /** * A console warning wrapper which formats your output with useful debugging information. * @example Logger.warn("Hello"); + * @param msgs console.log params */ - static warn(...msgs: unknown[]) { + static warn(...msgs: unknown[]): void { console.warn(...msgs); } @@ -77,8 +81,9 @@ export class Logger implements ReactiveController { * A console error wrapper which formats your output with useful debugging information. * For use inside a component's function. * @example Logger.error("Hello"); + * @param msgs console.log params */ - static error(...msgs: unknown[]) { + static error(...msgs: unknown[]): void { console.error([...msgs].join(' ')); } @@ -87,24 +92,27 @@ export class Logger implements ReactiveController { /** * Debug logging that outputs the tag name as a prefix automatically * @example this.logger.log("Hello"); + * @param msgs console.log params */ - debug(...msgs: unknown[]) { + debug(...msgs: unknown[]): void { Logger.debug(this.prefix, ...msgs); } /** * Info logging that outputs the tag name as a prefix automatically * @example this.logger.log("Hello"); + * @param msgs console.log params */ - info(...msgs: unknown[]) { + info(...msgs: unknown[]): void { Logger.info(this.prefix, ...msgs); } /** * Local logging that outputs the tag name as a prefix automatically * @example this.logger.log("Hello"); + * @param msgs console.log params */ - log(...msgs: unknown[]) { + log(...msgs: unknown[]): void { Logger.log(this.prefix, ...msgs); } @@ -112,8 +120,9 @@ export class Logger implements ReactiveController { * Local warning wrapper that outputs the tag name as a prefix automatically. * For use inside a component's function. * @example this.logger.warn("Hello"); + * @param msgs console.log params */ - warn(...msgs: unknown[]) { + warn(...msgs: unknown[]): void { Logger.warn(this.prefix, ...msgs); } @@ -121,8 +130,9 @@ export class Logger implements ReactiveController { * Local error wrapper that outputs the tag name as a prefix automatically. * For use inside a component's function. * @example this.logger.error("Hello"); + * @param msgs console.log params */ - error(...msgs: unknown[]) { + error(...msgs: unknown[]): void { Logger.error(this.prefix, ...msgs); } @@ -135,7 +145,7 @@ export class Logger implements ReactiveController { Logger.instances.set(host, this); } - hostConnected() { + hostConnected(): void { this.debug('connected'); } } diff --git a/core/pfe-core/controllers/overflow-controller.ts b/core/pfe-core/controllers/overflow-controller.ts index 5d4b888497..368922b4b0 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(); } @@ -63,7 +63,7 @@ export class OverflowController implements ReactiveController { constructor( // TODO: widen this type to ReactiveControllerHost public host: ReactiveElement, - private options?: Options, + private options?: Options | undefined, ) { this.#hideOverflowButtons = options?.hideOverflowButtons ?? false; this.#scrollTimeoutDelay = options?.scrollTimeoutDelay ?? 0; @@ -102,18 +102,18 @@ export class OverflowController implements ReactiveController { } } - init(container: HTMLElement, items: HTMLElement[]) { + init(container: HTMLElement, items: HTMLElement[]): void { this.#container = container; // convert HTMLCollection to HTMLElement[] this.#items = items; } - onScroll = () => { + onScroll = (): void => { clearTimeout(this.#scrollTimeout); this.#scrollTimeout = setTimeout(() => this.#setOverflowState(), this.#scrollTimeoutDelay); }; - scrollLeft() { + scrollLeft(): void { if (!this.#container) { return; } @@ -122,7 +122,7 @@ export class OverflowController implements ReactiveController { this.#setOverflowState(); } - scrollRight() { + scrollRight(): void { if (!this.#container) { return; } @@ -131,7 +131,7 @@ export class OverflowController implements ReactiveController { this.#setOverflowState(); } - update() { + update(): void { this.#setOverflowState(); } diff --git a/core/pfe-core/controllers/perf-controller.ts b/core/pfe-core/controllers/perf-controller.ts index 7e7e38a1b4..f5e866065d 100644 --- a/core/pfe-core/controllers/perf-controller.ts +++ b/core/pfe-core/controllers/perf-controller.ts @@ -22,13 +22,13 @@ export class PerfController implements ReactiveController { performance.mark(`${this.markId}-defined`); } - hostUpdate() { + hostUpdate(): void { if (!this.hasMeasured) { this.measure(); } } - measure() { + measure(): void { this.hasMeasured = true; performance.mark(`${this.markId}-rendered`); diff --git a/core/pfe-core/controllers/property-observer-controller.ts b/core/pfe-core/controllers/property-observer-controller.ts index 0e5d0b6698..0703f8efb1 100644 --- a/core/pfe-core/controllers/property-observer-controller.ts +++ b/core/pfe-core/controllers/property-observer-controller.ts @@ -1,6 +1,6 @@ import type { ReactiveController, ReactiveElement } from 'lit'; -export const observedController = Symbol('observed properties controller'); +export const observedController: unique symbol = Symbol('observed properties controller'); export type ChangeCallback = ( this: T, @@ -33,7 +33,7 @@ export class PropertyObserverController implements ReactiveController { } /** Set any cached valued accumulated between constructor and connectedCallback */ - hostUpdate() { + hostUpdate(): void { for (const [key, [methodName, [oldVal, newVal]]] of this.values) { // @ts-expect-error: be cool, typescript this.host[methodName as keyof ReactiveElement]?.(oldVal, newVal); @@ -42,11 +42,11 @@ export class PropertyObserverController implements ReactiveController { } /** Once the element has updated, we no longer need this controller, so we remove it */ - hostUpdated() { + hostUpdated(): void { this.host.removeController(this); } - cache(key: string, methodName: string, ...vals: [unknown, unknown]) { + cache(key: string, methodName: string, ...vals: [unknown, unknown]): void { this.values.set(key, [methodName, vals]); } } diff --git a/core/pfe-core/controllers/roving-tabindex-controller.ts b/core/pfe-core/controllers/roving-tabindex-controller.ts index 133cce7bab..afaa2fcaa3 100644 --- a/core/pfe-core/controllers/roving-tabindex-controller.ts +++ b/core/pfe-core/controllers/roving-tabindex-controller.ts @@ -28,12 +28,13 @@ export class RovingTabindexController< static of( host: ReactiveControllerHost, options: RovingTabindexControllerOptions & { getItems(): Item[] }, - ) { + ): RovingTabindexController { return new RovingTabindexController(host, options); } /** @internal */ - static elements = new WeakMap(); + static elements: WeakMap> = + new WeakMap(); /** active focusable element */ #activeItem?: Item; @@ -79,14 +80,14 @@ export class RovingTabindexController< /** * all items from array */ - get items() { + get items(): Item[] { return this.#items; } /** * all focusable items from array */ - get focusableItems() { + get focusableItems(): Item[] { return this.#focusableItems; } @@ -146,7 +147,7 @@ export class RovingTabindexController< this.updateItems(); } - hostUpdated() { + hostUpdated(): void { const oldContainer = this.#itemsContainer; const newContainer = this.#options.getHTMLElement(); if (oldContainer !== newContainer) { @@ -162,7 +163,7 @@ export class RovingTabindexController< /** * removes event listeners from items container */ - hostDisconnected() { + hostDisconnected(): void { this.#itemsContainer?.removeEventListener('keydown', this.#onKeydown); this.#itemsContainer = undefined; this.#gainedInitialFocus = false; @@ -179,6 +180,7 @@ export class RovingTabindexController< /** * handles keyboard navigation + * @param event keydown event */ #onKeydown = (event: Event) => { if (!(event instanceof KeyboardEvent) @@ -250,6 +252,7 @@ export class RovingTabindexController< /** * Sets the active item and focuses it + * @param item tabindex item */ setActiveItem(item?: Item): void { this.#activeItem = item; @@ -264,8 +267,9 @@ export class RovingTabindexController< /** * Focuses next focusable item + * @param items tabindex items */ - updateItems(items: Item[] = this.#options.getItems?.() ?? []) { + updateItems(items: Item[] = this.#options.getItems?.() ?? []): void { this.#items = items; const sequence = [ ...this.#items.slice(this.#itemIndex - 1), @@ -277,16 +281,21 @@ export class RovingTabindexController< this.setActiveItem(activeItem); } - /** @deprecated use setActiveItem */ + /** + * @deprecated use setActiveItem + * @param item tabindex item + */ focusOnItem(item?: Item): void { this.setActiveItem(item); } /** * from array of HTML items, and sets active items - * @deprecated: use getItems and getItemContainer option functions + * @deprecated use getItems and getItemContainer option functions + * @param items tabindex items + * @param itemsContainer */ - initItems(items: Item[], itemsContainer?: Element) { + initItems(items: Item[], itemsContainer?: Element): void { const element = itemsContainer ?? this.#options?.getItemContainer?.() ?? this.#options.getHTMLElement(); diff --git a/core/pfe-core/controllers/scroll-spy-controller.ts b/core/pfe-core/controllers/scroll-spy-controller.ts index 60c223d1ca..89af9d2481 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[] { @@ -52,7 +52,7 @@ export class ScrollSpyController implements ReactiveController { .filter(this.#getHash); } - get root() { + get root(): Element | Document | null | undefined { return this.#root; } @@ -62,7 +62,7 @@ export class ScrollSpyController implements ReactiveController { this.#initIo(); } - get rootMargin() { + get rootMargin(): string | undefined { return this.#rootMargin; } @@ -72,7 +72,7 @@ export class ScrollSpyController implements ReactiveController { this.#initIo(); } - get threshold() { + get threshold(): number | number[] { return this.#threshold; } @@ -92,16 +92,16 @@ 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')); } - hostConnected() { + hostConnected(): void { this.#initIo(); } #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 }); @@ -153,8 +153,11 @@ export class ScrollSpyController implements ReactiveController { this.#intersected = true; } - /** Explicitly set the active item */ - public async setActive(link: EventTarget | null) { + /** + * Explicitly set the active item + * @param link usually an `` + */ + public async setActive(link: EventTarget | null): Promise { this.#force = true; this.#setActive(link); let sawActive = false; diff --git a/core/pfe-core/controllers/slot-controller.ts b/core/pfe-core/controllers/slot-controller.ts index c271797715..519adf514d 100644 --- a/core/pfe-core/controllers/slot-controller.ts +++ b/core/pfe-core/controllers/slot-controller.ts @@ -41,6 +41,7 @@ function isObjectConfigSpread( /** * If it's a named slot, return its children, * for the default slot, look for direct children not assigned to a slot + * @param n slot name */ const isSlot = (n: string | typeof SlotController.default) => @@ -49,9 +50,10 @@ const isSlot = : child.getAttribute('slot') === n; export class SlotController implements ReactiveController { - public static default = Symbol('default slot'); + public static default = Symbol('default slot') satisfies symbol as symbol; + /** @deprecated use `default` */ - public static anonymous = this.default; + public static anonymous: symbol = this.default; #nodes = new Map(); @@ -83,7 +85,7 @@ export class SlotController implements ReactiveController { host.addController(this); } - async hostConnected() { + async hostConnected(): Promise { this.host.addEventListener('slotchange', this.#onSlotChange as EventListener); this.#firstUpdated = false; this.#mo.observe(this.host, { childList: true }); @@ -98,32 +100,33 @@ export class SlotController implements ReactiveController { this.host.requestUpdate(); } - hostUpdated() { + hostUpdated(): void { if (!this.#firstUpdated) { this.#slotNames.forEach(this.#initSlot); this.#firstUpdated = true; } } - hostDisconnected() { + hostDisconnected(): void { this.#mo.disconnect(); } /** * 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). + * @param slotNames slots to query * @example Get header-slotted elements - * ```js - * this.getSlotted('header') - * ``` + * ```js + * this.getSlotted('header') + * ``` * @example Get header- and footer-slotted elements - * ```js - * this.getSlotted('header', 'footer') - * ``` + * ```js + * this.getSlotted('header', 'footer') + * ``` * @example Get default-slotted elements - * ```js - * this.getSlotted(); - * ``` + * ```js + * this.getSlotted(); + * ``` */ getSlotted(...slotNames: string[]): T[] { if (!slotNames.length) { @@ -140,17 +143,16 @@ export class SlotController implements ReactiveController { * @example this.hasSlotted('header'); */ hasSlotted(...names: (string | null | undefined)[]): boolean { - const { anonymous } = SlotController; - const slotNames = Array.from(names, x => x == null ? anonymous : x); + const slotNames = Array.from(names, x => x == null ? SlotController.default : x); if (!slotNames.length) { - slotNames.push(anonymous); + slotNames.push(SlotController.default); } 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. + * @param names The slot names to query. If no value is provided, it returns the default slot. * @example this.isEmpty('header', 'footer'); * @example this.isEmpty(); * @returns diff --git a/core/pfe-core/controllers/tabs-aria-controller.ts b/core/pfe-core/controllers/tabs-aria-controller.ts index 93bb25659e..c4ef7e366a 100644 --- a/core/pfe-core/controllers/tabs-aria-controller.ts +++ b/core/pfe-core/controllers/tabs-aria-controller.ts @@ -36,6 +36,8 @@ export class TabsAriaController< } /** + * @param host controller host + * @param options controller options * @example Usage in PfTab * ```ts * new TabsController(this, { @@ -68,12 +70,12 @@ export class TabsAriaController< } } - hostConnected() { + hostConnected(): void { this.#mo.observe(this.#element, { attributes: false, childList: true, subtree: false }); this.#onSlotchange(); } - hostUpdated() { + hostUpdated(): void { for (const [tab, panel] of this.#tabPanelMap) { panel.setAttribute('aria-labelledby', tab.id); tab.setAttribute('aria-controls', panel.id); diff --git a/core/pfe-core/controllers/timestamp-controller.ts b/core/pfe-core/controllers/timestamp-controller.ts index 51727d7d82..e6deb68b49 100644 --- a/core/pfe-core/controllers/timestamp-controller.ts +++ b/core/pfe-core/controllers/timestamp-controller.ts @@ -35,11 +35,11 @@ export class TimestampController implements ReactiveController { #host: ReactiveControllerHost; - get localeString() { + get localeString(): string { return this.#date.toLocaleString(this.#options.locale); } - get date() { + get date(): Date { return this.#date; } @@ -47,11 +47,11 @@ export class TimestampController implements ReactiveController { this.#date = new Date(string); } - get isoString() { + get isoString(): string { return this.#date.toISOString(); } - get time() { + get time(): string { if (this.#options.relative) { return this.#getTimeRelative(); } else { @@ -127,7 +127,7 @@ export class TimestampController implements ReactiveController { return typeof (units) !== 'undefined' ? rtf.format(tense * qty, units) : 'just now'; } - set(prop: PropertyKey, value: unknown) { + set(prop: PropertyKey, value: unknown): void { if (TimestampController.#isTimestampOptionKey(prop)) { // @ts-expect-error: seems typescript compiler isn't up to the task here this.#options[prop] = value; diff --git a/core/pfe-core/core.ts b/core/pfe-core/core.ts index 55519b5c93..11d787529a 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 { @@ -54,14 +25,14 @@ function makeConverter( * A LitElement property converter which represents a list of numbers as a comma separated string * @see https://lit.dev/docs/components/properties/#conversion-converter */ -export const NumberListConverter = +export const NumberListConverter: ComplexAttributeConverter = makeConverter(x => parseInt(x?.trim(), 10)); /** * A LitElement property converter which represents a list of strings as a comma separated string * @see https://lit.dev/docs/components/properties/#conversion-converter */ -export const StringListConverter = +export const StringListConverter: ComplexAttributeConverter = makeConverter(x => x.trim()); /** @@ -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/bound.ts b/core/pfe-core/decorators/bound.ts index 11b87835e0..458b94cabe 100644 --- a/core/pfe-core/decorators/bound.ts +++ b/core/pfe-core/decorators/bound.ts @@ -2,7 +2,9 @@ const configurable = true; /** * Binds a class method to the instance - * + * @param _ + * @param key + * @param descriptor * @example Binding an event listener * ```ts * private mo = new MutationObserver(this.onMutation); diff --git a/core/pfe-core/decorators/cascades.ts b/core/pfe-core/decorators/cascades.ts index f800aa93bf..84134434c3 100644 --- a/core/pfe-core/decorators/cascades.ts +++ b/core/pfe-core/decorators/cascades.ts @@ -4,7 +4,8 @@ import { CascadeController } from '../controllers/cascade-controller.js'; /** * Cascades the decorated attribute to children - * @deprecated: use context, especially via `@patternfly/pfe-core/functions/context.js`; + * @param items + * @deprecated use context, especially via `@patternfly/pfe-core/functions/context.js`; */ export function cascades(...items: string[]): PropertyDecorator { return function(proto: T, key: string & keyof T) { diff --git a/core/pfe-core/decorators/deprecation.ts b/core/pfe-core/decorators/deprecation.ts index 20c5ff7c27..73be32fa44 100644 --- a/core/pfe-core/decorators/deprecation.ts +++ b/core/pfe-core/decorators/deprecation.ts @@ -9,19 +9,22 @@ export type DeprecationDeclaration = PropertyDeclaration /** * Aliases the decorated field to an existing property, and logs a warning if it is used + * @param options alias is a drop in replacement * @example deprecating an attribute - * ```ts - * @property({ reflect: true, attribute: 'color-palette'}) - * colorPalette: ColorPalette = 'base'; + * ```ts + * @property({ reflect: true, attribute: 'color-palette'}) + * colorPalette: ColorPalette = 'base'; * - * @deprecation('colorPalette') color?: ColorPalette; - * ``` + * @deprecation('colorPalette') color?: ColorPalette; + * ``` */ -export function deprecation(options: DeprecationDeclaration) { +export function deprecation( + options: DeprecationDeclaration, +) { return function( proto: Partial>, key: string & keyof T - ) { + ): void { const { alias, ...deprecationOptions } = options; const klass = (proto.constructor as typeof ReactiveElement); const declaration = klass.getPropertyOptions(alias); diff --git a/core/pfe-core/decorators/initializer.ts b/core/pfe-core/decorators/initializer.ts index 25192d5748..a041451f97 100644 --- a/core/pfe-core/decorators/initializer.ts +++ b/core/pfe-core/decorators/initializer.ts @@ -11,7 +11,7 @@ import { LightDOMController } from '../controllers/light-dom-controller.js'; * @param options Set `observe` to `false` to skip mutation observer setup, or pass a MutationObserverInit as options */ export function initializer(options?: Options) { - return function(proto: T, key: string) { + return function(proto: T, key: string): void { // @TODO: allow multiple initializers (proto.constructor as typeof ReactiveElement).addInitializer(instance => { const initializer = proto[key as keyof T] as unknown as () => void; diff --git a/core/pfe-core/decorators/observed.ts b/core/pfe-core/decorators/observed.ts index 48806b40ce..d0096b5662 100644 --- a/core/pfe-core/decorators/observed.ts +++ b/core/pfe-core/decorators/observed.ts @@ -12,33 +12,34 @@ import { type TypedFieldDecorator = (proto: T, key: string | keyof T) => void ; +// eslint-disable-next-line jsdoc/require-param /** * Calls a _fooChanged method on the instance when the value changes. * Works on any class field. When using on lit observed properties, * Make sure `@observed` is to the left (i.e. called after) the `@property` * or `@state` decorator. * @example observing a lit property - * ```ts - * @observed @property() foo = 'bar'; + * ```ts + * @observed @property() foo = 'bar'; * - * protected _fooChanged(oldValue?: string, newValue?: string) {} - * ``` + * protected _fooChanged(oldValue?: string, newValue?: string) {} + * ``` * @example using a custom callback - * ```ts - * @observed('_myCallback') size = 'lg'; + * ```ts + * @observed('_myCallback') size = 'lg'; * - * _myCallback(_, size) {...} - * ``` + * _myCallback(_, size) {...} + * ``` * @example using an arrow function - * ```ts - * @observed((oldVal, newVal) => console.log(`Size changed from ${oldVal} to ${newVal}`)) - * ``` + * ```ts + * @observed((oldVal, newVal) => console.log(`Size changed from ${oldVal} to ${newVal}`)) + * ``` */ export function observed(methodName: string): TypedFieldDecorator; export function observed(cb: ChangeCallback): TypedFieldDecorator; export function observed(proto: T, key: string): void; +// eslint-disable-next-line jsdoc/require-jsdoc export function observed(...as: any[]): void | TypedFieldDecorator { - /** @observed('_myCustomChangeCallback') */ if (as.length === 1) { const [methodNameOrCallback] = as; return function(proto, key) { @@ -54,11 +55,17 @@ export function observed(...as: any[]): void | TypedF } } +/** + * Creates an observer on a field + * @param proto + * @param key + * @param callbackOrMethod + */ export function observeProperty( proto: T, key: string & keyof T, callbackOrMethod?: ChangeCallback -) { +): void { const descriptor = Object.getOwnPropertyDescriptor(proto, key); Object.defineProperty(proto, key, { ...descriptor, diff --git a/core/pfe-core/decorators/time.ts b/core/pfe-core/decorators/time.ts index dca0b4ae31..bdb3f45dac 100644 --- a/core/pfe-core/decorators/time.ts +++ b/core/pfe-core/decorators/time.ts @@ -1,8 +1,13 @@ /** * 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) { + return function( + _: unknown, + key: string, + descriptor: PropertyDescriptor, + ): void { const { value: f } = descriptor ?? {}; if (!(typeof f === 'function')) { @@ -14,19 +19,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/decorators/trace.ts b/core/pfe-core/decorators/trace.ts index e94328ba83..7507ad5cdc 100644 --- a/core/pfe-core/decorators/trace.ts +++ b/core/pfe-core/decorators/trace.ts @@ -1,6 +1,13 @@ -/** Logs the result of a class method */ +/** + * Logs the result of a class method + * @param tag log tag, prepended to outputs + */ export function trace(tag?: string) { - return function(_: unknown, key: string, descriptor: PropertyDescriptor) { + return function( + _: unknown, + key: string, + descriptor: PropertyDescriptor, + ): void { const { value: f } = descriptor; descriptor.value = function(...args: any[]) { const x = f.call(this, ...args); diff --git a/core/pfe-core/functions/containsDeep.ts b/core/pfe-core/functions/containsDeep.ts index f339507fa7..b14454bdef 100644 --- a/core/pfe-core/functions/containsDeep.ts +++ b/core/pfe-core/functions/containsDeep.ts @@ -2,8 +2,13 @@ * Whether or not the container contains the node, * and if not, whether the node is contained by any element * slotted in to the container + * @param container haystack + * @param node needle */ -export function containsDeep(container: Element, node: Node) { +export function containsDeep( + container: Element, + node: Node, +): boolean { if (container.contains(node)) { return true; } else { diff --git a/core/pfe-core/functions/context.ts b/core/pfe-core/functions/context.ts index ec97e4bcfb..2e109b6b2c 100644 --- a/core/pfe-core/functions/context.ts +++ b/core/pfe-core/functions/context.ts @@ -1,10 +1,13 @@ -import { ContextRoot, createContext } from '@lit/context'; +import { ContextRoot, createContext, type Context } from '@lit/context'; +import { isServer } from 'lit'; let root: ContextRoot; function makeContextRoot() { - root = new ContextRoot(); - root.attach(document.body); + const root = new ContextRoot(); + if (!isServer) { + root.attach(document.body); + } return root; } @@ -12,8 +15,11 @@ function makeContextRoot() { * In order to prevent late-upgrading-context-consumers from 'missing' * their rightful context providers, we must set up a `ContextRoot` on the body. * Always use this function when creating contexts that are shared with child elements. + * @param args createContext args */ -export function createContextWithRoot(...args: Parameters) { +export function createContextWithRoot( + ...args: Parameters +): Context { root ??= makeContextRoot(); return createContext(...args); } diff --git a/core/pfe-core/functions/debounce.ts b/core/pfe-core/functions/debounce.ts index b66af5f44d..b367ef1f4d 100644 --- a/core/pfe-core/functions/debounce.ts +++ b/core/pfe-core/functions/debounce.ts @@ -12,7 +12,7 @@ export function debounce( immediate = false ) { let timeout: number | null; - return function(this: unknown, ...args: any[]) { + return function(this: unknown, ...args: any[]): void { // eslint-disable-next-line @typescript-eslint/no-this-alias const context = this; const later = function() { diff --git a/core/pfe-core/package.json b/core/pfe-core/package.json index 504fddee9c..67706c9ef9 100644 --- a/core/pfe-core/package.json +++ b/core/pfe-core/package.json @@ -53,9 +53,9 @@ "test": "wtr --files './test/*.spec.ts' --config ../../web-test-runner.config.js" }, "dependencies": { - "@floating-ui/dom": "^1.6.3", - "@lit/context": "^1.1.0", - "lit": "^3.1.2" + "@floating-ui/dom": "^1.6.7", + "@lit/context": "^1.1.2", + "lit": "^3.1.4" }, "repository": { "type": "git", diff --git a/declaration.d.ts b/declaration.d.ts index 7df17c6565..d9add9e501 100644 --- a/declaration.d.ts +++ b/declaration.d.ts @@ -1,4 +1,4 @@ declare module '*.css' { - const style: CSSStyleSheet + const style: CSSStyleSheet; export default style; } diff --git a/docs/_plugins/cem-render.cjs b/docs/_plugins/cem-render.cjs index d2931c00ea..5042e659a7 100644 --- a/docs/_plugins/cem-render.cjs +++ b/docs/_plugins/cem-render.cjs @@ -1,11 +1,20 @@ -/** quick and dirty dedent, also provides in-editor syntax highlighting */ +/** + * quick and dirty dedent, also provides in-editor syntax highlighting + * @param {string[]} args + */ const html = (...args) => String.raw(...args) .split('\n') .map(x => x.replace(/^ {6}/, '')) .join('\n'); -/** @typedef {import('@patternfly/pfe-tools/11ty/DocsPage').DocsPage} DocsPage */ +/** + * @import { UserConfig } from '@11ty/eleventy' + */ + +/** + * @param {UserConfig} eleventyConfig + */ module.exports = function(eleventyConfig) { eleventyConfig.addPairedShortcode('renderCodeDocs', function renderCodeDocs(content, kwargs = {}) { @@ -42,14 +51,7 @@ function renderBand(content, { level, header = '' } = {}) { `; } -/** - * docs pages contain a #styling-hooks anchor as back compat for older versions of the page - * to prevent this id from rendering more than once, we track the number of times each page - * renders css custom properties. - */ -const cssStylingHookIdTracker = new WeakSet(); - -/** @param {import('@11ty/eleventy').UserConfig} eleventyConfig */ +/** @param {UserConfig} eleventyConfig */ module.exports = function(eleventyConfig) { eleventyConfig.addFilter('innerMD', innerMD); eleventyConfig.addFilter('mdHeading', mdHeading); @@ -74,7 +76,6 @@ module.exports = function(eleventyConfig) { }; class Renderers { - /** @type{WeakMap} */ static renderers = new WeakMap(); static forPage(page) { return new Renderers(page); @@ -88,7 +89,6 @@ class Renderers { * NB: since the data for this shortcode is no a POJO, * but a DocsPage instance, 11ty assigns it to this.ctx._ * @see https://github.com/11ty/eleventy/blob/bf7c0c0cce1b2cb01561f57fdd33db001df4cb7e/src/Plugins/RenderPlugin.js#L89-L93 - * @type {DocsPage} */ this.docsPage = page.ctx._; this.manifest = this.docsPage.manifest; @@ -104,7 +104,10 @@ class Renderers { } } - /** Render the overview of a component page */ + /** + * Render the overview of a component page + * @param {string} content + */ renderOverview(content) { return html`
@@ -124,7 +127,13 @@ class Renderers {
`; } - /** Render the list of element attributes */ + /** + * Render the list of element attributes + * @param {string} content inner md content + * @param {object} kwargs + * @param {string} kwargs.header + * @param {number} kwargs.level + */ renderAttributes(content, { header = 'Attributes', level = 2, ...kwargs } = {}) { const _attrs = this.manifest.getAttributes(this.packageTagName(kwargs)) ?? []; const deprecated = _attrs.filter(x => x.deprecated); @@ -169,7 +178,13 @@ class Renderers { `; } - /** Render the list of element DOM properties */ + /** + * Render the list of element DOM properties + * @param {string} content inner md content + * @param {object} kwargs + * @param {string} kwargs.header + * @param {number} kwargs.level + */ renderProperties(content, { header = 'DOM Properties', level = 2, ...kwargs } = {}) { const allProperties = this.manifest.getProperties(this.packageTagName(kwargs)) ?? []; const deprecated = allProperties.filter(x => x.deprecated); @@ -211,7 +226,13 @@ class Renderers { `; } - /** Render a table of element CSS Custom Properties */ + /** + * Render a table of element CSS Custom Properties + * @param {string} content inner md content + * @param {object} kwargs + * @param {string} kwargs.header + * @param {number} kwargs.level + */ renderCssCustomProperties(content, { header = 'CSS Custom Properties', level = 2, @@ -270,7 +291,13 @@ class Renderers { `; } - /** Render the list of element CSS Shadow Parts */ + /** + * Render the list of element CSS Shadow Parts + * @param {string} content inner md content + * @param {object} kwargs + * @param {string} kwargs.header + * @param {number} kwargs.level + */ renderCssParts(content, { header = 'CSS Shadow Parts', level = 2, ...kwargs } = {}) { const allParts = this.manifest.getCssParts(this.packageTagName(kwargs)) ?? []; const parts = allParts.filter(x => !x.deprecated); @@ -297,7 +324,13 @@ class Renderers { `; } - /** Render the list of events for the element */ + /** + * Render the list of events for the element + * @param {string} content inner md content + * @param {object} kwargs + * @param {string} kwargs.header + * @param {number} kwargs.level + */ renderEvents(content, { header = 'Events', level = 2, ...kwargs } = {}) { const _events = this.manifest.getEvents(this.packageTagName(kwargs)) ?? []; const deprecated = _events.filter(x => x.deprecated); @@ -330,7 +363,14 @@ class Renderers { `; } - /** Render the installation instructions for the element */ + /** + * Render the installation instructions for the element + * @param {string} content inner md content + * @param {object} kwargs + * @param {string} kwargs.header + * @param {number} kwargs.level + * @param {string} kwargs.tagName + */ renderInstallation(content, { header = 'Installation', level = 2, @@ -338,7 +378,7 @@ class Renderers { } = {}) { return html`
-

Installation

+ ${header} We recommend loading elements via a CDN such as [JSPM][inst-jspm] and using an import map to manage your dependencies. @@ -369,7 +409,13 @@ class Renderers { [inst-bms]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules`; } - /** Render the list of element methods */ + /** + * Render the list of element methods + * @param {string} content inner md content + * @param {object} kwargs + * @param {string} kwargs.header + * @param {number} kwargs.level + */ renderMethods(content, { header = 'Methods', level = 2, ...kwargs } = {}) { const allMethods = this.manifest.getMethods(this.packageTagName(kwargs)) ?? []; const deprecated = allMethods.filter(x => x.deprecated); @@ -397,7 +443,13 @@ class Renderers {
`; } - /** Render the list of the element's slots */ + /** + * Render the list of the element's slots + * @param {string} content inner md content + * @param {object} kwargs + * @param {string} kwargs.header + * @param {number} kwargs.level + */ renderSlots(content, { header = 'Slots', level = 2, ...kwargs } = {}) { const allSlots = this.docsPage.manifest.getSlots(this.packageTagName(kwargs)) ?? []; const slots = allSlots.filter(x => !x.deprecated); diff --git a/docs/_plugins/pfe-assets.cjs b/docs/_plugins/pfe-assets.cjs index d920fdde4c..240eee6b01 100644 --- a/docs/_plugins/pfe-assets.cjs +++ b/docs/_plugins/pfe-assets.cjs @@ -1,11 +1,6 @@ const fs = require('fs'); const path = require('path'); -/** - * @typedef {object} EleventyTransformContext - * @property {string} outputPath path this file will be written to - */ - /** * Generate a map of files per package which should be copied to the site dir * @param {object} [options] @@ -55,7 +50,7 @@ const DEMO_PATHS_RE = /** * Replace paths in demo files from the dev SPA's format to 11ty's format - * @this {EleventyTransformContext} + * @param {string} content demo file */ function demoPaths(content) { if (this.outputPath.match(/(components|core|tools)\/.*\/demo\/index\.html$/)) { 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/custom-elements-manifest.config.js b/elements/custom-elements-manifest.config.js index 7b9ac24cda..4b2b67c7bb 100644 --- a/elements/custom-elements-manifest.config.js +++ b/elements/custom-elements-manifest.config.js @@ -9,6 +9,5 @@ export default pfeCustomElementsManifestConfig({ rootDir, globs: [ './*/pf-*.ts', - './*/Base*.ts' ], }); diff --git a/elements/form-control.css b/elements/form-control.css new file mode 100644 index 0000000000..cbb3e0206a --- /dev/null +++ b/elements/form-control.css @@ -0,0 +1,127 @@ +:host { + --pf-c-form-control--Color: var(--pf-global--Color--100, #151515); + --pf-c-form-control--FontSize: var(--pf-global--FontSize--md, 1rem); + --pf-c-form-control--LineHeight: var(--pf-global--LineHeight--md, 1.5); + --pf-c-form-control--BorderWidth: var(--pf-global--BorderWidth--sm, 1px); + --pf-c-form-control--BorderTopColor: var(--pf-global--BorderColor--300, #f0f0f0); + --pf-c-form-control--BorderRightColor: var(--pf-global--BorderColor--300, #f0f0f0); + --pf-c-form-control--BorderBottomColor: var(--pf-global--BorderColor--200, #8a8d90); + --pf-c-form-control--BorderLeftColor: var(--pf-global--BorderColor--300, #f0f0f0); + --pf-c-form-control--BorderRadius: 0; + --pf-c-form-control--BackgroundColor: var(--pf-global--BackgroundColor--100, #fff); + --pf-c-form-control--Width: 100%; + --pf-c-form-control--Height: calc(var(--pf-c-form-control--FontSize) * var(--pf-c-form-control--LineHeight) + var(--pf-c-form-control--BorderWidth) * 2 + var(--pf-c-form-control--PaddingTop) + var(--pf-c-form-control--PaddingBottom)); + --pf-c-form-control--inset--base: var(--pf-global--spacer--sm, 0.5rem); + --pf-c-form-control--PaddingTop: calc(var(--pf-global--spacer--form-element, 0.375rem) - var(--pf-global--BorderWidth--sm)); + --pf-c-form-control--PaddingBottom: calc(var(--pf-global--spacer--form-element, 0.375rem) - var(--pf-global--BorderWidth--sm)); + --pf-c-form-control--PaddingRight: var(--pf-c-form-control--inset--base); + --pf-c-form-control--PaddingLeft: var(--pf-c-form-control--inset--base); + --pf-c-form-control--hover--BorderBottomColor: var(--pf-global--primary-color--100, #06c); + --pf-c-form-control--focus--BorderBottomWidth: var(--pf-global--BorderWidth--md, 2px); + --pf-c-form-control--focus--PaddingBottom: calc(var(--pf-global--spacer--form-element, 0.375rem) - var(--pf-c-form-control--focus--BorderBottomWidth)); + --pf-c-form-control--focus--BorderBottomColor: var(--pf-global--primary-color--100, #06c); + --pf-c-form-control--m-expanded--BorderBottomWidth: var(--pf-global--BorderWidth--md, 2px); + --pf-c-form-control--m-expanded--PaddingBottom: calc(var(--pf-global--spacer--form-element, 0.375rem) - var(--pf-c-form-control--focus--BorderBottomWidth)); + --pf-c-form-control--m-expanded--BorderBottomColor: var(--pf-global--primary-color--100, #06c); + --pf-c-form-control--placeholder--Color: var(--pf-global--Color--dark-200, #6a6e73); + --pf-c-form-control--placeholder--child--Color: var(--pf-global--Color--100, #151515); + --pf-c-form-control--disabled--Color: var(--pf-global--disabled-color--100, #6a6e73); + --pf-c-form-control--disabled--BackgroundColor: var(--pf-global--disabled-color--300, #f0f0f0); + --pf-c-form-control--disabled--BorderColor: transparent; + --pf-c-form-control--readonly--BackgroundColor: var(--pf-global--disabled-color--300, #f0f0f0); + --pf-c-form-control--readonly--hover--BorderBottomColor: var(--pf-global--BorderColor--200, #8a8d90); + --pf-c-form-control--readonly--focus--PaddingBottom: calc(var(--pf-global--spacer--form-element, 0.375rem) - var(--pf-global--BorderWidth--sm)); + --pf-c-form-control--readonly--focus--BorderBottomWidth: var(--pf-global--BorderWidth--sm, 1px); + --pf-c-form-control--readonly--focus--BorderBottomColor: var(--pf-global--BorderColor--200, #8a8d90); + --pf-c-form-control--readonly--m-plain--BackgroundColor: transparent; + --pf-c-form-control--readonly--m-plain--inset--base: 0; + --pf-c-form-control--success--BorderBottomWidth: var(--pf-global--BorderWidth--md, 2px); + --pf-c-form-control--success--PaddingBottom: calc(var(--pf-global--spacer--form-element, 0.375rem) - var(--pf-c-form-control--success--BorderBottomWidth)); + --pf-c-form-control--success--BorderBottomColor: var(--pf-global--success-color--100, #3e8635); + --pf-c-form-control--success--PaddingRight: var(--pf-global--spacer--xl, 2rem); + --pf-c-form-control--success--BackgroundPositionX: calc(100% - var(--pf-c-form-control--PaddingLeft)); + --pf-c-form-control--success--BackgroundPositionY: center; + --pf-c-form-control--success--BackgroundPosition: var(--pf-c-form-control--success--BackgroundPositionX) var(--pf-c-form-control--success--BackgroundPositionY); + --pf-c-form-control--success--BackgroundSizeX: var(--pf-c-form-control--FontSize); + --pf-c-form-control--success--BackgroundSizeY: var(--pf-c-form-control--FontSize); + --pf-c-form-control--success--BackgroundSize: var(--pf-c-form-control--success--BackgroundSizeX) var(--pf-c-form-control--success--BackgroundSizeY); + --pf-c-form-control--success--BackgroundUrl: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath fill='%235ba352' d='M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zM227.314 387.314l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.249-16.379-6.249-22.628 0L216 308.118l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.249 16.379 6.249 22.628.001z'/%3E%3C/svg%3E"); + --pf-c-form-control--m-warning--BorderBottomWidth: var(--pf-global--BorderWidth--md, 2px); + --pf-c-form-control--m-warning--PaddingBottom: calc(var(--pf-global--spacer--form-element, 0.375rem) - var(--pf-c-form-control--m-warning--BorderBottomWidth)); + --pf-c-form-control--m-warning--BorderBottomColor: var(--pf-global--warning-color--100, #f0ab00); + --pf-c-form-control--m-warning--PaddingRight: var(--pf-global--spacer--xl, 2rem); + --pf-c-form-control--m-warning--BackgroundPositionX: calc(100% - calc(var(--pf-c-form-control--PaddingLeft) - 0.0625rem)); + --pf-c-form-control--m-warning--BackgroundPositionY: center; + --pf-c-form-control--m-warning--BackgroundPosition: var(--pf-c-form-control--m-warning--BackgroundPositionX) var(--pf-c-form-control--m-warning--BackgroundPositionY); + --pf-c-form-control--m-warning--BackgroundSizeX: 1.25rem; + --pf-c-form-control--m-warning--BackgroundSizeY: var(--pf-c-form-control--FontSize); + --pf-c-form-control--m-warning--BackgroundSize: var(--pf-c-form-control--m-warning--BackgroundSizeX) var(--pf-c-form-control--m-warning--BackgroundSizeY); + --pf-c-form-control--m-warning--BackgroundUrl: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath fill='%23f0ab00' d='M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z'/%3E%3C/svg%3E"); + --pf-c-form-control--invalid--BorderBottomWidth: var(--pf-global--BorderWidth--md, 2px); + --pf-c-form-control--invalid--PaddingBottom: calc(var(--pf-global--spacer--form-element, 0.375rem) - var(--pf-c-form-control--invalid--BorderBottomWidth)); + --pf-c-form-control--invalid--BorderBottomColor: var(--pf-global--danger-color--100, #c9190b); + --pf-c-form-control--invalid--PaddingRight: var(--pf-global--spacer--xl, 2rem); + --pf-c-form-control--invalid--BackgroundPositionX: calc(100% - var(--pf-c-form-control--PaddingLeft)); + --pf-c-form-control--invalid--BackgroundPositionY: center; + --pf-c-form-control--invalid--BackgroundPosition: var(--pf-c-form-control--invalid--BackgroundPositionX) var(--pf-c-form-control--invalid--BackgroundPositionY); + --pf-c-form-control--invalid--BackgroundSizeX: var(--pf-c-form-control--FontSize); + --pf-c-form-control--invalid--BackgroundSizeY: var(--pf-c-form-control--FontSize); + --pf-c-form-control--invalid--BackgroundSize: var(--pf-c-form-control--invalid--BackgroundSizeX) var(--pf-c-form-control--invalid--BackgroundSizeY); + --pf-c-form-control--invalid--BackgroundUrl: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath fill='%23fe5142' d='M504 256c0 136.997-111.043 248-248 248S8 392.997 8 256C8 119.083 119.043 8 256 8s248 111.083 248 248zm-248 50c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z'/%3E%3C/svg%3E"); + --pf-c-form-control--invalid--exclamation--Background: var(--pf-c-form-control--invalid--BackgroundUrl) var(--pf-c-form-control--invalid--BackgroundPosition) / var(--pf-c-form-control--invalid--BackgroundSize) no-repeat; + --pf-c-form-control--invalid--Background: var(--pf-c-form-control--BackgroundColor) var(--pf-c-form-control--invalid--exclamation--Background); + --pf-c-form-control--m-search--PaddingLeft: var(--pf-global--spacer--xl, 2rem); + --pf-c-form-control--m-search--BackgroundPosition: var(--pf-c-form-control--PaddingRight); + --pf-c-form-control--m-search--BackgroundSize: var(--pf-c-form-control--FontSize) var(--pf-c-form-control--FontSize); + --pf-c-form-control--m-search--BackgroundUrl: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath fill='%23aaabac' d='M505 442.7L405.3 343c-4.5-4.5-10.6-7-17-7H372c27.6-35.3 44-79.7 44-128C416 93.1 322.9 0 208 0S0 93.1 0 208s93.1 208 208 208c48.3 0 92.7-16.4 128-44v16.3c0 6.4 2.5 12.5 7 17l99.7 99.7c9.4 9.4 24.6 9.4 33.9 0l28.3-28.3c9.4-9.4 9.4-24.6.1-34zM208 336c-70.7 0-128-57.2-128-128 0-70.7 57.2-128 128-128 70.7 0 128 57.2 128 128 0 70.7-57.2 128-128 128z'/%3E%3C/svg%3E"); + --pf-c-form-control--m-icon--PaddingRight: calc(var(--pf-c-form-control--inset--base) + var(--pf-c-form-control--m-icon--BackgroundSizeX) + var(--pf-c-form-control--m-icon--icon--spacer)); + --pf-c-form-control--m-icon--BackgroundUrl: none; + --pf-c-form-control--m-icon--BackgroundPositionX: calc(100% - var(--pf-c-form-control--inset--base)); + --pf-c-form-control--m-icon--BackgroundPositionY: center; + --pf-c-form-control--m-icon--BackgroundSizeX: var(--pf-c-form-control--FontSize); + --pf-c-form-control--m-icon--BackgroundSizeY: var(--pf-c-form-control--FontSize); + --pf-c-form-control--m-icon--icon--spacer: var(--pf-global--spacer--sm, 0.5rem); + --pf-c-form-control--m-icon--icon--PaddingRight: calc(var(--pf-c-form-control--inset--base) + var(--pf-c-form-control--invalid--BackgroundSizeX) + var(--pf-c-form-control--m-icon--icon--spacer) + var(--pf-c-form-control--m-icon--BackgroundSizeX) + var(--pf-c-form-control--m-icon--icon--spacer)); + --pf-c-form-control--m-icon--icon--BackgroundPositionX: calc(var(--pf-c-form-control--m-icon--BackgroundPositionX) - var(--pf-c-form-control--m-icon--icon--spacer) - var(--pf-c-form-control--invalid--BackgroundSizeX)); + --pf-c-form-control--m-icon--invalid--BackgroundUrl: var(--pf-c-form-control--invalid--BackgroundUrl), var(--pf-c-form-control--m-icon--BackgroundUrl); + --pf-c-form-control--m-icon--invalid--BackgroundPosition: var(--pf-c-form-control--invalid--BackgroundPosition), var(--pf-c-form-control--m-icon--icon--BackgroundPositionX) var(--pf-c-form-control--m-icon--BackgroundPositionY); + --pf-c-form-control--m-icon--invalid--BackgroundSize: var(--pf-c-form-control--invalid--BackgroundSize), var(--pf-c-form-control--m-icon--BackgroundSizeX) var(--pf-c-form-control--m-icon--BackgroundSizeY); + --pf-c-form-control--m-icon--success--BackgroundUrl: var(--pf-c-form-control--success--BackgroundUrl), var(--pf-c-form-control--m-icon--BackgroundUrl); + --pf-c-form-control--m-icon--success--BackgroundPosition: var(--pf-c-form-control--success--BackgroundPosition), var(--pf-c-form-control--m-icon--icon--BackgroundPositionX) var(--pf-c-form-control--m-icon--BackgroundPositionY); + --pf-c-form-control--m-icon--success--BackgroundSize: var(--pf-c-form-control--success--BackgroundSize), var(--pf-c-form-control--m-icon--BackgroundSizeX) var(--pf-c-form-control--m-icon--BackgroundSizeY); + --pf-c-form-control--m-icon--m-warning--BackgroundUrl: var(--pf-c-form-control--m-warning--BackgroundUrl), var(--pf-c-form-control--m-icon--BackgroundUrl); + --pf-c-form-control--m-icon--m-warning--BackgroundPosition: var(--pf-c-form-control--m-warning--BackgroundPosition), var(--pf-c-form-control--m-icon--icon--BackgroundPositionX) var(--pf-c-form-control--m-icon--BackgroundPositionY); + --pf-c-form-control--m-icon--m-warning--BackgroundSize: var(--pf-c-form-control--m-warning--BackgroundSize), var(--pf-c-form-control--m-icon--BackgroundSizeX) var(--pf-c-form-control--m-icon--BackgroundSizeY); + --pf-c-form-control--m-calendar--BackgroundUrl: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath fill='%23aaabac' d='M0 464c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V192H0v272zm320-196c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v40c0 6.6-5.4 12-12 12h-40c-6.6 0-12-5.4-12-12v-40zm0 128c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v40c0 6.6-5.4 12-12 12h-40c-6.6 0-12-5.4-12-12v-40zM192 268c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v40c0 6.6-5.4 12-12 12h-40c-6.6 0-12-5.4-12-12v-40zm0 128c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v40c0 6.6-5.4 12-12 12h-40c-6.6 0-12-5.4-12-12v-40zM64 268c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v40c0 6.6-5.4 12-12 12H76c-6.6 0-12-5.4-12-12v-40zm0 128c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v40c0 6.6-5.4 12-12 12H76c-6.6 0-12-5.4-12-12v-40zM400 64h-48V16c0-8.8-7.2-16-16-16h-32c-8.8 0-16 7.2-16 16v48H160V16c0-8.8-7.2-16-16-16h-32c-8.8 0-16 7.2-16 16v48H48C21.5 64 0 85.5 0 112v48h448v-48c0-26.5-21.5-48-48-48z'/%3E%3C/svg%3E"); + --pf-c-form-control--m-clock--BackgroundUrl: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath fill='%23aaabac' d='M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm61.8-104.4l-84.9-61.7c-3.1-2.3-4.9-5.9-4.9-9.7V116c0-6.6 5.4-12 12-12h32c6.6 0 12 5.4 12 12v141.7l66.8 48.6c5.4 3.9 6.5 11.4 2.6 16.8L334.6 349c-3.9 5.3-11.4 6.5-16.8 2.6z'/%3E%3C/svg%3E"); + --pf-c-form-control__select--PaddingRight: calc(var(--pf-global--spacer--lg, 1.5rem) + var(--pf-c-form-control--BorderWidth) + var(--pf-c-form-control--BorderWidth)); + --pf-c-form-control__select--PaddingLeft: calc(var(--pf-global--spacer--sm, 0.5rem) - var(--pf-c-form-control--BorderWidth)); + --pf-c-form-control__select--BackgroundUrl: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 512'%3E%3Cpath fill='%23urrentColor' d='M31.3 192h257.3c17.8 0 26.7 21.5 14.1 34.1L174.1 354.8c-7.8 7.8-20.5 7.8-28.3 0L17.2 226.1C4.6 213.5 13.5 192 31.3 192z'/%3E%3C/svg%3E"); + --pf-c-form-control__select--BackgroundSize: .625em; + --pf-c-form-control__select--BackgroundPositionX: calc(100% - var(--pf-global--spacer--md, 1rem) + 1px); + --pf-c-form-control__select--BackgroundPositionY: center; + --pf-c-form-control__select--BackgroundPosition: var(--pf-c-form-control__select--BackgroundPositionX) var(--pf-c-form-control__select--BackgroundPositionY); + --pf-c-form-control__select--success--PaddingRight: var(--pf-global--spacer--3xl, 4rem); + --pf-c-form-control__select--success--BackgroundPosition: calc(var(--pf-c-form-control__select--BackgroundPositionX) - var(--pf-global--spacer--lg, 1.5rem)); + --pf-c-form-control__select--m-warning--PaddingRight: var(--pf-global--spacer--3xl, 4rem); + --pf-c-form-control__select--m-warning--BackgroundPosition: calc(var(--pf-c-form-control__select--BackgroundPositionX) - var(--pf-global--spacer--lg, 1.5rem) + 0.0625rem); + --pf-c-form-control__select--invalid--PaddingRight: var(--pf-global--spacer--3xl, 4rem); + --pf-c-form-control__select--invalid--BackgroundPosition: calc(var(--pf-c-form-control__select--BackgroundPositionX) - var(--pf-global--spacer--lg, 1.5rem)); + --pf-c-form-control--textarea--Width: var(--pf-c-form-control--Width); + --pf-c-form-control--textarea--Height: auto; + --pf-c-form-control--textarea--success--BackgroundPositionY: var(--pf-c-form-control--PaddingLeft); + --pf-c-form-control--textarea--m-warning--BackgroundPositionY: var(--pf-c-form-control--PaddingLeft); + --pf-c-form-control--textarea--invalid--BackgroundPositionY: var(--pf-c-form-control--PaddingLeft); + --pf-c-form-control--m-icon-sprite--success--BackgroundUrl: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath fill='%233e8635' d='M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zM227.314 387.314l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.249-16.379-6.249-22.628 0L216 308.118l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.249 16.379 6.249 22.628.001z'/%3E%3C/svg%3E%0A"); + --pf-c-form-control--m-icon-sprite--m-warning--BackgroundUrl: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath fill='%23f0ab00' d='M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346 7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z'/%3E%3C/svg%3E%0A"); + --pf-c-form-control--m-icon-sprite--invalid--BackgroundUrl: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath fill='%23c9190b' d='M504 256c0 136.997-111.043 248-248 248S8 392.997 8 256C8 119.083 119.043 8 256 8s248 111.083 248 248zm-248 50c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346 7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z'/%3E%3C/svg%3E%0A"); + --pf-c-form-control--m-icon-sprite__select--BackgroundUrl: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath fill='%23151515' d='M31.3 192h257.3c17.8 0 26.7 21.5 14.1 34.1L174.1 354.8c-7.8 7.8-20.5 7.8-28.3 0L17.2 226.1C4.6 213.5 13.5 192 31.3 192z'/%3E%3C/svg%3E%0A"); + --pf-c-form-control--m-icon-sprite--m-search--BackgroundUrl: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath fill='%236a6e73' d='M505 442.7 405.3 343c-4.5-4.5-10.6-7-17-7H372c27.6-35.3 44-79.7 44-128C416 93.1 322.9 0 208 0S0 93.1 0 208s93.1 208 208 208c48.3 0 92.7-16.4 128-44v16.3c0 6.4 2.5 12.5 7 17l99.7 99.7c9.4 9.4 24.6 9.4 33.9 0l28.3-28.3c9.4-9.4 9.4-24.6.1-34zM208 336c-70.7 0-128-57.2-128-128 0-70.7 57.2-128 128-128 70.7 0 128 57.2 128 128 0 70.7-57.2 128-128 128z'/%3E%3C/svg%3E%0A"); + --pf-c-form-control--m-icon-sprite--m-calendar--BackgroundUrl: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath fill='%236a6e73' d='M0 464c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V192H0v272zm320-196c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v40c0 6.6-5.4 12-12 12h-40c-6.6 0-12-5.4-12-12v-40zm0 128c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v40c0 6.6-5.4 12-12 12h-40c-6.6 0-12-5.4-12-12v-40zM192 268c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v40c0 6.6-5.4 12-12 12h-40c-6.6 0-12-5.4-12-12v-40zm0 128c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v40c0 6.6-5.4 12-12 12h-40c-6.6 0-12-5.4-12-12v-40zM64 268c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v40c0 6.6-5.4 12-12 12H76c-6.6 0-12-5.4-12-12v-40zm0 128c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v40c0 6.6-5.4 12-12 12H76c-6.6 0-12-5.4-12-12v-40zM400 64h-48V16c0-8.8-7.2-16-16-16h-32c-8.8 0-16 7.2-16 16v48H160V16c0-8.8-7.2-16-16-16h-32c-8.8 0-16 7.2-16 16v48H48C21.5 64 0 85.5 0 112v48h448v-48c0-26.5-21.5-48-48-48z'/%3E%3C/svg%3E%0A"); + --pf-c-form-control--m-icon-sprite--m-clock--BackgroundUrl: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath fill='%236a6e73' d='M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm61.8-104.4-84.9-61.7c-3.1-2.3-4.9-5.9-4.9-9.7V116c0-6.6 5.4-12 12-12h32c6.6 0 12 5.4 12 12v141.7l66.8 48.6c5.4 3.9 6.5 11.4 2.6 16.8L334.6 349c-3.9 5.3-11.4 6.5-16.8 2.6z'/%3E%3C/svg%3E%0A"); + --pf-c-form-control--m-icon-sprite__select--BackgroundSize: var(--pf-c-form-control--FontSize); + --pf-c-form-control--m-icon-sprite__select--BackgroundPositionX: calc(100% - var(--pf-global--spacer--md, 1rem) + 7px); + --pf-c-form-control--m-icon-sprite__select--success--BackgroundPosition: calc(100% - var(--pf-global--spacer--md, 1rem) + 1px - var(--pf-global--spacer--lg)); + --pf-c-form-control--m-icon-sprite__select--m-warning--BackgroundPosition: calc(100% - var(--pf-global--spacer--md, 1rem) - var(--pf-global--spacer--lg) + 0.0625rem); + --pf-c-form-control--m-icon-sprite__select--invalid--BackgroundPosition: calc(100% - var(--pf-global--spacer--md, 1rem) - var(--pf-global--spacer--lg)); +} diff --git a/elements/package.json b/elements/package.json index 9685158d59..7dbcec36e9 100644 --- a/elements/package.json +++ b/elements/package.json @@ -10,39 +10,31 @@ "types": "./pfe.d.ts", "exports": { ".": "./pfe.min.js", - "./pf-accordion/BaseAccordion.js": "./pf-accordion/BaseAccordion.js", - "./pf-accordion/BaseAccordionHeader.js": "./pf-accordion/BaseAccordionHeader.js", - "./pf-accordion/BaseAccordionPanel.js": "./pf-accordion/BaseAccordionPanel.js", + "./form-control.css": "./form-control.css", + "./form-control.css.js": "./form-control.css.js", "./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-avatar/BaseAvatar.js": "./pf-avatar/BaseAvatar.js", "./pf-avatar/pf-avatar.js": "./pf-avatar/pf-avatar.js", "./pf-back-to-top/pf-back-to-top.js": "./pf-back-to-top/pf-back-to-top.js", "./pf-background-image/pf-background-image.js": "./pf-background-image/pf-background-image.js", - "./pf-badge/BaseBadge.js": "./pf-badge/BaseBadge.js", "./pf-badge/pf-badge.js": "./pf-badge/pf-badge.js", "./pf-banner/pf-banner.js": "./pf-banner/pf-banner.js", - "./pf-button/BaseButton.js": "./pf-button/BaseButton.js", "./pf-button/pf-button.js": "./pf-button/pf-button.js", - "./pf-card/BaseCard.js": "./pf-card/BaseCard.js", "./pf-card/pf-card.js": "./pf-card/pf-card.js", "./pf-chip/pf-chip.js": "./pf-chip/pf-chip.js", "./pf-chip/pf-chip-group.js": "./pf-chip/pf-chip-group.js", - "./pf-clipboard-copy/BaseClipboardCopy.js": "./pf-clipboard-copy/BaseClipboardCopy.js", "./pf-clipboard-copy/pf-clipboard-copy.js": "./pf-clipboard-copy/pf-clipboard-copy.js", - "./pf-code-block/BaseCodeBlock.js": "./pf-code-block/BaseCodeBlock.js", "./pf-code-block/pf-code-block.js": "./pf-code-block/pf-code-block.js", "./pf-dropdown/pf-dropdown.js": "./pf-dropdown/pf-dropdown.js", "./pf-dropdown/pf-dropdown-group.js": "./pf-dropdown/pf-dropdown-group.ts", "./pf-dropdown/pf-dropdown-menu.js": "./pf-dropdown/pf-dropdown-menu.ts", "./pf-dropdown/pf-dropdown-item.js": "./pf-dropdown/pf-dropdown-item.ts", - "./pf-icon/BaseIcon.js": "./pf-icon/BaseIcon.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", "./pf-jump-links/pf-jump-links-list.js": "./pf-jump-links/pf-jump-links-list.js", "./pf-jump-links/pf-jump-links.js": "./pf-jump-links/pf-jump-links.js", - "./pf-label/BaseLabel.js": "./pf-label/BaseLabel.js", "./pf-label/pf-label.js": "./pf-label/pf-label.js", "./pf-select/pf-select.js": "./pf-select/pf-select.js", "./pf-select/pf-listbox.js": "./pf-select/pf-listbox.js", @@ -53,9 +45,7 @@ "./pf-progress-stepper/pf-progress-step.js": "./pf-progress-stepper/pf-progress-step.js", "./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-spinner/BaseSpinner.js": "./pf-spinner/BaseSpinner.js", "./pf-spinner/pf-spinner.js": "./pf-spinner/pf-spinner.js", - "./pf-switch/BaseSwitch.js": "./pf-switch/BaseSwitch.js", "./pf-switch/pf-switch.js": "./pf-switch/pf-switch.js", "./pf-table/pf-table.js": "./pf-table/pf-table.js", "./pf-table/pf-thead.js": "./pf-table/pf-thead.js", @@ -64,18 +54,13 @@ "./pf-table/pf-th.js": "./pf-table/pf-th.js", "./pf-table/pf-td.js": "./pf-table/pf-td.js", "./pf-table/pf-caption.js": "./pf-table/pf-caption.js", - "./pf-tabs/BaseTab.js": "./pf-tabs/BaseTab.js", - "./pf-tabs/BaseTabPanel.js": "./pf-tabs/BaseTabPanel.js", - "./pf-tabs/BaseTabs.js": "./pf-tabs/BaseTabs.js", "./pf-tabs/pf-tab-panel.js": "./pf-tabs/pf-tab-panel.js", "./pf-tabs/pf-tab.js": "./pf-tabs/pf-tab.js", "./pf-tabs/pf-tabs.js": "./pf-tabs/pf-tabs.js", "./pf-text-area/pf-text-area.js": "./pf-text-area/pf-text-area.js", "./pf-text-input/pf-text-input.js": "./pf-text-input/pf-text-input.js", - "./pf-tile/BaseTile.js": "./pf-tile/BaseTile.js", "./pf-tile/pf-tile.js": "./pf-tile/pf-tile.js", "./pf-timestamp/pf-timestamp.js": "./pf-timestamp/pf-timestamp.js", - "./pf-tooltip/BaseTooltip.js": "./pf-tooltip/BaseTooltip.js", "./pf-tooltip/pf-tooltip.js": "./pf-tooltip/pf-tooltip.js", "./pf-popover/pf-popover.js": "./pf-popover/pf-popover.js", "./react/*": "./react/*" @@ -143,10 +128,10 @@ "Ajinyka Shinde " ], "dependencies": { - "@lit/context": "^1.1.0", - "@patternfly/icons": "^1.0.2", + "@lit/context": "^1.1.2", + "@patternfly/icons": "^1.0.3", "@patternfly/pfe-core": "^3.0.0", - "lit": "^3.1.2", - "tslib": "^2.6.2" + "lit": "^3.1.4", + "tslib": "^2.6.3" } } diff --git a/elements/pf-accordion/BaseAccordion.ts b/elements/pf-accordion/BaseAccordion.ts deleted file mode 100644 index a79afd49a3..0000000000 --- a/elements/pf-accordion/BaseAccordion.ts +++ /dev/null @@ -1,327 +0,0 @@ -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/BaseAccordionHeader.css b/elements/pf-accordion/BaseAccordionHeader.css deleted file mode 100644 index d6de7bfbfe..0000000000 --- a/elements/pf-accordion/BaseAccordionHeader.css +++ /dev/null @@ -1,39 +0,0 @@ -#heading { - font-size: 100%; - padding: 0; - margin: 0; -} - -button, -a { - cursor: pointer; -} - -.toggle, -.toggle:before, -.toggle:after { - padding: 0; - margin: 0; -} - -.toggle { - position: relative; - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; - border: 0; -} - -.toggle:after { - content: ""; - position: absolute; - bottom: 0; - left: 0; -} - -span { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} diff --git a/elements/pf-accordion/BaseAccordionHeader.ts b/elements/pf-accordion/BaseAccordionHeader.ts deleted file mode 100644 index 07e5e1f9dd..0000000000 --- a/elements/pf-accordion/BaseAccordionHeader.ts +++ /dev/null @@ -1,145 +0,0 @@ -import type { TemplateResult } from 'lit'; - -import { LitElement, html } from 'lit'; -import { property } from 'lit/decorators/property.js'; - -import { BaseAccordion } from './BaseAccordion.js'; -import { ComposedEvent } from '@patternfly/pfe-core'; -import { getRandomId } from '@patternfly/pfe-core/functions/random.js'; -import { Logger } from '@patternfly/pfe-core/controllers/logger.js'; - -import style from './BaseAccordionHeader.css'; - -const isPorHeader = - (el: Node): el is HTMLElement => - el instanceof HTMLElement && !!el.tagName.match(/P|^H[1-6]/); - -export class AccordionHeaderChangeEvent extends ComposedEvent { - declare target: BaseAccordionHeader; - constructor( - public expanded: boolean, - public toggle: BaseAccordionHeader, - public accordion: BaseAccordion - ) { - super('change'); - } -} - -export abstract class BaseAccordionHeader extends LitElement { - static readonly styles = [style]; - - static override readonly shadowRootOptions = { - ...LitElement.shadowRootOptions, - delegatesFocus: true, - }; - - @property({ type: Boolean, reflect: true }) expanded = false; - - @property({ reflect: true, attribute: 'heading-text' }) headingText?: string; - - @property({ reflect: true, attribute: 'heading-tag' }) headingTag?: string; - - #generatedHtag?: HTMLHeadingElement; - - #logger = new Logger(this); - - #header?: HTMLElement; - - override connectedCallback() { - super.connectedCallback(); - this.addEventListener('click', this.#onClick); - this.hidden = true; - this.id ||= getRandomId(this.localName); - this.#initHeader(); - } - - async #initHeader() { - if (this.headingText && !this.headingTag) { - this.headingTag = 'h3'; - } - this.#header = this.#getOrCreateHeader(); - - // prevent double-logging - if (this.#header !== this.#generatedHtag) { - this.#generatedHtag = undefined; - } - - do { - await this.updateComplete; - } while (!await this.updateComplete); - - // Remove the hidden attribute after upgrade - this.hidden = false; - } - - /** Template hook: before */ - renderAfterButton?(): TemplateResult; - - override render(): TemplateResult { - switch (this.headingTag) { - case 'h1': return html`

${this.#renderHeaderContent()}

`; - case 'h2': return html`

${this.#renderHeaderContent()}

`; - case 'h3': return html`

${this.#renderHeaderContent()}

`; - case 'h4': return html`

${this.#renderHeaderContent()}

`; - case 'h5': return html`
${this.#renderHeaderContent()}
`; - case 'h6': return html`
${this.#renderHeaderContent()}
`; - default: return this.#renderHeaderContent(); - } - } - - #renderHeaderContent() { - const headingText = this.headingText?.trim() ?? this.#header?.textContent?.trim(); - return html` - - `; - } - - #getOrCreateHeader(): HTMLElement | undefined { - // Check if there is no nested element or nested textNodes - if (!this.firstElementChild && !this.firstChild) { - return void this.#logger.warn('No header content provided'); - } else if (this.firstElementChild) { - const [heading, ...otherContent] = Array.from(this.children) - .filter((x): x is HTMLElement => !x.hasAttribute('slot') && isPorHeader(x)); - - // If there is no content inside the slot, return empty with a warning - // else, if there is more than 1 element in the slot, capture the first h-tag - if (!heading) { - return void this.#logger.warn('No heading information was provided.'); - } else if (otherContent.length) { - this.#logger.warn('Heading currently only supports 1 tag; extra tags will be ignored.'); - } - return heading; - } else { - if (!this.#generatedHtag) { - this.#logger.warn('Header should contain at least 1 heading tag for correct semantics.'); - } - this.#generatedHtag = document.createElement('h3'); - - // If a text node was provided but no semantics, default to an h3 - // otherwise, incorrect semantics were used, create an H3 and try to capture the content - if (this.firstChild?.nodeType === Node.TEXT_NODE) { - this.#generatedHtag.textContent = this.firstChild.textContent; - } else { - this.#generatedHtag.textContent = this.textContent; - } - - return this.#generatedHtag; - } - } - - #onClick(event: MouseEvent) { - const expanded = !this.expanded; - const acc = event.composedPath().find(BaseAccordion.isAccordion); - if (acc) { - this.dispatchEvent(new AccordionHeaderChangeEvent(expanded, this, acc)); - } - } -} diff --git a/elements/pf-accordion/BaseAccordionPanel.css b/elements/pf-accordion/BaseAccordionPanel.css deleted file mode 100644 index da1f4f4f89..0000000000 --- a/elements/pf-accordion/BaseAccordionPanel.css +++ /dev/null @@ -1,27 +0,0 @@ -:host { - display: none; - overflow: hidden; - will-change: height; -} - -:host([expanded]) { - display: block; - position: relative; -} - -:host([fixed]) { - overflow-y: auto; -} - -.body { - position: relative; - overflow: hidden; -} - -.body:after { - content: ""; - position: absolute; - top: 0; - bottom: 0; - left: 0; -} diff --git a/elements/pf-accordion/BaseAccordionPanel.ts b/elements/pf-accordion/BaseAccordionPanel.ts deleted file mode 100644 index ba8067eec9..0000000000 --- a/elements/pf-accordion/BaseAccordionPanel.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { LitElement, html } from 'lit'; -import { property } from 'lit/decorators/property.js'; - -import { getRandomId } from '@patternfly/pfe-core/functions/random.js'; - -import style from './BaseAccordionPanel.css'; - -export class BaseAccordionPanel extends LitElement { - static readonly styles = [style]; - - @property({ type: Boolean, reflect: true }) expanded = false; - - connectedCallback() { - super.connectedCallback(); - this.id ||= getRandomId(this.localName); - this.setAttribute('role', 'region'); - } - - override render() { - return html` -
-
-
- -
-
-
- `; - } -} diff --git a/elements/pf-accordion/demo/pf-accordion.html b/elements/pf-accordion/demo/pf-accordion.html index dc76ddd6c0..e410992332 100644 --- a/elements/pf-accordion/demo/pf-accordion.html +++ b/elements/pf-accordion/demo/pf-accordion.html @@ -1,60 +1,51 @@ -
- - -

Level One - Item one

-
- -

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore - magna aliqua.

-
- -

Level One - Item two

-
- -

Vivamus et tortor sed arcu congue vehicula eget et diam. Praesent nec dictum lorem. Aliquam id diam ultrices, - faucibus erat id, maximus nunc.

-
- -

Level One - Item three

-
- -

Morbi vitae urna quis nunc convallis hendrerit. Aliquam congue orci quis ultricies tempus.

-
- -

Level One - Item four

-
- -

- Donec vel posuere orci. Phasellus quis tortor a ex hendrerit efficitur. Aliquam lacinia ligula pharetra, - sagittis ex ut, pellentesque diam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere - cubilia Curae; - Vestibulum ultricies nulla nibh. Etiam vel dui fermentum ligula ullamcorper eleifend non quis tortor. Morbi - tempus ornare tempus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. - Mauris - et velit neque. Donec ultricies condimentum mauris, pellentesque imperdiet libero convallis convallis. Aliquam - erat volutpat. Donec rutrum semper tempus. Proin dictum imperdiet nibh, quis dapibus nulla. Integer sed - tincidunt - lectus, sit amet auctor eros. -

-
- -

Level One - Item five

-
- -

Vivamus finibus dictum ex id ultrices. Mauris dictum neque a iaculis blandit.

-
-
-
- - + + +

Level One - Item one

+
+ +

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore + magna aliqua.

+
+ +

Level One - Item two

+
+ +

Vivamus et tortor sed arcu congue vehicula eget et diam. Praesent nec dictum lorem. Aliquam id diam ultrices, + faucibus erat id, maximus nunc.

+
+ +

Level One - Item three

+
+ +

Morbi vitae urna quis nunc convallis hendrerit. Aliquam congue orci quis ultricies tempus.

+
+ +

Level One - Item four

+
+ +

+ Donec vel posuere orci. Phasellus quis tortor a ex hendrerit efficitur. Aliquam lacinia ligula pharetra, + sagittis ex ut, pellentesque diam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere + cubilia Curae; + Vestibulum ultricies nulla nibh. Etiam vel dui fermentum ligula ullamcorper eleifend non quis tortor. Morbi + tempus ornare tempus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. + Mauris + et velit neque. Donec ultricies condimentum mauris, pellentesque imperdiet libero convallis convallis. Aliquam + erat volutpat. Donec rutrum semper tempus. Proin dictum imperdiet nibh, quis dapibus nulla. Integer sed + tincidunt + lectus, sit amet auctor eros. +

+
+ +

Level One - Item five

+
+ +

Vivamus finibus dictum ex id ultrices. Mauris dictum neque a iaculis blandit.

+
+
- diff --git a/elements/pf-accordion/demo/single-expanded-panel.html b/elements/pf-accordion/demo/single-expanded-panel.html index 164fa17229..97b4e07160 100644 --- a/elements/pf-accordion/demo/single-expanded-panel.html +++ b/elements/pf-accordion/demo/single-expanded-panel.html @@ -1,58 +1,49 @@ -
- - -

Level One - Item one

-
- -

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore - magna aliqua.

-
- -

Level One - Item two

-
- -

Vivamus et tortor sed arcu congue vehicula eget et diam. Praesent nec dictum lorem. Aliquam id diam ultrices, - faucibus erat id, maximus nunc.

-
- -

Level One - Item three

-
- -

Morbi vitae urna quis nunc convallis hendrerit. Aliquam congue orci quis ultricies tempus.

-
- -

Level One - Item four

-
- -

- Donec vel posuere orci. Phasellus quis tortor a ex hendrerit efficitur. Aliquam lacinia ligula pharetra, - sagittis ex ut, pellentesque diam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere - cubilia Curae; - Vestibulum ultricies nulla nibh. Etiam vel dui fermentum ligula ullamcorper eleifend non quis tortor. Morbi - tempus ornare tempus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. - Mauris - et velit neque. Donec ultricies condimentum mauris, pellentesque imperdiet libero convallis convallis. Aliquam - erat volutpat. Donec rutrum semper tempus. Proin dictum imperdiet nibh, quis dapibus nulla. Integer sed - tincidunt - lectus, sit amet auctor eros. -

-
- -

Level One - Item five

-
- -

Vivamus finibus dictum ex id ultrices. Mauris dictum neque a iaculis blandit.

-
-
-
- + + +

Level One - Item one

+
+ +

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore + magna aliqua.

+
+ +

Level One - Item two

+
+ +

Vivamus et tortor sed arcu congue vehicula eget et diam. Praesent nec dictum lorem. Aliquam id diam ultrices, + faucibus erat id, maximus nunc.

+
+ +

Level One - Item three

+
+ +

Morbi vitae urna quis nunc convallis hendrerit. Aliquam congue orci quis ultricies tempus.

+
+ +

Level One - Item four

+
+ +

+ Donec vel posuere orci. Phasellus quis tortor a ex hendrerit efficitur. Aliquam lacinia ligula pharetra, + sagittis ex ut, pellentesque diam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere + cubilia Curae; + Vestibulum ultricies nulla nibh. Etiam vel dui fermentum ligula ullamcorper eleifend non quis tortor. Morbi + tempus ornare tempus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. + Mauris + et velit neque. Donec ultricies condimentum mauris, pellentesque imperdiet libero convallis convallis. Aliquam + erat volutpat. Donec rutrum semper tempus. Proin dictum imperdiet nibh, quis dapibus nulla. Integer sed + tincidunt + lectus, sit amet auctor eros. +

+
+ +

Level One - Item five

+
+ +

Vivamus finibus dictum ex id ultrices. Mauris dictum neque a iaculis blandit.

+
+
- - diff --git a/elements/pf-accordion/pf-accordion-header.css b/elements/pf-accordion/pf-accordion-header.css index 9351af8836..cec8e35d07 100644 --- a/elements/pf-accordion/pf-accordion-header.css +++ b/elements/pf-accordion/pf-accordion-header.css @@ -32,11 +32,21 @@ #heading { font-weight: var(--pf-c-accordion__toggle--FontWeight, var(--pf-global--FontWeight--normal, 400)); + font-size: 100%; + padding: 0; + margin: 0; +} + +button, +a { + cursor: pointer; } .toggle, .toggle:before, .toggle:after { + padding: 0; + margin: 0; background-color: var(--pf-c-accordion__toggle--BackgroundColor, transparent); } @@ -45,6 +55,12 @@ } .toggle { + position: relative; + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + border: 0; padding: var(--pf-c-accordion__toggle--PaddingTop, var(--pf-global--spacer--md, 0.5rem)) var(--pf-c-accordion__toggle--PaddingRight, var(--pf-global--spacer--md, 1rem)) @@ -77,9 +93,16 @@ top: var(--pf-c-accordion__toggle--before--Top, -1px); width: var(--pf-c-accordion__toggle--before--Width, var(--pf-global--BorderWidth--lg, 3px)); background-color: var(--pf-c-accordion__toggle--after--BackgroundColor, transparent); + content: ""; + position: absolute; + bottom: 0; + left: 0; } span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; max-width: var(--pf-c-accordion__toggle-text--MaxWidth, calc(100% - var(--pf-global--spacer--lg, 1.5rem))); } diff --git a/elements/pf-accordion/pf-accordion-header.ts b/elements/pf-accordion/pf-accordion-header.ts index 42347d740c..3ae5f97612 100644 --- a/elements/pf-accordion/pf-accordion-header.ts +++ b/elements/pf-accordion/pf-accordion-header.ts @@ -1,30 +1,44 @@ -import { html } from 'lit'; +import type { PfAccordion } from './pf-accordion.js'; + +import { LitElement, html, type TemplateResult } from 'lit'; import { customElement } from 'lit/decorators/custom-element.js'; import { property } from 'lit/decorators/property.js'; import { SlotController } from '@patternfly/pfe-core/controllers/slot-controller.js'; +import { Logger } from '@patternfly/pfe-core/controllers/logger.js'; -import { BaseAccordionHeader } from './BaseAccordionHeader.js'; +import { getRandomId } from '@patternfly/pfe-core/functions/random.js'; import style from './pf-accordion-header.css'; import '@patternfly/elements/pf-icon/pf-icon.js'; +const isPorHeader = + (el: Node): el is HTMLElement => + el instanceof HTMLElement && !!el.tagName.match(/P|^H[1-6]/); + +export class PfAccordionHeaderChangeEvent extends Event { + declare target: PfAccordionHeader; + constructor( + public expanded: boolean, + public toggle: PfAccordionHeader, + public accordion: PfAccordion + ) { + super('change', { bubbles: true }); + } +} + /** * Accordion Header - * * @csspart text - inline element containing the heading text or slotted heading content * @csspart accents - container for accents within the header * @csspart icon - caret icon - * * @slot * We expect the light DOM of the pf-accordion-header to be a heading level tag (h1, h2, h3, h4, h5, h6) * @slot accents * These elements will appear inline with the accordion header, between the header and the chevron * (or after the chevron and header in disclosure mode). - * * @fires {AccordionHeaderChangeEvent} change - when the open panels change - * * @cssprop {} --pf-c-accordion__toggle--Color * Sets the font color for the accordion header. * {@default `var(--pf-global--Color--100, #151515)`} @@ -81,8 +95,13 @@ import '@patternfly/elements/pf-icon/pf-icon.js'; * {@default `0.2s ease-in 0s`} */ @customElement('pf-accordion-header') -export class PfAccordionHeader extends BaseAccordionHeader { - static readonly styles = [...BaseAccordionHeader.styles, style]; +export class PfAccordionHeader extends LitElement { + static readonly styles: CSSStyleSheet[] = [style]; + + static override readonly shadowRootOptions: ShadowRootInit = { + ...LitElement.shadowRootOptions, + delegatesFocus: true, + }; @property({ reflect: true }) bordered?: 'true' | 'false'; @@ -90,19 +109,117 @@ export class PfAccordionHeader extends BaseAccordionHeader { @property({ reflect: true, attribute: 'icon-set' }) iconSet?: string; + @property({ type: Boolean, reflect: true }) expanded = false; + + @property({ reflect: true, attribute: 'heading-text' }) headingText?: string; + + @property({ reflect: true, attribute: 'heading-tag' }) headingTag?: string; + + #generatedHtag?: HTMLHeadingElement; + + #logger = new Logger(this); + + #header?: HTMLElement; + #slots = new SlotController(this, 'accents', null); - renderAfterButton() { - return html`${!this.#slots.hasSlotted('accents') ? '' : html` - - - `} - + override connectedCallback(): void { + super.connectedCallback(); + this.hidden = true; + this.id ||= getRandomId(this.localName); + this.#initHeader(); + } + + override render(): TemplateResult<1> { + const headingText = this.headingText?.trim() ?? this.#header?.textContent?.trim(); + const content = html` + `; + switch (this.headingTag) { + case 'h1': return html`

${content}

`; + case 'h2': return html`

${content}

`; + case 'h3': return html`

${content}

`; + case 'h4': return html`

${content}

`; + case 'h5': return html`
${content}
`; + case 'h6': return html`
${content}
`; + default: return content; + } + } + + async #initHeader() { + if (this.headingText) { + this.headingTag ||= 'h3'; + } + this.#header = this.#getOrCreateHeader(); + + // prevent double-logging + if (this.#header !== this.#generatedHtag) { + this.#generatedHtag = undefined; + } + + do { + await this.updateComplete; + } while (!await this.updateComplete); + + // Remove the hidden attribute after upgrade + this.hidden = false; + } + + #getOrCreateHeader(): HTMLElement | undefined { + // Check if there is no nested element or nested textNodes + if (!this.firstElementChild && !this.firstChild) { + return void this.#logger.warn('No header content provided'); + } else if (this.firstElementChild) { + const [heading, ...otherContent] = Array.from(this.children) + .filter((x): x is HTMLElement => !x.hasAttribute('slot') && isPorHeader(x)); + + // If there is no content inside the slot, return empty with a warning + // else, if there is more than 1 element in the slot, capture the first h-tag + if (!heading) { + return void this.#logger.warn('No heading information was provided.'); + } else if (otherContent.length) { + this.#logger.warn('Heading currently only supports 1 tag; extra tags will be ignored.'); + } + return heading; + } else { + if (!this.#generatedHtag) { + this.#logger.warn('Header should contain at least 1 heading tag for correct semantics.'); + } + this.#generatedHtag = document.createElement('h3'); + + // If a text node was provided but no semantics, default to an h3 + // otherwise, incorrect semantics were used, create an H3 and try to capture the content + if (this.firstChild?.nodeType === Node.TEXT_NODE) { + this.#generatedHtag.textContent = this.firstChild.textContent; + } else { + this.#generatedHtag.textContent = this.textContent; + } + + return this.#generatedHtag; + } + } + + #onClick() { + const expanded = !this.expanded; + const acc = this.closest('pf-accordion'); + if (acc) { + this.dispatchEvent(new PfAccordionHeaderChangeEvent(expanded, this, acc)); + } } } diff --git a/elements/pf-accordion/pf-accordion-panel.css b/elements/pf-accordion/pf-accordion-panel.css index cf780d1bc4..25e90c75ba 100644 --- a/elements/pf-accordion/pf-accordion-panel.css +++ b/elements/pf-accordion/pf-accordion-panel.css @@ -1,4 +1,8 @@ :host { + display: none; + position: relative; + overflow: hidden; + will-change: height; color: var(--pf-global--Color--100, #151515); background-color: var( @@ -16,6 +20,11 @@ } .body:after { + content: ""; + position: absolute; + top: 0; + bottom: 0; + left: 0; width: var(--pf-c-accordion__panel-body--before--Width, var(--pf-global--BorderWidth--lg, 3px)); background-color: var(--pf-c-accordion__panel-body--before--BackgroundColor, transparent); } @@ -46,9 +55,15 @@ } :host([fixed]) { + overflow-y: auto; max-height: var(--pf-c-accordion__panel--m-fixed--MaxHeight, 9.375rem); } +:host([expanded]) { + display: block; + position: relative; +} + .content[expanded], :host([expanded]) .content { --pf-c-accordion__panel-body--before--BackgroundColor: diff --git a/elements/pf-accordion/pf-accordion-panel.ts b/elements/pf-accordion/pf-accordion-panel.ts index 90fc37fe8b..9542477cb2 100644 --- a/elements/pf-accordion/pf-accordion-panel.ts +++ b/elements/pf-accordion/pf-accordion-panel.ts @@ -1,13 +1,13 @@ +import { LitElement, html, type TemplateResult } from 'lit'; import { customElement } from 'lit/decorators/custom-element.js'; import { property } from 'lit/decorators/property.js'; -import { BaseAccordionPanel } from './BaseAccordionPanel.js'; +import { getRandomId } from '@patternfly/pfe-core/functions/random.js'; import style from './pf-accordion-panel.css'; /** * Accordion Panel - * * @slot - Panel content * @cssprop {} --pf-c-accordion--BackgroundColor * Sets the background color for the panel content. @@ -45,10 +45,30 @@ import style from './pf-accordion-panel.css'; * {@default `var(--pf-global--BorderWidth--lg, 3px)`} */ @customElement('pf-accordion-panel') -export class PfAccordionPanel extends BaseAccordionPanel { - static readonly styles = [...BaseAccordionPanel.styles, style]; +export class PfAccordionPanel extends LitElement { + static readonly styles: CSSStyleSheet[] = [style]; + + @property({ type: Boolean, reflect: true }) expanded = false; @property({ reflect: true }) bordered?: 'true' | 'false'; + + override connectedCallback(): void { + super.connectedCallback(); + this.id ||= getRandomId(this.localName); + this.setAttribute('role', 'region'); + } + + override render(): TemplateResult<1> { + return html` +
+
+
+ +
+
+
+ `; + } } declare global { diff --git a/elements/pf-accordion/pf-accordion.ts b/elements/pf-accordion/pf-accordion.ts index b1de02153c..76f7c914f5 100644 --- a/elements/pf-accordion/pf-accordion.ts +++ b/elements/pf-accordion/pf-accordion.ts @@ -1,15 +1,38 @@ +import { LitElement, html, type TemplateResult } from 'lit'; import { observed } from '@patternfly/pfe-core/decorators.js'; import { property } from 'lit/decorators/property.js'; import { customElement } from 'lit/decorators/custom-element.js'; -import { BaseAccordion } from './BaseAccordion.js'; -import { BaseAccordionHeader } from './BaseAccordionHeader.js'; +import { RovingTabindexController } from '@patternfly/pfe-core/controllers/roving-tabindex-controller.js'; +import { NumberListConverter } from '@patternfly/pfe-core'; +import { Logger } from '@patternfly/pfe-core/controllers/logger.js'; + +import { PfAccordionHeader, PfAccordionHeaderChangeEvent } from './pf-accordion-header.js'; +import { PfAccordionPanel } from './pf-accordion-panel.js'; export * from './pf-accordion-header.js'; export * from './pf-accordion-panel.js'; import style from './pf-accordion.css'; +export class PfAccordionExpandEvent extends Event { + constructor( + public toggle: PfAccordionHeader, + public panel: PfAccordionPanel, + ) { + super('expand', { bubbles: true, cancelable: true }); + } +} + +export class PfAccordionCollapseEvent extends Event { + constructor( + public toggle: PfAccordionHeader, + public panel: PfAccordionPanel, + ) { + super('collapse', { bubbles: true, cancelable: true }); + } +} + /** * An **accordion** is an interactive container that expands and collapses to hide or reveal nested content. It takes advantage of progressive disclosure to help reduce page scrolling, by allowing users to choose whether they want to show or hide more detailed information as needed. * @summary Toggle the visibility of sections of content @@ -81,8 +104,8 @@ import style from './pf-accordion.css'; * @cssprop --pf-c-accordion--m-bordered__expanded-content--m-expanded__expanded-content-body--last-child--after--BorderBottomColor {@default var(--pf-global--BorderColor--100, #d2d2d2)} */ @customElement('pf-accordion') -export class PfAccordion extends BaseAccordion { - static readonly styles = [style]; +export class PfAccordion extends LitElement { + static readonly styles: CSSStyleSheet[] = [style]; /** When true, only one accordion panel may be expanded at a time */ @property({ reflect: true, type: Boolean }) single = false; @@ -98,39 +121,304 @@ export class PfAccordion extends BaseAccordion { @property({ type: Boolean, reflect: true }) fixed = false; - async firstUpdated() { - let index: number | null = null; - if (this.single) { - const allHeaders = [...this.querySelectorAll('pf-accordion-header')]; - const lastExpanded = allHeaders.filter(x => x.hasAttribute('expanded')).pop(); - if (lastExpanded) { - index = allHeaders.indexOf(lastExpanded); + /** + * 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(): number[] { + 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); + } + }); + } + } + + #logger = new Logger(this); + + // actually is read in #init, by the `||=` operator + // eslint-disable-next-line no-unused-private-class-members + #initialized = false; + + #mo = new MutationObserver(() => this.#init()); + + #headerIndex = new RovingTabindexController(this, { + getItems: () => this.headers, + }); + + #expandedIndex: number[] = []; + + protected expandedSets: Set = new Set(); + + get #activeHeader() { + const { headers } = this; + const index = headers.findIndex(header => header.matches(':focus,:focus-within')); + return index > -1 ? headers.at(index) : undefined; + } + + get headers(): PfAccordionHeader[] { + return this.#allHeaders(); + } + + get panels(): PfAccordionPanel[] { + return this.#allPanels(); + } + + connectedCallback(): void { + super.connectedCallback(); + this.addEventListener('change', this.#onChange as EventListener); + this.#mo.observe(this, { childList: true }); + this.#init(); + } + + render(): TemplateResult<1> { + return html` + + `; + } + + async firstUpdated(): Promise { + let lastExpandedIndex: number; + const { headers, single } = this; + const lastExpanded = headers.filter(x => x.hasAttribute('expanded')).pop(); + if (lastExpanded) { + lastExpandedIndex = headers.indexOf(lastExpanded); + } + headers.forEach((header, index) => { + if (header.expanded && (!single || index === lastExpandedIndex)) { + this.#expandHeader(header, index); + const panel = this.#panelForHeader(header); + if (panel) { + this.#expandPanel(panel); + } } + }); + } + + 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); + } + + /** + * 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); } - await super.firstUpdated(); - if (index !== null) { - this.headers.forEach((_, i) => { - this.headers.at(i)?.toggleAttribute('expanded', i === index); - this.panels.at(i)?.toggleAttribute('expanded', i === index); - }); + } + + #panelForHeader(header: PfAccordionHeader) { + const next = header.nextElementSibling; + if (!(next instanceof PfAccordionPanel)) { + return void this.#logger.error('Sibling element to a header needs to be a panel'); + } else { + return next; } } - override async expand(index: number, parentAccordion?: BaseAccordion) { - if (index === -1) { + #expandHeader(header: PfAccordionHeader, 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: PfAccordionPanel) { + panel.expanded = true; + panel.hidden = false; + } + + async #collapseHeader(header: PfAccordionHeader, index = this.#getIndex(header)) { + if (!this.expandedSets) { + await this.updateComplete; + } + this.expandedSets.delete(index); + header.expanded = false; + await header.updateComplete; + } + + async #collapsePanel(panel: PfAccordionPanel) { + await panel.updateComplete; + if (!panel.expanded) { return; } - const allHeaders: BaseAccordionHeader[] = this.headers; + panel.expanded = false; + panel.hidden = true; + } + + #onChange(event: PfAccordionHeaderChangeEvent) { + if (event instanceof PfAccordionHeaderChangeEvent && event.accordion === this) { + const index = this.#getIndex(event.target); + if (event.expanded) { + this.expand(index); + } else { + this.collapse(index); + } + event.stopPropagation(); + } + } + + #allHeaders(accordion: PfAccordion = this): 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 => + x instanceof PfAccordionPanel); + } + + #getIndex(el: Element | null) { + if (el instanceof PfAccordionHeader) { + return this.headers.findIndex(header => header.id === el.id); + } + + if (el instanceof PfAccordionPanel) { + 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(): void { + 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. + * Accepts an optional parent accordion to search for headers and panels. + * @param index index (0-based) of the panel to expand + */ + public async expand(index: number): Promise { + if (index === -1) { + return; + } // Get all the headers and capture the item by index value if (this.single) { await Promise.all([ - ...allHeaders.map((header, index) => header.expanded && this.collapse(index)), + ...this.headers.map((header, index) => header.expanded && this.collapse(index)), ]); } - await super.expand(index, parentAccordion); + const header = this.headers[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 PfAccordionExpandEvent(header, panel)); + + await this.updateComplete; + } + + /** + * Accepts a 0-based index value (integer) for the set of accordion items to collapse. + * @param index index (0-based) of the panel to collapse + */ + public async collapse(index: number): Promise { + 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 PfAccordionCollapseEvent(header, panel)); + await this.updateComplete; + } + + /** + * Accepts a 0-based index value (integer) for the set of accordion items to expand or collapse. + * @param index index (0-based) of the panel to toggle + */ + public async toggle(index: number): Promise { + const { headers } = this; + const header = headers[index]; + + if (!header.expanded) { + await this.expand(index); + } else { + await this.collapse(index); + } + } + + /** + * Expands all accordion items. + */ + public async expandAll(): Promise { + this.headers.forEach(header => this.#expandHeader(header)); + this.panels.forEach(panel => this.#expandPanel(panel)); + await this.updateComplete; + } + + + /** + * Collapses all accordion items. + */ + public async collapseAll(): Promise { + this.headers.forEach(header => this.#collapseHeader(header)); + this.panels.forEach(panel => this.#collapsePanel(panel)); + await this.updateComplete; } } 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-accordion/test/pf-accordion.spec.ts b/elements/pf-accordion/test/pf-accordion.spec.ts index 5bb00395e6..f5fb3a215e 100644 --- a/elements/pf-accordion/test/pf-accordion.spec.ts +++ b/elements/pf-accordion/test/pf-accordion.spec.ts @@ -2,6 +2,9 @@ import { expect, html, aTimeout, nextFrame } from '@open-wc/testing'; import { createFixture } from '@patternfly/pfe-tools/test/create-fixture.js'; import { sendKeys } from '@web/test-runner-commands'; +import { allUpdates, clickElementAtCenter } from '@patternfly/pfe-tools/test/utils.js'; +import { a11ySnapshot, querySnapshot } from '@patternfly/pfe-tools/test/a11y-snapshot.js'; + // Import the element we're testing. import { PfAccordion, PfAccordionPanel, PfAccordionHeader } from '@patternfly/elements/pf-accordion/pf-accordion.js'; import { PfSwitch } from '@patternfly/elements/pf-switch/pf-switch.js'; @@ -9,7 +12,6 @@ import { PfSwitch } from '@patternfly/elements/pf-switch/pf-switch.js'; import { Logger } from '@patternfly/pfe-core/controllers/logger.js'; import '@patternfly/pfe-tools/test/stub-logger.js'; -import { allUpdates } from '@patternfly/pfe-tools/test/utils.js'; describe('', function() { let element: PfAccordion; @@ -21,12 +23,12 @@ describe('', function() { let secondPanel: PfAccordionPanel; async function clickFirstHeader() { - header.click(); + await clickElementAtCenter(header); await allUpdates(element); } async function clickSecondHeader() { - secondHeader.click(); + await clickElementAtCenter(secondHeader); await allUpdates(element); } @@ -126,18 +128,24 @@ describe('', function() { describe('clicking the first header', function() { beforeEach(clickFirstHeader); - it('expands first pair', function() { - expect(header.shadowRoot!.querySelector('button')?.getAttribute('aria-expanded')).to.equal('true'); + it('expands first pair', async function() { + const snapshot = await a11ySnapshot(); + const expanded = snapshot?.children?.find(x => x.expanded); + const focused = snapshot?.children?.find(x => x.focused); + expect(expanded?.name).to.equal(header.textContent?.trim()); expect(header.expanded).to.be.true; expect(panel.hasAttribute('expanded')).to.be.true; expect(panel.expanded).to.be.true; + expect(expanded).to.equal(focused); }); describe('then clicking first header again', function() { beforeEach(clickFirstHeader); - it('collapses first pair', function() { - expect(header.shadowRoot!.querySelector('button')?.getAttribute('aria-expanded')).to.equal('false'); + it('collapses first pair', async function() { + const snapshot = await a11ySnapshot(); + const expanded = snapshot?.children?.find(x => x.expanded); + expect(expanded).to.not.be.ok; expect(header.expanded).to.be.false; expect(panel.hasAttribute('expanded')).to.be.false; expect(panel.expanded).to.be.false; @@ -335,7 +343,7 @@ describe('', function() { describe('with all panels open', function() { beforeEach(async function() { for (const header of element.querySelectorAll('pf-accordion-header')) { - header.click(); + await clickElementAtCenter(header); } await nextFrame(); }); @@ -1003,7 +1011,6 @@ describe('', function() { describe('with nested pf-accordion', function() { let topLevelHeaderOne: PfAccordionHeader; let topLevelHeaderTwo: PfAccordionHeader; - let topLevelHeaderThree: PfAccordionHeader; let topLevelPanelOne: PfAccordionPanel; let topLevelPanelTwo: PfAccordionPanel; @@ -1019,32 +1026,56 @@ describe('', function() { beforeEach(async function() { element = await createFixture(html` - + + top-header-1 + + top-panel-1 - - + + nest-1-header-1 + + + nest-1-panel-1 + - + + top-header-2 + + top-panel-2 - - - - - - + + nest-2-header-1 + + + nest-2-header-1 + + + nest-2-header-2 + + + nest-2-panel-2 + + + nest-2-header-3 + + + nest-2-panel-3 + - - - + + top-header-3 + + + top-panel-3 + `); topLevelHeaderOne = document.getElementById('header-1') as PfAccordionHeader; topLevelHeaderTwo = document.getElementById('header-2') as PfAccordionHeader; - topLevelHeaderThree = document.getElementById('header-3') as PfAccordionHeader; topLevelPanelOne = document.getElementById('panel-1') as PfAccordionPanel; topLevelPanelTwo = document.getElementById('panel-2') as PfAccordionPanel; @@ -1062,47 +1093,44 @@ describe('', function() { describe('clicking the first top-level heading', function() { beforeEach(async function() { - topLevelHeaderOne.click(); + await clickElementAtCenter(topLevelHeaderOne); await allUpdates(element); }); describe('then clicking the second top-level heading', function() { beforeEach(async function() { - topLevelHeaderTwo.click(); + await clickElementAtCenter(topLevelHeaderTwo); await allUpdates(element); }); describe('then clicking the first nested heading', function() { beforeEach(async function() { - nestedHeaderOne.click(); + await clickElementAtCenter(nestedHeaderOne); await allUpdates(element); }); describe('then clicking the second nested heading', function() { beforeEach(async function() { - nestedHeaderTwo.click(); + await clickElementAtCenter(nestedHeaderTwo); await allUpdates(element); }); - it('expands the first top-level pair', function() { - expect(topLevelHeaderOne.shadowRoot!.querySelector('button')?.getAttribute('aria-expanded'), 'top level header 1 button aria-expanded attr').to.equal('true'); - expect(topLevelHeaderOne.expanded, 'top level header 1 expanded DOM property').to.be.true; - expect(topLevelPanelOne.hasAttribute('expanded'), 'top level panel 1 expanded attr').to.be.true; - expect(topLevelPanelOne.expanded, 'top level panel 1 DOM property').to.be.true; + it('expands the first top-level pair', async function() { + const snapshot = await a11ySnapshot(); + const expanded = snapshot?.children?.find(x => x.expanded); + expect(expanded?.name).to.equal(topLevelHeaderOne.textContent?.trim()); + expect(topLevelHeaderOne.expanded).to.be.true; + expect(topLevelPanelOne.hasAttribute('expanded')).to.be.true; + expect(topLevelPanelOne.expanded).to.be.true; }); - it('collapses the second top-level pair', function() { - expect(topLevelHeaderTwo.shadowRoot!.querySelector('button')?.getAttribute('aria-expanded'), 'top level header 2 button aria-expanded attr').to.equal('true'); - expect(topLevelHeaderTwo.expanded, 'top level header 2 expanded DOM property').to.be.true; - expect(topLevelPanelTwo.hasAttribute('expanded'), 'top level panel 2 expanded attr').to.be.true; - expect(topLevelPanelTwo.expanded, 'top level panel 2 expanded DOM property').to.be.true; + it('collapses the second top-level pair', async function() { + const snapshot = await a11ySnapshot(); + const header2 = querySnapshot(snapshot, { name: 'top-header-2' }); + expect(header2).to.have.property('expanded', true); }); - it('collapses the first nested pair', function() { - expect(nestedHeaderOne.shadowRoot!.querySelector('button')?.getAttribute('aria-expanded'), 'nested header 1 button aria-expanded attr').to.equal('false'); - expect(nestedHeaderOne.expanded, 'nested header 1 expanded DOM property').to.be.false; - expect(nestedPanelOne.hasAttribute('expanded'), 'nested panel 1 expanded attr').to.be.false; - expect(nestedPanelOne.expanded, 'nested panel 1 expanded DOM property').to.be.false; + it('collapses the first nested pair', async function() { + const snapshot = await a11ySnapshot(); + expect(querySnapshot(snapshot, { name: 'nest-1-header-1' })).to.not.have.property('expanded'); }); - it('collapses the second nested pair', function() { - expect(nestedHeaderTwo.shadowRoot!.querySelector('button')?.getAttribute('aria-expanded'), 'nested header 2 button aria-expanded attr').to.equal('true'); - expect(nestedHeaderTwo.expanded, 'nested header 2 expanded DOM property').to.be.true; - expect(nestedPanelTwo.hasAttribute('expanded'), 'nested panel 2 expanded attr').to.be.true; - expect(nestedPanelTwo.expanded, 'nested panel 2 expanded DOM property').to.be.true; + it('collapses the second nested pair', async function() { + const snapshot = await a11ySnapshot(); + expect(querySnapshot(snapshot, { name: 'nest-2-header-1' })).to.not.have.property('expanded'); }); }); }); @@ -1122,9 +1150,10 @@ describe('', function() { describe('with all panels open', function() { beforeEach(async function() { - for (const header of element.querySelectorAll('pf-accordion-header')) { - header.click(); - } + await Promise.all(Array.from( + document.querySelectorAll('pf-accordion'), + accordion => accordion.expandAll(), + )); await nextFrame(); }); it('removes hidden attribute from all panels', function() { @@ -1278,7 +1307,7 @@ describe('', function() { describe('with all panels open', function() { beforeEach(async function() { for (const header of multipleAccordionElements.querySelectorAll('pf-accordion-header')) { - header.click(); + await clickElementAtCenter(header); } await nextFrame(); }); @@ -1324,7 +1353,6 @@ describe('', function() { describe('with a single expanded header and panel containing a checkbox and a switch', function() { let element: PfAccordion; - let headers: NodeListOf; let panels: NodeListOf; let checkbox: HTMLInputElement; let pfswitch: PfSwitch; @@ -1340,7 +1368,6 @@ describe('', function() { `); - headers = document.querySelectorAll('pf-accordion-header'); panels = document.querySelectorAll('pf-accordion-panel'); checkbox = element.querySelector('input')!; pfswitch = element.querySelector('pf-switch')!; @@ -1351,7 +1378,7 @@ describe('', function() { describe('clicking the checkbox', function() { beforeEach(async function() { - checkbox.click(); + await clickElementAtCenter(checkbox); await element.updateComplete; }); it('does not collapse the panel', function() { @@ -1362,7 +1389,7 @@ describe('', function() { describe('clicking the switch', function() { beforeEach(async function() { const { checked } = pfswitch; - pfswitch.click(); + await clickElementAtCenter(pfswitch); await element.updateComplete; await pfswitch.updateComplete; expect(pfswitch.checked).to.not.equal(checked); diff --git a/elements/pf-avatar/BaseAvatar.css b/elements/pf-avatar/BaseAvatar.css deleted file mode 100644 index 17dfb3d9a1..0000000000 --- a/elements/pf-avatar/BaseAvatar.css +++ /dev/null @@ -1,13 +0,0 @@ -:host { - display: contents; -} - -svg, -:host([src]) img { - display: inline; - object-fit: cover; -} - -:host([hidden]) { - display: none; -} diff --git a/elements/pf-avatar/BaseAvatar.ts b/elements/pf-avatar/BaseAvatar.ts deleted file mode 100644 index 970f46a43d..0000000000 --- a/elements/pf-avatar/BaseAvatar.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { LitElement, html } from 'lit'; -import { property } from 'lit/decorators/property.js'; - -import style from './BaseAvatar.css'; - -export class AvatarLoadEvent extends Event { - constructor(public originalEvent: Event) { - super('load', { bubbles: true, composed: true }); - } -} - -/** - * Avatar is an element for displaying a user's avatar image. - * - * - * @summary For displaying a user's avatar image - */ - -export class BaseAvatar extends LitElement { - static readonly styles = [style]; - - /** The URL to the user's custom avatar image. */ - @property() src?: string; - - /** The alt text for the avatar image. */ - @property({ reflect: true }) alt?: string = 'Avatar image'; - - /** Size of the Avatar */ - @property({ reflect: true }) size: 'sm' | 'md' | 'lg' | 'xl' = 'sm'; - - /** Whether or not the Avatar image is dark */ - @property({ type: Boolean, reflect: true }) dark = false; - - render() { - return this.src != null ? html` - ${this.alt - ` : this.dark ? html` - - - - - ` : html` - - - - - - `; - } -} diff --git a/elements/pf-avatar/pf-avatar.css b/elements/pf-avatar/pf-avatar.css index 319668ddfc..6a72785021 100644 --- a/elements/pf-avatar/pf-avatar.css +++ b/elements/pf-avatar/pf-avatar.css @@ -1,40 +1,66 @@ +:host { + display: inline-block; + --pf-c-avatar--BorderColor: transparent; + --pf-c-avatar--BorderWidth: 0; + --pf-c-avatar--BorderRadius: var(--pf-global--BorderRadius--lg, 30em); + --pf-c-avatar--Width: 2.25rem; + --pf-c-avatar--Height: 2.25rem; + --pf-c-avatar--m-sm--Width: 1.5rem; + --pf-c-avatar--m-sm--Height: 1.5rem; + --pf-c-avatar--m-md--Width: 2.25rem; + --pf-c-avatar--m-md--Height: 2.25rem; + --pf-c-avatar--m-lg--Width: 4.5rem; + --pf-c-avatar--m-lg--Height: 4.5rem; + --pf-c-avatar--m-xl--Width: 8rem; + --pf-c-avatar--m-xl--Height: 8rem; + --pf-c-avatar--m-light--BorderColor: var(--pf-global--BorderColor--dark-100, #d2d2d2); + --pf-c-avatar--m-light--BorderWidth: var(--pf-global--BorderWidth--sm, 1px); + --pf-c-avatar--m-dark--BorderColor: var(--pf-global--palette--black-700, #4f5255); + --pf-c-avatar--m-dark--BorderWidth: var(--pf-global--BorderWidth--sm, 1px); + width: var(--pf-c-avatar--Width); + height: var(--pf-c-avatar--Height); + border-radius: var(--pf-c-avatar--BorderRadius); +} + +:host([hidden]), +[hidden] { + display: none !important; +} + svg, img { - width: var(--pf-c-avatar--Width, 24px); - height: var(--pf-c-avatar--Height, 24px); - border-radius: var(--pf-c-avatar--BorderRadius, var(--pf-global--BorderRadius--lg, 128px)); - border: - var(--pf-c-avatar--BorderWidth, 0) - solid - var(--pf-c-avatar--BorderColor, - var(--pf-global--BorderColor--dark-100, #d2d2d2)); + display: inline; + object-fit: cover; + width: var(--pf-c-avatar--Width); + height: var(--pf-c-avatar--Height); + border-radius: var(--pf-c-avatar--BorderRadius); + border: var(--pf-c-avatar--BorderWidth) solid var(--pf-c-avatar--BorderColor); } -:host([border]) :is(img, svg) { +:host([border]) { --pf-c-avatar--BorderWidth: var(--pf-global--BorderWidth--sm, 1px); } -:host([border="dark"]) :is(img, svg) { - --pf-c-avatar--BorderColor: var(--pf-c-avatar--m-dark--BorderColor, - var(--pf-global--palette--black-700, #4f5255)); +:host([border="dark"]) { + --pf-c-avatar--BorderColor: var(--pf-c-avatar--m-dark--BorderColor); } -:host([size='sm']) { - --pf-c-avatar--Width: var(--pf-c-avatar--m-sm--Width, 24px); - --pf-c-avatar--Height: var(--pf-c-avatar--m-sm--Height, 24px); +:host([size="sm"]) { + --pf-c-avatar--Width: var(--pf-c-avatar--m-sm--Width); + --pf-c-avatar--Height: var(--pf-c-avatar--m-sm--Height); } -:host([size='md']) { - --pf-c-avatar--Width: var(--pf-c-avatar--m-md--Width, 36px); - --pf-c-avatar--Height: var(--pf-c-avatar--m-md--Height, 36px); +:host([size="md"]) { + --pf-c-avatar--Width: var(--pf-c-avatar--m-md--Width); + --pf-c-avatar--Height: var(--pf-c-avatar--m-md--Height); } -:host([size='lg']) { - --pf-c-avatar--Width: var(--pf-c-avatar--m-lg--Width, 72px); - --pf-c-avatar--Height: var(--pf-c-avatar--m-lg--Height, 72px); +:host([size="lg"]) { + --pf-c-avatar--Width: var(--pf-c-avatar--m-lg--Width); + --pf-c-avatar--Height: var(--pf-c-avatar--m-lg--Height); } -:host([size='xl']) { - --pf-c-avatar--Width: var(--pf-c-avatar--m-xl--Width, 128px); - --pf-c-avatar--Height: var(--pf-c-avatar--m-xl--Height, 128px); +:host([size="xl"]) { + --pf-c-avatar--Width: var(--pf-c-avatar--m-xl--Width); + --pf-c-avatar--Height: var(--pf-c-avatar--m-xl--Height); } diff --git a/elements/pf-avatar/pf-avatar.ts b/elements/pf-avatar/pf-avatar.ts index 06c4c8bf25..d4aa243830 100644 --- a/elements/pf-avatar/pf-avatar.ts +++ b/elements/pf-avatar/pf-avatar.ts @@ -1,24 +1,78 @@ +import { LitElement, html, type TemplateResult } from 'lit'; import { property } from 'lit/decorators/property.js'; import { customElement } from 'lit/decorators/custom-element.js'; -import { BaseAvatar } from './BaseAvatar.js'; - import style from './pf-avatar.css'; +export class PfAvatarLoadEvent extends Event { + constructor(public originalEvent: Event) { + super('load', { bubbles: true }); + } +} + /** * An **avatar** is a visual used to represent a user. It may contain an image or a placeholder graphic. - * * @summary For displaying a user's avatar image + * @fires {PfAvatarLoadEvent} load - when the avatar loads + * @cssprop [--pf-c-avatar--Width=24px] + * @cssprop [--pf-c-avatar--Height=24px] + * @cssprop [--pf-c-avatar--BorderRadius=var(--pf-global--BorderRadius--lg, 128px)] + * @cssprop [--pf-c-avatar--BorderWidth=0] + * @cssprop [--pf-c-avatar--BorderColor=var(--pf-global--BorderColor--dark-100, #d2d2d2)] + * @cssprop [--pf-c-avatar--m-dark--BorderColor=var(--pf-global--palette--black-700, #4f5255)] + * @cssprop [--pf-c-avatar--m-sm--Width=24px] + * @cssprop [--pf-c-avatar--m-sm--Height=24px] + * @cssprop [--pf-c-avatar--m-md--Width=36px] + * @cssprop [--pf-c-avatar--m-md--Height=36px] + * @cssprop [--pf-c-avatar--m-lg--Width=72px] + * @cssprop [--pf-c-avatar--m-lg--Height=72px] + * @cssprop [--pf-c-avatar--m-xl--Width=28px] + * @cssprop [--pf-c-avatar--m-xl--Height=28px] */ @customElement('pf-avatar') -export class PfAvatar extends BaseAvatar { - static readonly styles = [style]; +export class PfAvatar extends LitElement { + static readonly styles: CSSStyleSheet[] = [style]; + + /** The URL to the user's custom avatar image. */ + @property() src?: string; + + /** The alt text for the avatar image. */ + @property({ reflect: true }) alt?: string = 'Avatar image'; /** Size of the Avatar */ @property({ reflect: true }) size: 'sm' | 'md' | 'lg' | 'xl' = 'sm'; /** Whether to display a border around the avatar */ @property({ reflect: true }) border?: 'light' | 'dark'; + + /** Whether or not the Avatar image is dark */ + @property({ type: Boolean, reflect: true }) dark = false; + + render(): TemplateResult<1> { + return this.src != null ? html` + ${this.alt ?? ''} + ` : this.dark ? html` + + + + + ` : html` + + + + + + `; + } + + #onLoad(event: Event) { + this.dispatchEvent(new PfAvatarLoadEvent(event)); + } } declare global { 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-avatar/test/pf-avatar.spec.ts b/elements/pf-avatar/test/pf-avatar.spec.ts index 7d8fd5d81a..99fdae3ee3 100644 --- a/elements/pf-avatar/test/pf-avatar.spec.ts +++ b/elements/pf-avatar/test/pf-avatar.spec.ts @@ -1,7 +1,6 @@ import { html, expect, oneEvent, nextFrame } from '@open-wc/testing'; import { createFixture } from '@patternfly/pfe-tools/test/create-fixture.js'; -import { PfAvatar } from '@patternfly/elements/pf-avatar/pf-avatar.js'; -import { AvatarLoadEvent } from '../BaseAvatar'; +import { PfAvatar, PfAvatarLoadEvent } from '@patternfly/elements/pf-avatar/pf-avatar.js'; describe('', function() { it('imperatively instantiates', function() { @@ -32,14 +31,14 @@ describe('', function() { let element: PfAvatar; let loaded: string | undefined; const datauri = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAB0UlEQVR4Xu3UAQ0AAAyDsM+/6QspcwAh2zXawGj64K8A8AgKoABwAzh+D1AAuAEcvwcoANwAjt8DFABuAMfvAQoAN4Dj9wAFgBvA8XuAAsAN4Pg9QAHgBnD8HqAAcAM4fg9QALgBHL8HKADcAI7fAxQAbgDH7wEKADeA4/cABYAbwPF7gALADeD4PUAB4AZw/B6gAHADOH4PUAC4ARy/BygA3ACO3wMUAG4Ax+8BCgA3gOP3AAWAG8Dxe4ACwA3g+D1AAeAGcPweoABwAzh+D1AAuAEcvwcoANwAjt8DFABuAMfvAQoAN4Dj9wAFgBvA8XuAAsAN4Pg9QAHgBnD8HqAAcAM4fg9QALgBHL8HKADcAI7fAxQAbgDH7wEKADeA4/cABYAbwPF7gALADeD4PUAB4AZw/B6gAHADOH4PUAC4ARy/BygA3ACO3wMUAG4Ax+8BCgA3gOP3AAWAG8Dxe4ACwA3g+D1AAeAGcPweoABwAzh+D1AAuAEcvwcoANwAjt8DFABuAMfvAQoAN4Dj9wAFgBvA8XuAAsAN4Pg9QAHgBnD8HqAAcAM4fg9QALgBHL8HKADcAI7fAxQAbgDH7wEKADeA4/cABYAbwPF7ADyAB6SPAIFm19U7AAAAAElFTkSuQmCC'; - const onLoad = (e: AvatarLoadEvent) => { + const onLoad = (e: PfAvatarLoadEvent) => { const paths = e.originalEvent.composedPath() as HTMLImageElement[]; loaded = paths.find(x => x.localName === 'img')?.src; }; beforeEach(async function() { element = await createFixture(html``); setTimeout(() => element.src = datauri); - await oneEvent(element, 'load', false); + await oneEvent(element, 'load'); }); it('loads the image', function() { expect(loaded).to.equal(datauri); 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 c594de18d1..b1add73dc7 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, type TemplateResult } 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'; @@ -13,14 +13,11 @@ import styles from './pf-back-to-top.css'; /** * The **back to top** component is a shortcut that allows users to quickly navigate to the top of a lengthy content page. * @summary A shortcut that allows users to quickly navigate to the top of a lengthy content page. - * * @csspart trigger - The `` or `` element - * * @slot icon * Contains the prefix icon to display before the link or button. * @slot * Text to display inside the link or button. - * * @cssprop {} --pf-c-back-to-top--Right {@default `3rem``} * @cssprop {} --pf-c-back-to-top--Bottom {@default `1.5rem``} * @cssprop --pf-c-back-to-top--c-button--BoxShadow {@default `0 0.75rem 0.75rem -0.5rem rgba(3, 3, 3, 0.18)`} @@ -33,11 +30,10 @@ import styles from './pf-back-to-top.css'; * @cssprop {} --pf-c-button--m-primary--Color {@default `#fff`} * @cssprop {} --pf-c-button--m-primary--BackgroundColor {@default `#06c`} * @cssprop {} --pf-c-button__icon--m-end--MarginLeft {@default `0.25rem`} - * */ @customElement('pf-back-to-top') export class PfBackToTop extends LitElement { - static readonly styles = [styles]; + static readonly styles: CSSStyleSheet[] = [styles]; /** Shorthand for the `icon` slot, the value is icon name */ @property({ reflect: true }) icon?: string; @@ -70,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; @@ -105,7 +103,7 @@ export class PfBackToTop extends LitElement { } } - render() { + render(): TemplateResult<1> { // ensure href has a hash if (this.href && this.href.charAt(0) !== '#') { this.href = `#${this.href}`; @@ -161,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/pf-background-image.ts b/elements/pf-background-image/pf-background-image.ts index b9aca2365d..12dd2e2588 100644 --- a/elements/pf-background-image/pf-background-image.ts +++ b/elements/pf-background-image/pf-background-image.ts @@ -1,4 +1,4 @@ -import { LitElement, html } from 'lit'; +import { LitElement, html, type TemplateResult } from 'lit'; import { customElement } from 'lit/decorators/custom-element.js'; import { queryAssignedElements } from 'lit/decorators/query-assigned-elements.js'; import { property } from 'lit/decorators/property.js'; @@ -22,7 +22,7 @@ import styles from './pf-background-image.css'; */ @customElement('pf-background-image') export class PfBackgroundImage extends LitElement { - static readonly styles = [styles]; + static readonly styles: CSSStyleSheet[] = [styles]; /** * The URL for the image shown on mobile @@ -60,7 +60,7 @@ export class PfBackgroundImage extends LitElement { #updated = false; - render() { + render(): TemplateResult<1> { const cssProps = { '--_background-image': this.src, '--_background-image-2x': this.src2x, 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/BaseBadge.css b/elements/pf-badge/BaseBadge.css deleted file mode 100644 index 859c924193..0000000000 --- a/elements/pf-badge/BaseBadge.css +++ /dev/null @@ -1,6 +0,0 @@ -:host { - position: relative; - white-space: nowrap; - text-align: center; - display: inline-block; -} \ No newline at end of file diff --git a/elements/pf-badge/BaseBadge.ts b/elements/pf-badge/BaseBadge.ts deleted file mode 100644 index 6f5783d496..0000000000 --- a/elements/pf-badge/BaseBadge.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { LitElement, html } from 'lit'; - -import style from './BaseBadge.css'; - -export abstract class BaseBadge extends LitElement { - static readonly styles = [style]; - - abstract state?: string; - - /** - * Sets a numeric value for a badge. - * - * You can pair it with `threshold` attribute to add a `+` sign - * if the number exceeds the threshold value. - */ - abstract number?: number; - - /** - * Sets a threshold for the numeric value and adds `+` sign if - * the numeric value exceeds the threshold value. - */ - abstract threshold?: number; - - override render() { - const { threshold, number, textContent } = this; - const displayText = - (threshold && number && (threshold < number)) ? `${threshold.toString()}+` - : (number != null) ? number.toString() - : textContent ?? ''; - return html` -
${displayText} - `; - } -} diff --git a/elements/pf-badge/pf-badge.css b/elements/pf-badge/pf-badge.css index 0814183957..6c0d4389c6 100644 --- a/elements/pf-badge/pf-badge.css +++ b/elements/pf-badge/pf-badge.css @@ -1,4 +1,8 @@ :host { + position: relative; + white-space: nowrap; + text-align: center; + display: inline-block; border-radius: var(--pf-c-badge--BorderRadius, var(--pf-global--BorderRadius--lg, 180em)); min-width: var(--pf-c-badge--MinWidth, diff --git a/elements/pf-badge/pf-badge.ts b/elements/pf-badge/pf-badge.ts index 7a1fd55b97..19aafec2e4 100644 --- a/elements/pf-badge/pf-badge.ts +++ b/elements/pf-badge/pf-badge.ts @@ -1,38 +1,30 @@ +import { LitElement, html, type TemplateResult } from 'lit'; import { customElement } from 'lit/decorators/custom-element.js'; import { property } from 'lit/decorators/property.js'; -import { BaseBadge } from './BaseBadge.js'; - import styles from './pf-badge.css'; /** * A **badge** is used to annotate other information like a label or an object name. - * * @cssprop {} --pf-c-badge--BorderRadius {@default `180em`} - * * @cssprop {} --pf-c-badge--MinWidth {@default `2rem`} - * * @cssprop {} --pf-c-badge--PaddingLeft {@default `0.5rem`} * @cssprop {} --pf-c-badge--PaddingRight {@default `0.5rem`} - * * @cssprop {} --pf-c-badge--FontSize {@default `0.85em`} * @cssprop {} --pf-c-badge--LineHeight {@default `1.5`} * @cssprop {} --pf-c-badge--FontWeight {@default `700`} - * * @cssprop {} --pf-c-badge--Color {@default `#151515`} * @cssprop {} --pf-c-badge--BackgroundColor {@default `#f0f0f0`} - * * @cssprop {} --pf-c-badge--m-read--Color {@default `#151515`} * @cssprop {} --pf-c-badge--m-read--BackgroundColor {@default `#f0f0f0`} - * * @cssprop {} --pf-c-badge--m-unread--Color {@default `#fff`} * @cssprop {} --pf-c-badge--m-unread--BackgroundColor {@default `#06c`} */ @customElement('pf-badge') -export class PfBadge extends BaseBadge { - static readonly styles = [...BaseBadge.styles, styles]; +export class PfBadge extends LitElement { + static readonly styles: CSSStyleSheet[] = [styles]; /** * Denotes the state-of-affairs this badge represents @@ -40,9 +32,30 @@ export class PfBadge extends BaseBadge { */ @property({ reflect: true }) state?: 'unread' | 'read'; + /** + * Sets a numeric value for a badge. + * + * You can pair it with `threshold` attribute to add a `+` sign + * if the number exceeds the threshold value. + */ @property({ reflect: true, type: Number }) number?: number; + /** + * Sets a threshold for the numeric value and adds `+` sign if + * the numeric value exceeds the threshold value. + */ @property({ reflect: true, type: Number }) threshold?: number; + + override render(): TemplateResult<1> { + const { threshold, number, textContent } = this; + const displayText = + (threshold && number && (threshold < number)) ? `${threshold.toString()}+` + : (number != null) ? number.toString() + : textContent ?? ''; + return html` + ${displayText} + `; + } } declare global { 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/demo/pf-banner.html b/elements/pf-banner/demo/pf-banner.html index 6d00374a8d..780ecbc842 100644 --- a/elements/pf-banner/demo/pf-banner.html +++ b/elements/pf-banner/demo/pf-banner.html @@ -1,15 +1,5 @@ Default Banner -Blue Banner -Red Banner -Green Banner -Gold Banner - - diff --git a/elements/pf-banner/pf-banner.ts b/elements/pf-banner/pf-banner.ts index fa356d5a57..d9d281abe7 100644 --- a/elements/pf-banner/pf-banner.ts +++ b/elements/pf-banner/pf-banner.ts @@ -1,4 +1,4 @@ -import { LitElement, html, type PropertyValues } from 'lit'; +import { LitElement, html, type PropertyValues, type TemplateResult } from 'lit'; import { customElement } from 'lit/decorators/custom-element.js'; import { property } from 'lit/decorators/property.js'; @@ -42,7 +42,7 @@ export type BannerVariant = ( */ @customElement('pf-banner') export class PfBanner extends LitElement { - static readonly styles = [styles]; + static readonly styles: CSSStyleSheet[] = [styles]; /** Changes the visual appearance of the banner. */ @property({ reflect: true }) variant?: BannerVariant; @@ -56,13 +56,13 @@ export class PfBanner extends LitElement { /** Represents the state of the anonymous and icon slots */ #slots = new SlotController(this, null, 'icon'); - override willUpdate(changed: PropertyValues) { + override willUpdate(changed: PropertyValues): void { if (changed.has('icon') && this.icon) { import('@patternfly/elements/pf-icon/pf-icon.js'); } } - override render() { + override render(): TemplateResult<1> { const { variant, icon } = this; const hasIcon = !!icon || this.#slots.hasSlotted('icon'); return html` 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/BaseButton.css b/elements/pf-button/BaseButton.css deleted file mode 100644 index beecabeee6..0000000000 --- a/elements/pf-button/BaseButton.css +++ /dev/null @@ -1,68 +0,0 @@ -:host([hidden]) { - display: none !important; -} - -[hidden] { - display: none !important; -} - -.pf-button { - cursor: pointer; - position: relative; - font-family: inherit; -} - -.pf-button { - border-width: 0; - border-style: solid; -} - -.pf-button::after { - position: absolute; - inset: 0 0 0 0; - content: ""; - border-style: solid; -} - -:host(:is(:disabled, [aria-disabled=true])), -:host(:is(:disabled, [aria-disabled=true])) #container, -:host(:is(:disabled, [aria-disabled=true])) .pf-button, -:host(:is(:disabled, [aria-disabled=true])[danger]) .pf-button, -:host(:is(:disabled, [aria-disabled=true])[variant=link]) .pf-button { - pointer-events: none; - cursor: default; -} - -[part=icon] { - display: none; - pointer-events: none; -} - -.pf-button.hasIcon { - position: relative; - display: flex; - align-items: center; -} - -.pf-button.hasIcon [part=icon] { - display: inline-flex; - align-items: center; - position: absolute; - width: 16px; -} - -:host(:not([disabled])) .hasIcon [part=icon] { - cursor: pointer; -} - -[part=icon] ::slotted(*) { - width: 16px; - max-width: 16px; - height: 16px; - max-height: 16px; -} - -.hasIcon button { - position: absolute; - inset: 0; -} diff --git a/elements/pf-button/BaseButton.ts b/elements/pf-button/BaseButton.ts deleted file mode 100644 index 05907ec0d1..0000000000 --- a/elements/pf-button/BaseButton.ts +++ /dev/null @@ -1,106 +0,0 @@ -// we will shortly remove this file in #2631 -/* eslint-disable lit-a11y/no-aria-slot */ -import type { TemplateResult } from 'lit'; -import { LitElement, html } from 'lit'; -import { property } from 'lit/decorators/property.js'; -import { classMap } from 'lit/directives/class-map.js'; -import { ifDefined } from 'lit/directives/if-defined.js'; - -import { InternalsController } from '@patternfly/pfe-core/controllers/internals-controller.js'; - -import styles from './BaseButton.css'; - -/** - * Base button class - * @csspart button - Internal button element - * @csspart icon - Container for the icon slot - * @slot icon - * Contains the button's icon or state indicator, e.g. a spinner. - * @slot - * Must contain exactly one ` - `; - } - - async formDisabledCallback() { - await this.updateComplete; - this.requestUpdate(); - } - - #onClick() { - switch (this.type) { - case 'reset': - return this.#internals.reset(); - default: - return this.#internals.submit(); - } - } - - /** - * Fallback content for the icon slot. When the `icon` attribute is set, it - * should render an icon corresponding to the value. - * @example ```html - * - * ``` - */ - protected abstract renderDefaultIcon(): TemplateResult; -} diff --git a/elements/pf-button/demo/block.html b/elements/pf-button/demo/block.html index 55b8fdf1c2..37952652e4 100644 --- a/elements/pf-button/demo/block.html +++ b/elements/pf-button/demo/block.html @@ -6,7 +6,6 @@ import 'element-internals-polyfill'; import '@patternfly/elements/pf-button/pf-button.js'; for (const button of document.querySelectorAll('pf-button')) { - // eslint-disable-next-line no-console button.addEventListener('click', console.log); } diff --git a/elements/pf-button/demo/pf-button.html b/elements/pf-button/demo/pf-button.html index a9873d3254..b4c4f8e5ce 100644 --- a/elements/pf-button/demo/pf-button.html +++ b/elements/pf-button/demo/pf-button.html @@ -1,17 +1,20 @@ -
- Primary -
+ + Primary + + diff --git a/elements/pf-button/demo/sizes.html b/elements/pf-button/demo/sizes.html index 42c5fdfd56..8a5bca1070 100644 --- a/elements/pf-button/demo/sizes.html +++ b/elements/pf-button/demo/sizes.html @@ -18,7 +18,6 @@ import 'element-internals-polyfill'; import '@patternfly/elements/pf-button/pf-button.js'; for (const button of document.querySelectorAll('pf-button')) { - // eslint-disable-next-line no-console button.addEventListener('click', console.log); } diff --git a/elements/pf-button/demo/user-role.html b/elements/pf-button/demo/user-role.html new file mode 100644 index 0000000000..adaeccfbf5 --- /dev/null +++ b/elements/pf-button/demo/user-role.html @@ -0,0 +1,70 @@ + +

Button Role Example

+
+ HTML + CSS + JS +
+ +
+ HTML is the foundational language of the web. +
+ + + + + + + Note: the purpose of this demo is to show how custom + roles can be applied to <pf-button>. + However, we recommend always using <pf-tabs>, and avoiding custom + implementations of existing design system patterns + +
+ + + + diff --git a/elements/pf-button/demo/variants.html b/elements/pf-button/demo/variants.html index f216382edf..589b88b0de 100644 --- a/elements/pf-button/demo/variants.html +++ b/elements/pf-button/demo/variants.html @@ -85,7 +85,6 @@

Disabled

import 'element-internals-polyfill'; import '@patternfly/elements/pf-button/pf-button.js'; for (const button of document.querySelectorAll('pf-button')) { - // eslint-disable-next-line no-console button.addEventListener('click', console.log); } diff --git a/elements/pf-button/pf-button.css b/elements/pf-button/pf-button.css index fe846daca8..3ffb4f44e9 100644 --- a/elements/pf-button/pf-button.css +++ b/elements/pf-button/pf-button.css @@ -1,5 +1,4 @@ :host { - position: relative; font-size: var(--pf-c-button--FontSize); font-weight: var(--pf-c-button--FontWeight); line-height: var(--pf-c-button--LineHeight); diff --git a/elements/pf-button/pf-button.ts b/elements/pf-button/pf-button.ts index 7890013788..e3b809f003 100644 --- a/elements/pf-button/pf-button.ts +++ b/elements/pf-button/pf-button.ts @@ -1,4 +1,4 @@ -import { LitElement, html } from 'lit'; +import { LitElement, html, type TemplateResult } 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'; @@ -29,7 +29,9 @@ export type ButtonVariant = ( * actions a user can take in an application, like submitting a form, canceling a * process, or creating a new object. Buttons can also be used to take a user to a * new location, like another page inside of a web application, or an external site - * such as help or documentation.. + * such as help or documentation. + * @slot - Button text label + * @slot icon - Button Icon, overrides `icon` attribute * @summary Allows users to perform an action when triggered * @cssprop {} --pf-c-button--FontSize {@default `1rem`} * @cssprop --pf-c-button--FontWeight {@default `400`} @@ -143,26 +145,34 @@ export type ButtonVariant = ( export class PfButton extends LitElement { static readonly formAssociated = true; - static readonly styles = [ + static readonly styles: CSSStyleSheet[] = [ tokensStyles, iconStyles, styles, ]; - /** Represents the state of a stateful button */ - @property({ type: Boolean, reflect: true }) loading = false; + @property({ reflect: true }) type?: 'button' | 'submit' | 'reset'; - /** Applies plain styles */ - @property({ type: Boolean, reflect: true }) plain = false; + /** Accessible name for the button, use when the button does not have slotted text */ + @property() label?: string; - /** Not as urgent as danger */ - @property({ type: Boolean, reflect: true }) warning = false; + /** Form value for the button */ + @property() value?: string; + + /** Form element name for the button */ + @property() name?: string; + + /** Disables the button */ + @property({ reflect: true, type: Boolean }) disabled = false; + + /** Represents the state of a stateful button */ + @property({ type: Boolean, reflect: true }) loading = false; /** Changes the size of the button. */ @property({ reflect: true }) size?: 'small' | 'large'; - /** Icon set for the `icon` property */ - @property({ attribute: 'icon-set' }) iconSet?: string; + /** Not as urgent as danger */ + @property({ type: Boolean, reflect: true }) warning = false; /** * Use danger buttons for actions a user can take that are potentially @@ -171,6 +181,9 @@ export class PfButton extends LitElement { */ @property({ type: Boolean, reflect: true }) danger = false; + /** Applies plain styles */ + @property({ type: Boolean, reflect: true }) plain = false; + /** * Changes the style of the button. * - Primary: Used for the most important call to action on a page. Try to @@ -187,21 +200,12 @@ export class PfButton extends LitElement { @property({ reflect: true, type: Boolean }) block = false; - /** Disables the button */ - @property({ reflect: true, type: Boolean }) disabled = false; - - @property({ reflect: true }) type?: 'button' | 'submit' | 'reset'; - - /** Accessible name for the button, use when the button does not have slotted text */ - @property() label?: string; - - @property() value?: string; - - @property() name?: string; - /** Shorthand for the `icon` slot, the value is icon name */ @property() icon?: string; + /** Icon set for the `icon` property */ + @property({ attribute: 'icon-set' }) iconSet?: string; + #internals = InternalsController.of(this, { role: 'button' }); #slots = new SlotController(this, 'icon', null); @@ -222,7 +226,12 @@ export class PfButton extends LitElement { this.#internals.ariaDisabled = String(!!this.disabled); } - protected override render() { + async formDisabledCallback(): Promise { + await this.updateComplete; + this.requestUpdate(); + } + + override render(): TemplateResult<1> { const hasIcon = !!this.icon || !!this.loading || this.#slots.hasSlotted('icon'); const { warning, variant, danger, loading, plain, inline, block, size } = this; const disabled = this.#disabled; @@ -240,11 +249,14 @@ export class PfButton extends LitElement { plain, warning, })}"> - + + ?hidden="${!this.icon || this.loading}"> @@ -254,11 +266,6 @@ export class PfButton extends LitElement { `; } - async formDisabledCallback() { - await this.updateComplete; - this.requestUpdate(); - } - #onClick() { if (!this.#disabled) { switch (this.type) { 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/BaseCard.css b/elements/pf-card/BaseCard.css deleted file mode 100644 index 2e51a2a5e4..0000000000 --- a/elements/pf-card/BaseCard.css +++ /dev/null @@ -1,36 +0,0 @@ -:host { - display: flex; - flex-direction: column; -} - -article { - position: relative; - height: 100%; - display: flex; - flex-direction: column; -} - -[part=header] { - display: flex; - flex-direction: row; - align-items: center; -} - -[part=body] ::slotted(:not([slot]):first-of-type) { - margin-block-start: 0 !important; -} - -[part=body] ::slotted(:not([slot]):last-of-type) { - margin-block-end: 0 !important; -} - -[part=footer] { - display: flex; - gap: 0.5em; - inset-block-end: 0; -} - -.empty { - display: none; -} - diff --git a/elements/pf-card/BaseCard.ts b/elements/pf-card/BaseCard.ts deleted file mode 100644 index 690c928d19..0000000000 --- a/elements/pf-card/BaseCard.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { LitElement, html } from 'lit'; -import { SlotController } from '@patternfly/pfe-core/controllers/slot-controller.js'; - -import { classMap } from 'lit/directives/class-map.js'; - -import style from './BaseCard.css'; - -/** - * This element creates a header, body, and footer region in which to place - * content or other components. - * - * @summary Gives a preview of information in a small layout - * - * @slot header - * If this slot is used, we expect a heading level tag (h1, h2, h3, h4, h5, h6). - * An icon, svg, or use of the icon component are also valid in this region. - * @slot - Any content that is not designated for the header or footer slot, will go to this slot. - * @slot footer - * Use this slot for anything that you want to be stuck to the base of the card. - * - * @csspart header - The container for *header* content - * @csspart body - The container for *body* content - * @csspart footer - The container for *footer* content - */ -export abstract class BaseCard extends LitElement { - static readonly styles = [style]; - - protected slots = new SlotController(this, 'header', null, 'footer'); - - render() { - return html` -
- -
- -
-
- -
-
- `; - } -} diff --git a/elements/pf-card/demo/header-images-and-actions.html b/elements/pf-card/demo/header-images-and-actions.html new file mode 100644 index 0000000000..03cd21ee88 --- /dev/null +++ b/elements/pf-card/demo/header-images-and-actions.html @@ -0,0 +1,31 @@ + + + + + + + + + Action + Link + Disabled Action + Disabled Link +
+ Separated Action + Separated Link +
+
+

Title

+ Body + Footer +
+ + diff --git a/elements/pf-card/demo/modifiers.html b/elements/pf-card/demo/modifiers.html new file mode 100644 index 0000000000..d25fab3f30 --- /dev/null +++ b/elements/pf-card/demo/modifiers.html @@ -0,0 +1,107 @@ +
+
+ + + + + + + + + + + + + + + + + +
+ +
+
+ + + + diff --git a/elements/pf-card/demo/pf-card.html b/elements/pf-card/demo/pf-card.html index 300c4e9a5e..3f5df03b08 100644 --- a/elements/pf-card/demo/pf-card.html +++ b/elements/pf-card/demo/pf-card.html @@ -1,18 +1,15 @@ -

Lightest card

-

This is the lightest pf-card and a link, and a visited link with border.

- Try - Buy +

Header

+ Body + Footer
diff --git a/elements/pf-card/demo/settings.html b/elements/pf-card/demo/settings.html deleted file mode 100644 index a242b24405..0000000000 --- a/elements/pf-card/demo/settings.html +++ /dev/null @@ -1,94 +0,0 @@ -
-
- - - - - - - - - - - - - - - -
- -
- -

Lightest card

-

This is the lightest pf-card and a link, and a visited link with border.

- Try - Buy -
-
-
- - - - diff --git a/elements/pf-card/demo/title-inline-with-images-and-actions.html b/elements/pf-card/demo/title-inline-with-images-and-actions.html new file mode 100644 index 0000000000..4157935d7e --- /dev/null +++ b/elements/pf-card/demo/title-inline-with-images-and-actions.html @@ -0,0 +1,22 @@ + +

This is a really really really really really really really really really really long header

+ + + + Action + Link + Disabled Action + Disabled Link +
+ Separated Action + Separated Link +
+
+ Body + Footer +
+ + diff --git a/elements/pf-card/docs/pf-card.md b/elements/pf-card/docs/pf-card.md index 7410a941ea..e8101deb2b 100644 --- a/elements/pf-card/docs/pf-card.md +++ b/elements/pf-card/docs/pf-card.md @@ -13,7 +13,9 @@ {% band header="Usage" %} - ### Compact card + ### Basic cards + + ### Modifiers {% htmlexample %}

Header

@@ -22,20 +24,20 @@
{% endhtmlexample %} - ### Rounded card + ### Large card {% htmlexample %} - -

Header

-

This is the rounded card

+ +

Large card

+

This is the large card

Link in the footer
{% endhtmlexample %} - ### Large card + ### Rounded card {% htmlexample %} - -

Large card

-

This is the large card

+ +

Header

+

This is the rounded card

Link in the footer
{% endhtmlexample %} @@ -57,6 +59,65 @@ Link in the footer
{% endhtmlexample %} + + ### Header images and actions + You can include header images and actions in the `header` slot, along with a + title in the `title` slot. The following example includes an SVG image, and + also includes a kebab dropdown. + + {% htmlexample %} + + + + + + + + + Action + Link + Disabled Action + Disabled Link +
+ Separated Action + Separated Link +
+
+

Title

+ Body + Footer +
+ {% endhtmlexample %} + + ### Title inline with images and actions + Slotting the `

` into the `header` slot, instead of the `title` slot will + style it inline with any images or actions. + + {% htmlexample %} + +

This is a really really really really really really really really really really long header

+ + + + Action + Link + Disabled Action + Disabled Link +
+ Separated Action + Separated Link +
+
+ Body + Footer +
+ {% endhtmlexample %} + {% endband %} {% renderSlots %} @@ -79,3 +140,8 @@ {% renderCssCustomProperties %}{% endrenderCssCustomProperties %} {% renderCssParts %}{% endrenderCssParts %} + + diff --git a/elements/pf-card/pf-card.css b/elements/pf-card/pf-card.css index 507b5eabc9..93f47a4ade 100644 --- a/elements/pf-card/pf-card.css +++ b/elements/pf-card/pf-card.css @@ -1,73 +1,202 @@ :host { - background-color: var(--pf-c-card--BackgroundColor, var(--pf-global--BackgroundColor--100, #ffffff)); - box-shadow: var(--pf-c-card--BoxShadow, var(--pf-global--BoxShadow--sm, 0 0.0625rem 0.125rem 0 rgba(3, 3, 3, 0.12), 0 0 0.125rem 0 rgba(3, 3, 3, 0.06))); + --pf-c-card--BackgroundColor: var(--pf-global--BackgroundColor--100, #fff); + --pf-c-card--BoxShadow: var(--pf-global--BoxShadow--sm, 0 0.0625rem 0.125rem 0 rgba(3, 3, 3, 0.12), 0 0 0.125rem 0 rgba(3, 3, 3, 0.06)); + --pf-c-card--first-child--PaddingTop: var(--pf-global--spacer--lg, 1.5rem); + --pf-c-card--child--PaddingRight: var(--pf-global--spacer--lg, 1.5rem); + --pf-c-card--child--PaddingBottom: var(--pf-global--spacer--lg, 1.5rem); + --pf-c-card--child--PaddingLeft: var(--pf-global--spacer--lg, 1.5rem); + --pf-c-card--c-divider--child--PaddingTop: var(--pf-global--spacer--lg, 1.5rem); + --pf-c-card__title--FontFamily: var(--pf-global--FontFamily--heading--sans-serif, "RedHatDisplay", "Overpass", overpass, helvetica, arial, sans-serif); + --pf-c-card__title--FontSize: var(--pf-global--FontSize--md, 1rem); + --pf-c-card__title--FontWeight: var(--pf-global--FontWeight--bold, 700); + --pf-c-card__title--not--last-child--PaddingBottom: var(--pf-global--spacer--md, 1rem); + --pf-c-card__body--FontSize: var(--pf-global--FontSize--md, 1rem); + --pf-c-card__footer--FontSize: var(--pf-global--FontSize--md, 1rem); + --pf-c-card__actions--PaddingLeft: var(--pf-global--spacer--md, 1rem); + --pf-c-card__actions--child--MarginLeft: var(--pf-global--spacer--sm, 0.5rem); + --pf-c-card__header-toggle--MarginTop: calc(var(--pf-global--spacer--form-element, 0.375rem) * -1); + --pf-c-card__header-toggle--MarginRight: var(--pf-global--spacer--xs, 0.25rem); + --pf-c-card__header-toggle--MarginBottom: calc(var(--pf-global--spacer--form-element, 0.375rem) * -1); + --pf-c-card__header-toggle--MarginLeft: calc(var(--pf-global--spacer--md, 1rem) * -1); + --pf-c-card__header-toggle-icon--Transition: var(--pf-global--Transition, all 250ms cubic-bezier(0.42, 0, 0.58, 1)); + --pf-c-card--m-expanded__header-toggle-icon--Rotate: 90deg; + --pf-c-card--m-hoverable--hover--BoxShadow: var(--pf-global--BoxShadow--lg, 0 0.5rem 1rem 0 rgba(3, 3, 3, 0.16), 0 0 0.375rem 0 rgba(3, 3, 3, 0.08)); + --pf-c-card--m-selectable--hover--BoxShadow: var(--pf-global--BoxShadow--lg, 0 0.5rem 1rem 0 rgba(3, 3, 3, 0.16), 0 0 0.375rem 0 rgba(3, 3, 3, 0.08)); + --pf-c-card--m-selectable--focus--BoxShadow: var(--pf-global--BoxShadow--lg, 0 0.5rem 1rem 0 rgba(3, 3, 3, 0.16), 0 0 0.375rem 0 rgba(3, 3, 3, 0.08)); + --pf-c-card--m-selectable--active--BoxShadow: var(--pf-global--BoxShadow--lg, 0 0.5rem 1rem 0 rgba(3, 3, 3, 0.16), 0 0 0.375rem 0 rgba(3, 3, 3, 0.08)); + --pf-c-card--m-selectable--m-selected--BoxShadow: var(--pf-global--BoxShadow--lg, 0 0.5rem 1rem 0 rgba(3, 3, 3, 0.16), 0 0 0.375rem 0 rgba(3, 3, 3, 0.08)); + --pf-c-card--m-selectable--m-selected--before--Height: var(--pf-global--BorderWidth--lg, 3px); + --pf-c-card--m-selectable--m-selected--before--BackgroundColor: var(--pf-global--active-color--100, #06c); + --pf-c-card--m-hoverable-raised--hover--BoxShadow: var(--pf-global--BoxShadow--md, 0 0.25rem 0.5rem 0rem rgba(3, 3, 3, 0.12), 0 0 0.25rem 0 rgba(3, 3, 3, 0.06)); + --pf-c-card--m-hoverable-raised--hover--before--BackgroundColor: var(--pf-global--active-color--400, #73bcf7); + --pf-c-card--m-selectable-raised--before--Right: 0; + --pf-c-card--m-selectable-raised--before--Bottom: 0; + --pf-c-card--m-selectable-raised--before--Left: 0; + --pf-c-card--m-flat--m-selectable-raised--before--Right: calc(-1 * var(--pf-c-card--m-flat--BorderWidth)); + --pf-c-card--m-flat--m-selectable-raised--before--Bottom: calc(-1 * var(--pf-c-card--m-flat--BorderWidth)); + --pf-c-card--m-flat--m-selectable-raised--before--Left: calc(-1 * var(--pf-c-card--m-flat--BorderWidth)); + --pf-c-card--m-selectable-raised--before--Height: var(--pf-global--BorderWidth--xl, 4px); + --pf-c-card--m-selectable-raised--before--BackgroundColor: transparent; + --pf-c-card--m-selectable-raised--before--Transition: none; + --pf-c-card--m-selectable-raised--before--ScaleY: 1; + --pf-c-card--m-selectable-raised--before--TranslateY: 0; + --pf-c-card--m-selectable-raised--hover--BoxShadow: var(--pf-global--BoxShadow--md, 0 0.25rem 0.5rem 0rem rgba(3, 3, 3, 0.12), 0 0 0.25rem 0 rgba(3, 3, 3, 0.06)); + --pf-c-card--m-selectable-raised--hover--before--BackgroundColor: var(--pf-global--active-color--400, #73bcf7); + --pf-c-card--m-selectable-raised--focus--BoxShadow: var(--pf-global--BoxShadow--md, 0 0.25rem 0.5rem 0rem rgba(3, 3, 3, 0.12), 0 0 0.25rem 0 rgba(3, 3, 3, 0.06)); + --pf-c-card--m-selectable-raised--focus--before--BackgroundColor: var(--pf-global--active-color--400, #73bcf7); + --pf-c-card--m-selectable-raised--active--BoxShadow: var(--pf-global--BoxShadow--md, 0 0.25rem 0.5rem 0rem rgba(3, 3, 3, 0.12), 0 0 0.25rem 0 rgba(3, 3, 3, 0.06)); + --pf-c-card--m-selectable-raised--active--before--BackgroundColor: var(--pf-global--active-color--400, #73bcf7); + --pf-c-card--m-selectable-raised--m-selected-raised--before--BackgroundColor: var(--pf-global--active-color--100, #06c); + --pf-c-card--m-selectable-raised--m-selected-raised--BoxShadow: var(--pf-global--BoxShadow--lg, 0 0.5rem 1rem 0 rgba(3, 3, 3, 0.16), 0 0 0.375rem 0 rgba(3, 3, 3, 0.08)); + --pf-c-card--m-selectable-raised--m-selected-raised--TranslateY--base: -0.5rem; + --pf-c-card--m-selectable-raised--m-selected-raised--TranslateY: var(--pf-c-card--m-selectable-raised--m-selected-raised--TranslateY--base); + --pf-c-card--m-flat--m-selectable-raised--m-selected-raised--TranslateY: calc(var(--pf-c-card--m-selectable-raised--m-selected-raised--TranslateY--base) + var(--pf-c-card--m-flat--BorderWidth)); + --pf-c-card--m-rounded--m-selectable-raised--m-selected-raised--TranslateY: calc(var(--pf-c-card--m-selectable-raised--m-selected-raised--TranslateY--base) + var(--pf-c-card--m-rounded--BorderRadius)); + --pf-c-card--m-selectable-raised--m-selected-raised--ZIndex: var(--pf-global--ZIndex--xs, 100); + --pf-c-card--m-selectable-raised--m-selected-raised--Transition: transform .25s linear, box-shadow .25s linear; + --pf-c-card--m-selectable-raised--m-selected-raised--before--Transition: transform .25s linear; + --pf-c-card--m-selectable-raised--m-selected-raised--before--TranslateY: calc(var(--pf-c-card--m-selectable-raised--m-selected-raised--TranslateY) * -1); + --pf-c-card--m-selectable-raised--m-selected-raised--before--ScaleY: 2; + --pf-c-card--m-non-selectable-raised--BackgroundColor: var(--pf-global--BackgroundColor--light-200, #fafafa); + --pf-c-card--m-non-selectable-raised--before--BackgroundColor: var(--pf-global--disabled-color--200, #d2d2d2); + --pf-c-card--m-non-selectable-raised--before--ScaleY: 2; + --pf-c-card--m-flat--m-non-selectable-raised--before--BorderColor: var(--pf-global--disabled-color--200, #d2d2d2); + --pf-c-card--m-compact__body--FontSize: var(--pf-global--FontSize--sm, 0.875rem); + --pf-c-card--m-compact__footer--FontSize: var(--pf-global--FontSize--sm, 0.875rem); + --pf-c-card--m-compact--first-child--PaddingTop: var(--pf-global--spacer--md, 1rem); + --pf-c-card--m-compact--child--PaddingRight: var(--pf-global--spacer--md, 1rem); + --pf-c-card--m-compact--child--PaddingBottom: var(--pf-global--spacer--md, 1rem); + --pf-c-card--m-compact--child--PaddingLeft: var(--pf-global--spacer--md, 1rem); + --pf-c-card--m-compact--c-divider--child--PaddingTop: var(--pf-global--spacer--md, 1rem); + --pf-c-card--m-compact__title--not--last-child--PaddingBottom: var(--pf-global--spacer--sm, 0.5rem); + --pf-c-card--m-display-lg__title--FontSize: var(--pf-global--FontSize--xl, 1.25rem); + --pf-c-card--m-display-lg--first-child--PaddingTop: var(--pf-global--spacer--xl, 2rem); + --pf-c-card--m-display-lg--child--PaddingRight: var(--pf-global--spacer--xl, 2rem); + --pf-c-card--m-display-lg--child--PaddingBottom: var(--pf-global--spacer--xl, 2rem); + --pf-c-card--m-display-lg--child--PaddingLeft: var(--pf-global--spacer--xl, 2rem); + --pf-c-card--m-display-lg--c-divider--child--PaddingTop: var(--pf-global--spacer--xl, 2rem); + --pf-c-card--m-display-lg__title--not--last-child--PaddingBottom: var(--pf-global--spacer--lg, 1.5rem); + --pf-c-card--m-flat--BorderWidth: var(--pf-global--BorderWidth--sm, 1px); + --pf-c-card--m-flat--BorderColor: var(--pf-global--BorderColor--100, #d2d2d2); + --pf-c-card--m-rounded--BorderRadius: var(--pf-global--BorderRadius--sm, 3px); + --pf-c-card--m-full-height--Height: 100%; + --pf-c-card--m-plain--BoxShadow: none; + --pf-c-card--m-plain--BackgroundColor: transparent; + --pf-c-card__header--m-toggle-right--toggle--MarginRight: calc(var(--pf-global--spacer--form-element, 0.375rem) * -1); + --pf-c-card__header--m-toggle-right--toggle--MarginLeft: var(--pf-global--spacer--xs, 0.25rem); + --pf-c-card__header--m-toggle-right--actions--MarginRight: 0; + --pf-c-card__input--focus--BorderWidth: var(--pf-global--BorderWidth--md, 2px); + --pf-c-card__input--focus--BorderColor: var(--pf-global--primary-color--100, #06c); + display: flex; + flex-direction: column; + background-color: var(--pf-c-card--BackgroundColor); + box-shadow: var(--pf-c-card--BoxShadow); } -:host([size="compact"]) { - --_pf-c-card__body--FontSize: var(--pf-c-card--size-compact__body--FontSize, var(--pf-global--FontSize--sm, .875rem)); - --_pf-c-card__footer--FontSize: var(--pf-c-card--size-compact__footer--FontSize, var(--pf-global--spacer--md, 1rem)); - --_pf-c-card--first-child--PaddingTop: var(--pf-c-card--size-compact--first-child--PaddingTop, var(--pf-global--spacer--lg, 1.5rem)); - --_pf-c-card--child--PaddingRight: var(--pf-c-card--size-compact--child--PaddingRight, var(--pf-global--spacer--md, 1rem)); - --_pf-c-card--child--PaddingBottom: var(--pf-c-card--size-compact--child--PaddingBottom, var(--pf-global--spacer--md, 1rem)); - --_pf-c-card--child--PaddingLeft: var(--pf-c-card--size-compact--child--PaddingLeft, var(--pf-global--spacer--md, 1rem)); - --_pf-c-card__title--not--last-child--PaddingBottom: var(--pf-c-card--size-compact__title--not--last-child--PaddingBottom, var(--pf-global--spacer--sm, .5rem)); +[hidden], +.empty { + display: none !important; } -:host([size="large"]) { - --pf-c-card__title--FontSize: var(--pf-c-card--size-large__title--FontSize, var(--pf-global--FontSize--xl, 1.25rem)); - --_pf-c-card--first-child--PaddingTop: var(--pf-c-card--size-large--first-child--PaddingTop, var(--pf-global--spacer--xl, 2rem)); - --_pf-c-card--child--PaddingRight: var(--pf-c-card--size-large--child--PaddingRight, var(--pf-global--spacer--xl, 2rem)); - --_pf-c-card--child--PaddingBottom: var(--pf-c-card--size-large--child--PaddingBottom, var(--pf-global--spacer--xl, 2rem)); - --_pf-c-card--child--PaddingLeft: var(--pf-c-card--size-large--child--PaddingLeft, var(--pf-global--spacer--xl, 2rem)); - --_pf-c-card__title--not--last-child--PaddingBottom: var(--pf-c-card--size-large__title--not--last-child--PaddingBottom, var(--pf-global--spacer--lg, 1.5rem)); +header { + padding-block-start: var(--pf-c-card--first-child--PaddingTop); + padding-block-end: var(--pf-c-card__title--not--last-child--PaddingBottom); + display: flex; + flex-flow: row wrap; + align-items: center; } -:host([flat]) { - --pf-c-card--BoxShadow: none; - border: var(--pf-c-card--m-flat--BorderWidth, var(--pf-global--BorderWidth--sm, 1px)) solid var(--pf-c-card--m-flat--BorderColor, var(--pf-global--BorderColor--100, #d2d2d2)); -} +header ::slotted(*) { + margin-block: 0 !important; -:host([plain]) { - --pf-c-card--BoxShadow: var(--pf-c-card--m-plain--BoxShadow, none); - --pf-c-card--BackgroundColor: var(--pf-c-card--m-plain--BackgroundColor, transparent); + font-family: var(--pf-c-card__title--FontFamily) !important; + font-size: var(--pf-c-card__title--FontSize) !important; + font-weight: var(--pf-c-card__title--FontWeight) !important; } -:host([rounded]) { - border-radius: var(--pf-c-card--m-rounded--BorderRadius, var(--pf-global--BorderRadius--sm, 3px)); +header ::slotted(pf-dropdown) { + margin-inline-start: auto; } -:host([full-height]) { - height: var(--pf-c-card--m-full-height--Height, 100%); - --_pf-c-card__body--FullHeight--Flex: 1 1 auto; +article { + position: relative; + height: 100%; + display: flex; + flex-direction: column; } [part="header"], [part="body"], [part="footer"] { - padding-inline-start: var(--_pf-c-card--child--PaddingLeft, var(--pf-global--spacer--lg, 1.5rem)); - padding-inline-end: var(--_pf-c-card--child--PaddingRight, var(--pf-global--spacer--lg, 1.5rem)); - padding-block-end: var(--_pf-c-card--child--PaddingBottom, var(--pf-global--spacer--lg, 1.5rem)); + padding-inline-start: var(--pf-c-card--child--PaddingLeft); + padding-inline-end: var(--pf-c-card--child--PaddingRight); + padding-block-end: var(--pf-c-card--child--PaddingBottom); +} + +#title { + display: block; + flex: 1 0 100%; + padding-block-start: var(--pf-c-card__title--not--last-child--PaddingBottom); } [part="body"] { - font-size: var(--_pf-c-card__body--FontSize, var(--pf-global--FontSize--md, 1rem)); - flex: var(--_pf-c-card__body--FullHeight--Flex, initial); + font-size: var(--pf-c-card__body--FontSize); + flex: var(--pf-c-card__body--FullHeight--Flex); } -header { - padding-block-start: var(--_pf-c-card--first-child--PaddingTop, var(--pf-global--spacer--lg, 1.5rem)); - padding-block-end: var(--_pf-c-card__title--not--last-child--PaddingBottom, var(--pf-global--spacer--md, 1rem)); +[part="body"] ::slotted(:not([slot]):first-of-type) { + margin-block-start: 0 !important; } -header ::slotted(*) { - font-family: var(--pf-c-card__title--FontFamily, var(--pf-global--FontFamily--heading--sans-serif, "RedHatDisplayUpdated", helvetica, arial, sans-serif)) !important; - font-size: var(--pf-c-card__title--FontSize, var(--pf-global--FontSize--md, 1rem)) !important; - font-weight: var(--pf-c-card__title--FontWeight, var(--pf-global--FontWeight--bold, 700)) !important; - margin-block: 0 !important; +[part="body"] ::slotted(:not([slot]):last-of-type) { + margin-block-end: 0 !important; } [part="footer"] { - font-size: var(--_pf-c-card__footer--FontSize, var(--pf-global--FontSize--md, 1rem)); margin-block-start: auto; + display: flex; + gap: 0.5em; + inset-block-end: 0; + font-size: var(--pf-c-card__footer--FontSize); +} + +:host([size="compact"]) { + --pf-c-card__body--FontSize: var(--pf-c-card--m-compact__body--FontSize); + --pf-c-card__footer--FontSize: var(--pf-c-card--m-compact__footer--FontSize); + --pf-c-card--first-child--PaddingTop: var(--pf-c-card--m-compact--first-child--PaddingTop); + --pf-c-card--child--PaddingRight: var(--pf-c-card--m-compact--child--PaddingRight); + --pf-c-card--child--PaddingBottom: var(--pf-c-card--m-compact--child--PaddingBottom); + --pf-c-card--child--PaddingLeft: var(--pf-c-card--m-compact--child--PaddingLeft); + --pf-c-card--c-divider--child--PaddingTop: var(--pf-c-card--m-compact--c-divider--child--PaddingTop); + --pf-c-card__title--not--last-child--PaddingBottom: var(--pf-c-card--m-compact__title--not--last-child--PaddingBottom); +} + +:host([size="large"]) { + --pf-c-card__title--FontSize: var(--pf-c-card--m-display-lg__title--FontSize); + --pf-c-card--first-child--PaddingTop: var(--pf-c-card--m-display-lg--first-child--PaddingTop); + --pf-c-card--child--PaddingRight: var(--pf-c-card--m-display-lg--child--PaddingRight); + --pf-c-card--child--PaddingBottom: var(--pf-c-card--m-display-lg--child--PaddingBottom); + --pf-c-card--child--PaddingLeft: var(--pf-c-card--m-display-lg--child--PaddingLeft); + --pf-c-card--c-divider--child--PaddingTop: var(--pf-c-card--m-display-lg--c-divider--child--PaddingTop); + --pf-c-card__title--not--last-child--PaddingBottom: var(--pf-c-card--m-display-lg__title--not--last-child--PaddingBottom); +} + +:host([flat]) { + --pf-c-card--BoxShadow: none; + border: var(--pf-c-card--m-flat--BorderWidth) solid var(--pf-c-card--m-flat--BorderColor); +} + +:host([plain]) { + --pf-c-card--BoxShadow: var(--pf-c-card--m-plain--BoxShadow); + --pf-c-card--BackgroundColor: var(--pf-c-card--m-plain--BackgroundColor); +} + +:host([rounded]) { + border-radius: var(--pf-c-card--m-rounded--BorderRadius); +} + +:host([full-height]) { + height: var(--pf-c-card--m-full-height--Height); + --pf-c-card__body--FullHeight--Flex: 1 1 auto; } diff --git a/elements/pf-card/pf-card.ts b/elements/pf-card/pf-card.ts index 850e5624ac..20e2605cec 100644 --- a/elements/pf-card/pf-card.ts +++ b/elements/pf-card/pf-card.ts @@ -1,8 +1,11 @@ +import { LitElement, html, type TemplateResult } from 'lit'; import { customElement } from 'lit/decorators/custom-element.js'; import { property } from 'lit/decorators/property.js'; +import { classMap } from 'lit/directives/class-map.js'; + +import { SlotController } from '@patternfly/pfe-core/controllers/slot-controller.js'; import style from './pf-card.css'; -import { BaseCard } from './BaseCard.js'; /** * A **card** is a square or rectangular container that can contain any kind of content. @@ -10,21 +13,22 @@ import { BaseCard } from './BaseCard.js'; * users to access more details. For example, in dashboards and catalog views, cards * function as a preview of a detailed page. Cards may also be used in data displays * like card views, or for positioning content on a page. - * * @summary Gives a preview of information in a small layout - * * @slot header - * If this slot is used, we expect a heading level tag (h1, h2, h3, h4, h5, h6). - * An icon, svg, or use of the icon component are also valid in this region. - * @slot - Any content that is not designated for the header or footer slot, will go to this slot. + * When included, defines the contents of a card. Card headers can contain images as well as + * the title of a card and an actions menu represented by the right-aligned kebab. + * In most cases, your card should include a header. The only exceptions are when cards being + * used as a layout element to create a white background behind other content. + * @slot title + * Communicates the title of a card if it's not included in the header. + * If a card will be utilized as a selectable and clickable card, the title needs to be made as a linked text to trigger action and indicate interaction. + * @slot - Body. Provides details about the item. A card body can include any combination of static + * text and/or active content. * @slot footer - * Use this slot for anything that you want to be stuck to the base of the card. - * + * Contains external links, actions, or static text at the bottom of a card. * @csspart header - The container for *header* content * @csspart body - The container for *body* content * @csspart footer - The container for *footer* content - * - * * @cssproperty {} --pf-c-card--BackgroundColor {@default `#ffffff`} * @cssproperty {} --pf-c-card--BoxShadow {@default `0 0.0625rem 0.125rem 0 rgba(3, 3, 3, 0.12), 0 0 0.125rem 0 rgba(3, 3, 3, 0.06)`} * @cssproperty {} --pf-c-card--size-compact__body--FontSize {@default `.875rem`} @@ -50,8 +54,8 @@ import { BaseCard } from './BaseCard.js'; * @cssproperty {} --pf-c-card__title--FontWeight {@default `700`} */ @customElement('pf-card') -export class PfCard extends BaseCard { - static readonly styles = [...BaseCard.styles, style]; +export class PfCard extends LitElement { + static readonly styles: CSSStyleSheet[] = [style]; /** * Optionally provide a size for the card and the card contents. @@ -62,19 +66,44 @@ export class PfCard extends BaseCard { @property({ reflect: true }) size?: 'compact' | 'large'; /** - * Optionally apply a border radius for the drop shadow and/or border. - */ + * Optionally apply a border radius for the drop shadow and/or border. + */ @property({ type: Boolean, reflect: true }) rounded = false; /** - * Optionally allow the card to take up the full height of the parent element. - */ + * Optionally allow the card to take up the full height of the parent element. + */ @property({ type: Boolean, reflect: true, attribute: 'full-height' }) fullHeight = false; /** * Optionally remove the border on the card container. */ @property({ type: Boolean, reflect: true }) plain = false; + + #slots = new SlotController(this, 'header', 'title', null, 'footer'); + + render(): TemplateResult<1> { + return html` +
+ +
+ this.requestUpdate()}> +
+
+ +
+
+ `; + } } declare global { 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/pf-chip-group.ts b/elements/pf-chip/pf-chip-group.ts index 17377b82cb..2ca7a6abcd 100644 --- a/elements/pf-chip/pf-chip-group.ts +++ b/elements/pf-chip/pf-chip-group.ts @@ -1,4 +1,4 @@ -import { LitElement, html, type PropertyValues } from 'lit'; +import { LitElement, html, type PropertyValues, type TemplateResult } from 'lit'; import { customElement } from 'lit/decorators/custom-element.js'; import { property } from 'lit/decorators/property.js'; import { query } from 'lit/decorators/query.js'; @@ -44,9 +44,9 @@ const REMAINING_RE = /\$\{\s*remaining\s*\}/g; */ @customElement('pf-chip-group') export class PfChipGroup extends LitElement { - static readonly styles = [shared, styles]; + static readonly styles: CSSStyleSheet[] = [shared, styles]; - static override readonly shadowRootOptions = { + static override readonly shadowRootOptions: ShadowRootInit = { ...LitElement.shadowRootOptions, delegatesFocus: true, }; @@ -105,7 +105,7 @@ export class PfChipGroup extends LitElement { this.addEventListener('remove', this.#onChipRemoved); } - render() { + render(): TemplateResult<1> { const empty = this.#chips.length <= 0; return html`
) { + override updated(changed: PropertyValues): void { if (changed.has('accessibleCloseLabel') || changed.has('numChips') || changed.has('closeable') @@ -167,11 +167,11 @@ export class PfChipGroup extends LitElement { /** * whether or not group has a category */ - get hasCategory() { + get hasCategory(): boolean { return (this._categorySlotted || []).length > 0; } - get remaining() { + get remaining(): number { return this.#chips.length - this.numChips; } @@ -197,6 +197,7 @@ export class PfChipGroup extends LitElement { /** * handles a chip's `chip-remove` event + * @param event remove event */ async #onChipRemoved(event: Event) { if (event instanceof PfChipRemoveEvent) { @@ -216,6 +217,7 @@ export class PfChipGroup extends LitElement { /** * handles overflow chip's click event + * @param event click event */ async #onMoreClick(event: Event) { event.stopPropagation(); @@ -259,8 +261,9 @@ export class PfChipGroup extends LitElement { /** * Activates the specified chip and sets focus on it + * @param chip pf-chip element */ - focusOnChip(chip: HTMLElement) { + focusOnChip(chip: HTMLElement): void { this.#tabindex.setActiveItem(chip); } } diff --git a/elements/pf-chip/pf-chip.ts b/elements/pf-chip/pf-chip.ts index c171c6e591..7898efa775 100644 --- a/elements/pf-chip/pf-chip.ts +++ b/elements/pf-chip/pf-chip.ts @@ -1,4 +1,4 @@ -import { LitElement, html } from 'lit'; +import { LitElement, html, type TemplateResult } from 'lit'; import { customElement } from 'lit/decorators/custom-element.js'; import { property } from 'lit/decorators/property.js'; @@ -23,9 +23,9 @@ export class PfChipRemoveEvent extends Event { */ @customElement('pf-chip') export class PfChip extends LitElement { - static readonly styles = [shared, styles]; + static readonly styles: CSSStyleSheet[] = [shared, styles]; - static override readonly shadowRootOptions = { + static override readonly shadowRootOptions: ShadowRootInit = { ...LitElement.shadowRootOptions, delegatesFocus: true, }; @@ -45,7 +45,7 @@ export class PfChip extends LitElement { */ @property({ attribute: 'overflow-chip', reflect: true, type: Boolean }) overflowChip = false; - render() { + render(): TemplateResult<1> { return this.overflowChip ? html`
`; } + + #toggle() { + this.expanded = !this.expanded; + } } declare global { 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/context.ts b/elements/pf-dropdown/context.ts index 9c1b023d16..f3f0c6e0ad 100644 --- a/elements/pf-dropdown/context.ts +++ b/elements/pf-dropdown/context.ts @@ -1,8 +1,9 @@ +import type { Context } from '@lit/context'; import { createContextWithRoot } from '@patternfly/pfe-core/functions/context.js'; export interface PfDropdownContext { disabled: boolean; } -export const context = - createContextWithRoot(Symbol('pf-dropdown-menu-context')); +export const context: Context = + createContextWithRoot(Symbol('pf-dropdown-menu-context')); diff --git a/elements/pf-dropdown/pf-dropdown-group.ts b/elements/pf-dropdown/pf-dropdown-group.ts index bdfc2e9fff..cabe98d10a 100644 --- a/elements/pf-dropdown/pf-dropdown-group.ts +++ b/elements/pf-dropdown/pf-dropdown-group.ts @@ -1,4 +1,4 @@ -import { LitElement, html } from 'lit'; +import { LitElement, html, type TemplateResult } from 'lit'; import { customElement } from 'lit/decorators/custom-element.js'; import { property } from 'lit/decorators/property.js'; @@ -11,9 +11,9 @@ import styles from './pf-dropdown-group.css'; */ @customElement('pf-dropdown-group') export class PfDropdownGroup extends LitElement { - static readonly styles = [styles]; + static readonly styles: CSSStyleSheet[] = [styles]; - static override readonly shadowRootOptions = { + static override readonly shadowRootOptions: ShadowRootInit = { ...LitElement.shadowRootOptions, delegatesFocus: true, }; @@ -23,7 +23,7 @@ export class PfDropdownGroup extends LitElement { */ @property({ reflect: true }) label?: string; - render() { + render(): TemplateResult<1> { return html`

${this.label}

diff --git a/elements/pf-dropdown/pf-dropdown-item.ts b/elements/pf-dropdown/pf-dropdown-item.ts index b86fb5ea25..bc2cd52d98 100644 --- a/elements/pf-dropdown/pf-dropdown-item.ts +++ b/elements/pf-dropdown/pf-dropdown-item.ts @@ -1,12 +1,10 @@ -import { LitElement, html, type PropertyValues } from 'lit'; +import { LitElement, html, type PropertyValues, type TemplateResult } from 'lit'; import { customElement } from 'lit/decorators/custom-element.js'; import { property } from 'lit/decorators/property.js'; import { classMap } from 'lit/directives/class-map.js'; import { query } from 'lit/decorators/query.js'; import { consume } from '@lit/context'; -import { InternalsController } from '@patternfly/pfe-core/controllers/internals-controller.js'; - import { context, type PfDropdownContext } from './context.js'; import styles from './pf-dropdown-item.css'; @@ -55,9 +53,9 @@ export class DropdownItemChange extends Event { */ @customElement('pf-dropdown-item') export class PfDropdownItem extends LitElement { - static readonly styles = [styles]; + static readonly styles: CSSStyleSheet[] = [styles]; - static override readonly shadowRootOptions = { + static override readonly shadowRootOptions: ShadowRootInit = { ...LitElement.shadowRootOptions, delegatesFocus: true, }; @@ -100,7 +98,7 @@ export class PfDropdownItem extends LitElement { } } - render() { + render(): TemplateResult<1> { const { disabled } = this.ctx ?? { disabled: false }; const isDisabled = !!this.disabled || !!this.ctx?.disabled; return html` diff --git a/elements/pf-dropdown/pf-dropdown-menu.ts b/elements/pf-dropdown/pf-dropdown-menu.ts index a492a34267..80c5d2d927 100644 --- a/elements/pf-dropdown/pf-dropdown-menu.ts +++ b/elements/pf-dropdown/pf-dropdown-menu.ts @@ -1,4 +1,4 @@ -import { LitElement, html } from 'lit'; +import { LitElement, html, type TemplateResult } from 'lit'; import { customElement } from 'lit/decorators/custom-element.js'; import { consume } from '@lit/context'; import { state } from 'lit/decorators/state.js'; @@ -26,9 +26,9 @@ function isDisabledItemClick(event: MouseEvent) { */ @customElement('pf-dropdown-menu') export class PfDropdownMenu extends LitElement { - static readonly styles = [styles]; + static readonly styles: CSSStyleSheet[] = [styles]; - static override readonly shadowRootOptions = { + static override readonly shadowRootOptions: ShadowRootInit = { ...LitElement.shadowRootOptions, delegatesFocus: true, }; @@ -46,14 +46,14 @@ export class PfDropdownMenu extends LitElement { /** * current active descendant in menu */ - get activeItem() { + get activeItem(): HTMLElement | undefined { return this.#tabindex.activeItem ?? this.#tabindex.firstItem; } /** * index of current active descendant in menu */ - get activeIndex() { + get activeIndex(): number { if (!this.#tabindex.activeItem) { return -1; } else { @@ -65,7 +65,7 @@ export class PfDropdownMenu extends LitElement { return this.#getSlottedItems(this.shadowRoot?.querySelector('slot')); } - connectedCallback() { + connectedCallback(): void { super.connectedCallback(); this.addEventListener('focusin', this.#onMenuitemFocusin); this.addEventListener('click', this.#onMenuitemClick); @@ -75,7 +75,7 @@ export class PfDropdownMenu extends LitElement { this.#internals.ariaDisabled = String(!!this.ctx?.disabled); } - render() { + render(): TemplateResult<1> { const { disabled = false } = this.ctx ?? {}; return html` !!x); } - willUpdate(changed: PropertyValues) { + willUpdate(changed: PropertyValues): void { if (changed.has('disabled')) { const { disabled } = this; this.ctx = { disabled }; } } - render() { + render(): TemplateResult<1> { const { expanded } = this; const { anchor, alignment, styles = {} } = this.#float; const { disabled } = this; @@ -134,11 +134,11 @@ export class PfDropdown extends LitElement {

`; } - override firstUpdated() { + override firstUpdated(): void { this.#onSlotchange(); } - updated(changed: PropertyValues) { + updated(changed: PropertyValues): void { if (changed.has('expanded')) { this.#expandedChanged(); } @@ -247,7 +247,7 @@ export class PfDropdown extends LitElement { /** * Opens the dropdown */ - async show() { + async show(): Promise { this.expanded = true; await this.updateComplete; } @@ -255,12 +255,12 @@ export class PfDropdown extends LitElement { /** * Closes the dropdown */ - async hide() { + async hide(): Promise { this.expanded = false; await this.updateComplete; } - async toggle() { + async toggle(): Promise { this.expanded = !this.expanded; await this.updateComplete; } 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/BaseIcon.css b/elements/pf-icon/BaseIcon.css deleted file mode 100644 index 4dd0882446..0000000000 --- a/elements/pf-icon/BaseIcon.css +++ /dev/null @@ -1,22 +0,0 @@ -:host { - position: relative; - display: inline-block; - line-height: 0; - height: fit-content !important; - width: fit-content !important; -} - -#container { - display: grid; - grid-template: 1fr / 1fr; - place-content: center; -} - -#container.content ::slotted(*) { - display: none; -} - -svg { - fill: currentcolor; -} - diff --git a/elements/pf-icon/BaseIcon.ts b/elements/pf-icon/BaseIcon.ts deleted file mode 100644 index d0227ac6bc..0000000000 --- a/elements/pf-icon/BaseIcon.ts +++ /dev/null @@ -1,162 +0,0 @@ -import type { PropertyValues } from 'lit'; - -import { LitElement, html } from 'lit'; -import { property } from 'lit/decorators/property.js'; -import { state } from 'lit/decorators/state.js'; -import { Logger } from '@patternfly/pfe-core/controllers/logger.js'; - -import style from './BaseIcon.css'; - -export type URLGetter = (set: string, icon: string) => URL | string; - -/** requestIdleCallback when available, requestAnimationFrame when not */ -const ric = window.requestIdleCallback ?? window.requestAnimationFrame; - -/** Fired when an icon fails to load */ -class IconLoadError extends ErrorEvent { - constructor( - pathname: string, - /** The original error when importing the icon module */ - public originalError: Error - ) { - super('error', { message: `Could not load icon at ${pathname}` }); - } -} - -/** - * Icon component lazy-loads icons and allows custom icon sets - * @slot - Slotted content is used as a fallback in case the icon doesn't load - * @fires load - Fired when an icon is loaded and rendered - * @fires error - Fired when an icon fails to load - * @csspart fallback - Container for the fallback (i.e. slotted) content - */ -export abstract class BaseIcon extends LitElement { - public static readonly styles = [style]; - - public static addIconSet(setName: string, getter: typeof BaseIcon['getIconUrl']) { - if (typeof getter !== 'function') { - Logger.warn(`[${this.name}.addIconSet(setName, getter)]: getter must be a function`); - } else { - this.getters.set(setName, getter); - for (const instance of this.instances) { - instance.load(); - } - } - } - - public static getIconUrl: URLGetter = (set: string, icon: string) => - `@patternfly/icons/${set}/${icon}.js`; - - private static onIntersect: IntersectionObserverCallback = records => - records.forEach(({ isIntersecting, target }) => { - const icon = target as BaseIcon; - icon.#intersecting = isIntersecting; - ric(() => { - if (icon.#intersecting) { - icon.load(); - } - }); - }); - - private static io = new IntersectionObserver(this.onIntersect); - - private static getters = new Map(); - - private static instances = new Set(); - - declare public static defaultIconSet: string; - - /** Icon set */ - @property() set = this.#class.defaultIconSet; - - /** Icon name */ - @property({ reflect: true }) icon = ''; - - /** Size of the icon */ - abstract size: string; - - /** - * Controls how eager the element will be to load the icon data - * - `eager`: eagerly load the icon, blocking the main thread - * - `idle`: wait for the browser to attain an idle state before loading - * - `lazy` (default): wait for the element to enter the viewport before loading - */ - @property() loading?: 'idle' | 'lazy' | 'eager' = 'lazy'; - - /** Icon content. Any value that lit can render */ - @state() private content?: unknown; - - #intersecting = false; - - #logger = new Logger(this); - - get #class(): typeof BaseIcon { - return this.constructor as typeof BaseIcon; - } - - #lazyLoad() { - this.#class.io.observe(this); - if (this.#intersecting) { - this.load(); - } - } - - #iconChanged() { - switch (this.loading) { - case 'idle': return void ric(() => this.load()); - case 'lazy': return void this.#lazyLoad(); - case 'eager': return void this.load(); - } - } - - connectedCallback() { - super.connectedCallback(); - this.#class.instances.add(this); - } - - willUpdate(changed: PropertyValues) { - if (changed.has('icon')) { - this.#iconChanged(); - } - } - - disconnectedCallback() { - super.disconnectedCallback(); - this.#class.instances.delete(this); - } - - render() { - const content = this.content ?? ''; - return html` - - `; - } - - protected async load() { - const { set, icon } = this; - const getter = this.#class.getters.get(set) ?? this.#class.getIconUrl; - let spec = 'UNKNOWN ICON'; - if (set && icon) { - try { - const gotten = getter(set, icon); - if (gotten instanceof URL) { - spec = gotten.pathname; - } else { - spec = gotten; - } - const mod = await import(spec); - this.content = mod.default instanceof Node ? mod.default.cloneNode(true) : mod.default; - await this.updateComplete; - this.dispatchEvent(new Event('load', { bubbles: true })); - } catch (error: unknown) { - const event = new IconLoadError(spec, error as Error); - this.#logger.error((error as IconLoadError).message); - this.dispatchEvent(event); - } - } - } -} diff --git a/elements/pf-icon/README.md b/elements/pf-icon/README.md index 280462273e..f739929014 100644 --- a/elements/pf-icon/README.md +++ b/elements/pf-icon/README.md @@ -49,6 +49,7 @@ Icon comes with three built-in icon sets: 1. `fas`: Font Awesome Free Solid icons (the default set) 1. `far`: Font Awesome Free Regular icons +1. `fab`: Font Awesome Free Bold icons 1. `patternfly`: PatternFly icons Use the `set` attribute to pick an alternative icon set. @@ -61,19 +62,31 @@ Use the `set` attribute to pick an alternative icon set. It is possible to add custom icon sets or override the default sets. Icon sets are defined in detail in [the docs][icon-sets]. -### Bundling +### Bundling and custom loading behaviour -When bundling PfIcon with other modules, the default icon imports will be -relative to the bundle, not the source file, so be sure to either register all -the icon sets you'll need, or override the default getter. +When bundling `` with other modules (e.g. using webpack, rollup, +esbuild, vite, or similar tools), icon imports will be code-split into chunks, +as they are imported from the `@patternfly/icons` package. Ensure that your +bundler is configured to permit dynamic imports, or mark the `@patternfly/icons` +package as "external" and apply an [import map][importmap] to your page instead. +If you would like to +customize the loading behaviour, override the `PfIcon.resolve()` static method. +This methods takes two arguments: the icon set (a string) and the icon name +(a string), and returns a promise of the icon contents, which is a DOM node, or +[anything else that lit-html can render][renderable]. ```js -// Workaround for bundled pf-icon: make icon imports absolute, instead of -relative to the bundle import { PfIcon } from '/pfe.min.js'; -PfIcon.getIconUrl = (set, icon) => - new URL(`/assets/icons/${set}/${icon}.js`, import.meta.url); - // default: new URL(`./icons/${set}/${icon}.js`, import.meta.url); +PfIcon.resolve = async function(set, icon) { + try { + const { default: content } = await import(`/assets/icons/${set}/${icon}.js`); + if (content instanceof Node) { + return content.cloneNode(true); + } + } catch (e) { + return ''; + } +} ``` ## Loading @@ -84,3 +97,5 @@ see the [docs][docs] for more info. [docs]: https://patternflyelements.org/components/icon/ [icon-sets]: https://patternflyelements.org/components/icon/#icon-sets +[renderable]: https://lit.dev/docs/components/rendering/#renderable-values +[importmap]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap diff --git a/elements/pf-icon/demo/custom-icon-sets.html b/elements/pf-icon/demo/custom-icon-sets.html index ef9ba98e14..9e62817bd6 100644 --- a/elements/pf-icon/demo/custom-icon-sets.html +++ b/elements/pf-icon/demo/custom-icon-sets.html @@ -15,8 +15,9 @@

Custom Icon Sets

``` ```js import { PfIcon } from '@patternfly/elements/pf-icon/pf-icon.js'; - PfIcon.addIconSet('rh', (set, icon) => - new URL(`./icons/${set}/${icon}.js`, import.meta.url)); + PfIcon.addIconSet('rh', async (set, icon) => + import(`./icons/${set}/${icon}.js`) + .then(mod => mod.default)); ``` @@ -25,8 +26,9 @@

Custom Icon Sets

Red Warning diff --git a/elements/pf-label/pf-label.css b/elements/pf-label/pf-label.css index 8705c80cda..40f770bdb5 100644 --- a/elements/pf-label/pf-label.css +++ b/elements/pf-label/pf-label.css @@ -1,27 +1,61 @@ -#pf-container { - display: contents; +:host { + position: relative; + white-space: nowrap; + border: 0; } +pf-icon, ::slotted(pf-icon) { + color: currentColor; +} + +:host, #container { - --pf-global--icon--FontSize--sm: 14px; + display: inline-flex; + align-items: center; + vertical-align: middle; +} +#container { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + border-width: 0; padding-top: var(--pf-c-label--PaddingTop, var(--pf-global--spacer--xs, 0.25rem)); padding-left: var(--pf-c-label--PaddingLeft, var(--pf-global--spacer--sm, 0.5rem)); padding-bottom: var(--pf-c-label--PaddingBottom, var(--pf-global--spacer--xs, 0.25rem)); padding-right: var(--pf-c-label--PaddingRight, var(--pf-global--spacer--sm, 0.5rem)); - font-size: var(--pf-c-label--FontSize, 0.875em); + font-size: var(--pf-c-label--FontSize, var(--pf-global--FontSize--sm, 0.875rem)); color: var(--pf-c-label--Color, var(--pf-global--Color--100, #151515)); background-color: var(--pf-c-label--BackgroundColor, var(--pf-global--palette--black-150, #f5f5f5)); border-radius: var(--pf-c-label--BorderRadius, 30em); max-width: var(--pf-c-label__content--MaxWidth, 100%); color: var(--pf-c-label__content--Color, var(--pf-global--Color--100, #151515)); + + --pf-global--icon--FontSize--sm: 14px; } #container::before { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + pointer-events: none; + content: ""; border-radius: var(--pf-c-label--BorderRadius, 30em); border: var(--pf-c-label__content--before--BorderWidth, 1px) solid var(--pf-c-label__content--before--BorderColor, var(--pf-global--palette--black-300, #d2d2d2)); } +[part=icon] { + display: none; + pointer-events: none; +} + +.hasIcon [part=icon] { + display: inline-flex; + width: 1em; +} + .compact { --pf-c-label--PaddingTop: var(--pf-c-label--m-compact--PaddingTop, 0); --pf-c-label--PaddingRight: var(--pf-c-label--m-compact--PaddingRight, var(--pf-global--spacer--sm, 0.5rem)); @@ -30,7 +64,6 @@ --pf-global--icon--FontSize--sm: 12px; } - .blue { --pf-c-label__content--Color: var(--pf-c-label--m-blue__content--Color, var(--pf-global--info-color--200, #002952)); --pf-c-label--BackgroundColor: var(--pf-c-label--m-blue--BackgroundColor, var(--pf-global--palette--blue-50, #e7f1fa)); diff --git a/elements/pf-label/pf-label.ts b/elements/pf-label/pf-label.ts index 69675e5a76..e05d2bb5f7 100644 --- a/elements/pf-label/pf-label.ts +++ b/elements/pf-label/pf-label.ts @@ -1,37 +1,25 @@ -import { html } from 'lit'; +import { LitElement, html, type TemplateResult } from 'lit'; import { customElement } from 'lit/decorators/custom-element.js'; import { property } from 'lit/decorators/property.js'; import { classMap } from 'lit/directives/class-map.js'; -import { ComposedEvent } from '@patternfly/pfe-core'; - -import { BaseLabel } from './BaseLabel.js'; +import { SlotController } from '@patternfly/pfe-core/controllers/slot-controller.js'; import '@patternfly/elements/pf-button/pf-button.js'; import styles from './pf-label.css'; -export type LabelVariant = ( - | 'filled' - | 'outline' -); - -export type LabelColor = ( - | 'blue' - | 'cyan' - | 'green' - | 'orange' - | 'purple' - | 'red' - | 'grey' - | 'gold' -); +export class LabelCloseEvent extends Event { + constructor() { + super('close', { bubbles: true, cancelable: true }); + } +} /** * The **label** component allows users to add specific element captions for user * clarity and convenience. * @summary Allows users to display meta data in a stylized form. - * @fires close - when a removable label's close button is clicked + * @fires {LabelCloseEvent} close - when a removable label's close button is clicked * @cssprop {} --pf-c-label--FontSize {@default `0.875em`} * @cssprop {} --pf-c-label--PaddingTop {@default `0.25rem`} * @cssprop {} --pf-c-label--PaddingRight {@default `0.5rem`} @@ -92,11 +80,11 @@ export type LabelColor = ( * @cssprop {} --pf-c-label--m-compact--PaddingLeft {@default `0.5rem`} */ @customElement('pf-label') -export class PfLabel extends BaseLabel { - static readonly styles = [...BaseLabel.styles, styles]; +export class PfLabel extends LitElement { + static readonly styles: CSSStyleSheet[] = [styles]; - static override readonly shadowRootOptions = { - ...BaseLabel.shadowRootOptions, + static override readonly shadowRootOptions: ShadowRootInit = { + ...LitElement.shadowRootOptions, delegatesFocus: true, }; @@ -105,12 +93,22 @@ export class PfLabel extends BaseLabel { * - Filled: Colored background with colored border. * - Outline: White background with colored border. */ - @property() variant: LabelVariant = 'filled'; + @property() variant: + | 'filled' + | 'outline' = 'filled'; /** * Changes the color of the label */ - @property() color: LabelColor = 'grey'; + @property() color: + | 'blue' + | 'cyan' + | 'green' + | 'orange' + | 'purple' + | 'red' + | 'grey' + | 'gold' = 'grey'; /** Shorthand for the `icon` slot, the value is icon name */ @property() icon?: string; @@ -127,34 +125,51 @@ export class PfLabel extends BaseLabel { /** Text label for a removable label's close button */ @property({ attribute: 'close-button-label' }) closeButtonLabel?: string; - override render() { + /** Represents the state of the anonymous and icon slots */ + #slots = new SlotController(this, null, 'icon'); + + override render(): TemplateResult<1> { const { compact, truncated } = this; + const { variant, color, icon } = this; + const hasIcon = !!icon || this.#slots.hasSlotted('icon'); return html` - ${super.render()} - `; - } - - protected override renderDefaultIcon() { - return !this.icon ? '' : html` - + + + + + + + + + + + + + `; } - protected override renderSuffix() { - return !this.removable ? '' : html` - - - - - - - - `; + #onClickClose() { + if (this.removable && this.dispatchEvent(new LabelCloseEvent())) { + this.remove(); + } } } +export type LabelVariant = PfLabel['variant']; + +export type LabelColor = PfLabel['color']; + declare global { interface HTMLElementTagNameMap { 'pf-label': PfLabel; 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-label/test/pf-label.spec.ts b/elements/pf-label/test/pf-label.spec.ts index 17376667cc..b7866eba29 100644 --- a/elements/pf-label/test/pf-label.spec.ts +++ b/elements/pf-label/test/pf-label.spec.ts @@ -22,14 +22,6 @@ const exampleWithOutlineAttribute = html` Default Outline `; -const exampleWithIconAttribute = html` - Default Icon -`; - -const exampleWithIconAttributeEmpty = html` - Default -`; - const exampleWithCompactAttribute = html` Default Compact `; @@ -102,18 +94,34 @@ describe('', function() { expect(beforeStyles.getPropertyValue('border-color')).to.equal('rgb(210, 210, 210)'); }); - it('should render a pf-icon if the icon attribute is present and valid', async function() { - const el = await createFixture(exampleWithIconAttribute); - await el.updateComplete; - const icon = el.shadowRoot!.querySelector('pf-icon')!; - expect(icon.icon).to.equal(el.icon); + describe('with valid icon attribute', function() { + let element: PfLabel; + beforeEach(async function() { + element = await createFixture(html` + Default Icon + `); + element.updateComplete; + }); + it('should render a pf-icon', async function() { + const icon = element.shadowRoot!.querySelector('pf-icon')!; + expect(icon.hidden).to.be.false; + expect(icon.icon).to.equal(element.icon); + }); }); - it('should not render a pf-icon if the icon attribute is present but empty', async function() { - const el = await createFixture(exampleWithIconAttributeEmpty); - await el.updateComplete; - const icon = el.shadowRoot!.querySelector('pf-icon')!; - expect(icon).to.be.equal(null); + describe('with empty icon attribute', function() { + let element: PfLabel; + beforeEach(async function() { + element = await createFixture(html` + Default + `); + element.updateComplete; + }); + it('should not render a pf-icon', async function() { + const icon = element.shadowRoot!.querySelector('pf-icon')!; + expect(icon.hidden).to.be.true; + expect(icon.icon).to.be.undefined; + }); }); it('should display compact version if the attribute is-compact is present', async function() { diff --git a/elements/pf-modal/pf-modal.ts b/elements/pf-modal/pf-modal.ts index 866a9c95ef..8e39ec1745 100644 --- a/elements/pf-modal/pf-modal.ts +++ b/elements/pf-modal/pf-modal.ts @@ -1,4 +1,4 @@ -import { LitElement, html } from 'lit'; +import { LitElement, html, type TemplateResult } from 'lit'; import { customElement } from 'lit/decorators/custom-element.js'; import { property } from 'lit/decorators/property.js'; import { query } from 'lit/decorators/query.js'; @@ -67,12 +67,12 @@ export class ModalOpenEvent extends ComposedEvent { */ @customElement('pf-modal') export class PfModal extends LitElement implements HTMLDialogElement { - static override readonly shadowRootOptions = { + static override readonly shadowRootOptions: ShadowRootInit = { ...LitElement.shadowRootOptions, delegatesFocus: true, }; - static readonly styles = [style]; + static readonly styles: CSSStyleSheet[] = [style]; /** Should the dialog close when user clicks outside the dialog? */ protected static closeOnOutsideClick = false; @@ -111,13 +111,13 @@ export class PfModal extends LitElement implements HTMLDialogElement { #slots = new SlotController(this, null, 'header', 'description', 'footer'); - connectedCallback() { + connectedCallback(): void { super.connectedCallback(); this.addEventListener('keydown', this.onKeydown); this.addEventListener('click', this.onClick); } - render() { + render(): TemplateResult<1> { const headerId = (this.#header || this.#headings.length) ? this.#headerId : undefined; const headerLabel = this.#triggerElement ? this.#triggerElement.innerText : undefined; const hasHeader = this.#slots.hasSlotted('header'); @@ -162,7 +162,7 @@ export class PfModal extends LitElement implements HTMLDialogElement { `; } - disconnectedCallback() { + disconnectedCallback(): void { super.disconnectedCallback(); this.removeEventListener('keydown', this.onKeydown); @@ -171,7 +171,7 @@ export class PfModal extends LitElement implements HTMLDialogElement { } @initializer() - protected async _init() { + protected async _init(): Promise { await this.updateComplete; this.#header = this.querySelector(`[slot$="header"]`); this.#body = [...this.querySelectorAll(`*:not([slot])`)]; @@ -190,7 +190,7 @@ export class PfModal extends LitElement implements HTMLDialogElement { } } - protected async _openChanged(oldValue?: boolean, newValue?: boolean) { + protected async _openChanged(oldValue?: boolean, newValue?: boolean): Promise { // loosening types to prevent running these effects in unexpected circumstances // eslint-disable-next-line eqeqeq if (oldValue == null || newValue == null || oldValue == newValue) { @@ -216,7 +216,7 @@ export class PfModal extends LitElement implements HTMLDialogElement { } } - protected _triggerChanged() { + protected _triggerChanged(): void { if (this.trigger) { this.#triggerElement = (this.getRootNode() as Document | ShadowRoot) .getElementById(this.trigger); @@ -272,7 +272,7 @@ export class PfModal extends LitElement implements HTMLDialogElement { this.#cancelling = false; } - setTrigger(element: HTMLElement) { + setTrigger(element: HTMLElement): void { this.#triggerElement = element; this.#triggerElement.addEventListener('click', this.onTriggerClick); } @@ -283,7 +283,7 @@ export class PfModal extends LitElement implements HTMLDialogElement { * modal.toggle(); * ``` */ - @bound toggle() { + @bound toggle(): void { this.open = !this.open; } @@ -293,11 +293,11 @@ export class PfModal extends LitElement implements HTMLDialogElement { * modal.open(); * ``` */ - @bound show() { + @bound show(): void { this.open = true; } - @bound showModal() { + @bound showModal(): void { // TODO: non-modal mode this.show(); } @@ -307,8 +307,9 @@ export class PfModal extends LitElement implements HTMLDialogElement { * ```js * modal.close(); * ``` + * @param returnValue dialog return value */ - @bound close(returnValue?: string) { + @bound close(returnValue?: string): void { if (typeof returnValue === 'string') { this.returnValue = returnValue; } 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/pf-panel.ts b/elements/pf-panel/pf-panel.ts index 09e2542151..c3f594fd73 100644 --- a/elements/pf-panel/pf-panel.ts +++ b/elements/pf-panel/pf-panel.ts @@ -1,4 +1,4 @@ -import { LitElement, html } from 'lit'; +import { LitElement, html, type TemplateResult } from 'lit'; import { customElement } from 'lit/decorators/custom-element.js'; import { property } from 'lit/decorators/property.js'; @@ -11,14 +11,13 @@ import styles from './pf-panel.css'; * be used to house other components such as fields, forms, videos, buttons, and more. * The panel should not be confused with the [drawer](https://www.patternfly.org/v4/components/drawer/design-guidelines/) * component, which allows you to surface information via a collapsable container. - * * @slot header - Place header content here * @slot - Place main content here * @slot footer - Place footer content here */ @customElement('pf-panel') export class PfPanel extends LitElement { - static readonly styles = [styles]; + static readonly styles: CSSStyleSheet[] = [styles]; @property({ type: Boolean, reflect: true }) scrollable = false; @@ -26,7 +25,7 @@ export class PfPanel extends LitElement { #slots = new SlotController(this, 'header', null, 'footer'); - render() { + render(): TemplateResult<1> { const hasHeader = this.#slots.hasSlotted('header'); const hasFooter = this.#slots.hasSlotted('footer'); return html` 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..dcc846f375 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, type TemplateResult } 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,16 +9,15 @@ 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; +import styles from './pf-popover.css'; -type HeadingLevel = (typeof headingLevels)[number]; +type HeadingLevel = 2 | 3 | 4 | 5 | 6; type AlertSeverity = 'default' | 'info' | 'warning' | 'success' | 'danger'; @@ -176,7 +177,7 @@ export class PopoverShownEvent extends ComposedEvent { */ @customElement('pf-popover') export class PfPopover extends LitElement { - static readonly styles = [styles]; + static readonly styles: CSSStyleSheet[] = [styles]; private static instances = new Set(); @@ -189,13 +190,15 @@ export class PfPopover extends LitElement { } satisfies Record) as [AlertSeverity, string][]); static { - document.addEventListener('click', function(event) { - for (const instance of PfPopover.instances) { - if (!instance.noOutsideClick) { - instance.#outsideClick(event); + if (!isServer) { + document.addEventListener('click', function(event) { + for (const instance of PfPopover.instances) { + if (!instance.noOutsideClick) { + instance.#outsideClick(event); + } } - } - }); + }); + } } /** @@ -330,10 +333,12 @@ export class PfPopover extends LitElement { constructor() { super(); - this.addEventListener('keydown', this.#onKeydown); + if (!isServer) { + this.addEventListener('keydown', this.#onKeydown); + } } - render() { + render(): TemplateResult<1> { const { alignment, anchor, styles } = this.#float; const hasFooter = this.#slots.hasSlotted('footer') || !!this.footer; const hasHeading = this.#slots.hasSlotted('heading') || !!this.heading; @@ -404,7 +409,7 @@ export class PfPopover extends LitElement { `; } - disconnectedCallback() { + disconnectedCallback(): void { super.disconnectedCallback(); PfPopover.instances.delete(this); this.#referenceTrigger?.removeEventListener('click', this.toggle); @@ -412,8 +417,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() { @@ -453,8 +461,9 @@ export class PfPopover extends LitElement { /** * Removes event listeners from the old trigger element and attaches * them to the new trigger element. + * @param changed changed props */ - override willUpdate(changed: PropertyValues) { + override willUpdate(changed: PropertyValues): void { if (changed.has('trigger')) { this.#triggerChanged(); } @@ -463,14 +472,18 @@ export class PfPopover extends LitElement { /** * Toggle the popover */ - @bound async toggle() { - this.#float.open ? this.hide() : this.show(); + @bound async toggle(): Promise { + if (this.#float.open) { + this.hide(); + } else { + this.show(); + } } /** * Opens the popover */ - @bound async show() { + @bound async show(): Promise { this.#hideDialog = false; this.requestUpdate(); this.dispatchEvent(new PopoverShowEvent()); @@ -489,7 +502,7 @@ export class PfPopover extends LitElement { /** * Closes the popover */ - @bound async hide() { + @bound async hide(): Promise { this.dispatchEvent(new PopoverHideEvent()); await this.#float.hide(); this._popover?.close(); 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-popover/test/pf-popover.spec.ts b/elements/pf-popover/test/pf-popover.spec.ts index 72ef5614a7..bd44cd1722 100644 --- a/elements/pf-popover/test/pf-popover.spec.ts +++ b/elements/pf-popover/test/pf-popover.spec.ts @@ -1,5 +1,5 @@ import { expect, html, fixture, fixtureCleanup, nextFrame } from '@open-wc/testing'; -import { a11ySnapshot, type A11yTreeSnapshot } from '@patternfly/pfe-tools/test/a11y-snapshot.js'; +import { a11ySnapshot } from '@patternfly/pfe-tools/test/a11y-snapshot.js'; import { clickElementAtCenter } from '@patternfly/pfe-tools/test/utils.js'; import { sendKeys, resetMouse } from '@web/test-runner-commands'; import { PfPopover } from '@patternfly/elements/pf-popover/pf-popover.js'; @@ -29,18 +29,6 @@ describe('', function() { await expect(element).to.be.accessible(); } - /** - * Assert that the accessibility tree reports the expected snapshot - * e.g. for a closed popover, does not announce popover child content - * e.g. for an opened popover, it does announce popover child content - * If the expected children snapshot is undefined, then assistive technology - * reports nothing at all, e.g. a popover element with no attrs and no children - */ - async function expectA11ySnapshot(expected: A11yTreeSnapshot = { role: 'WebArea', name: '' }) { - const snapshot = await a11ySnapshot(); - expect(snapshot).to.deep.equal(expected); - } - function resetElement() { document.querySelectorAll('pf-popover').forEach(e => e.remove()); // @ts-expect-error: resetting test state, so we don't mind the ts error. diff --git a/elements/pf-progress-stepper/pf-progress-step.ts b/elements/pf-progress-stepper/pf-progress-step.ts index 73d5058afd..6ebe9e2a1f 100644 --- a/elements/pf-progress-stepper/pf-progress-step.ts +++ b/elements/pf-progress-stepper/pf-progress-step.ts @@ -1,4 +1,4 @@ -import type { PropertyValues } from 'lit'; +import type { PropertyValues, TemplateResult } from 'lit'; import type { PfProgressStepper } from './pf-progress-stepper.js'; import { LitElement, html } from 'lit'; @@ -26,13 +26,12 @@ const ICONS = new Map(Object.entries({ * Longer description of the current step. * @slot icon * Overrides the icon property - * */ @customElement('pf-progress-step') export class PfProgressStep extends LitElement { protected static parentTagName = 'pf-progress-stepper'; - static readonly styles = [style]; + static readonly styles: CSSStyleSheet[] = [style]; /** Optional extended description of the step */ @property() description?: string; @@ -53,12 +52,12 @@ export class PfProgressStep extends LitElement { #internals = InternalsController.of(this, { role: 'listitem' }); - render() { + render(): TemplateResult<1> { const hasDescription = !!(this.description ?? this.#slots.hasSlotted('description')); 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,8 +73,7 @@ export class PfProgressStep extends LitElement { `; } - updated(changed: PropertyValues) { - super.updated?.(changed); + updated(changed: PropertyValues): void { 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..1f13f17a00 100644 --- a/elements/pf-progress-stepper/pf-progress-stepper.ts +++ b/elements/pf-progress-stepper/pf-progress-stepper.ts @@ -1,7 +1,6 @@ -import { LitElement, html } from 'lit'; +import { LitElement, html, type PropertyValues, type TemplateResult } 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'; import style from './pf-progress-stepper.css'; @@ -17,7 +16,7 @@ import '@patternfly/elements/pf-icon/pf-icon.js'; export class PfProgressStepper extends LitElement { protected static childTagName = 'pf-progress-step'; - static readonly styles = [style]; + static readonly styles: CSSStyleSheet[] = [style]; static formAssociated = true; @@ -28,9 +27,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, { @@ -40,10 +36,10 @@ export class PfProgressStepper extends LitElement { #mo = new MutationObserver(() => this.#onMutation()); - get value() { + get value(): number { 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; } @@ -57,11 +53,17 @@ export class PfProgressStepper extends LitElement { this.#internals.ariaValueNow = this.value.toString(); } - render() { + render(): TemplateResult<1> { // TODO: add label prop // eslint-disable-next-line lit-a11y/accessible-name return html`
`; } + + updated(changed: PropertyValues): void { + 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/pf-progress.ts b/elements/pf-progress/pf-progress.ts index 93094bd5fb..a156ab3ea5 100644 --- a/elements/pf-progress/pf-progress.ts +++ b/elements/pf-progress/pf-progress.ts @@ -1,4 +1,4 @@ -import type { PropertyValues } from 'lit'; +import type { PropertyValues, TemplateResult } from 'lit'; import { LitElement, html } from 'lit'; import { classMap } from 'lit/directives/class-map.js'; import { customElement } from 'lit/decorators/custom-element.js'; @@ -77,7 +77,7 @@ const ICONS = new Map(Object.entries({ */ @customElement('pf-progress') export class PfProgress extends LitElement { - static readonly styles = [styles]; + static readonly styles: CSSStyleSheet[] = [styles]; #internals = this.attachInternals(); @@ -122,7 +122,7 @@ export class PfProgress extends LitElement { return ICONS.get(this.variant ?? '')?.icon; } - override willUpdate(changed: PropertyValues) { + override willUpdate(changed: PropertyValues): void { if (changed.has('value') || changed.has('min') || changed.has('max')) { this.#internals.ariaValueNow = this.#calculatedPercentage.toString(); } @@ -134,7 +134,7 @@ export class PfProgress extends LitElement { } } - render() { + render(): TemplateResult<1> { const { size, measureLocation, variant, description, descriptionTruncated } = this; const icon = this.#icon; const singleLine = description?.length === 0; 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-option-group.ts b/elements/pf-select/pf-option-group.ts index c626bdf298..8c6be3cc22 100644 --- a/elements/pf-select/pf-option-group.ts +++ b/elements/pf-select/pf-option-group.ts @@ -1,4 +1,4 @@ -import { LitElement, html } from 'lit'; +import { LitElement, html, type TemplateResult } from 'lit'; import { classMap } from 'lit/directives/class-map.js'; import { customElement } from 'lit/decorators/custom-element.js'; import { property } from 'lit/decorators/property.js'; @@ -14,7 +14,7 @@ import styles from './pf-option-group.css'; */ @customElement('pf-option-group') export class PfOptionGroup extends LitElement { - static readonly styles = [styles]; + static readonly styles: CSSStyleSheet[] = [styles]; /** Group description. Overridden by `label` slot. */ @property() label?: string; @@ -26,7 +26,7 @@ export class PfOptionGroup extends LitElement { // eslint-disable-next-line no-unused-private-class-members #internals = InternalsController.of(this, { role: 'group' }); - render() { + render(): TemplateResult<1> { const { disabled } = this; return html`
{ const { disabled, active } = this; return html`
@@ -114,7 +114,7 @@ export class PfOption extends LitElement { `; } - willUpdate(changed: PropertyValues) { + willUpdate(changed: PropertyValues): void { if (changed.has('selected') // don't fire on initialization && !(changed.get('selected') === undefined) && this.selected === false) { @@ -128,7 +128,7 @@ export class PfOption extends LitElement { /** * text content within option (used for filtering) */ - get optionText() { + get optionText(): string { return this._slottedText.map(node => node.textContent).join('').trim(); } } diff --git a/elements/pf-select/pf-select.ts b/elements/pf-select/pf-select.ts index c7705afc3e..817327facc 100644 --- a/elements/pf-select/pf-select.ts +++ b/elements/pf-select/pf-select.ts @@ -1,6 +1,4 @@ -import type { PfChipRemoveEvent } from '@patternfly/elements/pf-chip/pf-chip.js'; - -import { LitElement, html, type PropertyValues } from 'lit'; +import { LitElement, html, isServer, type PropertyValues, type TemplateResult } from 'lit'; import { customElement } from 'lit/decorators/custom-element.js'; import { property } from 'lit/decorators/property.js'; import { query } from 'lit/decorators/query.js'; @@ -51,9 +49,9 @@ export class PfSelectChangeEvent extends Event { */ @customElement('pf-select') export class PfSelect extends LitElement { - static readonly styles = [styles]; + static readonly styles: CSSStyleSheet[] = [styles]; - static override readonly shadowRootOptions = { + static override readonly shadowRootOptions: ShadowRootInit = { ...LitElement.shadowRootOptions, delegatesFocus: true, }; @@ -144,12 +142,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; + } } } @@ -193,7 +195,7 @@ export class PfSelect extends LitElement { } } - override willUpdate(changed: PropertyValues) { + override willUpdate(changed: PropertyValues): void { if (this.variant === 'checkbox') { import('@patternfly/elements/pf-badge/pf-badge.js'); } @@ -212,11 +214,11 @@ export class PfSelect extends LitElement { // } } - override render() { + override render(): TemplateResult<1> { 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 ?? []; @@ -301,7 +303,7 @@ export class PfSelect extends LitElement { `; } - override updated(changed: PropertyValues) { + override updated(changed: PropertyValues): void { if (changed.has('expanded')) { this.#expandedChanged(); } @@ -321,7 +323,7 @@ export class PfSelect extends LitElement { // } } - override firstUpdated() { + override firstUpdated(): void { // kick the renderer to that the placeholder gets picked up this.requestUpdate(); // TODO: don't do filtering in the controller @@ -433,9 +435,10 @@ export class PfSelect extends LitElement { /** * handles chip's remove button clicking - * @param opt chip text to be removed from values + * @param event remove event + * @param opt pf-option */ - #onChipRemove(opt: PfOption, event: PfChipRemoveEvent) { + #onChipRemove(opt: PfOption, event: Event) { // if (event.chip) { // opt.selected = false; // this._input?.focus(); @@ -456,7 +459,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 @@ -468,7 +471,7 @@ export class PfSelect extends LitElement { /** * Opens the dropdown */ - async show() { + async show(): Promise { this.expanded = true; await this.updateComplete; } @@ -476,7 +479,7 @@ export class PfSelect extends LitElement { /** * Closes listbox */ - async hide() { + async hide(): Promise { this.expanded = false; await this.updateComplete; } @@ -484,7 +487,7 @@ export class PfSelect extends LitElement { /** * toggles popup based on current state */ - async toggle() { + async toggle(): Promise { this.expanded = !this.expanded; await this.updateComplete; } 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/BaseSpinner.css b/elements/pf-spinner/BaseSpinner.css deleted file mode 100644 index ef5b8cde05..0000000000 --- a/elements/pf-spinner/BaseSpinner.css +++ /dev/null @@ -1,20 +0,0 @@ -:host { - display: inline-block; - width: min-content; - min-height: 0; - aspect-ratio: 1 / 1; -} - -svg { - overflow: hidden; -} - -circle { - width: 100%; - height: 100%; - transform-origin: 50% 50%; - stroke-linecap: round; - stroke-dasharray: 283; - stroke-dashoffset: 280; -} - diff --git a/elements/pf-spinner/BaseSpinner.ts b/elements/pf-spinner/BaseSpinner.ts deleted file mode 100644 index d3fd62f4b9..0000000000 --- a/elements/pf-spinner/BaseSpinner.ts +++ /dev/null @@ -1,49 +0,0 @@ -// will remove file in 2627 - -import { LitElement, html } from 'lit'; -import { property } from 'lit/decorators/property.js'; - -import styles from './BaseSpinner.css'; - -export type SpinnerSize = ( - | 'sm' - | 'md' - | 'lg' - | 'xl' -); - -/** - * Base spinner class - * @cssprop {} --pf-c-spinner--diameter {@default `3.375rem`} - * @cssprop {} --pf-c-spinner--Width {@default `3.375rem`} - * @cssprop {} --pf-c-spinner--Height {@default `3.375rem`} - * @cssprop {} --pf-c-spinner--Color {@default `#06c`} - * @cssprop {} --pf-c-spinner--m-sm--diameter {@default `0.625rem`} - * @cssprop {} --pf-c-spinner--m-md--diameter {@default `1.125rem`} - * @cssprop {} --pf-c-spinner--m-lg--diameter {@default `1.5rem`} - * @cssprop {} --pf-c-spinner--m-xl--diameter {@default `3.375rem`} - * @cssprop {