Skip to content

Commit ec6cb80

Browse files
committed
feat: add counter to input and textarea
1 parent d1c661b commit ec6cb80

File tree

8 files changed

+116
-21
lines changed

8 files changed

+116
-21
lines changed

.changeset/tasty-bobcats-prove.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@indielayer/ui": minor
3+
---
4+
5+
feat: add counter to input and textarea
Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,28 @@
11
<script setup lang="ts">
22
import { ref } from 'vue'
33
const name = ref('John')
4+
const title = ref('')
45
</script>
56

67
<template>
7-
<x-input
8-
v-model="name"
9-
label="Name"
10-
name="name"
11-
placeholder="Placeholder"
12-
helper="Helper text here"
13-
tooltip="Tooltip here"
14-
/>
8+
<div class="grid gap-6">
9+
<x-input
10+
v-model="name"
11+
label="Name"
12+
name="name"
13+
placeholder="Placeholder"
14+
helper="Helper text here"
15+
tooltip="Tooltip here"
16+
/>
17+
18+
<x-input
19+
v-model="title"
20+
label="Title with character counter"
21+
name="title"
22+
maxlength="50"
23+
show-counter
24+
helper="Character counter is displayed"
25+
placeholder="Enter a title (max 50 characters)"
26+
/>
27+
</div>
1528
</template>

packages/ui/docs/pages/component/textarea/usage.vue

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,31 @@
22
import { ref } from 'vue'
33
44
const multiline = ref('')
5+
const comment = ref('')
56
</script>
67

78
<template>
8-
<div class="grid grid-cols-2 gap-4">
9+
<div class="grid gap-6">
10+
<div class="grid grid-cols-2 gap-4">
11+
<x-textarea
12+
v-model="multiline"
13+
label="Normal textarea"
14+
helper="Helper text"
15+
resizable
16+
placeholder="Placeholder"
17+
tooltip="Tooltip here"
18+
/>
19+
<x-textarea v-model="multiline" label="Multiline adjust" adjust-to-text />
20+
</div>
21+
922
<x-textarea
10-
v-model="multiline"
11-
label="Normal textarea"
12-
helper="Helper text"
13-
resizable
14-
placeholder="Placeholder"
15-
tooltip="Tooltip here"
23+
v-model="comment"
24+
label="Comment with character counter"
25+
maxlength="200"
26+
show-counter
27+
helper="Maximum 200 characters"
28+
placeholder="Enter your comment"
29+
rows="4"
1630
/>
17-
<x-textarea v-model="multiline" label="Multiline adjust" adjust-to-text />
1831
</div>
1932
</template>

packages/ui/src/components/input/Input.vue

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const inputProps = {
2626
},
2727
step: [Number, String],
2828
block: Boolean,
29+
showCounter: Boolean,
2930
}
3031
3132
export type InputProps = ExtractPublicPropTypes<typeof inputProps>
@@ -107,6 +108,12 @@ const {
107108
setError,
108109
} = useInputtable(props, { focus, emit })
109110
111+
const currentLength = computed(() => {
112+
const value = props.modelValue
113+
114+
return value ? String(value).length : 0
115+
})
116+
110117
const { styles, classes, className } = useTheme('Input', {}, props, { errorInternal })
111118
112119
defineExpose({ focus, blur, reset, validate, setError })
@@ -188,6 +195,13 @@ defineExpose({ focus, blur, reset, validate, setError })
188195
</slot>
189196
</div>
190197

191-
<x-input-footer v-if="!hideFooterInternal" :error="errorInternal" :helper="helper"/>
198+
<x-input-footer
199+
v-if="!hideFooterInternal"
200+
:error="errorInternal"
201+
:helper="helper"
202+
:character-count="currentLength"
203+
:max-characters="maxlength"
204+
:show-counter="showCounter"
205+
/>
192206
</x-label>
193207
</template>

packages/ui/src/components/inputFooter/InputFooter.vue

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,60 @@
22
const inputFooterProps = {
33
helper: String,
44
error: String,
5+
characterCount: Number,
6+
maxCharacters: [Number, String],
7+
showCounter: Boolean,
58
}
69
710
export type InputFooterProps = ExtractPublicPropTypes<typeof inputFooterProps>
811
9-
type InternalClasses = 'wrapper' | 'helperText' | 'errorText'
12+
type InternalClasses = 'wrapper' | 'helperText' | 'errorText' | 'container' | 'counter'
1013
export interface InputFooterTheme extends ThemeComponent<InputFooterProps, InternalClasses> {}
1114
1215
export default { name: 'XInputFooter' }
1316
</script>
1417

