From 60a3dccf028f5f8fec689c48f8541ca067660fd1 Mon Sep 17 00:00:00 2001 From: Benny Powers Date: Fri, 20 Oct 2023 10:52:52 +0300 Subject: [PATCH 01/10] feat(core): internals controller improvements --- ...nternals-controller-computed-label-text.md | 4 + .changeset/internals-controller-props.md | 4 + .../controllers/internals-controller.ts | 169 +++++++++++++----- elements/pf-text-input/pf-text-input.ts | 21 +-- 4 files changed, 139 insertions(+), 59 deletions(-) create mode 100644 .changeset/internals-controller-computed-label-text.md create mode 100644 .changeset/internals-controller-props.md diff --git a/.changeset/internals-controller-computed-label-text.md b/.changeset/internals-controller-computed-label-text.md new file mode 100644 index 0000000000..f54ef38597 --- /dev/null +++ b/.changeset/internals-controller-computed-label-text.md @@ -0,0 +1,4 @@ +--- +"@patternfly/pfe-core": minor +--- +`InternalsController`: added `computedLabelText` read-only property diff --git a/.changeset/internals-controller-props.md b/.changeset/internals-controller-props.md new file mode 100644 index 0000000000..a5bb82e69b --- /dev/null +++ b/.changeset/internals-controller-props.md @@ -0,0 +1,4 @@ +--- +"@patternfly/pfe-core": minor +--- +`InternalsController`: reflect all methods and properties from `ElementInternals` diff --git a/core/pfe-core/controllers/internals-controller.ts b/core/pfe-core/controllers/internals-controller.ts index a7be52c2d3..edc10e8bd6 100644 --- a/core/pfe-core/controllers/internals-controller.ts +++ b/core/pfe-core/controllers/internals-controller.ts @@ -1,10 +1,63 @@ import type { ReactiveController, ReactiveControllerHost } from 'lit'; +interface FACE extends HTMLElement { + formDisabledCallback?(disabled: boolean): void | Promise; + formResetCallback?(): void | Promise; + formStateRestoreCallback?(state: string, mode: string): void | Promise; +} + +const READONLY_KEYS_LIST = [ + 'form', + 'labels', + 'shadowRoot', + 'states', + 'validationMessage', + 'validity', + 'willValidate', +] as const; +const READONLY_KEYS = new Set(READONLY_KEYS_LIST); +const METHODS_LIST = [ + 'checkValidity', + 'reportValidity', + 'setFormValue', + 'setValidity', +] as const; +const METHODS_KEYS = new Set(METHODS_LIST); + +type ReadonlyInternalsProp = (typeof READONLY_KEYS_LIST)[number]; + +function isReadonlyInternalsProp(key: string): key is ReadonlyInternalsProp { + return READONLY_KEYS.has(key as ReadonlyInternalsProp); +} + function isARIAMixinProp(key: string): key is keyof ARIAMixin { return key === 'role' || key.startsWith('aria'); } +function isInternalsMethod(key: string): key is keyof ElementInternals { + return METHODS_KEYS.has(key as unknown as (typeof METHODS_LIST)[number]); +} + +function getLabelText(label: HTMLElement) { + if (label.hidden) { + return ''; + } else { + const ariaLabel = label.getAttribute?.('aria-label'); + return ariaLabel ?? label.textContent; + } +} + export class InternalsController implements ReactiveController, ARIAMixin { + static protos = new WeakMap(); + + declare readonly form: ElementInternals['form']; + declare readonly labels: ElementInternals['labels']; + declare readonly shadowRoot: ElementInternals['shadowRoot']; + // https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/states + declare readonly states: unknown; + declare readonly validity: ElementInternals['validity']; + declare readonly willValidate: ElementInternals['willValidate']; + declare role: ARIAMixin['role']; declare ariaAtomic: ARIAMixin['ariaAtomic']; declare ariaAutoComplete: ARIAMixin['ariaAutoComplete']; @@ -46,75 +99,107 @@ export class InternalsController implements ReactiveController, ARIAMixin { declare ariaValueNow: ARIAMixin['ariaValueNow']; declare ariaValueText: ARIAMixin['ariaValueText']; + declare checkValidity: (...args: Parameters) => boolean; + declare reportValidity: (...args: Parameters) => boolean; + declare setFormValue: (...args: Parameters) => void; + declare setValidity: (...args: Parameters) => void; + + hostConnected?(): void + #internals: ElementInternals; #formDisabled = false; + static { + + } + /** True when the control is disabled via it's containing fieldset element */ get formDisabled() { return this.host.matches(':disabled') || this.#formDisabled; } - static protos = new WeakMap(); - - get labels() { - return this.#internals.labels; - } - - get validity() { - return this.#internals.validity; + /** A best-attempt based on observed behaviour in FireFox 115 on fedora 38 */ + get computedLabelText() { + return this.#internals.ariaLabel || + Array.from(this.#internals.labels as NodeListOf) + .reduce((acc, label) => + `${acc}${getLabelText(label)}`, ''); } constructor( - public host: ReactiveControllerHost & HTMLElement, - options?: Partial + public host: ReactiveControllerHost & FACE, + private options?: Partial ) { this.#internals = host.attachInternals(); - // We need to polyfill :disabled - // see https://github.com/calebdwilliams/element-internals-polyfill/issues/88 - const orig = (host as HTMLElement & { formDisabledCallback?(disabled: boolean): void }).formDisabledCallback; - (host as HTMLElement & { formDisabledCallback?(disabled: boolean): void }).formDisabledCallback = disabled => { + this.#polyfillDisabledPseudo(); + this.#defineInternalsProps(); + } + + /** + * We need to polyfill :disabled + * see https://github.com/calebdwilliams/element-internals-polyfill/issues/88 + */ + #polyfillDisabledPseudo() { + const orig = this.host.formDisabledCallback; + this.host.formDisabledCallback = disabled => { this.#formDisabled = disabled; - orig?.call(host, disabled); + orig?.call(this.host, disabled); }; - // proxy the internals object's aria prototype - for (const key of Object.keys(Object.getPrototypeOf(this.#internals))) { - if (isARIAMixinProp(key)) { - Object.defineProperty(this, key, { - get() { - return this.#internals[key]; - }, - set(value) { - this.#internals[key] = value; - this.host.requestUpdate(); - } - }); - } - } + } - for (const [key, val] of Object.entries(options ?? {})) { + /** Reflect the internals object's aria prototype */ + #defineInternalsProps() { + // TODO: can we define these statically on the prototype instead? + for (const key in this.#internals) { if (isARIAMixinProp(key)) { - this[key] = val; + this.#defineARIAMixinProp(key); + } else if (isReadonlyInternalsProp(key)) { + this.#defineReadonlyProp(key); + } else if (isInternalsMethod(key)) { + this.#defineMethod(key); } } - } - - hostConnected?(): void - setFormValue(...args: Parameters) { - return this.#internals.setFormValue(...args); + // for (const [key, val] of Object.entries(this.options ?? {})) { + // if (isARIAMixinProp(key)) { + // this[key] = val; + // } + // } } - setValidity(...args: Parameters) { - return this.#internals.setValidity(...args); + #defineARIAMixinProp(key: keyof ARIAMixin) { + Object.defineProperty(this, key, { + get: () => this.#internals[key], + set: value => { + this.#internals[key] = value; + this.host.requestUpdate(); + } + }); + if (this.options && key in this.options) { + this[key as unknown as 'role'] = this.options?.[key] as string; + } } - checkValidity(...args: Parameters) { - return this.#internals.checkValidity(...args); + #defineReadonlyProp(key: ReadonlyInternalsProp) { + Object.defineProperty(this, key, { + enumerable: true, + configurable: false, + get: () => this.#internals[key as Exclude], + }); } - reportValidity(...args: Parameters) { - return this.#internals.reportValidity(...args); + #defineMethod(key: keyof ElementInternals) { + Object.defineProperty(this, key, { + enumerable: true, + configurable: false, + writable: false, + value: (...args: unknown[]) => { + const val = this.#internals[key as 'setValidity'](...args as []); + this.host.requestUpdate(); + return val; + } + }); } submit() { diff --git a/elements/pf-text-input/pf-text-input.ts b/elements/pf-text-input/pf-text-input.ts index f4f1b15471..f3ce3ffef0 100644 --- a/elements/pf-text-input/pf-text-input.ts +++ b/elements/pf-text-input/pf-text-input.ts @@ -4,16 +4,9 @@ import { property } from 'lit/decorators/property.js'; import { ifDefined } from 'lit/directives/if-defined.js'; import { styleMap } from 'lit/directives/style-map.js'; -import styles from './pf-text-input.css'; +import { InternalsController } from '@patternfly/pfe-core/controllers/internals-controller.js'; -function getLabelText(label: HTMLElement) { - if (label.hidden) { - return ''; - } else { - const ariaLabel = label.getAttribute?.('aria-label'); - return ariaLabel ?? label.textContent; - } -} +import styles from './pf-text-input.css'; /** * A **text input** is used to gather free-form text from a user. @@ -194,7 +187,7 @@ export class PfTextInput extends LitElement { /** Value of the input. */ @property() value = ''; - #internals = this.attachInternals(); + #internals = new InternalsController(this); #derivedLabel = ''; @@ -203,13 +196,7 @@ export class PfTextInput extends LitElement { } override willUpdate() { - /** A best-attempt based on observed behaviour in FireFox 115 on fedora 38 */ - this.#derivedLabel = - this.accessibleLabel || - this.#internals.ariaLabel || - Array.from(this.#internals.labels as NodeListOf) - .reduce((acc, label) => - `${acc}${getLabelText(label)}`, ''); + this.#derivedLabel = this.accessibleLabel || this.#internals.computedLabelText; } override render() { From e3a69ba29f4a74201e9c1058a268eae3293faa3f Mon Sep 17 00:00:00 2001 From: Benny Powers Date: Fri, 20 Oct 2023 11:06:19 +0300 Subject: [PATCH 02/10] feat(text-area): add pf-text-area --- .changeset/pf-text-area.md | 16 +++ elements/package.json | 1 + elements/pf-text-area/README.md | 11 ++ elements/pf-text-area/demo/auto-resizing.html | 6 + elements/pf-text-area/demo/disabled.html | 6 + .../demo/horizontally-resizable.html | 6 + elements/pf-text-area/demo/invalid.html | 8 ++ elements/pf-text-area/demo/pf-text-area.html | 6 + elements/pf-text-area/demo/readonly.html | 6 + elements/pf-text-area/demo/validated.html | 9 ++ .../demo/vertically-resizable.html | 6 + elements/pf-text-area/docs/pf-text-area.md | 17 +++ elements/pf-text-area/pf-text-area.css | 3 + elements/pf-text-area/pf-text-area.ts | 117 ++++++++++++++++++ .../pf-text-area/test/pf-text-area.e2e.ts | 12 ++ .../pf-text-area/test/pf-text-area.spec.ts | 21 ++++ 16 files changed, 251 insertions(+) create mode 100644 .changeset/pf-text-area.md create mode 100644 elements/pf-text-area/README.md create mode 100644 elements/pf-text-area/demo/auto-resizing.html create mode 100644 elements/pf-text-area/demo/disabled.html create mode 100644 elements/pf-text-area/demo/horizontally-resizable.html create mode 100644 elements/pf-text-area/demo/invalid.html create mode 100644 elements/pf-text-area/demo/pf-text-area.html create mode 100644 elements/pf-text-area/demo/readonly.html create mode 100644 elements/pf-text-area/demo/validated.html create mode 100644 elements/pf-text-area/demo/vertically-resizable.html create mode 100644 elements/pf-text-area/docs/pf-text-area.md create mode 100644 elements/pf-text-area/pf-text-area.css create mode 100644 elements/pf-text-area/pf-text-area.ts create mode 100644 elements/pf-text-area/test/pf-text-area.e2e.ts create mode 100644 elements/pf-text-area/test/pf-text-area.spec.ts diff --git a/.changeset/pf-text-area.md b/.changeset/pf-text-area.md new file mode 100644 index 0000000000..67838811c2 --- /dev/null +++ b/.changeset/pf-text-area.md @@ -0,0 +1,16 @@ +--- +"@patternfly/elements": minor +--- +✨ Added `` + +```html +
+ +
+``` diff --git a/elements/package.json b/elements/package.json index 98933cf996..7f5e86daa3 100644 --- a/elements/package.json +++ b/elements/package.json @@ -60,6 +60,7 @@ "./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", diff --git a/elements/pf-text-area/README.md b/elements/pf-text-area/README.md new file mode 100644 index 0000000000..e1774bb0a3 --- /dev/null +++ b/elements/pf-text-area/README.md @@ -0,0 +1,11 @@ +# Text Area +Add a description of the component here. + +## Usage +Describe how best to use this web component along with best practices. + +```html + + + +``` diff --git a/elements/pf-text-area/demo/auto-resizing.html b/elements/pf-text-area/demo/auto-resizing.html new file mode 100644 index 0000000000..b3c1966f3d --- /dev/null +++ b/elements/pf-text-area/demo/auto-resizing.html @@ -0,0 +1,6 @@ + + + + diff --git a/elements/pf-text-area/demo/disabled.html b/elements/pf-text-area/demo/disabled.html new file mode 100644 index 0000000000..9f2c5a71b2 --- /dev/null +++ b/elements/pf-text-area/demo/disabled.html @@ -0,0 +1,6 @@ + + + + diff --git a/elements/pf-text-area/demo/horizontally-resizable.html b/elements/pf-text-area/demo/horizontally-resizable.html new file mode 100644 index 0000000000..71b914a159 --- /dev/null +++ b/elements/pf-text-area/demo/horizontally-resizable.html @@ -0,0 +1,6 @@ + + + + diff --git a/elements/pf-text-area/demo/invalid.html b/elements/pf-text-area/demo/invalid.html new file mode 100644 index 0000000000..c36bf058b2 --- /dev/null +++ b/elements/pf-text-area/demo/invalid.html @@ -0,0 +1,8 @@ + + + + diff --git a/elements/pf-text-area/demo/pf-text-area.html b/elements/pf-text-area/demo/pf-text-area.html new file mode 100644 index 0000000000..19561aaa7f --- /dev/null +++ b/elements/pf-text-area/demo/pf-text-area.html @@ -0,0 +1,6 @@ + + + + diff --git a/elements/pf-text-area/demo/readonly.html b/elements/pf-text-area/demo/readonly.html new file mode 100644 index 0000000000..eed70e21be --- /dev/null +++ b/elements/pf-text-area/demo/readonly.html @@ -0,0 +1,6 @@ + + + + diff --git a/elements/pf-text-area/demo/validated.html b/elements/pf-text-area/demo/validated.html new file mode 100644 index 0000000000..93182db69d --- /dev/null +++ b/elements/pf-text-area/demo/validated.html @@ -0,0 +1,9 @@ +
+ + Validate +
+ + + diff --git a/elements/pf-text-area/demo/vertically-resizable.html b/elements/pf-text-area/demo/vertically-resizable.html new file mode 100644 index 0000000000..b112a982a2 --- /dev/null +++ b/elements/pf-text-area/demo/vertically-resizable.html @@ -0,0 +1,6 @@ + + + + diff --git a/elements/pf-text-area/docs/pf-text-area.md b/elements/pf-text-area/docs/pf-text-area.md new file mode 100644 index 0000000000..3c555f467b --- /dev/null +++ b/elements/pf-text-area/docs/pf-text-area.md @@ -0,0 +1,17 @@ +{% renderOverview %} + +{% endrenderOverview %} + +{% band header="Usage" %}{% endband %} + +{% renderSlots %}{% endrenderSlots %} + +{% renderAttributes %}{% endrenderAttributes %} + +{% renderMethods %}{% endrenderMethods %} + +{% renderEvents %}{% endrenderEvents %} + +{% renderCssCustomProperties %}{% endrenderCssCustomProperties %} + +{% renderCssParts %}{% endrenderCssParts %} diff --git a/elements/pf-text-area/pf-text-area.css b/elements/pf-text-area/pf-text-area.css new file mode 100644 index 0000000000..5d4e87f30f --- /dev/null +++ b/elements/pf-text-area/pf-text-area.css @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/elements/pf-text-area/pf-text-area.ts b/elements/pf-text-area/pf-text-area.ts new file mode 100644 index 0000000000..df4e2acba0 --- /dev/null +++ b/elements/pf-text-area/pf-text-area.ts @@ -0,0 +1,117 @@ +import { LitElement, html } 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'; + +import { InternalsController } from '@patternfly/pfe-core/controllers/internals-controller.js'; + +import styles from './pf-text-area.css'; + +/** + * A **text area** component is used for entering a paragraph of text that is longer than one line. + * + */ +@customElement('pf-text-area') +export class PfTextArea extends LitElement { + static readonly styles = [styles]; + + static readonly formAssociated = true; + + static override shadowRootOptions: ShadowRootInit = { ...LitElement.shadowRootOptions, delegatesFocus: true }; + + /** Accessible label for the input when no `