diff --git a/docs/src/modules/components/AppTableOfContents.js b/docs/src/modules/components/AppTableOfContents.js index 63449b633d4c35..88cbfb8f539ba7 100644 --- a/docs/src/modules/components/AppTableOfContents.js +++ b/docs/src/modules/components/AppTableOfContents.js @@ -122,7 +122,7 @@ function useThrottledOnScroll(callback, delay) { React.useEffect(() => { if (throttledCallback === noop) { - return () => {}; + return undefined; } window.addEventListener('scroll', throttledCallback); diff --git a/packages/material-ui/src/useMediaQuery/useMediaQuery.js b/packages/material-ui/src/useMediaQuery/useMediaQuery.js index 09daf44e3c2425..c48c73330f665b 100644 --- a/packages/material-ui/src/useMediaQuery/useMediaQuery.js +++ b/packages/material-ui/src/useMediaQuery/useMediaQuery.js @@ -12,10 +12,16 @@ function useMediaQuery(queryInput, options = {}) { let queries = multiple ? queryInput : [queryInput]; queries = queries.map(query => query.replace('@media ', '')); + // We are waiting for JSDOM to support the match media feature. + // All the targetd browsers have the feature built-in. + // This defensive check is here for simplicity. + // Most of the time, the match media logic isn't central to people tests. + const supportMatchMedia = typeof window.matchMedia !== 'undefined'; + const { defaultMatches = false, noSsr = false, ssrMatchMedia = null } = options; const [matches, setMatches] = React.useState(() => { - if (hydrationCompleted || noSsr) { + if ((hydrationCompleted || noSsr) && supportMatchMedia) { return queries.map(query => window.matchMedia(query).matches); } if (ssrMatchMedia) { @@ -30,6 +36,10 @@ function useMediaQuery(queryInput, options = {}) { React.useEffect(() => { hydrationCompleted = true; + if (!supportMatchMedia) { + return undefined; + } + const queryLists = queries.map(query => window.matchMedia(query)); setMatches(prev => { const next = queryLists.map(queryList => queryList.matches); diff --git a/packages/material-ui/src/useMediaQuery/useMediaQuery.test.js b/packages/material-ui/src/useMediaQuery/useMediaQuery.test.js index eef9e394437452..b89fdcbcf26655 100644 --- a/packages/material-ui/src/useMediaQuery/useMediaQuery.test.js +++ b/packages/material-ui/src/useMediaQuery/useMediaQuery.test.js @@ -48,33 +48,117 @@ describe('useMediaQuery', () => { mount = createMount({ strict: true }); }); - beforeEach(() => { - testReset(); - values = spy(); - matchMediaInstances = []; - window.matchMedia = createMatchMedia(1200, matchMediaInstances); - }); - after(() => { mount.cleanUp(); }); - describe('option: defaultMatches', () => { - it('should be false by default', () => { + describe('without feature', () => { + it('should work without window.matchMedia available', () => { + assert.strictEqual(typeof window.matchMedia, 'undefined'); const ref = React.createRef(); const text = () => ref.current.textContent; const Test = () => { - const matches = useMediaQuery('(min-width:2000px)'); - React.useEffect(() => values(matches)); + const matches = useMediaQuery('(min-width:100px)'); return {`${matches}`}; }; mount(); assert.strictEqual(text(), 'false'); - assert.strictEqual(values.callCount, 1); }); + }); + + describe('with feature', () => { + beforeEach(() => { + testReset(); + values = spy(); + matchMediaInstances = []; + window.matchMedia = createMatchMedia(1200, matchMediaInstances); + }); + + describe('option: defaultMatches', () => { + it('should be false by default', () => { + const ref = React.createRef(); + const text = () => ref.current.textContent; + const Test = () => { + const matches = useMediaQuery('(min-width:2000px)'); + React.useEffect(() => values(matches)); + return {`${matches}`}; + }; + + mount(); + assert.strictEqual(text(), 'false'); + assert.strictEqual(values.callCount, 1); + }); + + it('should take the option into account', () => { + const ref = React.createRef(); + const text = () => ref.current.textContent; + const Test = () => { + const matches = useMediaQuery('(min-width:2000px)', { + defaultMatches: true, + }); + React.useEffect(() => values(matches)); + return {`${matches}`}; + }; + + mount(); + assert.strictEqual(text(), 'false'); + assert.strictEqual(values.callCount, 2); + }); + }); + + describe('option: noSsr', () => { + it('should render once if the default value match the expectation', () => { + const ref = React.createRef(); + const text = () => ref.current.textContent; + const Test = () => { + const matches = useMediaQuery('(min-width:2000px)', { + defaultMatches: false, + }); + React.useEffect(() => values(matches)); + return {`${matches}`}; + }; + + mount(); + assert.strictEqual(text(), 'false'); + assert.strictEqual(values.callCount, 1); + }); + + it('should render twice if the default value does not match the expectation', () => { + const ref = React.createRef(); + const text = () => ref.current.textContent; + const Test = () => { + const matches = useMediaQuery('(min-width:2000px)', { + defaultMatches: true, + }); + React.useEffect(() => values(matches)); + return {`${matches}`}; + }; + + mount(); + assert.strictEqual(text(), 'false'); + assert.strictEqual(values.callCount, 2); + }); - it('should take the option into account', () => { + it('should render once if the default value does not match the expectation', () => { + const ref = React.createRef(); + const text = () => ref.current.textContent; + const Test = () => { + const matches = useMediaQuery('(min-width:2000px)', { + defaultMatches: true, + noSsr: true, + }); + React.useEffect(() => values(matches)); + return {`${matches}`}; + }; + + mount(); + assert.strictEqual(text(), 'false'); + assert.strictEqual(values.callCount, 1); + }); + }); + + it('should try to reconcile only the first time', () => { const ref = React.createRef(); const text = () => ref.current.textContent; const Test = () => { @@ -88,125 +172,58 @@ describe('useMediaQuery', () => { mount(); assert.strictEqual(text(), 'false'); assert.strictEqual(values.callCount, 2); - }); - }); - describe('option: noSsr', () => { - it('should render once if the default value match the expectation', () => { - const ref = React.createRef(); - const text = () => ref.current.textContent; - const Test = () => { - const matches = useMediaQuery('(min-width:2000px)', { - defaultMatches: false, - }); - React.useEffect(() => values(matches)); - return {`${matches}`}; - }; + ReactDOM.unmountComponentAtNode(mount.attachTo); mount(); assert.strictEqual(text(), 'false'); - assert.strictEqual(values.callCount, 1); + assert.strictEqual(values.callCount, 3); }); - it('should render twice if the default value does not match the expectation', () => { + it('should be able to change the query dynamically', () => { const ref = React.createRef(); const text = () => ref.current.textContent; - const Test = () => { - const matches = useMediaQuery('(min-width:2000px)', { + const Test = props => { + const matches = useMediaQuery(props.query, { defaultMatches: true, }); React.useEffect(() => values(matches)); return {`${matches}`}; }; + Test.propTypes = { + query: PropTypes.string.isRequired, + }; - mount(); + const wrapper = mount(); assert.strictEqual(text(), 'false'); assert.strictEqual(values.callCount, 2); + wrapper.setProps({ query: '(min-width:100px)' }); + assert.strictEqual(text(), 'true'); + assert.strictEqual(values.callCount, 4); }); - it('should render once if the default value does not match the expectation', () => { + it('should observe the media query', () => { const ref = React.createRef(); const text = () => ref.current.textContent; - const Test = () => { - const matches = useMediaQuery('(min-width:2000px)', { - defaultMatches: true, - noSsr: true, - }); + const Test = props => { + const matches = useMediaQuery(props.query); React.useEffect(() => values(matches)); return {`${matches}`}; }; + Test.propTypes = { + query: PropTypes.string.isRequired, + }; - mount(); - assert.strictEqual(text(), 'false'); + mount(); assert.strictEqual(values.callCount, 1); - }); - }); - - it('should try to reconcile only the first time', () => { - const ref = React.createRef(); - const text = () => ref.current.textContent; - const Test = () => { - const matches = useMediaQuery('(min-width:2000px)', { - defaultMatches: true, - }); - React.useEffect(() => values(matches)); - return {`${matches}`}; - }; - - mount(); - assert.strictEqual(text(), 'false'); - assert.strictEqual(values.callCount, 2); - - ReactDOM.unmountComponentAtNode(mount.attachTo); - - mount(); - assert.strictEqual(text(), 'false'); - assert.strictEqual(values.callCount, 3); - }); + assert.strictEqual(text(), 'false'); - it('should be able to change the query dynamically', () => { - const ref = React.createRef(); - const text = () => ref.current.textContent; - const Test = props => { - const matches = useMediaQuery(props.query, { - defaultMatches: true, + act(() => { + matchMediaInstances[0].instance.matches = true; + matchMediaInstances[0].listeners[0](); }); - React.useEffect(() => values(matches)); - return {`${matches}`}; - }; - Test.propTypes = { - query: PropTypes.string.isRequired, - }; - - const wrapper = mount(); - assert.strictEqual(text(), 'false'); - assert.strictEqual(values.callCount, 2); - wrapper.setProps({ query: '(min-width:100px)' }); - assert.strictEqual(text(), 'true'); - assert.strictEqual(values.callCount, 4); - }); - - it('should observe the media query', () => { - const ref = React.createRef(); - const text = () => ref.current.textContent; - const Test = props => { - const matches = useMediaQuery(props.query); - React.useEffect(() => values(matches)); - return {`${matches}`}; - }; - Test.propTypes = { - query: PropTypes.string.isRequired, - }; - - mount(); - assert.strictEqual(values.callCount, 1); - assert.strictEqual(text(), 'false'); - - act(() => { - matchMediaInstances[0].instance.matches = true; - matchMediaInstances[0].listeners[0](); + assert.strictEqual(text(), 'true'); + assert.strictEqual(values.callCount, 2); }); - assert.strictEqual(text(), 'true'); - assert.strictEqual(values.callCount, 2); }); });