mui / material-ui

Material UI: Comprehensive React component library that implements Google's Material Design. Free forever.
https://mui.com/material-ui/
MIT License
94.03k stars 32.3k forks source link

[joy-ui] Add `useMediaQuery` hook #35037

Open scherroman opened 2 years ago

scherroman commented 2 years ago

Duplicates

Latest version

Summary 💡

I'd like to be able to import and use useMediaQuery with Joy UI just like Material UI supports to adapt my layout for different screen sizes

Examples 🌈

import { Button, IconButton, useMediaQuery } from '@mui/joy'
import SettingsIcon from '@mui/icons-material/Settings'

const NavigationBar = () => {
    let isScreenMobile = useMediaQuery((theme) =>
        theme.breakpoints.down('mobile')
    )

    return (
        <div>
            {isScreenMobile ? (
                <Button startDecorator={<SettingsIcon />}>
                    Settings
                </Button>
            ) : (
                <IconButton>
                    <SettingsIcon />
                </IconButton>
            )}
        </div>
    )
}

Motivation 🔦

I'm trying to adapt my interface to have my navigation bar show just an IconButton when on a tablet or larger sized screen, but a larger regular Button with an icon when on a mobile sized screen. At the moment I appear to be unable to do so without something like useMediaQuery which has access to my theme's media breakpoints, which makes the app less friendly and functional on mobile. While the sx prop is very useful for adapting individual components, it's common to want to render entirely different components at different breakpoints.

For reference, my breakpoints are set to:

mobile: 0,
tablet: 640,
laptop: 1024,
desktop: 1200

Many well-designed web apps like Twitter have elements like side bars that collapse, disappear completely (like the "What's happening" bar on the righthand side) or are replaced by new layouts (like the navigation bar on the lefthand side, which collapses at smaller sizes and is wholly replaced by a bottom navigation bar on mobile). This kind of adaptability is essential for designing a responsive/mobile-friendly app.

If there's a workaround in the meantime I'd be happy to learn it! I love the aesthetic of Joy UI much more than Material UI 2, so am eager and making an effort to use it although it's still in alpha.

scherroman commented 2 years ago

Dug around some more and found the Display page of MUI System which recommends using the sx breakpoints to hide and show elements at different screen sizes using the display property. Makes sense and works well! Also would work better than a hook if using server-side rendering. Though if you use display: block to show buttons it messes up their styling. Tried to just omit the breakpoints where the component is visible as it's redundant, but that causes the button not to show, so had to look in the browser console to find that normally display: inline-flex is used and explicitly use that. Would be nice to have a way to just use the default or set display to 'whatever the value should be/would have been if I wasn't setting it'

<IconButton
    sx={{
        display: { mobile: 'none', tablet: 'inline-flex' }
    }}
>
    <SettingsIcon />
</IconButton>
<Button
    startDecorator={<SettingsIcon />}
    sx={{
        display: { mobile: 'inline-flex', tablet: 'none' }
    }}
>
    Settings
</Button>
scherroman commented 2 years ago

Reopening this as I've run into a situation where I want to adapt a modal from layout='fullscreen' when on mobile to layout='center' when on tablet or larger, which is not covered by the use of breakpoints on the sx property. Passing in an object with breakpoints like <ModalDialog layout={{ mobile: 'fullscreen', tablet: 'center' }}> doesn't appear to work either.

It seems the useMediaQuery hook is needed to support this. In the meantime, I've gotten around this by using the useMedia hook from react-use:

import { useMedia } from 'react-use'

import {
    Modal,
    ModalDialog,
    useTheme
} from '@mui/joy'

const EditModal = () => {
    let theme = useTheme()
    let isMobile = useMedia(`(max-width: ${theme.breakpoints.values.tablet}px)`)

    return (
        <Modal open onClose={onClose}>
            <ModalDialog layout={isMobile ? 'fullscreen' : 'center'}>
            </ModalDialog>
        </Modal>
    )
}
siriwatknp commented 1 year ago

Honestly, I don't recommend using JS to switch between layouts. Always use CSS if you can.

This is better for sure:

<Modal open onClose={onClose}>
    <ModalDialog layout="center" sx={theme => ({
      [theme.breakpoints.only('xs')]: {
        top: 0,
        left: 0,
        right: 0,
        bottom: 0,
        border: 0,
        borderRadius: 0,
        transform: 'initial',
      }
    })}>
    </ModalDialog>
</Modal>

I am changing this issue to a question instead.

siriwatknp commented 1 year ago

After reading the original issue, it is not about the Modal. I will try to add some docs about the media query to Joy UI.

scherroman commented 1 year ago

Honestly, I don't recommend using JS to switch between layouts. Always use CSS if you can.

This is better for sure:

