Skip to content

Commit

Permalink
feat(NcActions): Allow to manually specify the semantic menu type
Browse files Browse the repository at this point in the history
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
  • Loading branch information
susnux committed Mar 1, 2024
1 parent 89ba71e commit 9ca3647
Show file tree
Hide file tree
Showing 3 changed files with 140 additions and 32 deletions.
122 changes: 93 additions & 29 deletions src/components/NcActions/NcActions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -813,7 +813,7 @@ p {
`<NcActions>` is supposed to be used with direct `<NcAction*>` children.
Although it works when actions are not direct children but wrapped in custom components, it has limitations:
- No `inline` prop property, including a single action display;
- Accessibility issues, including changed keyboard behavior;
- Accessibility issues, including changed keyboard behavior (see below);
- Invalid HTML.

```
Expand Down Expand Up @@ -932,6 +932,40 @@ export default {
}
</style>
```

#### Manually providing semantic menu information
Due to limitations of Vue 2, when using a custom wrapper for action components, you have to provide the semantic menu type yourself.
This is used for keyboard navigation and accessibility.
In this example a `NcActionInput` component is used within a custom wrapper, so `NcActions` is not able to detect the semantic menu type of 'dialog' automatically,
meaning it must be provided manually:

```vue
<template>
<NcActions menu-semantic-type="dialog">
<MyWrapper />
</NcActions>
</template>
<script>
import Pencil from 'vue-material-design-icons/Pencil.vue'

export default {
components: {
MyWrapper: {
template: `
<NcActionInput trailing-button-label="Submit" label="Rename group">
<template #icon>
<Pencil :size="20" />
</template>
</NcActionInput>`,
},
components: {
Pencil,
},
},
}
</script>
```

</docs>

<script>
Expand Down Expand Up @@ -1022,6 +1056,33 @@ export default {
default: null,
},

/**
* NcActions can be used as:
*
* - Application menu (has menu role)
* - Navigation (has no specific role, should be used an element with navigation role)
* - Popover with plain text or text inputs (has no specific role)
*
* By default the used type is automatically detected by components used in the default slot.#
*
* With Vue 2 this is limited to direct children of the NcActions component.
* So if you use a wrapper, you have to provide the semantic type yourself (see Example)
*
* Choose:
*
* - 'dialog' if you use any of these components: NcActionInput', 'NcActionTextEditable'
* - 'menu' if you use any of these components: 'NcActionButton', 'NcActionButtonGroup', 'NcActionCheckbox', 'NcActionRadio'
* - 'navigation' if using one of these: 'NcActionLink', 'NcActionRouter'
* - Leave this property unset otherwise
*/
menuSemanticType: {
type: String,
default: null,
validator(value) {
return ['dialog', 'menu', 'navigation'].includes(value)
},
},

/**
* Apply primary styling for this menu
*/
Expand Down Expand Up @@ -1600,36 +1661,39 @@ export default {
* Determine what kind of menu we have.
* It defines keyboard navigation and a11y.
*/

const menuItemsActions = ['NcActionButton', 'NcActionButtonGroup', 'NcActionCheckbox', 'NcActionRadio']
const textInputActions = ['NcActionInput', 'NcActionTextEditable']
const linkActions = ['NcActionLink', 'NcActionRouter']

const hasTextInputAction = menuActions.some(action => textInputActions.includes(this.getActionName(action)))
const hasMenuItemAction = menuActions.some(action => menuItemsActions.includes(this.getActionName(action)))
const hasLinkAction = menuActions.some(action => linkActions.includes(this.getActionName(action)))

if (hasTextInputAction) {
this.actionsMenuSemanticType = 'dialog'
} else if (hasMenuItemAction) {
this.actionsMenuSemanticType = 'menu'
} else if (hasLinkAction) {
this.actionsMenuSemanticType = 'navigation'
if (this.menuSemanticType) {
this.actionsMenuSemanticType = this.menuSemanticType
} else {
// (!) Hotfix (!)
// In Vue 2 it is not easy to search for NcAction* in sub-component of a slot.
// When a menu is rendered, children are not mounted yet.
// If we have NcActions > MyActionsList > NcActionButton, only MyActionsList's vnode is available.
// So when NcActions has actions as non-direct children, here then we don't know about them.
// Like this menu has no buttons/links/inputs.
// It makes the menu incorrectly considered a tooltip.
const ncActions = actions.filter((action) => this.getActionName(action).startsWith('NcAction'))
if (ncActions.length === actions.length) {
// True tooltip
this.actionsMenuSemanticType = 'tooltip'
const textInputActions = ['NcActionInput', 'NcActionTextEditable']
const menuItemsActions = ['NcActionButton', 'NcActionButtonGroup', 'NcActionCheckbox', 'NcActionRadio']
const linkActions = ['NcActionLink', 'NcActionRouter']

const hasTextInputAction = menuActions.some(action => textInputActions.includes(this.getActionName(action)))
const hasMenuItemAction = menuActions.some(action => menuItemsActions.includes(this.getActionName(action)))
const hasLinkAction = menuActions.some(action => linkActions.includes(this.getActionName(action)))

if (hasTextInputAction) {
this.actionsMenuSemanticType = 'dialog'
} else if (hasMenuItemAction) {
this.actionsMenuSemanticType = 'menu'
} else if (hasLinkAction) {
this.actionsMenuSemanticType = 'navigation'
} else {
// Custom components are passed to the NcActions
this.actionsMenuSemanticType = 'unknown'
// (!) Hotfix (!)
// In Vue 2 it is not easy to search for NcAction* in sub-component of a slot.
// When a menu is rendered, children are not mounted yet.
// If we have NcActions > MyActionsList > NcActionButton, only MyActionsList's vnode is available.
// So when NcActions has actions as non-direct children, here then we don't know about them.
// Like this menu has no buttons/links/inputs.
// It makes the menu incorrectly considered a tooltip.
const ncActions = actions.filter((action) => this.getActionName(action).startsWith('NcAction'))
if (ncActions.length === actions.length) {
// True tooltip
this.actionsMenuSemanticType = 'tooltip'
} else {
// Custom components are passed to the NcActions
this.actionsMenuSemanticType = 'unknown'
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion styleguide.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ module.exports = async () => {
},
resolve: {
alias: {
vue: 'vue/dist/vue.js',
vue$: 'vue/dist/vue.js',
},
},
}),
Expand Down
48 changes: 46 additions & 2 deletions tests/unit/components/NcActions/NcActions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,57 @@
*/

import { mount } from '@vue/test-utils'
import { Fragment } from 'vue-frag'

import NcActions from '../../../../src/components/NcActions/NcActions.vue'
import NcActionButton from '../../../../src/components/NcActionButton/NcActionButton.vue'
import NcActionInput from '../../../../src/components/NcActionInput/NcActionInput.vue'
import TestCompositionApi from './TestCompositionApi.vue'
import { defineComponent } from 'vue'

describe('NcActions.vue', () => {
'use strict'

describe('semantic menu type', () => {
const MyWrapper = defineComponent({
template: '<Fragment><NcActionInput /></Fragment>',
components: { Fragment, NcActionInput },
})

// This currently fails due to limitations of Vue 2
it.failing('Can auto detect semantic menu type in wrappers', () => {
const wrapper = mount(NcActions, {
slots: {
default: [
'<MyWrapper />',
],
},
stubs: {
MyWrapper,
},
})

expect(wrapper.vm.$data.actionsMenuSemanticType).toBe('dialog')
})

it('Can set the type manually', () => {
const wrapper = mount(NcActions, {
propsData: {
menuSemanticType: 'dialog',
},
slots: {
default: [
'<MyWrapper />',
],
},
stubs: {
MyWrapper,
},
})

expect(wrapper.vm.$data.actionsMenuSemanticType).toBe('dialog')
})
})

describe('when using the component with', () => {
it('no actions elements', () => {
const wrapper = mount(NcActions, {
Expand Down Expand Up @@ -178,7 +222,7 @@ describe('NcActions.vue', () => {
inline: 1,
},
})

expect(wrapper.find('img[src="http://example.com/image.png"').exists()).toBe(true)
})
})
Expand Down

0 comments on commit 9ca3647

Please sign in to comment.