Skip to content

Commit

Permalink
Merge pull request #121 from fortanix/feature/gh-120-button-variant-card
Browse files Browse the repository at this point in the history
Implement "card" variant for `Button` component
  • Loading branch information
mkrause authored Feb 3, 2025
2 parents f081d4c + 285a7b3 commit 3511fde
Show file tree
Hide file tree
Showing 28 changed files with 120 additions and 73 deletions.
23 changes: 20 additions & 3 deletions src/components/actions/Button/Button.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@

--bk-button-color-accent: #{bk.$theme-button-primary-background-default};
--bk-button-color-contrast: #{bk.$theme-button-primary-text-default};

&.bk-button--card {
--bk-button-color-accent: #{bk.$theme-button-card-primary-background-default};
--bk-button-color-contrast: #{bk.$theme-button-card-primary-text-default};
}

cursor: pointer;
user-select: none;

Expand Down Expand Up @@ -58,19 +62,32 @@
/* NOTE: the ordering here is important, more important rules must come later (in case of multiple states) */
&:is(:focus-visible, :global(.pseudo-focus-visible)) {
--bk-button-color-accent: #{bk.$theme-button-primary-background-focused};

&.bk-button--card {
--bk-button-color-accent: #{bk.$theme-button-card-primary-background-focused};
}
}
&:is(:hover, :global(.pseudo-hover)) {
--bk-button-color-accent: #{bk.$theme-button-primary-background-hover};

&:where(.bk-button--card) {
--bk-button-color-accent: #{bk.$theme-button-card-primary-background-hover};
}
}
&:is(:disabled, .bk-button--disabled) {
cursor: not-allowed;
--bk-button-color-accent: #{bk.$theme-button-primary-background-disabled};
--bk-button-color-contrast: #{bk.$theme-button-primary-text-disabled};
cursor: not-allowed;
}
&.bk-button--nonactive {
cursor: not-allowed;
--bk-button-color-accent: #{bk.$theme-button-primary-background-non-active};
--bk-button-color-contrast: #{bk.$theme-button-primary-text-non-active};
cursor: not-allowed;

&.bk-button--card {
--bk-button-color-accent: #{bk.$theme-button-card-primary-background-non-active};
--bk-button-color-contrast: #{bk.$theme-button-card-primary-text-non-active};
}
}

