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
Open
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
79 changes: 65 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,15 +59,17 @@ export function SkinTonePicker({

const vertical = direction === SkinTonePickerDirection.VERTICAL;

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 }
}
>
<div className="epr-skin-tone-select" ref={SkinTonePickerRef}>
Expand All @@ -75,37 +81,82 @@ 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>
);
})}
</div>
</Relative>
);

function getHorizontalTranslation({
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)`;
}
}

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
24 changes: 24 additions & 0 deletions src/dataUtils/skinTone.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
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));
} catch {
// ignore
}
}
8 changes: 5 additions & 3 deletions src/hooks/useKeyboardNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
focusNextVisibleEmoji,
focusPrevVisibleEmoji,
focusVisibleEmojiOneRowDown,
focusVisibleEmojiOneRowUp
focusVisibleEmojiOneRowUp,
} from '../DomUtils/keyboardNavigation';
import { useScrollTo } from '../DomUtils/scrollTo';
import { buttonFromTarget } from '../DomUtils/selectors';
Expand Down Expand Up @@ -43,14 +43,15 @@ import {
useIsSkinToneInSearch
} from './useShouldShowSkinTonePicker';

enum KeyboardEvents {
export enum KeyboardEvents {
ArrowDown = 'ArrowDown',
ArrowUp = 'ArrowUp',
ArrowLeft = 'ArrowLeft',
ArrowRight = 'ArrowRight',
Escape = 'Escape',
Enter = 'Enter',
Space = ' '
Space = ' ',
Tab = "Tab"
}

export function useKeyboardNavigation() {
Expand Down Expand Up @@ -132,6 +133,7 @@ function useSearchInputKeyboardEvents() {
const { key } = event;

switch (key) {
case KeyboardEvents.Tab:
case KeyboardEvents.ArrowRight:
if (!isSkinToneInSearch) {
return;
Expand Down