UNPKG

@awsui/components-react

Version:

On July 19th, 2022, we launched [Cloudscape Design System](https://cloudscape.design). Cloudscape is an evolution of AWS-UI. It consists of user interface guidelines, front-end components, design resources, and development tools for building intuitive, en

277 lines • 10.4 kB
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { useEffect, useReducer, useRef } from 'react'; export function calculateNextState(state, action) { switch (action.type) { case 'POINTER_DOWN': { const { nativeEvent, metadata } = action.payload; return { value: 'dnd-start', eventData: nativeEvent, metadata, }; } case 'POINTER_MOVE': { const { nativeEvent } = action.payload; if (state.value === 'dnd-start' || state.value === 'dnd-active') { return { ...state, value: 'dnd-active', eventData: nativeEvent, }; } return state; } case 'POINTER_UP': { if (state.value === 'dnd-start' || state.value === 'dnd-active') { const dndSubStateBeforeUp = state.value; if (dndSubStateBeforeUp === 'dnd-start') { return { value: 'uap-action-start', metadata: state.metadata, // Preserve metadata when transitioning from dnd-start to uap-action-start }; } else { return { value: 'dnd-end', eventData: state.eventData, }; } } return state; } case 'POINTER_CANCEL': { if (state.value === 'dnd-start' || state.value === 'dnd-active') { return { value: 'dnd-end', }; } return state; } case 'KEY_DOWN': { const { key, metadata } = action.payload; if (key === 'Enter' || key === ' ') { if (state.value === null || state.value === 'uap-action-end' || state.value === 'dnd-end') { return { value: 'uap-action-start', metadata, }; } else if (state.value === 'uap-action-start') { return { value: 'uap-action-end', }; } else if (state.value === 'dnd-start' || state.value === 'dnd-active') { return { value: 'uap-action-start', metadata, }; } } else if (key === 'Escape') { if (state.value === 'uap-action-start') { return { value: 'uap-action-end', }; } } return state; } case 'FOCUS': return state; case 'BLUR': if (state.value === 'uap-action-start') { return { value: 'uap-action-end', }; } return state; default: throw new Error(`Invariant violation: unexpected action type: "${action.type}".`); } } export function getCallbacksForTransition(prevState, nextState) { const callbacks = []; // Transitions from dnd-start or dnd-active to any other state if ((prevState.value === 'dnd-start' || prevState.value === 'dnd-active') && nextState.value !== 'dnd-start' && nextState.value !== 'dnd-active') { callbacks.push({ type: 'onDndEnd' }); } // From uap-action-start to uap-action-end directly if (prevState.value === 'uap-action-start' && nextState.value === 'uap-action-end') { callbacks.push({ type: 'onUapActionEnd' }); } // Transitions to dnd-start if (nextState.value === 'dnd-start') { callbacks.push({ type: 'onDndStart', payload: nextState.eventData, metadata: nextState.metadata, }); } // Transitions to dnd-active if (nextState.value === 'dnd-active' && prevState.value === 'dnd-start') { callbacks.push({ type: 'onDndActive', payload: nextState.eventData, }); } // Transitions to uap-action-start if (nextState.value === 'uap-action-start') { callbacks.push({ type: 'onUapActionStart', metadata: nextState.metadata, }); } return callbacks; } function interactionReducer(state, action) { if (action.type === 'CLEAR_CALLBACKS') { return { ...state, transitionCallbacks: undefined, }; } const nextState = calculateNextState(state, action); // Special handling for POINTER_MOVE to always trigger onDndActive callback if (action.type === 'POINTER_MOVE' && action.payload.nativeEvent) { const transitionCallbacks = getCallbacksForTransition(state, nextState); // Check if there's already an onDndActive callback from the transition const hasOnDndActiveCallback = transitionCallbacks.some(callback => callback.type === 'onDndActive'); const callbacks = hasOnDndActiveCallback ? transitionCallbacks : [...transitionCallbacks, { type: 'onDndActive', payload: action.payload.nativeEvent }]; return { ...nextState, transitionCallbacks: callbacks, }; } // Return current state if state didn't change const isDndState = (state) => { return state.value === 'dnd-start' || state.value === 'dnd-active' || state.value === 'dnd-end'; }; if (nextState.value === state.value) { if (isDndState(nextState) && isDndState(state)) { if (nextState.eventData === state.eventData) { return state; } } else { return state; } } const callbacks = getCallbacksForTransition(state, nextState); return { ...nextState, transitionCallbacks: callbacks.length > 0 ? callbacks : undefined, }; } function useStateLogger(state, isEnabled) { const prevStateRef = useRef(state); useEffect(() => { if (state.value !== prevStateRef.current.value && isEnabled) { console.log(`State transition: ${prevStateRef.current.value} -> ${state.value}`, { prevState: prevStateRef.current, nextState: state, }); } prevStateRef.current = state; }, [state, isEnabled]); } function useCallbackHandler(pendingCallbacks, props, dispatch) { useEffect(() => { if (!(pendingCallbacks === null || pendingCallbacks === void 0 ? void 0 : pendingCallbacks.length)) { return; } pendingCallbacks.forEach(callback => { var _a, _b, _c, _d, _e; switch (callback.type) { case 'onDndStart': (_a = props.onDndStartAction) === null || _a === void 0 ? void 0 : _a.call(props, callback.payload, callback.metadata); break; case 'onDndActive': (_b = props.onDndActiveAction) === null || _b === void 0 ? void 0 : _b.call(props, callback.payload); break; case 'onDndEnd': (_c = props.onDndEndAction) === null || _c === void 0 ? void 0 : _c.call(props); break; case 'onUapActionStart': (_d = props.onUapActionStartAction) === null || _d === void 0 ? void 0 : _d.call(props, callback.metadata); break; case 'onUapActionEnd': (_e = props.onUapActionEndAction) === null || _e === void 0 ? void 0 : _e.call(props); break; } }); // Clean up callbacks to prevent re-execution on subsequent re-renders. dispatch({ type: 'CLEAR_CALLBACKS' }); }, [pendingCallbacks, props, dispatch]); } /** * Manages interaction states for drag handle components. * * The hook implements a state machine for drag handles that supports: * - Pointer-based drag-and-drop (DnD) interactions * - Keyboard-based universal access pattern (UAP) interactions * - State transitions with appropriate callbacks * - Metadata passing between states * * States: * - null: Idle state with no active interaction * - dnd-start: Initial pointer down, beginning of potential drag * - dnd-active: Active dragging with pointer movement * - dnd-end: Completed drag operation * - uap-action-start: Keyboard/accessibility action initiated * - uap-action-end: Keyboard/accessibility action completed */ export default function useDragHandleInteractionState(props = {}, options = { debug: false }) { const propsRef = useRef(props); propsRef.current = props; const [interaction, dispatch] = useReducer((interactionReducer), { value: null }); useStateLogger(interaction, options.debug); useCallbackHandler(interaction.transitionCallbacks, propsRef.current, dispatch); return { interaction, processPointerDown: (event, metadata) => { dispatch({ type: 'POINTER_DOWN', payload: { nativeEvent: event, metadata }, }); }, processPointerMove: (event) => { dispatch({ type: 'POINTER_MOVE', payload: { nativeEvent: event }, }); }, processPointerUp: (event) => { dispatch({ type: 'POINTER_UP', payload: { nativeEvent: event }, }); }, processPointerCancel: () => { dispatch({ type: 'POINTER_CANCEL', }); }, processKeyDown: (event, metadata) => { const key = event.key; if (key === 'Enter' || key === 'Escape' || key === ' ') { dispatch({ type: 'KEY_DOWN', payload: { key, metadata }, }); } }, processFocus: () => { dispatch({ type: 'FOCUS' }); }, processBlur: () => { dispatch({ type: 'BLUR' }); }, }; } //# sourceMappingURL=use-drag-handle-interaction-state.js.map