Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
sarayourfriend committed Jul 23, 2021
1 parent 3b8d568 commit 102811e
Show file tree
Hide file tree
Showing 12 changed files with 972 additions and 1 deletion.
23 changes: 23 additions & 0 deletions packages/components/src/input/backdrop.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* WordPress dependencies
*/
import { memo } from '@wordpress/element';
/**
* Internal dependencies
*/
import { BackdropUI } from './styles';

function Backdrop( { disabled = false, isFocused = false } ) {
return (
<BackdropUI
aria-hidden="true"
className="components-input-control__backdrop"
disabled={ disabled }
isFocused={ isFocused }
/>
);
}

const MemoizedBackdrop = memo( Backdrop );

export default MemoizedBackdrop;
268 changes: 268 additions & 0 deletions packages/components/src/input/component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
/**
* External dependencies
*/
import { noop } from 'lodash';
import { useDrag } from 'react-use-gesture';
// eslint-disable-next-line no-restricted-imports
import type { Ref, SyntheticEvent, PointerEvent, MouseEvent } from 'react';

/**
* WordPress dependencies
*/
import { useRef } from '@wordpress/element';
import { UP, DOWN, ENTER } from '@wordpress/keycodes';
/**
* Internal dependencies
*/
import { useDragCursor } from './utils';
import { Input as InputView, Container, Prefix, Suffix } from './styles';
import Backdrop from './backdrop';
import { useInputControlStateReducer } from './reducer/reducer';
import { isValueEmpty } from '../utils/values';
import { useUpdateEffect } from '../utils';
import {
contextConnect,
useContextSystem,
PolymorphicComponentProps,
} from '../ui/context';
import type { Props } from './types';

function Input(
props: PolymorphicComponentProps< Props, 'input' >,
forwardedRef: Ref< any >
) {
const {
disabled = false,
dragDirection = 'n',
dragThreshold = 10,
id,
isDragEnabled = false,
isFocused,
isPressEnterToChange = false,
onBlur = noop,
onChange = noop,
onDrag = noop,
onDragEnd = noop,
onDragStart = noop,
onFocus = noop,
onKeyDown = noop,
onValidate = noop,
size = 'default',
setIsFocused,
stateReducer = ( state ) => state,
value: valueProp,
type,
hideLabelFromVision = false,
__unstableInputWidth,
labelPosition,
prefix,
suffix,
...otherProps
} = useContextSystem( props, 'Input' );

const {
// State
state,
// Actions
change,
commit,
drag,
dragEnd,
dragStart,
invalidate,
pressDown,
pressEnter,
pressUp,
reset,
update,
} = useInputControlStateReducer( stateReducer, {
isDragEnabled,
value: valueProp,
isPressEnterToChange,
} );

const { _event, value, isDragging, isDirty } = state;
const wasDirtyOnBlur = useRef( false );

const dragCursor = useDragCursor( isDragging, dragDirection );

/*
* Handles synchronization of external and internal value state.
* If not focused and did not hold a dirty value[1] on blur
* updates the value from the props. Otherwise if not holding
* a dirty value[1] propagates the value and event through onChange.
* [1] value is only made dirty if isPressEnterToChange is true
*/
useUpdateEffect( () => {
if ( valueProp === value ) {
return;
}
if ( ! isFocused && ! wasDirtyOnBlur.current ) {
update( valueProp, _event as SyntheticEvent );
} else if ( ! isDirty ) {
onChange( value, { event: _event } );
wasDirtyOnBlur.current = false;
}
}, [ value, isDirty, isFocused, valueProp ] );

const handleOnBlur = ( event ) => {
onBlur( event );
setIsFocused( false );

/**
* If isPressEnterToChange is set, this commits the value to
* the onChange callback.
*/
if ( isPressEnterToChange && isDirty ) {
wasDirtyOnBlur.current = true;
if ( ! isValueEmpty( value ) ) {
handleOnCommit( event );
} else {
reset( valueProp, event );
}
}
};

const handleOnFocus = ( event ) => {
onFocus( event );
setIsFocused( true );
};

const handleOnChange = ( event ) => {
const nextValue = event.target.value;
change( nextValue, event );
};

const handleOnCommit = ( event ) => {
const nextValue = event.target.value;

try {
onValidate( nextValue );
commit( nextValue, event );
} catch ( err ) {
invalidate( err, event );
}
};

const handleOnKeyDown = ( event ) => {
const { keyCode } = event;
onKeyDown( event );

switch ( keyCode ) {
case UP:
pressUp( event );
break;

case DOWN:
pressDown( event );
break;

case ENTER:
pressEnter( event );

if ( isPressEnterToChange ) {
event.preventDefault();
handleOnCommit( event );
}
break;
}
};

const dragGestureProps = useDrag< PointerEvent< HTMLElement > >(
( dragProps ) => {
const { distance, dragging, event } = dragProps;
// The event is persisted to prevent errors in components using this
// to check if a modifier key was held while dragging.
( event as SyntheticEvent ).persist();

if ( ! distance ) return;
event.stopPropagation();

/**
* Quick return if no longer dragging.
* This prevents unnecessary value calculations.
*/
if ( ! dragging ) {
onDragEnd( dragProps );
dragEnd( dragProps );
return;
}

onDrag( dragProps );
drag( dragProps );

if ( ! isDragging ) {
onDragStart( dragProps );
dragStart( dragProps );
}
},
{
threshold: dragThreshold,
enabled: isDragEnabled,
}
);

const dragProps = isDragEnabled ? dragGestureProps() : {};
/*
* Works around the odd UA (e.g. Firefox) that does not focus inputs of
* type=number when their spinner arrows are pressed.
*/
let handleOnMouseDown:
| undefined
| ( ( event: MouseEvent< HTMLInputElement > ) => void );
if ( type === 'number' ) {
handleOnMouseDown = ( event ) => {
props.onMouseDown?.( event );
if (
event.target !==
( event.target as HTMLInputElement ).ownerDocument.activeElement
) {
( event.target as HTMLInputElement ).focus();
}
};
}

return (
<Container
__unstableInputWidth={ __unstableInputWidth }
className="components-input-control__container"
disabled={ disabled }
hideLabel={ hideLabelFromVision }
labelPosition={ labelPosition }
>
{ prefix && (
<Prefix className="components-input-control__prefix">
{ prefix }
</Prefix>
) }
<InputView
{ ...otherProps }
{ ...dragProps }
className="components-input-control__input"
disabled={ disabled }
dragCursor={ dragCursor }
isDragging={ isDragging }
id={ id }
onBlur={ handleOnBlur }
onChange={ handleOnChange }
onFocus={ handleOnFocus }
onKeyDown={ handleOnKeyDown }
onMouseDown={ handleOnMouseDown }
ref={ forwardedRef }
inputSize={ size }
value={ value }
type={ type }
/>
{ suffix && (
<Suffix className="components-input-control__suffix">
{ suffix }
</Suffix>
) }
<Backdrop disabled={ disabled } isFocused={ isFocused } />
</Container>
);
}

