aboutsummaryrefslogtreecommitdiff
path: root/src/ui/c-input-text
diff options
context:
space:
mode:
authorGravatar Corentin Thomasset <corentin.thomasset74@gmail.com> 2023-05-14 21:26:18 +0200
committerGravatar Corentin THOMASSET <corentin.thomasset74@gmail.com> 2023-05-14 22:30:23 +0200
commit77f2efc0b92847c3b1198446f4b520ecb2263164 (patch)
tree942ea5ffd922dbe7a9913e235a4fae17b117c925 /src/ui/c-input-text
parentaad8d84e13ce31c1b7c1cbb930fb8bd4c0abe13a (diff)
downloadit-tools-77f2efc0b92847c3b1198446f4b520ecb2263164.tar.gz
it-tools-77f2efc0b92847c3b1198446f4b520ecb2263164.tar.zst
it-tools-77f2efc0b92847c3b1198446f4b520ecb2263164.zip
refactor(ui): replaced some n-input with c-input-text
Diffstat (limited to 'src/ui/c-input-text')
-rw-r--r--src/ui/c-input-text/c-input-text.demo.vue45
-rw-r--r--src/ui/c-input-text/c-input-text.test.ts79
-rw-r--r--src/ui/c-input-text/c-input-text.vue180
3 files changed, 256 insertions, 48 deletions
diff --git a/src/ui/c-input-text/c-input-text.demo.vue b/src/ui/c-input-text/c-input-text.demo.vue
index 2363219..5a5fa99 100644
--- a/src/ui/c-input-text/c-input-text.demo.vue
+++ b/src/ui/c-input-text/c-input-text.demo.vue
@@ -2,6 +2,9 @@
<h2>Default</h2>
<c-input-text value="qsd" />
+ <c-input-text
+ value="Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolorum, est modi iusto repellendus fuga accusantium atque at magnam aliquam eum explicabo vero quia, nobis quasi quis! Earum amet quam a?"
+ />
<h2>With placeholder</h2>
@@ -24,16 +27,50 @@
<h2>Validation</h2>
- <c-input-text
- v-model:value="value"
- :validation-rules="[{ message: 'Length must be > 10', validator: (value) => value.length > 10 }]"
- />
+ <c-input-text v-model:value="value" :validation-rules="validationRules" mb-2 />
+ <c-input-text v-model:value="value" :validation-rules="validationRules" mb-2 label-position="left" label="Yo " />
+ <c-input-text v-model:value="value" :validation="validation" />
+ <c-input-text v-model:value="value" :validation="validation" multiline rows="3" />
<h2>Clearable</h2>
<c-input-text v-model:value="value" clearable />
+
+ <h2>Type password</h2>
+
+ <c-input-text value="value" type="password" />
+
+ <h2>Multiline</h2>
+
+ <c-input-text value="value" multiline label="Label" mb-2 rows="1" />
+ <c-input-text value="value" multiline label="Label" mb-2 />
+ <c-input-text
+ value="Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolorum, est modi iusto repellendus fuga accusantium atque at magnam aliquam eum explicabo vero quia, nobis quasi quis! Earum amet quam a?"
+ multiline
+ mb-2
+ />
+
+ <c-input-text v-model:value="valueLong" multiline autosize mb-2 rows="5" />
+
+ <c-input-text
+ value="Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolorum, est modi iusto repellendus fuga accusantium atque at magnam aliquam eum explicabo vero quia, nobis quasi quis! Earum amet quam a?"
+ multiline
+ clearable
+ />
</template>
<script lang="ts" setup>
+import { useValidation } from '@/composable/validation';
+
const value = ref('value');
+const valueLong = ref(
+ 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolorum, est modi iusto repellendus fuga accusantium atque at magnam aliquam eum explicabo vero quia, nobis quasi quis! Earum amet quam a?',
+);
+
+const validationRules = [{ message: 'Length must be > 10', validator: (value: string) => value.length > 10 }];
+
+const validation = useValidation({
+ source: value,
+ rules: validationRules,
+});
</script>
diff --git a/src/ui/c-input-text/c-input-text.test.ts b/src/ui/c-input-text/c-input-text.test.ts
index 56b5855..69f4046 100644
--- a/src/ui/c-input-text/c-input-text.test.ts
+++ b/src/ui/c-input-text/c-input-text.test.ts
@@ -1,7 +1,8 @@
import { describe, expect, it, beforeEach } from 'vitest';
-import { shallowMount } from '@vue/test-utils';
+import { shallowMount, mount } from '@vue/test-utils';
import { setActivePinia, createPinia } from 'pinia';
import _ from 'lodash';
+import { useValidation } from '@/composable/validation';
import CInputText from './c-input-text.vue';
describe('CInputText', () => {
@@ -71,10 +72,28 @@ describe('CInputText', () => {
it('renders a feedback message for invalid rules', async () => {
const wrapper = shallowMount(CInputText, {
- props: { rules: [{ validator: () => false, message: 'Message' }] },
+ props: { validationRules: [{ validator: () => false, message: 'Message' }] },
});
- expect(wrapper.get('.feedback').text()).to.equal('Message');
+ const feedback = wrapper.find('.feedback');
+ expect(feedback.exists()).to.equal(true);
+ expect(feedback.text()).to.equal('Message');
+ });
+
+ it('if the value become valid according to rules, the feedback disappear', async () => {
+ const wrapper = shallowMount(CInputText, {
+ props: {
+ validationRules: [{ validator: (value: string) => value === 'Hello', message: 'Value should be Hello' }],
+ },
+ });
+
+ const feedback = wrapper.find('.feedback');
+ expect(feedback.exists()).to.equal(true);
+ expect(feedback.text()).to.equal('Value should be Hello');
+
+ await wrapper.setProps({ value: 'Hello' });
+
+ expect(wrapper.find('.feedback').exists()).to.equal(false);
});
it('feedback does not render for valid rules', async () => {
@@ -84,4 +103,58 @@ describe('CInputText', () => {
expect(wrapper.find('.feedback').exists()).to.equal(false);
});
+
+ it('renders a feedback message for invalid custom validation wrapper', async () => {
+ const wrapper = shallowMount(CInputText, {
+ props: {
+ validation: useValidation({ source: ref(), rules: [{ validator: () => false, message: 'Message' }] }),
+ },
+ });
+
+ const feedback = wrapper.find('.feedback');
+ expect(feedback.exists()).to.equal(true);
+ expect(feedback.text()).to.equal('Message');
+ });
+
+ it('feedback does not render for valid custom validation wrapper', async () => {
+ const wrapper = shallowMount(CInputText, {
+ props: {
+ validation: useValidation({ source: ref(), rules: [{ validator: () => true, message: 'Message' }] }),
+ },
+ });
+ expect(wrapper.find('.feedback').exists()).to.equal(false);
+ });
+
+ it('if the value become valid according to the custom validation wrapper, the feedback disappear', async () => {
+ const source = ref('');
+
+ const wrapper = shallowMount(CInputText, {
+ props: {
+ validation: useValidation({
+ source,
+ rules: [{ validator: (value: string) => value === 'Hello', message: 'Value should be Hello' }],
+ }),
+ },
+ });
+
+ const feedback = wrapper.find('.feedback');
+ expect(feedback.exists()).to.equal(true);
+ expect(feedback.text()).to.equal('Value should be Hello');
+
+ source.value = 'Hello';
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.find('.feedback').exists()).to.equal(false);
+ });
+
+ it('[prop:testId] renders a test id on the input', async () => {
+ const wrapper = mount(CInputText, {
+ props: {
+ testId: 'TEST',
+ },
+ });
+
+ expect(wrapper.get('input').attributes('data-test-id')).to.equal('TEST');
+ });
});
diff --git a/src/ui/c-input-text/c-input-text.vue b/src/ui/c-input-text/c-input-text.vue
index a40d26a..51c2805 100644
--- a/src/ui/c-input-text/c-input-text.vue
+++ b/src/ui/c-input-text/c-input-text.vue
@@ -1,32 +1,60 @@
<template>
- <div class="c-input-text" :class="{ disabled, error: !validation.isValid, 'label-left': labelPosition === 'left' }">
+ <div
+ class="c-input-text"
+ :class="{ disabled, error: !validation.isValid, 'label-left': labelPosition === 'left', multiline }"
+ >
<label v-if="label" :for="id" class="label"> {{ label }} </label>
- <div class="input-wrapper">
- <slot name="prefix" />
-
- <input
- :id="id"
- v-model="value"
- type="text"
- class="input"
- :placeholder="placeholder"
- :readonly="readonly"
- :disabled="disabled"
- :data-test-id="testId"
- :autocapitalize="autocapitalize ?? (rawText ? 'off' : undefined)"
- :autocomplete="autocomplete ?? (rawText ? 'off' : undefined)"
- :autocorrect="autocorrect ?? (rawText ? 'off' : undefined)"
- :spellcheck="spellcheck ?? (rawText ? false : undefined)"
- />
-
- <c-button v-if="clearable && value" variant="text" circle size="small" @click="value = ''">
- <icon-mdi-close />
- </c-button>
- <slot name="suffix" />
- </div>
+ <div class="feedback-wrapper">
+ <div ref="inputWrapperRef" class="input-wrapper">
+ <slot name="prefix" />
+
+ <textarea
+ v-if="multiline"
+ :id="id"
+ ref="textareaRef"
+ v-model="value"
+ class="input"
+ :placeholder="placeholder"
+ :readonly="readonly"
+ :disabled="disabled"
+ :data-test-id="testId"
+ :autocapitalize="autocapitalize ?? (rawText ? 'off' : undefined)"
+ :autocomplete="autocomplete ?? (rawText ? 'off' : undefined)"
+ :autocorrect="autocorrect ?? (rawText ? 'off' : undefined)"
+ :spellcheck="spellcheck ?? (rawText ? false : undefined)"
+ :rows="rows"
+ />
+
+ <input
+ v-else
+ :id="id"
+ v-model="value"
+ :type="htmlInputType"
+ class="input"
+ size="1"
+ :placeholder="placeholder"
+ :readonly="readonly"
+ :disabled="disabled"
+ :data-test-id="testId"
+ :autocapitalize="autocapitalize ?? (rawText ? 'off' : undefined)"
+ :autocomplete="autocomplete ?? (rawText ? 'off' : undefined)"
+ :autocorrect="autocorrect ?? (rawText ? 'off' : undefined)"
+ :spellcheck="spellcheck ?? (rawText ? false : undefined)"
+ />
+
+ <c-button v-if="clearable && value" variant="text" circle size="small" @click="value = ''">
+ <icon-mdi-close />
+ </c-button>
- <span v-if="!validation.isValid" class="feedback"> {{ validation.message }} </span>
+ <c-button v-if="type === 'password'" variant="text" circle size="small" @click="showPassword = !showPassword">
+ <icon-mdi-eye v-if="!showPassword" />
+ <icon-mdi-eye-off v-if="showPassword" />
+ </c-button>
+ <slot name="suffix" />
+ </div>
+ <span v-if="!validation.isValid" class="feedback"> {{ validation.message }} </span>
+ </div>
</div>
</template>
@@ -45,6 +73,7 @@ const props = withDefaults(
readonly?: boolean;
disabled?: boolean;
validationRules?: UseValidationRule<string>[];
+ validation?: ReturnType<typeof useValidation>;
labelPosition?: 'top' | 'left';
labelWidth?: string;
labelAlign?: 'left' | 'right';
@@ -55,6 +84,10 @@ const props = withDefaults(
autocorrect?: 'on' | 'off' | string;
spellcheck?: 'true' | 'false' | boolean;
rawText?: boolean;
+ type?: 'text' | 'password';
+ multiline?: boolean;
+ rows?: number | string;
+ autosize?: boolean;
}>(),
{
value: '',
@@ -64,6 +97,7 @@ const props = withDefaults(
readonly: false,
disabled: false,
validationRules: () => [],
+ validation: undefined,
labelPosition: 'top',
labelWidth: 'auto',
labelAlign: 'left',
@@ -74,20 +108,58 @@ const props = withDefaults(
autocorrect: undefined,
spellcheck: undefined,
rawText: false,
+ type: 'text',
+ multiline: false,
+ rows: 3,
+ autosize: false,
},
);
const emit = defineEmits(['update:value']);
const value = useVModel(props, 'value', emit);
+const showPassword = ref(false);
-const { id, placeholder, label, validationRules, labelPosition, labelWidth, labelAlign } = toRefs(props);
+const { id, placeholder, label, validationRules, labelPosition, labelWidth, labelAlign, autosize } = toRefs(props);
-const validation = useValidation({
- rules: validationRules,
- source: value,
-});
+const validation =
+ props.validation ??
+ useValidation({
+ rules: validationRules,
+ source: value,
+ });
const theme = useTheme();
const appTheme = useAppTheme();
+
+const textareaRef = ref<HTMLTextAreaElement>();
+const inputWrapperRef = ref<HTMLElement>();
+
+watch(
+ value,
+ () => {
+ if (props.multiline && autosize.value) {
+ resizeTextarea();
+ }
+ },
+ { immediate: true },
+);
+
+function resizeTextarea() {
+ if (!textareaRef.value || !inputWrapperRef.value) {
+ return;
+ }
+
+ const { scrollHeight } = textareaRef.value;
+
+ inputWrapperRef.value.style.height = `${scrollHeight + 2}px`;
+}
+
+const htmlInputType = computed(() => {
+ if (props.type === 'password' && !showPassword.value) {
+ return 'password';
+ }
+
+ return 'text';
+});
</script>
<style lang="less" scoped>
@@ -114,29 +186,55 @@ const appTheme = useAppTheme();
}
}
- & > .feedback {
+ & .feedback {
color: v-bind('appTheme.error.color');
}
}
& > .label {
+ flex-shrink: 0;
margin-bottom: 5px;
flex: 0 0 v-bind('labelWidth');
text-align: v-bind('labelAlign');
- padding-right: 10px;
+ padding-right: 12px;
}
- .input-wrapper {
+ .feedback-wrapper {
flex: 1 1 0;
min-width: 0;
-
+ }
+ .input-wrapper {
display: flex;
flex-direction: row;
align-items: center;
background-color: v-bind('theme.backgroundColor');
+ color: transparent;
border: 1px solid v-bind('theme.borderColor');
border-radius: 4px;
padding: 0 4px 0 12px;
+ transition: border-color 0.2s ease-in-out;
+
+ .multiline& {
+ resize: vertical;
+ overflow: hidden;
+
+ & > textarea {
+ height: 100%;
+ resize: none;
+ word-break: break-word;
+ white-space: pre-wrap;
+ overflow-wrap: break-word;
+ border: none;
+ outline: none;
+ font-family: inherit;
+ font-size: inherit;
+ color: v-bind('appTheme.text.baseColor');
+
+ &::placeholder {
+ color: v-bind('appTheme.text.mutedColor');
+ }
+ }
+ }
& > .input {
flex: 1 1 0;
@@ -144,7 +242,6 @@ const appTheme = useAppTheme();
padding: 8px 0;
outline: none;
- transition: border-color 0.2s ease-in-out;
background-color: transparent;
background-image: none;
-webkit-box-shadow: none;
@@ -159,12 +256,13 @@ const appTheme = useAppTheme();
}
}
- &:hover,
- &:focus {
+ &:hover {
border-color: v-bind('appTheme.primary.color');
}
- &:focus {
+ &:focus-within {
+ border-color: v-bind('appTheme.primary.color');
+
background-color: v-bind('theme.focus.backgroundColor');
}
}
@@ -173,11 +271,11 @@ const appTheme = useAppTheme();
border-color: v-bind('appTheme.error.color');
&:hover,
- &:focus {
+ &:focus-within {
border-color: v-bind('appTheme.error.color');
}
- &:focus {
+ &:focus-within {
background-color: v-bind('appTheme.error.color + 22');
}
}
@@ -186,7 +284,7 @@ const appTheme = useAppTheme();
opacity: 0.5;
&:hover,
- &:focus {
+ &:focus-within {
border-color: v-bind('theme.borderColor');
}