diff --git a/src/components/context/PickerContext.tsx b/src/components/context/PickerContext.tsx index ce38dcd7..fa4f0c72 100644 --- a/src/components/context/PickerContext.tsx +++ b/src/components/context/PickerContext.tsx @@ -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'; @@ -61,7 +62,7 @@ const PickerContext = React.createContext<{ searchTerm: [string, (term: string) => Promise]; suggestedUpdateState: [number, (term: number) => void]; activeCategoryState: ReactState; - activeSkinTone: ReactState; + activeSkinTone: [SkinTones, (skinTone: SkinTones) => void]; emojisThatFailedToLoadState: ReactState>; isPastInitialLoad: boolean; emojiVariationPickerState: ReactState; @@ -71,18 +72,18 @@ const PickerContext = React.createContext<{ disallowMouseRef: React.MutableRefObject; disallowedEmojisRef: React.MutableRefObject>; }>({ - 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(() => undefined)], - skinToneFanOpenState: [false, () => {}], - suggestedUpdateState: [Date.now(), () => {}] + skinToneFanOpenState: [false, () => { }], + suggestedUpdateState: [Date.now(), () => { }] }); type Props = Readonly<{ diff --git a/src/components/header/Search.tsx b/src/components/header/Search.tsx index 34fb1919..a43d3361 100644 --- a/src/components/header/Search.tsx +++ b/src/components/header/Search.tsx @@ -76,6 +76,7 @@ export function Search() { diff --git a/src/components/header/SkinTonePicker.tsx b/src/components/header/SkinTonePicker.tsx index 17a6d26b..6e433fde 100644 --- a/src/components/header/SkinTonePicker.tsx +++ b/src/components/header/SkinTonePicker.tsx @@ -4,10 +4,12 @@ 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'; @@ -15,7 +17,7 @@ import { Button } from '../atoms/Button'; import { useSkinTonePickerRef } from '../context/ElementRefContext'; import { useActiveSkinToneState, - useSkinToneFanOpenState + useSkinToneFanOpenState, } from '../context/PickerContext'; import './SkinTonePicker.css'; @@ -23,6 +25,7 @@ const ITEM_SIZE = 28; type Props = { direction?: SkinTonePickerDirection; + fanOutDirection?: SkinTonePickerFanOutDirection; }; export function SkinTonePickerMenu() { @@ -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(); @@ -55,15 +59,17 @@ export function SkinTonePicker({ const vertical = direction === SkinTonePickerDirection.VERTICAL; + const buttonStyle = { backgroundColor: "transparent", border: "none" } + return (
@@ -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] + }`} > ); })}
); + + 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', } diff --git a/src/config/config.ts b/src/config/config.ts index 066bcf9a..4876fbe6 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -1,5 +1,6 @@ import { GetEmojiUrl } from '../components/emoji/Emoji'; import { emojiUrlByUnified } from '../dataUtils/emojiSelectors'; +import { getSkinTone } from '../dataUtils/skinTone'; import { EmojiClickData, EmojiStyle, @@ -30,6 +31,8 @@ export function mergeConfig( suggestionMode: config.suggestedEmojisMode }); + const activeSkinTone = getSkinTone() + const skinTonePickerLocation = config.searchDisabled ? SkinTonePickerLocation.PREVIEW : config.skinTonePickerLocation; @@ -37,6 +40,7 @@ export function mergeConfig( return { ...config, categories, + defaultSkinTone: activeSkinTone, previewConfig, skinTonePickerLocation }; diff --git a/src/dataUtils/skinTone.ts b/src/dataUtils/skinTone.ts new file mode 100644 index 00000000..25a469fe --- /dev/null +++ b/src/dataUtils/skinTone.ts @@ -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 + } +} \ No newline at end of file diff --git a/src/hooks/useKeyboardNavigation.ts b/src/hooks/useKeyboardNavigation.ts index e460634a..a1961a49 100644 --- a/src/hooks/useKeyboardNavigation.ts +++ b/src/hooks/useKeyboardNavigation.ts @@ -12,7 +12,7 @@ import { focusNextVisibleEmoji, focusPrevVisibleEmoji, focusVisibleEmojiOneRowDown, - focusVisibleEmojiOneRowUp + focusVisibleEmojiOneRowUp, } from '../DomUtils/keyboardNavigation'; import { useScrollTo } from '../DomUtils/scrollTo'; import { buttonFromTarget } from '../DomUtils/selectors'; @@ -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() { @@ -132,6 +133,7 @@ function useSearchInputKeyboardEvents() { const { key } = event; switch (key) { + case KeyboardEvents.Tab: case KeyboardEvents.ArrowRight: if (!isSkinToneInSearch) { return;