Handling of event addons

This commit is contained in:
2026-08-12 17:18:45 +02:00
parent 085299336e
commit 6e4e6b645c
74 changed files with 3072 additions and 95 deletions
+2 -2
View File
@@ -32,7 +32,7 @@
</template>
<script setup>
import { ref, watch, onMounted, onUnmounted, nextTick } from 'vue'
import {nextTick, onMounted, onUnmounted, ref, watch} from 'vue'
const props = defineProps({
show: Boolean,
@@ -115,7 +115,7 @@ onUnmounted(() => {
}
.modal-body {
max-height: 600px;
max-height: 80vh;
overflow: auto;
}
+53 -22
View File
@@ -1,9 +1,11 @@
<script setup>
import {computed, onBeforeUnmount, onMounted, ref} from 'vue'
import {computed, nextTick, onBeforeUnmount, onMounted, ref} from 'vue'
import Icon from './Icon.vue'
// Generisches Custom-Dropdown, das je Option ein Icon + Label anzeigt (ein natives <select> kann
// keine SVG-Icons rendern). Wiederverwendbar für Icon-/Rich-Selects.
// keine SVG-Icons rendern). Die Options-Liste wird per <Teleport> an <body> gehängt und fixed
// positioniert, damit sie nicht von einem Eltern-Container mit overflow (z. B. .tab-content in
// TabbedPage) abgeschnitten wird. Wiederverwendbar für Icon-/Rich-Selects.
const props = defineProps({
// Ausgewählter Wert (value einer Option).
modelValue: {type: String, default: ''},
@@ -15,11 +17,23 @@ const emit = defineEmits(['update:modelValue'])
const open = ref(false)
const root = ref(null)
const listRef = ref(null)
const pos = ref({top: 0, left: 0, width: 0})
const selected = computed(() => props.options.find(o => o.value === props.modelValue) ?? null)
function toggle() {
function updatePosition() {
if (!root.value) return
const rect = root.value.getBoundingClientRect()
pos.value = {top: rect.bottom + 4, left: rect.left, width: rect.width}
}
async function toggle() {
open.value = !open.value
if (open.value) {
await nextTick()
updatePosition()
}
}
function select(option) {
@@ -28,7 +42,9 @@ function select(option) {
}
function onClickOutside(event) {
if (root.value && !root.value.contains(event.target)) {
const inRoot = root.value && root.value.contains(event.target)
const inList = listRef.value && listRef.value.contains(event.target)
if (!inRoot && !inList) {
open.value = false
}
}
@@ -39,13 +55,22 @@ function onKeydown(event) {
}
}
function onReposition() {
if (open.value) updatePosition()
}
onMounted(() => {
document.addEventListener('click', onClickOutside)
document.addEventListener('keydown', onKeydown)
window.addEventListener('resize', onReposition)
// capture=true, damit auch Scrollen in inneren Containern die Position aktualisiert
window.addEventListener('scroll', onReposition, true)
})
onBeforeUnmount(() => {
document.removeEventListener('click', onClickOutside)
document.removeEventListener('keydown', onKeydown)
window.removeEventListener('resize', onReposition)
window.removeEventListener('scroll', onReposition, true)
})
</script>
@@ -67,20 +92,28 @@ onBeforeUnmount(() => {
<span class="rich-select__caret" aria-hidden="true"></span>
</button>
<ul v-if="open" class="rich-select__list" role="listbox">
<li
v-for="option in options"
:key="option.value"
class="rich-select__option"
:class="{ 'rich-select__option--active': option.value === modelValue }"
role="option"
:aria-selected="option.value === modelValue"
@click="select(option)"
<Teleport to="body">
<ul
v-if="open"
ref="listRef"
class="rich-select__list"
role="listbox"
:style="{ top: pos.top + 'px', left: pos.left + 'px', width: pos.width + 'px' }"
>
<Icon v-if="option.icon" :name="option.icon"/>
<span>{{ option.label }}</span>
</li>
</ul>
<li
v-for="option in options"
:key="option.value"
class="rich-select__option"
:class="{ 'rich-select__option--active': option.value === modelValue }"
role="option"
:aria-selected="option.value === modelValue"
@click="select(option)"
>
<Icon v-if="option.icon" :name="option.icon"/>
<span>{{ option.label }}</span>
</li>
</ul>
</Teleport>
</div>
</template>
@@ -126,11 +159,8 @@ onBeforeUnmount(() => {
}
.rich-select__list {
position: absolute;
z-index: 50;
top: calc(100% + 4px);
left: 0;
right: 0;
position: fixed;
z-index: 1000;
margin: 0;
padding: 4px;
list-style: none;
@@ -140,6 +170,7 @@ onBeforeUnmount(() => {
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
max-height: 260px;
overflow: auto;
box-sizing: border-box;
}
.rich-select__option {
+4 -1
View File
@@ -1,5 +1,5 @@
<script setup>
import { ref, shallowRef, onMounted } from "vue"
import {onMounted, provide, ref, shallowRef} from "vue"
const props = defineProps({
tabs: {
@@ -55,6 +55,9 @@ async function selectTab(index) {
}
}
// Kind-Komponenten (z. B. Leer-Zustands-Hinweise) können damit gezielt zu einem anderen Tab springen.
provide('selectTab', selectTab)
onMounted(() => {
if (props.tabs.length > 0) {
selectTab(props.subTabIndex)