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;