material-ui
material-ui copied to clipboard
[Divider] Responsive orientation
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.
This would be cool to implement, not seeing many downsides.
Would love to see this implemented as well.
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>
)
}
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} />;
}
Still looking forward to this!
Any updates on this issue? Its still not fixed.
waiting for this, also for the calendar ahahhahaha
Any updates on this, to have different orientation in different breakpoints
How is this not a thing yet?
Would love to see this
Crap I thought this was the Nextui page haha
Hey maintainers
is this issue ready to take ?
+1 -- with RSC this would be very useful!
workaround:
<Divider
sx={{ borderBottomWidth: { xs: 'thin', md: 0 }, borderRightWidth: { xs: 0, md: 'thin' } }}
/>
(horz on mobile, vertical on desktop)
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',
}}
/>