@gechiui/components
Version:
UI components for GeChiUI.
246 lines (216 loc) • 6.12 kB
text/typescript
/**
* External dependencies
*/
import { isEmpty } from 'lodash';
// eslint-disable-next-line no-restricted-imports
import type { SyntheticEvent } from 'react';
/**
* GeChiUI dependencies
*/
import { useReducer } from '@gechiui/element';
/**
* Internal dependencies
*/
import {
InputState,
StateReducer,
initialInputControlState,
initialStateReducer,
} from './state';
import * as actions from './actions';
/**
* Prepares initialState for the reducer.
*
* @param initialState The initial state.
* @return Prepared initialState for the reducer
*/
function mergeInitialState(
initialState: Partial< InputState > = initialInputControlState
): InputState {
const { value } = initialState;
return {
...initialInputControlState,
...initialState,
initialValue: value,
} as InputState;
}
/**
* Composes multiple stateReducers into a single stateReducer, building
* the pipeline to control the flow for state and actions.
*
* @param fns State reducers.
* @return The single composed stateReducer.
*/
export const composeStateReducers = (
...fns: StateReducer[]
): StateReducer => {
return ( ...args ) => {
return fns.reduceRight( ( state, fn ) => {
const fnState = fn( ...args );
return isEmpty( fnState ) ? state : { ...state, ...fnState };
}, {} as InputState );
};
};
/**
* Creates a reducer that opens the channel for external state subscription
* and modification.
*
* This technique uses the "stateReducer" design pattern:
* https://kentcdodds.com/blog/the-state-reducer-pattern/
*
* @param composedStateReducers A custom reducer that can subscribe and modify state.
* @return The reducer.
*/
function inputControlStateReducer(
composedStateReducers: StateReducer
): StateReducer {
return ( state, action ) => {
const nextState = { ...state };
switch ( action.type ) {
/**
* Keyboard events
*/
case actions.PRESS_UP:
nextState.isDirty = false;
break;
case actions.PRESS_DOWN:
nextState.isDirty = false;
break;
/**
* Drag events
*/
case actions.DRAG_START:
nextState.isDragging = true;
break;
case actions.DRAG_END:
nextState.isDragging = false;
break;
/**
* Input events
*/
case actions.CHANGE:
nextState.error = null;
nextState.value = action.payload.value;
if ( state.isPressEnterToChange ) {
nextState.isDirty = true;
}
break;
case actions.COMMIT:
nextState.value = action.payload.value;
nextState.isDirty = false;
break;
case actions.RESET:
nextState.error = null;
nextState.isDirty = false;
nextState.value = action.payload.value || state.initialValue;
break;
case actions.UPDATE:
nextState.value = action.payload.value;
nextState.isDirty = false;
break;
/**
* Validation
*/
case actions.INVALIDATE:
nextState.error = action.payload.error;
break;
}
if ( action.payload.event ) {
nextState._event = action.payload.event;
}
/**
* Send the nextState + action to the composedReducers via
* this "bridge" mechanism. This allows external stateReducers
* to hook into actions, and modify state if needed.
*/
return composedStateReducers( nextState, action );
};
}
/**
* A custom hook that connects and external stateReducer with an internal
* reducer. This hook manages the internal state of InputControl.
* However, by connecting an external stateReducer function, other
* components can react to actions as well as modify state before it is
* applied.
*
* This technique uses the "stateReducer" design pattern:
* https://kentcdodds.com/blog/the-state-reducer-pattern/
*
* @param stateReducer An external state reducer.
* @param initialState The initial state for the reducer.
* @return State, dispatch, and a collection of actions.
*/
export function useInputControlStateReducer(
stateReducer: StateReducer = initialStateReducer,
initialState: Partial< InputState > = initialInputControlState
) {
const [ state, dispatch ] = useReducer< StateReducer >(
inputControlStateReducer( stateReducer ),
mergeInitialState( initialState )
);
const createChangeEvent = ( type: actions.ChangeEventAction[ 'type' ] ) => (
nextValue: actions.ChangeEventAction[ 'payload' ][ 'value' ],
event: actions.ChangeEventAction[ 'payload' ][ 'event' ]
) => {
/**
* Persist allows for the (Synthetic) event to be used outside of
* this function call.
* https://reactjs.org/docs/events.html#event-pooling
*/
if ( event && event.persist ) {
event.persist();
}
dispatch( {
type,
payload: { value: nextValue, event },
} as actions.InputAction );
};
const createKeyEvent = ( type: actions.KeyEventAction[ 'type' ] ) => (
event: actions.KeyEventAction[ 'payload' ][ 'event' ]
) => {
/**
* Persist allows for the (Synthetic) event to be used outside of
* this function call.
* https://reactjs.org/docs/events.html#event-pooling
*/
if ( event && event.persist ) {
event.persist();
}
dispatch( { type, payload: { event } } );
};
const createDragEvent = ( type: actions.DragEventAction[ 'type' ] ) => (
payload: actions.DragEventAction[ 'payload' ]
) => {
dispatch( { type, payload } );
};
/**
* Actions for the reducer
*/
const change = createChangeEvent( actions.CHANGE );
const invalidate = ( error: unknown, event: SyntheticEvent ) =>
dispatch( { type: actions.INVALIDATE, payload: { error, event } } );
const reset = createChangeEvent( actions.RESET );
const commit = createChangeEvent( actions.COMMIT );
const update = createChangeEvent( actions.UPDATE );
const dragStart = createDragEvent( actions.DRAG_START );
const drag = createDragEvent( actions.DRAG );
const dragEnd = createDragEvent( actions.DRAG_END );
const pressUp = createKeyEvent( actions.PRESS_UP );
const pressDown = createKeyEvent( actions.PRESS_DOWN );
const pressEnter = createKeyEvent( actions.PRESS_ENTER );
return {
change,
commit,
dispatch,
drag,
dragEnd,
dragStart,
invalidate,
pressDown,
pressEnter,
pressUp,
reset,
state,
update,
} as const;
}