feat(textarea): new component textarea

This commit is contained in:
Timothy Yin 2024-11-26 14:09:11 +08:00
parent 77cc38e552
commit 00a7c05aec
6 changed files with 419 additions and 0 deletions

View File

@ -0,0 +1,182 @@
---
description: Create a textarea component
since: 1.3.5
---
## Usage
The basic usage.
::ComponentPreview
---
privateProps:
placeholder: Description
---
::
### Sizes
::ComponentPreview
---
privateProps:
placeholder: Description
props:
size: sm
---
::
### Colors
::ComponentPreview
---
privateProps:
placeholder: Description
props:
color: primary
---
::
### Variants
::ComponentPreview
---
privateProps:
placeholder: Description
props:
variant: outline
---
::
### Placeholder
You can also set a placeholder.
::ComponentPreview
---
props:
placeholder: "Description here..."
---
::
### Padded
::ComponentPreview
---
privateProps:
placeholder: Description
variant: plain
props:
padded: false
---
::
### Rows
Set the number of rows of the textarea.
::ComponentPreview
---
privateProps:
placeholder: Description
props:
rows: 4
---
::
### Resize
Enable the resize control.
::ComponentPreview
---
privateProps:
placeholder: Description
props:
resize: true
---
::
### Auto Resize
The `autosize` prop enables the auto resizing of the textarea. The textarea will grow in height as the user types.
::ComponentPreview
---
privateProps:
placeholder: Description
props:
autosize: true
---
::
The `maxrows` prop can be used to set the maximum number of rows the textarea can grow to.
::ComponentPreview
---
privateProps:
placeholder: Description
props:
autosize: true
maxrows: 8
---
::
### Disabled
::ComponentPreview
---
privateProps:
placeholder: Description
props:
disabled: true
---
::
### Model Modifiers
#### .trim
The `.trim` modifier trims the input value.
```vue [page]
<script lang="ts" setup>
const modal = ref<string>("");
</script>
<template>
<RayTextarea v-model.trim="modal" />
</template>
```
#### .number
The `.number` modifier converts the input value to a number. Non-numeric values are ignored.
```vue [page]
<script lang="ts" setup>
const modal = ref<number>(0);
</script>
<template>
<RayTextarea v-model.number="modal" />
</template>
```
#### .lazy
The `.lazy` modifier syncs the input value with the model only on `change` event.
```vue [page]
<script lang="ts" setup>
const modal = ref<string>("");
</script>
<template>
<RayTextarea v-model.lazy="modal" />
</template>
```
## Config
::ComponentDefaults
::

View File

