Skip to content

Commit

Permalink
fix: add span only for voiceover to inform users about valid/invalid …
Browse files Browse the repository at this point in the history
…messages
  • Loading branch information
nmerget committed Jul 25, 2024
1 parent 07904d4 commit 6ecbba2
Show file tree
Hide file tree
Showing 9 changed files with 102 additions and 6 deletions.
17 changes: 16 additions & 1 deletion packages/components/src/components/checkbox/checkbox.lite.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
useStore
} from '@builder.io/mitosis';
import { DBCheckboxProps, DBCheckboxState } from './model';
import { cls, uuid } from '../../utils';
import { cls, hasVoiceOver, uuid } from '../../utils';
import {
DEFAULT_INVALID_MESSAGE,
DEFAULT_INVALID_MESSAGE_ID_SUFFIX,
Expand All @@ -33,6 +33,7 @@ export default function DBCheckbox(props: DBCheckboxProps) {
_validMessageId: this._id + DEFAULT_VALID_MESSAGE_ID_SUFFIX,
_invalidMessageId: this._id + DEFAULT_INVALID_MESSAGE_ID_SUFFIX,
_descByIds: '',
_voiceOverFallback: '',
handleChange: (event: ChangeEvent<HTMLInputElement>) => {
if (props.onChange) {
props.onChange(event);
Expand All @@ -46,11 +47,21 @@ export default function DBCheckbox(props: DBCheckboxProps) {
/* For a11y reasons we need to map the correct message with the checkbox */
if (!ref?.validity.valid || props.customValidity === 'invalid') {
state._descByIds = state._invalidMessageId;
if (hasVoiceOver()) {
state._voiceOverFallback =
props.invalidMessage ??
ref?.validationMessage ??
DEFAULT_INVALID_MESSAGE;
}
} else if (
props.customValidity === 'valid' ||
(ref?.validity.valid && props.required)
) {
state._descByIds = state._validMessageId;
if (hasVoiceOver()) {
state._voiceOverFallback =
props.validMessage ?? DEFAULT_VALID_MESSAGE;
}
} else if (props.message) {
state._descByIds = state._messageId;
} else {
Expand Down Expand Up @@ -179,6 +190,10 @@ export default function DBCheckbox(props: DBCheckboxProps) {
ref?.validationMessage ??
DEFAULT_INVALID_MESSAGE}
</DBInfotext>

<span className="visually-hidden" role="status">
{state._voiceOverFallback}
</span>
</div>
);
}
17 changes: 16 additions & 1 deletion packages/components/src/components/input/input.lite.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
useRef,
useStore
} from '@builder.io/mitosis';
import { cls, isArrayOfStrings, uuid } from '../../utils';
import { cls, hasVoiceOver, isArrayOfStrings, uuid } from '../../utils';
import { DBInputProps, DBInputState } from './model';
import {
DEFAULT_DATALIST_ID_SUFFIX,
Expand Down Expand Up @@ -42,6 +42,7 @@ export default function DBInput(props: DBInputProps) {
_dataListId: this._id + DEFAULT_DATALIST_ID_SUFFIX,
_descByIds: '',
_value: '',
_voiceOverFallback: '',
defaultValues: {
label: DEFAULT_LABEL,
placeholder: ' '
Expand Down Expand Up @@ -69,6 +70,12 @@ export default function DBInput(props: DBInputProps) {
/* For a11y reasons we need to map the correct message with the input */
if (!ref?.validity.valid || props.customValidity === 'invalid') {
state._descByIds = state._invalidMessageId;
if (hasVoiceOver()) {
state._voiceOverFallback =
props.invalidMessage ??
ref?.validationMessage ??
DEFAULT_INVALID_MESSAGE;
}
} else if (
props.customValidity === 'valid' ||
(ref?.validity.valid &&
Expand All @@ -78,6 +85,10 @@ export default function DBInput(props: DBInputProps) {
props.pattern))
) {
state._descByIds = state._validMessageId;
if (hasVoiceOver()) {
state._voiceOverFallback =
props.validMessage ?? DEFAULT_VALID_MESSAGE;
}
} else if (props.message) {
state._descByIds = state._messageId;
} else {
Expand Down Expand Up @@ -230,6 +241,10 @@ export default function DBInput(props: DBInputProps) {
ref?.validationMessage ??
DEFAULT_INVALID_MESSAGE}
</DBInfotext>

<span class="visually-hidden" role="status">
{state._voiceOverFallback}
</span>
</div>
);
// jscpd:ignore-end
Expand Down
17 changes: 16 additions & 1 deletion packages/components/src/components/select/select.lite.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
useStore
} from '@builder.io/mitosis';
import { DBSelectOptionType, DBSelectProps, DBSelectState } from './model';
import { cls, uuid } from '../../utils';
import { cls, hasVoiceOver, uuid } from '../../utils';
import {
DEFAULT_INVALID_MESSAGE,
DEFAULT_INVALID_MESSAGE_ID_SUFFIX,
Expand Down Expand Up @@ -47,6 +47,7 @@ export default function DBSelect(props: DBSelectProps) {
_descByIds: '',
_value: '',
initialized: false,
_voiceOverFallback: '',
handleClick: (event: ClickEvent<HTMLSelectElement>) => {
if (props.onClick) {
props.onClick(event);
Expand Down Expand Up @@ -75,11 +76,21 @@ export default function DBSelect(props: DBSelectProps) {
/* For a11y reasons we need to map the correct message with the select */
if (!ref?.validity.valid || props.customValidity === 'invalid') {
state._descByIds = state._invalidMessageId;
if (hasVoiceOver()) {
state._voiceOverFallback =
props.invalidMessage ??
ref?.validationMessage ??
DEFAULT_INVALID_MESSAGE;
}
} else if (
props.customValidity === 'valid' ||
(ref?.validity.valid && props.required)
) {
state._descByIds = state._validMessageId;
if (hasVoiceOver()) {
state._voiceOverFallback =
props.validMessage ?? DEFAULT_VALID_MESSAGE;
}
} else if (props.message) {
state._descByIds = state._messageId;
} else {
Expand Down Expand Up @@ -243,6 +254,10 @@ export default function DBSelect(props: DBSelectProps) {
ref?.validationMessage ??
DEFAULT_INVALID_MESSAGE}
</DBInfotext>

<span className="visually-hidden" role="status">
{state._voiceOverFallback}
</span>
</div>
);
// jscpd:ignore-end
Expand Down
17 changes: 16 additions & 1 deletion packages/components/src/components/textarea/textarea.lite.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from '@builder.io/mitosis';
import { DBTextareaProps, DBTextareaState } from './model';
import { DBInfotext } from '../infotext';
import { cls, uuid } from '../../utils';
import { cls, hasVoiceOver, uuid } from '../../utils';
import {
DEFAULT_INVALID_MESSAGE,
DEFAULT_INVALID_MESSAGE_ID_SUFFIX,
Expand Down Expand Up @@ -40,6 +40,7 @@ export default function DBTextarea(props: DBTextareaProps) {
placeholder: ' ',
rows: '4'
},
_voiceOverFallback: '',
handleInput: (event: InputEvent<HTMLTextAreaElement>) => {
if (props.onInput) {
props.onInput(event);
Expand All @@ -63,12 +64,22 @@ export default function DBTextarea(props: DBTextareaProps) {
/* For a11y reasons we need to map the correct message with the textarea */
if (!ref?.validity.valid || props.customValidity === 'invalid') {
state._descByIds = state._invalidMessageId;
if (hasVoiceOver()) {
state._voiceOverFallback =
props.invalidMessage ??
ref?.validationMessage ??
DEFAULT_INVALID_MESSAGE;
}
} else if (
props.customValidity === 'valid' ||
(ref?.validity.valid &&
(props.required || props.minLength || props.maxLength))
) {
state._descByIds = state._validMessageId;
if (hasVoiceOver()) {
state._voiceOverFallback =
props.validMessage ?? DEFAULT_VALID_MESSAGE;
}
} else if (props.message) {
state._descByIds = state._messageId;
} else {
Expand Down Expand Up @@ -188,6 +199,10 @@ export default function DBTextarea(props: DBTextareaProps) {
ref?.validationMessage ??
DEFAULT_INVALID_MESSAGE}
</DBInfotext>

<span className="visually-hidden" role="status">
{state._voiceOverFallback}
</span>
</div>
);
// jscpd:ignore-end
Expand Down
7 changes: 7 additions & 0 deletions packages/components/src/shared/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,13 @@ export type FormState = {
_invalidMessageId?: string;
_descByIds?: string;
_value?: string;

/**
* https://www.davidmacd.com/blog/test-aria-describedby-errormessage-aria-live.html
* Currently VoiceOver isn't supporting changes from aria-describedby.
* This is an internal Fallback
*/
_voiceOverFallback?: string;
};

export type InitializedState = {
Expand Down
2 changes: 2 additions & 0 deletions packages/components/src/styles/_form-components.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
@use "@db-ui/foundations/build/scss/helpers";
@use "component";

@forward "visually-hidden";

$dropdown-icon-transition: transform variables.$db-transition-straight-emotional;
$dropdown-icon-transform: rotate(-180deg);

Expand Down
21 changes: 21 additions & 0 deletions packages/components/src/styles/visually-hidden.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
%visually-hidden {
clip: rect(0, 0, 0, 0) !important;
overflow: hidden !important;
white-space: nowrap !important;
font-size: 0 !important;
all: initial;
inset-block-start: 0 !important;
block-size: 1px !important;
position: absolute !important;
inline-size: 1px !important;
border-width: 0 !important;
border-style: initial !important;
border-color: initial !important;
border-image: initial !important;
padding: 0 !important;
pointer-events: none !important;
}

.visually-hidden {
@extend %visually-hidden;
}
8 changes: 7 additions & 1 deletion packages/components/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,11 @@ export const handleDataOutside = (el: Element): DBDataOutsidePair => {
export const isArrayOfStrings = (value: unknown): value is string[] =>
Array.isArray(value) && value.every((item) => typeof item === 'string');

const appleOs = ['Mac', 'iPhone', 'iPad', 'iPod'];
export const hasVoiceOver = (): boolean =>
typeof window !== 'undefined' &&
appleOs.some((os) => window.navigator.userAgent.includes(os));

export default {
filterPassingProps,
cls,
Expand All @@ -203,5 +208,6 @@ export default {
visibleInVY,
isInView,
handleDataOutside,
isArrayOfStrings
isArrayOfStrings,
hasVoiceOver
};
2 changes: 1 addition & 1 deletion showcases/screen-reader/tests/input.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ test.describe('DBInput', () => {
/* Goto desired input */
await voiceOver?.next();
await voiceOver?.next();
await nvda?.clearSpokenPhraseLog();
await voiceOver?.next();

await voiceOver?.type('Test');
await voiceOver?.press('Command+A');
await voiceOver?.press('Delete');
Expand Down

0 comments on commit 6ecbba2

Please sign in to comment.