init commit,
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
import Popover from '@mui/material/Popover';
|
||||
import { listClasses } from '@mui/material/List';
|
||||
import { menuItemClasses } from '@mui/material/MenuItem';
|
||||
|
||||
import { Arrow } from './styles';
|
||||
import { calculateAnchorOrigin } from './utils';
|
||||
|
||||
import type { CustomPopoverProps } from './types';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export function CustomPopover({
|
||||
open,
|
||||
onClose,
|
||||
children,
|
||||
anchorEl,
|
||||
slotProps,
|
||||
...other
|
||||
}: CustomPopoverProps) {
|
||||
const { arrow: arrowProps, paper: paperProps, ...otherSlotProps } = slotProps ?? {};
|
||||
|
||||
const arrowSize = arrowProps?.size ?? 14;
|
||||
const arrowOffset = arrowProps?.offset ?? 17;
|
||||
const arrowPlacement = arrowProps?.placement ?? 'top-right';
|
||||
|
||||
const { paperStyles, anchorOrigin, transformOrigin } = calculateAnchorOrigin(arrowPlacement);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={!!open}
|
||||
anchorEl={anchorEl}
|
||||
onClose={onClose}
|
||||
anchorOrigin={anchorOrigin}
|
||||
transformOrigin={transformOrigin}
|
||||
slotProps={{
|
||||
...otherSlotProps,
|
||||
paper: {
|
||||
...paperProps,
|
||||
sx: [
|
||||
paperStyles,
|
||||
{
|
||||
overflow: 'inherit',
|
||||
[`& .${listClasses.root}`]: { minWidth: 140 },
|
||||
[`& .${menuItemClasses.root}`]: { gap: 2 },
|
||||
},
|
||||
...(Array.isArray(paperProps?.sx) ? (paperProps?.sx ?? []) : [paperProps?.sx]),
|
||||
],
|
||||
},
|
||||
}}
|
||||
{...other}
|
||||
>
|
||||
{!arrowProps?.hide && (
|
||||
<Arrow
|
||||
size={arrowSize}
|
||||
offset={arrowOffset}
|
||||
placement={arrowPlacement}
|
||||
sx={arrowProps?.sx}
|
||||
/>
|
||||
)}
|
||||
|
||||
{children}
|
||||
</Popover>
|
||||
);
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
export * from './custom-popover';
|
||||
|
||||
export type * from './types';
|
140
03_source/frontend/src/components/custom-popover/styles.tsx
Normal file
140
03_source/frontend/src/components/custom-popover/styles.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import type { Theme, CSSObject } from '@mui/material/styles';
|
||||
|
||||
import { varAlpha } from 'minimal-shared/utils';
|
||||
|
||||
import { styled } from '@mui/material/styles';
|
||||
|
||||
import type { PopoverArrow } from './types';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
const centerStyles: Record<string, CSSObject> = {
|
||||
hCenter: { left: 0, right: 0, margin: 'auto' },
|
||||
vCenter: { top: 0, bottom: 0, margin: 'auto' },
|
||||
};
|
||||
|
||||
const getRtlPosition = (position: 'left' | 'right', isRtl: boolean, value: number): CSSObject => ({
|
||||
[position]: isRtl ? 'auto' : value,
|
||||
[position === 'left' ? 'right' : 'left']: isRtl ? value : 'auto',
|
||||
});
|
||||
|
||||
const createBackgroundStyles = (theme: Theme, color: 'cyan' | 'red', size: number): CSSObject => {
|
||||
const colorChannel = theme.vars.palette[color === 'cyan' ? 'info' : 'error'].mainChannel;
|
||||
|
||||
return {
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundSize: `${size * 3}px ${size * 3}px`,
|
||||
backgroundColor: theme.vars.palette.background.paper,
|
||||
backgroundPosition: color === 'cyan' ? 'top right' : 'bottom left',
|
||||
backgroundImage: `linear-gradient(45deg, ${varAlpha(colorChannel, 0.1)}, ${varAlpha(colorChannel, 0.1)})`,
|
||||
};
|
||||
};
|
||||
|
||||
const arrowDirection: Record<string, CSSObject> = {
|
||||
top: { top: 0, rotate: '135deg', translate: '0 -50%' },
|
||||
bottom: { bottom: 0, rotate: '-45deg', translate: '0 50%' },
|
||||
left: { rotate: '45deg', translate: '-50% 0' },
|
||||
right: { rotate: '-135deg', translate: '50% 0' },
|
||||
};
|
||||
|
||||
export const Arrow = styled('span', {
|
||||
shouldForwardProp: (prop: string) => !['size', 'placement', 'offset', 'sx'].includes(prop),
|
||||
})<PopoverArrow>(({ offset = 0, size = 0, theme }) => {
|
||||
const isRtl = theme.direction === 'rtl';
|
||||
|
||||
const cyanBackgroundStyles = createBackgroundStyles(theme, 'cyan', size);
|
||||
const redBackgroundStyles = createBackgroundStyles(theme, 'red', size);
|
||||
|
||||
return {
|
||||
width: size,
|
||||
height: size,
|
||||
position: 'absolute',
|
||||
backdropFilter: '6px',
|
||||
borderBottomLeftRadius: size / 4,
|
||||
clipPath: 'polygon(0% 0%, 100% 100%, 0% 100%)',
|
||||
backgroundColor: theme.vars.palette.background.paper,
|
||||
border: `solid 1px ${varAlpha(theme.vars.palette.grey['500Channel'], 0.12)}`,
|
||||
...theme.applyStyles('dark', {
|
||||
border: `solid 1px ${varAlpha(theme.vars.palette.common.blackChannel, 0.12)}`,
|
||||
}),
|
||||
|
||||
variants: [
|
||||
/**
|
||||
* @position top*
|
||||
*/
|
||||
{
|
||||
props: ({ placement }) => placement?.startsWith('top-'),
|
||||
style: { ...arrowDirection.top },
|
||||
},
|
||||
{
|
||||
props: { placement: 'top-left' },
|
||||
style: { ...getRtlPosition('left', isRtl, offset) },
|
||||
},
|
||||
{
|
||||
props: { placement: 'top-center' },
|
||||
style: { ...centerStyles.hCenter },
|
||||
},
|
||||
{
|
||||
props: { placement: 'top-right' },
|
||||
style: { ...getRtlPosition('right', isRtl, offset), ...cyanBackgroundStyles },
|
||||
},
|
||||
/**
|
||||
* @position bottom*
|
||||
*/
|
||||
{
|
||||
props: ({ placement }) => placement?.startsWith('bottom-'),
|
||||
style: { ...arrowDirection.bottom },
|
||||
},
|
||||
{
|
||||
props: { placement: 'bottom-left' },
|
||||
style: { ...getRtlPosition('left', isRtl, offset), ...redBackgroundStyles },
|
||||
},
|
||||
{
|
||||
props: { placement: 'bottom-center' },
|
||||
style: { ...centerStyles.hCenter },
|
||||
},
|
||||
{
|
||||
props: { placement: 'bottom-right' },
|
||||
style: { ...getRtlPosition('right', isRtl, offset) },
|
||||
},
|
||||
/**
|
||||
* @position left*
|
||||
*/
|
||||
{
|
||||
props: ({ placement }) => placement?.startsWith('left-'),
|
||||
style: { ...getRtlPosition('left', isRtl, 0), ...arrowDirection.left },
|
||||
},
|
||||
{
|
||||
props: { placement: 'left-top' },
|
||||
style: { top: offset },
|
||||
},
|
||||
{
|
||||
props: { placement: 'left-center' },
|
||||
style: { ...centerStyles.vCenter, ...redBackgroundStyles },
|
||||
},
|
||||
{
|
||||
props: { placement: 'left-bottom' },
|
||||
style: { ...redBackgroundStyles, bottom: offset },
|
||||
},
|
||||
/**
|
||||
* @position right*
|
||||
*/
|
||||
{
|
||||
props: ({ placement }) => placement?.startsWith('right-'),
|
||||
style: { ...getRtlPosition('right', isRtl, 0), ...arrowDirection.right },
|
||||
},
|
||||
{
|
||||
props: { placement: 'right-top' },
|
||||
style: { ...cyanBackgroundStyles, top: offset },
|
||||
},
|
||||
{
|
||||
props: { placement: 'right-center' },
|
||||
style: { ...centerStyles.vCenter, ...cyanBackgroundStyles },
|
||||
},
|
||||
{
|
||||
props: { placement: 'right-bottom' },
|
||||
style: { bottom: offset },
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
32
03_source/frontend/src/components/custom-popover/types.ts
Normal file
32
03_source/frontend/src/components/custom-popover/types.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { PaperProps } from '@mui/material/Paper';
|
||||
import type { PopoverProps } from '@mui/material/Popover';
|
||||
import type { Theme, SxProps } from '@mui/material/styles';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export type PopoverArrow = {
|
||||
hide?: boolean;
|
||||
size?: number;
|
||||
offset?: number;
|
||||
sx?: SxProps<Theme>;
|
||||
placement?:
|
||||
| 'top-left'
|
||||
| 'top-center'
|
||||
| 'top-right'
|
||||
| 'bottom-left'
|
||||
| 'bottom-center'
|
||||
| 'bottom-right'
|
||||
| 'left-top'
|
||||
| 'left-center'
|
||||
| 'left-bottom'
|
||||
| 'right-top'
|
||||
| 'right-center'
|
||||
| 'right-bottom';
|
||||
};
|
||||
|
||||
export type CustomPopoverProps = PopoverProps & {
|
||||
slotProps?: PopoverProps['slotProps'] & {
|
||||
arrow?: PopoverArrow;
|
||||
paper?: PaperProps;
|
||||
};
|
||||
};
|
129
03_source/frontend/src/components/custom-popover/utils.ts
Normal file
129
03_source/frontend/src/components/custom-popover/utils.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import type { CSSObject } from '@mui/material/styles';
|
||||
import type { PopoverOrigin } from '@mui/material/Popover';
|
||||
|
||||
import type { PopoverArrow } from './types';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
const POPOVER_DISTANCE = 0.75;
|
||||
|
||||
export type CalculateAnchorOriginProps = {
|
||||
paperStyles?: CSSObject;
|
||||
anchorOrigin: PopoverOrigin;
|
||||
transformOrigin: PopoverOrigin;
|
||||
};
|
||||
|
||||
export function calculateAnchorOrigin(
|
||||
arrow: PopoverArrow['placement']
|
||||
): CalculateAnchorOriginProps {
|
||||
let props: CalculateAnchorOriginProps;
|
||||
|
||||
switch (arrow) {
|
||||
/**
|
||||
* top-*
|
||||
*/
|
||||
case 'top-left':
|
||||
props = {
|
||||
paperStyles: { ml: -POPOVER_DISTANCE },
|
||||
anchorOrigin: { vertical: 'bottom', horizontal: 'left' },
|
||||
transformOrigin: { vertical: 'top', horizontal: 'left' },
|
||||
};
|
||||
break;
|
||||
case 'top-center':
|
||||
props = {
|
||||
paperStyles: undefined,
|
||||
anchorOrigin: { vertical: 'bottom', horizontal: 'center' },
|
||||
transformOrigin: { vertical: 'top', horizontal: 'center' },
|
||||
};
|
||||
break;
|
||||
case 'top-right':
|
||||
props = {
|
||||
paperStyles: { ml: POPOVER_DISTANCE },
|
||||
anchorOrigin: { vertical: 'bottom', horizontal: 'right' },
|
||||
transformOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
};
|
||||
break;
|
||||
/**
|
||||
* bottom-*
|
||||
*/
|
||||
case 'bottom-left':
|
||||
props = {
|
||||
paperStyles: { ml: -POPOVER_DISTANCE },
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'left' },
|
||||
transformOrigin: { vertical: 'bottom', horizontal: 'left' },
|
||||
};
|
||||
break;
|
||||
case 'bottom-center':
|
||||
props = {
|
||||
paperStyles: undefined,
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'center' },
|
||||
transformOrigin: { vertical: 'bottom', horizontal: 'center' },
|
||||
};
|
||||
break;
|
||||
case 'bottom-right':
|
||||
props = {
|
||||
paperStyles: { ml: POPOVER_DISTANCE },
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
transformOrigin: { vertical: 'bottom', horizontal: 'right' },
|
||||
};
|
||||
break;
|
||||
/**
|
||||
* left-*
|
||||
*/
|
||||
case 'left-top':
|
||||
props = {
|
||||
paperStyles: { mt: -POPOVER_DISTANCE },
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
transformOrigin: { vertical: 'top', horizontal: 'left' },
|
||||
};
|
||||
break;
|
||||
case 'left-center':
|
||||
props = {
|
||||
paperStyles: undefined,
|
||||
anchorOrigin: { vertical: 'center', horizontal: 'right' },
|
||||
transformOrigin: { vertical: 'center', horizontal: 'left' },
|
||||
};
|
||||
break;
|
||||
case 'left-bottom':
|
||||
props = {
|
||||
paperStyles: { mt: POPOVER_DISTANCE },
|
||||
anchorOrigin: { vertical: 'bottom', horizontal: 'right' },
|
||||
transformOrigin: { vertical: 'bottom', horizontal: 'left' },
|
||||
};
|
||||
break;
|
||||
/**
|
||||
* right-*
|
||||
*/
|
||||
case 'right-top':
|
||||
props = {
|
||||
paperStyles: { mt: -POPOVER_DISTANCE },
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'left' },
|
||||
transformOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
};
|
||||
break;
|
||||
case 'right-center':
|
||||
props = {
|
||||
paperStyles: undefined,
|
||||
anchorOrigin: { vertical: 'center', horizontal: 'left' },
|
||||
transformOrigin: { vertical: 'center', horizontal: 'right' },
|
||||
};
|
||||
break;
|
||||
case 'right-bottom':
|
||||
props = {
|
||||
paperStyles: { mt: POPOVER_DISTANCE },
|
||||
anchorOrigin: { vertical: 'bottom', horizontal: 'left' },
|
||||
transformOrigin: { vertical: 'bottom', horizontal: 'right' },
|
||||
};
|
||||
break;
|
||||
|
||||
// top-right
|
||||
default:
|
||||
props = {
|
||||
paperStyles: { ml: POPOVER_DISTANCE },
|
||||
anchorOrigin: { vertical: 'bottom', horizontal: 'right' },
|
||||
transformOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
};
|
||||
}
|
||||
|
||||
return props;
|
||||
}
|
Reference in New Issue
Block a user