diff --git a/ColorPicker/ColorPicker.js b/ColorPicker/ColorPicker.js index 531f6c250..e618db408 100644 --- a/ColorPicker/ColorPicker.js +++ b/ColorPicker/ColorPicker.js @@ -24,7 +24,7 @@ import {Cell, Column, Row} from '@enact/ui/Layout'; import Toggleable from '@enact/ui/Toggleable'; import PropTypes from 'prop-types'; import compose from 'ramda/src/compose'; -import {useCallback, useEffect, useState} from 'react'; +import {useCallback, useEffect, useReducer} from 'react'; import BodyText from '../BodyText'; import Button, {ButtonBase} from '../Button'; @@ -52,28 +52,38 @@ const PopupContent = (props) => { checkPropTypes(PopupContent, props); const {color, colorHandler, css, presetColors} = props; - const [hue, setHue] = useState(0); - const [saturation, setSaturation] = useState(0); - const [lightness, setLightness] = useState(0); + + const reducer = (reducerState, payload) => { + return {...reducerState, ...payload}; + }; + + const createInitialState = () => { + return { + hue: 0, + saturation: 0, + lightness: 0 + }; + }; + + const [state, dispatch] = useReducer(reducer, null, createInitialState); + const {hue, saturation, lightness} = state; useEffect(() => { - let {h, s, l} = hexToHSL(color); + const {h, s, l} = hexToHSL(color); - setHue(h); - setSaturation(s); - setLightness(l); + dispatch({hue: h, saturation: s, lightness: l}); }, [color]); const changeHue = useCallback((ev) => { - setHue(ev.value); + dispatch({hue: ev.value}); }, []); const changeLightness = useCallback((ev) => { - setLightness(ev.value); + dispatch({lightness: ev.value}); }, []); const changeSaturation = useCallback((ev) => { - setSaturation(ev.value); + dispatch({saturation: ev.value}); }, []); const handleClick = useCallback((ev) => { diff --git a/Popup/Popup.js b/Popup/Popup.js index 80c6e6368..6bd1434c3 100644 --- a/Popup/Popup.js +++ b/Popup/Popup.js @@ -21,7 +21,7 @@ import {getLastContainer} from '@enact/spotlight/src/container'; import FloatingLayer from '@enact/ui/FloatingLayer'; import Transition from '@enact/ui/Transition'; import PropTypes from 'prop-types'; -import {useCallback, useEffect, useLayoutEffect, useRef, useState} from 'react'; +import {useCallback, useEffect, useLayoutEffect, useReducer, useRef, useState} from 'react'; import warning from 'warning'; import Skinnable from '../Skinnable'; @@ -308,55 +308,66 @@ const Popup = (props) => { // Assign the needed props to the rest object for the child component Object.assign(rest, {noAnimation, position, spotlightRestrict}); - const [activator, setActivator] = useState(null); - const [addKeyDownListener, setAddKeyDownListener] = useState(false); - const [floatLayerOpen, setFloatLayerOpen] = useState(open); - const [popupOpen, setPopupOpen] = useState(open ? OpenState.OPEN : OpenState.CLOSED); - const [prevOpen, setPrevOpen] = useState(open); + const reducer = (reducerState, payload) => { + return {...reducerState, ...payload}; + }; + + const createInitialState = () => { + return { + activator: null, + addKeyDownListener: false, + floatLayerOpen: open, + popupOpen: open ? OpenState.OPEN : OpenState.CLOSED, + prevOpen: open + }; + }; - const containerIdRef = useRef(null); - const pausedRef = useRef(new Pause('Popup')); + const [state, dispatch] = useReducer(reducer, null, createInitialState); + const {activator, addKeyDownListener, floatLayerOpen, popupOpen, prevOpen} = state; - if (!containerIdRef.current) { - containerIdRef.current = Spotlight.add(); - } + const [containerId] = useState(() => Spotlight.add()); + const pausedRef = useRef(new Pause('Popup')); const getDerivedStateFromProps = useCallback(() => { if (open !== prevOpen) { if (open) { - setPopupOpen(noAnimation || floatLayerOpen ? OpenState.OPEN : OpenState.CLOSED); - setFloatLayerOpen(true); - setActivator(Spotlight.getCurrent()); - setPrevOpen(open); + dispatch({ + activator: Spotlight.getCurrent(), + floatLayerOpen: true, + popupOpen: noAnimation || floatLayerOpen ? OpenState.OPEN : OpenState.CLOSED, + prevOpen: open + }); } else { // Disables the spotlight container of popup when `noAnimation` set if (noAnimation) { - const node = getContainerNode(containerIdRef.current); + const node = getContainerNode(containerId); if (node) { node.dataset['spotlightContainerDisabled'] = true; } } - setPopupOpen(OpenState.CLOSED); - setFloatLayerOpen(popupOpen === OpenState.OPEN ? !noAnimation : false); - setActivator(noAnimation ? null : activator); - setPrevOpen(open); + dispatch({ + activator: noAnimation ? null : activator, + floatLayerOpen: popupOpen === OpenState.OPEN ? !noAnimation : false, + popupOpen: OpenState.CLOSED, + prevOpen: open + }); } } return null; - }, [activator, floatLayerOpen, noAnimation, open, popupOpen, prevOpen]); + }, [activator, containerId, floatLayerOpen, noAnimation, open, popupOpen, prevOpen]); const handleKeyDown = useCallback((ev) => { if (no5WayClose) return; const keyCode = ev.keyCode; const direction = getDirection(keyCode); - const spottables = Spotlight.getSpottableDescendants(containerIdRef.current).length; + const spottables = Spotlight.getSpottableDescendants(containerId).length; const current = Spotlight.getCurrent(); - if (direction && (!spottables || current && getContainerNode(containerIdRef.current).contains(current))) { + if (direction && (!spottables || current && getContainerNode(containerId).contains(current))) { // explicitly restrict navigation in order to manage focus state when attempting to leave the popup - Spotlight.set(containerIdRef.current, {restrict: 'self-only'}); + Spotlight.set(containerId, {restrict: 'self-only'}); if (onClose) { let focusChanged; @@ -387,7 +398,7 @@ const Popup = (props) => { } } } - }, [componentProps, no5WayClose, onClose, position, spotlightRestrict]); + }, [componentProps, containerId, no5WayClose, onClose, position, spotlightRestrict]); const spotActivator = useCallback(() => { pausedRef.current.resume(); @@ -396,10 +407,10 @@ const Popup = (props) => { if (open) return; const current = Spotlight.getCurrent(); - const containerNode = getContainerNode(containerIdRef.current); + const containerNode = getContainerNode(containerId); const lastContainerId = getLastContainer(); - setAddKeyDownListener(false); + dispatch({addKeyDownListener: false}); // if there is no currently-spotted control, or it is wrapped by the popup's container, we // know it's safe to change focus @@ -417,7 +428,7 @@ const Popup = (props) => { } } } - }, [activator, open]); + }, [activator, containerId, open]); const spotPopupContent = useCallback(() => { pausedRef.current.resume(); @@ -425,9 +436,9 @@ const Popup = (props) => { // only spot the activator if the popup is open if (!open) return; - setAddKeyDownListener(true); + dispatch({addKeyDownListener: true}); - if (!Spotlight.isPaused() && !Spotlight.focus(containerIdRef.current)) { + if (!Spotlight.isPaused() && !Spotlight.focus(containerId)) { const current = Spotlight.getCurrent(); // In cases where the container contains no spottable controls, or we're in pointer-mode, focus @@ -436,13 +447,13 @@ const Popup = (props) => { if (current) { current.blur(); } - Spotlight.setActiveContainer(containerIdRef.current); + Spotlight.setActiveContainer(containerId); } - }, [open]); + }, [containerId, open]); const handleFloatingLayerOpen = useCallback(() => { if (!noAnimation && popupOpen !== OpenState.OPEN) { - setPopupOpen(OpenState.OPENING); + dispatch({popupOpen: OpenState.OPENING}); } else if (popupOpen === OpenState.OPEN && open) { spotPopupContent(); } @@ -455,25 +466,22 @@ const Popup = (props) => { const handlePopupHide = useCallback((ev) => { forwardHide(ev, componentProps); - setFloatLayerOpen(() => { - if (!ev.currentTarget || ev.currentTarget.getAttribute('data-spotlight-id') === containerIdRef.current) { - spotActivator(); - } - setActivator(null); + if (!ev.currentTarget || ev.currentTarget.getAttribute('data-spotlight-id') === containerId) { + spotActivator(); + } - return false; - }); - }, [componentProps, spotActivator]); + dispatch({activator: null, floatLayerOpen: false}); + }, [componentProps, containerId, spotActivator]); const handlePopupShow = useCallback((ev) => { forwardShow(ev, componentProps); - setPopupOpen(OpenState.OPEN); + dispatch({popupOpen: OpenState.OPEN}); - if (!ev.currentTarget || ev.currentTarget.getAttribute('data-spotlight-id') === containerIdRef.current) { + if (!ev.currentTarget || ev.currentTarget.getAttribute('data-spotlight-id') === containerId) { spotPopupContent(); } - }, [componentProps, spotPopupContent]); + }, [componentProps, containerId, spotPopupContent]); useLayoutEffect(() => { getDerivedStateFromProps(); @@ -511,7 +519,7 @@ const Popup = (props) => { // If the popup is open on mount, we need to pause spotlight so nothing steals focus // while the popup is rendering. pausedRef.current.pause(); - if (getContainerNode(containerIdRef.current)) { + if (getContainerNode(containerId)) { spotPopupContent(); } @@ -521,7 +529,7 @@ const Popup = (props) => { if (open) { off('keydown', handleKeyDown); } - Spotlight.remove(containerIdRef.current); + Spotlight.remove(containerId); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -552,7 +560,7 @@ const Popup = (props) => { onHide={handlePopupHide} onShow={handlePopupShow} open={popupOpen >= OpenState.OPENING} - spotlightId={containerIdRef.current} + spotlightId={containerId} /> ); diff --git a/Scroller/EditableWrapper.js b/Scroller/EditableWrapper.js index 8747c76bf..4558f5aff 100644 --- a/Scroller/EditableWrapper.js +++ b/Scroller/EditableWrapper.js @@ -344,7 +344,7 @@ const EditableWrapper = (props) => { // Add rearranged items const addRearrangedItems = useCallback(({currentMoveDirection, toIndex}) => { // Set the currentMoveDirection to css variable - const {rtl} = scrollContainerHandle.current; + const {current: {rtl}} = scrollContainerHandle; wrapperRef.current.style.setProperty('--move-direction', currentMoveDirection * (rtl ? -1 : 1)); const {fromIndex, rearrangedItems, selectedItem} = mutableRef.current; @@ -392,7 +392,7 @@ const EditableWrapper = (props) => { // Move items const moveItems = useCallback((toIndex) => { const {selectedItem} = mutableRef.current; - const {rtl} = scrollContainerHandle.current; + const {current: {rtl}} = scrollContainerHandle; if (selectedItem && !selectedItem.hasAttribute('data-is-hiding')) { // Bail out when index is out of scope @@ -433,7 +433,7 @@ const EditableWrapper = (props) => { const {keyCode} = ev; const container = scrollContentRef.current; const {itemWidth, prevToIndex} = mutableRef.current; - const {rtl} = scrollContainerHandle.current; + const {current: {rtl}} = scrollContainerHandle; const toIndex = (!rtl ^ !is('left', keyCode)) ? prevToIndex - 1 : prevToIndex + 1; const scrollLeft = container.scrollLeft * (rtl ? -1 : 1); @@ -538,7 +538,7 @@ const EditableWrapper = (props) => { const getNextIndexFromPosition = useCallback((x, tolerance) => { const {centeredOffset, itemWidth, prevToIndex} = mutableRef.current; - const {rtl} = scrollContainerHandle.current; + const {current: {rtl}} = scrollContainerHandle; const bodyWidth = document.body.getBoundingClientRect().width; // Determine toIndex with mouse client x position @@ -575,7 +575,7 @@ const EditableWrapper = (props) => { const handleMouseLeave = useCallback(() => { const {focusedItem, itemWidth, lastInputType, lastMouseClientX, selectedItem} = mutableRef.current; - const {rtl} = scrollContainerHandle.current; + const {current: {rtl}} = scrollContainerHandle; const scrollContentNode = scrollContentRef.current; const scrollContentCenter = scrollContentNode.getBoundingClientRect().width / 2; @@ -788,7 +788,7 @@ const EditableWrapper = (props) => { const handleTouchEnd = useCallback((ev) => { const {itemWidth, lastInputType, lastMouseClientX, selectedItem} = mutableRef.current; - const {rtl} = scrollContainerHandle.current; + const {current: {rtl}} = scrollContainerHandle; const scrollContentNode = scrollContentRef.current; const scrollContentCenter = scrollContentNode.getBoundingClientRect().width / 2; diff --git a/tests/screenshot/apps/Limestone-View.js b/tests/screenshot/apps/Limestone-View.js index 21985ac3c..021a88abc 100644 --- a/tests/screenshot/apps/Limestone-View.js +++ b/tests/screenshot/apps/Limestone-View.js @@ -135,6 +135,10 @@ class App extends ReactComponent { } } +// Declared at module scope so, the components are not created during render (react-hooks/static-components). +const WrappedAppAutoFocus = ThemeDecorator({noAutoFocus: false}, App); +const WrappedAppNoAutoFocus = ThemeDecorator({noAutoFocus: true}, App); + const ExportedApp = (props) => { // Common test parameters let skin = url.searchParams.get('skin'); @@ -178,14 +182,14 @@ const ExportedApp = (props) => { } } - const WrappedApp = ThemeDecorator({noAutoFocus}, App); + const WrappedApp = noAutoFocus ? WrappedAppNoAutoFocus : WrappedAppAutoFocus; useEffect(() => { document.querySelector('#root').classList.add('spotlight-input-key'); }, []); return ( - + ); };