Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add box component #804

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions components/box/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
@@ -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>Box</Box>)
expect(() => wrapper.unmount()).not.toThrow()
})

it('should render as the provided element', async () => {
const wrapper = mount(
<Box as="a" href="https://geist.com">
Box
</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 = '<div id="root"></div>'
const wrapper = mount(
<Box px="2rem" w="300px" h="100px" my="2rem">
Box
</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 px="2rem">Box</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<HTMLDivElement>()
const wrapper = mount(<Box ref={ref}>Box</Box>)
expect(wrapper.find('div').getDOMNode()).toEqual(ref.current)
expect(() => wrapper.unmount()).not.toThrow()
})
})
120 changes: 120 additions & 0 deletions components/box/box.tsx
Original file line number Diff line number Diff line change
@@ -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<E extends keyof JSX.IntrinsicElements | React.JSXElementConstructor<any>> =
JSX.LibraryManagedAttributes<E, React.ComponentPropsWithRef<E>>

export interface BoxOwnProps<E extends React.ElementType = React.ElementType> {
as?: E
}

export type BoxProps<E extends React.ElementType> = BoxOwnProps<E> &
Omit<PropsOf<E>, keyof (BoxOwnProps & ScaleProps)> &
ScaleProps

const defaultElement = 'div'

export type BoxComponent = {
<E extends React.ElementType = typeof defaultElement>(
props: BoxProps<E>,
): React.ReactElement | null
displayName?: string
}

export const Box: BoxComponent = React.forwardRef(
<E extends React.ElementType = typeof defaultElement>(
{ as, children, className, ...restProps }: BoxProps<E>,
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 (
<Element className={useClasses('box', className)} ref={ref} {...innerProps}>
{children}
<style jsx>{`
.box {
line-height: ${SCALES.height(1)};
font-size: ${SCALES.font(1)};
width: ${SCALES.width(0, 'auto')};
height: ${SCALES.height(0, 'auto')};
padding: ${SCALES.pt(0)} ${SCALES.pr(0)} ${SCALES.pb(0)} ${SCALES.pl(0)};
margin: ${SCALES.mt(0)} ${SCALES.mr(0)} ${SCALES.mb(0)} ${SCALES.ml(0)};
}
`}</style>
</Element>
)
},
)

Box.displayName = 'GeistBox'

export default Box
4 changes: 4 additions & 0 deletions components/box/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import Box from './box'

export type { BoxProps } from './box'
export default Box
3 changes: 3 additions & 0 deletions components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
31 changes: 31 additions & 0 deletions components/use-scale/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import {
GetScalePropsFunction,
ScaleProps,
ScalePropKeys,
DynamicLayoutPipe,
} from './scale-context'
import { isCSSNumberValue } from '../utils/collections'

export const generateGetScaleProps = <P>(
props: P & ScaleProps,
Expand Down Expand Up @@ -37,3 +39,32 @@ export const generateGetAllScaleProps = <P>(
}
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})`
}
79 changes: 37 additions & 42 deletions components/use-scale/with-scale.tsx
Original file line number Diff line number Diff line change
@@ -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 = <T, P = {}>(
Render: React.ComponentType<P & { ref?: React.Ref<T> }>,
Expand Down Expand Up @@ -47,43 +44,41 @@ const withScale = <T, P = {}>(
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),
Expand Down
55 changes: 55 additions & 0 deletions pages/en-us/components/box.mdx
Original file line number Diff line number Diff line change
@@ -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.

<Playground
scope={{ Box }}
code={`
<Box>
Hello
</Box>
`}
/>

<Playground
title="Polymorphic property"
desc="Provide an element to render the Box as."
scope={{ Box }}
code={`
<Box as='a' href='#'>
I am an anchor tag
</Box>
`}
/>

<Playground
title="Scalability"
desc="Use props to scale the Box."
scope={{ Box }}
code={`
<Box font={3} py='3rem' mx='auto' width='300px'>
I am scalable
</Box>
`}
/>

<Attributes edit="/pages/en-us/components/page.mdx">
<Attributes.Title>Box.Props</Attributes.Title>

| 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', ...` | - |

</Attributes>

export default ({ children }) => <Layout meta={meta}>{children}</Layout>