Skip to content

Commit

Permalink
feat(text-area): provide additional context for AT users when charact…
Browse files Browse the repository at this point in the history
…er limit exceeds (#7412)

**Related Issue:** #6630

## Summary
This PR will provide additional context for Assistive Technology users
when character limit exceeds. After this change, AT users will receive
error message as soon as the character limit is exceeded.
  • Loading branch information
anveshmekala authored Aug 2, 2023
1 parent 7c20289 commit c1af3c7
Show file tree
Hide file tree
Showing 3 changed files with 43 additions and 25 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export const CSS = {
assistiveText: "assistive-text",
characterLimit: "character-limit",
content: "content",
container: "container",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,10 @@ textarea.block-size--full {
@apply justify-end;
}

.assistive-text {
@apply sr-only;
}

@include hidden-form-input();
@include disabled();
@include base-component();
63 changes: 38 additions & 25 deletions packages/calcite-components/src/components/text-area/text-area.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
updateHostInteraction,
} from "../../utils/interactive";
import { CharacterLengthObj } from "./interfaces";
import { guid } from "../../utils/guid";

/**
* @slot - A slot for adding text.
Expand Down Expand Up @@ -260,12 +261,13 @@ export class TextArea
return (
<Host>
<textarea
aria-invalid={toAriaBoolean(this.value?.length > this.maxLength)}
aria-describedby={this.guid}
aria-invalid={toAriaBoolean(this.isCharacterLimitExceeded())}
aria-label={getLabelText(this)}
autofocus={this.autofocus}
class={{
[CSS.readOnly]: this.readOnly,
[CSS.textAreaInvalid]: this.value?.length > this.maxLength,
[CSS.textAreaInvalid]: this.isCharacterLimitExceeded(),
[CSS.footerSlotted]: this.endSlotHasElements && this.startSlotHasElements,
[CSS.blockSizeFull]: !hasFooter,
[CSS.borderColor]: !hasFooter,
Expand Down Expand Up @@ -317,6 +319,11 @@ export class TextArea
{this.renderCharacterLimit()}
</footer>
<HiddenFormInputSlot component={this} />
{this.isCharacterLimitExceeded() && (
<span aria-hidden={true} aria-live="polite" class={CSS.assistiveText} id={this.guid}>
{this.replacePlaceHoldersInMessages()}
</span>
)}
</Host>
);
}
Expand Down Expand Up @@ -371,6 +378,8 @@ export class TextArea
updateMessages(this, this.effectiveLocale);
}

private guid = guid();

//--------------------------------------------------------------------------
//
// Private Methods
Expand Down Expand Up @@ -410,7 +419,7 @@ export class TextArea
this.localizedCharacterLengthObj = this.getLocalizedCharacterLength();
return (
<span class={CSS.characterLimit}>
<span class={{ [CSS.characterOverLimit]: this.value?.length > this.maxLength }}>
<span class={{ [CSS.characterOverLimit]: this.isCharacterLimitExceeded() }}>
{this.localizedCharacterLengthObj.currentLength}
</span>
{"/"}
Expand Down Expand Up @@ -454,32 +463,11 @@ export class TextArea

syncHiddenFormInput(input: HTMLInputElement): void {
input.setCustomValidity("");
if (this.value?.length > this.maxLength) {
if (this.isCharacterLimitExceeded()) {
input.setCustomValidity(this.replacePlaceHoldersInMessages());
}
}

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

// height and width are set to auto here to avoid overlapping on to neighboring elements in the layout when user starts resizing.
// throttle is used to avoid flashing of textarea when user resizes.
private setHeightAndWidthToAuto = throttle(
(): void => {
if (this.resize === "vertical" || this.resize === "both") {
this.el.style.height = "auto";
}
if (this.resize === "horizontal" || this.resize === "both") {
this.el.style.width = "auto";
}
},
RESIZE_TIMEOUT,
{ leading: false }
);

setTextAreaEl = (el: HTMLTextAreaElement): void => {
this.textAreaEl = el;
this.resizeObserver.observe(el);
Expand Down Expand Up @@ -514,4 +502,29 @@ export class TextArea
footerWidth,
};
}

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

// height and width are set to auto here to avoid overlapping on to neighboring elements in the layout when user starts resizing.
// throttle is used to avoid flashing of textarea when user resizes.
private setHeightAndWidthToAuto = throttle(
(): void => {
if (this.resize === "vertical" || this.resize === "both") {
this.el.style.height = "auto";
}
if (this.resize === "horizontal" || this.resize === "both") {
this.el.style.width = "auto";
}
},
RESIZE_TIMEOUT,
{ leading: false }
);

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

0 comments on commit c1af3c7

Please sign in to comment.