diff options
Diffstat (limited to 'apps/theming/src/components/AppOrderSelectorElement.vue')
-rw-r--r-- | apps/theming/src/components/AppOrderSelectorElement.vue | 224 |
1 files changed, 224 insertions, 0 deletions
diff --git a/apps/theming/src/components/AppOrderSelectorElement.vue b/apps/theming/src/components/AppOrderSelectorElement.vue new file mode 100644 index 00000000000..fc41e8e6165 --- /dev/null +++ b/apps/theming/src/components/AppOrderSelectorElement.vue @@ -0,0 +1,224 @@ +<!-- + - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> +<template> + <li :data-cy-app-order-element="app.id" + :class="{ + 'order-selector-element': true, + 'order-selector-element--disabled': app.default + }" + @focusin="$emit('update:focus')"> + <svg width="20" + height="20" + viewBox="0 0 20 20" + role="presentation"> + <image preserveAspectRatio="xMinYMin meet" + x="0" + y="0" + width="20" + height="20" + :xlink:href="app.icon" + class="order-selector-element__icon" /> + </svg> + + <div class="order-selector-element__label"> + {{ app.label ?? app.id }} + </div> + + <div class="order-selector-element__actions"> + <NcButton v-show="!isFirst && !app.default" + ref="buttonUp" + :aria-label="t('settings', 'Move up')" + :aria-describedby="ariaDescribedby" + :aria-details="ariaDetails" + data-cy-app-order-button="up" + type="tertiary-no-background" + @click="moveUp"> + <template #icon> + <IconArrowUp :size="20" /> + </template> + </NcButton> + <div v-show="isFirst || !!app.default" aria-hidden="true" class="order-selector-element__placeholder" /> + <NcButton v-show="!isLast && !app.default" + ref="buttonDown" + :aria-label="t('settings', 'Move down')" + :aria-describedby="ariaDescribedby" + :aria-details="ariaDetails" + data-cy-app-order-button="down" + type="tertiary-no-background" + @click="moveDown"> + <template #icon> + <IconArrowDown :size="20" /> + </template> + </NcButton> + <div v-show="isLast || !!app.default" aria-hidden="true" class="order-selector-element__placeholder" /> + </div> + </li> +</template> + +<script lang="ts"> +import type { PropType } from 'vue' + +import { translate as t } from '@nextcloud/l10n' +import { defineComponent, nextTick, ref } from 'vue' + +import IconArrowDown from 'vue-material-design-icons/ArrowDown.vue' +import IconArrowUp from 'vue-material-design-icons/ArrowUp.vue' +import NcButton from '@nextcloud/vue/components/NcButton' + +interface IApp { + id: string // app id + icon: string // path to the icon svg + label?: string // display name + default?: boolean // for app as default app +} + +export default defineComponent({ + name: 'AppOrderSelectorElement', + components: { + IconArrowDown, + IconArrowUp, + NcButton, + }, + props: { + /** + * Needs to be forwarded to the buttons (as interactive elements) + */ + ariaDescribedby: { + type: String, + default: null, + }, + ariaDetails: { + type: String, + default: null, + }, + app: { + type: Object as PropType<IApp>, + required: true, + }, + isFirst: { + type: Boolean, + default: false, + }, + isLast: { + type: Boolean, + default: false, + }, + }, + emits: { + 'move:up': () => true, + 'move:down': () => true, + /** + * We need this as Sortable.js removes all native focus event listeners + */ + 'update:focus': () => true, + }, + setup(props, { emit }) { + const buttonUp = ref() + const buttonDown = ref() + + /** + * Used to decide if we need to trigger focus() an a button on update + */ + let needsFocus = 0 + + /** + * Handle move up, ensure focus is kept on the button + */ + const moveUp = () => { + emit('move:up') + needsFocus = 1 // request focus on buttonUp + } + + /** + * Handle move down, ensure focus is kept on the button + */ + const moveDown = () => { + emit('move:down') + needsFocus = -1 // request focus on buttonDown + } + + /** + * Reset the focus on the last used button. + * If the button is now visible anymore (because this element is the first/last) then the opposite button is focussed + * + * This function is exposed to the "AppOrderSelector" component which triggers this when the list was successfully rerendered + */ + const keepFocus = () => { + if (needsFocus !== 0) { + // focus requested + if ((needsFocus === 1 || props.isLast) && !props.isFirst) { + // either requested to btn up and it is not the first, or it was requested to btn down but it is the last + nextTick(() => buttonUp.value.$el.focus()) + } else { + nextTick(() => buttonDown.value.$el.focus()) + } + } + needsFocus = 0 + } + + return { + buttonUp, + buttonDown, + + moveUp, + moveDown, + + keepFocus, + + t, + } + }, +}) +</script> + +<style lang="scss" scoped> +.order-selector-element { + // hide default styling + list-style: none; + // Align children + display: flex; + flex-direction: row; + align-items: center; + // Spacing + gap: 12px; + padding-inline: 12px; + + &:hover { + background-color: var(--color-background-hover); + border-radius: var(--border-radius-large); + } + + &--disabled { + border-color: var(--color-text-maxcontrast); + color: var(--color-text-maxcontrast); + + .order-selector-element__icon { + opacity: 75%; + } + } + + &__actions { + flex: 0 0; + display: flex; + flex-direction: row; + gap: 6px; + } + + &__label { + flex: 1 1; + text-overflow: ellipsis; + overflow: hidden; + } + + &__placeholder { + height: 44px; + width: 44px; + } + + &__icon { + filter: var(--background-invert-if-bright); + } +} +</style> |