@ -0,0 +1,192 @@
<script lang="ts">
import { computed, defineComponent, onMounted, ref, toRef, watch, type PropType } from 'vue'
import { twMerge, twJoin } from 'tailwind-merge'
import defu from 'defu'
import { textarea } from '../../ui.config'
import type { DeepPartial, Strategy, TextareaColor, TextareaModelModifiers, TextareaSize, TextareaVariant } from '../../types'
import { useRayUI } from '#build/imports'
const config = textarea
export default defineComponent({
props: {
modelValue: {
type: [String, Number] as PropType<string | number | null>,
default: '',
},
size: {
type: String as PropType<TextareaSize>,
default: config.default.size,
},
variant: {
type: String as PropType<TextareaVariant>,
default: config.default.variant,
},
color: {
type: String as PropType<TextareaColor>,
default: config.default.color,
},
autofocus: {
type: Boolean,
default: false,
},
autofocusDelay: {
type: Number,
default: 100,
},
placeholder: {
type: String,
default: null,
},
rows: {
type: Number,
default: 3,
},
autosize: {
type: Boolean,
default: false,
},
maxrows: {
type: Number,
default: 0,
},
padded: {
type: Boolean,
default: true,
},
resize: {
type: Boolean,
default: false,
},
class: {
type: String,
default: '',
},
ui: {
type: Object as PropType<DeepPartial<typeof config> & { strategy?: Strategy }>,
default: () => ({}),
},
modelModifiers: {
type: Object as PropType<TextareaModelModifiers>,
default: () => ({}),
},
},
emits: [
'update:modelValue',
'blur',
'change',
],
setup(props, { emit }) {
const { ui, attrs } = useRayUI('textarea', toRef(props, 'ui'), config)
const modelModifiers = ref(defu({}, props.modelModifiers, { lazy: false, number: false, trim: false }))
const textarea = ref<HTMLTextAreaElement | null>(null)
const baseClass = computed(() => {
return twMerge(twJoin(
ui.value.base,
ui.value.rounded,
ui.value.placeholder,
ui.value.size[props.size],
props.padded && ui.value.padding[props.size],
ui.value.variant[props.variant].replaceAll('{color}', props.color),
!props.resize && 'resize-none',
), props.class)
})
const autoResize = () => {
if (!props.autosize) return
if (!textarea.value) return
textarea.value.rows = props.rows
const overflowBefore = textarea.value.style.overflow
textarea.value.style.overflow = 'hidden'
const style = window.getComputedStyle(textarea.value)
const padding = Number.parseInt(style.paddingTop) + Number.parseInt(style.paddingBottom)
const lineHeight = Number.parseInt(style.lineHeight)
const { scrollHeight, clientHeight } = textarea.value
const computedRows = Math.floor((scrollHeight - padding) / lineHeight)
if (computedRows > props.rows) {
textarea.value.rows = props.maxrows ? Math.min(computedRows, props.maxrows) : computedRows
}
textarea.value.style.overflow = overflowBefore
console.log('computedRows', computedRows)
}
const updateValue = (value: string) => {
if (modelModifiers.value.trim) {
value = value.trim()
}
if (modelModifiers.value.number) {
const n = Number.parseFloat(value)
value = (Number.isNaN(n) ? value : n) as any
}
emit('update:modelValue', value)
}
const onInput = (e: Event) => {
autoResize()
if (modelModifiers.value.lazy) return
updateValue((e.target as HTMLInputElement).value)
}
const onChange = (e: Event) => {
const value = (e.target as HTMLInputElement).value
emit('change', value)
if (modelModifiers.value.lazy) {
updateValue(value)
}
if (modelModifiers.value.trim) {
(e.target as HTMLInputElement).value = value.trim()
}
}
const onBlur = (e: Event) => {
emit('blur', e)
}
watch(() => props.modelValue, () => {
autoResize()
})
onMounted(() => {
if (props.autofocus) {
setTimeout(() => {
textarea.value?.focus()
}, props.autofocusDelay)
}
autoResize()
})
return {
// eslint-disable-next-line vue/no-dupe-keys
ui,
attrs,
textarea,
baseClass,
onInput,
onChange,
onBlur,
}
},
})
</script>
<template>
<div :class="ui.wrapper">
<textarea
ref="textarea"
:class="baseClass"
:rows="rows"
:placeholder="placeholder"
v-bind="attrs"
:value="modelValue"
@input="onInput"
@change="onChange"
@blur="onBlur"
/>
</div>
</template>

View File

@ -1,6 +1,7 @@
export * from './button'
export * from './message'
export * from './input'
export * from './textarea'
export * from './kbd'
export * from './toggle'

19
src/runtime/types/textarea.d.ts vendored Normal file
View File

@ -0,0 +1,19 @@
import type { AppConfig } from '@nuxt/schema'
import type { textarea } from '../ui.config'
import type { ExtractDeepKey } from './utils'
import type colors from '#ray-colors'
export type TextareaSize =
| keyof typeof textarea.size
| ExtractDeepKey<AppConfig, ['rayui', 'textarea', 'size']>
export type TextareaColor =
| ExtractDeepKey<AppConfig, ['rayui', 'textarea', 'color']>
| (typeof colors)[number]
export type TextareaVariant =
| keyof typeof textarea.variant
| ExtractDeepKey<AppConfig, ['rayui', 'textarea', 'variant']>
export type TextareaModelModifiers = {
number?: boolean
trim?: boolean
lazy?: boolean
}

View File

@ -0,0 +1,24 @@
import { standard } from '..'
export default {
wrapper: 'relative',
base: 'relative w-full block focus:outline-none disabled:cursor-not-allowed disabled:opacity-70 transition',
placeholder: 'placeholder:text-gray-400 dark:placeholder:text-gray-500',
rounded: 'rounded-md',
size: {
...standard.size,
},
padding: {
...standard.padding,
},
variant: {
outline:
'shadow-sm bg-transparent text-gray-900 dark:text-white ring ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-{color}-500 dark:focus:ring-{color}-400',
plain: 'bg-transparent',
},
default: {
size: 'sm',
color: 'primary',
variant: 'outline',
},
}

View File

@ -7,6 +7,7 @@ export { default as kbd } from './elements/kbd'
// forms
export { default as input } from './forms/input'
export { default as textarea } from './forms/textarea'
export { default as toggle } from './forms/toggle'
// overlays