diff --git a/components/box/__tests__/index.test.tsx b/components/box/__tests__/index.test.tsx new file mode 100644 index 000000000..c84403e27 --- /dev/null +++ b/components/box/__tests__/index.test.tsx @@ -0,0 +1,60 @@ +import React from 'react' +import { mount } from 'enzyme' +import { Box } from 'components' + +describe('Button', () => { + it('should render correctly', () => { + const wrapper = mount(Box) + expect(() => wrapper.unmount()).not.toThrow() + }) + + it('should render as the provided element', async () => { + const wrapper = mount( + + Box + , + ) + + expect(wrapper.exists('a[href="https://geist.com"]')).toBe(true) + expect(() => wrapper.unmount()).not.toThrow() + }) + + it('should render with provided styles', async () => { + document.body.innerHTML = '
' + const wrapper = mount( + + Box + , + { + attachTo: document.querySelector('#root') as HTMLDivElement, + }, + ) + + expect(wrapper.find('div').getDOMNode()).toHaveStyle({ + display: 'block', + lineHeight: '100px', + fontSize: 'calc(1 * 16px)', + width: '300px', + height: '100px', + margin: '2rem 0px 2rem 0px', + visibility: 'visible', + padding: '0px 2rem 0px 2rem', + }) + expect(() => wrapper.unmount()).not.toThrow() + }) + + it('filter out scale related props', () => { + const wrapper = mount(Box) + + expect(wrapper.exists('div')).toBe(true) + expect(wrapper.getDOMNode().hasAttribute('px')).toBe(false) + expect(() => wrapper.unmount()).not.toThrow() + }) + + it('should forward the provided ref', () => { + const ref = React.createRef() + const wrapper = mount(Box) + expect(wrapper.find('div').getDOMNode()).toEqual(ref.current) + expect(() => wrapper.unmount()).not.toThrow() + }) +}) diff --git a/components/box/box.tsx b/components/box/box.tsx new file mode 100644 index 000000000..17126155a --- /dev/null +++ b/components/box/box.tsx @@ -0,0 +1,120 @@ +import React from 'react' +import { DynamicScales, makeScaleHandler, ScaleProps } from '../use-scale' +import useClasses from '../use-classes' +import useTheme from '../use-theme' + +type PropsOf> = + JSX.LibraryManagedAttributes> + +export interface BoxOwnProps { + as?: E +} + +export type BoxProps = BoxOwnProps & + Omit, keyof (BoxOwnProps & ScaleProps)> & + ScaleProps + +const defaultElement = 'div' + +export type BoxComponent = { + ( + props: BoxProps, + ): React.ReactElement | null + displayName?: string +} + +export const Box: BoxComponent = React.forwardRef( + ( + { as, children, className, ...restProps }: BoxProps, + ref: typeof restProps.ref | null, + ) => { + const Element = as || defaultElement + const { layout } = useTheme() + const { + paddingLeft, + pl, + paddingRight, + pr, + paddingTop, + pt, + paddingBottom, + pb, + marginTop, + mt, + marginRight, + mr, + marginBottom, + mb, + marginLeft, + ml, + px, + py, + mx, + my, + width, + height, + font, + w, + h, + margin, + padding, + unit = layout.unit, + scale = 1, + ...innerProps + } = restProps + + const SCALES: DynamicScales = { + pt: makeScaleHandler(paddingTop ?? pt ?? py ?? padding, scale, unit), + pr: makeScaleHandler(paddingRight ?? pr ?? px ?? padding, scale, unit), + pb: makeScaleHandler(paddingBottom ?? pb ?? py ?? padding, scale, unit), + pl: makeScaleHandler(paddingLeft ?? pl ?? px ?? padding, scale, unit), + px: makeScaleHandler( + px ?? paddingLeft ?? paddingRight ?? pl ?? pr ?? padding, + scale, + unit, + ), + py: makeScaleHandler( + py ?? paddingTop ?? paddingBottom ?? pt ?? pb ?? padding, + scale, + unit, + ), + mt: makeScaleHandler(marginTop ?? mt ?? my ?? margin, scale, unit), + mr: makeScaleHandler(marginRight ?? mr ?? mx ?? margin, scale, unit), + mb: makeScaleHandler(marginBottom ?? mb ?? my ?? margin, scale, unit), + ml: makeScaleHandler(marginLeft ?? ml ?? mx ?? margin, scale, unit), + mx: makeScaleHandler( + mx ?? marginLeft ?? marginRight ?? ml ?? mr ?? margin, + scale, + unit, + ), + my: makeScaleHandler( + my ?? marginTop ?? marginBottom ?? mt ?? mb ?? margin, + scale, + unit, + ), + width: makeScaleHandler(width ?? w, scale, unit), + height: makeScaleHandler(height ?? h, scale, unit), + font: makeScaleHandler(font, scale, unit), + } + + return ( + + {children} + + + ) + }, +) + +Box.displayName = 'GeistBox' + +export default Box diff --git a/components/box/index.ts b/components/box/index.ts new file mode 100644 index 000000000..b545fe8e5 --- /dev/null +++ b/components/box/index.ts @@ -0,0 +1,4 @@ +import Box from './box' + +export type { BoxProps } from './box' +export default Box diff --git a/components/index.ts b/components/index.ts index 8e69423d0..530539366 100644 --- a/components/index.ts +++ b/components/index.ts @@ -10,6 +10,9 @@ export type { AvatarProps, AvatarGroupProps } from './avatar' export { default as Badge } from './badge' export type { BadgeProps, BadgeAnchorProps } from './badge' +export { default as Box } from './box' +export type { BoxProps } from './box' + export { default as Breadcrumbs } from './breadcrumbs' export type { BreadcrumbsProps, diff --git a/components/use-scale/utils.ts b/components/use-scale/utils.ts index ef5f8ba55..f3e50689b 100644 --- a/components/use-scale/utils.ts +++ b/components/use-scale/utils.ts @@ -3,7 +3,9 @@ import { GetScalePropsFunction, ScaleProps, ScalePropKeys, + DynamicLayoutPipe, } from './scale-context' +import { isCSSNumberValue } from '../utils/collections' export const generateGetScaleProps =

( props: P & ScaleProps, @@ -37,3 +39,32 @@ export const generateGetAllScaleProps =

( } return getAllScaleProps } + +export const reduceScaleCoefficient = (scale: number) => { + if (scale === 1) return scale + const diff = Math.abs((scale - 1) / 2) + return scale > 1 ? 1 + diff : 1 - diff +} + +export const makeScaleHandler = + ( + attrValue: string | number | undefined, + scale: number, + unit: string, + ): DynamicLayoutPipe => + (scale1x, defaultValue) => { + // 0 means disable scale and the default value is 0 + if (scale1x === 0) { + scale1x = 1 + defaultValue = defaultValue || 0 + } + const factor = reduceScaleCoefficient(scale) * scale1x + if (typeof attrValue === 'undefined') { + if (typeof defaultValue !== 'undefined') return `${defaultValue}` + return `calc(${factor} * ${unit})` + } + + if (!isCSSNumberValue(attrValue)) return `${attrValue}` + const customFactor = factor * Number(attrValue) + return `calc(${customFactor} * ${unit})` + } diff --git a/components/use-scale/with-scale.tsx b/components/use-scale/with-scale.tsx index 0d7ad21d2..7a15f051a 100644 --- a/components/use-scale/with-scale.tsx +++ b/components/use-scale/with-scale.tsx @@ -1,14 +1,11 @@ import React, { forwardRef } from 'react' -import { DynamicLayoutPipe, ScaleConfig, ScaleContext, ScaleProps } from './scale-context' +import { ScaleConfig, ScaleContext, ScaleProps } from './scale-context' import useTheme from '../use-theme' -import { isCSSNumberValue } from '../utils/collections' -import { generateGetAllScaleProps, generateGetScaleProps } from './utils' - -const reduceScaleCoefficient = (scale: number) => { - if (scale === 1) return scale - const diff = Math.abs((scale - 1) / 2) - return scale > 1 ? 1 + diff : 1 - diff -} +import { + generateGetAllScaleProps, + generateGetScaleProps, + makeScaleHandler, +} from './utils' const withScale = ( Render: React.ComponentType

}>, @@ -47,43 +44,41 @@ const withScale = ( scale = 1, ...innerProps } = props - const makeScaleHandler = - (attrValue: string | number | undefined): DynamicLayoutPipe => - (scale1x, defaultValue) => { - // 0 means disable scale and the default value is 0 - if (scale1x === 0) { - scale1x = 1 - defaultValue = defaultValue || 0 - } - const factor = reduceScaleCoefficient(scale) * scale1x - if (typeof attrValue === 'undefined') { - if (typeof defaultValue !== 'undefined') return `${defaultValue}` - return `calc(${factor} * ${unit})` - } - - if (!isCSSNumberValue(attrValue)) return `${attrValue}` - const customFactor = factor * Number(attrValue) - return `calc(${customFactor} * ${unit})` - } const value: ScaleConfig = { unit: unit, SCALES: { - pt: makeScaleHandler(paddingTop ?? pt ?? py ?? padding), - pr: makeScaleHandler(paddingRight ?? pr ?? px ?? padding), - pb: makeScaleHandler(paddingBottom ?? pb ?? py ?? padding), - pl: makeScaleHandler(paddingLeft ?? pl ?? px ?? padding), - px: makeScaleHandler(px ?? paddingLeft ?? paddingRight ?? pl ?? pr ?? padding), - py: makeScaleHandler(py ?? paddingTop ?? paddingBottom ?? pt ?? pb ?? padding), - mt: makeScaleHandler(marginTop ?? mt ?? my ?? margin), - mr: makeScaleHandler(marginRight ?? mr ?? mx ?? margin), - mb: makeScaleHandler(marginBottom ?? mb ?? my ?? margin), - ml: makeScaleHandler(marginLeft ?? ml ?? mx ?? margin), - mx: makeScaleHandler(mx ?? marginLeft ?? marginRight ?? ml ?? mr ?? margin), - my: makeScaleHandler(my ?? marginTop ?? marginBottom ?? mt ?? mb ?? margin), - width: makeScaleHandler(width ?? w), - height: makeScaleHandler(height ?? h), - font: makeScaleHandler(font), + pt: makeScaleHandler(paddingTop ?? pt ?? py ?? padding, scale, unit), + pr: makeScaleHandler(paddingRight ?? pr ?? px ?? padding, scale, unit), + pb: makeScaleHandler(paddingBottom ?? pb ?? py ?? padding, scale, unit), + pl: makeScaleHandler(paddingLeft ?? pl ?? px ?? padding, scale, unit), + px: makeScaleHandler( + px ?? paddingLeft ?? paddingRight ?? pl ?? pr ?? padding, + scale, + unit, + ), + py: makeScaleHandler( + py ?? paddingTop ?? paddingBottom ?? pt ?? pb ?? padding, + scale, + unit, + ), + mt: makeScaleHandler(marginTop ?? mt ?? my ?? margin, scale, unit), + mr: makeScaleHandler(marginRight ?? mr ?? mx ?? margin, scale, unit), + mb: makeScaleHandler(marginBottom ?? mb ?? my ?? margin, scale, unit), + ml: makeScaleHandler(marginLeft ?? ml ?? mx ?? margin, scale, unit), + mx: makeScaleHandler( + mx ?? marginLeft ?? marginRight ?? ml ?? mr ?? margin, + scale, + unit, + ), + my: makeScaleHandler( + my ?? marginTop ?? marginBottom ?? mt ?? mb ?? margin, + scale, + unit, + ), + width: makeScaleHandler(width ?? w, scale, unit), + height: makeScaleHandler(height ?? h, scale, unit), + font: makeScaleHandler(font, scale, unit), }, getScaleProps: generateGetScaleProps(props), getAllScaleProps: generateGetAllScaleProps(props), diff --git a/pages/en-us/components/box.mdx b/pages/en-us/components/box.mdx new file mode 100644 index 000000000..3f0560651 --- /dev/null +++ b/pages/en-us/components/box.mdx @@ -0,0 +1,55 @@ +import { Box } from 'components' +import { Layout, Playground, Attributes } from 'lib/components' + +export const meta = { + title: 'Box', + group: 'Layout', +} + +## Box + +Polymorphic scalable component. + + + Hello + +`} +/> + + + I am an anchor tag + +`} +/> + + + I am scalable + +`} +/> + + +Box.Props + +| Attribute | Description | Type | Accepted values | Default | +| --------- | ------------ | ------------------- | ---------------------------- | ------- | +| **as** | render mode | `React.ElementType` | `'span', 'p', 'form', ...` | `div` | +| ... | scale props | `ScaleProps` | `'width', 'px', 'font', ...` | - | +| ... | native props | `HTMLAttributes` | `'id', 'className', ...` | - | + + + +export default ({ children }) => {children}