/* eslint-disable max-lines, max-statements */ import { FloatingPortal, autoUpdate, useFloating } from '@floating-ui/react'; import classNames from 'classnames'; import { type ChangeEvent, type ClipboardEvent, type FunctionComponent, type RefObject, createRef, useCallback, useMemo, useRef, useState, } from 'react'; import useOnclickOutside from 'react-cool-onclickoutside'; import { useDispatch } from 'react-redux'; import { useAppLayout } from '@/app-layout'; import { LOCALE_FEATURES } from '@/i18n'; import { createKeyboardNavigation, extractCharacters, extractInputValue, getTileSizes, isCtrl } from '@/lib'; import { rackSlice, selectConfig, selectInputMode, selectLocale, selectRack, useTypedSelector } from '@/state'; import { InputPrompt, RackTile } from './components'; import styles from './Rack.module.scss'; import { selectRemainingTilesGroups } from './selectors'; interface Props { className?: string; tileSize: number; } export const Rack: FunctionComponent = ({ className, tileSize }) => { const dispatch = useDispatch(); const { rackHeight } = useAppLayout(); const config = useTypedSelector(selectConfig); const locale = useTypedSelector(selectLocale); const rack = useTypedSelector(selectRack); const inputMode = useTypedSelector(selectInputMode); const tiles = useTypedSelector(selectRemainingTilesGroups); const tilesCount = tiles.length; const tilesRefs = useMemo( () => Array.from({ length: tilesCount }).map(() => createRef()), [tilesCount], ); const activeIndexRef = useRef(undefined); const [hasFocus, setHasFocus] = useState(false); const [input, setInput] = useState(''); const { direction } = LOCALE_FEATURES[locale]; const { tileFontSize } = getTileSizes(tileSize); const showInputPrompt = inputMode === 'touchscreen' && hasFocus; const ref = useRef(null); const floatingInputPrompt = useFloating({ placement: 'bottom-start', whileElementsMounted: autoUpdate, }); useOnclickOutside(() => setHasFocus(false), { ignoreClass: [InputPrompt.styles.form, InputPrompt.styles.input], refs: ref.current ? [ref as RefObject] : [], }); const changeActiveIndex = useCallback( (offset: number) => { const nextActiveIndex = Math.min(Math.max((activeIndexRef.current || 0) + offset, 0), tilesCount - 1); const tileRef = tilesRefs[nextActiveIndex].current; if (tileRef) { tileRef.focus(); } activeIndexRef.current = nextActiveIndex; }, [activeIndexRef, tilesCount, tilesRefs], ); const handleChange = useCallback( (event: ChangeEvent) => { const value = extractInputValue(event.target); const characters = value ? extractCharacters(config, value) : []; changeActiveIndex(value ? characters.length : -1); }, [changeActiveIndex, config], ); const handlePaste = useCallback( (event: ClipboardEvent) => { const index = activeIndexRef.current; if (typeof index === 'undefined') { return; } event.preventDefault(); const value = event.clipboardData.getData('text/plain').toLocaleLowerCase(); const characters = value ? extractCharacters(config, value) : []; changeActiveIndex(value ? characters.length : -1); dispatch(rackSlice.actions.changeCharacters({ characters, index })); }, [changeActiveIndex, config, dispatch], ); const handleFocus = useCallback(() => { setHasFocus(true); floatingInputPrompt.refs.setPositionReference(ref.current); const characters = rack.filter((character) => character !== null); const uppercasedDigraphs = characters.map((character) => { return character.length > 1 ? character.toLocaleUpperCase(locale) : character; }); setInput(uppercasedDigraphs.join('')); }, [floatingInputPrompt.refs, locale, rack, ref]); const handleKeyDown = useMemo(() => { return createKeyboardNavigation({ onArrowLeft: (event) => { event.preventDefault(); changeActiveIndex(direction === 'ltr' ? -1 : 1); }, onArrowRight: (event) => { event.preventDefault(); changeActiveIndex(direction === 'ltr' ? 1 : -1); }, onBackspace: (event) => { event.preventDefault(); changeActiveIndex(-1); }, onDelete: (event) => { const index = activeIndexRef.current; if (typeof index === 'undefined') { return; } event.preventDefault(); dispatch(rackSlice.actions.changeCharacters({ characters: [null], index })); changeActiveIndex(1); }, onKeyDown: (event) => { if (isCtrl(event) && config.isTwoCharacterTilePrefix(event.key.toLocaleLowerCase())) { changeActiveIndex(1); } else if (event.currentTarget.value.toLocaleLowerCase() === event.key.toLocaleLowerCase()) { // change event did not fire because the same character was typed over the current one // but we still want to move the caret event.preventDefault(); event.stopPropagation(); changeActiveIndex(1); } }, }); }, [changeActiveIndex, config, direction, dispatch]); return ( <>
{tiles.map(({ character, tile }, index) => ( ))}
{showInputPrompt && ( setHasFocus(false)} onChange={(event) => setInput(event.target.value)} onSubmit={() => setHasFocus(false)} /> )} ); };