@workday/canvas-kit-react
Version:
The parent module that contains all Workday Canvas Kit React components
292 lines (291 loc) • 11.3 kB
JavaScript
import * as React from 'react';
export var InputType;
(function (InputType) {
InputType["Initial"] = "initial";
InputType["Keyboard"] = "keyboard";
InputType["Mouse"] = "mouse";
InputType["Pointer"] = "pointer";
InputType["Touch"] = "touch";
})(InputType || (InputType = {}));
export var InputEventType;
(function (InputEventType) {
InputEventType["KeyDown"] = "keydown";
InputEventType["KeyUp"] = "keyup";
InputEventType["MouseDown"] = "mousedown";
InputEventType["MouseMove"] = "mousemove";
InputEventType["Wheel"] = "wheel";
InputEventType["MouseWheel"] = "mousewheel";
InputEventType["PointerDown"] = "pointerdown";
InputEventType["PointerMove"] = "pointermove";
InputEventType["TouchStart"] = "touchstart";
})(InputEventType || (InputEventType = {}));
// form input types
const formInputs = ['input', 'select', 'textarea'];
// list of modifier keys commonly used with the mouse and
// can be safely ignored to prevent false keyboard detection
const ignoreKeys = ['Shift', 'Control', 'Alt', 'Meta', 'OS'];
// mapping of events to input types
export const inputEventMap = {
[InputEventType.KeyDown]: InputType.Keyboard,
[InputEventType.KeyUp]: InputType.Keyboard,
[InputEventType.MouseDown]: InputType.Mouse,
[InputEventType.MouseMove]: InputType.Mouse,
[InputEventType.Wheel]: InputType.Mouse,
[InputEventType.MouseWheel]: InputType.Mouse,
[InputEventType.PointerDown]: InputType.Pointer,
[InputEventType.PointerMove]: InputType.Pointer,
[InputEventType.TouchStart]: InputType.Touch,
};
// map of IE 10 pointer events
const pointerMap = {
2: InputType.Touch,
3: InputType.Touch,
4: InputType.Mouse,
};
const getPointerType = (event) => {
if (typeof event.pointerType === 'number') {
return pointerMap[event.pointerType];
}
else if (event.pointerType === 'mouse') {
return InputType.Mouse;
}
else {
// treat pen like touch
return InputType.Touch;
}
};
// detect version of mouse wheel event to use
// via https://developer.mozilla.org/en-US/docs/Web/Events/wheel
const detectWheel = () => {
let wheelType;
// Modern browsers support "wheel"
/* istanbul ignore else for coverage */
if ('onwheel' in document.createElement('div')) {
wheelType = InputEventType.Wheel;
}
else {
// Webkit and IE support at least "mousewheel"
wheelType = InputEventType.MouseWheel;
}
return wheelType;
};
const supportsPassive = () => {
let supportsPassive;
try {
/* istanbul ignore next function for coverage */
const opts = Object.defineProperty({}, 'passive', {
get: () => {
supportsPassive = true;
},
});
/* istanbul ignore next function for coverage */
const stub = () => {
return;
};
window.addEventListener('test', stub, opts);
window.removeEventListener('test', stub, opts);
}
catch (e) {
/* istanbul ignore next line for coverage */
console.warn('Browser does not support passive event listeners');
}
return supportsPassive || false;
};
/**
* This component takes heavy inspiration from what-input (https://github.com/ten1seven/what-input)
*/
export class InputProvider extends React.Component {
constructor(props) {
super(props);
this.isBuffering = false;
this.isScrolling = false; // Unused if props.provideIntent is not defined
this.mousePosX = null; // Unused if props.provideIntent is not defined
this.mousePosY = null; // Unused if props.provideIntent is not defined
this.deferInputTracking = false; // True if there is another input provider
// Need to remember how it component was mounted so we ensure listener removal
this.provideIntent = this.props.provideIntent;
// check for sessionStorage support
// then check for session variables and use if available
let storedInput, storedIntent;
try {
storedInput = window.sessionStorage.getItem('what-input');
storedIntent = window.sessionStorage.getItem('what-intent');
}
catch (e) {
// Don't log if window is undefined (i.e. we are in an SSR environment),
// because we can assume the entire implementation will not work and is not needed.
if (typeof window !== 'undefined') {
console.warn('Failed to retrieve input status from session storage' + e);
}
}
this.currentInput = storedInput || InputType.Initial;
this.currentIntent = storedIntent || InputType.Initial;
this.setInput = this.setInput.bind(this);
this.setIntent = this.setIntent.bind(this);
this.eventBuffer = this.eventBuffer.bind(this);
}
getContainer(container) {
// Note: Not a default prop because using document when defining default props causes errors during SSR
if (!container) {
return document.body;
}
if ('current' in container) {
if (container.current === null) {
console.warn('Your ref object can not be null, therefore, falling back to document.body');
return document.body;
}
else {
return container.current;
}
}
return container;
}
componentDidMount() {
// Check for passive event listener support
this.supportsPassive = supportsPassive();
if (this.getContainer(this.props.container).closest('[data-whatinput]')) {
this.deferInputTracking = true;
return;
}
this.updateAttributes();
this.enableListeners(true);
}
updateAttributes() {
const intent = this.provideIntent ? this.currentIntent : null;
this.getContainer(this.props.container).setAttribute('data-whatinput', this.currentInput);
if (intent) {
this.getContainer(this.props.container).setAttribute('data-whatintent', intent);
}
try {
window.sessionStorage.setItem('what-input', this.currentInput);
window.sessionStorage.setItem('what-intent', this.currentIntent);
}
catch (e) {
/* istanbul ignore next line for coverage */
console.warn('Failed to set input status in session storage' + e);
}
}
componentWillUnmount() {
if (this.deferInputTracking) {
return;
}
this.getContainer(this.props.container).removeAttribute('data-whatinput');
this.getContainer(this.props.container).removeAttribute('data-whatintent');
window.clearTimeout(this.eventTimer);
this.enableListeners(false);
}
enableListeners(enable) {
/* istanbul ignore if for coverage */
if (typeof window === 'undefined') {
return;
}
// `pointermove`, `MSPointerMove`, `mousemove` and mouse wheel event binding
// can only demonstrate potential, but not actual, interaction
// and are treated separately
const options = this.supportsPassive
? { passive: true } // fixes Type '{ passive: boolean; }' has no properties in common with type 'EventListenerOptions'. TS2345
: false;
const fn = enable ? window.addEventListener : window.removeEventListener;
// pointer events (mouse, pen, touch)
if (window.PointerEvent) {
fn('pointerdown', this.setInput);
}
else {
// mouse events
fn('mousedown', this.setInput);
// touch events
if ('ontouchstart' in window) {
fn('touchstart', this.eventBuffer, options);
fn('touchend', this.setInput);
}
}
if (this.provideIntent) {
if (window.PointerEvent) {
fn('pointermove', this.setIntent);
}
else {
fn('mousemove', this.setIntent);
}
// mouse wheel
fn(detectWheel(), this.setIntent, options);
}
// keyboard events
fn('keydown', this.eventBuffer);
fn('keyup', this.eventBuffer);
}
setInput(event) {
// only execute if the event buffer timer isn't running
/* istanbul ignore if for coverage */
if (this.isBuffering) {
return;
}
const eventKey = 'key' in event ? event.key : undefined;
const eventType = event.type;
let value = inputEventMap[eventType];
if (value === InputType.Pointer) {
value = getPointerType(event);
}
const ignoreMatch = eventKey ? ignoreKeys.indexOf(eventKey) === -1 : undefined;
const shouldUpdate = (value === InputType.Keyboard && eventKey && ignoreMatch) ||
value === InputType.Mouse ||
value === InputType.Touch;
if (this.currentInput !== value && shouldUpdate) {
this.currentInput = value;
this.updateAttributes();
}
if (this.currentIntent !== value && shouldUpdate && this.provideIntent) {
// preserve intent for keyboard typing in form fields
const activeElem = document.activeElement;
const notFormInput = activeElem &&
activeElem.nodeName &&
formInputs.indexOf(activeElem.nodeName.toLowerCase()) === -1;
/* istanbul ignore else for coverage */
if (notFormInput) {
this.currentIntent = value;
this.updateAttributes();
}
}
}
// updates input intent for `mousemove` and `pointermove`
setIntent(event) {
// test to see if `mousemove` happened relative to the screen to detect scrolling versus mousemove
this.detectScrolling(event);
// only execute if the event buffer timer isn't running
// or scrolling isn't happening
/* istanbul ignore else for coverage */
if (!this.isBuffering && !this.isScrolling) {
const eventType = event.type;
let value = inputEventMap[eventType];
if (value === InputType.Pointer) {
value = getPointerType(event);
}
this.currentIntent = value;
this.updateAttributes();
}
}
// buffers events that frequently also fire mouse events
eventBuffer(event) {
// set the current input
this.setInput(event);
window.clearTimeout(this.eventTimer);
this.isBuffering = true;
/* istanbul ignore next function for coverage */
this.eventTimer = window.setTimeout(() => {
this.isBuffering = false;
}, 100);
}
detectScrolling(event) {
if (this.mousePosX !== event.screenX || this.mousePosY !== event.screenY) {
this.isScrolling = false;
this.mousePosX = event.screenX;
this.mousePosY = event.screenY;
}
else {
this.isScrolling = true;
}
}
render() {
return this.props.children || null;
}
}
export default InputProvider;