material-ui icon indicating copy to clipboard operation
material-ui copied to clipboard

[Divider] Responsive orientation

Open kevineaton603 opened this issue 4 years ago • 8 comments

Duplicates

  • [X] I have searched the existing issues

Latest version

  • [X] I have tested the latest version

Summary 💡

Currently the Divider component can only consume horizontal | vertical.

I would like to see it use the ResponsiveStyleValue as a possible option for the orientation property.

It should work something like this...

<Divider
  orientation={{ xs: 'horizontal', sm: 'vertical' }}
/>

Examples 🌈

No response

Motivation 🔦

I want to use a responsive Divider in conjunction with the Stack component as the Stack component can change directions responsively.

Using the components together would look something like this...

<Stack
  direction={{ xs: 'column', sm: 'row' }}
  alignItems={'center'}
  justifyContent={'center'}
  spacing={2}
  divider={(
    <Divider
      orientation={{ xs: 'horizontal', sm: 'vertical' }}
      flexItem={true}
    />
)}
>
{children}
</Stack>

I think that this features would be very useful not just to me but others developers as they start using the Stack component. I am sure that I won't be the only one to run into these problem in the future.

kevineaton603 avatar Nov 26 '21 16:11 kevineaton603

This would be cool to implement, not seeing many downsides.

aleccaputo avatar Apr 28 '22 19:04 aleccaputo

Would love to see this implemented as well.

dwgr8ergolfer avatar Apr 28 '22 21:04 dwgr8ergolfer

This would be very nice to see indeed. Until then, the snippet below works to achieve similar results

const Component = ({children}) => {
  const theme = useTheme();

  return (
    <Stack
      direction={{ xs: 'column', sm: 'row' }}
      alignItems={'center'}
      justifyContent={'center'}
      spacing={2}
      divider={(
        <Divider
          orientation={useMediaQuery(theme.breakpoints.down("md")) ? "horizontal" : "vertical"}
          flexItem={true}
        />
    )}
    >
    {children}
    </Stack>
  )
}

bjornpijnacker avatar May 11 '22 08:05 bjornpijnacker

