Skip to content
Merged
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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ Running `pnpm run build:ce` ("ce" for "Custom Elements") produces two files that
Example usage:

```html
<bolt-primary-button variant="outline" href="{{ url('roadmaps.desktop') }}">
<bolt-primary-button outline href="{{ url('roadmaps.desktop') }}">
Visit Desktop Roadmap
</bolt-primary-button>
```
Expand Down Expand Up @@ -124,7 +124,7 @@ export { BoltPrimaryButton };
When you use one of the Web Components in your markup, you will likely want to specify the text content:

```html
<bolt-primary-button variant="outline" href="{{ url('roadmaps.desktop') }}">
<bolt-primary-button outline href="{{ url('roadmaps.desktop') }}">
Visit Desktop Roadmap
</bolt-primary-button>
```
Expand All @@ -135,7 +135,7 @@ If the component you want to use does not have a default slot, add it:

```diff
<template>
<base-button type="primary">
<base-button variant="primary">
<template v-for="(_, name) in $slots" v-slot:[name]="slotData">
<slot :name="name" v-bind="slotData" />
</template>
Expand Down
31 changes: 16 additions & 15 deletions src/components/BaseButton.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ const meta: Meta<typeof BaseButton> = {
tags: ['autodocs'],
argTypes: {
size: { control: 'select', options: ['regular', 'small'] },
variant: { control: 'select', options: ['filled', 'outline'] },
type: { control: 'select', options: ['primary', 'brand', 'danger', 'link'] },
outline: { control: 'boolean' },
variant: { control: 'select', options: ['primary', 'brand', 'danger', 'link'] },
type: { control: 'select', options: ['button', 'submit', 'reset'] },
disabled: { control: 'boolean' },
},
args: {
Expand Down Expand Up @@ -56,7 +57,7 @@ export const Standard: Story = {
},
};

export const Type: Story = {
export const Variant: Story = {
render: (args) => ({
components: { PrimaryButton, BrandButton, DangerButton, LinkButton },
setup() {
Expand All @@ -80,23 +81,23 @@ export const Type: Story = {
},
};

export const Variant: Story = {
export const Outline: Story = {
render: (args) => ({
components: { PrimaryButton },
setup() {
return { args };
},
template: `
<div style="display:flex;gap:.5rem;align-items:center;">
<primary-button variant="filled">Filled</primary-button>
<primary-button variant="outline">Outline</primary-button>
<primary-button>Filled</primary-button>
<primary-button outline>Outline</primary-button>
</div>
`,
}),
parameters: {
docs: {
source: {
code: '<primary-button variant="filled">Filled</primary-button>\n<primary-button variant="outline">Outline</primary-button>',
code: '<primary-button>Filled</primary-button>\n<primary-button outline>Outline</primary-button>',
},
},
},
Expand Down Expand Up @@ -132,21 +133,21 @@ export const Disabled: Story = {
},
template: `
<div style="display:flex;gap:.5rem;align-items:center;">
<primary-button variant="filled" disabled>Disabled</primary-button>
<primary-button variant="outline" disabled>Disabled</primary-button>
<primary-button disabled>Disabled</primary-button>
<primary-button outline disabled>Disabled</primary-button>
</div>
`,
}),
parameters: {
docs: {
source: {
code: '<primary-button variant="filled" disabled>Filled</primary-button>\n<primary-button variant="outline" disabled>Outline</primary-button>',
code: '<primary-button disabled>Filled</primary-button>\n<primary-button outline disabled>Outline</primary-button>',
},
},
},
};

export const FormAction: Story = {
export const Type: Story = {
render: (args) => ({
components: { PrimaryButton, TextInput },
setup() {
Expand All @@ -156,23 +157,23 @@ export const FormAction: Story = {
<div style="display:flex;flex-direction:column;gap:1rem;">
<form style="display:flex;gap:.5rem;max-width:500px">
<text-input name="submit" type="text" placeholder="Fill and submit me" />
<primary-button form-action="submit">Submit</primary-button>
<primary-button type="submit">Submit</primary-button>
</form>
<form style="display:flex;gap:.5rem;max-width:500px">
<text-input name="submit" type="text" placeholder="Fill and reset me" />
<primary-button form-action="reset">Reset</primary-button>
<primary-button type="reset">Reset</primary-button>
</form>
<form style="display:flex;gap:.5rem;max-width:500px">
<text-input name="submit" type="text" placeholder="Fill me" />
<primary-button form-action="none">None</primary-button>
<primary-button type="button">None</primary-button>
</form>
</div>
`,
}),
parameters: {
docs: {
source: {
code: '<primary-button form-action="submit">Submit</primary-button>\n<primary-button form-action="reset">Reset</primary-button>\n<primary-button form-action="none">None</primary-button>',
code: '<primary-button type="submit">Submit</primary-button>\n<primary-button type="reset">Reset</primary-button>\n<primary-button type="button">None</primary-button>',
},
},
},
Expand Down
16 changes: 8 additions & 8 deletions src/components/BaseButton.vue
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
<script setup lang="ts">
// component properties
interface Props {
type?: 'primary' | 'brand' | 'danger' | 'link';
variant?: 'primary' | 'brand' | 'danger' | 'link';
size?: 'default' | 'small';
variant?: 'filled' | 'outline';
outline?: boolean;
/**
* Transform the button into an anchor tag. If null then the component will be rendered as a button tag.
*/
href?: string;
formAction?: 'none' | 'submit' | 'reset';
type?: 'button' | 'submit' | 'reset';
dataTestid?: string;
disabled?: boolean;
}
withDefaults(defineProps<Props>(), {
type: 'primary',
variant: 'primary',
size: 'default',
variant: 'filled',
formAction: 'none',
outline: false,
type: 'button',
dataTestid: 'button',
disabled: false,
});
Expand All @@ -27,8 +27,8 @@ withDefaults(defineProps<Props>(), {
:is="href ? 'a' : 'button'"
:href="href"
class="base"
:class="{ [type]: type, small: size === 'small', [variant]: variant }"
:type="formAction === 'none' ? 'button' : formAction"
:class="[variant, { small: size === 'small', outline, filled: !outline }]"
:type="!href ? type : undefined"
:data-testid="dataTestid"
:disabled="disabled"
>
Expand Down
2 changes: 1 addition & 1 deletion src/components/BrandButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import BaseButton from '@/components/BaseButton.vue';
</script>

<template>
<base-button type="brand">
<base-button variant="brand">
<template v-for="(_, name) in $slots" v-slot:[name]="slotData">
<slot :name="name" v-bind="slotData" />
</template>
Expand Down
2 changes: 1 addition & 1 deletion src/components/DangerButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import BaseButton from '@/components/BaseButton.vue';
</script>

<template>
<base-button type="danger">
<base-button variant="danger">
<template v-for="(_, name) in $slots" v-slot:[name]="slotData">
<slot :name="name" v-bind="slotData" />
</template>
Expand Down
2 changes: 1 addition & 1 deletion src/components/IconButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ withDefaults(defineProps<Props>(), {
</script>

<template>
<base-button type="link" class="icon-only" :class="{ [size]: size }">
<base-button variant="link" class="icon-only" :class="{ [size]: size }">
<template #iconLeft="slotData">
<slot v-bind="slotData" />
</template>
Expand Down
2 changes: 1 addition & 1 deletion src/components/LinkButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import BaseButton from '@/components/BaseButton.vue';
</script>

<template>
<base-button type="link">
<base-button variant="link">
<template v-for="(_, name) in $slots" v-slot:[name]="slotData">
<slot :name="name" v-bind="slotData" />
</template>
Expand Down
8 changes: 4 additions & 4 deletions src/components/ModalDialog.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export const Standard: Story = {
<span>The blessed trials awaited in the hollow temple of Silverfen. Legends in Dragon's Rest foretell a shadow child who will mend the crown. Knights of Redmarsh raised their lances to a pale sun. Each solstice, Oakshield honors the crimson sigil with fire and song. A hidden door opened within Nightveil's echoing caverns.</span>

<template #actions>
<primary-button name="cancel" variant="outline">Secondary</primary-button>
<primary-button name="cancel" outline>Secondary</primary-button>
<primary-button name="go">Primary</primary-button>
</template>

Expand All @@ -61,7 +61,7 @@ export const Standard: Story = {
<span>The blessed trials awaited in the hollow temple of Silverfen...</span>

<template #actions>
<primary-button name="cancel" variant="outline">Secondary</primary-button>
<primary-button name="cancel" outline>Secondary</primary-button>
<primary-button name="go">Primary</primary-button>
</template>

Expand Down Expand Up @@ -146,7 +146,7 @@ export const WithActions: Story = {
<text-input name="two" label="Another field" />
</form>
<template #actions>
<primary-button name="cancel" variant="outline">Cancel</primary-button>
<primary-button name="cancel" outline>Cancel</primary-button>
<primary-button name="save">Save</primary-button>
</template>
</modal-dialog>
Expand All @@ -161,7 +161,7 @@ export const WithActions: Story = {
<text-input name="two" label="Another field" />
</form>
<template #actions>
<primary-button name="cancel" variant="outline">Cancel</primary-button>
<primary-button name="cancel" outline>Cancel</primary-button>
<primary-button name="save">Save</primary-button>
</template>
</modal-dialog>`,
Expand Down
2 changes: 1 addition & 1 deletion src/components/PrimaryButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import BaseButton from '@/components/BaseButton.vue';
</script>

<template>
<base-button type="primary">
<base-button variant="primary">
<template v-for="(_, name) in $slots" v-slot:[name]="slotData">
<slot :name="name" v-bind="slotData" />
</template>
Expand Down
86 changes: 41 additions & 45 deletions test/components/BaseButton.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,47 +7,47 @@ import CheckCircleFilledIcon from '@/foundation/CheckCircleFilledIcon.vue';
describe('BaseButton', () => {
var wrapper;

// repeat all tests for each availble BaseButton type
describe.each([ 'primary', 'brand', 'danger', 'link'])('Type: %s', (type) => {
// repeat all tests for each availble BaseButton variant
describe.each([ 'primary', 'brand', 'danger', 'link'])('Variant: %s', (variant) => {

// build out test cases for variants/options
const testCases = [
{ type: type, size: 'default', variant: 'filled', iconLeft: false, iconRight: false, formAction: 'none',
disabled: false, href: null, text: `${type} button` },
{ type: type, size: 'default', variant: 'filled', iconLeft: true, iconRight: false, formAction: 'none',
disabled: false, href: null, text: `${type} button with icon left` },
{ type: type, size: 'default', variant: 'filled', iconLeft: false, iconRight: true, formAction: 'none',
disabled: false, href: null, text: `${type} button with icon right` },
{ type: type, size: 'default', variant: 'filled', iconLeft: true, iconRight: true, formAction: 'none',
disabled: false, href: null, text: `${type} button with icon left and right` },
{ type: type, size: 'small', variant: 'filled', iconLeft: false, iconRight: false, formAction: 'none',
disabled: false, href: null, text: `small ${type} button` },
{ type: type, size: 'default', variant: 'outline', iconLeft: false, iconRight: false, formAction: 'none',
disabled: false, href: null, text: `${type} outlined` },
{ type: type, size: 'default', variant: 'filled', iconLeft: false, iconRight: false, formAction: 'none',
disabled: false, href: 'http://tb.pro', text: `${type} HREF` },
{ type: type, size: 'default', variant: 'filled', iconLeft: false, iconRight: false, formAction: 'submit',
disabled: false, href: null, text: `${type} submit` },
{ type: type, size: 'default', variant: 'filled', iconLeft: false, iconRight: false, formAction: 'reset',
disabled: false, href: null, text: `${type} reset` },
{ type: type, size: 'default', variant: 'filled', iconLeft: false, iconRight: false, formAction: 'none',
disabled: true, href: null, text: `${type} disabled` },
{ variant: variant, size: 'default', outline: false, iconLeft: false, iconRight: false, type: 'button',
disabled: false, href: null, text: `${variant} button` },
{ variant: variant, size: 'default', outline: false, iconLeft: true, iconRight: false, type: 'button',
disabled: false, href: null, text: `${variant} button with icon left` },
{ variant: variant, size: 'default', outline: false, iconLeft: false, iconRight: true, type: 'button',
disabled: false, href: null, text: `${variant} button with icon right` },
{ variant: variant, size: 'default', outline: false, iconLeft: true, iconRight: true, type: 'button',
disabled: false, href: null, text: `${variant} button with icon left and right` },
{ variant: variant, size: 'small', outline: false, iconLeft: false, iconRight: false, type: 'button',
disabled: false, href: null, text: `small ${variant} button` },
{ variant: variant, size: 'default', outline: true, iconLeft: false, iconRight: false, type: 'button',
disabled: false, href: null, text: `${variant} outlined` },
{ variant: variant, size: 'default', outline: false, iconLeft: false, iconRight: false, type: 'button',
disabled: false, href: 'http://tb.pro', text: `${variant} HREF` },
{ variant: variant, size: 'default', outline: false, iconLeft: false, iconRight: false, type: 'submit',
disabled: false, href: null, text: `${variant} submit` },
{ variant: variant, size: 'default', outline: false, iconLeft: false, iconRight: false, type: 'reset',
disabled: false, href: null, text: `${variant} reset` },
{ variant: variant, size: 'default', outline: false, iconLeft: false, iconRight: false, type: 'button',
disabled: true, href: null, text: `${variant} disabled` },
];

afterEach(() => {
wrapper.unmount();
});

it.each(testCases)('$type button renders correctly with the given options',
async ({ type, size, variant, iconLeft, iconRight, formAction, disabled, href, text }) => {
it.each(testCases)('$variant button renders correctly with the given options',
async ({ variant, size, outline, iconLeft, iconRight, type, disabled, href, text }) => {
const ourProps = {
type: type,
size: size,
variant: variant,
formAction: formAction,
size: size,
outline: outline,
type: type,
disabled: disabled,
href: type == 'link'? 'http://tb.pro' : href, // we want our link type buttons to always have an href
dataTestid: `${type}-data-test-id`,
href: variant == 'link'? 'http://tb.pro' : href, // we want our link variant buttons to always have an href
dataTestid: `${variant}-data-test-id`,
};
const ourSlots = {
default: text,
Expand All @@ -72,8 +72,8 @@ describe('BaseButton', () => {

const btn = wrapper.find(btnSelector);
expect(btn.isVisible()).toBe(true);
expect(btn.attributes().class).toContain(ourProps['type']);
expect(btn.attributes().class).toContain(ourProps['variant']);
expect(btn.attributes().class).toContain(ourProps['outline'] ? 'outline' : 'filled');
expect(btn.text()).toBe(text);

// icon displayed or not depending on option
Expand All @@ -83,29 +83,25 @@ describe('BaseButton', () => {
expect(wrapper.find('.icon').exists()).toBe(false);
}

// button is of type 'button' unless formAction provided
if (ourProps['formAction'] == 'none') {
expect(btn.attributes().type).toBe('button');
} else {
expect(btn.attributes().type).toBe(ourProps['formAction']);
}
// native button type attribute matches the type prop, but is not set on anchor tags
expect(btn.attributes().type).toBe(ourProps['href'] ? undefined : ourProps['type']);

// providing href turns button into an anchor tag
if (href) {
expect(btn.attributes().href).toBe(ourProps['href']);
}
});

it.each(testCases)('able to click $type button with the given options',
async ({ type, size, variant, formAction, disabled, href, text }) => {
it.each(testCases)('able to click $variant button with the given options',
async ({ variant, size, outline, type, disabled, href, text }) => {
const ourProps = {
type: type,
size: size,
variant: variant,
formAction: formAction,
size: size,
outline: outline,
type: type,
disabled: disabled,
href: type == 'link'? 'http://tb.pro' : href, // we want our link type buttons to always have an href
dataTestid: `${type}-data-test-id`,
href: variant == 'link'? 'http://tb.pro' : href, // we want our link variant buttons to always have an href
dataTestid: `${variant}-data-test-id`,
};
const ourSlots = {
default: text,
Expand All @@ -130,10 +126,10 @@ describe('BaseButton', () => {
}
} else {
expect(btn.attributes().disabled).not.toBeNull();
if (type !== 'link') {
if (variant !== 'link') {
expect(wrapper.emitted().click, 'expected click event not to have been emitted because button is disabled').toBeFalsy();
} else {
// link type buttons can still be clicked (to navigate the to the link) even if disabled
// link variant buttons can still be clicked (to navigate the to the link) even if disabled
expect(wrapper.emitted().click, 'expected click event to have been emitted').toBeTruthy();
expect(wrapper.emitted()['click'].length).toBe(1);
}
Expand Down
Loading