diff --git a/Input/InputFieldSpotlightDecorator.js b/Input/InputFieldSpotlightDecorator.js index 9160e15f18..ba42fe3ea4 100644 --- a/Input/InputFieldSpotlightDecorator.js +++ b/Input/InputFieldSpotlightDecorator.js @@ -1,11 +1,11 @@ -import {call, forward, forwardCustom, forwardWithPrevent, handle, stopImmediate} from '@enact/core/handle'; +import {forward, forwardCustom, forwardWithPrevent, stopImmediate} from '@enact/core/handle'; import hoc from '@enact/core/hoc'; import {is} from '@enact/core/keymap'; import {getDirection, Spotlight} from '@enact/spotlight'; import Pause from '@enact/spotlight/Pause'; import Spottable from '@enact/spotlight/Spottable'; import PropTypes from 'prop-types'; -import {Component as ReactComponent} from 'react'; +import {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {lockPointer, releasePointer} from './pointer'; @@ -22,11 +22,6 @@ const isSelectionAtLocation = (target, location) => { } }; -const handleKeyDown = handle( - forwardWithPrevent('onKeyDown'), - call('onKeyDown') -); - /** * Default config for {@link sandstone/Input.InputSpotlightDecorator|InputSpotlightDecorator} * @@ -59,225 +54,165 @@ const InputSpotlightDecorator = hoc(defaultConfig, (config, Wrapped) => { const forwardBlur = forward('onBlur'); const forwardMouseDown = forward('onMouseDown'); const forwardFocus = forward('onFocus'); + const forwardKeyDown = forwardWithPrevent('onKeyDown'); const forwardKeyUp = forward('onKeyUp'); - return class extends ReactComponent { - static displayName = 'InputSpotlightDecorator'; - - static propTypes = /** @lends sandstone/Input/InputSpotlightDecorator.InputSpotlightDecorator.prototype */ { - /** - * Focuses the when the decorator is focused via 5-way. - * - * @type {Boolean} - * @default false - * @public - */ - autoFocus: PropTypes.bool, - - /** - * Applies a disabled style and the control becomes non-interactive. - * - * @type {Boolean} - * @default false - * @public - */ - disabled: PropTypes.bool, - - /** - * Blurs the input when the "enter" key is pressed. - * - * @type {Boolean} - * @default false - * @public - */ - dismissOnEnter: PropTypes.bool, - - /** - * Called when the internal is focused. - * - * @type {Function} - * @param {Object} event - * @public - */ - onActivate: PropTypes.func, - - /** - * Called when the internal loses focus. - * - * @type {Function} - * @param {Object} event - * @public - */ - onDeactivate: PropTypes.func, - - /** - * Called when the component is removed while retaining focus. - * - * @type {Function} - * @param {Object} event - * @public - */ - onSpotlightDisappear: PropTypes.func, - - /** - * Disables spotlight navigation into the component. - * - * @type {Boolean} - * @default false - * @public - */ - spotlightDisabled: PropTypes.bool - }; - - constructor (props) { - super(props); - - this.focused = null; - this.node = null; - this.fromMouse = false; - this.paused = new Pause('InputSpotlightDecorator'); - this.handleKeyDown = handleKeyDown.bind(this); - this.prevStatus = { - focused: null, - node: null - }; - } - - componentWillUnmount () { - this.paused.resume(); - - if (this.focused === 'input') { - const {onSpotlightDisappear} = this.props; - - if (onSpotlightDisappear) { - onSpotlightDisappear(); - } + const InputSpotlight = (props) => { + const {onSpotlightDisappear} = props; + const paused = useMemo(() => new Pause('InputSpotlightDecorator'), []); + const [downTarget, setDownTarget] = useState(); + const [fromMouse, setFromMouse] = useState(false); + const focused = useRef(null); + const node = useRef(null); + const prevStatus = useRef({ + focused: null, + node: null + }); + + useEffect(() => { + return () => { + paused.resume(); + + if (focused.current === 'input') { + if (onSpotlightDisappear) { + onSpotlightDisappear(); + } - if (!noLockPointer) { - releasePointer(this.node); + if (!noLockPointer) { + releasePointer(node.current); + } } - } - } + }; + }, []); // eslint-disable-line react-hooks/exhaustive-deps - updateFocus = () => { + const updateFocus = useCallback(() => { // focus node if `InputSpotlightDecorator` is pausing Spotlight or if Spotlight is paused if ( - this.node && - Spotlight.getCurrent() !== this.node && - (this.paused.isPaused() || !Spotlight.isPaused()) + node.current && + Spotlight.getCurrent() !== node.current && + (paused.isPaused() || !Spotlight.isPaused()) ) { - if (this.fromMouse) { - this.node.focus({preventScroll: true}); + if (fromMouse) { + node.current.focus({preventScroll: true}); } else { - this.node.focus(); + node.current.focus(); } } - const focusChanged = this.focused !== this.prevStatus.focused; + const focusChanged = focused.current !== prevStatus.current.focused; if (focusChanged) { - if (this.focused === 'input') { - forwardCustom('onActivate')(null, this.props); + if (focused.current === 'input') { + forwardCustom('onActivate')(null, props); if (!noLockPointer) { - lockPointer(this.node); + lockPointer(node.current); } - this.paused.pause(); - } else if (this.prevStatus.focused === 'input') { - forwardCustom('onDeactivate')(null, this.props); + paused.pause(); + } else if (prevStatus.current.focused === 'input') { + forwardCustom('onDeactivate')(null, props); if (!noLockPointer) { - releasePointer(this.prevStatus.node); + releasePointer(prevStatus.current.node); } - this.paused.resume(); + paused.resume(); } } - this.prevStatus.focused = this.focused; - this.prevStatus.node = this.node; - }; - - focus = (focused, node, fromMouse) => { - this.focused = focused; - this.node = node; - this.fromMouse = fromMouse; - this.updateFocus(); - }; - - blur = () => { - if (this.focused || this.node) { - this.focused = null; - this.node = null; - this.updateFocus(); + prevStatus.current = {focused: focused.current, node: node.current}; + }, [fromMouse, paused, props]); + + const focus = useCallback((focusedValue, nodeValue, fromMouseValue) => { + focused.current = focusedValue; + setFromMouse(fromMouseValue); + node.current = nodeValue; + updateFocus(); + }, [updateFocus]); + + const blur = useCallback(() => { + if (focused.current || node.current) { + focused.current = null; + node.current = null; + updateFocus(); } - }; + }, [updateFocus]); - focusDecorator = (decorator) => { - this.focus('decorator', decorator, false); - }; + const focusDecorator = useCallback((decorator) => { + focus('decorator', decorator, false); + }, [focus]); - focusInput = (decorator, fromMouse) => { - this.focus('input', decorator.querySelector('input'), fromMouse); - }; + const focusInput = useCallback((decorator, fromMouseValue) => { + focus('input', decorator.querySelector('input'), fromMouseValue); + }, [focus]); - onBlur = (ev) => { - if (!this.props.autoFocus) { + const onBlur = useCallback((ev) => { + if (!props.autoFocus) { if (isBubbling(ev)) { if (Spotlight.getPointerMode()) { - this.blur(); - forwardBlur(ev, this.props); + blur(); + forwardBlur(ev, props); } else { - this.focused = 'decorator'; - this.node = ev.currentTarget; - this.fromMouse = false; + focused.current = 'decorator'; + setFromMouse(false); + node.current = ev.currentTarget; ev.stopPropagation(); } } else if (!ev.currentTarget.contains(ev.relatedTarget)) { // Blurring decorator but not focusing input - forwardBlur(ev, this.props); - this.blur(); + forwardBlur(ev, props); + blur(); } } else if (isBubbling(ev)) { - if (this.focused === 'input' && this.node === ev.target && ev.currentTarget !== ev.relatedTarget) { - this.blur(); - forwardBlur(ev, this.props); + if (focused.current === 'input' && node.current === ev.target && ev.currentTarget !== ev.relatedTarget) { + blur(); + forwardBlur(ev, props); } else { - this.focusDecorator(ev.currentTarget); + focusDecorator(ev.currentTarget); ev.stopPropagation(); - this.blur(); + blur(); } } + }, [blur, focusDecorator, props]); + + const updateDownTarget = (ev) => { + const {repeat, target} = ev; + + if (!repeat) { + setDownTarget(target); + } }; - onMouseDown = (ev) => { - const {disabled, spotlightDisabled} = this.props; + const onMouseDown = useCallback((ev) => { + const {disabled, spotlightDisabled} = props; - this.setDownTarget(ev); + updateDownTarget(ev); // focus the whenever clicking on any part of the component to ensure both that // the has focus and Spotlight is paused. if (!disabled && !spotlightDisabled) { - this.focusInput(ev.currentTarget, true); + focusInput(ev.currentTarget, true); } - forwardMouseDown(ev, this.props); - }; + forwardMouseDown(ev, props); + }, [focusInput, props]); - onFocus = (ev) => { - forwardFocus(ev, this.props); + const onFocus = useCallback((ev) => { + forwardFocus(ev, props); // when in autoFocus mode, focusing the decorator directly will cause it to // forward the focus onto the - if (!isBubbling(ev) && (this.props.autoFocus && this.focused === null && !Spotlight.getPointerMode())) { - this.focusInput(ev.currentTarget, false); + if (!isBubbling(ev) && (props.autoFocus && focused.current === null && !Spotlight.getPointerMode())) { + focusInput(ev.currentTarget, false); ev.stopPropagation(); } - }; + }, [focusInput, props]); + + const onKeyDown = useCallback((ev) => { + forwardKeyDown(ev, props); - onKeyDown (ev) { const {currentTarget, keyCode, target} = ev; // cache the target if this is the first keyDown event to ensure the component had focus // when the key interaction started - this.setDownTarget(ev); + updateDownTarget(ev); - if (this.focused === 'input') { + if (focused.current === 'input') { const isDown = is('down', keyCode); const isLeft = is('left', keyCode); const isRight = is('right', keyCode); @@ -311,72 +246,129 @@ const InputSpotlightDecorator = hoc(defaultConfig, (config, Wrapped) => { } stopImmediate(ev); - this.paused.resume(); + paused.resume(); // Move spotlight in the keypress direction if (move(direction)) { // if successful, reset the internal state - this.blur(); + blur(); } else { // if there is no other spottable elements, focus `InputDecorator` instead - this.focusDecorator(currentTarget); + focusDecorator(currentTarget); } } else if (isLeft || isRight) { // prevent 5-way nav for left/right keys within the stopImmediate(ev); } } - } + }, [blur, focusDecorator, paused, props]); - onKeyUp = (ev) => { - const {dismissOnEnter} = this.props; + const onKeyUp = useCallback((ev) => { + const {dismissOnEnter} = props; const {currentTarget, keyCode, target} = ev; // verify that we have a matching pair of key down/up events to avoid adjusting focus // when the component received focus mid-press - if (target === this.downTarget) { - this.downTarget = null; + if (target === downTarget) { + setDownTarget(null); - if (!this.props.disabled) { - if (this.focused === 'input' && dismissOnEnter && is('enter', keyCode)) { - this.focusDecorator(currentTarget); + if (!props.disabled) { + if (focused.current === 'input' && dismissOnEnter && is('enter', keyCode)) { + focusDecorator(currentTarget); // prevent Enter onKeyPress which triggers an onMouseDown via Spotlight ev.preventDefault(); - } else if (this.focused !== 'input' && is('enter', keyCode)) { - this.focusInput(currentTarget, false); + } else if (focused.current !== 'input' && is('enter', keyCode)) { + focusInput(currentTarget, false); } } } - forwardKeyUp(ev, this.props); - }; - - setDownTarget (ev) { - const {repeat, target} = ev; - - if (!repeat) { - this.downTarget = target; - } - } - - render () { - const props = Object.assign({}, this.props); - delete props.autoFocus; - delete props.onActivate; - delete props.onDeactivate; - - return ( - - ); - } + forwardKeyUp(ev, props); + }, [downTarget, focusDecorator, focusInput, props]); + + const componentProps = Object.assign({}, props); + delete componentProps.autoFocus; + delete componentProps.onActivate; + delete componentProps.onDeactivate; + + return ( + + ); }; + InputSpotlight.displayName = 'InputSpotlightDecorator'; + InputSpotlight.propTypes = /** @lends sandstone/Input/InputSpotlightDecorator.InputSpotlightDecorator.prototype */ { + /** + * Focuses the when the decorator is focused via 5-way. + * + * @type {Boolean} + * @default false + * @public + */ + autoFocus: PropTypes.bool, + + /** + * Applies a disabled style and the control becomes non-interactive. + * + * @type {Boolean} + * @default false + * @public + */ + disabled: PropTypes.bool, + + /** + * Blurs the input when the "enter" key is pressed. + * + * @type {Boolean} + * @default false + * @public + */ + dismissOnEnter: PropTypes.bool, + + /** + * Called when the internal is focused. + * + * @type {Function} + * @param {Object} event + * @public + */ + onActivate: PropTypes.func, + + /** + * Called when the internal loses focus. + * + * @type {Function} + * @param {Object} event + * @public + */ + onDeactivate: PropTypes.func, + + /** + * Called when the component is removed while retaining focus. + * + * @type {Function} + * @param {Object} event + * @public + */ + onSpotlightDisappear: PropTypes.func, + + /** + * Disables spotlight navigation into the component. + * + * @type {Boolean} + * @default false + * @public + */ + spotlightDisabled: PropTypes.bool + }; + + return InputSpotlight; }); export default InputSpotlightDecorator;