Skip to content

Instantly share code, notes, and snippets.

@Jamiewarb
Last active April 13, 2022 18:49
Show Gist options
  • Save Jamiewarb/c51a6383b7e6c67bb75f568eb05bdbef to your computer and use it in GitHub Desktop.
Save Jamiewarb/c51a6383b7e6c67bb75f568eb05bdbef to your computer and use it in GitHub Desktop.
Vue BaseButton component
<template>
<component
:is="tag"
:class="buttonClasses"
:type="nativeType"
v-bind="$attrs"
@click="$emit('click', $event)"
>
<span class="v-btn__slot"><slot /></span>
</component>
</template>
<script>
/**
* Button Features:
* - Themes (primary, secondary, etc, light, dark)
* - Text
* - Disabled, Loading
* - Outlined
* - Block
* - Sizes
* - Router Tag implementation (button, a, input, router-link, nuxt-link)
*
* To do:
* - Inverted // @todo Styles for a button that look good when on a background color of the set 'theme' prop
* - Icons (left and right) // @todo When icons are added, add them here too
* - Button group? // @todo Add a button group then re-evaluate this component
*
* Native Considerations:
* aria-label - Add an aria-label when button content doesn't describe it, such as when icon-only. Read as "click {label}"
* aria-role - When not using <button> tag, add aria-role="button" if the element will do something in response to user input (such as when using an a tag to display a modal, rather than to navigate).
*/
const nativeTypes = ['button', 'submit', 'reset'];
const availableSizes = [null, 'tiny', 'small', 'large', 'huge'];
const availableTags = ['button', 'a', 'input', 'router-link', 'nuxt-link', 'n-link', 'NuxtLink', 'NLink'];
const availableThemes = ['white', 'black', 'primary', 'secondary'];
export default {
name: 'BaseButton',
inheritAttrs: false,
props: {
block: { type: Boolean, default: false },
disabled: { type: Boolean, default: false },
loading: { type: Boolean, default: false },
nativeType: { type: String, default: 'button', validator: (value) => nativeTypes.includes(value) },
outline: { type: Boolean, default: false },
size: { type: String, default: null, validator: (value) => availableSizes.includes(value) },
tag: { type: String, default: 'button', validator: (value) => availableTags.includes(value) },
text: { type: Boolean, default: false },
theme: { type: String, default: 'primary', validator: (value) => availableThemes.includes(value) },
},
computed: {
buttonClasses() {
return {
'v-btn': true,
[`v-btn--${this.theme}`]: this.theme,
[`v-btn--${this.size}`]: this.size,
'v-btn--block': this.block,
'v-btn--disabled': this.disabled,
'v-btn--loading': this.loading,
'v-btn--outlined': this.outline,
'v-btn--text': this.text,
};
},
},
};
</script>
<style scoped lang="scss">
/**
* The buttons are built to be customisable, via overriding the following maps:
*
* $button-theme-names - The names of the colour themes available to buttons. Should start with two dashes.
* $button-type-theme-maps - The names of the possible button types, and the maps that should be used for their themes.
* $button-size-map - The possible button sizes.
* $button-default-styles - The default button styles to apply to all buttons.
*/
$button-size-map: (
null: (
padding: 1rem,
),
'--tiny': (
font-size: 80%,
padding: 0.225rem,
),
'--small': (
font-size: 90%,
padding: 0.5rem,
),
'--large': (
font-size: 110%,
padding: 1.5rem,
),
'--huge': (
font-size: 120%,
padding: 1.75rem,
),
) !default;
$button-theme-names: ('--primary', '--secondary', '--white', '--black') !default;
$default-theme-map: (
'all': (
'disabled': (
opacity: 0.5,
),
),
'--primary': (
'styles': (
background-color: color('primary'),
border: 1px solid color('primary'),
color: color('white'),
),
'hover': (
background-color: lighten(color('primary'), 10%),
border-color: lighten(color('primary'), 10%),
),
),
'--secondary': (
'styles': (
background-color: color('secondary'),
border: 1px solid color('secondary'),
color: color('black'),
),
'hover': (
background-color: lighten(color('secondary'), 10%),
border-color: lighten(color('secondary'), 10%),
),
),
'--white': (
'styles': (
background-color: color('white'),
border: 1px solid color('white'),
color: color('black'),
),
'hover': (
background-color: darken(color('white'), 5%),
border-color: darken(color('white'), 5%),
),
),
'--black': (
'styles': (
background-color: color('black'),
border: 1px solid color('black'),
color: color('white'),
),
'hover': (
background-color: lighten(color('black'), 15%),
border-color: lighten(color('black'), 15%),
),
),
) !default;
$outline-theme-map: (
'all': (
'styles': (
background-color: transparent,
),
'disabled': (
opacity: 0.5,
),
),
'--primary': (
'styles': (
color: color('primary'),
),
'hover': map-deep-get($default-theme-map, '--primary', 'styles'),
),
'--secondary': (
'styles': (
color: color('secondary'),
),
'hover': map-deep-get($default-theme-map, '--secondary', 'styles'),
),
'--white': (
'styles': (
color: color('white'),
),
'hover': map-deep-get($default-theme-map, '--white', 'styles'),
),
'--black': (
'styles': (
color: color('black'),
),
'hover': map-deep-get($default-theme-map, '--black', 'styles'),
),
) !default;
$text-theme-map: (
'all': (
'styles': (
background: none,
border: none,
),
'disabled': (
opacity: 0.5,
),
),
'--primary': (
'styles': (
color: color('primary'),
),
'hover': (
background-color: color('white-two'),
),
),
'--secondary': (
'styles': (
color: color('secondary'),
),
'hover': (
background-color: color('white-two'),
),
),
'--white': (
'styles': (
color: color('white'),
),
'hover': (
background-color: color('white-two'),
),
),
'--black': (
'styles': (
color: color('black'),
),
'hover': (
background-color: color('white-two'),
),
),
) !default;
$button-type-theme-maps: (
null: $default-theme-map,
'--outlined': $outline-theme-map,
'--text': $text-theme-map,
) !default;
$button-default-styles: (
border: 1px solid transparent,
border-radius: 2px,
outline: none,
) !default;
.v-btn {
cursor: pointer;
@include styles-from-map($button-default-styles);
}
.v-btn--block {
display: block;
width: 100%;
}
.v-btn--disabled {
cursor: not-allowed;
}
.v-btn--loading {
pointer-events: none;
position: relative;
.v-btn__slot {
color: transparent;
}
&::after {
animation: animSpin 0.5s infinite linear;
border-radius: 50%;
border: 2px solid currentColor;
border-right-color: transparent;
border-top-color: transparent;
content: '';
display: block;
height: 1em;
left: calc(50% - 0.5em);
position: absolute;
top: calc(50% - 0.5em);
width: 1em;
}
}
/** Sizes */
@each $size-name, $style-map in $button-size-map {
.v-btn#{$size-name} {
@include styles-from-map($style-map);
}
}
@each $type-name, $theme-map in $button-type-theme-maps {
$all: ();
@if map-has-key($theme-map, 'all') {
$all: map-get($theme-map, 'all');
}
.v-btn#{$type-name} {
@each $theme-name in $button-theme-names {
$theme: recursive-map-merge($all, map-get($theme-map, $theme-name));
&.v-btn#{$theme-name} {
@include styles-from-map-quietly(map-get($theme, 'styles'));
&:hover:not(.v-btn--disabled) {
@include styles-from-map-quietly(map-get($theme, 'hover'));
}
&.v-btn--disabled {
@include styles-from-map-quietly(map-get($theme, 'disabled'));
}
}
}
}
}
</style>
/** You will usually want to use styles-from-map, unless you're trying to support a map that may not have the keys requested. */
@mixin styles-from-map-quietly($map) {
@each $style, $value in $map {
@if (type-of($value) == 'map') {
#{$style} {
@include styles-from-map($value);
}
} @else {
#{$style}: #{$value};
}
}
}
@mixin styles-from-map($map) {
@if type-of($map) == 'null' {
@warn 'Value passed to mixin styles-from-map is null. No styles will be outputted';
}
@if type-of($map) != 'map' and type-of($map) != 'null' {
@error 'Value passed to mixin styles-from-map must be a map - #{$map}';
}
@include styles-from-map-quietly($map);
}
@function map-deep-get($_map, $_keys...) {
@each $_key in $_keys {
@if not map-has-key($_map, $_key) {
@warn 'Key `#{$_key}` not found in map `#{$_map}`. Will return null';
}
$_map: map-get($_map, $_key);
}
@return $_map;
}
@function recursive-map-merge($map1, $map2, $config:() ) {
@if (type-of($map1) == map or (type-of($map1) == list and length($map1) == 0)) and (type-of($map2) == map or (type-of($map2) == list and length($map2) == 0)) {
$result: $map1;
@each $key, $value in $map2 {
@if (type-of(map-get($result, $key)) == map and type-of($value) == map) {
$result: map-merge($result, ($key: recursive-map-merge(map-get($result, $key), $value)));
}
@else if (map-get($config, "merge-lists") and type-of(map-get($result, $key)) == list and type-of($value) == list) {
$result: map-merge($result, ($key: join(map-get($result, $key), $value)));
}
@else {
$result: map-merge($result, ($key: $value));
}
}
@return $result;
}
@else {
@error "recursive-map-merge() expects parameters to be of type map!";
@return null;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment