wix-style-react
Version:
wix-style-react
142 lines • 5.67 kB
JavaScript
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