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 support for minLength #10970

Draft
wants to merge 14 commits into
base: dev
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"invalid": "Invalid",
"tooLong": "The current character length is {currentLength}, which exceeds the maximum character length of {maxLength}."
"tooLong": "The current character length is {currentLength}, which exceeds the maximum character length of {maxLength}.",
"tooShort": "The current character length is {currentLength}, which does not reach the min character length of {minLength}."
Elijbet marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"invalid": "Invalid",
"tooLong": "The current character length is {currentLength}, which exceeds the maximum character length of {maxLength}."
"tooLong": "The current character length is {currentLength}, which exceeds the maximum character length of {maxLength}.",
"tooShort": "The current character length is {currentLength}, which does not reach the min character length of {minLength}."
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export interface CharacterLengthObj {
currentLength: string;
maxLength: string;
minLength: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const CSS = {
resizeDisabled: "resize--disabled",
resizeDisabledX: "resize--disabled-x",
resizeDisabledY: "resize--disabled-y",
characterOverLimit: "character--over-limit",
characterPastLimit: "character--past-limit",
Elijbet marked this conversation as resolved.
Show resolved Hide resolved
readOnly: "readonly",
textAreaInvalid: "text-area--invalid",
footerSlotted: "footer--slotted",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ describe("calcite-text-area", () => {
accessible(
html`<calcite-label>
add notes
<calcite-text-area max-length="50" required name="something"></calcite-text-area>
<calcite-text-area min-length="5" max-length="50" required name="something"></calcite-text-area>
</calcite-label>`,
);
});
Expand Down Expand Up @@ -164,6 +164,23 @@ describe("calcite-text-area", () => {
expect(await element.getProperty("value")).toBe("rocky mountains");
});

it("should be able to enter characters below min-length", async () => {
const page = await newE2EPage();
await page.setContent("<calcite-text-area></calcite-text-area>");

const element = await page.find("calcite-text-area");
element.setAttribute("min-length", "5");
Elijbet marked this conversation as resolved.
Show resolved Hide resolved
await page.waitForChanges();

await page.keyboard.press("Tab");
await page.waitForChanges();

await page.keyboard.type("rock");
await page.waitForChanges();

expect(await element.getProperty("value")).toBe("rock");
});

it("should have footer--slotted class when slotted at both start and end", async () => {
const page = await newE2EPage();
await page.setContent(`<calcite-text-area>
Expand Down Expand Up @@ -273,13 +290,45 @@ describe("calcite-text-area", () => {
});
});

describe("over limit", () => {
describe("min-chars", () => {
themed(html`<calcite-text-area min-length="5"></calcite-text-area>`, {
"--calcite-text-area-divider-color": {
shadowSelector: `.${CSS.textArea}`,
targetProp: "borderBlockEndColor",
},
"--calcite-text-area-footer-border-color": [
{
shadowSelector: `.${CSS.footer}`,
targetProp: "borderBottomColor",
},
{
shadowSelector: `.${CSS.footer}`,
targetProp: "borderLeftColor",
},
{
shadowSelector: `.${CSS.footer}`,
targetProp: "borderRightColor",
},
],
});
});

describe("over max limit", () => {
themed(html`<calcite-text-area max-length="4" value="12345"></calcite-text-area>`, {
"--calcite-text-area-character-limit-text-color": {
shadowSelector: `.${CSS.characterLimit}`,
targetProp: "color",
},
});
});

describe("under min limit", () => {
themed(html`<calcite-text-area min-length="4" value="12345"></calcite-text-area>`, {
"--calcite-text-area-character-limit-text-color": {
shadowSelector: `.${CSS.characterLimit}`,
targetProp: "color",
},
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@
padding-inline-start: var(--calcite-spacing-md);
}

.character--over-limit {
.character--past-limit {
font-weight: var(--calcite-font-weight-bold);
color: var(--calcite-color-status-danger);
}
Expand Down
61 changes: 46 additions & 15 deletions packages/calcite-components/src/components/text-area/text-area.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ export class TextArea
@property() label: string;

/**
* Specifies the maximum number of characters allowed.
* Specifies the maximum number of characters allowed. Must be greater than or equal to the value of `minlength`, if present and valid.
Elijbet marked this conversation as resolved.
Show resolved Hide resolved
*
* @mdn [maxlength](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea#attr-maxlength)
*/
Expand All @@ -178,7 +178,7 @@ export class TextArea
messages = useT9n<typeof T9nStrings>({ blocking: true });

/**
* Specifies the minimum number of characters allowed.
* Specifies the minimum number of characters allowed. Must be less than or equal to the value of `maxlength`, if present and valid.
*
* @mdn [minlength](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea#attr-minlength)
*/
Expand Down Expand Up @@ -361,8 +361,9 @@ export class TextArea
private getLocalizedCharacterLength(): CharacterLengthObj {
const currentLength = this.value ? this.value.length.toString() : "0";
const maxLength = this.maxLength.toString();
const minLength = this.minLength.toString();
if (this.numberingSystem === "latn") {
return { currentLength, maxLength };
return { currentLength, maxLength, minLength };
}

numberStringFormatter.numberFormatOptions = {
Expand All @@ -374,13 +375,17 @@ export class TextArea
return {
currentLength: numberStringFormatter.localize(currentLength),
maxLength: numberStringFormatter.localize(maxLength),
minLength: numberStringFormatter.localize(minLength),
};
}

syncHiddenFormInput(input: HTMLInputElement): void {
input.setCustomValidity("");
if (this.isCharacterLimitExceeded()) {
input.setCustomValidity(this.replacePlaceholdersInMessages());
if (this.isCharacterOverMaxLimit()) {
input.setCustomValidity(this.replacePlaceholdersInLongMessages());
}
if (this.isCharacterUnderMinLimit()) {
Elijbet marked this conversation as resolved.
Show resolved Hide resolved
input.setCustomValidity(this.replacePlaceholdersInShortMessages());
}

syncHiddenFormInput("textarea", this, input);
Expand Down Expand Up @@ -426,34 +431,50 @@ export class TextArea
};
}

private replacePlaceholdersInMessages(): string {
private replacePlaceholdersInLongMessages(): string {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: can we use a different name here to be more meaningful.

replaceMaxLengthPlaceHolderMessage( ) or getMaxLengthMessage( ).

return this.messages.tooLong
.replace("{maxLength}", this.localizedCharacterLengthObj.maxLength)
.replace("{currentLength}", this.localizedCharacterLengthObj.currentLength);
.replace("{currentLength}", this.localizedCharacterLengthObj.currentLength)
.replace("{maxLength}", this.localizedCharacterLengthObj.maxLength);
}

private replacePlaceholdersInShortMessages(): string {
Elijbet marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: can we use a different name here to be more meaningful.

replaceMinLengthPlaceHolderMessage( ) or getMinLengthMessage( ).

return this.messages.tooShort
.replace("{currentLength}", this.localizedCharacterLengthObj.currentLength)
.replace("{minLength}", this.localizedCharacterLengthObj.minLength);
}

private isCharacterLimitExceeded(): boolean {
private isCharacterOverMaxLimit(): boolean {
return this.value?.length > this.maxLength;
}

private isCharacterUnderMinLimit(): boolean {
return this.value?.length < this.minLength;
}

// #endregion

// #region Rendering

override render(): JsxNode {
const hasFooter = this.startSlotHasElements || this.endSlotHasElements || !!this.maxLength;
const hasFooter =
this.startSlotHasElements || this.endSlotHasElements || !!this.maxLength || !!this.minLength;
return (
<InteractiveContainer disabled={this.disabled}>
<textarea
aria-describedby={this.guid}
aria-errormessage={IDS.validationMessage}
ariaInvalid={this.status === "invalid" || this.isCharacterLimitExceeded()}
ariaInvalid={
this.status === "invalid" ||
this.isCharacterOverMaxLimit() ||
this.isCharacterUnderMinLimit()
}
ariaLabel={getLabelText(this)}
autofocus={this.el.autofocus}
class={{
[CSS.textArea]: true,
[CSS.readOnly]: this.readOnly,
[CSS.textAreaInvalid]: this.isCharacterLimitExceeded(),
[CSS.textAreaInvalid]:
this.isCharacterOverMaxLimit() || this.isCharacterUnderMinLimit(),
Elijbet marked this conversation as resolved.
Show resolved Hide resolved
[CSS.footerSlotted]: this.endSlotHasElements && this.startSlotHasElements,
[CSS.textAreaOnly]: !hasFooter,
}}
Expand Down Expand Up @@ -504,9 +525,14 @@ export class TextArea
{this.renderCharacterLimit()}
</footer>
<HiddenFormInputSlot component={this} />
{this.isCharacterLimitExceeded() && (
{this.isCharacterOverMaxLimit() && (
<span ariaLive="polite" class={CSS.assistiveText} id={this.guid}>
{this.replacePlaceholdersInMessages()}
{this.replacePlaceholdersInLongMessages()}
</span>
)}
{this.isCharacterUnderMinLimit() && (
<span ariaLive="polite" class={CSS.assistiveText} id={this.guid}>
{this.replacePlaceholdersInShortMessages()}
</span>
)}
{this.validationMessage && this.status === "invalid" ? (
Expand All @@ -527,7 +553,12 @@ export class TextArea
this.localizedCharacterLengthObj = this.getLocalizedCharacterLength();
return (
<span class={CSS.characterLimit}>
<span class={{ [CSS.characterOverLimit]: this.isCharacterLimitExceeded() }}>
<span
class={{
[CSS.characterPastLimit]:
this.isCharacterOverMaxLimit() || this.isCharacterUnderMinLimit(),
}}
>
{this.localizedCharacterLengthObj.currentLength}
</span>
{"/"}
Expand Down
Loading