feat(NcButton): Add `pressed` state for stateful buttons, like the `NcAppSidebar` star button
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
Родитель
3c1bd764cc
Коммит
a7d9687b15
|
@ -32,6 +32,7 @@ include a standard-header like it's used by the files app.
|
|||
```vue
|
||||
<template>
|
||||
<NcAppSidebar
|
||||
:starred="starred"
|
||||
name="cat-picture.jpg"
|
||||
subname="last edited 3 weeks ago">
|
||||
<NcAppSidebarTab name="Search" id="search-tab">
|
||||
|
@ -65,6 +66,11 @@ include a standard-header like it's used by the files app.
|
|||
Cog,
|
||||
ShareVariant,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
starred: false,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
@ -376,6 +382,7 @@ export default {
|
|||
<slot name="tertiary-actions">
|
||||
<NcButton v-if="canStar"
|
||||
:aria-label="favoriteTranslated"
|
||||
:pressed="isStarred"
|
||||
class="app-sidebar-header__star"
|
||||
type="secondary"
|
||||
@click.prevent="toggleStarred">
|
||||
|
@ -974,7 +981,7 @@ $top-buttons-spacing: 6px;
|
|||
.app-sidebar-header__star {
|
||||
// Override default Button component styles
|
||||
box-shadow: none;
|
||||
&:hover {
|
||||
&:not([aria-pressed='true']):hover {
|
||||
box-shadow: none;
|
||||
background-color: var(--color-background-hover);
|
||||
}
|
||||
|
|
|
@ -196,6 +196,66 @@ button {
|
|||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
### Pressed state
|
||||
It is possible to make the button stateful by adding a pressed state, e.g. if you like to create a favorite button.
|
||||
The button will have the required `aria` attribute for accessibility and visual style (`primary` when pressed, and the configured type otherwise).
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<div style="display: flex; gap: 12px;">
|
||||
<NcButton :pressed.sync="isFavorite" :aria-label="ariaLabel" type="tertiary-no-background">
|
||||
<template #icon>
|
||||
<IconStar v-if="isFavorite" :size="20" />
|
||||
<IconStarOutline v-else :size="20" />
|
||||
</template>
|
||||
</NcButton>
|
||||
<NcButton :pressed.sync="isFavorite" :aria-label="ariaLabel" type="tertiary">
|
||||
<template #icon>
|
||||
<IconStar v-if="isFavorite" :size="20" />
|
||||
<IconStarOutline v-else :size="20" />
|
||||
</template>
|
||||
</NcButton>
|
||||
<NcButton :pressed.sync="isFavorite" :aria-label="ariaLabel">
|
||||
<template #icon>
|
||||
<IconStar v-if="isFavorite" :size="20" />
|
||||
<IconStarOutline v-else :size="20" />
|
||||
</template>
|
||||
</NcButton>
|
||||
</div>
|
||||
<div>
|
||||
It is {{ isFavorite ? 'a' : 'not a' }} favorite.
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import IconStar from 'vue-material-design-icons/Star.vue'
|
||||
import IconStarOutline from 'vue-material-design-icons/StarOutline.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
IconStar,
|
||||
IconStarOutline,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isFavorite: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
ariaLabel() {
|
||||
return this.isFavorite ? 'Remove as favorite' : 'Add as favorite'
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleFavorite() {
|
||||
this.isFavorite = !this.isFavorite
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
```
|
||||
</docs>
|
||||
|
||||
<script>
|
||||
|
@ -299,6 +359,35 @@ export default {
|
|||
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: {
|
||||
/**
|
||||
* 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
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -332,7 +421,7 @@ export default {
|
|||
'button-vue--icon-only': hasIcon && !hasText,
|
||||
'button-vue--text-only': hasText && !hasIcon,
|
||||
'button-vue--icon-and-text': hasIcon && hasText,
|
||||
[`button-vue--vue-${this.type}`]: this.type,
|
||||
[`button-vue--vue-${this.realType}`]: this.realType,
|
||||
'button-vue--wide': this.wide,
|
||||
active: isActive,
|
||||
'router-link-exact-active': isExactActive,
|
||||
|
@ -340,6 +429,7 @@ export default {
|
|||
],
|
||||
attrs: {
|
||||
'aria-label': this.ariaLabel,
|
||||
'aria-pressed': this.pressed,
|
||||
disabled: this.disabled,
|
||||
type: this.href ? null : this.nativeType,
|
||||
role: this.href ? 'button' : null,
|
||||
|
@ -352,6 +442,15 @@ export default {
|
|||
on: {
|
||||
...this.$listeners,
|
||||
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 call the listeners click handler
|
||||
this.$listeners?.click?.($event)
|
||||
navigate?.($event)
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* @copyright Copyright (c) 2023 Ferdinand Thiessen <opensource@fthiessen.de>
|
||||
*
|
||||
* @author Ferdinand Thiessen <opensource@fthiessen.de>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import NcButton from '../../../../src/components/NcButton/NcButton.vue'
|
||||
|
||||
describe('NcButton', () => {
|
||||
it('emits update:pressed', async () => {
|
||||
const wrapper = shallowMount(NcButton, { propsData: { pressed: true, ariaLabel: 'button' } })
|
||||
wrapper.findComponent('button').trigger('click')
|
||||
expect(wrapper.emitted('update:pressed')?.length).toBe(1)
|
||||
expect(wrapper.emitted('update:pressed')[0]).toEqual([false])
|
||||
|
||||
// Now the same but when pressed was false
|
||||
await wrapper.setProps({ pressed: false })
|
||||
wrapper.findComponent('button').trigger('click')
|
||||
expect(wrapper.emitted('update:pressed')?.length).toBe(2)
|
||||
expect(wrapper.emitted('update:pressed')[1]).toEqual([true])
|
||||
})
|
||||
|
||||
it('does not emit update:pressed when not configured', async () => {
|
||||
const wrapper = shallowMount(NcButton, { propsData: { ariaLabel: 'button' } })
|
||||
wrapper.findComponent('button').trigger('click')
|
||||
expect(wrapper.emitted('update:pressed')).toBe(undefined)
|
||||
})
|
||||
})
|
Загрузка…
Ссылка в новой задаче