aboutsummaryrefslogtreecommitdiffstats
path: root/apps/theming/src/components/AppOrderSelectorElement.vue
diff options
context:
space:
mode:
Diffstat (limited to 'apps/theming/src/components/AppOrderSelectorElement.vue')
-rw-r--r--apps/theming/src/components/AppOrderSelectorElement.vue224
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>