Fix: growing textarea now working properly. Placeholder wasn't working before

Preparation for #42
This commit is contained in:
2025-10-22 16:11:52 +02:00
parent b73f61e972
commit 3087e5fcd4
3 changed files with 75 additions and 52 deletions
@@ -1,57 +1,77 @@
<script setup lang="ts">
import { ref, HTMLAttributes, emit, computed } from 'vue'
import { cn } from '@/lib/utils'
import { useVModel } from '@vueuse/core'
import { watch, type HTMLAttributes, useTemplateRef, onMounted } from "vue"
import { useVModel } from "@vueuse/core"
import { cn } from "@/lib/utils"
const props = defineProps<{
defaultValue?: string | number
modelValue?: string | number
placeholder?: string | number
class?: HTMLAttributes['class']
class?: HTMLAttributes["class"]
defaultValue?: string
modelValue?: string
placeholder?: string
}>()
const textElement = ref<HTMLDivElement | null>(null)
const placeholderVisible = ref(true)
const emits = defineEmits<{
(e: 'update:modelValue', payload: string | number): void
(e: "update:modelValue", payload: string | number): void
}>()
const modelValue = useVModel(props, 'modelValue', emits, {
const wrapper = useTemplateRef('grow-wrap')
const modelValue = useVModel(props, "modelValue", emits, {
passive: true,
defaultValue: props.defaultValue,
})
function validate(event: Event) {
(event.target as HTMLInputElement).blur()
watch(modelValue, (value) => {
if (!wrapper.value) return
wrapper.value.dataset.replicatedValue = value as string
})
modelValue.value = textElement.value?.innerText.trim()
}
onMounted(() => {
if (!wrapper.value) return
wrapper.value.dataset.replicatedValue = modelValue.value as string
})
const updatePlaceholder = () => {
placeholderVisible.value = !textElement.value?.innerText.trim()
}
defineExpose({ titleElement: textElement })
</script>
<template>
<div class="relative">
<div ref="textElement" contenteditable spellcheck="false" @blur="validate" @update="updatePlaceholder"
@keydown="updatePlaceholder" :class="cn(
'whitespace-pre-line inline-block w-full',
'dark:bg-input/30 border-input w-full min-w-0 rounded-md border bg-input px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
props.class,
)">
{{ modelValue }}
</div>
<span v-if="placeholderVisible" class="pointer-events-none absolute left-1 text-muted-foreground/30">
{{ placeholder }}
</span>
<div ref="grow-wrap" class="grow-wrap">
<textarea rows="1" name="text" id="text" v-model="modelValue" data-slot="textarea" :placeholder="placeholder"
:class="cn('border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm', props.class)" />
</div>
</template>
</template>
<style>
.grow-wrap {
/* easy way to plop the elements on top of each other and have them both sized based on the tallest one's height */
display: grid;
}
.grow-wrap::after {
/* Note the weird space! Needed to preventy jumpy behavior */
content: attr(data-replicated-value) " ";
/* This is how textarea text behaves */
white-space: pre-wrap;
/* Hidden from view, clicks, and screen readers */
visibility: hidden;
}
.grow-wrap>textarea {
/* You could leave this, but after a user resizes, then it ruins the auto sizing */
resize: none;
/* Firefox shows scrollbar on growth, you can hide like this. */
overflow: hidden;
}
.grow-wrap>textarea,
.grow-wrap::after {
/* Identical styling required!! */
padding: calc(var(--spacing) * 1);
font: inherit;
/* Place on top of each other */
grid-area: 1 / 1 / 2 / 2;
}
</style>