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

Feature: Button asChild / Link asChild #552

Closed
wants to merge 11 commits into from
12 changes: 4 additions & 8 deletions lib/src/components/alert-dialog/alert-context/AlertDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,19 +41,15 @@ export const Alert: React.FC<AlertDialogContentProps> = ({
{cancelActionText && (
<Button
appearance="outline"
as={AlertDialog.Cancel}
onClick={() => onAction(false)}
size="sm"
asChild
>
{cancelActionText}
<AlertDialog.Cancel>{cancelActionText}</AlertDialog.Cancel>
</Button>
)}
<Button
as={AlertDialog.Action}
onClick={() => onAction(true)}
size="sm"
>
{confirmActionText}
<Button onClick={() => onAction(true)} size="sm" asChild>
<AlertDialog.Action>{confirmActionText}</AlertDialog.Action>
</Button>
</Stack>
</AlertDialog.Content>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -428,12 +428,18 @@ exports[`BannerRegular component renders 1`] = `
margin-left: var(--space-2);
}

.c-fkUJsw-eYnvrx-size-sm {
.c-fkUJsw-jMTgOB-size-sm {
font-size: var(--fontSizes-sm);
line-height: 1.53;
height: var(--sizes-3);
padding-left: var(--space-4);
padding-right: var(--space-4);
gap: var(--space-2);
}

.c-fkUJsw-jMTgOB-size-sm .c-dbrbZt {
height: 16px;
width: 16px;
}

.c-fkUJsw-fGHEql-fullWidth-true {
Expand Down Expand Up @@ -497,12 +503,20 @@ exports[`BannerRegular component renders 1`] = `
}

@media (min-width: 800px) {
.c-fkUJsw-hnmcil-size-md {
.c-fkUJsw-dbnTtJ-size-md {
font-size: var(--fontSizes-md);
line-height: 1.5;
height: var(--sizes-4);
padding-left: var(--space-5);
padding-right: var(--space-5);
gap: var(--space-3);
}
}

@media (min-width: 800px) {
.c-fkUJsw-dbnTtJ-size-md .c-dbrbZt {
height: 20px;
width: 20px;
}
}

Expand Down Expand Up @@ -586,13 +600,13 @@ exports[`BannerRegular component renders 1`] = `
class="c-jgdwfn c-jgdwfn-ejCoEP-direction-row c-jgdwfn-XefLA-wrap-wrap c-jgdwfn-awKDG-justify-start c-jgdwfn-jroWjL-align-center c-jgdwfn-dPMagj-gap-2 c-jgdwfn-hAgpth-gap-4"
>
<button
class="c-fkUJsw c-fkUJsw-eYnvrx-size-sm c-fkUJsw-fGHEql-fullWidth-true c-fkUJsw-hnmcil-size-md c-fkUJsw-fXTFOn-fullWidth-false c-fkUJsw-iRJCoR-cv"
class="c-fkUJsw c-fkUJsw-jMTgOB-size-sm c-fkUJsw-fGHEql-fullWidth-true c-fkUJsw-dbnTtJ-size-md c-fkUJsw-fXTFOn-fullWidth-false c-fkUJsw-iRJCoR-cv"
type="button"
>
Contact an expert
</button>
<button
class="c-fkUJsw c-fkUJsw-eYnvrx-size-sm c-fkUJsw-fGHEql-fullWidth-true c-fkUJsw-hnmcil-size-md c-fkUJsw-fXTFOn-fullWidth-false c-fkUJsw-bznQva-cv"
class="c-fkUJsw c-fkUJsw-jMTgOB-size-sm c-fkUJsw-fGHEql-fullWidth-true c-fkUJsw-dbnTtJ-size-md c-fkUJsw-fXTFOn-fullWidth-false c-fkUJsw-bznQva-cv"
type="button"
>
Secondary
Expand Down Expand Up @@ -1070,12 +1084,18 @@ exports[`BannerRegular component renders dismissible variant 1`] = `
margin-left: var(--space-2);
}

.c-fkUJsw-eYnvrx-size-sm {
.c-fkUJsw-jMTgOB-size-sm {
font-size: var(--fontSizes-sm);
line-height: 1.53;
height: var(--sizes-3);
padding-left: var(--space-4);
padding-right: var(--space-4);
gap: var(--space-2);
}

.c-fkUJsw-jMTgOB-size-sm .c-dbrbZt {
height: 16px;
width: 16px;
}

.c-fkUJsw-fGHEql-fullWidth-true {
Expand Down Expand Up @@ -1154,12 +1174,20 @@ exports[`BannerRegular component renders dismissible variant 1`] = `
}

@media (min-width: 800px) {
.c-fkUJsw-hnmcil-size-md {
.c-fkUJsw-dbnTtJ-size-md {
font-size: var(--fontSizes-md);
line-height: 1.5;
height: var(--sizes-4);
padding-left: var(--space-5);
padding-right: var(--space-5);
gap: var(--space-3);
}
}

@media (min-width: 800px) {
.c-fkUJsw-dbnTtJ-size-md .c-dbrbZt {
height: 20px;
width: 20px;
}
}

Expand Down Expand Up @@ -1258,13 +1286,13 @@ exports[`BannerRegular component renders dismissible variant 1`] = `
class="c-jgdwfn c-jgdwfn-ejCoEP-direction-row c-jgdwfn-XefLA-wrap-wrap c-jgdwfn-awKDG-justify-start c-jgdwfn-jroWjL-align-center c-jgdwfn-dPMagj-gap-2 c-jgdwfn-hAgpth-gap-4"
>
<button
class="c-fkUJsw c-fkUJsw-eYnvrx-size-sm c-fkUJsw-fGHEql-fullWidth-true c-fkUJsw-hnmcil-size-md c-fkUJsw-fXTFOn-fullWidth-false c-fkUJsw-iRJCoR-cv"
class="c-fkUJsw c-fkUJsw-jMTgOB-size-sm c-fkUJsw-fGHEql-fullWidth-true c-fkUJsw-dbnTtJ-size-md c-fkUJsw-fXTFOn-fullWidth-false c-fkUJsw-iRJCoR-cv"
type="button"
>
Contact an expert
</button>
<button
class="c-fkUJsw c-fkUJsw-eYnvrx-size-sm c-fkUJsw-fGHEql-fullWidth-true c-fkUJsw-hnmcil-size-md c-fkUJsw-fXTFOn-fullWidth-false c-fkUJsw-bznQva-cv"
class="c-fkUJsw c-fkUJsw-jMTgOB-size-sm c-fkUJsw-fGHEql-fullWidth-true c-fkUJsw-dbnTtJ-size-md c-fkUJsw-fXTFOn-fullWidth-false c-fkUJsw-bznQva-cv"
type="button"
>
Secondary
Expand Down
13 changes: 2 additions & 11 deletions lib/src/components/button/Button.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,8 @@ describe(`Button component`, () => {

it('is polymorphic', async () => {
render(
<Button as="a" href="https://app.atomlearning.co.uk">
BUTTON
<Button asChild>
<a href="https://app.atomlearning.co.uk">BUTTON</a>
</Button>
)

Expand Down Expand Up @@ -183,14 +183,5 @@ describe(`Button component`, () => {

expect(handleClick).toHaveBeenCalledTimes(0)
})

it('renders an anchor if provided a link', async () => {
render(<Button href="https://atomlearning.co.uk">ATOM</Button>)

expect(await screen.findByRole('link')).toHaveAttribute(
'href',
'https://atomlearning.co.uk'
)
})
D7Torres marked this conversation as resolved.
Show resolved Hide resolved
})
})
128 changes: 52 additions & 76 deletions lib/src/components/button/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Slot } from '@radix-ui/react-slot'
import type { VariantProps } from '@stitches/react'
import { darken, opacify } from 'color2k'
import * as React from 'react'

import { Box } from '~/components/box'
import { Icon } from '~/components/icon'
import { Icon, StyledIcon } from '~/components/icon'
import { Loader } from '~/components/loader'
import { styled, theme } from '~/stitches'
import { NavigatorActions } from '~/types'
import { Override } from '~/utilities'

const getButtonOutlineVariant = (
Expand Down Expand Up @@ -86,19 +86,24 @@ export const StyledButton = styled('button', {
fontSize: '$sm',
lineHeight: 1.53,
height: '$3',
px: '$4'
px: '$4',
gap: '$2',
[`& ${StyledIcon}`]: { size: 16 }
},
md: {
fontSize: '$md',
lineHeight: 1.5,
height: '$4',
px: '$5'
px: '$5',
gap: '$3',
[`& ${StyledIcon}`]: { size: 20 }
},
lg: {
fontSize: '$lg',
lineHeight: 1.5,
height: '$5',
px: '$5'
px: '$5',
[`& ${StyledIcon}`]: { size: 22 }
}
},
isLoading: {
Expand All @@ -117,7 +122,6 @@ export const StyledButton = styled('button', {
}
}
},

compoundVariants: [
{
theme: 'primary',
Expand Down Expand Up @@ -189,108 +193,80 @@ export const StyledButton = styled('button', {
]
})

const WithLoader = ({ isLoading, children }) => (
<>
<Loader
css={{
opacity: isLoading ? 1 : 0,
position: 'absolute',
transition: 'opacity 150ms'
}}
/>
<Box
as="span"
css={isLoading ? { opacity: 0, transition: 'opacity 150ms' } : {}}
>
{children}
</Box>
</>
)

const getIconSize = (size) => {
switch (size) {
case 'lg':
return 22
case 'md':
return 20
case 'sm':
default:
return 16
}
}

const getChildren = (children, size) =>
React.Children.map(children, (child, i) => {
if (child?.type === Icon) {
return React.cloneElement(child, {
css: {
[i === 0 ? 'mr' : 'ml']: size === 'sm' ? '$2' : '$3',
thomasdigby marked this conversation as resolved.
Show resolved Hide resolved
size: getIconSize(size),
...(child.props.css ? child.props.css : {})
}
})
}
return child
})

type ButtonProps = Override<
React.ComponentProps<typeof StyledButton>,
VariantProps<typeof StyledButton> & {
as?: React.ComponentType | React.ElementType
as?: never
asChild?: boolean
children: React.ReactNode
isLoading?: boolean
} & NavigatorActions
}
>

export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
(
{
children,
as,
asChild,
isLoading,
onClick,
href,
appearance = 'solid',
size = 'md',
theme = 'primary',
type = 'button',
...rest
},
ref
) => {
const linkSpecificProps = href
? {
as: 'a',
href,
onClick: undefined
}
: {}
const props = {
...rest,
appearance,
size,
theme,
children,
ref
}

const buttonSpecificProps = href ? {} : { type }
if (asChild) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand how the loader will work if we are using the asChild option?

return (
<StyledButton
isLoading={isLoading || false}
onClick={!isLoading ? onClick : undefined}
{...props}
as={Slot}
/>
)
}

// Note: button is not disabled when loading for accessibility purposes.
// Instead the click action is not fired and the button looks faded
thomasdigby marked this conversation as resolved.
Show resolved Hide resolved
return (
<StyledButton
type="button"
{...props}
isLoading={isLoading || false}
onClick={!isLoading ? onClick : undefined}
appearance={appearance}
size={size}
theme={theme}
{...rest}
{...linkSpecificProps}
{...buttonSpecificProps}
ref={ref}
>
{typeof isLoading === 'boolean' ? (
<WithLoader isLoading={isLoading}>
{getChildren(children, size)}
</WithLoader>
<>
<Loader
css={{
opacity: isLoading ? 1 : 0,
position: 'absolute',
transition: 'opacity 150ms'
}}
/>
<Box
as="span"
css={isLoading ? { opacity: 0, transition: 'opacity 150ms' } : {}}
>
{children}
</Box>
</>
) : (
getChildren(children, size)
children
)}
</StyledButton>
)
}
) as React.FC<ButtonProps>
)

Button.displayName = 'Button'
Loading
Loading