diff --git a/invokeai/frontend/web/src/app/components/GlobalHookIsolator.tsx b/invokeai/frontend/web/src/app/components/GlobalHookIsolator.tsx index b8b11287c12..c2cdde228d3 100644 --- a/invokeai/frontend/web/src/app/components/GlobalHookIsolator.tsx +++ b/invokeai/frontend/web/src/app/components/GlobalHookIsolator.tsx @@ -8,6 +8,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useFocusRegionWatcher } from 'common/hooks/focus'; import { useCloseChakraTooltipsOnDragFix } from 'common/hooks/useCloseChakraTooltipsOnDragFix'; import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys'; +import { useTouchDeviceClass } from 'common/hooks/useTouchDeviceClass'; import { useDndMonitor } from 'features/dnd/useDndMonitor'; import { useDynamicPromptsWatcher } from 'features/dynamicPrompts/hooks/useDynamicPromptsWatcher'; import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterModelsToast'; @@ -42,6 +43,7 @@ export const GlobalHookIsolator = memo(() => { useGetOpenAPISchemaQuery(); useSyncLoggingConfig(); useCloseChakraTooltipsOnDragFix(); + useTouchDeviceClass(); useDndMonitor(); useSyncNodeErrors(); useSyncLangDirection(); diff --git a/invokeai/frontend/web/src/app/components/touchDevice.css b/invokeai/frontend/web/src/app/components/touchDevice.css index 4753e2a9a8c..7a66951c61a 100644 --- a/invokeai/frontend/web/src/app/components/touchDevice.css +++ b/invokeai/frontend/web/src/app/components/touchDevice.css @@ -1,7 +1,5 @@ -/* Hide tooltips on touch devices where hover gets "stuck" */ -@media (hover: none) { - [role='tooltip'] { - visibility: hidden !important; - opacity: 0 !important; - } +/* Hide tooltips after touch input, where hover can get stuck. */ +.invokeai-touch-device [role='tooltip'] { + visibility: hidden !important; + opacity: 0 !important; } diff --git a/invokeai/frontend/web/src/app/components/touchDevice.test.ts b/invokeai/frontend/web/src/app/components/touchDevice.test.ts new file mode 100644 index 00000000000..69f3835828e --- /dev/null +++ b/invokeai/frontend/web/src/app/components/touchDevice.test.ts @@ -0,0 +1,15 @@ +import { readFileSync } from 'node:fs'; + +import { describe, expect, it } from 'vitest'; + +const css = readFileSync(new URL('./touchDevice.css', import.meta.url), 'utf8'); + +describe('touchDevice.css', () => { + it('hides tooltips only after touch input has been detected', () => { + expect(css).toMatch(/\.invokeai-touch-device\s+\[role='tooltip'\]\s*{/); + }); + + it('does not force all tooltips invisible', () => { + expect(css).not.toMatch(/@media\s*\([^)]*hover[^)]*\)/); + }); +}); diff --git a/invokeai/frontend/web/src/common/hooks/useTouchDeviceClass.ts b/invokeai/frontend/web/src/common/hooks/useTouchDeviceClass.ts new file mode 100644 index 00000000000..574d9ac4693 --- /dev/null +++ b/invokeai/frontend/web/src/common/hooks/useTouchDeviceClass.ts @@ -0,0 +1,23 @@ +import { useEffect } from 'react'; + +const TOUCH_DEVICE_CLASS = 'invokeai-touch-device'; + +export const useTouchDeviceClass = () => { + useEffect(() => { + const onPointerInput = (e: PointerEvent) => { + if (e.pointerType === 'touch') { + document.documentElement.classList.add(TOUCH_DEVICE_CLASS); + } else if (e.pointerType === 'mouse') { + document.documentElement.classList.remove(TOUCH_DEVICE_CLASS); + } + }; + + window.addEventListener('pointerdown', onPointerInput, { passive: true }); + window.addEventListener('pointermove', onPointerInput, { passive: true }); + + return () => { + window.removeEventListener('pointerdown', onPointerInput); + window.removeEventListener('pointermove', onPointerInput); + }; + }, []); +};