Skip to content

Commit

Permalink
Merge pull request #4744 from nextcloud-libraries/feat/action-button-…
Browse files Browse the repository at this point in the history
…active

feat(NcActionButton): Allow pressed state on NcActionButton - similar to NcButton
  • Loading branch information
susnux authored Jan 23, 2024
2 parents 3ba708c + 00b1c74 commit 01e1067
Show file tree
Hide file tree
Showing 5 changed files with 275 additions and 23 deletions.
2 changes: 1 addition & 1 deletion src/assets/action.scss
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
*/

@mixin action-active {
li {
li.action {
&.active {
background-color: var(--color-background-hover);
border-radius: 6px;
Expand Down
249 changes: 234 additions & 15 deletions src/components/NcActionButton/NcActionButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -170,17 +170,127 @@ export default {
}
</script>
```

### With different model behavior
By default the button will act like a normal button, but it is also possible to change the behavior to a toggle button, checkbox button or radio button.

For example to have the button act like a toggle button just set the `modelValue` property to the toggle state:

```vue
<template>
<NcActions>
<NcActionButton :model-value.sync="fullscreen">
<template #icon>
<Fullscreen :size="20" />
</template>
Fullscreen
</NcActionButton>
</NcActions>
</template>
<script>
import Fullscreen from 'vue-material-design-icons/Fullscreen.vue'
export default {
components: {
Fullscreen,
},
data() {
return {
fullscreen: true,
}
},
}
</script>
```

Another example would be using it with checkbox semantics, to enable or disable features.
This also allows tri-state behavior (`true`, `false`, `null`) in which case `aria-checked` will be either `true`, `false` or `mixed`.

```vue
<template>
<NcActions>
<NcActionButton :model-value.sync="handRaised" type="checkbox">
<template #icon>
<HandBackLeft :size="20" />
</template>
Raise hand
</NcActionButton>
<NcActionButton :model-value.sync="fullscreen" type="checkbox">
<template #icon>
<Fullscreen :size="20" />
</template>
Fullscreen
</NcActionButton>
</NcActions>
</template>
<script>
import HandBackLeft from 'vue-material-design-icons/HandBackLeft.vue'
import Fullscreen from 'vue-material-design-icons/Fullscreen.vue'
export default {
components: {
HandBackLeft,
Fullscreen,
},
data() {
return {
fullscreen: true,
handRaised: false,
}
},
}
</script>
```

It is also possible to use the button with radio semantics, this is only possible in menus and not for inline actions!

```vue
<template>
<NcActions>
<NcActionButton :model-value.sync="payment" type="radio" value="cash">
<template #icon>
<Cash :size="20" />
</template>
Pay with cash
</NcActionButton>
<NcActionButton :model-value.sync="payment" type="radio" value="card">
<template #icon>
<CreditCard :size="20" />
</template>
Pay by card
</NcActionButton>
</NcActions>
</template>
<script>
import Cash from 'vue-material-design-icons/Cash.vue'
import CreditCard from 'vue-material-design-icons/CreditCard.vue'
export default {
components: {
Cash,
CreditCard,
},
data() {
return {
payment: 'card',
}
},
}
</script>
```
</docs>

<template>
<li class="action" :class="{ 'action--disabled': disabled }" :role="isInSemanticMenu && 'presentation'">
<button class="action-button button-vue"
:class="{ focusable: isFocusable }"
:aria-label="ariaLabel"
<button :aria-label="ariaLabel"
:class="['action-button button-vue', {
'action-button--active': isChecked,
focusable: isFocusable,
}]"
:title="title"
:role="isInSemanticMenu && 'menuitem'"
type="button"
@click="onClick">
:type="nativeType"
v-bind="buttonAttributes"
@click="handleClick">
<!-- @slot Manually provide icon -->
<slot name="icon">
<span :class="[isIconUrl ? 'action-button__icon--url' : icon]"
Expand Down Expand Up @@ -212,7 +322,9 @@ export default {
<span v-else class="action-button__text">{{ text }}</span>

<!-- right arrow icon when there is a sub-menu -->
<ChevronRightIcon v-if="isMenu" class="action-button__menu-icon" />
<ChevronRightIcon v-if="isMenu" :size="20" class="action-button__menu-icon" />
<CheckIcon v-else-if="isChecked === true" :size="20" class="action-button__pressed-icon" />
<span v-else-if="isChecked === false" class="action-button__pressed-icon material-design-icon" />

<!-- fake slot to gather inner text -->
<slot v-if="false" />
Expand All @@ -221,6 +333,7 @@ export default {
</template>

<script>
import CheckIcon from 'vue-material-design-icons/Check.vue'
import ChevronRightIcon from 'vue-material-design-icons/ChevronRight.vue'
import ActionTextMixin from '../../mixins/actionText.js'
Expand All @@ -231,6 +344,7 @@ export default {
name: 'NcActionButton',
components: {
CheckIcon,
ChevronRightIcon,
},
mixins: [ActionTextMixin],
Expand All @@ -243,14 +357,6 @@ export default {
},
props: {
/**
* disabled state of the action button
*/
disabled: {
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,
Expand All @@ -261,6 +367,14 @@ export default {
default: null,
},
/**
* disabled state of the action button
*/
disabled: {
type: Boolean,
default: false,
},
/**
* If this is a menu, a chevron icon will
* be added at the end of the line
Expand All @@ -269,6 +383,40 @@ export default {
type: Boolean,
default: false,
},
/**
* The button's behavior, by default the button acts like a normal button with optional toggle button behavior if `modelValue` is `true` or `false`.
* But you can also set to checkbox button behavior with tri-state or radio button like behavior.
* This extends the native HTML button type attribute.
*/
type: {
type: String,
default: 'button',
validator: (behavior) => ['button', 'checkbox', 'radio', 'reset', 'submit'].includes(behavior),
},
/**
* The buttons state if `type` is 'checkbox' or 'radio' (meaning if it is pressed / selected)
* Either boolean for checkbox and toggle button behavior or `value` for radio behavior.
*
* **This is not availabe for `type='submit'` or `type='reset'`**
*
* If using `type='checkbox'` a `model-value` of `true` means checked, `false` means unchecked and `null` means indeterminate (tri-state)
* For `type='radio'` `null` is equal to `false`
*/
modelValue: {
type: [Boolean, String],
default: null,
},
/**
* The value used for the `modelValue` when this component is used with radio behavior
* Similar to the `value` attribute of `<input type="radio">`
*/
value: {
type: String,
default: null,
},
},
computed: {
Expand All @@ -280,6 +428,72 @@ export default {
isFocusable() {
return !this.disabled
},
/**
* The current "checked" or "pressed" state for the model behavior
*/
isChecked() {
if (this.type === 'radio') {
return this.modelValue === this.value
}
return this.modelValue
},
/**
* The native HTML type to set on the button
*/
nativeType() {
if (this.type === 'submit' || this.type === 'reset') {
return this.type
}
return 'button'
},
/**
* HTML attributes to bind to the <button>
*/
buttonAttributes() {
const attributes = {}
if (this.isInSemanticMenu) {
// By default it needs to be a menu item in semantic menus
attributes.role = 'menuitem'
if (this.type === 'radio') {
attributes.role = 'menuitemradio'
attributes['aria-checked'] = this.isChecked ? 'true' : 'false'
} else if (this.type === 'checkbox' || (this.nativeType === 'button' && this.modelValue !== null)) {
// either if checkbox behavior was set or the model value is not unset
attributes.role = 'menuitemcheckbox'
attributes['aria-checked'] = this.modelValue === null ? 'mixed' : (this.modelValue ? 'true' : 'false')
}
} else if (this.modelValue !== null && this.nativeType === 'button') {
// In case this has a modelValue it is considered a toggle button, so we need to set the aria-pressed
attributes['aria-pressed'] = this.modelValue ? 'true' : 'false'
}
return attributes
},
},
methods: {
/**
* Forward click event, let mixin handle the close-after-click and emit new modelValue if needed
* @param {MouseEvent} event The click event
*/
handleClick(event) {
this.onClick(event)
// If modelValue or type is set (so modelValue might be null for tri-state) we need to update it
if (this.modelValue !== null || this.type !== 'button') {
if (this.type === 'radio') {
if (!this.isChecked) {
this.$emit('update:modelValue', this.value)
}
} else {
this.$emit('update:modelValue', !this.isChecked)
}
}
},
},
}
</script>
Expand All @@ -289,4 +503,9 @@ export default {
@include action-active;
@include action--disabled;
@include action-item('button');
.action-button__pressed-icon {
margin-left: auto;
margin-right: -$icon-margin;
}
</style>
30 changes: 27 additions & 3 deletions src/components/NcActionButtonGroup/NcActionButtonGroup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,25 @@ This should be used sparingly for accessibility.
<NcActions>
<NcActionButtonGroup name="Text alignment">
<NcActionButton aria-label="Align left"
@click="showMessage('Align left')">
:model-value.sync="alignment"
type="radio"
value="l">
<template #icon>
<AlignLeft :size="20" />
</template>
</NcActionButton>
<NcActionButton aria-label="Align center"
@click="showMessage('Align center')">
:model-value.sync="alignment"
type="radio"
value="c">
<template #icon>
<AlignCenter :size="20" />
</template>
</NcActionButton>
<NcActionButton aria-label="Align right"
@click="showMessage('Align Right')">
:model-value.sync="alignment"
type="radio"
value="r">
<template #icon>
<AlignRight :size="20" />
</template>
Expand Down Expand Up @@ -70,6 +76,9 @@ export default {
AlignCenter,
Plus,
},
data() {
return { alignment: 'l' }
},
methods: {
showMessage(msg) {
alert(msg)
Expand Down Expand Up @@ -141,6 +150,7 @@ export default defineComponent({
ul.nc-button-group-content {
display: flex;
gap: 4px; // required for the focus-visible outline
justify-content: space-between;
li {
flex: 1 1;
Expand All @@ -152,6 +162,20 @@ export default defineComponent({
width: 100%;
display: flex;
justify-content: center;
&.action-button--active {
background-color: var(--color-primary-element);
border-radius: var(--border-radius-large);
color: var(--color-primary-element-text);
&:hover, &:focus, &:focus-within {
background-color: var(--color-primary-element-hover);
}
}
.action-button__pressed-icon {
display: none;
}
}
}
}
Expand Down
Loading

0 comments on commit 01e1067

Please sign in to comment.