diff options
author | Vinicius Reis <vinicius.reis@nextcloud.com> | 2022-05-21 01:31:58 +0300 |
---|---|---|
committer | Vinicius Reis <vinicius.reis@nextcloud.com> | 2022-05-21 01:31:58 +0300 |
commit | 8528429d1e3fcfb371fbd89cd4add3192798216c (patch) | |
tree | c70d972c30fdc09decb04b73114f516a1286d020 /src | |
parent | 3b8aaec4b1f04507e1899386f97f30947fde5f4f (diff) |
✨ (#2345): add ActionList
improve style/ux
Signed-off-by: Vinicius Reis <vinicius.reis@nextcloud.com>
Diffstat (limited to 'src')
-rw-r--r-- | src/components/Menu/ActionEntry.js | 15 | ||||
-rw-r--r-- | src/components/Menu/ActionEntry.mixin.js | 2 | ||||
-rw-r--r-- | src/components/Menu/ActionEntry.scss | 89 | ||||
-rw-r--r-- | src/components/Menu/ActionImageUpload.vue | 4 | ||||
-rw-r--r-- | src/components/Menu/ActionList.vue | 84 | ||||
-rw-r--r-- | src/components/Menu/ActionSingle.vue | 98 | ||||
-rw-r--r-- | src/components/Menu/Bar.vue | 14 | ||||
-rw-r--r-- | src/components/Menu/EmojiPickerAction.vue | 5 | ||||
-rw-r--r-- | src/components/Menu/entries.js | 2 | ||||
-rw-r--r-- | src/components/Menu/utils.js | 3 |
10 files changed, 257 insertions, 59 deletions
diff --git a/src/components/Menu/ActionEntry.js b/src/components/Menu/ActionEntry.js index 1d20a6a9b..8f9136c2b 100644 --- a/src/components/Menu/ActionEntry.js +++ b/src/components/Menu/ActionEntry.js @@ -20,18 +20,27 @@ * */ -import SingleAction from './ActionSingle.vue' +import ActionSingle from './ActionSingle.vue' +import ActionList from './ActionList.vue' export default { name: 'ActionEntry', functional: true, render(h, ctx) { const { actionEntry } = ctx.props + const { key } = ctx.data + + const params = { + ...ctx, + key, + } if (actionEntry.component) { - return h(actionEntry.component, ctx) + return h(actionEntry.component, params) } - return h(SingleAction, ctx) + return actionEntry.children + ? h(ActionList, params) + : h(ActionSingle, params) }, } diff --git a/src/components/Menu/ActionEntry.mixin.js b/src/components/Menu/ActionEntry.mixin.js index f3fb75818..b334c2115 100644 --- a/src/components/Menu/ActionEntry.mixin.js +++ b/src/components/Menu/ActionEntry.mixin.js @@ -28,6 +28,8 @@ import debounce from 'debounce' import { useEditorMixin, useIsMobileMixin } from '../EditorWrapper.provider.js' import { getActionState, getKeys } from './utils.js' +import './ActionEntry.scss' + /** * @type {import("vue").ComponentOptions} BaseActionEntry */ diff --git a/src/components/Menu/ActionEntry.scss b/src/components/Menu/ActionEntry.scss index d69baf1d0..77de40478 100644 --- a/src/components/Menu/ActionEntry.scss +++ b/src/components/Menu/ActionEntry.scss @@ -1,40 +1,67 @@ -button.entry-action, -.entry-action > button, -.entry-action > div > button { - position: relative; - width: 44px; +.text__is-active-item-btn { + opacity: 1; + + &::before { + transform: translateX(-50%); + border-radius: 100%; + position: absolute; + background: var(--color-primary-element); + bottom: 3px; + height: 6px; + width: 6px; + content: ""; + left: 50%; + } +} + +.text-menubar, .popover { + button.entry-action__button { height: 44px; margin: 0; - background-size: 16px; border: 0; - background-color: transparent; - opacity: .5; - color: var(--color-main-text); - background-position: center center; - vertical-align: top; - padding: 0.7em; - &:hover, - &:focus, - &:active { - background-color: var(--color-background-dark); - } + opacity: 0.5; + position: relative; + color: var(--color-main-text); + background-color: transparent; + vertical-align: top; + padding: 0; - &.is-active::before { - transform: translateX(-50%); - border-radius: 100%; - position: absolute; - background: var(--color-primary-element); - bottom: 3px; - height: 6px; - width: 6px; - content: ''; - left: 50%; + p { + padding: 0; + } + &:is(li.entry-action-item button) { + padding: 0 0.5em 0 0; } - &.is-active, - &:hover, - &:focus { - opacity: 1; + &:not(li.entry-action-item button) { + width: 44px; } + + &:hover, + &:focus, + &:active { + background-color: var(--color-background-dark); + } + + &:hover, + &:focus { + opacity: 1; + } + + &.is-active { + @extend .text__is-active-item-btn; + } + } + + .entry-action.is-active:not(.entry-action-item) { + @extend .text__is-active-item-btn; + } + + .entry-action.entry-action-item { + &.is-active { + background-color: var(--color-primary-light); + box-shadow: inset 2px 0 var(--color-primary); + } + } } diff --git a/src/components/Menu/ActionImageUpload.vue b/src/components/Menu/ActionImageUpload.vue index f2c72ccd3..25a337ef9 100644 --- a/src/components/Menu/ActionImageUpload.vue +++ b/src/components/Menu/ActionImageUpload.vue @@ -20,7 +20,7 @@ - --> <template> - <Actions class="entry-image-upload-action entry-action" + <Actions class="entry-action entry-action__image-upload" :data-action-entry="actionEntry._key" :title="actionEntry.label" :aria-label="actionEntry.label" @@ -33,6 +33,7 @@ </template> <ActionButton close-after-click :disabled="$isUploadingImages" + :data-action-entry="`${actionEntry._key}-upload`" @click="$callChooseLocalImage"> <template #icon> <Upload /> @@ -42,6 +43,7 @@ <ActionButton v-if="!$isPublic" close-after-click :disabled="$isUploadingImages" + :data-action-entry="`${actionEntry._key}-insert`" @click="$callImagePrompt"> <template #icon> <Folder /> diff --git a/src/components/Menu/ActionList.vue b/src/components/Menu/ActionList.vue new file mode 100644 index 000000000..79798a89b --- /dev/null +++ b/src/components/Menu/ActionList.vue @@ -0,0 +1,84 @@ +<!-- + - @copyright Copyright (c) 2022 Vinicius Reis <vinicius@nextcloud.com> + - + - @author Vinicius Reis <vinicius@nextcloud.com> + - + - @license GNU AGPL version 3 or any later version + - + - 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/>. + - + --> + +<template> + <Actions v-tooltip="tooltip" + class="entry-list-action entry-action" + v-bind="state" + :title="actionEntry.label" + :data-action-entry="actionEntry._key" + v-on="$listeners"> + <template #icon> + <component :is="icon" /> + </template> + <ActionSingle v-for="child in actionEntry.children" + :key="`child-${child._key}`" + is-item + :action-entry="child" /> + </Actions> +</template> + +<script> +import Actions from '@nextcloud/vue/dist/Components/Actions' +import ActionButton from '@nextcloud/vue/dist/Components/ActionButton' +import { BaseActionEntry } from './ActionEntry.mixin.js' +import ActionSingle from './ActionSingle.vue' +import { getIsActive } from './utils.js' + +export default { + name: 'ActionList', + components: { Actions, ActionButton, ActionSingle }, + extends: BaseActionEntry, + data: () => ({ + activeChild: null, + }), + computed: { + currentChind() { + const { + state, + $editor, + actionEntry: { children }, + } = this + + if (!state.active) { + return null + } + + return children.find(child => { + return getIsActive(child, $editor) + }) + }, + icon() { + if (this.currentChind) { + return this.currentChind.icon + } + + return this.actionEntry.icon + }, + }, + methods: { + runAction() { + + }, + }, +} +</script> diff --git a/src/components/Menu/ActionSingle.vue b/src/components/Menu/ActionSingle.vue index d9e68d8ac..5e96c8558 100644 --- a/src/components/Menu/ActionSingle.vue +++ b/src/components/Menu/ActionSingle.vue @@ -20,24 +20,47 @@ - --> -<template> - <button v-tooltip="tooltip" - class="entry-single-action entry-action" - v-bind="state" - :title="actionEntry.label" - :data-action-entry="actionEntry._key" - v-on="$listeners" - @click="runAction"> - <component :is="icon" /> - </button> -</template> - <script> +import Button from '@nextcloud/vue/dist/Components/Button' +import ActionButton from '@nextcloud/vue/dist/Components/ActionButton' import { BaseActionEntry } from './ActionEntry.mixin.js' export default { name: 'ActionSingle', extends: BaseActionEntry, + props: { + isItem: { + type: Boolean, + default: false, + }, + }, + computed: { + component() { + // Button and ActionButton shares styles and behaviours + // to keep it simple, this component handle the small differences + return this.isItem + ? ActionButton + : Button + }, + bindState() { + const state = { + ...this.state, + } + + state.class = { + ...state.class, + // inject a extra class + 'entry-action-item': this.isItem, + } + + // item list bejaviour + if (this.isItem) { + state.closeAfterClick = true + } + + return state + }, + }, methods: { runAction() { const { actionEntry } = this @@ -50,7 +73,54 @@ export default { actionEntry.action(this.$editor.chain().focus())?.run() }, }, + + render(h) { + const { + $listeners, + actionEntry, + bindState, + component, + icon, + isItem, + runAction, + tooltip, + } = this + + const { class: classes, ...attrs } = bindState + + // do not use tooltip if is a item of action list + const directives = isItem + ? [] + : [{ + name: 'tooltip', + value: tooltip, + }] + + const children = [h(icon, { slot: 'icon' })] + + // do not use title if is a item of action list + const title = isItem ? undefined : actionEntry.label + + if (isItem) { + // add label + children.push(actionEntry.label) + } + + return h(component, { + directives, + staticClass: 'entry-single-action entry-action', + class: classes, + attrs: { + title, + type: 'tertiary', + 'data-action-entry': actionEntry._key, + ...attrs, + }, + on: { + ...$listeners, + click: runAction, + }, + }, children) + }, } </script> - -<style scoped lang="scss" src="./ActionEntry.scss" /> diff --git a/src/components/Menu/Bar.vue b/src/components/Menu/Bar.vue index 4d965256d..041051d8c 100644 --- a/src/components/Menu/Bar.vue +++ b/src/components/Menu/Bar.vue @@ -22,11 +22,11 @@ --> <template> - <div class="menubar" :class="{ 'show': isVisible, 'autohide': autohide }"> - <div v-if="$isRichEditor" ref="menubar" class="menubar-entries"> + <div class="text-menubar" :class="{ 'show': isVisible, 'text-menubar--autohide': autohide }"> + <div v-if="$isRichEditor" ref="menubar" class="text-menubar__entries"> <ActionEntry v-for="actionEntry of visibleEntries" v-bind="{ actionEntry }" - :key="`single-action-${actionEntry._key}`" /> + :key="`text-action--${actionEntry._key}`" /> </div> </div> </template> @@ -137,7 +137,7 @@ export default { </script> <style scoped lang="scss"> - .menubar { + .text-menubar { --background-blur: blur(10px); position: sticky; top: 0; @@ -148,7 +148,7 @@ export default { padding-top:3px; padding-bottom: 3px; - &.autohide { + &.text-menubar--autohide { visibility: hidden; opacity: 0; transition: visibility 0.2s 0.4s, opacity 0.2s 0.4s; @@ -157,12 +157,12 @@ export default { opacity: 1; } } - .menubar-entries { + .text-menubar__entries { display: flex; justify-content: flex-start; } @media (max-width: 660px) { - .menubar-entries { + .text-menubar__entries { margin-left: 0; } } diff --git a/src/components/Menu/EmojiPickerAction.vue b/src/components/Menu/EmojiPickerAction.vue index eaa4f29b7..044300cdc 100644 --- a/src/components/Menu/EmojiPickerAction.vue +++ b/src/components/Menu/EmojiPickerAction.vue @@ -20,10 +20,11 @@ - --> <template> - <EmojiPicker class="entry-emoji-action entry-action" + <EmojiPicker class="entry-action entry-action__emoji" :data-action-entry="actionEntry._key" @selectData="addEmoji"> <button v-tooltip="actionEntry.label" + class="entry-action__button" :title="actionEntry.label" :aria-label="actionEntry.label" :aria-haspopup="true"> @@ -55,5 +56,3 @@ export default { }, } </script> - -<style scoped lang="scss" src="./ActionEntry.scss" /> diff --git a/src/components/Menu/entries.js b/src/components/Menu/entries.js index 35f89b313..0a39e1d49 100644 --- a/src/components/Menu/entries.js +++ b/src/components/Menu/entries.js @@ -130,6 +130,7 @@ export default [ keyModifiers: ['ctrl', 'shift'], visible: false, icon: FormatHeader1, + isActive: 'heading', children: [ { _key: 'headings-01', @@ -247,6 +248,7 @@ export default [ label: t('text', 'Callouts'), visible: false, icon: Info, + isActive: 'callout', children: [ { _key: 'info', diff --git a/src/components/Menu/utils.js b/src/components/Menu/utils.js index 896230fc8..2b3f85887 100644 --- a/src/components/Menu/utils.js +++ b/src/components/Menu/utils.js @@ -67,13 +67,16 @@ const getActionState = (actionEntry, $editor) => { const active = getIsActive(actionEntry, $editor) return { + ariaLabel: actionEntry.label, disabled: isDisabled(actionEntry, $editor), class: getEntryClasses(actionEntry, active), + active, } } export { isDisabled, + getIsActive, getKeys, getEntryClasses, getActionState, |