UNPKG

wix-style-react

Version:
142 lines 5.67 kB
import React from 'react'; import hoistNonReactMethods from 'hoist-non-react-methods'; import { st, classes } from './Focusable.st.css'; import { getDisplayName, isStatelessComponent } from '../hocUtils'; /** * Assigned the given propTypes to the given class. * * This is a hack because since Yoshi3, with babel-preset-yoshi, * the babel-plugin-transform-react-remove-prop-types is enabled and removes propTypes. * * So if we simply do Focusable.propTypes = Component.propTypes, it is being stripped away. * * This later becomes a problem if another component defines: * <code> * Comp.propTypes = { * prop1: SomeFocusableComp.propTypes.prop1 * } * </code> */ const assignPropTypesHack = (target, propTypes) => { target.propTypes = propTypes; }; /** * Singleton for managing current input method (keyboard or mouse). */ const inputMethod = new (class { constructor() { // Default is keyboard in case an element is focused programmatically. this.method = 'keyboard'; this.subscribers = new Map(); this.subscribe = (target, callback) => this.subscribers.set(target, callback); this.unsubscribe = (target) => this.subscribers.delete(target); /** * Is the current input method `keyboard`. if `false` is means it is `mouse` */ this.isKeyboard = () => this.method === 'keyboard'; if (typeof window !== 'undefined') { window.addEventListener('mousedown', () => this.setMethod('mouse')); window.addEventListener('keydown', () => this.setMethod('keyboard')); // We need to listen on keyUp, in case a TAB is made from the browser's address-bar, // so the keyDown is not fired, only the keyUp. window.addEventListener('keyup', () => this.setMethod('keyboard')); } } setMethod(method) { if (method !== this.method) { this.method = method; this.subscribers.forEach(f => f()); } } })(); /* * TODO: Consider adding 'disabled' state to this HOC, since: * - When component is focused and then it becomes disabled, then the focus needs to be blured. * * TODO: Consider using [Recompose](https://github.com/acdlite/recompose/tree/master/src/packages/recompose) to do: * - the static hoisting * - set displayName */ export function withFocusable(Component, options = {}) { class Focusable extends React.Component { constructor() { super(...arguments); this.wrappedComponentRef = null; this.state = { focus: false, focusVisible: false, }; this.focus = () => { if (this.wrappedComponentRef?.focus) { this.wrappedComponentRef.focus(); } }; this.markAsFocused = () => { this.setState({ focus: true, focusVisible: options.isFocusWithMouse || inputMethod.isKeyboard(), }); inputMethod.subscribe(this, () => { if (options.isFocusWithMouse || inputMethod.isKeyboard()) { this.setState({ focusVisible: true }); } }); }; this.markAsBlurred = () => { inputMethod.unsubscribe(this); this.setState({ focus: false, focusVisible: false }); }; this.onFocus = (event) => { const { onFocus } = this.props; onFocus ? onFocus(event, { blur: this.markAsBlurred, focus: this.markAsFocused, }) : this.markAsFocused(); }; this.onBlur = (event) => { const { onBlur } = this.props; onBlur ? onBlur(event, { blur: this.markAsBlurred, focus: this.markAsFocused }) : this.markAsBlurred(); }; } componentWillUnmount() { inputMethod.unsubscribe(this); } componentDidUpdate(prevProps) { /* in case when button was focused and then become disabled, we need to trigger blur logic and remove all listers, as disabled button do not trigger onFocus and onBlur events */ const isFocused = this.state.focus || this.state.focusVisible; const isBecomeDisabled = !prevProps.disabled && this.props.disabled; if (isFocused && isBecomeDisabled) { this.onBlur({}); } } render() { const reference = isStatelessComponent(Component) ? undefined : (ref) => { this.wrappedComponentRef = ref; }; return (React.createElement(Component, { ...this.props, ref: reference, focusableOnFocus: this.onFocus, focusableOnBlur: this.onBlur, className: st(classes.root, { focus: this.state.focus, 'focus-visible': this.state.focusVisible, }, this.props.className) })); } } Focusable.displayName = getDisplayName(Component); Focusable.defaultProps = Component.defaultProps; assignPropTypesHack(Focusable, Component.propTypes); return isStatelessComponent(Component) ? Focusable : hoistNonReactMethods(Focusable, Component, { delegateTo: (c) => c.wrappedComponentRef, hoistStatics: true, }); } //# sourceMappingURL=Focusable.js.map