Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(text-area): add pf-text-area #2639

Merged
merged 11 commits into from
Dec 6, 2023
4 changes: 4 additions & 0 deletions .changeset/internals-controller-computed-label-text.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
"@patternfly/pfe-core": minor
---
`InternalsController`: added `computedLabelText` read-only property
4 changes: 4 additions & 0 deletions .changeset/internals-controller-props.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
"@patternfly/pfe-core": minor
---
`InternalsController`: reflect all methods and properties from `ElementInternals`
16 changes: 16 additions & 0 deletions .changeset/pf-text-area.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
"@patternfly/elements": minor
---
✨ Added `<pf-text-area>`

```html
<form>
<pf-text-area id="textarea"
name="comments"
placeholder="OpenShift enabled our team to..."
resize="vertical"
auto-resize
required
></pf-text-area>
</form>
```
161 changes: 118 additions & 43 deletions core/pfe-core/controllers/internals-controller.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,63 @@
import type { ReactiveController, ReactiveControllerHost } from 'lit';

interface FACE extends HTMLElement {
formDisabledCallback?(disabled: boolean): void | Promise<void>;
formResetCallback?(): void | Promise<void>;
formStateRestoreCallback?(state: string, mode: string): void | Promise<void>;
}

const READONLY_KEYS_LIST = [
'form',
'labels',
'shadowRoot',
'states',
'validationMessage',
'validity',
'willValidate',
] as const;
zeroedin marked this conversation as resolved.
Show resolved Hide resolved
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'];
Expand Down Expand Up @@ -46,6 +99,13 @@ export class InternalsController implements ReactiveController, ARIAMixin {
declare ariaValueNow: ARIAMixin['ariaValueNow'];
declare ariaValueText: ARIAMixin['ariaValueText'];

declare checkValidity: (...args: Parameters<ElementInternals['checkValidity']>) => boolean;
declare reportValidity: (...args: Parameters<ElementInternals['reportValidity']>) => boolean;
declare setFormValue: (...args: Parameters<ElementInternals['setFormValue']>) => void;
declare setValidity: (...args: Parameters<ElementInternals['setValidity']>) => void;

hostConnected?(): void

#internals: ElementInternals;

#formDisabled = false;
Expand All @@ -55,66 +115,81 @@ export class InternalsController implements ReactiveController, ARIAMixin {
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<HTMLElement>)
.reduce((acc, label) =>
`${acc}${getLabelText(label)}`, '');
}

constructor(
public host: ReactiveControllerHost & HTMLElement,
options?: Partial<ARIAMixin>
public host: ReactiveControllerHost & FACE,
private options?: Partial<ARIAMixin>
) {
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(bennypowers): 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<ElementInternals['setFormValue']>) {
return this.#internals.setFormValue(...args);
}

setValidity(...args: Parameters<ElementInternals['setValidity']>) {
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<ElementInternals['checkValidity']>) {
return this.#internals.checkValidity(...args);
#defineReadonlyProp(key: ReadonlyInternalsProp) {
Object.defineProperty(this, key, {
enumerable: true,
configurable: false,
get: () => this.#internals[key as Exclude<ReadonlyInternalsProp, 'states'>],
});
}

reportValidity(...args: Parameters<ElementInternals['reportValidity']>) {
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() {
Expand Down
1 change: 1 addition & 0 deletions elements/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion elements/pf-button/BaseButton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export abstract class BaseButton extends LitElement {
`;
}

protected async formDisabledCallback() {
async formDisabledCallback() {
await this.updateComplete;
this.requestUpdate();
}
Expand Down
11 changes: 11 additions & 0 deletions elements/pf-text-area/README.md
Original file line number Diff line number Diff line change
@@ -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
<pf-text-area>

</pf-text-area>
```
6 changes: 6 additions & 0 deletions elements/pf-text-area/demo/auto-resizing.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<pf-text-area auto-resize></pf-text-area>

<script type="module">
import '@patternfly/elements/pf-text-area/pf-text-area.js';
</script>

6 changes: 6 additions & 0 deletions elements/pf-text-area/demo/disabled.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<pf-text-area disabled></pf-text-area>

<script type="module">
import '@patternfly/elements/pf-text-area/pf-text-area.js';
</script>

6 changes: 6 additions & 0 deletions elements/pf-text-area/demo/horizontally-resizable.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<pf-text-area resize="horizontal"></pf-text-area>

<script type="module">
import '@patternfly/elements/pf-text-area/pf-text-area.js';
</script>

9 changes: 9 additions & 0 deletions elements/pf-text-area/demo/invalid.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<pf-text-area required></pf-text-area>

<script type="module">
import '@patternfly/elements/pf-text-area/pf-text-area.js';
const textarea = document.querySelector('pf-text-area');
await textarea.updateComplete;
textarea.checkValidity();
</script>

6 changes: 6 additions & 0 deletions elements/pf-text-area/demo/pf-text-area.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<pf-text-area></pf-text-area>

<script type="module">
import '@patternfly/elements/pf-text-area/pf-text-area.js';
</script>

6 changes: 6 additions & 0 deletions elements/pf-text-area/demo/readonly.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<pf-text-area readonly value="I am the very model of a modern major general"></pf-text-area>

<script type="module">
import '@patternfly/elements/pf-text-area/pf-text-area.js';
</script>

13 changes: 13 additions & 0 deletions elements/pf-text-area/demo/validated.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<form>
<pf-text-area required></pf-text-area>
<pf-button>Validate</pf-button>
</form>

<script type="module">
import '@patternfly/elements/pf-text-area/pf-text-area.js';
document.querySelector('form').addEventListener('submit', event => {
event.preventDefault();
document.querySelector('pf-text-area').checkValidity();
});
</script>

6 changes: 6 additions & 0 deletions elements/pf-text-area/demo/vertically-resizable.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<pf-text-area resize="vertical"></pf-text-area>

<script type="module">
import '@patternfly/elements/pf-text-area/pf-text-area.js';
</script>

17 changes: 17 additions & 0 deletions elements/pf-text-area/docs/pf-text-area.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{% renderOverview %}
<pf-text-area></pf-text-area>
{% endrenderOverview %}

{% band header="Usage" %}{% endband %}

{% renderSlots %}{% endrenderSlots %}

{% renderAttributes %}{% endrenderAttributes %}

{% renderMethods %}{% endrenderMethods %}

{% renderEvents %}{% endrenderEvents %}

{% renderCssCustomProperties %}{% endrenderCssCustomProperties %}

{% renderCssParts %}{% endrenderCssParts %}
Loading
Loading