From 22641b7493a5e342c89586db072673a9fb1a77d2 Mon Sep 17 00:00:00 2001 From: Davide Date: Thu, 18 May 2023 23:14:40 +0100 Subject: [PATCH] (demo/src/components/playground) Add color highlight for trapped elements --- .../src/components/playground/Playground.tsx | 5 ++- .../playground/demo-elements/DemoElements.tsx | 18 ++++---- .../playground/trap-controls/TrapControls.tsx | 42 ++++++++++++++++--- 3 files changed, 51 insertions(+), 14 deletions(-) diff --git a/apps/demo/src/components/playground/Playground.tsx b/apps/demo/src/components/playground/Playground.tsx index 0b60a57b..8f229b6f 100644 --- a/apps/demo/src/components/playground/Playground.tsx +++ b/apps/demo/src/components/playground/Playground.tsx @@ -19,9 +19,10 @@ const initialKeysState = { TrapControls: 0, ButtonControls: 0 }; export function Playground() { // Change components `key` to reset their states. const [keysState, dispatchKeys] = useReducer(keysReducer, initialKeysState); - const [selectedButtonIdState, setSelectedButtonIdState] = useState(''); const [showTrapControlsState, setShowTrapControlsState] = useState(true); const [demoElementsRootState, setDemoElementsRootState] = useState(); + const [rootsToHighlightState, setRootsToHighlightState] = useState([]); + const [selectedButtonIdState, setSelectedButtonIdState] = useState(''); const { skeletonState, skeletonButtonsIds, getSkeletonButtonById, patchSkeletonButton } = useSkeleton(); const demoElementsRootCallbackRef = (rootNodeRef: HTMLDivElement) => { @@ -35,6 +36,7 @@ export function Playground() { skeletonState={skeletonState} setSelectedButtonIdState={setSelectedButtonIdState} ref={demoElementsRootCallbackRef} + rootsToHighlightState={rootsToHighlightState} />
@@ -47,6 +49,7 @@ export function Playground() { demoElementsRootState={demoElementsRootState} dispatchKeys={dispatchKeys} displayComponent={showTrapControlsState} + setRootsToHighlightState={setRootsToHighlightState} /> >; + rootsToHighlightState: string[]; } export const DemoElements = forwardRef(function DemoElements( - { skeletonState, setSelectedButtonIdState }: DemoElementsProps, + { skeletonState, setSelectedButtonIdState, rootsToHighlightState }: DemoElementsProps, ref: React.ForwardedRef ) { const { id: rootId, children: rootChildren } = skeletonState; - // const handleButtonClick = (event) => "It's in the JSX for convinience" + const isAllToHighlight = rootsToHighlightState.includes(rootId); + + const isToHighlight = (parentId: string, id: string) => + isAllToHighlight || rootsToHighlightState.includes(parentId) || rootsToHighlightState.includes(id); return (
@@ -27,9 +31,9 @@ export const DemoElements = forwardRef(function DemoElements(
- + {group.id} {(group.children as SkeletonButton[]).map( @@ -44,9 +48,9 @@ export const DemoElements = forwardRef(function DemoElements( data-parent-id={parentId} disabled={disabled} onClick={() => setSelectedButtonIdState(id)} - className={`${ - display ? 'inline-block' : 'hidden' - } relative m-[1.8vw] h-[10vw] w-[10vw] rounded border border-indigo-500 text-sm font-medium text-gray-500 transition hover:scale-110 hover:shadow-xl focus:scale-110 focus:text-indigo-600 focus:shadow-xl focus:outline-none focus:ring-1 sm:m-3 sm:h-16 sm:w-16`} + className={`${display ? 'inline-block' : 'hidden'} ${ + isToHighlight(parentId, id) ? 'outline outline-4 outline-green-300 sm:outline-8' : 'outline-none' + } relative m-[1.8vw] h-[10vw] w-[10vw] rounded-md border-2 border-blue-700 bg-blue-100 text-sm font-medium text-gray-500 transition hover:scale-110 hover:shadow-xl focus:scale-110 focus:text-indigo-600 focus:shadow-xl focus:ring-1 sm:m-3 sm:h-16 sm:w-16`} > {id} {tabIndex} diff --git a/apps/demo/src/components/playground/trap-controls/TrapControls.tsx b/apps/demo/src/components/playground/trap-controls/TrapControls.tsx index 61e2f999..7e00f839 100644 --- a/apps/demo/src/components/playground/trap-controls/TrapControls.tsx +++ b/apps/demo/src/components/playground/trap-controls/TrapControls.tsx @@ -14,6 +14,7 @@ interface TrapControlsProps { demoElementsRootState: HTMLDivElement | undefined; dispatchKeys: React.Dispatch; displayComponent?: boolean; + setRootsToHighlightState: React.Dispatch>; } // Defined as a value rather than just a type so that we can loop through it to render the actions. @@ -58,16 +59,22 @@ const trapControlsReducer = (state: TrapControlsState, action: TrapControlsReduc return { ...state, trapConfig: { ...state.trapConfig, ...action } }; }; -export function TrapControls({ demoElementsRootState, dispatchKeys, displayComponent }: TrapControlsProps) { +export function TrapControls({ + demoElementsRootState, + dispatchKeys, + displayComponent, + setRootsToHighlightState, +}: TrapControlsProps) { const [demoElementsState, setDemoElementsState] = useState([]); const [{ trapAction, trapConfig }, dispatchTrapControlsState] = useReducer(trapControlsReducer, initialControlsState); const [initialFocusFilterState, setInitialFocusFilterState] = useState(false); + const [lastTrapEscape, setLastTrapEscape] = useState(false); - useEffect(() => { + if (demoElementsRootState && demoElementsRootState !== demoElementsState[0]) { // Filtering out nodes without an `id`, so it's trivial to add elements in `DemoElements.tsx` (just to improve the UX) // without having them (and their whole subtree) appear as options for renedered in this component. setDemoElementsState(getHTMLElementFlatSubTree(demoElementsRootState, (el) => !!el.id)); - }, [demoElementsRootState]); + } const rootsAndInitialFocusConfigValues = useMemo( () => ({ roots: trapConfig.roots, initialFocus: trapConfig.initialFocus }), @@ -85,17 +92,40 @@ export function TrapControls({ demoElementsRootState, dispatchKeys, displayCompo const handleReset = () => dispatchKeys('TrapControls'); const handleSubmit = (event: React.FormEvent) => { + if (!trapAction) return; + event.preventDefault(); - if (trapAction === 'BUILD') { + if (trapActionIsNotBuild) { + setRootsToHighlightState(trapAction === 'RESUME' ? trapConfig.roots : []); + + focusTrap(trapAction); + + return; + } + + setRootsToHighlightState(trapConfig.roots); + + setLastTrapEscape( focusTrap({ ...trapConfig, initialFocus: strToBoolOrItself(trapConfig.initialFocus), returnFocus: strToBoolOrItself(trapConfig.returnFocus), - }); - } else if (trapAction) focusTrap(trapAction); + }).escape + ); }; + useEffect(() => { + const escHandler = (event: KeyboardEvent) => { + if (event.key === 'Escape' || event.key === 'Esc' || event.keyCode === 27) { + if (lastTrapEscape) setRootsToHighlightState([]); + } + }; + + document.addEventListener('keydown', escHandler); + return () => document.removeEventListener('keydown', escHandler); + }, [lastTrapEscape, setRootsToHighlightState]); + return (