diff --git a/PopupTabLayout/PopupTabLayout.js b/PopupTabLayout/PopupTabLayout.js index 3b19d5bc6..fda47b796 100644 --- a/PopupTabLayout/PopupTabLayout.js +++ b/PopupTabLayout/PopupTabLayout.js @@ -22,7 +22,7 @@ import {getContainersForNode, getContainerNode} from '@enact/spotlight/src/conta import {getTargetByDirectionFromElement} from '@enact/spotlight/src/target'; import {IdProvider} from '@enact/ui/internal/IdProvider'; import PropTypes from 'prop-types'; -import {Component, use, useEffect} from 'react'; +import {use, useEffect, useLayoutEffect, useState} from 'react'; import compose from 'ramda/src/compose'; import Skinnable from '../Skinnable'; @@ -324,68 +324,67 @@ const PopupTabLayoutBase = kind({ }); const OptimizedFocusDecorator = hoc((config, Wrapped) => { - return class extends Component { - static displayName = 'OptimizedFocusDecorator'; - - static propTypes = /** @lends limestone/PopupTabLayout.OptimizedFocusDecorator.prototype */ { - /** - * Controls the visibility of the PopupTabLayout. - * - * @type {Boolean} - * @private - */ - open: PropTypes.bool, - - /** - * Optimizes PopupTabLayout without Popup when true. - * - * @type {Boolean} - * @private - */ - optimized: PropTypes.bool, - - /** - * The container id. - * - * @type {String} - * @private - */ - spotlightId: PropTypes.string - }; - - constructor (props) { - super(props); - if (props.optimized) { - this.paused = new Pause('PopupTabLayout'); - if (props.open) { - this.paused.pause(); - } - } - } + // eslint-disable-next-line no-shadow + const OptimizedFocusDecorator = (props) => { + checkPropTypes(OptimizedFocusDecorator, props); - componentDidMount () { - if (this.props.optimized && this.props.open) { - this.paused.resume(); - Spotlight.focus(this.props.spotlightId); - } - } + const {open, optimized, spotlightId} = props; + + // Pauses Spotlight before the children are mounted so the focus can be set to the layout on mount + const [paused] = useState(() => { + const pause = new Pause('PopupTabLayout'); - componentDidUpdate (prevProps) { - if (this.props.optimized && this.props.open !== prevProps.open) { - if (this.props.open) { - Spotlight.focus(this.props.spotlightId); - } + if (optimized && open) { + pause.pause(); } - } - render () { - if (this.props.optimized) { - return ; - } else { - return ; + return pause; + }); + + useLayoutEffect(() => { + if (optimized && open) { + // Only resumes when paused on mount + paused.resume(); + Spotlight.focus(spotlightId); } + }, [open]); // eslint-disable-line react-hooks/exhaustive-deps + + if (optimized) { + return ; + } else { + return ; } }; + + OptimizedFocusDecorator.displayName = 'OptimizedFocusDecorator'; + + OptimizedFocusDecorator.propTypes = /** @lends limestone/PopupTabLayout.OptimizedFocusDecorator.prototype */ { + /** + * Controls the visibility of the PopupTabLayout. + * + * @type {Boolean} + * @private + */ + open: PropTypes.bool, + + /** + * Optimizes PopupTabLayout without Popup when true. + * + * @type {Boolean} + * @private + */ + optimized: PropTypes.bool, + + /** + * The container id. + * + * @type {String} + * @private + */ + spotlightId: PropTypes.string + }; + + return OptimizedFocusDecorator; }); diff --git a/internal/DebounceDecorator/DebounceDecorator.js b/internal/DebounceDecorator/DebounceDecorator.js index 0e5462dbf..1a55b5505 100644 --- a/internal/DebounceDecorator/DebounceDecorator.js +++ b/internal/DebounceDecorator/DebounceDecorator.js @@ -6,9 +6,9 @@ */ import hoc from '@enact/core/hoc'; -import {Job} from '@enact/core/util'; +import {checkPropTypes, Job} from '@enact/core/util'; import PropTypes from 'prop-types'; -import {Component} from 'react'; +import {useCallback, useEffect, useMemo, useRef} from 'react'; /** * Default config for {@link limestone/internal/DebounceDecorator.DebounceDecorator}. @@ -60,66 +60,72 @@ const defaultConfig = { const DebounceDecorator = hoc(defaultConfig, (config, Wrapped) => { const {cancel, debounce, delay} = config; - return class extends Component { - static displayName = 'DebounceDecorator'; - - static propTypes = /** @lends limestone/internal/DebounceDecorator.DebounceDecorator.prototype */ { - /** - * Handler for `onChange` events - * - * `'onChange'` can be changed to a different prop name by specifying the `debounce` - * config option. - * - * @see {@link limestone/internal/DebounceDecorator.DebounceDecorator.defaultConfig#debounce} - * @name onChange - * @memberof limestone/internal/DebounceDecorator.DebounceDecorator.prototype - * @type {Function} - * @public - */ - [debounce]: PropTypes.func - }; - - constructor (props) { - super(props); - this.job = new Job(this.emitEvent.bind(this), delay); - } - - componentWillUnmount () { - this.job.stop(); - } - - emitEvent (ev) { - if (this.props[debounce]) { - this.props[debounce](ev); - } - } + // eslint-disable-next-line no-shadow + const DebounceDecorator = (props) => { + checkPropTypes(DebounceDecorator, props); + + // The debounced event is emitted after a delay so it must use the latest props + const propsRef = useRef(props); - handleEvent = (ev) => { - this.job.start(ev); - }; + const job = useMemo(() => new Job((ev) => { + const {[debounce]: onDebounce} = propsRef.current; - handleCancel = (ev) => { - if (this.props[cancel]) { - this.props[cancel](ev); + if (onDebounce) { + onDebounce(ev); } - this.job.stop(); - }; + }, delay), []); + + useEffect(() => { + propsRef.current = props; + }); + + useEffect(() => { + return () => { + job.stop(); + }; + }, [job]); - render () { - let props = this.props; + const handleEvent = useCallback((ev) => { + job.start(ev); + }, [job]); - if (debounce || cancel) { - props = {...props}; + const handleCancel = useCallback((ev) => { + const {[cancel]: onCancel} = propsRef.current; - if (debounce) props[debounce] = this.handleEvent; - if (cancel) props[cancel] = this.handleCancel; + if (onCancel) { + onCancel(ev); } + job.stop(); + }, [job]); - return ( - - ); - } + const handlers = {}; + if (debounce) handlers[debounce] = handleEvent; + if (cancel) handlers[cancel] = handleCancel; + + return ( + + ); + }; + + DebounceDecorator.displayName = 'DebounceDecorator'; + + DebounceDecorator.propTypes = /** @lends limestone/internal/DebounceDecorator.DebounceDecorator.prototype */ { + /** + * Handler for `onChange` events + * + * `'onChange'` can be changed to a different prop name by specifying the `debounce` + * config option. + * + * @see {@link limestone/internal/DebounceDecorator.DebounceDecorator.defaultConfig#debounce} + * @name onChange + * @memberof limestone/internal/DebounceDecorator.DebounceDecorator.prototype + * @type {Function} + * @public + */ + [debounce]: PropTypes.func }; + + return DebounceDecorator; }); export default DebounceDecorator;