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 new buttons variants and sizes #1003

Merged
merged 14 commits into from
Apr 4, 2023
113 changes: 111 additions & 2 deletions frontend/src/components/VButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,17 @@
{
[$style[`${variant}-pressed`]]: isActive,
[$style[`connection-${connections}`]]: isConnected,
'border border-tx ring-offset-1 focus-visible:outline-none focus-visible:ring focus-visible:ring-pink':
!isPlainDangerous,
[$style[`icon-start-${size}`]]: hasIconStart,
[$style[`icon-end-${size}`]]: hasIconEnd,
'gap-x-2':
(hasIconEnd || hasIconStart) && (size == 'medium' || size == 'large'),
'gap-x-1': (hasIconEnd || hasIconStart) && size == 'small',
// Custom tailwind classes don't work with CSS modules in Vue so they are written here explicitly instead of accessed off of `$style`.
'focus-slim-filled': isFilled,
'focus-slim-tx': isBordered || isTransparent,
'description-bold': isNewVariant,
'border border-tx ring-offset-1 focus:outline-none focus-visible:ring focus-visible:ring-pink':
!isPlainDangerous && !isNewVariant,
},
]"
:aria-pressed="pressed"
Expand Down Expand Up @@ -158,6 +167,24 @@ const VButton = defineComponent({
type: String as PropType<ButtonConnections>,
default: "none",
},
/**
* Whether the button has an icon at the inline start of the button.
*
* @default false
*/
hasIconStart: {
type: Boolean,
default: false,
},
/**
* Whether the button has an icon at the inline end of the button.
*
* @default false
*/
hasIconEnd: {
type: Boolean,
default: false,
},
},
setup(props, { attrs }) {
const propsRef = toRefs(props)
Expand All @@ -182,6 +209,15 @@ const VButton = defineComponent({
const isPlainDangerous = computed(() => {
return propsRef.variant.value === "plain--avoid"
})
const isFilled = computed(() => {
return props.variant.startsWith("filled-")
})
const isBordered = computed(() => {
return props.variant.startsWith("bordered-")
})
const isTransparent = computed(() => {
return props.variant.startsWith("transparent-")
})

watch(
[propsRef.disabled, propsRef.focusableWhenDisabled],
Expand Down Expand Up @@ -223,13 +259,26 @@ const VButton = defineComponent({
{ immediate: true }
)

// TODO: remove after the Core UI improvements are done
const isNewVariant = computed(() => {
return (
props.variant.startsWith("filled-") ||
props.variant.startsWith("bordered-") ||
props.variant.startsWith("transparent-")
)
})

return {
disabledAttributeRef,
ariaDisabledRef,
typeRef,
isActive,
isConnected,
isPlainDangerous,
isNewVariant,
isFilled,
isBordered,
isTransparent,
}
},
})
Expand All @@ -253,10 +302,70 @@ export default VButton
@apply py-6 px-8;
}

.size-small {
@apply h-8 py-0 px-2;
}
.icon-start-small {
@apply ps-1;
}
.icon-end-small {
@apply pe-1;
}

.size-medium {
@apply h-10 py-0 px-3;
}
.icon-start-medium {
@apply ps-2;
}
.icon-end-medium {
@apply pe-2;
}

.size-large {
@apply h-12 py-0 px-5;
}
.icon-start-large {
@apply ps-4;
}
.icon-end-large {
@apply pe-4;
}

a.button {
@apply no-underline hover:no-underline;
}

.filled-pink {
@apply bg-pink text-white hover:bg-dark-pink hover:text-white;
}

.filled-dark {
@apply bg-dark-charcoal text-white hover:bg-dark-charcoal-80 hover:text-white;
}

.filled-gray {
@apply bg-dark-charcoal-10 text-dark-charcoal hover:bg-dark-charcoal hover:text-white;
}

.filled-white {
@apply bg-white text-dark-charcoal hover:bg-dark-charcoal hover:text-white;
}

.bordered-white {
@apply border border-white bg-white text-dark-charcoal hover:border-dark-charcoal-20;
}

.bordered-gray {
@apply border border-dark-charcoal-20 bg-white text-dark-charcoal hover:border-dark-charcoal;
}
.transparent-gray {
@apply bg-tx text-dark-charcoal hover:bg-dark-charcoal-10;
}
.transparent-dark {
@apply bg-tx text-dark-charcoal hover:bg-dark-charcoal hover:text-white;
}

.primary {
@apply border-tx bg-pink text-white hover:border-tx hover:bg-dark-pink hover:text-white;
}
Expand Down
122 changes: 94 additions & 28 deletions frontend/src/components/meta/VButton.stories.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,39 @@ import {
Meta,
Story,
} from "@storybook/addon-docs"
import { buttonForms, buttonSizes, buttonVariants } from "~/types/button"
import {
buttonForms,
buttonSizes as allButtonSizes,
buttonVariants as allButtonVariants,
} from "~/types/button"
import VButton from "~/components/VButton.vue"
import VIcon from "~/components/VIcon/VIcon.vue"
import { capital } from "case"

import replayIcon from "~/assets/icons/replay.svg"
import externalLinkIcon from "~/assets/icons/external-link.svg"

<Meta title="Components/VButton" components={VButton} />

export const buttonVariants = allButtonVariants.filter(
(variant) =>
variant.startsWith("filled-") ||
variant.startsWith("bordered-") ||
variant.startsWith("transparent-")
)
export const buttonSizes = allButtonSizes.filter(
(size) => !size.endsWith("-old")
)

export const Template = (args) => ({
template: `
<div id="wrapper"
class="w-40 h-16 flex items-center justify-center"
:class="args?.variant.startsWith('transparent') ? 'bg-dark-charcoal-06': 'bg-white'">
<VButton v-bind="args" @click="onClick" href="/">
Code is Poetry
</VButton>
</div>
`,
components: { VButton },
methods: {
Expand All @@ -28,6 +50,25 @@ export const Template = (args) => ({
},
})

export const TemplateWithIcons = (args) => ({
template: `
<div class="flex flex-col items-center gap-4 flex-wrap">
<VButton :variant="args.variant" :size="args.size" :has-icon-start="true">
<VIcon :icon-path="replayIcon" />Button
</VButton>
<VButton :variant="args.variant" :size="args.size" has-icon-end>
Button<VIcon :icon-path="externalLinkIcon" />
</VButton>
<VButton :variant="args.variant" :size="args.size" has-icon-start has-icon-end>
<VIcon :icon-path="replayIcon" />Button<VIcon :icon-path="externalLinkIcon" />
</VButton>
</div>`,
components: { VButton, VIcon },
setup() {
return { args, replayIcon, externalLinkIcon }
},
})

# VButton

<Description of={VButton} />
Expand All @@ -48,11 +89,13 @@ export const Template = (args) => ({
variant: {
options: buttonVariants,
control: { type: "select" },
defaultValue: "filled-pink",
},
pressed: { control: "boolean" },
size: {
options: buttonSizes,
control: { type: "radio" },
defaultValue: "medium",
},
disabled: { control: "boolean" },
focusableWhenDisabled: { control: "boolean" },
Expand All @@ -69,7 +112,7 @@ export const VariantsTemplate = (args) => ({
<VButton v-for="variant in args.variants"
:variant="variant"
:key="variant"
class="caption-bold"
class="description-bold"
v-bind="args">
{{ capitalize(variant) }}
</VButton>
Expand All @@ -87,20 +130,24 @@ export const VariantsTemplate = (args) => ({

## Button variants

### Primary
### Filled

The style used for Call-to-action buttons, such as the 'Search' button or 'Get
this media item' buttons. It is a pink button.
These buttons have a solid background color and no border.

<Canvas>
<Story
name="primary"
args={{ variants: ["primary"] }}
name="filled"
args={{
variants: buttonVariants.filter((variant) =>
variant.startsWith("filled-")
),
}}
argTypes={{
pressed: { control: "boolean" },
size: {
options: buttonSizes,
control: { type: "radio" },
defaultValue: "medium",
},
disabled: { control: "boolean" },
}}
Expand All @@ -109,22 +156,24 @@ this media item' buttons. It is a pink button.
</Story>
</Canvas>

### Secondary

The styles used for other buttons.
### Bordered

There are three variants of secondary buttons: filled, bordered and text
(without border).
These buttons have a white background and a border.

<Canvas>
<Story
name="secondary"
args={{ variants: ["secondary-bordered", "secondary-filled", "secondary"] }}
name="bordered"
args={{
variants: buttonVariants.filter((variant) =>
variant.startsWith("bordered-")
),
}}
argTypes={{
pressed: { control: "boolean" },
size: {
options: buttonSizes,
control: { type: "radio" },
defaultValue: "medium",
},
disabled: { control: "boolean" },
}}
Expand All @@ -133,35 +182,52 @@ There are three variants of secondary buttons: filled, bordered and text
</Story>
</Canvas>

### Action-menu

The styles used for header 'action-menu' buttons.
### Transparent

'action-menu' also has no border and no background. On hover, there is a light
border. It is used in the desktop header buttons and for the content type
switcher inside the searchbar.

'action-menu-bordered' has a border but no background. It is used in the desktop
header buttons when the (old) header is scrolled.

'action-menu-muted' has a light charcoal background. It is used for filters when
some filters are applied.
These buttons are transparent and don't have a border in resting state.

<Canvas>
<Story
name="action-menu"
name="transparent"
args={{
variants: ["action-menu", "action-menu-bordered", "action-menu-muted"],
variants: buttonVariants.filter((variant) =>
variant.startsWith("transparent-")
),
}}
argTypes={{
pressed: { control: "boolean" },
size: {
options: buttonSizes,
control: { type: "radio" },
defaultValue: "medium",
},
disabled: { control: "boolean" },
}}
>
{VariantsTemplate.bind({})}
</Story>
</Canvas>

### Buttons with Icons

<Canvas>
<Story
name="icons"
argTypes={{
pressed: { control: "boolean" },
size: {
options: buttonSizes,
control: { type: "radio" },
defaultValue: "medium",
},
variant: {
options: buttonVariants,
control: { type: "select" },
defaultValue: "bordered-dark",
},
disabled: { control: "boolean" },
}}
>
{TemplateWithIcons.bind({})}
</Story>
</Canvas>
8 changes: 8 additions & 0 deletions frontend/src/types/button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ export const buttonVariants = [
"full",
"dropdown-label",
"dropdown-label-pressed",
"filled-pink",
"filled-dark",
"filled-gray",
"filled-white",
"bordered-white",
"bordered-gray",
"transparent-gray",
"transparent-dark",
] as const
export type ButtonVariant = typeof buttonVariants[number]

Expand Down
Loading