diff options
author | 2023-05-14 21:26:18 +0200 | |
---|---|---|
committer | 2023-05-14 22:30:23 +0200 | |
commit | 77f2efc0b92847c3b1198446f4b520ecb2263164 (patch) | |
tree | 942ea5ffd922dbe7a9913e235a4fae17b117c925 /src/ui/c-input-text | |
parent | aad8d84e13ce31c1b7c1cbb930fb8bd4c0abe13a (diff) | |
download | it-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.vue | 45 | ||||
-rw-r--r-- | src/ui/c-input-text/c-input-text.test.ts | 79 | ||||
-rw-r--r-- | src/ui/c-input-text/c-input-text.vue | 180 |
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'); } |