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

fix(input, combobox, input-date-picker, input-number, input-text, input-time-picker, radio-button-group, segmented-control, select, text-area): provide clear field error messaging for AT #9880

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ import { Validation } from "../functional/Validation";
import { IconNameOrString } from "../icon/interfaces";
import { ComboboxMessages } from "./assets/combobox/t9n";
import { ComboboxChildElement, SelectionDisplay } from "./interfaces";
import { ComboboxChildSelector, ComboboxItem, ComboboxItemGroup, CSS } from "./resources";
import { ComboboxChildSelector, ComboboxItem, ComboboxItemGroup, CSS, IDS } from "./resources";
import {
getItemAncestors,
getItemChildren,
Expand Down Expand Up @@ -1634,8 +1634,10 @@ export class Combobox
aria-activedescendant={this.activeDescendant}
aria-autocomplete="list"
aria-controls={`${listboxUidPrefix}${guid}`}
aria-errormessage={IDS.validationMessage}
aria-expanded={toAriaBoolean(open)}
aria-haspopup="listbox"
aria-invalid={this.status === "invalid"}
Copy link
Member

Choose a reason for hiding this comment

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

Late to the party but I think this should use the toAriaBoolean function to convert this to a string of "false" or "true".

Copy link
Member

Choose a reason for hiding this comment

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

Excellent catch! Can you create an issue for me to set up an ESLint rule for this?

Copy link
Member

Choose a reason for hiding this comment

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

Never mind, we already have one. I'll bring it up during our next triage session.

aria-label={getLabelText(this)}
aria-owns={`${listboxUidPrefix}${guid}`}
class={{
Expand Down Expand Up @@ -1811,6 +1813,7 @@ export class Combobox
{this.validationMessage && this.status === "invalid" ? (
<Validation
icon={this.validationIcon}
id={IDS.validationMessage}
message={this.validationMessage}
scale={this.scale}
status={this.status}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,7 @@ export const CSS = {
placeholderIcon: "placeholder-icon",
selectedIcon: "selected-icon",
};

export const IDS = {
validationMessage: "comboboxValidationMessage",
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ interface ValidationProps extends JSXBase.HTMLAttributes {
scale: Scale;
status: Status;
icon?: IconNameOrString | boolean;
id?: string;
message: string;
}

Expand All @@ -17,11 +18,12 @@ export const CSS = {
export const Validation: FunctionalComponent<ValidationProps> = ({
scale,
status,
id,
icon,
message,
}) => (
<div class={CSS.validationContainer}>
<calcite-input-message icon={icon} scale={scale} status={status}>
<calcite-input-message aria-live="polite" icon={icon} id={id} scale={scale} status={status}>
{message}
</calcite-input-message>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ import { syncHiddenFormInput } from "../input/common/input";
import { isBrowser } from "../../utils/browser";
import { normalizeToCurrentCentury, isTwoDigitYear } from "./utils";
import { InputDatePickerMessages } from "./assets/input-date-picker/t9n";
import { CSS } from "./resources";
import { CSS, IDS } from "./resources";

@Component({
tag: "calcite-input-date-picker",
Expand Down Expand Up @@ -568,8 +568,10 @@ export class InputDatePicker
aria-autocomplete="none"
aria-controls={this.dialogId}
aria-describedby={this.placeholderTextId}
aria-errormessage={IDS.validationMessage}
aria-expanded={toAriaBoolean(this.open)}
aria-haspopup="dialog"
aria-invalid={this.status === "invalid"}
Copy link
Member

Choose a reason for hiding this comment

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

same here

class={{
[CSS.input]: true,
[CSS.inputNoBottomBorder]: this.layout === "vertical" && this.range,
Expand Down Expand Up @@ -661,8 +663,10 @@ export class InputDatePicker
<calcite-input-text
aria-autocomplete="none"
aria-controls={this.dialogId}
aria-errormessage={IDS.validationMessage}
aria-expanded={toAriaBoolean(this.open)}
aria-haspopup="dialog"
aria-invalid={this.status === "invalid"}
Copy link
Member

Choose a reason for hiding this comment

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

same here

class={{
[CSS.input]: true,
[CSS.inputBorderTopColorOne]: this.layout === "vertical" && this.range,
Expand All @@ -689,6 +693,7 @@ export class InputDatePicker
{this.validationMessage && this.status === "invalid" ? (
<Validation
icon={this.validationIcon}
id={IDS.validationMessage}
Copy link
Member

Choose a reason for hiding this comment

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

Since IDs are scoped within their respective shadow DOM, one option worth exploring (separately) to simplify this pattern would be:

  1. define a common validation message ID (exported from Validation.tsx)
  2. always assign the common ID Validation's input-message and optionally drop id from ValidationProps (could keep as optional to allow overrides if truly needed)
  3. use the common ID in all aria-errormessage props

Copy link
Member

Choose a reason for hiding this comment

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

@benelan Do you think we should add coverage for this in formAssociated? We could possibly assert on the validation ID being associated with aria-errormessage internally.

If so, we can tackle this separately.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jcfranco I created this follow up issue to work on simplifying the validation message IDs.

message={this.validationMessage}
scale={this.scale}
status={this.status}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,7 @@ export const CSS = {
verticalArrowContainer: "vertical-arrow-container",
chevronIcon: "chevron-icon",
};

export const IDS = {
validationMessage: "inputDatePickerValidationMessage",
};
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ import {
TextualInputComponent,
} from "../input/common/input";
import { IconNameOrString } from "../icon/interfaces";
import { CSS, SLOTS } from "./resources";
import { CSS, IDS, SLOTS } from "./resources";
import { InputNumberMessages } from "./assets/input-number/t9n";

/**
Expand Down Expand Up @@ -1081,6 +1081,8 @@ export class InputNumber

const childEl = (
<input
aria-errormessage={IDS.validationMessage}
aria-invalid={this.status === "invalid"}
Copy link
Member

Choose a reason for hiding this comment

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

same here

aria-label={getLabelText(this)}
autocomplete={this.autocomplete}
autofocus={this.el.autofocus ? true : null}
Expand Down Expand Up @@ -1132,6 +1134,7 @@ export class InputNumber
{this.validationMessage && this.status === "invalid" ? (
<Validation
icon={this.validationIcon}
id={IDS.validationMessage}
message={this.validationMessage}
scale={this.scale}
status={this.status}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ export const CSS = {
numberButtonItem: "number-button-item",
};

export const IDS = {
validationMessage: "inputNumberValidationMessage",
};

export const SLOTS = {
action: "action",
};
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ import { getIconScale } from "../../utils/component";
import { Validation } from "../functional/Validation";
import { syncHiddenFormInput, TextualInputComponent } from "../input/common/input";
import { IconNameOrString } from "../icon/interfaces";
import { CSS, SLOTS } from "./resources";
import { CSS, IDS, SLOTS } from "./resources";
import { InputTextMessages } from "./assets/input-text/t9n";

/**
Expand Down Expand Up @@ -663,6 +663,8 @@ export class InputText

const childEl = (
<input
aria-errormessage={IDS.validationMessage}
aria-invalid={this.status === "invalid"}
Copy link
Member

Choose a reason for hiding this comment

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

same here

aria-label={getLabelText(this)}
autocomplete={this.autocomplete}
autofocus={this.el.autofocus ? true : null}
Expand Down Expand Up @@ -712,6 +714,7 @@ export class InputText
{this.validationMessage && this.status === "invalid" ? (
<Validation
icon={this.validationIcon}
id={IDS.validationMessage}
message={this.validationMessage}
scale={this.scale}
status={this.status}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ export const CSS = {
resizeIconWrapper: "resize-icon-wrapper",
};

export const IDS = {
validationMessage: "inputTextValidationMessage",
};

export const SLOTS = {
action: "action",
};
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ import { Validation } from "../functional/Validation";
import { focusFirstTabbable } from "../../utils/dom";
import { IconNameOrString } from "../icon/interfaces";
import { syncHiddenFormInput } from "../input/common/input";
import { CSS } from "./resources";
import { CSS, IDS } from "./resources";
import { InputTimePickerMessages } from "./assets/input-time-picker/t9n";

// some bundlers (e.g., Webpack) need dynamic import paths to be static
Expand Down Expand Up @@ -1025,7 +1025,9 @@ export class InputTimePicker
<div class="input-wrapper" onClick={this.onInputWrapperClick}>
<calcite-input-text
aria-autocomplete="none"
aria-errormessage={IDS.validationMessage}
aria-haspopup="dialog"
aria-invalid={this.status === "invalid"}
Copy link
Member

Choose a reason for hiding this comment

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

same here

disabled={disabled}
icon="clock"
id={this.referenceElementId}
Expand Down Expand Up @@ -1071,6 +1073,7 @@ export class InputTimePicker
{this.validationMessage && this.status === "invalid" ? (
<Validation
icon={this.validationIcon}
id={IDS.validationMessage}
message={this.validationMessage}
scale={this.scale}
status={this.status}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
export const CSS = {
toggleIcon: "toggle-icon",
};

export const IDS = {
validationMessage: "inputTimePickerValidationMessage",
};
7 changes: 6 additions & 1 deletion packages/calcite-components/src/components/input/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ import { Validation } from "../functional/Validation";
import { IconNameOrString } from "../icon/interfaces";
import { InputMessages } from "./assets/input/t9n";
import { InputPlacement, NumberNudgeDirection, SetValueOrigin } from "./interfaces";
import { CSS, INPUT_TYPE_ICONS, SLOTS } from "./resources";
import { CSS, IDS, INPUT_TYPE_ICONS, SLOTS } from "./resources";
import { NumericInputComponent, syncHiddenFormInput, TextualInputComponent } from "./common/input";

/**
Expand Down Expand Up @@ -1172,6 +1172,8 @@ export class Input
this.type === "number" ? (
<input
accept={this.accept}
aria-errormessage={IDS.validationMessage}
Copy link
Member

Choose a reason for hiding this comment

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

@geospatialem Do you know which input needs the corresponding ARIA attributes? The one users interact with is in shadow DOM and the one used for form submitting/validation is in light DOM (created by form.tsx and slotted into HiddenFormInputSlot). cc @benelan

Copy link
Member

Choose a reason for hiding this comment

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

@jcfranco Unfortunately can't confirm with my local environment at this time, but testing via the Chromatic build is performing as-expected providing context to the message. In the past have we tried to stick to the light DOM?

aria-invalid={this.status === "invalid"}
Copy link
Member

Choose a reason for hiding this comment

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

same here

aria-label={getLabelText(this)}
autocomplete={this.autocomplete}
autofocus={autofocus}
Expand Down Expand Up @@ -1203,6 +1205,8 @@ export class Input
? [
<this.childElType
accept={this.accept}
aria-errormessage={IDS.validationMessage}
aria-invalid={this.status === "invalid"}
Copy link
Member

Choose a reason for hiding this comment

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

same here

aria-label={getLabelText(this)}
autocomplete={this.autocomplete}
autofocus={autofocus}
Expand Down Expand Up @@ -1276,6 +1280,7 @@ export class Input
{this.validationMessage && this.status === "invalid" ? (
<Validation
icon={this.validationIcon}
id={IDS.validationMessage}
message={this.validationMessage}
scale={this.scale}
status={this.status}
Expand Down
4 changes: 4 additions & 0 deletions packages/calcite-components/src/components/input/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ export const CSS = {
numberButtonItem: "number-button-item",
};

export const IDS = {
validationMessage: "inputValidationMessage",
};

export const INPUT_TYPE_ICONS = {
tel: "phone",
password: "lock",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
} from "../../utils/loadable";
import { Validation } from "../functional/Validation";
import { IconNameOrString } from "../icon/interfaces";
import { CSS } from "./resources";
import { CSS, IDS } from "./resources";

/**
* @slot - A slot for adding `calcite-radio-button`s.
Expand Down Expand Up @@ -207,12 +207,17 @@ export class RadioButtonGroup implements LoadableComponent {
render(): VNode {
return (
<Host role="radiogroup">
<div class={CSS.itemWrapper}>
<div
aria-errormessage={IDS.validationMessage}
aria-invalid={this.status === "invalid"}
Copy link
Member

Choose a reason for hiding this comment

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

same here

class={CSS.itemWrapper}
>
<slot />
</div>
{this.validationMessage && this.status === "invalid" ? (
<Validation
icon={this.validationIcon}
id={IDS.validationMessage}
message={this.validationMessage}
scale={this.scale}
status={this.status}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
export const CSS = {
itemWrapper: "item-wrapper",
};

export const IDS = {
validationMessage: "radioButtonGroupValidationMessage",
};
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
export const CSS = {
itemWrapper: "item-wrapper",
};

export const IDS = {
validationMessage: "segmentedControlValidationMessage",
};
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import { createObserver } from "../../utils/observers";
import { Validation } from "../functional/Validation";
import { IconNameOrString } from "../icon/interfaces";
import { isBrowser } from "../../utils/browser";
import { CSS } from "./resources";
import { CSS, IDS } from "./resources";

/**
* @slot - A slot for adding `calcite-segmented-control-item`s.
Expand Down Expand Up @@ -202,7 +202,11 @@ export class SegmentedControl
render(): VNode {
return (
<Host onClick={this.handleClick} role="radiogroup">
<div class={CSS.itemWrapper}>
<div
aria-errormessage={IDS.validationMessage}
aria-invalid={this.status === "invalid"}
Copy link
Member

Choose a reason for hiding this comment

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

same here

class={CSS.itemWrapper}
>
<InteractiveContainer disabled={this.disabled}>
<slot />
<HiddenFormInputSlot component={this} />
Expand All @@ -211,6 +215,7 @@ export class SegmentedControl
{this.validationMessage && this.status === "invalid" ? (
<Validation
icon={this.validationIcon}
id={IDS.validationMessage}
message={this.validationMessage}
scale={this.scale}
status={this.status}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@ export const CSS = {
select: "select",
wrapper: "wrapper",
};

export const IDS = {
validationMessage: "selectValidationMessage",
};
5 changes: 4 additions & 1 deletion packages/calcite-components/src/components/select/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import { Scale, Status, Width } from "../interfaces";
import { getIconScale } from "../../utils/component";
import { Validation } from "../functional/Validation";
import { IconNameOrString } from "../icon/interfaces";
import { CSS } from "./resources";
import { CSS, IDS } from "./resources";

type OptionOrGroup = HTMLCalciteOptionElement | HTMLCalciteOptionGroupElement;
type NativeOptionOrGroup = HTMLOptionElement | HTMLOptGroupElement;
Expand Down Expand Up @@ -421,6 +421,8 @@ export class Select
<InteractiveContainer disabled={disabled}>
<div class={CSS.wrapper}>
<select
aria-errormessage={IDS.validationMessage}
aria-invalid={this.status === "invalid"}
Copy link
Member

Choose a reason for hiding this comment

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

same here

aria-label={getLabelText(this)}
class={CSS.select}
disabled={disabled}
Expand All @@ -435,6 +437,7 @@ export class Select
{this.validationMessage && this.status === "invalid" ? (
<Validation
icon={this.validationIcon}
id={IDS.validationMessage}
message={this.validationMessage}
scale={this.scale}
status={this.status}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ export const CSS = {
footerEndSlotOnly: "footer--end-only",
};

export const IDS = {
validationMessage: "textAreaValidationMessage",
};

export const SLOTS = {
footerStart: "footer-start",
footerEnd: "footer-end",
Expand Down
Loading
Loading