+
+ @keydown.esc.stop="showAddCard = false">
Date: Thu, 30 Jul 2026 22:44:47 +0000
Subject: [PATCH 24/24] Extract add card form to keep tab order intact
Signed-off-by: Theo <36564257+theoholl@users.noreply.github.com>
---
src/components/board/Stack.vue | 221 +++-----------------------
src/components/board/StackCardAdd.vue | 205 ++++++++++++++++++++++++
2 files changed, 227 insertions(+), 199 deletions(-)
create mode 100644 src/components/board/StackCardAdd.vue
diff --git a/src/components/board/Stack.vue b/src/components/board/Stack.vue
index c18920a8ff..5335ae0021 100644
--- a/src/components/board/Stack.vue
+++ b/src/components/board/Stack.vue
@@ -12,9 +12,8 @@
'stack--dragging-card': draggingCard,
}"
:data-cy-stack="stack.title">
-
+
+
-
-
-
-
-
- {{ t('deck', 'Add card') }}
-
-
-
+
@@ -162,11 +138,11 @@ import { mapGetters, mapState } from 'vuex'
import { Container, Draggable } from 'vue-smooth-dnd'
import ArchiveIcon from 'vue-material-design-icons/ArchiveOutline.vue'
import CheckCircleOutline from 'vue-material-design-icons/CheckCircleOutline.vue'
-import PlusIcon from 'vue-material-design-icons/Plus.vue'
-import { NcActions, NcActionButton, NcButton, NcModal } from '@nextcloud/vue'
-import { showError, showUndo } from '@nextcloud/dialogs'
+import { NcActions, NcActionButton, NcModal } from '@nextcloud/vue'
+import { showUndo } from '@nextcloud/dialogs'
import CardItem from '../cards/CardItem.vue'
+import StackCardAdd from './StackCardAdd.vue'
import '@nextcloud/dialogs/style.css'
import { mapActions } from 'pinia'
@@ -178,14 +154,13 @@ export default {
components: {
NcActions,
NcActionButton,
- NcButton,
CardItem,
+ StackCardAdd,
Container,
Draggable,
NcModal,
ArchiveIcon,
CheckCircleOutline,
- PlusIcon,
},
directives: {
ClickOutside,
@@ -205,9 +180,6 @@ export default {
editing: false,
draggingCard: false,
copiedStack: '',
- newCardTitle: '',
- showAddCard: false,
- stateCardCreating: false,
animate: false,
modalArchivAllCardsShow: false,
stackTransfer: {
@@ -239,14 +211,6 @@ export default {
dragHandleSelector() {
return this.canEdit && !this.showArchived ? null : '.no-drag'
},
- cardDetailsInModal: {
- get() {
- return this.$store.getters.config('cardDetailsInModal')
- },
- set(newValue) {
- this.$store.dispatch('setConfig', { cardDetailsInModal: newValue })
- },
- },
stackAddCardAtTop() {
return this.$store.getters.config('stackAddCardAtTop') === true
},
@@ -254,18 +218,6 @@ export default {
return this.canEdit && !this.showArchived && !this.isArchived
},
},
- watch: {
- showAddCard(newValue) {
- if (!newValue) {
- this.$store.dispatch('toggleShortcutLock', false)
- } else {
- this.$nextTick(() => {
- this.$refs.newCardInput.focus()
- })
- }
- },
- },
-
mounted() {
this.setupAutoscrollOnDrag()
},
@@ -273,16 +225,6 @@ export default {
methods: {
...mapActions(useTrashbinStore, ['stackUndoDelete']),
...mapActions(useStackStore, ['setDoneStack', 'deleteStack', 'updateStack']),
- stopCardCreation(e) {
- // For some reason the submit event triggers a MouseEvent that is bubbling to the outside
- // so we have to ignore it
- e.stopPropagation()
- if (this.$refs.newCardInput && this.$refs.newCardInput.parentElement === e.target.parentElement) {
- return false
- }
- this.showAddCard = false
- return false
- },
async onDropCard(stackId, event) {
const { addedIndex, removedIndex, payload } = event
const card = Object.assign({}, payload)
@@ -343,41 +285,12 @@ export default {
cancelEdit() {
this.editing = false
},
- async clickAddCard() {
- this.stateCardCreating = true
- try {
- const addCardAtTop = this.stackAddCardAtTop
- this.animate = true
- const newCard = await this.$store.dispatch('addCard', {
- title: this.newCardTitle,
- stackId: this.stack.id,
- boardId: this.stack.boardId,
- // Without an order the API appends the card to the end of the stack
- ...(addCardAtTop ? { order: 0 } : {}),
- })
- if (addCardAtTop) {
- // Creating a card does not move the existing cards down, so reorder
- await this.$store.dispatch('reorderCard', { ...newCard, order: 0 })
- }
- this.newCardTitle = ''
- this.showAddCard = true
- this.$nextTick(() => {
- this.$refs.newCardInput.focus()
- this.animate = false
- // Refs of a v-for are registered in creation order, not in list order
- this.$refs.card?.find((card) => card.id === newCard.id)?.scrollIntoView()
- })
- if (!this.cardDetailsInModal) {
- this.$router.push({ name: 'card', params: { cardId: newCard.id } })
- }
- } catch (e) {
- showError('Could not create card: ' + e.response.data.message)
- } finally {
- this.stateCardCreating = false
- }
- },
- onCreateCardFocus() {
- this.$store.dispatch('toggleShortcutLock', true)
+ handleCardCreated(newCard) {
+ this.$nextTick(() => {
+ this.animate = false
+ // Refs of a v-for are registered in creation order, not in list order
+ this.$refs.card?.find((card) => card.id === newCard.id)?.scrollIntoView()
+ })
},
setupAutoscrollOnDrag() {
let timer
@@ -436,18 +349,6 @@ export default {
}
&.stack--add-card-at-top {
- .stack__header {
- order: 1;
- }
-
- .stack__card-add {
- order: 2;
- }
-
- .stack__cards-list {
- order: 3;
- }
-
&:after {
content: '';
display: block;
@@ -573,84 +474,6 @@ export default {
}
}
- .stack__card-add {
- flex-shrink: 0;
- z-index: 100;
- display: flex;
- background-color: var(--color-main-background);
- position: relative;
-
- .stack--add-card-at-top & {
- padding-top: $stack-gap;
-
- &:after {
- content: '';
- display: block;
- position: absolute;
- width: 100%;
- height: $stack-gap;
- bottom: 0;
- z-index: 99;
- pointer-events: none;
- background-image: linear-gradient(180deg, var(--color-main-background) 0%, transparent 100%);
- transform: translateY(100%);
- }
- }
-
- .stack--add-card-at-bottom & {
- padding-bottom: $stack-gap;
- }
-
- // Smooth fade out of the cards at the top
- &:before {
- content: '';
- display: block;
- position: absolute;
- width: 100%;
- height: $stack-gap;
- z-index: 99;
- transition: bottom var(--animation-slow);
- background-image: linear-gradient(0deg, var(--color-main-background) 0%, transparent 100%);
- transform: translateY(-100%);
- }
-
- :deep(.stack__card-add-button.button-vue) {
- --button-size: var(--stack-card-add-control-height);
- color: var(--color-text-maxcontrast);
-
- &:hover:not(:disabled),
- &:focus-visible {
- color: var(--color-main-text);
- }
- }
-
- form {
- display: flex;
- width: 100%;
- height: var(--stack-card-add-control-height);
- box-sizing: border-box;
- border: 2px solid var(--color-border-maxcontrast);
- border-radius: var(--border-radius-large);
- overflow: hidden;
- padding: 2px;
- }
-
- &.icon-loading-small:after,
- &.icon-loading-small-dark:after {
- margin-inline-start: calc(50% - 25px);
- }
-
- input[type=text] {
- flex-grow: 1;
- padding-inline-end: 16px;
- }
-
- input {
- border: none;
- margin: 0;
- }
- }
-
.modal__content {
width: 25vw;
min-width: 250px;
diff --git a/src/components/board/StackCardAdd.vue b/src/components/board/StackCardAdd.vue
new file mode 100644
index 0000000000..b235cd036e
--- /dev/null
+++ b/src/components/board/StackCardAdd.vue
@@ -0,0 +1,205 @@
+
+
+
+
+
+
+
+
+ {{ t('deck', 'Add card') }}
+
+
+
+
+
+
+
+