Skip to content

Commit 54a5e4e

Browse files
committed
refactor(textarea): convert to a form associated shadow component
1 parent 68e634c commit 54a5e4e

File tree

3 files changed

+66
-3
lines changed

3 files changed

+66
-3
lines changed

core/src/components/textarea/textarea.tsx

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { ComponentInterface, EventEmitter } from '@stencil/core';
22
import {
3+
AttachInternals,
34
Build,
45
Component,
56
Element,
@@ -15,7 +16,7 @@ import {
1516
writeTask,
1617
} from '@stencil/core';
1718
import type { NotchController } from '@utils/forms';
18-
import { createNotchController } from '@utils/forms';
19+
import { createNotchController, reportValidityToElementInternals } from '@utils/forms';
1920
import type { Attributes } from '@utils/helpers';
2021
import { inheritAriaAttributes, debounceEvent, inheritAttributes, componentOnReady } from '@utils/helpers';
2122
import { createSlotMutationController } from '@utils/slot-mutation-controller';
@@ -43,7 +44,8 @@ import type { TextareaChangeEventDetail, TextareaInputEventDetail } from './text
4344
md: 'textarea.md.scss',
4445
ionic: 'textarea.ionic.scss',
4546
},
46-
scoped: true,
47+
shadow: true,
48+
formAssociated: true
4749
})
4850
export class Textarea implements ComponentInterface {
4951
private nativeInput?: HTMLTextAreaElement;
@@ -73,6 +75,8 @@ export class Textarea implements ComponentInterface {
7375

7476
@Element() el!: HTMLIonTextareaElement;
7577

78+
@AttachInternals() internals!: ElementInternals;
79+
7680
/**
7781
* The `hasFocus` state ensures the focus class is
7882
* added regardless of how the element is focused.
@@ -184,7 +188,7 @@ export class Textarea implements ComponentInterface {
184188
/**
185189
* If `true`, the user must fill in a value before submitting a form.
186190
*/
187-
@Prop() required = false;
191+
@Prop({ reflect: true }) required = false;
188192

189193
/**
190194
* If `true`, the element will have its spelling and grammar checked.
@@ -288,6 +292,15 @@ export class Textarea implements ComponentInterface {
288292
nativeInput.value = value;
289293
}
290294
this.runAutoGrow();
295+
this.reportValidity();
296+
}
297+
298+
/**
299+
* Update validation state when required prop changes
300+
*/
301+
@Watch('required')
302+
protected requiredChanged() {
303+
this.reportValidity();
291304
}
292305

293306
/**
@@ -433,6 +446,7 @@ export class Textarea implements ComponentInterface {
433446
componentDidLoad() {
434447
this.originalIonInput = this.ionInput;
435448
this.runAutoGrow();
449+
this.reportValidity();
436450
}
437451

438452
componentDidRender() {
@@ -554,6 +568,15 @@ export class Textarea implements ComponentInterface {
554568
return this.value || '';
555569
}
556570

571+
/**
572+
* Reports the validity state to the browser via ElementInternals.
573+
* This delegates to the native textarea's built-in validation,
574+
* which automatically handles the required prop and other constraints.
575+
*/
576+
private reportValidity() {
577+
reportValidityToElementInternals(this.nativeInput, this.internals);
578+
}
579+
557580
// `Event` type is used instead of `InputEvent`
558581
// since the types from Stencil are not derived
559582
// from the element (e.g. textarea and input
@@ -568,6 +591,11 @@ export class Textarea implements ComponentInterface {
568591
};
569592

570593
private onChange = (ev: Event) => {
594+
const input = ev.target as HTMLTextAreaElement | null;
595+
if (input) {
596+
this.internals.setFormValue(input.value);
597+
this.reportValidity();
598+
}
571599
this.emitValueChange(ev);
572600
};
573601

core/src/utils/forms/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './notch-controller';
22
export * from './compare-with-utils';
3+
export * from './validity';

core/src/utils/forms/validity.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// TODO this file is going to cause conflicts once it updates from main
2+
3+
export const getValidityFlags = (validity: ValidityState): ValidityStateFlags => {
4+
return {
5+
badInput: validity.badInput,
6+
customError: validity.customError,
7+
patternMismatch: validity.patternMismatch,
8+
rangeOverflow: validity.rangeOverflow,
9+
rangeUnderflow: validity.rangeUnderflow,
10+
stepMismatch: validity.stepMismatch,
11+
tooLong: validity.tooLong,
12+
tooShort: validity.tooShort,
13+
typeMismatch: validity.typeMismatch,
14+
valueMissing: validity.valueMissing,
15+
};
16+
};
17+
18+
/**
19+
* Reports the validity state of a native form element to ElementInternals.
20+
* This delegates to the native element's built-in validation, which automatically
21+
* handles required, minlength, maxlength, and other constraints.
22+
*/
23+
export const reportValidityToElementInternals = (nativeElement: HTMLInputElement | HTMLTextAreaElement | null | undefined, internals: ElementInternals): void => {
24+
if (!nativeElement) {
25+
return;
26+
}
27+
28+
if (nativeElement.validity.valid) {
29+
internals.setValidity({});
30+
} else {
31+
const validityFlags = getValidityFlags(nativeElement.validity);
32+
internals.setValidity(validityFlags, nativeElement.validationMessage, nativeElement);
33+
}
34+
};

0 commit comments

Comments
 (0)