const ConnectedInput = contextConnect( Input, 'Input' );

export default ConnectedInput;
1 change: 1 addition & 0 deletions packages/components/src/input/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as Input } from './component';
61 changes: 61 additions & 0 deletions packages/components/src/input/reducer/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* External dependencies
*/
// eslint-disable-next-line no-restricted-imports
import type { SyntheticEvent } from 'react';

export const CHANGE = 'CHANGE';
export const COMMIT = 'COMMIT';
export const DRAG_END = 'DRAG_END';
export const DRAG_START = 'DRAG_START';
export const DRAG = 'DRAG';
export const INVALIDATE = 'INVALIDATE';
export const PRESS_DOWN = 'PRESS_DOWN';
export const PRESS_ENTER = 'PRESS_ENTER';
export const PRESS_UP = 'PRESS_UP';
export const RESET = 'RESET';
export const UPDATE = 'UPDATE';

interface EventPayload {
event?: SyntheticEvent;
}

interface Action< Type, ExtraPayload = {} > {
type: Type;
payload: EventPayload & ExtraPayload;
}

interface ValuePayload {
value: string;
}

export type ChangeAction = Action< typeof CHANGE, ValuePayload >;
export type CommitAction = Action< typeof COMMIT, ValuePayload >;
export type PressUpAction = Action< typeof PRESS_UP >;
export type PressDownAction = Action< typeof PRESS_DOWN >;
export type PressEnterAction = Action< typeof PRESS_ENTER >;
export type DragStartAction = Action< typeof DRAG_START >;
export type DragEndAction = Action< typeof DRAG_END >;
export type DragAction = Action< typeof DRAG >;
export type ResetAction = Action< typeof RESET, Partial< ValuePayload > >;
export type UpdateAction = Action< typeof UPDATE, ValuePayload >;
export type InvalidateAction = Action<
typeof INVALIDATE,
{ error: Error | null }
>;

export type ChangeEventAction =
| ChangeAction
| ResetAction
| CommitAction
| UpdateAction;

export type DragEventAction = DragStartAction | DragEndAction | DragAction;

export type KeyEventAction = PressDownAction | PressUpAction | PressEnterAction;

export type InputAction =
| ChangeEventAction
| KeyEventAction
| DragEventAction
| InvalidateAction;
Loading

0 comments on commit 102811e

Please sign in to comment.