From f6661ed4f74bf0abfad84b3309fca90847a5a939 Mon Sep 17 00:00:00 2001 From: Nozomu Ikuta <16436160+NozomuIkuta@users.noreply.github.com> Date: Mon, 30 Jan 2023 14:53:01 +0900 Subject: [PATCH] feat(input-addon): add `SInputAddon` (#202) (#206) close #202 Co-authored-by: Kia Ishii --- docs/.vitepress/config.ts | 1 + docs/components/input-addon.md | 178 +++++++++++++ docs/components/input-number.md | 24 ++ lib/components/SInputAddon.vue | 143 ++++++++++ lib/components/SInputDropdown.vue | 32 +-- lib/components/SInputNumber.vue | 2 + lib/components/SInputText.vue | 248 +++++++++++------- lib/composables/Dropdown.ts | 67 ++++- lib/composables/Flyout.ts | 10 +- lib/styles/variables.css | 1 + .../SInputNumber.02_Addons.story.vue | 221 ++++++++++++++++ .../components/SInputText.02_Addons.story.vue | 187 +++++++++++++ tests/Utils.ts | 2 +- tests/components/SInputAddon.spec.ts | 103 ++++++++ tests/components/SInputText.spec.ts | 10 +- 15 files changed, 1089 insertions(+), 140 deletions(-) create mode 100644 docs/components/input-addon.md create mode 100644 lib/components/SInputAddon.vue create mode 100644 stories/components/SInputNumber.02_Addons.story.vue create mode 100644 stories/components/SInputText.02_Addons.story.vue create mode 100644 tests/components/SInputAddon.spec.ts diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index f9077be8..438a8cb1 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -48,6 +48,7 @@ function sidebar() { items: [ { text: 'SAvatar', link: '/components/avatar' }, { text: 'SButton', link: '/components/button' }, + { text: 'SInputAddon', link: '/components/input-addon' }, { text: 'SInputFile', link: '/components/input-file' }, { text: 'SInputNumber', link: '/components/input-number' }, { text: 'SInputRadios', link: '/components/input-radios' }, diff --git a/docs/components/input-addon.md b/docs/components/input-addon.md new file mode 100644 index 00000000..ed84b0ca --- /dev/null +++ b/docs/components/input-addon.md @@ -0,0 +1,178 @@ + + +# SInputAddon + +`` is a special component that can be used to add extra label or action to other input. + + + + + + + +## Usage + +In order to use ``, you must inject it through slots. Currently, the supported components are listed below. + +- `` +- [``](/components/input-number) + +You may choose the position of the addon to be injected using the slot name either `#addon-before` or `#addon-after`. + +```vue + + + +``` + +## Props + +Here are the list of props you may pass to the component. + +### `:label` + +Defines the label of the addon button. + +```ts +import { IconifyIcon } from '@iconify/vue/dist/offline' + +interface Props { + label?: string | IconifyIcon +} +``` + +```vue-html + +``` + +### `:clickable` + +Defines whether the button should be clickable. Defaults to `true`. + +```ts +interface Props { + clickable?: boolean +} +``` + +```vue-html + +``` + +### `:dropdown` + +Defines dropdown option. When this prop is set, the dropdown will pop up when user clicks the addon button. In addition, if `:label` is not defined, the selected option's label will be used for label value. + +```ts +import { DropdownSection } from '@globalbrain/sefirot/lib/composables/Dropdown' + +interface Props { + dropdown?: DropdownSection[] +} +``` + +```vue + + + +``` + +### `:dropdown-caret` + +Whether to show caret icon when `:dropdown` is defined. Defaults to `true`. + +```ts +interface Props { + dropdownCaret?: boolean +} +``` + +```vue-html + +``` + +### `:dropdown-position` + +Fix the dropdown dialog position. If it's not defined, the dialog will be placed based on window space. + +```ts +interface Props { + dropdowpPosition?: 'top' | 'bottom' +} +``` + +```vue-html + +``` + +### `:disabled` + +Disable the addon action. When this prop is set, no events are emitted. + +```ts +interface Props { + disabled?: boolean +} +``` + +```vue-html + +``` + +## Events + +Here are the list of events the component may emit. + +### `@click` + +Emits when the user clicks the addon button. It will not be emitted when `:clickable` is set to `false`, or `:disabled` is set. + +```ts +interface Emits { + (e: 'click'): void +} +``` diff --git a/docs/components/input-number.md b/docs/components/input-number.md index a0474d87..a189d119 100644 --- a/docs/components/input-number.md +++ b/docs/components/input-number.md @@ -256,6 +256,30 @@ Same as `info` prop. When `info` prop and this slot are defined at the same time ``` +### `#addon-before` + +Inject [``](/components/input-addon) before the input. Learn more details about addon in [Components: SInputAddon](/components/input-addon). + +```vue-html + + + +``` + +### `#addon-after` + +Inject [``](/components/input-addon) after the input. Learn more details about addon in [Components: SInputAddon](/components/input-addon). + +```vue-html + + + +``` + ## Events Here are the list of events the component may emit. diff --git a/lib/components/SInputAddon.vue b/lib/components/SInputAddon.vue new file mode 100644 index 00000000..4384a7dd --- /dev/null +++ b/lib/components/SInputAddon.vue @@ -0,0 +1,143 @@ + + + + + diff --git a/lib/components/SInputDropdown.vue b/lib/components/SInputDropdown.vue index 359e5e06..c529a118 100644 --- a/lib/components/SInputDropdown.vue +++ b/lib/components/SInputDropdown.vue @@ -1,10 +1,9 @@ + + + + diff --git a/stories/components/SInputText.02_Addons.story.vue b/stories/components/SInputText.02_Addons.story.vue new file mode 100644 index 00000000..5c157702 --- /dev/null +++ b/stories/components/SInputText.02_Addons.story.vue @@ -0,0 +1,187 @@ + + + + + diff --git a/tests/Utils.ts b/tests/Utils.ts index 4c27785b..cb83b56f 100644 --- a/tests/Utils.ts +++ b/tests/Utils.ts @@ -31,7 +31,7 @@ export function assertEmitted( wrapper: VueWrapper, event: string, count: number, - value: any + value?: any ): void { expect((wrapper.emitted(event) as any[][])[count - 1][0]).toBe(value) } diff --git a/tests/components/SInputAddon.spec.ts b/tests/components/SInputAddon.spec.ts new file mode 100644 index 00000000..00c57960 --- /dev/null +++ b/tests/components/SInputAddon.spec.ts @@ -0,0 +1,103 @@ +import { mount } from '@vue/test-utils' +import SInputAddon from 'sefirot/components/SInputAddon.vue' +import { assertEmitted, assertNotEmitted } from 'tests/Utils' + +describe('components/SInputAddon', () => { + test('it displays given label', () => { + const wrapper = mount(SInputAddon, { + props: { + label: 'Label' + } + }) + + expect(wrapper.find('.SInputAddon .action-label').text()).toBe('Label') + }) + + test('it focuses the button', async () => { + const wrapper = mount(SInputAddon) + + await wrapper.find('.SInputAddon .action').trigger('focus') + + expect(wrapper.find('.SInputAddon').classes('focused')).toBe(true) + }) + + test('it blurs the button', async () => { + const wrapper = mount(SInputAddon) + + await wrapper.find('.SInputAddon .action').trigger('focus') + expect(wrapper.find('.SInputAddon').classes('focused')).toBe(true) + + await wrapper.find('.SInputAddon .action').trigger('blur') + expect(wrapper.find('.SInputAddon').classes('focused')).toBe(false) + }) + + test('it emits `click` event', async () => { + const wrapper = mount(SInputAddon) + + await wrapper.find('.SInputAddon .action').trigger('click') + + assertEmitted(wrapper, 'click', 1) + }) + + test('it renders `div` for action if it is not clickable', () => { + const wrapper = mount(SInputAddon, { + props: { + clickable: false + } + }) + + expect(wrapper.find('.SInputAddon .action').element.tagName).toBe('DIV') + }) + + test('it opens dropdown if it is defined', async () => { + const wrapper = mount(SInputAddon, { + props: { + dropdown: [ + { + type: 'filter', + selected: null, + options: [ + { label: 'Item 1', value: 1, onClick: () => {} }, + { label: 'Item 2', value: 2, onClick: () => {} } + ] + } + ] + } + }) + + await wrapper.find('.SInputAddon .action').trigger('click') + + expect(wrapper.find('.SInputAddon .dialog').isVisible()).toBe(true) + }) + + test('it displayes selected dropdown item if `label` is not set', async () => { + const wrapper = mount(SInputAddon, { + props: { + dropdown: [ + { + type: 'filter', + selected: 2, + options: [ + { label: 'Item 1', value: 1, onClick: () => {} }, + { label: 'Item 2', value: 2, onClick: () => {} } + ] + } + ] + } + }) + + expect(wrapper.find('.SInputAddon .action-label').text()).toBe('Item 2') + }) + + test('it does not emit events if it is disabled', async () => { + const wrapper = mount(SInputAddon, { + props: { + disabled: true + } + }) + + await wrapper.find('.SInputAddon .action').trigger('click') + + assertNotEmitted(wrapper, 'click') + }) +}) diff --git a/tests/components/SInputText.spec.ts b/tests/components/SInputText.spec.ts index d3d35060..0373310e 100644 --- a/tests/components/SInputText.spec.ts +++ b/tests/components/SInputText.spec.ts @@ -11,13 +11,13 @@ describe('components/SInputText', () => { }) await wrapper.find('.SInputText .input').setValue('') - assertEmitted(wrapper, 'update:modelValue', 1, null) + assertEmitted(wrapper, 'update:model-value', 1, null) await wrapper.find('.SInputText .input').setValue('text') - assertEmitted(wrapper, 'update:modelValue', 2, 'text') + assertEmitted(wrapper, 'update:model-value', 2, 'text') await wrapper.find('.SInputText .input').setValue('0') - assertEmitted(wrapper, 'update:modelValue', 3, '0') + assertEmitted(wrapper, 'update:model-value', 3, '0') }) it('should emit blur event', async () => { @@ -28,7 +28,7 @@ describe('components/SInputText', () => { }) await wrapper.find('.SInputText .input').trigger('blur') - assertEmitted(wrapper, 'update:modelValue', 1, 'text') + assertEmitted(wrapper, 'update:model-value', 1, 'text') assertEmitted(wrapper, 'blur', 1, 'text') }) @@ -40,7 +40,7 @@ describe('components/SInputText', () => { }) await wrapper.find('.SInputText .input').trigger('keypress', { key: 'enter' }) - assertEmitted(wrapper, 'update:modelValue', 1, 'text') + assertEmitted(wrapper, 'update:model-value', 1, 'text') assertEmitted(wrapper, 'enter', 1, 'text') }) })