tethys.backend/resources/js/Components/NcButton.vue

545 lines
15 KiB
Vue
Raw Normal View History

<template>
<component :is="is" :class="componentClass" :target="target" href="href" aria-label="ariaLabel" aria-pressed="pressed"
:disabled="disabled" :type="type" v-on:click="click">
<span class="button-vue__wrapper">
<!-- <span v-if="hasIcon" class="button-vue__icon" aria-hidden="true">
{{ this.$slots.icon }}-->
<BaseIcon v-if="icon" :path="icon" class="button-vue__icon" aria-hidden="true" :size="size" />
<!-- <span v-if="hasText" class="button-vue__text">{{ this.$slots.default }}</span> -->
<span v-if="label" class="font-bold whitespace-nowrap text-ellipsis overflow-hidden" :class="labelClass">{{
label }}</span>
</span>
</component>
</template>
<script lang="ts">
import BaseIcon from '@/Components/BaseIcon.vue';
// import { h } from 'vue';
export default {
name: 'NcButton',
components: {
BaseIcon
},
props: {
size: {
type: [String, Number],
default: 16,
},
small: Boolean,
roundedFull: {
type: Boolean,
default: false,
},
label: {
type: [String, Number],
default: null,
},
icon: {
type: String,
default: null,
},
/**
* Set the text and icon alignment
*
* @default 'center'
*/
alignment: {
type: String,
default: 'center',
validator: (alignment: string) => ['start', 'start-reverse', 'center', 'center-reverse', 'end', 'end-reverse'].includes(alignment),
},
/**
* Toggles the disabled state of the button on and off.
*/
disabled: {
type: Boolean,
default: false,
},
/**
* Specifies the button type
* Accepted values: primary, secondary, tertiary, tertiary-no-background, tertiary-on-primary, error, warning, success. If left empty,
* the default button style will be applied.
*/
type: {
type: String,
validator(value: string) {
return (
[
'primary',
'secondary',
'tertiary',
'tertiary-no-background',
'tertiary-on-primary',
'error',
'warning',
'success',
].indexOf(value) !== -1
);
},
default: 'secondary',
},
/**
* Specifies the button native type
* Accepted values: submit, reset, button. If left empty,
* the default "button" type will be used.
*/
nativeType: {
type: String,
validator(value: string) {
return ['submit', 'reset', 'button'].indexOf(value) !== -1;
},
default: 'button',
},
/**
* Specifies whether the button should span all the available width.
* By default, buttons span the whole width of the container.
*/
wide: {
type: Boolean,
default: false,
},
/**
* Always try to provide an aria-label to your button. Make it more
* specific than the button's name by provide some more context. E.g. if
* the name of the button is "send" in the Mail app, the aria label could
* be "Send email".
*/
ariaLabel: {
type: String,
default: null,
},
/**
* Providing the href attribute turns the button component into an `a`
* element.
*/
href: {
type: String,
default: null,
},
/**
* Providing the download attribute with href downloads file when clicking.
*/
download: {
type: String,
default: null,
},
/**
* Providing the to attribute turns the button component into a `router-link`
* element. Takes precedence over the href attribute.
*/
to: {
type: [String, Object],
default: null,
},
/**
* Pass in `true` if you want the matching behaviour of `router-link` to
* be non-inclusive: https://router.vuejs.org/api/#exact
*/
exact: {
type: Boolean,
default: false,
},
/**
* @deprecated To be removed in @nextcloud/vue 9. Migration guide: remove ariaHidden prop from NcAction* components.
* @todo Add a check in @nextcloud/vue 9 that this prop is not provided,
* otherwise root element will inherit incorrect aria-hidden.
*/
ariaHidden: {
type: Boolean,
default: null,
},
/**
* The pressed state of the button if it has a checked state
* This will add the `aria-pressed` attribute and for the button to have the primary style in checked state.
*/
pressed: {
type: Boolean,
default: null,
},
},
emits: ['update:pressed', 'click'],
computed: {
labelClass() {
return (this.small && this.icon) ? 'px-1' : 'px-2';
},
is() {
if (this.href) {
return 'a';
}
return 'button';
},
hasText() {
// return !!this.$slots.default;
return this.label != undefined;
},
hasIcon() {
// return this.$slots?.icon;
return this.icon != undefined;
},
// type() {
// return this.href ? null : this.nativeType;
// },
componentClass() {
const base = [
'button-vue',
this.roundedFull ? 'rounded-full' : 'rounded',
'inline-flex',
'cursor-pointer',
'justify-center',
'items-center',
'whitespace-nowrap',
'focus:outline-none',
'transition-colors',
// 'focus:ring-2',
{
'button-vue--icon-only': this.hasIcon && !this.hasText,
'button-vue--text-only': this.hasText && !this.hasIcon,
'button-vue--icon-and-text': this.hasIcon && this.hasText,
[`button-vue--vue-${this.realType}`]: this.realType,
'button-vue--wide': this.wide,
[`button-vue--${this.flexAlignment}`]: this.flexAlignment !== 'center',
'button-vue--reverse': this.isReverseAligned,
// Add other classes based on conditions as needed
},
];
if (this.small) {
base.push('text-sm', this.roundedFull ? 'px-3 py-1' : 'p-1');
} else {
base.push('py-2', this.roundedFull ? 'px-6' : 'px-3');
}
return base;
},
target() {
return (!this.to && this.href) ? '_self' : null;
},
/**
* The real type to be used for the button, enforces `primary` for pressed state and, if stateful button, any other type for not pressed state
* Otherwise the type property is used.
*/
realType() {
// Force *primary* when pressed
if (this.pressed) {
return 'primary';
}
// If not pressed but button is configured as stateful button then the type must not be primary
if (this.pressed === false && this.type === 'primary') {
return 'secondary';
}
return this.type;
},
/**
* The flexbox alignment of the button content
*/
flexAlignment() {
return this.alignment.split('-')[0];
},
/**
* If the button content should be reversed (icon on the end)
*/
isReverseAligned() {
return this.alignment.includes('-');
},
},
methods: {
click($event) {
// Update pressed prop on click if it is set
if (typeof this.pressed === 'boolean') {
/**
* Update the current pressed state of the button (if the `pressed` property was configured)
*
* @property {boolean} newValue The new `pressed`-state
*/
this.$emit('update:pressed', !this.pressed)
}
// We have to both navigate and emit the click event
this.$emit('click', $event)
// navigate?.($event)
},
},
};
</script>
<style lang="css" scoped>
.button-vue {
position: relative;
/* width: fit-content;
overflow: hidden;
border: 0; */
padding: 0;
/* font-size: var(--default-font-size);
font-weight: bold; */
min-height: 44px;
min-width: 44px;
display: flex;
align-items: center;
/* justify-content: center; */
/* Cursor pointer on element and all children */
cursor: pointer;
& *,
span {
cursor: pointer;
}
/* border-radius: math.div($clickable-area, 2); */
/* transition-property: color, */
/* border-color,
background-color;
transition-duration: 0.1s;
transition-timing-function: linear; */
/* border-radius: math.div($clickable-area, 2); */
/* border-radius: calc(50% - var(--clickable-area) / 2);
transition-property: color, border-color, background-color;
transition-duration: 0.1s;
transition-timing-function: linear; */
/* No outline feedback for focus. Handled with a toggled class in js (see data) */
&:focus {
outline: none;
}
&:disabled {
cursor: default;
& * {
cursor: default;
}
opacity: 0.5;
/* // Gives a wash out effect */
filter: saturate(0.7l);
}
/* // Default button type */
color: var(--color-primary-element-light-text);
background-color: var(--color-primary-element-light);
&:hover:not(:disabled) {
background-color: var(--color-primary-element-light-hover);
}
/* Back to the default color for this button when active */
/* TODO: add ripple effect */
&:active {
background-color: var(--color-primary-element-light);
}
&__wrapper {
display: inline-flex;
align-items: center;
justify-content: center;
width: 100%;
}
&--end &__wrapper {
justify-content: end;
}
&--start &__wrapper {
justify-content: start;
}
&--reverse &__wrapper {
flex-direction: row-reverse;
}
&--reverse &--icon-and-text {
padding-inline: calc(var(--default-grid-baseline) * 4) var(--default-grid-baseline);
}
&__icon {
height: 44px;
width: 44px;
min-height: 44px;
min-width: 44px;
display: flex;
justify-content: center;
align-items: center;
}
/* &__text {
font-weight: bold;
margin-bottom: 1px;
padding: 2px 0;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
} */
/* Icon-only button */
&--icon-only {
width: 44px !important;
}
/* Text-only button */
&--text-only {
padding: 0 12px;
& .button-vue__text {
margin-left: 4px;
margin-right: 4px;
}
}
/* Icon and text button */
&--icon-and-text {
padding-block: 0;
padding-inline: var(--default-grid-baseline) calc(var(--default-grid-baseline) * 4);
}
/* Wide button spans the whole width of the container */
&--wide {
width: 100%;
}
&:focus-visible {
outline: 2px solid var(--color-main-text) !important;
box-shadow: 0 0 0 4px var(--color-main-background) !important;
&.button-vue--vue-tertiary-on-primary {
outline: 2px solid var(--color-primary-element-text);
border-radius: var(--border-radius);
background-color: transparent;
}
}
/* Button types */
/* // Primary */
&--vue-primary {
background-color: var(--color-primary-element);
color: var(--color-primary-element-text);
&:hover:not(:disabled) {
background-color: var(--color-primary-element-hover);
}
/* Back to the default color for this button when active
TODO: add ripple effect */
&:active {
background-color: var(--color-primary-element);
}
}
/* Secondary */
&--vue-secondary {
color: var(--color-primary-element-light-text);
background-color: var(--color-primary-element-light);
&:hover:not(:disabled) {
color: var(--color-primary-element-light-text);
background-color: var(--color-primary-element-light-hover);
}
}
/* Tertiary */
&--vue-tertiary {
color: var(--color-main-text);
background-color: transparent;
&:hover:not(:disabled) {
background-color: var(--color-background-hover);
}
}
/* Tertiary, no background */
&--vue-tertiary-no-background {
color: var(--color-main-text);
background-color: transparent;
&:hover:not(:disabled) {
background-color: transparent;
}
}
/* Tertiary on primary color (like the header) */
&--vue-tertiary-on-primary {
color: var(--color-primary-element-text);
background-color: transparent;
&:hover:not(:disabled) {
background-color: transparent;
}
}
/* Success */
&--vue-success {
background-color: var(--color-success);
color: white;
&:hover:not(:disabled) {
background-color: var(--color-success-hover);
}
/* Back to the default color for this button when active
: add ripple effect */
&:active {
background-color: var(--color-success);
}
}
/* Warning */
&--vue-warning {
background-color: var(--color-warning);
color: white;
&:hover:not(:disabled) {
background-color: var(--color-warning-hover);
}
/* Back to the default color for this button when active
TODO: add ripple effect */
&:active {
background-color: var(--color-warning);
}
}
/* Error */
&--vue-error {
background-color: var(--color-error);
color: white;
&:hover:not(:disabled) {
background-color: var(--color-error-hover);
}
/* Back to the default color for this button when active
TODO: add ripple effect */
&:active {
background-color: var(--color-error);
}
}
}
</style>