1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-25 11:28:51 +01:00
metamask-extension/ui/components/component-library/box/box.tsx

235 lines
8.3 KiB
TypeScript

import React from 'react';
import classnames from 'classnames';
import { memoize } from 'lodash';
import { BREAKPOINTS } from '../../../helpers/constants/design-system';
import type {
BoxComponent,
BoxProps,
PolymorphicRef,
StyleDeclarationType,
StylePropValueType,
ClassNamesObject,
} from './box.types';
const BASE_CLASS_NAME = 'mm-box';
function isValidSize(
styleProp: StyleDeclarationType,
value: StylePropValueType,
) {
// Only margin types allow 'auto'
return (
typeof value === 'number' ||
((styleProp === 'margin' ||
styleProp === 'margin-top' ||
styleProp === 'margin-right' ||
styleProp === 'margin-bottom' ||
styleProp === 'margin-left' ||
styleProp === 'margin-inline' ||
styleProp === 'margin-inline-start' ||
styleProp === 'margin-inline-end') &&
value === 'auto')
);
}
function isValidString(type: StyleDeclarationType, value: StylePropValueType) {
return typeof type === 'string' && typeof value === 'string';
}
/**
* Generate classnames
* Generates classnames for different utility styles
* Also accepts responsive props in the form of an array
* Maps responsive props to mobile first breakpoints
*
* @param {string} styleDeclaration - The style declaration type "margin", "margin-top", "padding", "display" etc
* @param {array || number || string} value - prop value being passed in array props are responsive props
* @param {*} validatorFn - The validation function for each type of value
* @returns
*/
const generateClassNames = memoize(
(
styleDeclaration: StyleDeclarationType,
value: StylePropValueType,
validatorFn: typeof isValidString | typeof isValidSize,
) => {
// if value does not exist return empty object for classnames library
// Accepts 0 as a valid value
if (!value && typeof value !== 'number') {
return {};
}
const classNamesObject: ClassNamesObject = {};
// if value is an array with single item e.g. marginTop={[1]}
const singleArrayItemProp =
Array.isArray(value) && value.length === 1 ? value[0] : undefined;
// if value single value e.g. marginTop={1}
const singleValueProp =
(!Array.isArray(value) && typeof value === 'string') ||
typeof value === 'number'
? value
: undefined;
// single digit equals single value or single array item
let singleValue;
if (singleValueProp || singleValueProp === 0) {
singleValue = singleValueProp;
}
if (singleArrayItemProp || singleArrayItemProp === 0) {
singleValue = singleArrayItemProp;
}
// 0 is an acceptable value but is falsy in js
if (singleValue || singleValue === 0) {
// add base style without any breakpoint prefixes to classObject
classNamesObject[
`${BASE_CLASS_NAME}--${styleDeclaration}-${singleValue}`
] = validatorFn(styleDeclaration, singleValue);
} else if (Array.isArray(value)) {
// If array with more than one item
switch (value.length) {
case 4:
// add base/sm/md/lg
classNamesObject[
`${BASE_CLASS_NAME}--${styleDeclaration}-${value[0]}`
] = validatorFn(styleDeclaration, value[0]);
classNamesObject[
`${BASE_CLASS_NAME}--${BREAKPOINTS[1]}:${styleDeclaration}-${value[1]}`
] = validatorFn(styleDeclaration, value[1]);
classNamesObject[
`${BASE_CLASS_NAME}--${BREAKPOINTS[2]}:${styleDeclaration}-${value[2]}`
] = validatorFn(styleDeclaration, value[2]);
classNamesObject[
`${BASE_CLASS_NAME}--${BREAKPOINTS[3]}:${styleDeclaration}-${value[3]}`
] = validatorFn(styleDeclaration, value[3]);
break;
case 3:
// add base/sm/md
classNamesObject[
`${BASE_CLASS_NAME}--${styleDeclaration}-${value[0]}`
] = validatorFn(styleDeclaration, value[0]);
classNamesObject[
`${BASE_CLASS_NAME}--${BREAKPOINTS[1]}:${styleDeclaration}-${value[1]}`
] = validatorFn(styleDeclaration, value[1]);
classNamesObject[
`${BASE_CLASS_NAME}--${BREAKPOINTS[2]}:${styleDeclaration}-${value[2]}`
] = validatorFn(styleDeclaration, value[2]);
break;
case 2:
// add base/sm
classNamesObject[
`${BASE_CLASS_NAME}--${styleDeclaration}-${value[0]}`
] = validatorFn(styleDeclaration, value[0]);
classNamesObject[
`${BASE_CLASS_NAME}--${BREAKPOINTS[1]}:${styleDeclaration}-${value[1]}`
] = validatorFn(styleDeclaration, value[1]);
break;
default:
console.log(`Invalid array prop length: ${value.length}`);
}
}
return classNamesObject;
},
(styleDeclaration, value) => `${styleDeclaration}${value}`,
);
export const Box: BoxComponent = React.forwardRef(
<C extends React.ElementType = 'div'>(
{
as,
padding,
paddingTop,
paddingRight,
paddingBottom,
paddingLeft,
paddingInline,
paddingInlineStart,
paddingInlineEnd,
margin,
marginTop,
marginRight,
marginBottom,
marginLeft,
marginInline,
marginInlineStart,
marginInlineEnd,
borderColor,
borderWidth,
borderRadius,
borderStyle,
alignItems,
justifyContent,
textAlign,
flexDirection,
flexWrap,
gap,
display,
width,
height,
children,
className = '',
backgroundColor,
color,
...props
}: BoxProps<C>,
ref?: PolymorphicRef<C>,
) => {
const Component = as || 'div';
const boxClassName = classnames(
BASE_CLASS_NAME,
className,
// Margin
generateClassNames('margin', margin, isValidSize),
generateClassNames('margin-top', marginTop, isValidSize),
generateClassNames('margin-right', marginRight, isValidSize),
generateClassNames('margin-bottom', marginBottom, isValidSize),
generateClassNames('margin-left', marginLeft, isValidSize),
generateClassNames('margin-inline', marginInline, isValidSize),
generateClassNames('margin-inline-start', marginInlineStart, isValidSize),
generateClassNames('margin-inline-end', marginInlineEnd, isValidSize),
// Padding
generateClassNames('padding', padding, isValidSize),
generateClassNames('padding-top', paddingTop, isValidSize),
generateClassNames('padding-right', paddingRight, isValidSize),
generateClassNames('padding-bottom', paddingBottom, isValidSize),
generateClassNames('padding-left', paddingLeft, isValidSize),
generateClassNames('padding-inline', paddingInline, isValidSize),
generateClassNames(
'padding-inline-start',
paddingInlineStart,
isValidSize,
),
generateClassNames('padding-inline-end', paddingInlineEnd, isValidSize),
generateClassNames('display', display, isValidString),
generateClassNames('gap', gap, isValidSize),
generateClassNames('flex-direction', flexDirection, isValidString),
generateClassNames('flex-wrap', flexWrap, isValidString),
generateClassNames('justify-content', justifyContent, isValidString),
generateClassNames('align-items', alignItems, isValidString),
generateClassNames('text-align', textAlign, isValidString),
generateClassNames('width', width, isValidString),
generateClassNames('height', height, isValidString),
generateClassNames('color', color, isValidString),
generateClassNames('background-color', backgroundColor, isValidString),
generateClassNames('rounded', borderRadius, isValidString),
generateClassNames('border-style', borderStyle, isValidString),
generateClassNames('border-color', borderColor, isValidString),
generateClassNames('border-width', borderWidth, isValidSize),
{
// Auto applied classes
// ---Borders---
// if borderWidth or borderColor is supplied w/o style, default to solid
'box--border-style-solid':
!borderStyle && (Boolean(borderWidth) || Boolean(borderColor)),
// if borderColor supplied w/o width, default to 1
'box--border-width-1': !borderWidth && Boolean(borderColor),
},
);
return (
<Component className={boxClassName} ref={ref} {...props}>
{children}
</Component>
);
},
);