Skip to content

Commit

Permalink
feat(text-area): add pf-text-area (#2639)
Browse files Browse the repository at this point in the history
* feat(core): internals controller improvements

* feat(text-area): add pf-text-area

* fix(text-area): styles

still to do: validations, icons, auto-resize

* fix(text-area): styles and states

still to do: success and warning states, autoresize

* fix: allow for build

* refactor(core): remove comments and empty blocks

* docs(text-area): add css prop docs

* fix(text-area): resize class

* fix(text-area): add resize both css rule

* test(text-area): disable function tests

---------

Co-authored-by: Steven Spriggs <[email protected]>
  • Loading branch information
bennypowers and zeroedin authored Dec 6, 2023
1 parent ac0c376 commit c71bbe5
Show file tree
Hide file tree
Showing 21 changed files with 790 additions and 61 deletions.
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;
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

0 comments on commit c71bbe5

Please sign in to comment.