|
import { useStore } from '@nanostores/react'; |
|
import { logger } from 'app/logging/logger'; |
|
import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; |
|
import type { Atom } from 'nanostores'; |
|
import { atom, computed } from 'nanostores'; |
|
import type { RefObject } from 'react'; |
|
import { useEffect } from 'react'; |
|
import { objectKeys } from 'tsafe'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const log = logger('system'); |
|
|
|
|
|
|
|
|
|
type FocusRegionName = 'gallery' | 'layers' | 'canvas' | 'workflows' | 'viewer'; |
|
|
|
|
|
|
|
|
|
const REGION_TARGETS: Record<FocusRegionName, Set<HTMLElement>> = { |
|
gallery: new Set<HTMLElement>(), |
|
layers: new Set<HTMLElement>(), |
|
canvas: new Set<HTMLElement>(), |
|
workflows: new Set<HTMLElement>(), |
|
viewer: new Set<HTMLElement>(), |
|
} as const; |
|
|
|
|
|
|
|
|
|
const $focusedRegion = atom<FocusRegionName | null>(null); |
|
|
|
|
|
|
|
|
|
const FOCUS_REGIONS = objectKeys(REGION_TARGETS).reduce( |
|
(acc, region) => { |
|
acc[`$${region}`] = computed($focusedRegion, (focusedRegion) => focusedRegion === region); |
|
return acc; |
|
}, |
|
{} as Record<`$${FocusRegionName}`, Atom<boolean>> |
|
); |
|
|
|
|
|
|
|
|
|
const setFocus = (region: FocusRegionName | null) => { |
|
$focusedRegion.set(region); |
|
log.trace(`Focus changed: ${region}`); |
|
}; |
|
|
|
type UseFocusRegionOptions = { |
|
focusOnMount?: boolean; |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const useFocusRegion = ( |
|
region: FocusRegionName, |
|
ref: RefObject<HTMLElement>, |
|
options?: UseFocusRegionOptions |
|
) => { |
|
useEffect(() => { |
|
if (!ref.current) { |
|
return; |
|
} |
|
|
|
const { focusOnMount = false } = { focusOnMount: false, ...options }; |
|
|
|
const element = ref.current; |
|
|
|
REGION_TARGETS[region].add(element); |
|
|
|
if (focusOnMount) { |
|
setFocus(region); |
|
} |
|
|
|
return () => { |
|
REGION_TARGETS[region].delete(element); |
|
|
|
if (REGION_TARGETS[region].size === 0 && $focusedRegion.get() === region) { |
|
setFocus(null); |
|
} |
|
}; |
|
}, [options, ref, region]); |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
export const useIsRegionFocused = (region: FocusRegionName) => { |
|
return useStore(FOCUS_REGIONS[`$${region}`]); |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const onFocus = (_: FocusEvent) => { |
|
const activeElement = document.activeElement; |
|
if (!(activeElement instanceof HTMLElement)) { |
|
return; |
|
} |
|
|
|
const regionCandidates: { region: FocusRegionName; element: HTMLElement }[] = []; |
|
|
|
for (const region of objectKeys(REGION_TARGETS)) { |
|
for (const element of REGION_TARGETS[region]) { |
|
if (element.contains(activeElement)) { |
|
regionCandidates.push({ region, element }); |
|
} |
|
} |
|
} |
|
|
|
if (regionCandidates.length === 0) { |
|
return; |
|
} |
|
|
|
|
|
regionCandidates.sort((a, b) => { |
|
if (b.element.contains(a.element)) { |
|
return -1; |
|
} |
|
if (a.element.contains(b.element)) { |
|
return 1; |
|
} |
|
return 0; |
|
}); |
|
|
|
|
|
const focusedRegion = regionCandidates[0]?.region; |
|
|
|
if (!focusedRegion) { |
|
log.warn('No focused region found'); |
|
return; |
|
} |
|
|
|
setFocus(focusedRegion); |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
export const useFocusRegionWatcher = () => { |
|
useAssertSingleton('useFocusRegionWatcher'); |
|
|
|
useEffect(() => { |
|
window.addEventListener('focus', onFocus, { capture: true }); |
|
return () => { |
|
window.removeEventListener('focus', onFocus, { capture: true }); |
|
}; |
|
}, []); |
|
}; |
|
|