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

refactor(AutoSuggestionInput): add more a11y messages #1671

Merged
merged 11 commits into from
Feb 4, 2025
8 changes: 8 additions & 0 deletions app/components/inputs/autoSuggestInput/AutoSuggestInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Select, { type InputActionMeta } from "react-select";
import { useField } from "remix-validated-form";
import type { DataListType } from "~/services/cms/components/StrapiAutoSuggestInput";
import type { DataListOptions } from "~/services/dataListOptions/getDataListOptions";
import { useTranslations } from "~/services/translations/translationsContext";
import { type ErrorMessageProps } from "..";
import {
CustomClearIndicator,
Expand All @@ -19,6 +20,10 @@ import Input from "../Input";
import InputError from "../InputError";
import InputLabel from "../InputLabel";
import { widthClassname, type FieldWidth } from "../width";
import {
ariaLiveMessages,
screenReaderStatus,
} from "./accessibilityConfig/ariaLiveMessages";

const MINIMUM_SEARCH_SUGGESTION_CHARACTERS = 3;
const AIRPORT_CODE_LENGTH = 3;
Expand Down Expand Up @@ -124,6 +129,7 @@ const AutoSuggestInput = ({
const [optionWasSelected, setOptionWasSelected] = useState(false);
useEffect(() => setJsAvailable(true), []);
const [options, setOptions] = useState<DataListOptions[]>([]);
const { accessibility: translations } = useTranslations();

const onInputChange = (value: string, { action }: InputActionMeta) => {
if (action === "input-change") {
Expand Down Expand Up @@ -173,6 +179,7 @@ const AutoSuggestInput = ({
aria-describedby={error && errorId}
aria-errormessage={error && errorId}
aria-invalid={error !== undefined}
ariaLiveMessages={ariaLiveMessages(translations)}
className={classNames(
"w-full",
{ "has-error": error },
Expand Down Expand Up @@ -220,6 +227,7 @@ const AutoSuggestInput = ({
onInputChange={onInputChange}
options={options}
placeholder={placeholder ?? ""}
screenReaderStatus={screenReaderStatus(translations)}
styles={customStyles(hasError)}
tabIndex={isDisabled ? -1 : 0}
value={currentItemValue}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import {
AriaGuidanceProps,
AriaOnChangeProps,
AriaOnFilterProps,
AriaOnFocusProps,
GroupBase,
OptionsOrGroups,
} from "react-select";
import {
getTranslationByKey,
type Translations,
} from "~/services/translations/getTranslationByKey";
import {
GUIDANCE_MENU_BASE,
GUIDANCE_MENU_TAB_SELECTS_VALUE,
GUIDANCE_INPUT_ARIA_LABEL_FALLBACK,
GUIDANCE_INPUT_BASE,
GUIDANCE_INPUT_IS_SEARCHABLE,
GUIDANCE_INPUT_OPEN_MENU,
GUIDANCE_VALUE,
ON_CHANGE_DESELECTED,
ON_CHANGE_CLEAR,
ON_CHANGE_INITIAL_INPUT_FOCUS_SINGULAR,
ON_CHANGE_SELECT_OPTION_ENABLED,
ARRAY_INDEX_FORMAT,
ON_FOCUS_VALUE,
ON_FOCUS_MENU_SELECTED,
ON_FOCUS_MENU_DISABLED,
ON_FOCUS_MENU_BASE,
ON_FILTER_INPUT_PART,
SCREEN_READER_STATUS,
} from "./ariaTranslationKeys";

export const ariaLiveMessages = (translations: Translations) => ({
guidance: (props: AriaGuidanceProps) => {
const { isSearchable, tabSelectsValue, context, isInitialFocus } = props;

switch (context) {
case "menu": {
let message = getTranslationByKey(GUIDANCE_MENU_BASE, translations);
if (tabSelectsValue) {
message += getTranslationByKey(
GUIDANCE_MENU_TAB_SELECTS_VALUE,
translations,
);
}
message += ".";
return message;
}

case "input": {
if (!isInitialFocus) return "";

const ariaLabel =
props["aria-label"] ??
getTranslationByKey(GUIDANCE_INPUT_ARIA_LABEL_FALLBACK, translations);
let message = getTranslationByKey(
GUIDANCE_INPUT_BASE,
translations,
).replace("{{ariaLabel}}", ariaLabel);

if (isSearchable) {
message += getTranslationByKey(
GUIDANCE_INPUT_IS_SEARCHABLE,
translations,
);
}

message += getTranslationByKey(GUIDANCE_INPUT_OPEN_MENU, translations);

return message;
}

case "value":
return getTranslationByKey(GUIDANCE_VALUE, translations);

default:
return "";
}
},

onChange: <Option, IsMulti extends boolean>(
props: AriaOnChangeProps<Option, IsMulti>,
) => {
const { action, label = "", labels } = props;
switch (action) {
case "pop-value":
case "remove-value": {
const message = getTranslationByKey(ON_CHANGE_DESELECTED, translations);
return message.replace("{{label}}", label);
}
case "clear":
pgurusinga marked this conversation as resolved.
Show resolved Hide resolved
return getTranslationByKey(ON_CHANGE_CLEAR, translations);
case "initial-input-focus": {
const message = getTranslationByKey(
ON_CHANGE_INITIAL_INPUT_FOCUS_SINGULAR,
translations,
);
return message.replace("{{labels}}", labels.join(", "));
}
case "select-option": {
const message = getTranslationByKey(
ON_CHANGE_SELECT_OPTION_ENABLED,
translations,
);
return message.replace("{{label}}", label);
}
default:
return "";
}
},

onFocus: <Option, Group extends GroupBase<Option>>(
props: AriaOnFocusProps<Option, Group>,
) => {
const {
context,
focused,
options,
label = "",
selectValue,
isDisabled,
isSelected,
isAppleDevice,
} = props;

const getArrayIndex = (
array: OptionsOrGroups<Option, Group>,
item: Option,
) => {
if (array?.length) {
const currentIndex = array.indexOf(item) + 1;
const totalCount = array.length;
const format = getTranslationByKey(ARRAY_INDEX_FORMAT, translations);
return format
.replace("{{currentIndex}}", currentIndex.toString())
.replace("{{totalCount}}", totalCount.toString());
} else {
return "";
}
};

if (context === "value" && selectValue) {
const index = getArrayIndex(selectValue, focused);
const message = getTranslationByKey(ON_FOCUS_VALUE, translations);
return message.replace("{{label}}", label).replace("{{index}}", index);
}

if (context === "menu" && isAppleDevice) {
const statusParts = [];
if (isSelected) {
statusParts.push(
getTranslationByKey(ON_FOCUS_MENU_SELECTED, translations),
);
}
if (isDisabled) {
statusParts.push(
getTranslationByKey(ON_FOCUS_MENU_DISABLED, translations),
);
}
const status = statusParts.join("");
const index = getArrayIndex(options, focused);
const messageBase = getTranslationByKey(ON_FOCUS_MENU_BASE, translations);
return messageBase
.replace("{{label}}", label)
.replace("{{status}}", status)
.replace("{{index}}", index);
}
return "";
},

onFilter: (props: AriaOnFilterProps) => {
const { inputValue, resultsMessage } = props;
const inputPart = inputValue
? getTranslationByKey(ON_FILTER_INPUT_PART, translations).replace(
"{{inputValue}}",
inputValue,
)
: "";
return `${resultsMessage}${inputPart}.`;
},
});

// **Updated screenReaderStatus Function**
export const screenReaderStatus =
(translations: Translations) =>
({ count }: { count: number }) => {
const message = getTranslationByKey(SCREEN_READER_STATUS, translations);
const optionsText = count !== 1 ? "en" : "";
return message
.replace("{{count}}", count.toString())
.replace("{{options}}", optionsText);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export const GUIDANCE_MENU_BASE = "auto-suggest-guidance-menu-base";
export const GUIDANCE_MENU_TAB_SELECTS_VALUE =
"auto-suggest-guidance-menu-tab-selects-value";
export const GUIDANCE_INPUT_ARIA_LABEL_FALLBACK =
"guidance-input-aria-label-fallback";
export const GUIDANCE_INPUT_BASE = "auto-suggest-guidance-input-base";
export const GUIDANCE_INPUT_IS_SEARCHABLE =
"auto-suggest-guidance-input-is-searchable";
export const GUIDANCE_INPUT_OPEN_MENU = "auto-suggest-guidance-input-open-menu";
export const GUIDANCE_VALUE = "auto-suggest-guidance-value";
export const ON_CHANGE_DESELECTED = "auto-suggest-on-change-deselected";
export const ON_CHANGE_CLEAR = "auto-suggest-on-change-clear";
export const ON_CHANGE_INITIAL_INPUT_FOCUS_SINGULAR =
"auto-suggest-on-change-initial-input-focus-singular";
export const ON_CHANGE_SELECT_OPTION_ENABLED =
"auto-suggest-on-change-select-option-enabled";
export const ON_FOCUS_VALUE = "auto-suggest-on-focus-value";
export const ON_FOCUS_MENU_BASE = "auto-suggest-on-focus-menu-base";
export const ON_FOCUS_MENU_SELECTED = "auto-suggest-on-focus-menu-selected";
export const ON_FOCUS_MENU_DISABLED = "auto-suggest-on-focus-menu-disabled";
export const ON_FILTER_INPUT_PART = "auto-suggest-on-filter-input-part";
export const ARRAY_INDEX_FORMAT = "auto-suggest-array-index-format";
export const SCREEN_READER_STATUS = "auto-suggest-screen-reader-status";
Loading