1518
<script setup lang="ts">
19+
import { computed } from 'vue'
1620
import type { ExtractPublicPropTypes } from 'vue'
1721
import { useTheme, type ThemeComponent } from '../../composables/useTheme'
1822
1923
const props = defineProps(inputFooterProps)
2024
2125
const { styles, classes, className } = useTheme('InputFooter', {}, props)
26+
27+
const maxChars = computed(() => {
28+
return props.maxCharacters ? Number(props.maxCharacters) : undefined
29+
})
30+
31+
const counterText = computed(() => {
32+
if (props.characterCount === undefined) return ''
33+
34+
if (maxChars.value) {
35+
return `${props.characterCount}/${maxChars.value}`
36+
}
37+
38+
return `${props.characterCount}`
39+
})
40+
41+
const hasMessage = computed(() => props.error || props.helper)
2242
</script>
2343

2444
<template>
2545
<div :class="[className, classes.wrapper]" :style="styles">
26-
<p v-if="error" :class="classes.errorText">{{ error }}</p>
27-
<p v-else-if="helper" :class="classes.helperText">{{ helper }}</p>
46+
<div v-if="hasMessage || showCounter" :class="classes.container">
47+
<div>
48+
<p v-if="error" :class="classes.errorText">{{ error }}</p>
49+
<p v-else-if="helper" :class="classes.helperText">{{ helper }}</p>
50+
</div>
51+
<p
52+
v-if="showCounter"
53+
:class="classes.counter"
54+
role="status"
55+
aria-live="polite"
56+
>
57+
{{ counterText }}
58+
</p>
59+
</div>
2860
</div>
2961
</template>

packages/ui/src/components/inputFooter/theme/InputFooter.base.theme.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import type { InputFooterTheme } from '../InputFooter.vue'
33
const theme: InputFooterTheme = {
44
classes: {
55
wrapper: 'text-xs mt-1',
6+
container: 'flex justify-between items-start gap-2',
67
helperText: 'text-secondary-500 dark:text-secondary-400',
78
errorText: 'text-error-500 dark:text-error-400',
9+
counter: 'text-secondary-500 dark:text-secondary-400 whitespace-nowrap',
810
},
911
}
1012

packages/ui/src/components/inputFooter/theme/InputFooter.carbon.theme.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import type { InputFooterTheme } from '../InputFooter.vue'
33
const theme: InputFooterTheme = {
44
classes: {
55
wrapper: 'text-xs mt-1',
6+
container: 'flex justify-between items-start gap-2',
67
helperText: 'text-secondary-500 dark:text-secondary-400',
78
errorText: 'text-error-500 dark:text-error-400',
9+
counter: 'text-secondary-500 dark:text-secondary-400 whitespace-nowrap',
810
},
911
}
1012

packages/ui/src/components/textarea/Textarea.vue

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const textareaProps = {
2020
preventEnter: Boolean,
2121
block: Boolean,
2222
resizable: Boolean,
23+
showCounter: Boolean,
2324
}
2425
2526
export type TextareaProps = ExtractPublicPropTypes<typeof textareaProps>
@@ -107,6 +108,12 @@ const {
107108
setError,
108109
} = useInputtable(props, { focus, emit })
109110
111+
const currentLength = computed(() => {
112+
const value = props.modelValue
113+
114+
return value ? String(value).length : 0
115+
})
116+
110117
const { styles, classes, className } = useTheme('Textarea', {}, props, { errorInternal })
111118
112119
defineExpose({ focus, blur, reset, validate, setError })
@@ -159,6 +166,13 @@ defineExpose({ focus, blur, reset, validate, setError })
159166
<slot name="suffix"></slot>
160167
</div>
161168

162-
<x-input-footer v-if="!hideFooterInternal" :error="errorInternal" :helper="helper"/>
169+
<x-input-footer
170+
v-if="!hideFooterInternal"
171+
:error="errorInternal"
172+
:helper="helper"
173+
:character-count="currentLength"
174+
:max-characters="maxlength"
175+
:show-counter="showCounter"
176+
/>
163177
</x-label>
164178
</template>

0 commit comments

Comments
 (0)