diff --git a/packages/ui/Marquee/MarqueeController.js b/packages/ui/Marquee/MarqueeController.js
index 21c749fb29..dfb39d116c 100644
--- a/packages/ui/Marquee/MarqueeController.js
+++ b/packages/ui/Marquee/MarqueeController.js
@@ -1,15 +1,5 @@
-import {forward} from '@enact/core/handle';
import hoc from '@enact/core/hoc';
-import {Job} from '@enact/core/util';
-import {createContext, Component} from 'react';
-
-const STATE = {
- inactive: 0, // Marquee is not necessary (render or focus not happened)
- active: 1, // Marquee in progress, awaiting complete
- ready: 2 // Marquee completed or not needed, but state is active
-};
-
-const MarqueeControllerContext = createContext(null);
+import {MarqueeControllerContext, useMarqueeController} from './useMarqueeController';
/**
* Default configuration parameters for {@link ui/Marquee.MarqueeController}.
@@ -40,268 +30,23 @@ const defaultConfig = {
*/
const MarqueeController = hoc(defaultConfig, (config, Wrapped) => {
const {marqueeOnFocus} = config;
- const forwardBlur = forward('onBlur');
- const forwardFocus = forward('onFocus');
- return class extends Component {
- static displayName = 'MarqueeController';
+ return function (props) {
+ const {handleBlur, handleFocus, provideMarqueeControllerContext} = useMarqueeController(props);
+ let wrappedProps = props;
- constructor (props) {
- super(props);
-
- this.controlled = [];
- this.isFocused = false;
- this.childContext = {
- cancel: this.handleCancel,
- complete: this.handleComplete,
- enter: this.handleEnter,
- leave: this.handleLeave,
- register: this.handleRegister,
- start: this.handleStart,
- unregister: this.handleUnregister
+ if (marqueeOnFocus) {
+ wrappedProps = {
+ ...props,
+ onBlur: handleBlur,
+ onFocus: handleFocus
};
}
- componentWillUnmount () {
- this.cancelJob.stop();
- }
-
- cancelJob = new Job(() => this.doCancel(), 30);
-
- /*
- * Registers `component` with a set of handlers for `start` and `stop`.
- *
- * @param {Object} component A component, typically a React component instance, on
- * which handlers will be dispatched.
- * @param {Object} handlers An object containing `start` and `stop` functions
- *
- * @returns {undefined}
- */
- handleRegister = (component, handlers) => {
- const needsStart = !this.allInactive() || this.isFocused;
-
- this.controlled.push({
- ...handlers,
- state: STATE.inactive,
- component
- });
-
- if (needsStart) {
- this.dispatch('start');
- }
- };
-
- /*
- * Unregisters `component` for synchronization
- *
- * @param {Object} component A previously registered component
- *
- * @returns {undefined}
- */
- handleUnregister = (component) => {
- let wasRunning = false;
- for (let i = 0; i < this.controlled.length; i++) {
- if (this.controlled[i].component === component) {
- wasRunning = this.controlled[i].state === STATE.active;
- this.controlled.splice(i, 1);
- break;
- }
- }
- if (wasRunning && !this.anyRunning()) {
- this.dispatch('start');
- }
- };
-
- /*
- * Handler for the `start` context function
- *
- * @param {Object} component A previously registered component
- *
- * @returns {undefined}
- */
- handleStart = (component) => {
- this.cancelJob.stop();
- if (!this.anyRunning()) {
- this.markAll(STATE.ready);
- this.dispatch('start', component);
- }
- };
-
- /*
- * Handler for the `cancel` context function
- *
- * @param {Object} component A previously registered component
- *
- * @returns {undefined}
- */
- handleCancel = () => {
- if (this.anyRunning()) {
- this.cancelJob.start();
- }
- };
-
- doCancel = () => {
- if (this.isHovered || this.isFocused) {
- return;
- }
- this.markAll(STATE.inactive);
- this.dispatch('stop');
- };
-
- /*
- * Handler for the `complete` context function
- *
- * @param {Object} component A previously registered component
- *
- * @returns {undefined}
- */
- handleComplete = (component) => {
- const complete = this.markReady(component);
- if (complete) {
- this.markAll(STATE.ready);
- this.dispatch('start');
- }
- };
-
- handleEnter = () => {
- this.isHovered = true;
- if (!this.anyRunning()) {
- this.dispatch('start');
- }
- this.cancelJob.stop();
- };
-
- handleLeave = () => {
- this.isHovered = false;
- this.cancelJob.start();
- };
-
- /*
- * Handler for the focus event
- */
- handleFocus = (ev) => {
- this.isFocused = true;
- if (!this.anyRunning()) {
- this.dispatch('start');
- }
- this.cancelJob.stop();
- forwardFocus(ev, this.props);
- };
-
- /*
- * Handler for the blur event
- */
- handleBlur = (ev) => {
- this.isFocused = false;
- if (this.anyRunning()) {
- this.cancelJob.start();
- }
- forwardBlur(ev, this.props);
- };
-
- /*
- * Invokes the `action` handler for each synchronized component except the invoking
- * `component`.
- *
- * @param {String} action `'start'` or `'stop'`
- * @param {Object} component A previously registered component
- *
- * @returns {undefined}
- */
- dispatch (action, component) {
- this.controlled.forEach((controlled) => {
- const {component: controlledComponent, [action]: handler} = controlled;
- if (component !== controlledComponent && typeof handler === 'function') {
- const complete = handler.call(controlledComponent);
-
- // Returning `true` from a start request means that the marqueeing is
- // unnecessary and is therefore not awaiting a finish
- if (action === 'start' && complete) {
- controlled.state = STATE.ready;
- } else if (action === 'start') {
- controlled.state = STATE.active;
- }
- } else if ((action === 'start') && (component === controlledComponent)) {
- controlled.state = STATE.active;
- }
- });
- }
-
- /*
- * Marks all components with the passed-in state
- *
- * @param {Enum} state The state to set
- *
- * @returns {undefined}
- */
- markAll (state) {
- this.controlled.forEach(c => {
- c.state = state;
- });
- }
-
- /*
- * Marks `component` as ready for next marquee action
- *
- * @param {Object} component A previously registered component
- *
- * @returns {Boolean} `true` if no components are STATE.active
- */
- markReady (component) {
- let complete = true;
- this.controlled.forEach(c => {
- if (c.component === component) {
- c.state = STATE.ready;
- }
-
- complete = complete && (c.state !== STATE.active);
- });
-
- return complete;
- }
-
- /*
- * Checks that all components are inactive
- *
- * @returns {Boolean} `true` if any components should be running
- */
- allInactive () {
- const activeOrReady = this.controlled.reduce((res, component) => {
- return res || !(component.state === STATE.inactive);
- }, false);
- return !activeOrReady;
- }
-
- /*
- * Checks for any components currently marqueeing
- *
- * @returns {Boolean} `true` if any component is marqueeing
- */
- anyRunning () {
- return this.controlled.reduce((res, component) => {
- return res || (component.state === STATE.active);
- }, false);
- }
-
- render () {
- let props = this.props;
-
- if (marqueeOnFocus) {
- props = {
- ...this.props,
- onBlur: this.handleBlur,
- onFocus: this.handleFocus
- };
- }
-
- return (
-
-
-
- );
- }
+ return provideMarqueeControllerContext(
+
+ );
};
-
});
export default MarqueeController;
diff --git a/packages/ui/Marquee/useMarqueeController.js b/packages/ui/Marquee/useMarqueeController.js
new file mode 100644
index 0000000000..0d06f9ecc7
--- /dev/null
+++ b/packages/ui/Marquee/useMarqueeController.js
@@ -0,0 +1,266 @@
+import {forward} from '@enact/core/handle';
+import {Job} from '@enact/core/util';
+import {createContext, useCallback, useMemo, useEffect, useRef} from 'react';
+
+const STATE = {
+ inactive: 0, // Marquee is not necessary (render or focus not happened)
+ active: 1, // Marquee in progress, awaiting complete
+ ready: 2 // Marquee completed or not needed, but state is active
+};
+
+const MarqueeControllerContext = createContext(null);
+
+const forwardBlur = forward('onBlur');
+const forwardFocus = forward('onFocus');
+
+const useMarqueeController = (props) => {
+ const mutableRef = useRef({
+ controlled: [],
+ isFocused: false,
+ isHovered: false
+ });
+
+ /*
+ * Invokes the `action` handler for each synchronized component except the invoking
+ * `component`.
+ *
+ * @param {String} action `'start'` or `'stop'`
+ * @param {Object} component A previously registered component
+ *
+ * @returns {undefined}
+ */
+ const dispatch = useCallback((action, component) => {
+ mutableRef.current.controlled.forEach((controlled) => {
+ const {component: controlledComponent, [action]: handler} = controlled;
+ if (component !== controlledComponent && typeof handler === 'function') {
+ const complete = handler.call(controlledComponent);
+
+ // Returning `true` from a start request means that the marqueeing is
+ // unnecessary and is therefore not awaiting a finish
+ if (action === 'start' && complete) {
+ controlled.state = STATE.ready;
+ } else if (action === 'start') {
+ controlled.state = STATE.active;
+ }
+ } else if ((action === 'start') && (component === controlledComponent)) {
+ controlled.state = STATE.active;
+ }
+ });
+ }, []);
+
+ /*
+ * Marks all components with the passed-in state
+ *
+ * @param {Enum} state The state to set
+ *
+ * @returns {undefined}
+ */
+ const markAll = useCallback((state) => {
+ mutableRef.current.controlled.forEach(c => {
+ c.state = state;
+ });
+ }, []);
+
+ /*
+ * Marks `component` as ready for next marquee action
+ *
+ * @param {Object} component A previously registered component
+ *
+ * @returns {Boolean} `true` if no components are STATE.active
+ */
+ const markReady = useCallback((component) => {
+ let complete = true;
+ mutableRef.current.controlled.forEach(c => {
+ if (c.component === component) {
+ c.state = STATE.ready;
+ }
+ complete = complete && (c.state !== STATE.active);
+ });
+ return complete;
+ }, []);
+
+ /*
+ * Checks that all components are inactive
+ *
+ * @returns {Boolean} `true` if any components should be running
+ */
+ const allInactive = useCallback(() => {
+ const activeOrReady = mutableRef.current.controlled.reduce((res, component) => {
+ return res || !(component.state === STATE.inactive);
+ }, false);
+ return !activeOrReady;
+ }, []);
+
+ /*
+ * Checks for any components currently marqueeing
+ *
+ * @returns {Boolean} `true` if any component is marqueeing
+ */
+ const anyRunning = useCallback(() => {
+ return mutableRef.current.controlled.reduce((res, component) => {
+ return res || (component.state === STATE.active);
+ }, false);
+ }, []);
+
+ const doCancel = useCallback(() => {
+ if (mutableRef.current.isHovered || mutableRef.current.isFocused) {
+ return;
+ }
+ markAll(STATE.inactive);
+ dispatch('stop');
+ }, [dispatch, markAll]);
+
+ const cancelJob = useMemo(() => new Job(() => doCancel(), 30), [doCancel]);
+
+ /*
+ * Registers `component` with a set of handlers for `start` and `stop`.
+ *
+ * @param {Object} component A component, typically a React component instance, on
+ * which handlers will be dispatched.
+ * @param {Object} handlers An object containing `start` and `stop` functions
+ *
+ * @returns {undefined}
+ */
+ const handleRegister = useCallback((component, handlers) => {
+ const needStart = !allInactive() || mutableRef.current.isFocused;
+
+ mutableRef.current.controlled.push({
+ ...handlers,
+ state: STATE.inactive,
+ component
+ });
+
+ if (needStart) {
+ dispatch('start');
+ }
+ }, [allInactive, dispatch]);
+
+ /*
+ * Unregisters `component` for synchronization
+ *
+ * @param {Object} component A previously registered component
+ *
+ * @returns {undefined}
+ */
+ const handleUnregister = useCallback((component) => {
+ let wasRunning = false;
+ for (let i = 0; i < mutableRef.current.controlled.length; i++) {
+ if (mutableRef.current.controlled[i].component === component) {
+ wasRunning = mutableRef.current.controlled[i].state === STATE.active;
+ mutableRef.current.controlled.splice(i, 1);
+ break;
+ }
+ }
+ if (wasRunning && !anyRunning()) {
+ dispatch('start');
+ }
+ }, [anyRunning, dispatch]);
+
+ /*
+ * Handler for the `start` context function
+ *
+ * @param {Object} component A previously registered component
+ *
+ * @returns {undefined}f
+ */
+ const handleStart = useCallback((component) => {
+ cancelJob.stop();
+ if (!anyRunning()) {
+ markAll(STATE.ready);
+ dispatch('start', component);
+ }
+ }, [anyRunning, cancelJob, dispatch, markAll]);
+
+ /*
+ * Handler for the `cancel` context function
+ *
+ * @param {Object} component A previously registered component
+ *
+ * @returns {undefined}
+ */
+ const handleCancel = useCallback(() => {
+ if (anyRunning()) {
+ cancelJob.start();
+ }
+ }, [anyRunning, cancelJob]);
+
+ /*
+ * Handler for the `complete` context function
+ *
+ * @param {Object} component A previously registered component
+ *
+ * @returns {undefined}
+ */
+ const handleComplete = useCallback((component) => {
+ const complete = markReady(component);
+ if (complete) {
+ markAll(STATE.ready);
+ dispatch('start');
+ }
+ }, [dispatch, markAll, markReady]);
+
+ const handleEnter = useCallback(() => {
+ mutableRef.current.isHovered = true;
+ if (!anyRunning()) {
+ dispatch('start');
+ }
+ cancelJob.stop();
+ }, [anyRunning, cancelJob, dispatch]);
+
+ const handleLeave = useCallback(() => {
+ mutableRef.current.isHovered = false;
+ cancelJob.start();
+ }, [cancelJob]);
+
+ /*
+ * Handler for the focus event
+ */
+ const handleFocus = useCallback((ev) => {
+ mutableRef.current.isFocused = true;
+ if (!anyRunning()) {
+ dispatch('start');
+ }
+ cancelJob.stop();
+ forwardFocus(ev, props);
+ }, [anyRunning, cancelJob, dispatch, props]);
+
+ /*
+ * Handler for the blur event
+ */
+ const handleBlur = useCallback((ev) => {
+ mutableRef.current.isFocused = false;
+ if (anyRunning()) {
+ cancelJob.start();
+ }
+ forwardBlur(ev, props);
+ }, [anyRunning, cancelJob, props]);
+
+ useEffect(() => {
+ return () => {
+ cancelJob.stop();
+ };
+ }, [cancelJob]);
+
+ const value = useMemo(() => ({cancel: handleCancel, complete: handleComplete, enter: handleEnter, leave: handleLeave, register: handleRegister, start: handleStart, unregister: handleUnregister}),
+ [handleCancel, handleComplete, handleEnter, handleLeave, handleRegister, handleStart, handleUnregister]);
+
+ const provideMarqueeControllerContext = useCallback((children) => {
+ return (
+
+ {children}
+
+ );
+ }, [value]);
+
+ return {
+ handleBlur,
+ handleFocus,
+ provideMarqueeControllerContext
+ };
+};
+
+export default useMarqueeController;
+export {
+ MarqueeControllerContext,
+ useMarqueeController
+};