diff --git a/newIDE/app/src/EventsSheet/index.js b/newIDE/app/src/EventsSheet/index.js index 663cd4b84107..043c951df2cf 100644 --- a/newIDE/app/src/EventsSheet/index.js +++ b/newIDE/app/src/EventsSheet/index.js @@ -95,7 +95,10 @@ import { hasClipboardConditions, pasteInstructionsFromClipboardInInstructionsList, } from './ClipboardKind'; -import { useScreenType } from '../UI/Responsive/ScreenTypeMeasurer'; +import { + useScreenType, + type ScreenType, +} from '../UI/Responsive/ScreenTypeMeasurer'; import { type WindowSizeType, useResponsiveWindowSize, @@ -176,6 +179,7 @@ type Props = {| type ComponentProps = {| ...Props, windowSize: WindowSizeType, + screenType: ScreenType, authenticatedUser: AuthenticatedUser, preferences: Preferences, tutorials: ?Array, @@ -734,9 +738,7 @@ export class EventsSheetComponentWithoutHandle extends React.Component< } ); - // This is not a real hook. - // eslint-disable-next-line react-hooks/rules-of-hooks - const screenType = useScreenType(); + const screenType = this.props.screenType; if ( screenType !== 'touch' && (type === 'BuiltinCommonInstructions::Comment' || @@ -2323,13 +2325,11 @@ export class EventsSheetComponentWithoutHandle extends React.Component< tutorials, hotReloadPreviewButtonProps, windowSize, + screenType, highlightedAiGeneratedEventIds, } = this.props; if (!project) return null; - // eslint-disable-next-line react-hooks/rules-of-hooks - const screenType = useScreenType(); - const isFunctionOnlyCallingItself = scope.eventsFunctionsExtension && scope.eventsFunction && @@ -2860,6 +2860,7 @@ const EventsSheet = (props, ref) => { const leaderboardsManager = React.useContext(LeaderboardContext); const { windowSize } = useResponsiveWindowSize(); const shortcutMap = useShortcutMap(); + const screenType = useScreenType(); return ( { leaderboardsManager={leaderboardsManager} shortcutMap={shortcutMap} windowSize={windowSize} + screenType={screenType} highlightedAiGeneratedEventIds={highlightedAiGeneratedEventIds} {...props} /> diff --git a/newIDE/app/src/UI/Menu/MaterialUIMenuImplementation.js b/newIDE/app/src/UI/Menu/MaterialUIMenuImplementation.js index 4cdd08082cc4..5ad74e13f6d2 100644 --- a/newIDE/app/src/UI/Menu/MaterialUIMenuImplementation.js +++ b/newIDE/app/src/UI/Menu/MaterialUIMenuImplementation.js @@ -46,6 +46,19 @@ const styles = { }, }; +// MenuItem whose `dense` prop adapts to the current screen type. +// Defined as a function component so useScreenType() can be called properly. +const DenseMenuItem = React.forwardRef((props, ref) => { + const screenType = useScreenType(); + return ( + + ); +}); + // $FlowFixMe[missing-local-annot] const SubMenuItem = ({ item, buildFromTemplate }) => { // The invisible backdrop behind the submenu is either: @@ -132,8 +145,6 @@ const SubMenuItem = ({ item, buildFromTemplate }) => { }, 75); } - // This is not a real hook. - // eslint-disable-next-line react-hooks/rules-of-hooks const isTouchscreen = useScreenType() === 'touch'; return ( @@ -217,10 +228,6 @@ export default class MaterialUIMenuImplementation template: Array, forceUpdate?: () => void ): any { - // This is not a real hook. - // eslint-disable-next-line react-hooks/rules-of-hooks - const isTouchscreen = useScreenType() === 'touch'; - return template .map((item, id) => { if (item.visible === false) return null; @@ -233,8 +240,7 @@ export default class MaterialUIMenuImplementation return ; } else if (item.type === 'checkbox') { return ( - - + ); } else if (item.submenu) { return ( @@ -286,8 +292,7 @@ export default class MaterialUIMenuImplementation ); } else { return ( - { @@ -311,7 +316,7 @@ export default class MaterialUIMenuImplementation {accelerator} )} - + ); } }) diff --git a/newIDE/app/src/UI/Responsive/ScreenTypeMeasurer.js b/newIDE/app/src/UI/Responsive/ScreenTypeMeasurer.js index f3ae18049203..0f48cf2e6db6 100644 --- a/newIDE/app/src/UI/Responsive/ScreenTypeMeasurer.js +++ b/newIDE/app/src/UI/Responsive/ScreenTypeMeasurer.js @@ -3,40 +3,25 @@ import * as React from 'react'; export type ScreenType = 'normal' | 'touch'; -let userHasTouchedScreen = false; -let userHasMovedMouse = false; +// Module-level state shared across all hook instances. +// A single pointerdown listener detects the current input type and only +// notifies subscribers when the type actually changes (touch ↔ mouse/pen). +let _screenType: ScreenType = 'normal'; +const _listeners: Set<(type: ScreenType) => void> = new Set(); if (typeof window !== 'undefined') { - window.addEventListener( - 'touchstart', - function onFirstTouch() { - console.info('Touch detected, considering the screen as touch enabled.'); - userHasTouchedScreen = true; - window.removeEventListener('touchstart', onFirstTouch, false); - }, - false - ); - - // An event listener is added (and then removed at the first event triggering) and - // will determine if the user is on a device that uses a mouse. - // If the first pointermove event is not triggered by a mouse move, the device - // will never be considered as mouse-enabled. - // Note: mousemove cannot be used since browsers emulate the mouse movement when - // the screen is touched. - window.addEventListener( - 'pointermove', - function onPointerMove(event: PointerEvent) { - console.info('Pointer move detected.'); - if (event.pointerType === 'mouse') { - console.info( - 'Pointer type is mouse, considering the device is a desktop/laptop computer.' - ); - userHasMovedMouse = true; - } - window.removeEventListener('pointermove', onPointerMove, false); - }, - false - ); + window.addEventListener('pointerdown', (event: PointerEvent) => { + const newType: ScreenType = + event.pointerType === 'touch' ? 'touch' : 'normal'; + if (newType === _screenType) return; + console.info( + `Screen type changed from "${_screenType}" to "${newType}" (pointerType: "${ + event.pointerType + }").` + ); + _screenType = newType; + _listeners.forEach(fn => fn(newType)); + }); } type Props = {| @@ -50,21 +35,28 @@ export const ScreenTypeMeasurer = ({ children }: Props): React.Node => children(useScreenType()); /** - * Return if the screen is a touchscreen or not. + * Returns whether the screen is currently being used as a touchscreen or not. + * Dynamically switches when the user alternates between touch and mouse/pen, + * so hybrid devices (e.g. Windows touchscreen laptops) are handled correctly. */ export const useScreenType = (): ScreenType => { - // Note: this is not a React hook but is named as one to encourage - // components to use it as such, so that it could be reworked - // at some point to use a context (verify in this case all usages). - if (typeof window === 'undefined') return 'normal'; + const [screenType, setScreenType] = React.useState(_screenType); - return userHasTouchedScreen ? 'touch' : 'normal'; -}; + React.useEffect(() => { + // setScreenType is stable across renders, safe to store in the Set. + _listeners.add(setScreenType); + return () => { + _listeners.delete(setScreenType); + }; + }, []); -export const useShouldAutofocusInput = (): boolean => { - const isTouchscreen = useScreenType() === 'touch'; - // Whatever size the screen is, if a touch event has been detected, no autofocus should - // be triggered (that would annoyingly open the keyboard) unless a mouse move has been - // detected (in that case, the device should be a touch-enabled desktop/laptop computer). - return !(isTouchscreen && !userHasMovedMouse); + return screenType; }; + +/** + * Returns true if inputs should be auto-focused. + * No autofocus when the last interaction was touch, to avoid opening the + * on-screen keyboard unexpectedly. + */ +export const useShouldAutofocusInput = (): boolean => + useScreenType() !== 'touch';