Skip to content
Merged
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: 16 additions & 0 deletions src/internal/breakpoints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

export type Breakpoint = 'default' | 'xxs' | 'xs' | 's' | 'm' | 'l' | 'xl';

const BREAKPOINT_MAPPING: [Breakpoint, number][] = [
['xl', 1840],
['l', 1320],
['m', 1120],
['s', 912],
['xs', 688],
['xxs', 465],
['default', -1],
];

export const mobileBreakpoint = BREAKPOINT_MAPPING.filter(b => b[0] === 'xs')[0][1];
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is also necessary to extract the breakpoints because useMobile needs the mobileBreakpoint.

57 changes: 57 additions & 0 deletions src/internal/use-mobile/_tests__/use-mobile.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import React, { useRef } from 'react';
import { act, render } from '@testing-library/react';

import { useMobile } from '../index';

function Demo() {
const renderCount = useRef(0);
const isMobile = useMobile();
renderCount.current++;
return (
<div>
<span data-testid="mobile">{String(isMobile)}</span>
<span data-testid="render-count">{renderCount.current}</span>
</div>
);
}

function resizeWindow(width: number) {
act(() => {
Object.defineProperty(window, 'innerWidth', { value: width });
window.dispatchEvent(new CustomEvent('resize'));
});
}

test('should report mobile width on the initial render', () => {
resizeWindow(400);
const { getByTestId } = render(<Demo />);
expect(getByTestId('mobile').textContent).toBe('true');
});

test('should report desktop width on the initial render', () => {
resizeWindow(1200);
const { getByTestId } = render(<Demo />);
expect(getByTestId('mobile').textContent).toBe('false');
});

test('should report the updated value after resize', () => {
resizeWindow(400);
const { getByTestId } = render(<Demo />);
const countBefore = getByTestId('render-count').textContent;
resizeWindow(1200);
const countAfter = getByTestId('render-count').textContent;
expect(getByTestId('mobile').textContent).toBe('false');
expect(countBefore).not.toEqual(countAfter);
});

test('no renders when resize does not hit the breakpoint', () => {
resizeWindow(1000);
const { getByTestId } = render(<Demo />);
const countBefore = getByTestId('render-count').textContent;
resizeWindow(1200);
const countAfter = getByTestId('render-count').textContent;
expect(countBefore).toEqual(countAfter);
});
42 changes: 42 additions & 0 deletions src/internal/use-mobile/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { createSingletonState } from '../singleton-handler';

import { mobileBreakpoint } from '../breakpoints';
import { safeMatchMedia } from '../utils/safe-match-media';

export const forceMobileModeSymbol = Symbol.for('awsui-force-mobile-mode');

function getIsMobile() {
// allow overriding the mobile mode in tests
// any is needed because of this https://github.com/microsoft/TypeScript/issues/36813
const forceMobileMode = (globalThis as any)[forceMobileModeSymbol];
if (typeof forceMobileMode !== 'undefined') {
return forceMobileMode;
}
if (typeof window === 'undefined') {
// assume desktop in server-rendering
return false;
}

/**
* Some browsers include the scrollbar width in their media query calculations, but
* some browsers don't. Thus we can't use `window.innerWidth` or
* `document.documentElement.clientWidth` to get a very accurate result (since we
* wouldn't know which one of them to use).
* Instead, we use the media query here in JS too.
*/
return safeMatchMedia(document.body, `(max-width: ${mobileBreakpoint}px)`);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small difference with the existing implementation in components —I am not considering the case where window.matchMedia is falsy —TypeScript told me it is not possible and browser support quite agrees.

If, for any reason, in the very worst case, the call to matchMedia errors out, the error will be caught in safeMatchMedia.

}

export const useMobile = createSingletonState<boolean>({
initialState: () => getIsMobile(),
factory: handler => {
const listener = () => handler(getIsMobile());
window.addEventListener('resize', listener);
return () => {
window.removeEventListener('resize', listener);
};
},
});
12 changes: 12 additions & 0 deletions src/internal/utils/safe-match-media.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

export function safeMatchMedia(element: HTMLElement, query: string) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved from src/internal/visual-mode/index.ts so that the new useMobile hook can use it too

try {
const targetWindow = element.ownerDocument?.defaultView ?? window;
return targetWindow.matchMedia?.(query).matches ?? false;
} catch (error) {
console.warn(error);
return false;
}
}
11 changes: 1 addition & 10 deletions src/internal/visual-mode/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,7 @@ import { useStableCallback } from '../stable-callback';
import { isDevelopment } from '../is-development';
import { warnOnce } from '../logging';
import { awsuiVisualRefreshFlag, getGlobal } from '../global-flags';

function safeMatchMedia(element: HTMLElement, query: string) {
try {
const targetWindow = element.ownerDocument?.defaultView ?? window;
return targetWindow.matchMedia?.(query).matches ?? false;
} catch (error) {
console.warn(error);
return false;
}
}
import { safeMatchMedia } from '../utils/safe-match-media';

export function isMotionDisabled(element: HTMLElement): boolean {
return (
Expand Down
Loading