Skip to content

Commit

Permalink
Merge pull request #4344 from nextcloud/fix/ncbutton-pressed-style
Browse files Browse the repository at this point in the history
feat(NcButton): Add `pressed` state for stateful buttons
  • Loading branch information
susnux committed Jul 19, 2023
2 parents 10ed2a3 + 90400c1 commit 04945bc
Show file tree
Hide file tree
Showing 68 changed files with 158 additions and 3 deletions.
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
},
},
}
</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)
})
})

0 comments on commit 04945bc

Please sign in to comment.