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

chore: allow shape prop to use object notation with breakpoints as keys #2455

Closed
wants to merge 1 commit into from
Closed
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
1 change: 1 addition & 0 deletions docs/pages/components/button.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Built with [Ariakit](https://ariakit.org/components/button) for a better accessi
<Button variant="secondary">Secondary</Button>
<Button variant="tertiary">Tertiary</Button>
<Button variant="ghost">Ghost</Button>
<Button shape={{_: 'square', md: 'default', lg: 'circle'}}><WttjIcon /></Button>
```

### States
Expand Down
3 changes: 2 additions & 1 deletion packages/Button/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@
"@welcome-ui/box": "^5.16.2",
"@welcome-ui/loader": "^5.17.0",
"@welcome-ui/system": "^5.16.2",
"@welcome-ui/utils": "^5.16.2"
"@welcome-ui/utils": "^5.16.2",
"@welcome-ui/core": "^5.16.2"
},
"devDependencies": {
"@welcome-ui/icons": "^5.16.2",
Expand Down
4 changes: 3 additions & 1 deletion packages/Button/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import React from 'react'
import { CreateWuiProps, forwardRef } from '@welcome-ui/system'
import { Box } from '@welcome-ui/box'
import { Loader } from '@welcome-ui/loader'
import type { WuiTheme } from '@welcome-ui/core'

import * as S from './styles'

export type Shape = 'circle' | 'square'
export type ShapeValues = 'circle' | 'square' | 'default'
export type Shape = ShapeValues | Record<keyof WuiTheme['screens'], ShapeValues>
export type Size = 'xxs' | 'xs' | 'sm' | 'md' | 'lg'
export type Variant =
| 'primary'
Expand Down
89 changes: 64 additions & 25 deletions packages/Button/src/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,57 @@ import styled, { css, system, th } from '@xstyled/styled-components'
import { Button as AriakitButton } from '@ariakit/react'
import { shouldForwardProp } from '@welcome-ui/system'
import { hideFocusRingsDataAttribute } from '@welcome-ui/utils'
import type { WuiTheme } from '@welcome-ui/core'

import { ButtonOptions } from './index'

const shapeStyles = (size: ButtonOptions['size'], shape: ButtonOptions['shape'] = 'square') => css`
width: ${th(`buttons.sizes.${size}.height`)};
padding: 0;
${shape === 'circle' &&
css`
border-radius: ${th(`buttons.sizes.${size}.height`)};
`};
`
const shapeStyles = (
size: ButtonOptions['size'],
shape: ButtonOptions['shape'],
theme: WuiTheme
) => {
if (!shape) return
const styles = {
circle: css`
width: ${theme.buttons.sizes[size].height};
padding: 0;
border-radius: ${theme.buttons.sizes[size].height};
`,
// square and circle styles must override each other for mediaqueries to be able
// to work as expected
square: css`
width: ${theme.buttons.sizes[size].height};
padding: 0;
border-radius: 0;
`,
default: css`
width: auto;
padding: ${theme.buttons.sizes[size].padding};
border-radius: 0;
`,
}

if (typeof shape === 'string') {
return styles[shape as keyof typeof styles]
}

return Object.keys(shape).map((breakpoint: keyof WuiTheme['screens']) => {
const screenWidth = theme.screens[breakpoint]
if (breakpoint === '_') {
return styles[shape[breakpoint] as keyof typeof styles]
}
if (screenWidth) {
return css`
@media (width >= ${screenWidth}px) {
${styles[shape[breakpoint] as keyof typeof styles]};
}
`
}
})
}

export const Button = styled(AriakitButton).withConfig({ shouldForwardProp })<ButtonOptions>(
({ disabled, shape, size = 'md', variant }) => css`
({ disabled, shape, size = 'md', theme, variant }: ButtonOptions & { theme: WuiTheme }) => css`
${th(`buttons.${variant}`)};
position: relative;
display: inline-flex;
Expand All @@ -33,7 +70,7 @@ export const Button = styled(AriakitButton).withConfig({ shouldForwardProp })<Bu
appearance: none;
overflow: hidden;
transition: medium;
${shape && shapeStyles(size, shape)};
${shapeStyles(size, shape, theme)}};
${system};

& > svg.wui-icon,
Expand All @@ -56,21 +93,23 @@ export const Button = styled(AriakitButton).withConfig({ shouldForwardProp })<Bu
margin-right: sm;
}

${!disabled &&
css`
[${hideFocusRingsDataAttribute}] &:focus {
box-shadow: none;
}
&:focus {
${th(`buttons.focus.${variant || 'primary'}`)};
}
&:hover {
${th(`buttons.hover.${variant || 'primary'}`)};
}
&:active {
${th(`buttons.active.${variant || 'primary'}`)};
}
`};
${
!disabled &&
css`
[${hideFocusRingsDataAttribute}] &:focus {
box-shadow: none;
}
&:focus {
${th(`buttons.focus.${variant || 'primary'}`)};
}
&:hover {
${th(`buttons.hover.${variant || 'primary'}`)};
}
&:active {
${th(`buttons.active.${variant || 'primary'}`)};
}
`
};

&[disabled] {
cursor: not-allowed;
Expand Down
123 changes: 123 additions & 0 deletions packages/Button/tests/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,39 @@ describe('<Button />', () => {
expect(button).toHaveStyleRule('height', theme.buttons.sizes.sm.height)
})

it('should look like a circle', () => {
const theme = createTheme()

render(
<Button dataTestId="button" shape="circle" size="sm">
{content}
</Button>
)
const button = screen.getByTestId('button')

expect(button).toHaveStyleRule('width', theme.buttons.sizes.sm.height)
expect(button).toHaveStyleRule('padding', '0')
expect(button).toHaveStyleRule('border-radius', theme.buttons.sizes.sm.height)
})

it('should look like the default button', () => {
const theme = createTheme()

render(
// Disabling type check since people wui user can be wrong on the value if not using TS
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
<Button dataTestId="button" shape="wrong-value" size="sm">
{content}
</Button>
)
const button = screen.getByTestId('button')

expect(button).toHaveStyleRule('width', 'auto')
expect(button).toHaveStyleRule('padding', theme.buttons.sizes.sm.padding)
expect(button).toHaveStyleRule('border-radius', theme.buttons.primary.borderRadius)
})

it('should have correct size', () => {
const theme = createTheme()

Expand Down Expand Up @@ -138,6 +171,96 @@ describe('<Button />', () => {
expect(button).toHaveAttribute('rel', 'noopener noreferrer') // added by target="_blank" on Link
})

it('should render width the shape prop being an object', () => {
const theme = createTheme()

render(
<Button dataTestId="button" shape={{ _: 'circle' }} size="sm">
{content}
</Button>
)

const button = screen.getByTestId('button')

expect(button).toHaveStyle({
height: theme.buttons.sizes.sm.height,
})
})

it('should render width the shape prop set as circle for md breakpoint', () => {
const theme = createTheme()
render(
<Button dataTestId="button" shape={{ md: 'circle' }} size="sm">
{content}
</Button>
)

const button = screen.getByTestId('button')

expect(button).toHaveStyleRule('width', theme.buttons.sizes.sm.height, {
media: `(width >= ${theme.screens.md}px)`,
})
expect(button).toHaveStyleRule('padding', '0', {
media: `(width >= ${theme.screens.md}px)`,
})
expect(button).toHaveStyleRule('border-radius', theme.buttons.sizes.sm.height, {
media: `(width >= ${theme.screens.md}px)`,
})
})

it('should render width the shape prop being and object and set as circle (using _)', () => {
const theme = createTheme()
render(
<Button dataTestId="button" shape={{ _: 'circle' }} size="sm">
{content}
</Button>
)

const button = screen.getByTestId('button')

expect(button).toHaveStyleRule('width', theme.buttons.sizes.sm.height)
expect(button).toHaveStyleRule('padding', '0')
expect(button).toHaveStyleRule('border-radius', theme.buttons.sizes.sm.height)
})

it('should render width the shape prop set as circle for _, then default for md and square for lg breakpoints', () => {
const theme = createTheme()
render(
<Button dataTestId="button" shape={{ _: 'circle', md: 'default', lg: 'square' }}>
{content}
</Button>
)

const button = screen.getByTestId('button')

// breakpoint '_'
expect(button).toHaveStyleRule('width', theme.buttons.sizes.md.height)
expect(button).toHaveStyleRule('padding', '0')
expect(button).toHaveStyleRule('border-radius', theme.buttons.sizes.md.height)

// breakpoint 'md'
expect(button).toHaveStyleRule('width', 'auto', {
media: `(width >= ${theme.screens.md}px)`,
})
expect(button).toHaveStyleRule('padding', theme.buttons.sizes.md.padding, {
media: `(width >= ${theme.screens.md}px)`,
})
expect(button).toHaveStyleRule('border-radius', '0', {
media: `(width >= ${theme.screens.md}px)`,
})

// breakpoint 'lg'
expect(button).toHaveStyleRule('width', theme.buttons.sizes.md.height, {
media: `(width >= ${theme.screens.lg}px)`,
})
expect(button).toHaveStyleRule('padding', '0', {
media: `(width >= ${theme.screens.lg}px)`,
})
expect(button).toHaveStyleRule('border-radius', '0', {
media: `(width >= ${theme.screens.lg}px)`,
})
})

it('should have correct Icon size with Icon and text', () => {
const theme = createTheme()

Expand Down