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

Skin Tone Accessibility Updates #317

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
21 changes: 19 additions & 2 deletions src/components/Layout/Relative.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,27 @@ type Props = Readonly<{
children: React.ReactNode;
className?: string;
style?: React.CSSProperties;
button?: boolean;
tabIndex?: number;
onKeyDown?: (event: React.KeyboardEvent<HTMLButtonElement>) => void;
}>;

export default function Relative({ children, className, style }: Props) {
return (
export default function Relative({
children,
className,
style,
button,
tabIndex,
onKeyDown
}: Props) {
return button ? (
<button
style={{ ...style, position: 'relative' }}
className={className}
tabIndex={tabIndex}
onKeyDown={onKeyDown}
>{children}</button>
) : (
<div style={{ ...style, position: 'relative' }} className={className}>
{children}
</div>
Expand Down
15 changes: 8 additions & 7 deletions src/components/context/PickerContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useState } from 'react';
import { useDefaultSkinToneConfig } from '../../config/useConfig';
import { DataEmoji } from '../../dataUtils/DataTypes';
import { alphaNumericEmojiIndex } from '../../dataUtils/alphaNumericEmojiIndex';
import { getSkinTone, setSkinTone } from '../../dataUtils/skinTone';
import { useDebouncedState } from '../../hooks/useDebouncedState';
import { useDisallowedEmojis } from '../../hooks/useDisallowedEmojis';
import { FilterDict } from '../../hooks/useFilter';
Expand Down Expand Up @@ -61,7 +62,7 @@ const PickerContext = React.createContext<{
searchTerm: [string, (term: string) => Promise<string>];
suggestedUpdateState: [number, (term: number) => void];
activeCategoryState: ReactState<ActiveCategoryState>;
activeSkinTone: ReactState<SkinTones>;
activeSkinTone: [SkinTones, (skinTone: SkinTones) => void];
emojisThatFailedToLoadState: ReactState<Set<string>>;
isPastInitialLoad: boolean;
emojiVariationPickerState: ReactState<DataEmoji | null>;
Expand All @@ -71,18 +72,18 @@ const PickerContext = React.createContext<{
disallowMouseRef: React.MutableRefObject<boolean>;
disallowedEmojisRef: React.MutableRefObject<Record<string, boolean>>;
}>({
activeCategoryState: [null, () => {}],
activeSkinTone: [SkinTones.NEUTRAL, () => {}],
activeCategoryState: [null, () => { }],
activeSkinTone: [getSkinTone(), (skinTone) => setSkinTone(skinTone)],
disallowClickRef: { current: false },
disallowMouseRef: { current: false },
disallowedEmojisRef: { current: {} },
emojiVariationPickerState: [null, () => {}],
emojisThatFailedToLoadState: [new Set(), () => {}],
emojiVariationPickerState: [null, () => { }],
emojisThatFailedToLoadState: [new Set(), () => { }],
filterRef: { current: {} },
isPastInitialLoad: true,
searchTerm: ['', () => new Promise<string>(() => undefined)],
skinToneFanOpenState: [false, () => {}],
suggestedUpdateState: [Date.now(), () => {}]
skinToneFanOpenState: [false, () => { }],
suggestedUpdateState: [Date.now(), () => { }]
});

type Props = Readonly<{
Expand Down
1 change: 1 addition & 0 deletions src/components/header/Search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export function Search() {
<Button
className={clsx('epr-btn-clear-search', 'epr-visible-on-search-only')}
onClick={clearSearch}
aria-label={'Clear search'}
>
<div className="epr-icn-clear-search" />
</Button>
Expand Down
90 changes: 76 additions & 14 deletions src/components/header/SkinTonePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,28 @@ import * as React from 'react';
import { ClassNames } from '../../DomUtils/classNames';
import { useSkinTonesDisabledConfig } from '../../config/useConfig';
import skinToneVariations, {
skinTonesNamed
skinTonesNamed,
} from '../../data/skinToneVariations';
import { setSkinTone } from '../../dataUtils/skinTone';
import { useCloseAllOpenToggles } from '../../hooks/useCloseAllOpenToggles';
import { useFocusSearchInput } from '../../hooks/useFocus';
import { KeyboardEvents } from '../../hooks/useKeyboardNavigation';
import { SkinTones } from '../../types/exposedTypes';
import Absolute from '../Layout/Absolute';
import Relative from '../Layout/Relative';
import { Button } from '../atoms/Button';
import { useSkinTonePickerRef } from '../context/ElementRefContext';
import {
useActiveSkinToneState,
useSkinToneFanOpenState
useSkinToneFanOpenState,
} from '../context/PickerContext';
import './SkinTonePicker.css';

const ITEM_SIZE = 28;

type Props = {
direction?: SkinTonePickerDirection;
fanOutDirection?: SkinTonePickerFanOutDirection;
};

export function SkinTonePickerMenu() {
Expand All @@ -36,7 +39,8 @@ export function SkinTonePickerMenu() {
}

export function SkinTonePicker({
direction = SkinTonePickerDirection.HORIZONTAL
direction = SkinTonePickerDirection.HORIZONTAL,
fanOutDirection = SkinTonePickerFanOutDirection.LEFT,
}: Props) {
const SkinTonePickerRef = useSkinTonePickerRef();
const isDisabled = useSkinTonesDisabledConfig();
Expand All @@ -55,16 +59,52 @@ export function SkinTonePicker({

const vertical = direction === SkinTonePickerDirection.VERTICAL;

const getHorizontalTranslation = ({
neckenth marked this conversation as resolved.
Show resolved Hide resolved
ix,
fanOutDirection,
isOpen,
}: {
ix: number;
fanOutDirection: SkinTonePickerFanOutDirection;
isOpen: boolean;
}): string => {
// By fanning out to the left, the focus remains on the last (right-most) tone in the array,
// so tabbing takes a user out of the SkinTonePicker. In order to tab through the tones, a user
// must first tab backwards.
//
// Fanning out to the right keeps the focus on the first (left-most) tone in the array so a user
// can tab from left to right.
if (fanOutDirection === SkinTonePickerFanOutDirection.LEFT) {
return `translateX(-${ix * (isOpen ? ITEM_SIZE : 0)}px)`;
}
return `translateX(${ix * (isOpen ? ITEM_SIZE : 0) -
(isOpen ? (skinToneVariations.length - 1) * ITEM_SIZE : 0)
}px)`;
};

const buttonStyle = { backgroundColor: "transparent", border: "none" }

return (
<Relative
className={clsx('epr-skin-tones', direction, {
[ClassNames.open]: isOpen
[ClassNames.open]: isOpen,
})}
style={
vertical
? { flexBasis: expandedSize, height: expandedSize }
: { flexBasis: expandedSize }
? { flexBasis: expandedSize, height: expandedSize, ...buttonStyle }
: { flexBasis: expandedSize, ...buttonStyle }
}
button={true}
neckenth marked this conversation as resolved.
Show resolved Hide resolved
tabIndex={0}
onKeyDown={(event: React.KeyboardEvent<HTMLButtonElement>) => {
const { key } = event;
if (key === KeyboardEvents.Enter) {
if (!isOpen) {
setIsOpen(true)
}
closeAllOpenToggles();
}
}}
>
<div className="epr-skin-tone-select" ref={SkinTonePickerRef}>
{skinToneVariations.map((skinToneVariation, i) => {
Expand All @@ -75,28 +115,46 @@ export function SkinTonePicker({
transform: clsx(
vertical
? `translateY(-${i * (isOpen ? ITEM_SIZE : 0)}px)`
: `translateX(-${i * (isOpen ? ITEM_SIZE : 0)}px)`,
: getHorizontalTranslation({
ix: i,
fanOutDirection,
isOpen,
}),
isOpen && active && 'scale(1.3)'
)
),
}}
onClick={() => {
if (isOpen) {
setActiveSkinTone(skinToneVariation);
setSkinTone(skinToneVariation)
focusSearchInput();
} else {
setIsOpen(true);
}
closeAllOpenToggles();
}}
// When tabbed onto the SkinTonePicker, allow Enter to open and close the fan of tones
onKeyDown={(event) => {
const { key } = event;
if (key === KeyboardEvents.Enter) {
if (isOpen) {
setActiveSkinTone(skinToneVariation);
setSkinTone(skinToneVariation)
focusSearchInput();
} else {
setIsOpen(true);
}
closeAllOpenToggles();
}
}}
tabIndex={isOpen ? 0 : -1}
key={skinToneVariation}
className={clsx(`epr-tone-${skinToneVariation}`, 'epr-tone', {
[ClassNames.active]: active
[ClassNames.active]: active,
})}
tabIndex={isOpen ? 0 : -1}
aria-pressed={active}
aria-label={`Skin tone ${
skinTonesNamed[skinToneVariation as SkinTones]
}`}
aria-label={`Skin tone ${skinTonesNamed[skinToneVariation as SkinTones]
}`}
></Button>
);
})}
Expand All @@ -107,5 +165,9 @@ export function SkinTonePicker({

export enum SkinTonePickerDirection {
VERTICAL = ClassNames.vertical,
HORIZONTAL = ClassNames.horizontal
HORIZONTAL = ClassNames.horizontal,
}
export enum SkinTonePickerFanOutDirection {
LEFT = 'LEFT',
RIGHT = 'RIGHT',
}
4 changes: 4 additions & 0 deletions src/config/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { GetEmojiUrl } from '../components/emoji/Emoji';
import { emojiUrlByUnified } from '../dataUtils/emojiSelectors';
import { getSkinTone } from '../dataUtils/skinTone';
import {
EmojiClickData,
EmojiStyle,
Expand Down Expand Up @@ -30,13 +31,16 @@ export function mergeConfig(
suggestionMode: config.suggestedEmojisMode
});

const activeSkinTone = getSkinTone()

const skinTonePickerLocation = config.searchDisabled
? SkinTonePickerLocation.PREVIEW
: config.skinTonePickerLocation;

return {
...config,
categories,
defaultSkinTone: activeSkinTone,
previewConfig,
skinTonePickerLocation
};
Expand Down
25 changes: 25 additions & 0 deletions src/dataUtils/skinTone.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { SkinTones } from '../types/exposedTypes';

const SKINTONE_LS_KEY = 'epr_skin_tone';


export function getSkinTone(): SkinTones {
try {
if (!window?.localStorage) {
return SkinTones.NEUTRAL;
}

return JSON.parse(window?.localStorage.getItem(SKINTONE_LS_KEY) ?? SkinTones.NEUTRAL)
} catch {
return SkinTones.NEUTRAL;
}
}

export function setSkinTone(skinTone: SkinTones) {
try {
window?.localStorage.setItem(SKINTONE_LS_KEY, JSON.stringify(skinTone));
// Prevents the change from being seen immediately.
neckenth marked this conversation as resolved.
Show resolved Hide resolved
} catch {
// ignore
}
}
2 changes: 1 addition & 1 deletion src/hooks/useKeyboardNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ import {
useIsSkinToneInSearch
} from './useShouldShowSkinTonePicker';

enum KeyboardEvents {
export enum KeyboardEvents {
ArrowDown = 'ArrowDown',
ArrowUp = 'ArrowUp',
ArrowLeft = 'ArrowLeft',
Expand Down