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

Templates a11y #6

Merged
merged 8 commits into from
Oct 21, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ export const Dropdown = observer((properties: DropdownProps): JSX.Element => {
</div>

{(content || children) && (
<div className={`ss__dropdown__content ss__dropdown__content--${classId}`}>
<div className={`ss__dropdown__content ss__dropdown__content--${classId}`} ref={(e) => (!disableA11y ? useA11y(e) : null)}>
{cloneWithProps(content, { open: showContent, toggleOpen: toggleShowContent, treePath })}
{cloneWithProps(children, { open: showContent, toggleOpen: toggleShowContent, treePath })}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import classnames from 'classnames';
import { Theme, useTheme, CacheProvider } from '../../../../providers';
import { ComponentProps, ResultsLayout, RootNodeProperties } from '../../../../types';
import { mergeProps } from '../../../../utilities';

import type { Banner } from '@searchspring/snap-store-mobx';
import { useA11y } from '../../../../hooks/useA11y';

const CSS = {
inlineBanner: ({ width }: Partial<InlineBannerProps>) =>
Expand Down Expand Up @@ -40,7 +40,7 @@ export function InlineBanner(properties: InlineBannerProps): JSX.Element {

const props = mergeProps('inlineBanner', globalTheme, defaultProps, properties);

const { banner, disableStyles, className, layout, onClick, style, styleScript } = props;
const { banner, disableStyles, className, disableA11y, layout, onClick, style, styleScript } = props;

const styling: RootNodeProperties = { 'ss-name': props.name };
const stylingProps = props;
Expand All @@ -59,6 +59,8 @@ export function InlineBanner(properties: InlineBannerProps): JSX.Element {
onClick={(e: React.MouseEvent<Element, MouseEvent>) => {
onClick && onClick(e, banner);
}}
role={'article'}
ref={(e) => (!disableA11y ? useA11y(e) : null)}
className={classnames('ss__inline-banner', `ss__inline-banner--${layout}`, className)}
{...styling}
dangerouslySetInnerHTML={{
Expand All @@ -76,4 +78,5 @@ export interface InlineBannerProps extends ComponentProps {
width?: string;
layout?: keyof typeof ResultsLayout | ResultsLayout;
onClick?: (e: React.MouseEvent, banner: Banner) => void;
disableA11y?: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import classnames from 'classnames';
import { Theme, useTheme, CacheProvider } from '../../../providers';
import { ComponentProps, RootNodeProperties } from '../../../types';
import { mergeProps } from '../../../utilities';
import { useA11y } from '../../../hooks';

const CSS = {
overlay: ({ color, transitionSpeed }: Partial<OverlayProps>) =>
Expand Down Expand Up @@ -34,7 +35,7 @@ export function Overlay(properties: OverlayProps): JSX.Element {

const props = mergeProps('overlay', globalTheme, defaultProps, properties);

const { active, onClick, disableStyles, className, style, styleScript } = props;
const { active, onClick, disableStyles, disableA11y, className, style, styleScript } = props;

const styling: RootNodeProperties = { 'ss-name': props.name };
const stylingProps = props;
Expand All @@ -51,6 +52,7 @@ export function Overlay(properties: OverlayProps): JSX.Element {
<CacheProvider>
<div
onClick={(e: React.MouseEvent<HTMLDivElement, Event>) => onClick && active && onClick(e)}
ref={(e) => (!disableA11y ? useA11y(e) : null)}
className={classnames('ss__overlay', { 'ss__overlay--active': active }, className)}
{...styling}
/>
Expand All @@ -63,4 +65,5 @@ export interface OverlayProps extends ComponentProps {
color?: string;
transitionSpeed?: string;
onClick?: (e: React.MouseEvent<HTMLDivElement, Event>) => void;
disableA11y?: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,24 +75,15 @@ export const SearchHeader = observer((properties: SearchHeaderProps): JSX.Elemen
const defaultLang = {
titleText: {
value: titleText,
attributes: {
'aria-label': `Now showing ${pagination?.totalResults} items in the product grid`,
},
},
subtitleText: {
value: subtitleText,
},
correctedQueryText: {
value: correctedQueryText,
attributes: {
'aria-label': `No results found for ${search?.originalQuery?.string}, showing results for ${search?.query?.string} instead`,
},
},
noResultsText: {
value: noResultsText,
attributes: {
'aria-label': `No results found for ${search?.query?.string}`,
},
},
didYouMeanText: {
value: didYouMeanText,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,7 @@ export const Checkbox = observer((properties: CheckboxProps): JSX.Element => {

//initialize lang
const defaultLang = {
checkbox: {
attributes: {
'aria-label': `${disabled ? 'disabled' : ''} ${checkedState ? 'checked' : 'unchecked'} checkbox`,
},
},
checkbox: {},
};

//deep merge with props.lang
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export const FacetHierarchyOptions = observer((properties: FacetHierarchyOptions
attributes: {
'aria-label': `${
value.filtered
? `remove selected filter ${facet?.label || ''} - ${value.label}`
? `selected ${value.label}`
: facet?.label
? `filter by ${facet?.label} - ${value.label}`
: `filter by ${value.label}`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ export const FacetPaletteOptions = observer((properties: FacetPaletteOptionsProp
value.url?.link?.onClick(e);
onClick && onClick(e);
}}
aria-atomic="false"
{...(previewOnFocus ? createHoverProps(() => value?.preview && value.preview()) : {})}
{...mergedLang.paletteOption?.all}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,10 +228,9 @@ export const FacetSlider = observer((properties: FacetSliderProps): JSX.Element
const defaultLang = {
sliderHandle: {
attributes: {
'aria-label': `${facet.label} slider button`,
'aria-valuetext': `${facet.label} slider button, current value ${value}, ${
facet.range?.low ? `min value ${facet.range?.low},` : ``
} ${facet.range?.high ? `max value ${facet.range?.high}` : ``}`,
'aria-valuetext': `${facet.label} button, current value ${value}, ${facet.range?.low ? `min value ${facet.range?.low},` : ``} ${
facet.range?.high ? `max value ${facet.range?.high}` : ``
}`,
},
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,9 @@ export const Filter = observer((properties: FilterProps): JSX.Element => {
onClick && onClick(e);
}}
href={link?.href}
{...mergedLang.filter?.attributes}
{...mergedLang.filter?.all}
>
<Button {...subProps.button} disableA11y={true}>
<Button {...subProps.button}>
<Icon {...subProps.icon} {...(typeof icon == 'string' ? { icon: icon } : (icon as Partial<IconProps>))} />
{!hideFacetLabel && (
<span className="ss__filter__label">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ export function Grid(properties: GridProps): JSX.Element {
style,
styleScript,
treePath,
disableA11y,
} = props;

const subProps: GridSubProps = {
Expand Down Expand Up @@ -255,6 +256,8 @@ export function Grid(properties: GridProps): JSX.Element {
!disableOverflowAction && setLimited(!limited);
onOverflowButtonClick && onOverflowButtonClick(e, Boolean(limited), remainder);
}}
role={'button'}
ref={(e) => (!disableA11y && !disableOverflowAction ? useA11y(e) : null)}
{...(limited ? mergedLang.showMoreText.attributes : mergedLang.showLessText.attributes)}
>
{overflowButton ? (
Expand Down Expand Up @@ -295,6 +298,7 @@ export function Grid(properties: GridProps): JSX.Element {
title={option.label || option.value.toString()}
role="option"
aria-selected={selected}
aria-disabled={option.disabled}
>
{!option.background && option.backgroundImageUrl ? (
<Image {...subProps.image} src={option.backgroundImageUrl} alt={option.label || option.value.toString()} />
Expand Down Expand Up @@ -334,6 +338,7 @@ export interface GridProps extends ComponentProps {
overflowButtonInGrid?: boolean;
onOverflowButtonClick?: (e: React.MouseEvent<HTMLElement, MouseEvent>, status: boolean, remainder: number) => void;
lang?: Partial<GridLang>;
disableA11y?: boolean;
}

export interface GridLang {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ describe('List Component', () => {
expect(optionElements).toBeInTheDocument();

expect(optionElements.innerHTML).toBe(
`<span class=\"ss__checkbox ss-1mgplc8\" role=\"checkbox\" aria-checked=\"false\" ss-lang=\"checkbox\" aria-label=\" unchecked checkbox\"><span class=\"ss__checkbox__empty\"></span></span>`
`<span class=\"ss__checkbox ss-1mgplc8\" role=\"checkbox\" aria-checked=\"false\" ss-lang=\"checkbox\"><span class=\"ss__checkbox__empty\"></span></span>`
);

await userEvent.click(optionElements);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ export function List(properties: ListProps): JSX.Element {
title={option.label}
role="option"
aria-selected={selected}
aria-disabled={option.disabled || option?.available === false}
>
{!hideOptionCheckboxes && <Checkbox {...subProps.checkbox} checked={selected} disableA11y={true} />}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ describe('LoadMore Component', () => {
expect(Object.values(loadMoreElement?.classList || {}).includes('ss__load-more--loading')).toBe(true);

const buttonElement = rendered.container.querySelector('.ss__load-more__button');
expect(buttonElement).toHaveAttribute('disabled');
expect(Object.values(buttonElement?.classList || {}).includes('ss__button--disabled')).toBe(true);
//disabled attribute messes with screen reader. so we just visually disabled with css.
expect(buttonElement).not.toHaveAttribute('disabled');
});

it('renders different loading icon using loadingIcon prop', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ const CSS = {
const radialAngle = Math.max(3.6, ((360 / 100) * Math.floor((pagination!.end / pagination!.totalResults) * 100)) / 2);

return css({
'& .ss__load-more__button--disabled': {
opacity: 0.7,
borderColor: 'rgba(51,51,51,0.7)',
backgroundColor: 'initial',
chrisFrazier77 marked this conversation as resolved.
Show resolved Hide resolved
pointerEvents: 'none',
'&:hover': {
cursor: 'default',
},
},
'& .ss__load-more__button--hidden': {
display: 'none',
},
Expand Down Expand Up @@ -171,7 +180,6 @@ export const LoadMore = observer((properties: LoadMoreProps): JSX.Element => {
{ 'ss__load-more__button--hidden': isLoading && loadingLocation === 'outside' },
{ 'ss__load-more__button--disabled': isButtonDisabled }
),
disabled: isButtonDisabled,
// global theme
...globalTheme?.components?.button,
// inherited props
Expand Down Expand Up @@ -297,14 +305,16 @@ export const LoadMore = observer((properties: LoadMoreProps): JSX.Element => {
<div className={`ss__load-more__progress__indicator__bar`}></div>
</div>
)}
{!hideProgressText && <div className={'ss__load-more__progress__text'} {...mergedLang.progressText?.all}></div>}
{!hideProgressText && (
<div aria-atomic="true" aria-live="polite" className={'ss__load-more__progress__text'} {...mergedLang.progressText?.all}></div>
)}
</Fragment>
)}
{progressIndicator === 'radial' && (
<Fragment>
{!hideProgressText && hideProgressIndicator ? (
// displays text when progress indicator is hidden but hideProgressText is not
<div className="ss__load-more__progress__text">{`${store.end} / ${store.totalResults}`}</div>
<div aria-atomic="true" aria-live="polite" className="ss__load-more__progress__text">{`${store.end} / ${store.totalResults}`}</div>
) : !hideProgressIndicator ? (
<div className={'ss__load-more__progress__indicator'}>
<div className="ss__load-more__progress__indicator__radial">
Expand All @@ -314,7 +324,10 @@ export const LoadMore = observer((properties: LoadMoreProps): JSX.Element => {
<div className="ss__load-more__progress__indicator__radial__mask ss__load-more__progress__indicator__radial__mask--half">
<div className="ss__load-more__progress__indicator__radial__mask__fill"></div>
</div>
<div className="ss__load-more__progress__text"> {!hideProgressText ? `${store.end} / ${store.totalResults}` : ''}</div>
<div aria-atomic="true" aria-live="polite" className="ss__load-more__progress__text">
{' '}
{!hideProgressText ? `${store.end} / ${store.totalResults}` : ''}
</div>
</div>
</div>
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ export const Pagination = observer((properties: PaginationProps): JSX.Element =>
className={classnames('ss__pagination__page', 'ss__pagination__page--active')}
{...mergedPageLang.page?.all}
aria-current="true"
aria-live="polite"
>
{page.number}
</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ export const PerPage = observer((properties: PerPageProps): JSX.Element => {
onSelect={(e: any, option: any) => {
store.setPageSize(+option!.value);
}}
requireSelection
options={store.pageSizeOptions}
selected={store.pageSizeOption}
titleText={label}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,7 @@ export const Radio = observer((properties: RadioProps): JSX.Element => {

//initialize lang
const defaultLang = {
radio: {
attributes: {
'aria-label': `${disabled ? 'disabled' : ''} ${checkedState ? 'checked' : 'unchecked'} radio button`,
role: 'radio',
},
},
radio: {},
};

//deep merge with props.lang
Expand All @@ -173,6 +168,7 @@ export const Radio = observer((properties: RadioProps): JSX.Element => {
onClick={(e) => clickFunc(e)}
disabled={disabled}
checked={checkedState}
tabIndex={!disableA11y ? 0 : -1}
/>
</div>
) : (
Expand All @@ -184,6 +180,7 @@ export const Radio = observer((properties: RadioProps): JSX.Element => {
{...mergedLang.radio?.all}
role="radio"
aria-checked={checkedState}
aria-disabled={disabled}
>
{checkedState ? (
<Icon {...subProps.activeIcon} {...(typeof checkedIcon == 'string' ? { icon: checkedIcon } : (checkedIcon as Partial<IconProps>))} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ describe('RadioList Component', () => {
expect(optionElements).toBeInTheDocument();

expect(optionElements.innerHTML).toBe(
`<span class=\"ss__radio ss__radio-list__option__radio ss-1n8vnmv\" ss-lang=\"radio\" aria-label=\" unchecked radio button\" role=\"radio\" aria-checked=\"false\"><svg ss-name=\"inactive\" class=\"ss__icon ss__icon--bullet-o ss__radio__icon--inactive ss-rbptfi\" viewBox=\"0 0 56 56\" xmlns=\"http://www.w3.org/2000/svg\"><circle cx=\"28\" cy=\"28\" r=\"20\" stroke-width=\"3\" fill=\"white\"></circle></svg></span>`
`<span class=\"ss__radio ss__radio-list__option__radio ss-1n8vnmv\" ss-lang=\"radio\" role=\"radio\" aria-checked=\"false\"><svg ss-name=\"inactive\" class=\"ss__icon ss__icon--bullet-o ss__radio__icon--inactive ss-rbptfi\" viewBox=\"0 0 56 56\" xmlns=\"http://www.w3.org/2000/svg\"><circle cx=\"28\" cy=\"28\" r=\"20\" stroke-width=\"3\" fill=\"white\"></circle></svg></span>`
);

await userEvent.click(optionElements);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ describe('Select Component', () => {

expect(icon).toBeInTheDocument();
expect(button?.innerHTML).toBe(
'<span class="ss__button__content"><span class="ss__select__label" aria-expanded="false" role="button" ss-lang="buttonLabel" aria-label="selectme dropdown, 7 options , Currently selected option is Orange" ss-a11y="true" tabindex="0"><label ss-lang="buttonLabel">selectme</label><span class="ss__select__label__separator">: </span></span><svg ss-name="close" class="ss__icon ss__icon--angle-down ss__select__dropdown__button__icon ss-i1kidn" viewBox="0 0 56 56" xmlns="http://www.w3.org/2000/svg"><path d="M56 16.329c0 0.449-0.224 0.954-0.561 1.291l-26.148 26.148c-0.337 0.337-0.842 0.561-1.291 0.561s-0.954-0.224-1.291-0.561l-26.148-26.148c-0.337-0.337-0.561-0.842-0.561-1.291s0.224-0.954 0.561-1.291l2.806-2.806c0.337-0.337 0.786-0.561 1.291-0.561 0.449 0 0.954 0.224 1.291 0.561l22.052 22.052 22.052-22.052c0.337-0.337 0.842-0.561 1.291-0.561s0.954 0.224 1.291 0.561l2.806 2.806c0.337 0.337 0.561 0.842 0.561 1.291z"></path></svg></span>'
'<span class="ss__button__content"><span class="ss__select__label" aria-expanded="false" ss-lang="buttonLabel" aria-label="selectme dropdown, 7 options , Currently selected option is Orange"><label ss-lang="buttonLabel">selectme</label><span class="ss__select__label__separator">: </span></span><svg ss-name="close" class="ss__icon ss__icon--angle-down ss__select__dropdown__button__icon ss-i1kidn" viewBox="0 0 56 56" xmlns="http://www.w3.org/2000/svg"><path d="M56 16.329c0 0.449-0.224 0.954-0.561 1.291l-26.148 26.148c-0.337 0.337-0.842 0.561-1.291 0.561s-0.954-0.224-1.291-0.561l-26.148-26.148c-0.337-0.337-0.561-0.842-0.561-1.291s0.224-0.954 0.561-1.291l2.806-2.806c0.337-0.337 0.786-0.561 1.291-0.561 0.449 0 0.954 0.224 1.291 0.561l22.052 22.052 22.052-22.052c0.337-0.337 0.842-0.561 1.291-0.561s0.954 0.224 1.291 0.561l2.806 2.806c0.337 0.337 0.561 0.842 0.561 1.291z"></path></svg></span>'
);
expect(selection).not.toBeInTheDocument();
});
Expand All @@ -357,7 +357,7 @@ describe('Select Component', () => {
expect(icon).not.toBeInTheDocument();

expect(button?.innerHTML).toBe(
'<span class="ss__button__content"><span class="ss__select__label" aria-expanded="false" role="button" ss-lang="buttonLabel" aria-label="selectme dropdown, 7 options , Currently selected option is Orange" ss-a11y="true" tabindex="0"><label ss-lang="buttonLabel">selectme</label><span class="ss__select__label__separator">: </span></span><span class="ss__select__selection">Orange</span></span>'
'<span class="ss__button__content"><span class="ss__select__label" aria-expanded="false" ss-lang="buttonLabel" aria-label="selectme dropdown, 7 options , Currently selected option is Orange"><label ss-lang="buttonLabel">selectme</label><span class="ss__select__label__separator">: </span></span><span class="ss__select__selection">Orange</span></span>'
);
expect(selection).toBeInTheDocument();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -288,15 +288,9 @@ export const Select = observer((properties: SelectProps): JSX.Element => {
onClick={() => setOpen((prev) => !prev)}
disableA11y
button={
<Button {...subProps.button} disableA11y={true}>
<Button {...subProps.button}>
{(label || lang.buttonLabel.value) && !hideLabelOnSelection && !hideLabel && (
<span
className="ss__select__label"
ref={(e) => useA11y(e)}
aria-expanded={open}
role="button"
{...mergedLang.buttonLabel.attributes}
>
<span className="ss__select__label" aria-expanded={open} {...mergedLang.buttonLabel.attributes}>
<label {...mergedLang.buttonLabel.value}></label>
{separator && selection && <span className="ss__select__label__separator">{separator}</span>}
</span>
Expand Down Expand Up @@ -327,7 +321,12 @@ export const Select = observer((properties: SelectProps): JSX.Element => {
</Button>
}
>
<ul className="ss__select__select" role="listbox" aria-label={typeof label == 'string' ? label : ''}>
<ul
className="ss__select__select"
role="listbox"
aria-label={typeof label == 'string' ? label : ''}
ref={(e) => useA11y(e, -1, true, () => setOpen(false))}
>
{options.map((option) => (
<li
ref={(e) => useA11y(e)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ export function Swatches(properties: SwatchesProps): JSX.Element {
style={{ background: option.background ? option.background : option.backgroundImageUrl ? `` : option.value }}
onClick={(e) => !disabled && !option?.disabled && makeSelection(e as any, option)}
ref={(e) => useA11y(e)}
aria-disabled={option.disabled || option?.available === false}
role="option"
aria-selected={selected}
>
Expand Down Expand Up @@ -247,9 +248,7 @@ export type SwatchesProps = {
carousel?: Partial<CarouselProps>;
grid?: Partial<GridProps>;
type?: 'carousel' | 'grid';
} & // } // grid?: Partial<GridProps>; // type?: 'grid'; // | { // } // carousel?: Partial<CarouselProps>; // type?: 'carousel'; // | { // & (
// )
ComponentProps;
} & ComponentProps;

interface SwatchesSubProps {
carousel: Partial<CarouselProps>;
Expand Down
Loading
Loading