1
0

feat: create BaseButton component (#1123)

Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/1123
Reviewed-by: konrad <k@knt.li>
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
This commit is contained in:
Dominik Pschenitschni
2022-01-04 18:58:06 +00:00
parent cb37fd773d
commit cdbd1c2ac4
39 changed files with 254 additions and 146 deletions

View File

@ -0,0 +1,118 @@
<template>
<component
:is="componentNodeName"
class="base-button"
:class="{ 'base-button--type-button': isButton }"
v-bind="elementBindings"
:disabled="disabled || undefined"
>
<slot />
</component>
</template>
<script lang="ts">
// see https://v3.vuejs.org/api/sfc-script-setup.html#usage-alongside-normal-script
export default {
inheritAttrs: false,
}
</script>
<script lang="ts" setup>
// this component removes styling differences between links / vue-router links and button elements
// by doing so we make it easy abstract the functionality from style and enable easier and semantic
// correct button and link usage. Also see: https://css-tricks.com/a-complete-guide-to-links-and-buttons/#accessibility-considerations
// the component tries to heuristically determine what it should be checking the props (see the
// componentNodeName and elementBindings ref for this).
// NOTE: Do NOT use buttons with @click to push routes. => Use router-links instead!
import { ref, watchEffect, computed, useAttrs, PropType } from 'vue'
const BASE_BUTTON_TYPES_MAP = Object.freeze({
button: 'button',
submit: 'submit',
})
type BaseButtonTypes = keyof typeof BASE_BUTTON_TYPES_MAP
const props = defineProps({
type: {
type: String as PropType<BaseButtonTypes>,
default: 'button',
},
disabled: {
type: Boolean,
default: false,
},
})
const componentNodeName = ref<Node['nodeName']>('button')
interface ElementBindings {
type?: string;
rel?: string,
}
const elementBindings = ref({})
const attrs = useAttrs()
watchEffect(() => {
// by default this component is a button element with the attribute of the type "button" (default prop value)
let nodeName = 'button'
let bindings: ElementBindings = {type: props.type}
// if we find a "to" prop we set it as router-link
if ('to' in attrs) {
nodeName = 'router-link'
bindings = {}
}
// if there is a href we assume the user wants an external link via a link element
// we also set the attribute rel to "noopener" but make it possible to overwrite this by the user.
if ('href' in attrs) {
nodeName = 'a'
bindings = {rel: 'noopener'}
}
componentNodeName.value = nodeName
elementBindings.value = {
...bindings,
...attrs,
}
})
const isButton = computed(() => componentNodeName.value === 'button')
</script>
<style lang="scss">
// NOTE: we do not use scoped styles to reduce specifity and make it easy to overwrite
// We reset the default styles of a button element to enable easier styling
:where(.base-button--type-button) {
border: 0;
margin: 0;
padding: 0;
text-decoration: none;
background-color: transparent;
text-align: center;
appearance: none;
}
:where(.base-button) {
cursor: pointer;
display: block;
color: inherit;
font: inherit;
user-select: none;
pointer-events: auto; // disable possible resets
&:focus {
outline: transparent;
}
&[disabled] {
cursor: default;
}
}
</style>

View File

@ -37,7 +37,7 @@
<dropdown class="is-right" ref="usernameDropdown">
<template #trigger>
<x-button
type="secondary"
variant="secondary"
:shadow="false">
<span class="username">{{ userInfo.name !== '' ? userInfo.name : userInfo.username }}</span>
<span class="icon is-small">

View File

@ -1,79 +1,64 @@
<template>
<a
<BaseButton
class="button"
:class="{
'is-loading': loading,
'has-no-shadow': !shadow,
'is-primary': type === 'primary',
'is-outlined': type === 'secondary',
'is-text is-inverted has-no-shadow underline-none':
type === 'tertary',
}"
:disabled="disabled || null"
@click="click"
:href="href !== '' ? href : null"
:class="[
variantClass,
{
'is-loading': loading,
'has-no-shadow': !shadow || variant === 'tertiary',
}
]"
>
<icon :icon="icon" v-if="showIconOnly"/>
<span class="icon is-small" v-else-if="icon !== ''">
<icon :icon="icon"/>
</span>
<slot></slot>
</a>
<slot />
</BaseButton>
</template>
<script>
<script lang="ts">
export default {
name: 'x-button',
props: {
type: {
type: String,
default: 'primary',
},
href: {
type: String,
default: '',
},
to: {
default: false,
},
icon: {
default: '',
},
loading: {
type: Boolean,
default: false,
},
shadow: {
type: Boolean,
default: true,
},
disabled: {
type: Boolean,
default: false,
},
},
emits: ['click'],
computed: {
showIconOnly() {
return this.icon !== '' && typeof this.$slots.default === 'undefined'
},
},
methods: {
click(e) {
if (this.disabled) {
return
}
if (this.to !== false) {
this.$router.push(this.to)
}
this.$emit('click', e)
},
},
}
</script>
<script setup lang="ts">
import {computed, useSlots, PropType} from 'vue'
import BaseButton from '@/components/base/BaseButton.vue'
const BUTTON_TYPES_MAP = Object.freeze({
primary: 'is-primary',
secondary: 'is-outlined',
tertiary: 'is-text is-inverted underline-none',
})
type ButtonTypes = keyof typeof BUTTON_TYPES_MAP
const props = defineProps({
variant: {
type: String as PropType<ButtonTypes>,
default: 'primary',
},
icon: {
default: '',
},
loading: {
type: Boolean,
default: false,
},
shadow: {
type: Boolean,
default: true,
},
})
const variantClass = computed(() => BUTTON_TYPES_MAP[props.variant])
const slots = useSlots()
const showIconOnly = computed(() => props.icon !== '' && typeof slots.default === 'undefined')
</script>
<style lang="scss" scoped>
.button {
transition: all $transition;
@ -83,8 +68,8 @@ export default {
font-weight: bold;
height: $button-height;
box-shadow: var(--shadow-sm);
display: inline-flex;
&.is-hovered,
&:hover {
box-shadow: var(--shadow-md);
}
@ -106,9 +91,10 @@ export default {
color: var(--white);
}
&.is-small {
border-radius: $radius;
}
}
.is-small {
border-radius: $radius;
}
.underline-none {

View File

@ -27,7 +27,7 @@
@click="reset"
class="is-small ml-2"
:shadow="false"
type="secondary"
variant="secondary"
>
{{ $t('input.resetColor') }}
</x-button>

View File

@ -101,6 +101,7 @@
class="is-fullwidth"
:shadow="false"
@click="close"
v-cy="'closeDatepicker'"
>
{{ $t('misc.confirm') }}
</x-button>

View File

@ -35,7 +35,7 @@
<a @click="toggleEdit">{{ $t('input.editor.edit') }}</a>
</li>
</ul>
<x-button v-else-if="isEditActive" @click="toggleEdit" type="secondary" :shadow="false">
<x-button v-else-if="isEditActive" @click="toggleEdit" variant="secondary" :shadow="false" v-cy="'saveEditor'">
{{ $t('misc.save') }}
</x-button>
</template>

View File

@ -1,7 +1,7 @@
<template>
<x-button
v-if="hasFilters"
type="secondary"
variant="secondary"
@click="clearFilters"
>
{{ $t('filters.clear') }}
@ -10,7 +10,7 @@
<template #trigger="{toggle}">
<x-button
@click.prevent.stop="toggle()"
type="secondary"
variant="secondary"
icon="filter"
>
{{ $t('filters.title') }}

View File

@ -14,25 +14,25 @@
</div>
<footer class="modal-card-foot is-flex is-justify-content-flex-end">
<x-button
v-if="tertiary !== ''"
:shadow="false"
type="tertary"
@click.prevent.stop="$emit('tertary')"
v-if="tertary !== ''"
variant="tertiary"
@click.prevent.stop="$emit('tertiary')"
>
{{ tertary }}
{{ tertiary }}
</x-button>
<x-button
type="secondary"
variant="secondary"
@click.prevent.stop="$router.back()"
>
{{ $t('misc.cancel') }}
</x-button>
<x-button
type="primary"
v-if="primaryLabel !== ''"
variant="primary"
@click.prevent.stop="primary"
:icon="primaryIcon"
:disabled="primaryDisabled"
v-if="primaryLabel !== ''"
>
{{ primaryLabel }}
</x-button>
@ -65,7 +65,7 @@ export default {
type: Boolean,
default: false,
},
tertary: {
tertiary: {
type: String,
default: '',
},
@ -78,7 +78,7 @@ export default {
default: false,
},
},
emits: ['create', 'primary', 'tertary'],
emits: ['create', 'primary', 'tertiary'],
methods: {
primary() {
this.$emit('create')

View File

@ -26,7 +26,7 @@
@click="action.callback"
:shadow="false"
class="is-small"
type="secondary"
variant="secondary"
v-for="(action, i) in item.data.actions"
>
{{ action.title }}

View File

@ -1,6 +1,6 @@
<template>
<x-button
type="secondary"
variant="secondary"
:icon="icon"
v-tooltip="tooltipText"
@click="changeSubscription"

View File

@ -31,14 +31,15 @@
<div class="actions">
<x-button
@click="$emit('close')"
type="tertary"
variant="tertiary"
class="has-text-danger"
>
{{ $t('misc.cancel') }}
</x-button>
<x-button
@click="$emit('submit')"
type="primary"
variant="primary"
v-cy="'modalPrimary'"
:shadow="false"
>
{{ $t('misc.doit') }}

View File

@ -83,7 +83,7 @@
@click="$refs.files.click()"
class="mb-4"
icon="cloud-upload-alt"
type="secondary"
variant="secondary"
:shadow="false"
>
{{ $t('task.attachment.upload') }}

View File

@ -8,21 +8,21 @@
<x-button
@click.prevent.stop="() => deferDays(1)"
:shadow="false"
type="secondary"
variant="secondary"
>
{{ $t('task.deferDueDate.1day') }}
</x-button>
<x-button
@click.prevent.stop="() => deferDays(3)"
:shadow="false"
type="secondary"
variant="secondary"
>
{{ $t('task.deferDueDate.3days') }}
</x-button>
<x-button
@click.prevent.stop="() => deferDays(7)"
:shadow="false"
type="secondary"
variant="secondary"
>
{{ $t('task.deferDueDate.1week') }}
</x-button>

View File

@ -6,7 +6,7 @@
class="is-pulled-right add-task-relation-button"
:class="{'is-active': showNewRelationForm}"
v-tooltip="$t('task.relation.add')"
type="secondary"
variant="secondary"
icon="plus"
:shadow="false"
/>

View File

@ -1,9 +1,9 @@
<template>
<div class="control repeat-after-input">
<div class="buttons has-addons is-centered mt-2">
<x-button type="secondary" class="is-small" @click="() => setRepeatAfter(1, 'days')">{{ $t('task.repeat.everyDay') }}</x-button>
<x-button type="secondary" class="is-small" @click="() => setRepeatAfter(1, 'weeks')">{{ $t('task.repeat.everyWeek') }}</x-button>
<x-button type="secondary" class="is-small" @click="() => setRepeatAfter(1, 'months')">{{ $t('task.repeat.everyMonth') }}</x-button>
<x-button variant="secondary" class="is-small" @click="() => setRepeatAfter(1, 'days')">{{ $t('task.repeat.everyDay') }}</x-button>
<x-button variant="secondary" class="is-small" @click="() => setRepeatAfter(1, 'weeks')">{{ $t('task.repeat.everyWeek') }}</x-button>
<x-button variant="secondary" class="is-small" @click="() => setRepeatAfter(1, 'months')">{{ $t('task.repeat.everyMonth') }}</x-button>
</div>
<div class="is-flex is-align-items-center mb-2">
<label for="repeatMode" class="is-fullwidth">