/* eslint-disable max-lines, max-statements */ import { FloatingPortal, type ReferenceType } from '@floating-ui/react'; import { EMPTY_CELL } from '@scrabble-solver/constants'; import classNames from 'classnames'; import { type CSSProperties, type FocusEventHandler, type FunctionComponent, useCallback, 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 { TRANSITION } from '@/parameters'; import { boardSlice, cellFiltersSlice, selectCellFilters, selectInputMode, selectLocale, selectShowCoordinates, solveSlice, useTypedSelector, } from '@/state'; import styles from './Board.module.scss'; import { BoardPure } from './BoardPure'; import { Actions, InputPrompt } from './components'; import { useBoardStyle, useFloatingActions, useFloatingFocus, useFloatingInputPrompt, useGrid } from './hooks'; import { selectRowsWithCandidate } from './selectors'; interface Props { className?: string; } export const Board: FunctionComponent = ({ className }) => { const dispatch = useDispatch(); const locale = useTypedSelector(selectLocale); const rows = useTypedSelector(selectRowsWithCandidate); const inputMode = useTypedSelector(selectInputMode); const cellFilters = useTypedSelector(selectCellFilters); const showCoordinates = useTypedSelector(selectShowCoordinates); const { cellSize, coordinatesFontSize, coordinatesSize } = useAppLayout(); const [ { activeIndex, direction, inputRefs }, { insertValue, onChange, onDirectionToggle, onFocus, onKeyDown, onPaste }, ] = useGrid(rows); const boardStyle = useBoardStyle(); const [hasFocus, setHasFocus] = useState(false); const [showInputPrompt, setShowInputPrompt] = useState(false); const [transition, setTransition] = useState(TRANSITION); const inputRef = inputRefs[activeIndex.y][activeIndex.x]; const cell = rows[activeIndex.y][activeIndex.x]; const floatingActions = useFloatingActions(); const floatingInputPrompt = useFloatingInputPrompt(); const floatingFocus = useFloatingFocus(); const handleBlur: FocusEventHandler = useCallback( (event) => { const comesFromActions = floatingActions.refs.floating.current?.contains(event.relatedTarget); const comesFromBoard = event.currentTarget.contains(event.relatedTarget); const comesFromFocus = floatingFocus.refs.floating.current?.contains(event.relatedTarget); const comesFromInputPrompt = floatingInputPrompt.refs.floating.current?.contains(event.relatedTarget); const isLocalEvent = comesFromActions || comesFromBoard || comesFromFocus || comesFromInputPrompt; if (!isLocalEvent) { setHasFocus(false); } }, [floatingActions.refs.floating, floatingFocus.refs.floating, floatingInputPrompt.refs.floating], ); const updateFloatingReference = useCallback( (newReference: ReferenceType | null) => { floatingActions.refs.setReference(newReference); floatingFocus.refs.setReference(newReference); floatingInputPrompt.refs.setReference(newReference); }, [floatingActions.refs, floatingFocus.refs, floatingInputPrompt.refs], ); const handleFocus: typeof onFocus = useCallback( (newX, newY) => { const isFirstFocus = !hasFocus; const originalTransition = floatingActions.refs.floating.current?.style.transition || ''; const newInputRef = inputRefs[newY][newX].current; const newTileElement = newInputRef?.parentElement || null; updateFloatingReference(newTileElement); onFocus(newX, newY); setHasFocus(true); setShowInputPrompt(false); if (isFirstFocus) { setTransition('none'); globalThis.setTimeout(() => { setTransition(originalTransition); }, 0); } }, [floatingActions.refs.floating, hasFocus, inputRefs, onFocus, updateFloatingReference], ); const handleEnterWord = useCallback(() => { setShowInputPrompt(true); }, []); const handleInsertWord = useCallback( (word: string) => { if (word.trim().length === 0) { dispatch(boardSlice.actions.changeCellValue({ ...activeIndex, value: EMPTY_CELL })); } else { insertValue(activeIndex, word.toLocaleLowerCase(locale)); } setShowInputPrompt(false); dispatch(solveSlice.actions.submit()); setHasFocus(false); }, [activeIndex, dispatch, insertValue, locale], ); const handleToggleBlank = useCallback(() => { if (inputMode === 'keyboard') { inputRef.current?.focus(); } dispatch(boardSlice.actions.toggleCellIsBlank(cell)); }, [cell, dispatch, inputMode, inputRef]); const handleToggleDirection = useCallback(() => { if (inputMode === 'keyboard') { inputRef.current?.focus(); } onDirectionToggle(); }, [inputMode, inputRef, onDirectionToggle]); const handleToggleFilterCell = useCallback(() => { if (inputMode === 'keyboard') { inputRef.current?.focus(); } dispatch(cellFiltersSlice.actions.toggle(cell)); }, [cell, dispatch, inputMode, inputRef]); const ref = useOnclickOutside(() => setHasFocus(false), { ignoreClass: [styles.floating], }); return ( <>
{hasFocus && !showInputPrompt && ( )} {hasFocus && showInputPrompt && ( )} ); };