@media (prefers-reduced-motion: no-preference) {
Expand Down
32 changes: 28 additions & 4 deletions src/components/actions/Button/Button.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { ErrorBoundary } from 'react-error-boundary';

import { notify } from '../../overlays/ToastProvider/ToastProvider.tsx';
import { Icon } from '../../graphics/Icon/Icon.tsx';
import { Card } from '../../containers/Card/Card.tsx';
import { Banner } from '../../containers/Banner/Banner.tsx';

import { Button } from './Button.tsx';
Expand All @@ -29,7 +30,7 @@ export default {
args: {
unstyled: false,
label: 'Button',
variant: 'primary',
kind: 'primary',
nonactive: false,
disabled: false,
onPress: () => { notify.success('You pressed the button.'); },
Expand Down Expand Up @@ -60,7 +61,7 @@ const BaseStory: Story = {

const PrimaryStory: Story = {
...BaseStory,
args: { ...BaseStory.args, variant: 'primary' },
args: { ...BaseStory.args, kind: 'primary' },
};

export const PrimaryStandard: Story = {
Expand Down Expand Up @@ -95,7 +96,7 @@ export const PrimaryDisabled: Story = {
export const Secondary: Story = {
...BaseStory,
name: 'Secondary [standard]',
args: { ...BaseStory.args, variant: 'secondary' },
args: { ...BaseStory.args, kind: 'secondary' },
};

export const SecondaryHover: Story = {
Expand Down Expand Up @@ -125,7 +126,7 @@ export const SecondaryDisabled: Story = {
export const Tertiary: Story = {
...BaseStory,
name: 'Tertiary [standard]',
args: { ...BaseStory.args, variant: 'tertiary' },
args: { ...BaseStory.args, kind: 'tertiary' },
};

export const TertiaryHover: Story = {
Expand All @@ -152,6 +153,29 @@ export const TertiaryDisabled: Story = {
args: { ...Tertiary.args, disabled: true },
};

export const VariantCard: Story = {
render: (args) => (
<Card style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gridAutoFlow: 'row',
gap: '1rem',
}}>
<p><Button variant="card" {...args} kind="primary"/></p>
<p><Button variant="card" {...args} kind="primary" nonactive/></p>
<p><Button variant="card" {...args} kind="primary" disabled/></p>

<p><Button variant="card" {...args} kind="secondary"/></p>
<p><Button variant="card" {...args} kind="secondary" nonactive/></p>
<p><Button variant="card" {...args} kind="secondary" disabled/></p>

<p><Button variant="card" {...args} kind="tertiary"/></p>
<p><Button variant="card" {...args} kind="tertiary" nonactive/></p>
<p><Button variant="card" {...args} kind="tertiary" disabled/></p>
</Card>
),
};

export const AsyncButton: Story = {
...PrimaryStory,
args: {
Expand Down
15 changes: 10 additions & 5 deletions src/components/actions/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,11 @@ export type ButtonProps = React.PropsWithChildren<Omit<ComponentProps<'button'>,
*/
label?: undefined | string,

/** What variant the button is, from higher prominance to lower. */
variant?: undefined | 'primary' | 'secondary' | 'tertiary',
/** The kind of button, from higher prominance to lower. */
kind?: undefined | 'primary' | 'secondary' | 'tertiary',

/** Which visual variant to use. Default: 'normal'. */
variant?: undefined | 'normal' | 'card',

/**
* Whether the button is disabled. This is meant for essentially permanent disabled buttons, not for buttons that
Expand Down Expand Up @@ -60,9 +63,10 @@ export const Button = (props: ButtonProps) => {
unstyled = false,
trimmed = false,
label,
kind = 'tertiary',
variant = 'normal',
disabled = false,
nonactive = false,
variant = 'tertiary',
onPress,
asyncTimeout = 30_000,
...propsRest
Expand Down Expand Up @@ -145,8 +149,9 @@ export const Button = (props: ButtonProps) => {
bk: true,
[cl['bk-button']]: !unstyled,
[cl['bk-button--trimmed']]: trimmed,
[cl['bk-button--primary']]: variant === 'primary',
[cl['bk-button--secondary']]: variant === 'secondary',
[cl['bk-button--primary']]: kind === 'primary',
[cl['bk-button--secondary']]: kind === 'secondary',
[cl['bk-button--card']]: variant === 'card',
[cl['bk-button--disabled']]: !isInteractive,
[cl['bk-button--nonactive']]: isNonactive,
'nonactive': isNonactive, // Global class name so that consumers can style nonactive states
Expand Down
2 changes: 1 addition & 1 deletion src/components/actions/ButtonAsLink/ButtonAsLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export type ButtonAsLinkProps = React.PropsWithChildren<ComponentProps<'button'>
label?: NonNullable<LinkProps['label']>,

// Button props
//variant: NonNullable<ButtonProps['variant']>,
//kind: NonNullable<ButtonProps['kind']>,
//nonactive: NonNullable<ButtonProps['nonactive']>,
//disabled: NonNullable<ButtonProps['disabled']>,
onPress?: NonNullable<ButtonProps['onPress']>,
Expand Down
22 changes: 11 additions & 11 deletions src/components/actions/LinkAsButton/LinkAsButton.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export default {
argTypes: {},
args: {
unstyled: false,
variant: 'primary',
kind: 'primary',
label: 'Link',
href: 'https://fortanix.com',
target: '_blank',
Expand All @@ -41,17 +41,17 @@ export const Variants: Story = {
gridAutoFlow: 'row',
gap: '1rem',
}}>
<p><LinkAsButton {...args} variant="primary"/></p>
<p><LinkAsButton {...args} variant="primary" nonactive/></p>
<p><LinkAsButton {...args} variant="primary" disabled/></p>
<p><LinkAsButton {...args} kind="primary"/></p>
<p><LinkAsButton {...args} kind="primary" nonactive/></p>
<p><LinkAsButton {...args} kind="primary" disabled/></p>

<p><LinkAsButton {...args} variant="secondary"/></p>
<p><LinkAsButton {...args} variant="secondary" nonactive/></p>
<p><LinkAsButton {...args} variant="secondary" disabled/></p>
<p><LinkAsButton {...args} kind="secondary"/></p>
<p><LinkAsButton {...args} kind="secondary" nonactive/></p>
<p><LinkAsButton {...args} kind="secondary" disabled/></p>

<p><LinkAsButton {...args} variant="tertiary"/></p>
<p><LinkAsButton {...args} variant="tertiary" nonactive/></p>
<p><LinkAsButton {...args} variant="tertiary" disabled/></p>
<p><LinkAsButton {...args} kind="tertiary"/></p>
<p><LinkAsButton {...args} kind="tertiary" nonactive/></p>
<p><LinkAsButton {...args} kind="tertiary" disabled/></p>
</div>
),
};
Expand All @@ -62,7 +62,7 @@ export const Variants: Story = {
*/
export const Download: Story = {
args: {
variant: 'tertiary',
kind: 'tertiary',
download: 'my_file.txt',
label: 'Download',
href: `data:text/plain,Lorem ipsum`,
Expand Down
8 changes: 4 additions & 4 deletions src/components/actions/LinkAsButton/LinkAsButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export type LinkAsButtonProps = React.PropsWithChildren<ComponentProps<'a'> & {
label?: NonNullable<LinkProps['label']>,

// Button props
variant?: NonNullable<ButtonProps['variant']>,
kind?: NonNullable<ButtonProps['kind']>,
nonactive?: NonNullable<ButtonProps['nonactive']>,
disabled?: NonNullable<ButtonProps['disabled']>,
}>;
Expand All @@ -34,7 +34,7 @@ export const LinkAsButton = (props: LinkAsButtonProps) => {
const {
unstyled = false,
label,
variant,
kind,
nonactive,
disabled,
...propsRest
Expand All @@ -48,8 +48,8 @@ export const LinkAsButton = (props: LinkAsButtonProps) => {
className={cx({
bk: true,
[ButtonClassNames['bk-button']]: !unstyled,
[ButtonClassNames['bk-button--primary']]: variant === 'primary',
[ButtonClassNames['bk-button--secondary']]: variant === 'secondary',
[ButtonClassNames['bk-button--primary']]: kind === 'primary',
[ButtonClassNames['bk-button--secondary']]: kind === 'secondary',
[ButtonClassNames['bk-button--nonactive']]: nonactive,
[ButtonClassNames['bk-button--disabled']]: disabled,
}, props.className)}
Expand Down
2 changes: 1 addition & 1 deletion src/components/containers/Banner/Banner.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ export const BannerWithThemedContent: Story = {
The following components should always have a light theme, even in dark mode:
</p>
<div style={{ display: 'flex', gap: '2ch', marginTop: '1lh' }}>
<Button nonactive variant="primary" onPress={() => alert('clicked')}>Button</Button>
<Button nonactive kind="primary" onPress={() => alert('clicked')}>Button</Button>
<SegmentedControl
options={['Test 1', 'Test 2']}
defaultValue="Test 1"
Expand Down
1 change: 1 addition & 0 deletions src/components/containers/Card/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as React from 'react';
import { classNames as cx, type ComponentProps } from '../../../util/componentUtil.ts';

import { H5 } from '../../../typography/Heading/Heading.tsx';
import { Button } from '../../actions/Button/Button.tsx';

import cl from './Card.module.scss';

Expand Down
4 changes: 2 additions & 2 deletions src/components/containers/Dialog/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,12 @@ const ActionIcon = ({ tooltip, ...buttonProps }: ActionIconProps) => {
const CancelAction = (props: ActionProps) => {
const context = useDialogContext();
const handlePress = () => { props.onPress?.(); context.close(); };
return <Action variant="secondary" label="Cancel" {...props} onPress={handlePress}/>;
return <Action kind="secondary" label="Cancel" {...props} onPress={handlePress}/>;
};
const SubmitAction = (props: ActionProps) => {
const context = useDialogContext();
const handlePress = () => { props.onPress?.(); context.close(); };
return <Action variant="primary" label="Submit" {...props} onPress={handlePress}/>;
return <Action kind="primary" label="Submit" {...props} onPress={handlePress}/>;
};

export type DialogProps = Omit<ComponentProps<'dialog'>, 'title'> & {
Expand Down
2 changes: 1 addition & 1 deletion src/components/forms/context/SubmitButton/SubmitButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export const SubmitButton = (props: SubmitButtonProps) => {

return (
<Button
variant="primary" // Primary by default
kind="primary" // Primary by default
form={formContext.formId}
{...propsButton}
// @ts-expect-error We're using an invalid `type` on purpose here, this is meant to be used internally only.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ export default {

const actions = (
<PlaceholderEmptyAction>
<Button variant="secondary">Button</Button>
<Button variant="primary">Button</Button>
<Button kind="secondary">Button</Button>
<Button kind="primary">Button</Button>
</PlaceholderEmptyAction>
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export const PlaceholderEmpty = (props: PlaceholderEmptyProps) => {
export type PlaceholderEmptyActionProps = React.PropsWithChildren<ComponentProps<'div'>>;
/**
* A wrapper component, intended to easily add some styling to children's `<Button/>`'s.
* UX expects that such buttons are `<Button variant="tertiary"/>`.
* UX expects that such buttons are `<Button kind="tertiary"/>`.
*/
export const PlaceholderEmptyAction = (props: PlaceholderEmptyActionProps) => {
return (
Expand Down
20 changes: 10 additions & 10 deletions src/components/overlays/DialogModal/DialogModal.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export default {
argTypes: {},
args: {
title: 'Modal Dialog',
trigger: ({ activate }) => <Button variant="primary" label="Open modal" onPress={activate}/>,
trigger: ({ activate }) => <Button kind="primary" label="Open modal" onPress={activate}/>,
children: <LoremIpsum paragraphs={15}/>,
},
render: (args) => <><LoremIpsum paragraphs={2}/> <DialogModal {...args}/> <LoremIpsum paragraphs={2}/></>,
Expand Down Expand Up @@ -85,7 +85,7 @@ export const DialogModalNested: Story = {
<DialogModal
className="inner"
title="Submodal"
trigger={({ activate }) => <Button variant="primary" label="Open submodal" onPress={activate}/>}
trigger={({ activate }) => <Button kind="primary" label="Open submodal" onPress={activate}/>}
>
This is a submodal. Closing this modal should keep the outer modal still open.
</DialogModal>
Expand All @@ -99,16 +99,16 @@ export const DialogModalWithToast: Story = {
className: 'outer',
children: (
<>
<Button variant="primary" onPress={() => { notifyTest(); }}>
<Button kind="primary" onPress={() => { notifyTest(); }}>
Trigger toast notification
</Button>
<DialogModal
className="inner"
title="Submodal"
trigger={({ activate }) => <Button variant="primary" label="Open submodal" onPress={activate}/>}
trigger={({ activate }) => <Button kind="primary" label="Open submodal" onPress={activate}/>}
>
<p>Test rendering toast notifications over the modal:</p>
<Button variant="primary" onPress={() => { notifyTest(); }}>
<Button kind="primary" onPress={() => { notifyTest(); }}>
Trigger toast notification
</Button>
</DialogModal>
Expand Down Expand Up @@ -136,7 +136,7 @@ export const DialogModalUncloseable: Story = {
children: ({ close }) => (
<article className="bk-body-text">
<p>It should not be possible to close this dialog, except through the following button:</p>
<p><Button variant="primary" label="Force close" onPress={close}/></p>
<p><Button kind="primary" label="Force close" onPress={close}/></p>
</article>
),
},
Expand Down Expand Up @@ -181,8 +181,8 @@ const DialogModalControlledWithSubject = (props: React.ComponentProps<typeof Dia

<p>A single details modal will be used, filled in with the subject based on which name was pressed.</p>

<p><Button variant="primary" label="Open: Alice" onPress={() => { modal.activateWith({ name: 'Alice' }); }}/></p>
<p><Button variant="primary" label="Open: Bob" onPress={() => { modal.activateWith({ name: 'Bob' }); }}/></p>
<p><Button kind="primary" label="Open: Alice" onPress={() => { modal.activateWith({ name: 'Alice' }); }}/></p>
<p><Button kind="primary" label="Open: Bob" onPress={() => { modal.activateWith({ name: 'Bob' }); }}/></p>
</article>
);
};
Expand Down Expand Up @@ -213,14 +213,14 @@ const DialogModalControlledConfirmation = (props: React.ComponentProps<typeof Di
<p>A single details modal will be used, filled in with the subject based on which name was pressed.</p>

<p>
<Button variant="primary"
<Button kind="primary"
label={deleted.has('Item 1') ? 'Deleted' : `Delete Item 1`}
disabled={deleted.has('Item 1')}
onPress={() => { deleteConfirmer.activateWith({ name: 'Item 1' }); }}
/>
</p>
<p>
<Button variant="primary"
<Button kind="primary"
label={deleted.has('Item 2') ? 'Deleted' : `Delete Item 2`}
disabled={deleted.has('Item 2')}
onPress={() => { deleteConfirmer.activateWith({ name: 'Item 2' }); }}
Expand Down
Loading

0 comments on commit 3511fde

Please sign in to comment.