Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/pf-text-input-error-text.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@patternfly/elements": minor
---
`<pf-text-input>`: adds `helper-text`, `error-text`, and `validate-on` attributes. Forwards `pattern` attribute

```html
<pf-text-input id="validated"
error-text="Enter a three digit integer"
helper-text="How much wood could a woodchuck chuck?"
validate-on="blur"
pattern="\d{3}"
required></pf-text-input>
<pf-button id="validate">Validate</pf-button>
```
26 changes: 26 additions & 0 deletions elements/pf-text-input/demo/validation.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<fieldset>
<legend>Invalid</legend>
<label>Validate on Blur? <pf-switch id="onblur"></pf-switch></label>
<pf-text-input id="validated"
error-text="Enter a three digit integer"
pattern="\d{3}"
required></pf-text-input>
<pf-button id="validate">Validate</pf-button>
</fieldset>

<script type="module">
import '@patternfly/elements/pf-button/pf-button.js';
import '@patternfly/elements/pf-switch/pf-switch.js';
import '@patternfly/elements/pf-text-input/pf-text-input.js';
const onblur = document.getElementById('onblur');
const input = document.getElementById('validated');
const validate = document.getElementById('validate');
onblur.addEventListener('change', function() {
validated.validateOn = this.checked ? 'blur' : null;
});
validate.addEventListener('click', function() {
validated.checkValidity();
});
</script>

<link rel="stylesheet" href="demo.css">
6 changes: 6 additions & 0 deletions elements/pf-text-input/pf-text-input.css
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@
--pf-c-form-control--m-icon-sprite__select--success--BackgroundPosition: calc(100% - var(--pf-global--spacer--md, 1rem) + 1px - var(--pf-global--spacer--lg, 1.5rem));
--pf-c-form-control--m-icon-sprite__select--m-warning--BackgroundPosition: calc(100% - var(--pf-global--spacer--md, 1rem) - var(--pf-global--spacer--lg, 1.5rem) + 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, 1.5rem));
/* NB: this var doesn't exist in pf react */
--pf-c-form-control__error-text--m-status--Color: var(--pf-global--danger-color--100, #c9190b);

display: inline-block;
/* adjust the host to fit the input */
Expand Down Expand Up @@ -163,6 +165,10 @@ input::placeholder {
color: var(--pf-c-form-control--placeholder--Color);
}

#error-text {
color: var(--pf-c-form-control__error-text--m-status--Color);
}

:host([left-truncated]) {
position: relative;
}
Expand Down
53 changes: 49 additions & 4 deletions elements/pf-text-input/pf-text-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ function getLabelText(label: HTMLElement) {
* @cssprop --pf-c-form-control--m-icon-sprite__select--success--BackgroundPosition - {@default calc(100% - var(--pf-global--spacer--md, 1rem) + 1px - var(--pf-global--spacer--lg, 1.5rem))}
* @cssprop --pf-c-form-control--m-icon-sprite__select--m-warning--BackgroundPosition - {@default calc(100% - var(--pf-global--spacer--md, 1rem) - var(--pf-global--spacer--lg, 1.5rem) + 0.0625rem)}
* @cssprop --pf-c-form-control--m-icon-sprite__select--invalid--BackgroundPosition - {@default calc(100% - var(--pf-global--spacer--md, 1rem) - var(--pf-global--spacer--lg, 1.5rem))}
* @cssprop --pf-c-form-control__error-text--m-status--Color - {@default var(--pf-global--danger-color--100, #c9190b)}
*/
@customElement('pf-text-input')
export class PfTextInput extends LitElement {
Expand Down Expand Up @@ -185,9 +186,21 @@ export class PfTextInput extends LitElement {
/** Flag to show if the input is required. */
@property({ type: Boolean, reflect: true }) required = false;

/** Validation pattern, like `<input>` */
@property() pattern?: string;

/** Flag to show if the input is read only. */
@property({ type: Boolean, reflect: true }) readonly = false;

/** Helper text is text below a form field that helps a user provide the right information, like "Enter a unique name". */
@property({ attribute: 'helper-text' }) helperText?: string;

/** If set to 'blur', will validate when focus leaves the input */
@property({ attribute: 'validate-on' }) validateOn?: 'blur';

/** Displayed when validation fails */
@property({ attribute: 'error-text' }) errorText?: string;

/** Input placeholder. */
@property() placeholder?: string;

Expand All @@ -198,6 +211,8 @@ export class PfTextInput extends LitElement {

#derivedLabel = '';

#touched = false;

get #input() {
return this.shadowRoot?.getElementById('input') as HTMLInputElement ?? null;
}
Expand All @@ -213,48 +228,78 @@ export class PfTextInput extends LitElement {
}

override render() {
const { valid } = this.#internals.validity;
return html`
<input id="input"
.placeholder="${this.placeholder ?? ''}"
.value="${this.value}"
.pattern="${this.pattern as string}"
@input="${this.#onInput}"
@blur="${this.#onBlur}"
?disabled="${this.matches(':disabled') || this.disabled}"
?readonly="${this.readonly}"
?required="${this.required}"
aria-label="${this.#derivedLabel}"
placeholder="${ifDefined(this.placeholder)}"
type="${ifDefined(this.type)}"
.value="${this.value}"
style="${ifDefined(this.customIconUrl && styleMap({
backgroundImage: `url('${this.customIconUrl}')`,
backgroundSize: this.customIconDimensions,
}))}">
<span id="helper-text" ?hidden="${!this.helperText ?? valid}">${this.helperText}</span>
<span id="error-text" ?hidden="${valid}">${this.#internals.validationMessage}</span>
`;
}

#onInput(event: Event & { target: HTMLInputElement }) {
const { value } = event.target;
this.value = value;
this.#internals.setFormValue(value);
if (this.#touched && !this.#internals.validity.valid) {
this.#onBlur();
}
this.#touched = true;
}

#onBlur() {
if (this.validateOn === 'blur') {
this.checkValidity();
}
}

#setValidityFromInput() {
this.#internals.setValidity(
this.#input?.validity,
this.#input.validationMessage,
this.errorText ?? this.#input.validationMessage,
);
this.requestUpdate();
}

async formStateRestoreCallback(state: string, mode: string) {
if (mode === 'restore') {
const [controlMode, value] = state.split('/');
this.value = value ?? controlMode;
this.requestUpdate();
await this.updateComplete;
this.#setValidityFromInput();
}
}


async formDisabledCallback() {
await this.updateComplete;
this.requestUpdate();
}

setCustomValidity(message: string) {
this.#internals.setValidity({}, message);
this.requestUpdate();
}

checkValidity() {
this.#setValidityFromInput();
return this.#internals.checkValidity();
const validity = this.#internals.checkValidity();
this.requestUpdate();
return validity;
}

reportValidity() {
Expand Down