-
Notifications
You must be signed in to change notification settings - Fork 4
feat: Add useMobile hook and breakpoints #173
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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]; | ||
| 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); | ||
| }); |
| 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)`); | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 If, for any reason, in the very worst case, the call to |
||
| } | ||
|
|
||
| export const useMobile = createSingletonState<boolean>({ | ||
| initialState: () => getIsMobile(), | ||
| factory: handler => { | ||
| const listener = () => handler(getIsMobile()); | ||
| window.addEventListener('resize', listener); | ||
| return () => { | ||
| window.removeEventListener('resize', listener); | ||
| }; | ||
| }, | ||
| }); | ||
| 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) { | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moved from |
||
| try { | ||
| const targetWindow = element.ownerDocument?.defaultView ?? window; | ||
| return targetWindow.matchMedia?.(query).matches ?? false; | ||
| } catch (error) { | ||
| console.warn(error); | ||
| return false; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
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
useMobileneeds themobileBreakpoint.