Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions newIDE/app/src/EventsSheet/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -176,6 +179,7 @@ type Props = {|
type ComponentProps = {|
...Props,
windowSize: WindowSizeType,
screenType: ScreenType,
authenticatedUser: AuthenticatedUser,
preferences: Preferences,
tutorials: ?Array<Tutorial>,
Expand Down Expand Up @@ -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' ||
Expand Down Expand Up @@ -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 &&
Expand Down Expand Up @@ -2860,6 +2860,7 @@ const EventsSheet = (props, ref) => {
const leaderboardsManager = React.useContext(LeaderboardContext);
const { windowSize } = useResponsiveWindowSize();
const shortcutMap = useShortcutMap();
const screenType = useScreenType();
return (
<EventsSheetComponentWithoutHandle
ref={component}
Expand All @@ -2869,6 +2870,7 @@ const EventsSheet = (props, ref) => {
leaderboardsManager={leaderboardsManager}
shortcutMap={shortcutMap}
windowSize={windowSize}
screenType={screenType}
highlightedAiGeneratedEventIds={highlightedAiGeneratedEventIds}
{...props}
/>
Expand Down
29 changes: 17 additions & 12 deletions newIDE/app/src/UI/Menu/MaterialUIMenuImplementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<any, any>((props, ref) => {
const screenType = useScreenType();
return (
<MenuItem
dense={!!electron || screenType !== 'touch'}
ref={ref}
{...props}
/>
);
});

// $FlowFixMe[missing-local-annot]
const SubMenuItem = ({ item, buildFromTemplate }) => {
// The invisible backdrop behind the submenu is either:
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -217,10 +228,6 @@ export default class MaterialUIMenuImplementation
template: Array<MenuItemTemplate>,
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;
Expand All @@ -233,8 +240,7 @@ export default class MaterialUIMenuImplementation
return <Divider key={'separator' + id} style={styles.divider} />;
} else if (item.type === 'checkbox') {
return (
<MenuItem
dense={!!electron || !isTouchscreen}
<DenseMenuItem
key={'checkbox' + item.label}
checked={
// $FlowFixMe[incompatible-type] - existence should be inferred by Flow.
Expand Down Expand Up @@ -272,7 +278,7 @@ export default class MaterialUIMenuImplementation
)}
</ListItemIcon>
<ListItemText primary={item.label} />
</MenuItem>
</DenseMenuItem>
);
} else if (item.submenu) {
return (
Expand All @@ -286,8 +292,7 @@ export default class MaterialUIMenuImplementation
);
} else {
return (
<MenuItem
dense={!!electron || !isTouchscreen}
<DenseMenuItem
key={'item' + item.label}
disabled={item.enabled === false}
onClick={e => {
Expand All @@ -311,7 +316,7 @@ export default class MaterialUIMenuImplementation
<span style={styles.accelerator}>{accelerator}</span>
</div>
)}
</MenuItem>
</DenseMenuItem>
);
}
})
Expand Down
82 changes: 37 additions & 45 deletions newIDE/app/src/UI/Responsive/ScreenTypeMeasurer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {|
Expand All @@ -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>(_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';
Loading