diff --git a/src/components/NcActionButton/NcActionButton.vue b/src/components/NcActionButton/NcActionButton.vue
index e1c904609b..8808386c08 100644
--- a/src/components/NcActionButton/NcActionButton.vue
+++ b/src/components/NcActionButton/NcActionButton.vue
@@ -171,18 +171,51 @@ export default {
```
-You can set an "pressed" state, for example you have a toggle button, in this example the "fullscreen" button:
+### 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
+
+
+
+
+
+
+ Fullscreen
+
+
+
+
+```
+
+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
-
+
Raise hand
-
+
@@ -208,21 +241,54 @@ export default {
}
```
+
+It is also possible to use the button with radio semantics, this is only possible in menus and not for inline actions!
+
+```vue
+
+
+
+
+
+
+ Pay with cash
+
+
+
+
+
+ Pay by card
+
+
+
+
+```
+ v-bind="buttonAttributes"
+ @click="handleClick">
-
-
+
+
@@ -294,7 +360,7 @@ export default {
* @todo Add a check in @nextcloud/vue 9 that this prop is not provided,
* otherwise root element will inherit incorrect aria-hidden.
*/
- ariaHidden: {
+ ariaHidden: {
type: Boolean,
default: null,
},
@@ -317,11 +383,30 @@ export default {
},
/**
- * 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 a primary border in checked state.
+ * 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.
*/
- pressed: {
- type: Boolean,
+ modelBehavior: {
+ type: String,
+ default: 'button',
+ validator: (behavior) => ['checkbox', 'radio', 'button'].includes(behavior),
+ },
+
+ /**
+ * The buttons state if `modelBehavior` is 'checkbox' or 'radio' (meaning if it is pressed / selected)
+ * Either boolean for checkbox and toggle button behavior or `radioValue` for radio behavior
+ */
+ 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 ` `
+ */
+ radioValue: {
+ type: String,
default: null,
},
},
@@ -335,6 +420,66 @@ export default {
isFocusable() {
return !this.disabled
},
+
+ /**
+ * The current "checked" or "pressed" state for the model behavior
+ */
+ isChecked() {
+ if (this.modelBehavior === 'radio') {
+ return this.modelValue === this.radioValue
+ }
+ return this.modelValue
+ },
+
+ /**
+ * HTML attributes to bind to the
+ */
+ buttonAttributes() {
+ const attributes = {
+ 'aria-label': this.ariaLabel,
+ title: this.title,
+ type: 'button',
+ }
+
+ if (this.isInSemanticMenu) {
+ // By default it needs to be a menu item in semantic menus
+ attributes.role = 'menuitem'
+
+ if (this.modelBehavior === 'radio') {
+ attributes.role = 'menuitemradio'
+ attributes['aria-checked'] = this.isChecked ? 'true' : 'false'
+ } else if (this.modelBehavior === 'checkbox' || 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) {
+ // 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 modelBehavior is set (so modelValue might be null for tri-state) we need to update it
+ if (this.modelValue !== null || this.modelBehavior !== 'button') {
+ if (this.modelBehavior === 'radio') {
+ if (!this.isChecked) {
+ this.$emit('update:modelValue', this.radioValue)
+ }
+ } else {
+ this.$emit('update:modelValue', !this.isChecked)
+ }
+ }
+ },
},
}
diff --git a/src/components/NcActionButtonGroup/NcActionButtonGroup.vue b/src/components/NcActionButtonGroup/NcActionButtonGroup.vue
index ebe51879ae..c86ab7e165 100644
--- a/src/components/NcActionButtonGroup/NcActionButtonGroup.vue
+++ b/src/components/NcActionButtonGroup/NcActionButtonGroup.vue
@@ -29,22 +29,25 @@ This should be used sparingly for accessibility.
+ :modelValue.sync="alignment"
+ modelBehavior="radio"
+ radioValue="l">
+ :modelValue.sync="alignment"
+ modelBehavior="radio"
+ radioValue="c">
+ :modelValue.sync="alignment"
+ modelBehavior="radio"
+ radioValue="r">
diff --git a/src/components/NcActions/NcActions.vue b/src/components/NcActions/NcActions.vue
index 4610f61460..43a9dad751 100644
--- a/src/components/NcActions/NcActions.vue
+++ b/src/components/NcActions/NcActions.vue
@@ -1303,6 +1303,11 @@ export default {
title = text
}
+ const propsToForward = { ...(action?.componentOptions?.propsData ?? {}) }
+ // not available on NcButton
+ delete propsToForward.modelValue
+ delete propsToForward.modelBehavior
+
return h('NcButton',
{
class: [
@@ -1320,11 +1325,19 @@ export default {
// If it has a menuName, we use a secondary button
type: this.type || (buttonText ? 'secondary' : 'tertiary'),
disabled: this.disabled || action?.componentOptions?.propsData?.disabled,
- ...action?.componentOptions?.propsData,
+ pressed: action?.componentOptions?.propsData?.modelValue,
+ ...propsToForward,
},
on: {
focus: this.onFocus,
blur: this.onBlur,
+ // forward any pressed state from NcButton just like NcActionButton does
+ 'update:pressed': (pressed) => {
+ const listeners = action?.componentOptions?.listeners
+ if (listeners?.['update:modelValue']) {
+ listeners['update:modelValue'](pressed)
+ }
+ },
// If we have a click listener,
// we bind it to execute on click and forward the click event
...(!!clickListener && {