diff --git a/package.json b/package.json index 798a66b..21ec341 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@vee-validate/rules": "^4.1.20", "@vueform/multiselect": "^2.0.1", "esno": "0.4.4", + "maska": "^1.4.4", "vee-validate": "4.4.4", "vue": "3.1.1" }, diff --git a/src/components/LcForm/LcForm.stories.ts b/src/components/LcForm/LcForm.stories.ts index ea0d1ed..240c9f6 100644 --- a/src/components/LcForm/LcForm.stories.ts +++ b/src/components/LcForm/LcForm.stories.ts @@ -1,3 +1,5 @@ +import { action } from '@storybook/addon-actions' + import LcForm from './LcForm' export default { @@ -10,7 +12,11 @@ const Template = (args: any) => ({ setup() { return { args } }, - template: '', + template: '', + methods: { + onSubmit: action('onSubmit'), + onCancel: action('onCancel'), + }, }) export const Base = Template.bind({}) as any @@ -34,7 +40,7 @@ Base.args = { }, fields: [ { - model: '', + model: 'Bob', inputType: 'input', attr: { disabled: true, @@ -104,5 +110,17 @@ Base.args = { wrapperClass: 'mb-4', }, }, + { + model: '', + inputType: 'mask', + attr: { + label: 'Siret', + mask: '### ### ### #####', + name: 'siret', + placeholder: '### ### ### #####', + rules: { required: true, regex: /^[0-9]{3} [0-9]{3} [0-9]{3} [0-9]{5}$/ }, + wrapperClass: 'w-full lc-col mb-4', + }, + }, ], } diff --git a/src/components/LcForm/LcForm.vue b/src/components/LcForm/LcForm.vue index 9df7144..06a8b39 100644 --- a/src/components/LcForm/LcForm.vue +++ b/src/components/LcForm/LcForm.vue @@ -40,6 +40,11 @@ :options="field.options || []" v-bind="field.attr" /> + @@ -76,6 +81,7 @@ import { defineComponent, ref } from 'vue' import { Form as vForm, defineRule, configure } from 'vee-validate' import { + regex, required, email, min, @@ -90,6 +96,7 @@ import LcButton from '../LcButton' import LcMultiselect from '../LcMultiselect' import LcCheckbox from '../LcCheckbox' import LcInput from '../LcInput' +import LcMaskInput from '../LcMaskInput' import LcTextarea from '../LcTextarea' import { LcRadioGroup } from '../LcRadio' import { FieldClassContainer, FormValues } from './types' @@ -101,6 +108,7 @@ configure({ }) defineRule('required', required) +defineRule('regex', regex) defineRule('email', email) defineRule('min', min) defineRule('numeric', numeric) @@ -113,6 +121,7 @@ export default defineComponent({ LcButton, LcCheckbox, LcInput, + LcMaskInput, LcMultiselect, LcTextarea, LcRadioGroup, diff --git a/src/components/LcInput/LcInput.vue b/src/components/LcInput/LcInput.vue index 61a30fe..412dd60 100644 --- a/src/components/LcInput/LcInput.vue +++ b/src/components/LcInput/LcInput.vue @@ -22,7 +22,7 @@ @blur="onBlur" > - + @@ -136,7 +136,4 @@ export default defineComponent({ .lc-input[disabled=disabled] { -webkit-text-fill-color: #aaaaaa; } -.lc-input--error { - @apply text-sm text-error; -} diff --git a/src/components/LcMaskInput/LcMaskInput.d.ts b/src/components/LcMaskInput/LcMaskInput.d.ts new file mode 100644 index 0000000..dd6e676 --- /dev/null +++ b/src/components/LcMaskInput/LcMaskInput.d.ts @@ -0,0 +1,3 @@ +import { DefineComponent } from 'vue' +const component: DefineComponent<{}, {}, any> +export default component diff --git a/src/components/LcMaskInput/LcMaskInput.stories.ts b/src/components/LcMaskInput/LcMaskInput.stories.ts new file mode 100644 index 0000000..e8a49b6 --- /dev/null +++ b/src/components/LcMaskInput/LcMaskInput.stories.ts @@ -0,0 +1,39 @@ +import { action } from '@storybook/addon-actions' + +import LcMaskInput from './LcMaskInput' + +export default { + title: 'Example/LcMaskInput', + component: LcMaskInput, +} + +const Template = (args: any) => ({ + components: { LcMaskInput }, + setup() { + return { args } + }, + template: ``, + methods: { + onBlur: action('onBlur'), + rawValue: action('rawValue'), + }, +}) + +export const Base = Template.bind({}) as any +Base.args = { + label: 'Siret', + name: 'siret', + modelValue: '', + wrapperClass: 'w-full', + mask: '### ### ### #####', +} + +export const Value = Template.bind({}) as any +Value.args = { + ...Base.args, + modelValue: '11122233355555', +} diff --git a/src/components/LcMaskInput/LcMaskInput.vue b/src/components/LcMaskInput/LcMaskInput.vue new file mode 100644 index 0000000..568a5e7 --- /dev/null +++ b/src/components/LcMaskInput/LcMaskInput.vue @@ -0,0 +1,145 @@ + + + + + diff --git a/src/components/LcMaskInput/__tests__/LcMaskInput.spec.ts b/src/components/LcMaskInput/__tests__/LcMaskInput.spec.ts new file mode 100644 index 0000000..6f12eb1 --- /dev/null +++ b/src/components/LcMaskInput/__tests__/LcMaskInput.spec.ts @@ -0,0 +1,107 @@ +import { mount } from '@vue/test-utils' +import LcMaskInput from '../LcMaskInput' + +let wrapper: any + +afterEach(() => { + wrapper?.unmount() +}) + +describe('LcInput', () => { + it('is a Vue instance', () => { + wrapper = mount(LcMaskInput, { props: { name: 'siret' } }) + expect(wrapper.vm).toBeTruthy() + }) + + describe('Global input behavior', () => { + beforeEach(() => { + wrapper = mount(LcMaskInput, { props: { name: 'siret' } }) + }) + + it('should set good id and name attributes', () => { + const input = wrapper.find('[data-testid="lc-mask-input"]') + + expect(input.attributes('id')).toBe('siret') + expect(input.attributes('name')).toBe('siret') + }) + + it('should render a label', async() => { + const labelText = 'Your Siret' + await wrapper.setProps({ label: labelText }) + const label = wrapper.get('label') + + expect(label.text()).toBe(labelText) + }) + + it('should set the right input class', async() => { + await wrapper.setProps({ wrapperClass: 'foo' }) + + expect(wrapper.classes()).toContain('foo') + }) + + it('should set a placeholder', async() => { + const placeholderText = 'your siret' + await wrapper.setProps({ placeholder: placeholderText }) + const input = wrapper.find('[data-testid="lc-mask-input"]') + + expect(input.attributes('placeholder')).toBe(placeholderText) + }) + + it('should emit blur event when leave input', async() => { + const input = wrapper.find('[data-testid="lc-mask-input"]') + await input.trigger('blur') + + expect(wrapper.emitted('blur')).toBeTruthy() + }) + }) + + describe('Maska behavior', () => { + it('should value input is formatted', () => { + wrapper = mount(LcMaskInput, { + props: { + name: 'siret', + mask: '### ### ### #####', + modelValue: '12345678910112', + }, + }) + + const input = wrapper.find('[data-testid="lc-mask-input"]') + + expect(input.element.value).toBe('123 456 789 10112') + }) + + it('should value input is empty', async() => { + wrapper = mount(LcMaskInput, { + props: { + name: 'number', + mask: '###', + modelValue: '', + }, + }) + + const input = wrapper.find('[data-testid="lc-mask-input"]') + await input.setValue('AZE') + + expect(input.element.value).toBe('') + }) + + it('should emit raw-value event when value change', async() => { + wrapper = mount(LcMaskInput, { + props: { + name: 'number', + mask: '### ####', + modelValue: '', + }, + }) + + const input = wrapper.find('[data-testid="lc-mask-input"]') + await input.setValue('123 456') + + const eventEmitted = wrapper.emitted('raw-value') + // take the last event because maska launches several + const lastEvent = eventEmitted.length - 1 + + expect(eventEmitted[lastEvent]).toEqual(['123456']) + }) + }) +}) diff --git a/src/components/LcMaskInput/index.ts b/src/components/LcMaskInput/index.ts new file mode 100644 index 0000000..8c0b9db --- /dev/null +++ b/src/components/LcMaskInput/index.ts @@ -0,0 +1 @@ +export { default } from './LcMaskInput.vue' diff --git a/src/library.ts b/src/library.ts index f6c2f6c..65e6957 100644 --- a/src/library.ts +++ b/src/library.ts @@ -6,6 +6,7 @@ import LcCheckbox from './components/LcCheckbox' import LcForm from './components/LcForm' import LcIcon from './components/LcIcon' import LcInput from './components/LcInput' +import LcMaskInput from './components/LcMaskInput' import LcModal from './components/LcModal' import LcMultiselect from './components/LcMultiselect' import LcPagination from './components/LcPagination' @@ -21,6 +22,7 @@ export { LcForm, LcIcon, LcInput, + LcMaskInput, LcModal, LcMultiselect, LcPagination, diff --git a/yarn.lock b/yarn.lock index 6d4dbc1..73c2835 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10118,6 +10118,11 @@ markdown-to-jsx@^7.1.3: resolved "https://registry.yarnpkg.com/markdown-to-jsx/-/markdown-to-jsx-7.1.3.tgz#f00bae66c0abe7dd2d274123f84cb6bd2a2c7c6a" integrity sha512-jtQ6VyT7rMT5tPV0g2EJakEnXLiPksnvlYtwQsVVZ611JsWGN8bQ1tVSDX4s6JllfEH6wmsYxNjTUAMrPmNA8w== +maska@^1.4.4: + version "1.4.4" + resolved "https://registry.yarnpkg.com/maska/-/maska-1.4.4.tgz#0a43dcd10c7ab5d58be59a7a5fd577c496e73fd1" + integrity sha512-0z6O2s+KhA/13y2GXSSeX6rg5at9F9mMt+wbx+v/juUAFFGPtiPjXlqZu09lkN7gi4G1xJ2yeN+gvHBJi5SsTA== + md5.js@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"