<Modal open onClose={onClose}>
    <ModalDialog layout="center" sx={theme => ({
      [theme.breakpoints.only('xs')]: {
        top: 0,
        left: 0,
        right: 0,
        bottom: 0,
        border: 0,
        borderRadius: 0,
        transform: 'initial',
      }
    })}>
    </ModalDialog>
</Modal>

Thanks for showing that! Though are there any reasons to prefer using css over js aside from css better supporting server-side rendering? It does seem like a lost opportunity to have to directly override all the styles already applied by a property like layout to replicate a different layout at certain breakpoints, instead of being able to simply change the value of the property itself with something like layout={{ mobile: 'fullscreen', tablet: 'center' }} or some similar way.

siriwatknp commented 1 year ago

Though are there any reasons to prefer using css over js aside from css better supporting server-side rendering?

I think CSS is simpler, and framework agnostic. This is just one small example but in a production app, the more JS you have the more complexity you have to handle as well.

layout={{ mobile: 'fullscreen', tablet: 'center' }}

I have been thinking about providing style utilities in Joy UI to make it easier for developers in situations like this:

import ModalDialog, { getLayoutFullscreenStyle } from '@mui/joy/ModalDialog';

<ModalDialog
  layout="center"
  sx={theme => ({
    [theme.breakpoints.only('xs')]: getLayoutFullscreenStyle(),
  })}
/>

I think this will benefit the developer's productivity and allows us to create tailor-made styles that won't impact production bundle size if you don't use them.

What do you think?

scherroman commented 1 year ago

I have been thinking about providing style utilities in Joy UI to make it easier for developers in situations like this:

import ModalDialog, { getLayoutFullscreenStyle } from '@mui/joy/ModalDialog';

<ModalDialog
  layout="center"
  sx={theme => ({
    [theme.breakpoints.only('xs')]: getLayoutFullscreenStyle(),
  })}
/>

I think this will benefit the developer's productivity and allows us to create tailor-made styles that won't impact production bundle size if you don't use them.

That would be a big improvement over having to manually specify the styles as it's simpler, clearer, and less mental overhead/error-prone in determining which styles to override.

Personally however I love the simplicity, conciseness, and explicitness of being able to optionally pass in an object with breakpoints to properties the most, similar to how the Stack and Grid components from MUI System allow for some properties:

<Stack direction={{ xs: 'column', sm: 'row' }} spacing={{ xs: 1, sm: 2, md: 4 }}>

<Grid container spacing={{ xs: 2, md: 3 }} columns={{ xs: 4, sm: 8, md: 12 }}>

Not sure if this is more difficult to achieve for higher-level properties, or about its impact on bundle size and the best way to optimize there, but my biggest consideration would be for the fluidity and expressiveness of the API. Perhaps the two methods are not mutually exclusive

justintoman commented 1 year ago

I'm trying to render a table with fewer columns on smaller screens and I can't set colSpan dynamically with css/sx, so I need useMediaQuery or to not use a table. Joy supports <Table> with native <td> / <th>, so I think it could make sense to add this feature as well.

Studio384 commented 1 year ago

Is there any progress on this? It would be great if hooks like these aren't specific to Material (or Joy for that matter). So far all use cases mention here have been related to width/height/dimensions, but I personally also have various use-cases for this hook that depend on capabilities of hover, the pointer type, whether or not its a screen, etc.

crusaider commented 1 year ago

Another use case where at least I can not make use of the sx prop is the Drawer component. I have a need to be able to anchor the drawer to the left side of the screen on desktop but to the right side on mobile. I can not see a easy way of doing that without giving the Drawer component different values of the the anchor prop.

siriwatknp commented 1 year ago

The useMediaQuery can be moved to @mui/system to be shared across Material UI and Joy UI.

Does anyone want to take this?

justintoman commented 1 year ago

@siriwatknp I can try!

I'm thinking move the hook into@mui/system then re-export it from @mui/material to prevent breaking changes.

Sen-442b commented 11 months ago

Can we not use the useMediaQuery hook from @mui/material using experimental_extendTheme?

AlanGreyjoy commented 1 month ago

Create your own hook

import { useEffect, useState } from 'react'

const useMediaQuery = query => {
  const [matches, setMatches] = useState(false)

  useEffect(() => {
    const mediaQuery = window.matchMedia(query)
    setMatches(mediaQuery.matches)

    const handler = event => setMatches(event.matches)
    mediaQuery.addEventListener('change', handler)

    return () => mediaQuery.removeEventListener('change', handler)
  }, [query])

  return matches
}

export default useMediaQuery

In a file

import useMediaQuery from 'somewhere/in/your/project/useMediaQuery';

const isMdOrSmaller = useMediaQuery('(max-width: 900px)')
console.log('isMdOrSmaller', isMdOrSmaller)