Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(NcButton): Add pressed state for stateful buttons #4344

Merged
merged 3 commits into from Jul 19, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 5 additions & 1 deletion cypress/visual/NcAppSidebar/NcAppSidebarMixin.js
Expand Up @@ -96,7 +96,11 @@ export default function(compact) {

it('Renders ' + fileName, () => {
cy.mount(NcAppSidebar, defaultOptions)
cy.get('.app-sidebar-header').compareSnapshot(fileName)
cy.get('.app-sidebar-header')
// Ensure cursor is not displayed to prevent flaky tests (flashing input cursor)
.invoke('css', 'caret-color', 'transparent')
// Compare to "golden" standard
.compareSnapshot(fileName)
})
})
})
Expand Down
9 changes: 8 additions & 1 deletion src/components/NcAppSidebar/NcAppSidebar.vue
Expand Up @@ -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">
Expand Down Expand Up @@ -65,6 +66,11 @@ include a standard-header like it's used by the files app.
Cog,
ShareVariant,
},
data() {
return {
starred: false,
}
},
}
</script>
```
Expand Down Expand Up @@ -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">
Expand Down Expand Up @@ -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);
}
Expand Down
101 changes: 100 additions & 1 deletion src/components/NcButton/NcButton.vue
Expand Up @@ -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
},
},
susnux marked this conversation as resolved.
Show resolved Hide resolved
}
</script>
```
</docs>

<script>
Expand Down Expand Up @@ -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
},
},

/**
Expand Down Expand Up @@ -332,14 +421,15 @@ 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,
},
],
attrs: {
'aria-label': this.ariaLabel,
'aria-pressed': this.pressed,
disabled: this.disabled,
type: this.href ? null : this.nativeType,
role: this.href ? 'button' : null,
Expand All @@ -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)
Expand Down
45 changes: 45 additions & 0 deletions tests/unit/components/NcButton/button.spec.js
@@ -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)
})
})