I have created a wrapper for the Divider using logic similar to the MUI breakpoint utilities(https://github.com/mui/material-ui/issues/29864). This enables me to use the prop exactly like the direction prop. As you can see I have used "react-singleton-hook. If you have a custom theme (specifically custom breakpoints), you must ensure you add <SingletonHooksContainer /> inside the context of your theme provider.

// src/hooks/useCurrentBreakpoint/index.ts

import { useTheme } from "@mui/material";
 import { Breakpoint } from "@mui/system";
 import { useEffect, useState } from "react";
 import { singletonHook } from "react-singleton-hook";

 // https://github.com/Light-Keeper/react-singleton-hook/issues/406#issuecomment-962282765
 // eslint-disable-next-line no-underscore-dangle
 export function _useCurrentBreakpoint(): Breakpoint {
   const globalTheme = useTheme();

   const mqs: [Breakpoint, string][] = globalTheme.breakpoints.keys.map(
     (key, index, breakpoints) => {
       let mq = "";
       if (index === breakpoints.length - 1) {
         mq = globalTheme.breakpoints.up(key);
       } else {
         mq = globalTheme.breakpoints.between(key, breakpoints[index + 1]);
       }
       return [key, mq.replace(/^@media( ?)/m, "")];
     }
   );
   const [currentBreakpoint, setCurrentBreakpoint] = useState<Breakpoint>(() => {
     const bp = mqs.find(([, mq]) => window.matchMedia(mq).matches);
     return bp ? bp[0] : "xs";
   });

   useEffect(() => {
     function handleCurrentBreakpointChange(
       key: Breakpoint,
       e: MediaQueryListEvent
     ) {
       if (e.matches) {
         setCurrentBreakpoint(key);
       }
     }

     const handlers: [string, (e: MediaQueryListEvent) => void][] = mqs.map(
       ([key, mq]) => {
         const handler = (e: MediaQueryListEvent) =>
           handleCurrentBreakpointChange(key, e);
         return [mq, handler];
       }
     );

     handlers.forEach(([mq, handler]) => {
       window.matchMedia(mq).addEventListener("change", handler);
     });

     return () => {
       handlers.forEach(([mq, handler]) => {
         window.matchMedia(mq).removeEventListener("change", handler);
       });
     };
   }, [mqs]);

   return currentBreakpoint;
 }

 const useCurrentBreakpoint = singletonHook("xs", _useCurrentBreakpoint);

 export { useCurrentBreakpoint };
// src/components/ResponsiveDivider/index.tsx

import { useTheme } from "@mui/material";
 import { Breakpoint, ResponsiveStyleValue } from "@mui/system";

 function isBPValueAnObject<T>(
   breakpointValues: ResponsiveStyleValue<T>
 ): breakpointValues is Record<Breakpoint, T | null> {
   return (
     typeof breakpointValues === "object" && !Array.isArray(breakpointValues)
   );
 }

 export function useResolveAllBreakpoints<T>(
   breakpointValues: ResponsiveStyleValue<T>
 ): Record<Breakpoint, T> {
   const bpKeys = useTheme().breakpoints.keys;
   const bpsProvided = (() => {
     if (typeof breakpointValues !== "object") {
       return [];
     }

     let b: Breakpoint[] = [];

     if (Array.isArray(breakpointValues)) {
       b = breakpointValues.map((_, i) => bpKeys[i]);
     } else {
       b = Object.entries(breakpointValues as Record<Breakpoint, T | null>)
         .filter(([k, v]) => bpKeys.includes(k as Breakpoint) && v != null)
         .map(([k]) => k as Breakpoint);
     }
     return b;
   })();
   if (bpsProvided.length === 0) {
     return Object.fromEntries(
       bpKeys.map((k) => [k, breakpointValues as T])
     ) as Record<Breakpoint, T>;
   }

   let previous: Breakpoint | number;

   return bpKeys.reduce(
     (acc: Partial<Record<Breakpoint, unknown>>, breakpoint, i) => {
       if (Array.isArray(breakpointValues)) {
         if (breakpointValues[i] != null) {
           acc[breakpoint] = breakpointValues[i];
           previous = i;
         } else {
           acc[breakpoint] = breakpointValues[previous as number];
         }
       } else if (isBPValueAnObject(breakpointValues)) {
         if (breakpointValues[breakpoint] != null) {
           acc[breakpoint] = breakpointValues[breakpoint];
           previous = breakpoint;
         } else {
           acc[breakpoint] = breakpointValues[previous as Breakpoint];
         }
       } else {
         acc[breakpoint] = breakpointValues;
       }
       return acc;
     },
     {}
   ) as Record<Breakpoint, T>;
 }
// src/hooks/useResolveAllBreakpoints/index.ts

import { Divider, DividerProps } from "@mui/material";
import { ResponsiveStyleValue } from "@mui/system";

import { useCurrentBreakpoint } from "src/hooks/useCurrentBreakpoint";
import { useResolveAllBreakpoints } from "src/hooks/useResolveAllBreakpoints";

export function ResponsiveDivider({
 orientation,
 ...props
}: {
 orientation: ResponsiveStyleValue<"horizontal" | "vertical">;
} & Omit<DividerProps, "orientation">): JSX.Element {
 const currentBreakpoint = useCurrentBreakpoint();

 const bpValues = useResolveAllBreakpoints(orientation);

 const currentOrientation = bpValues[currentBreakpoint];

 return <Divider {...props} orientation={currentOrientation} />;
}

Zach-Jaensch avatar Mar 17 '23 05:03 Zach-Jaensch

Still looking forward to this!

Melancholism avatar Nov 06 '23 12:11 Melancholism

Any updates on this issue? Its still not fixed.

sourabratabose avatar Mar 03 '24 08:03 sourabratabose

waiting for this, also for the calendar ahahhahaha

pablojsx avatar Apr 04 '24 21:04 pablojsx

Any updates on this, to have different orientation in different breakpoints

Linuhusainnk avatar Aug 28 '24 07:08 Linuhusainnk

How is this not a thing yet?

korydondzila avatar Sep 16 '24 15:09 korydondzila

Would love to see this

mleister97 avatar Dec 22 '24 17:12 mleister97

Crap I thought this was the Nextui page haha

pablojsx avatar Dec 23 '24 17:12 pablojsx

Hey maintainers is this issue ready to take ?

yash49 avatar Dec 24 '24 12:12 yash49

+1 -- with RSC this would be very useful!

mr-rpl avatar Feb 21 '25 20:02 mr-rpl

workaround:

<Divider
  sx={{ borderBottomWidth: { xs: 'thin', md: 0 }, borderRightWidth: { xs: 0, md: 'thin' } }}
/>

(horz on mobile, vertical on desktop)

mr-rpl avatar Feb 21 '25 20:02 mr-rpl

I found myself doing the above too often, I created a local <Divider /> component:

'use client'

import MuiDivider, { type DividerProps as MuiDividerProps } from '@mui/material/Divider'
import { useMemo } from 'react'

type DividerProps = Omit<MuiDividerProps, 'orientation'> & {
  orientation?: MuiDividerProps['orientation'] | Record<string, MuiDividerProps['orientation']>
}

type SxProps = NonNullable<DividerProps['sx']>

const Divider = (props: DividerProps) => {
  const { orientation = 'horizontal', sx, ...dividerProps } = props

  const sxProps = useMemo((): SxProps => {
    if (typeof orientation === 'string') {
      return {
        borderBottomWidth: orientation === 'vertical' ? 0 : 'thin',
        borderRightWidth: orientation === 'vertical' ? 'thin' : 0,
      }
    }

    const borderBottomWidth: Record<string, string> = {}
    const borderRightWidth: Record<string, string> = {}

    for (const [breakpoint, value] of Object.entries(orientation)) {
      borderBottomWidth[breakpoint] = value === 'vertical' ? '0' : 'thin'
      borderRightWidth[breakpoint] = value === 'vertical' ? 'thin' : '0'
    }

    return Object.assign(
      {
        borderBottomWidth,
        borderRightWidth,
      },
      sx
    )
  }, [orientation])

  return <MuiDivider {...dividerProps} sx={sxProps} />
}

export { Divider, type DividerProps }

usage:

<Divider
  flexItem
  orientation={{
    xs: 'horizontal',
    md: 'vertical',
  }}
/>

mr-rpl avatar Apr 18 '25 23:04 mr-rpl