diff --git a/src/js/reducers/device.js b/src/js/reducers/device.js
index 081e24ca..d1bd245c 100644
--- a/src/js/reducers/device.js
+++ b/src/js/reducers/device.js
@@ -1,9 +1,6 @@
import { TRIGGER_USER_TYPE_CHANGE, TRIGGER_RESIZE_VIEWPORT, PREFERENCE_CHANGE } from '../constants/actions.js';
import { getScrollbarWidth, pick } from 'web-common/utils';
-const isInitiallyMouse = typeof(matchMedia) === 'function' ? matchMedia('(pointer:fine)').matches : null;
-const isInitiallyTouch = typeof(matchMedia) === 'function' ? matchMedia('(pointer:coarse)').matches : null;
-
const getViewport = ({ width }) => {
return {
xxs: width < 480,
@@ -14,19 +11,25 @@ const getViewport = ({ width }) => {
};
};
-const defaultState = {
- isKeyboardUser: false,
- isMouseUser: isInitiallyMouse,
- isSingleColumn: false,
- isTouchOrSmall: false,
- isTouchUser: isInitiallyTouch,
- scrollbarWidth: getScrollbarWidth(),
- shouldUseEditMode: false,
- shouldUseModalCreatorField: false,
- shouldUseSidebar: false,
- shouldUseTabs: false,
- userType: isInitiallyTouch ? 'touch' : 'mouse',
- ...getViewport(process.env.NODE_ENV === 'test' ? {} : window.innerWidth)
+const getDefaultState = () => {
+ const isWindows = navigator.userAgent.indexOf("Windows") >= 0;
+ const isInitiallyMouse = typeof (window.matchMedia) === 'function' ? (window.matchMedia('(pointer:fine)').matches || (isWindows && window.matchMedia('(any-pointer:fine)'))) : null;
+ const isInitiallyTouch = !isInitiallyMouse && (typeof(window.matchMedia) === 'function' ? window.matchMedia('(pointer:coarse)').matches : null);
+
+ return {
+ isKeyboardUser: false,
+ isMouseUser: isInitiallyMouse,
+ isSingleColumn: false,
+ isTouchOrSmall: false,
+ isTouchUser: isInitiallyTouch,
+ scrollbarWidth: getScrollbarWidth(),
+ shouldUseEditMode: false,
+ shouldUseModalCreatorField: false,
+ shouldUseSidebar: false,
+ shouldUseTabs: false,
+ userType: isInitiallyTouch ? 'touch' : 'mouse',
+ ...getViewport(process.env.NODE_ENV === 'test' ? {} : window.innerWidth)
+ }
};
const getDevice = (userType, viewport, { isEmbedded } = {}) => {
@@ -65,7 +68,7 @@ const getUserTypeBooleans = (state, action, userType) => {
};
}
-const device = (state = defaultState, action, { config, preferences } = {}) => {
+const device = (state = getDefaultState(), action, { config, preferences } = {}) => {
switch(action.type) {
case PREFERENCE_CHANGE:
case TRIGGER_RESIZE_VIEWPORT:
diff --git a/test/mocks/matchmedia.js b/test/mocks/matchmedia.js
new file mode 100644
index 00000000..154f3f53
--- /dev/null
+++ b/test/mocks/matchmedia.js
@@ -0,0 +1,34 @@
+export function mockMatchMedia({ isTouch = false, isMouse = true, isPrimaryTouch = null, isPrimaryMouse = null }) {
+ isPrimaryTouch = isPrimaryTouch ?? isTouch;
+ isPrimaryMouse = isPrimaryMouse ?? isMouse;
+
+ Object.defineProperty(window, 'matchMedia', {
+ writable: true,
+ value: jest.fn().mockImplementation((query) => {
+ const queryResult = {
+ matches: false,
+ media: query,
+ onchange: null,
+ addListener: jest.fn(), // deprecated
+ removeListener: jest.fn(), // deprecated
+ addEventListener: jest.fn(),
+ removeEventListener: jest.fn(),
+ dispatchEvent: jest.fn(),
+ };
+
+ if (query === '(pointer:fine)') {
+ return { ...queryResult, matches: isPrimaryMouse };
+ }
+ if (query === '(any-pointer:fine)') {
+ return { ...queryResult, matches: isMouse };
+ }
+ if (query === '(pointer:coarse)') {
+ return { ...queryResult, matches: isPrimaryTouch };
+ }
+ if (query === '(any-pointer:coarse)') {
+ return { ...queryResult, matches: isTouch };
+ }
+ return { ...queryResult, matches: false };
+ }),
+ });
+}
diff --git a/test/usert-type.test.jsx b/test/user-type.test.jsx
similarity index 69%
rename from test/usert-type.test.jsx
rename to test/user-type.test.jsx
index 8b84845e..e69fe36c 100644
--- a/test/usert-type.test.jsx
+++ b/test/user-type.test.jsx
@@ -8,10 +8,11 @@ import { act, fireEvent, getByRole, screen, waitFor } from '@testing-library/rea
import userEvent from '@testing-library/user-event'
import { renderWithProviders } from './utils/render';
-import { JSONtoState } from './utils/state';
+import { JSONtoState, getStateWithout } from './utils/state';
import { MainZotero } from '../src/js/component/main';
-import { applyAdditionalJestTweaks } from './utils/common';
+import { applyAdditionalJestTweaks, waitForPosition } from './utils/common';
import stateLibraryView from './fixtures/state/desktop-test-user-library-view.json';
+import { mockMatchMedia } from './mocks/matchmedia';
const libraryViewState = JSONtoState(stateLibraryView);
applyAdditionalJestTweaks();
@@ -37,6 +38,7 @@ describe('User Type', () => {
afterEach(() => {
server.resetHandlers()
localStorage.clear();
+ delete window.matchMedia;
});
afterAll(() => server.close());
@@ -124,4 +126,34 @@ describe('User Type', () => {
await userEvent.selectOptions(densityComboBox, 'Mouse');
expect(window.document.documentElement).toHaveClass('mouse');
});
+
+ test("It should opt for touch interface on device that reports touch pointer", async () => {
+ mockMatchMedia({ isTouch: true, isMouse: false });
+ renderWithProviders(, { preloadedState: getStateWithout(libraryViewState, 'device') });
+ await waitForPosition();
+ expect(window.document.documentElement).toHaveClass('touch');
+ });
+
+ test("It should opt for mouse interface on device that reports mouse pointer", async () => {
+ mockMatchMedia({ isTouch: false, isMouse: true });
+ renderWithProviders(, { preloadedState: getStateWithout(libraryViewState, 'device') });
+ await waitForPosition();
+ expect(window.document.documentElement).toHaveClass('mouse');
+ });
+
+ test("It should opt for mouse interface on Windows device that reports both touch and mouse pointers", async () => {
+ jest.spyOn(window.navigator, 'userAgent', 'get').mockReturnValue('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3');
+ mockMatchMedia({ isTouch: true, isMouse: true, isPrimaryMouse: false, isPrimaryTouch: true });
+ renderWithProviders(, { preloadedState: getStateWithout(libraryViewState, 'device') });
+ await waitForPosition();
+ expect(window.document.documentElement).toHaveClass('mouse');
+ });
+
+ test("It should opt for touch interface on iOS device that reports both touch and mouse pointers", async () => {
+ jest.spyOn(window.navigator, 'userAgent', 'get').mockReturnValue('Mozilla/5.0 (iPad; CPU OS 17_7_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Mobile/15E148 Safari/604.1');
+ mockMatchMedia({ isTouch: true, isMouse: true, isPrimaryMouse: false, isPrimaryTouch: true });
+ renderWithProviders(, { preloadedState: getStateWithout(libraryViewState, 'device') });
+ await waitForPosition();
+ expect(window.document.documentElement).toHaveClass('touch');
+ });
});