From 948745969649a60a203c0c784d0c97e4da223e31 Mon Sep 17 00:00:00 2001 From: smithoo Date: Tue, 11 Jun 2024 17:36:27 +0900 Subject: [PATCH] fix(VsSelect): allow vs-select multiple chips flex-wrap (#206) --- .../src/components/vs-chip/VsChip.scss | 4 +- .../vlossom/src/components/vs-chip/VsChip.vue | 1 + .../src/components/vs-section/VsSection.scss | 6 +- .../src/components/vs-select/VsSelect.scss | 199 ++++++++------ .../src/components/vs-select/VsSelect.vue | 259 +++++++++--------- .../vs-select/__tests__/vs-select.test.ts | 164 ++++++----- .../composables/autocomplete-composable.ts | 10 +- .../composables/select-option-composable.ts | 5 +- .../composables/toggle-options-composable.ts | 2 +- .../vlossom/src/components/vs-select/types.ts | 1 + .../src/components/vs-table/VsTable.scss | 8 +- .../src/components/vs-table/VsTable.vue | 30 +- .../vs-table/__tests__/vs-table.test.ts | 2 +- .../src/components/vs-tabs/VsTabs.scss | 3 + 14 files changed, 373 insertions(+), 321 deletions(-) diff --git a/packages/vlossom/src/components/vs-chip/VsChip.scss b/packages/vlossom/src/components/vs-chip/VsChip.scss index 8bf2cbf6f..7a2dbbd7a 100644 --- a/packages/vlossom/src/components/vs-chip/VsChip.scss +++ b/packages/vlossom/src/components/vs-chip/VsChip.scss @@ -21,8 +21,8 @@ border-radius: 50%; background-color: var(--vs-white); border: var(--vs-chip-border, 1px solid var(--vs-line-color)); - height: calc(var(--vs-chip-height, 1.35rem) * 0.8); - width: calc(var(--vs-chip-height, 1.35rem) * 0.8); + height: calc(var(--vs-chip-height, 1.6rem) * 0.6); + width: calc(var(--vs-chip-height, 1.6rem) * 0.6); } .close-button { diff --git a/packages/vlossom/src/components/vs-chip/VsChip.vue b/packages/vlossom/src/components/vs-chip/VsChip.vue index 1759e42bb..31e7ca9ce 100644 --- a/packages/vlossom/src/components/vs-chip/VsChip.vue +++ b/packages/vlossom/src/components/vs-chip/VsChip.vue @@ -13,6 +13,7 @@ type="button" class="icon-container close-button" aria-label="close" + tabindex="-1" @click.stop="$emit('close')" > diff --git a/packages/vlossom/src/components/vs-section/VsSection.scss b/packages/vlossom/src/components/vs-section/VsSection.scss index 0cea30b76..bdd0359fe 100644 --- a/packages/vlossom/src/components/vs-section/VsSection.scss +++ b/packages/vlossom/src/components/vs-section/VsSection.scss @@ -3,7 +3,7 @@ border-radius: var(--vs-section-borderRadius, calc(var(--vs-radius-ratio) * 0.6rem)); box-shadow: var(--vs-section-boxShadow, var(--vs-area-shadow-outer)); color: var(--vs-section-fontColor, var(--vs-font-color)); - padding: var(--vs-section-padding, 2rem); + padding: var(--vs-section-padding, 2.4rem 3rem); position: relative; overflow: auto; @@ -14,13 +14,13 @@ @media screen and (max-width: 992px) { .vs-section { - padding: var(--vs-section-padding, 1.6rem); + padding: var(--vs-section-padding, 2rem 2.4rem); } } @media screen and (max-width: 768px) { .vs-section { - padding: var(--vs-section-padding, 1.2rem); + padding: var(--vs-section-padding, 1.6rem); } } diff --git a/packages/vlossom/src/components/vs-select/VsSelect.scss b/packages/vlossom/src/components/vs-select/VsSelect.scss index 8c8a76977..6c3b7ae7d 100644 --- a/packages/vlossom/src/components/vs-select/VsSelect.scss +++ b/packages/vlossom/src/components/vs-select/VsSelect.scss @@ -2,76 +2,99 @@ .vs-select { display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: nowrap; background-color: var(--vs-select-backgroundColor, var(--vs-no-color)); - border: var(--vs-select-border, solid 1px var(--vs-line-color)); - height: var(--vs-select-height, 2.4rem); - line-height: var(--vs-select-height, 2.4rem); + border: var(--vs-select-border, 1px solid var(--vs-line-color)); + min-height: var(--vs-select-height, 2.4rem); border-radius: var(--vs-select-borderRadius, calc(var(--vs-radius-ratio) * 0.4rem)); font-size: var(--vs-select-fontSize, 0.9rem); - .multiple-chips { - display: flex; - padding-left: 0.6rem; - overflow: hidden; + .vs-select-wrap { + .vs-select-input { + width: 100%; + height: var(--vs-select-height, 2.4rem); + padding: 0 0.8rem; + border: none; + outline: none; + background-color: transparent; + color: var(--vs-select-fontColor, var(--vs-font-color)); + cursor: pointer; - .chips { - display: flex; - align-items: center; - flex-wrap: nowrap; - overflow-x: auto; - overflow-y: hidden; + &.autocomplete { + cursor: default; + } - .chip-others { - margin-left: 0.2rem; - padding: 0 0.8rem; + &:not(.autocomplete)::selection { + background: none; } } - } - input { - flex: 1; - padding: 0 0.8rem; - border: none; - outline: none; - background-color: transparent; - color: var(--vs-select-fontColor, var(--vs-font-color)); - cursor: pointer; - min-width: 2rem; - line-height: 1rem; - - &.autocomplete { - cursor: default; - } + .multiple-chips { + display: flex; + padding-left: 0.6rem; + overflow: hidden; + + .chips { + display: flex; + align-items: center; + flex-wrap: wrap; + overflow-x: auto; + overflow-y: hidden; + + .select-chip { + margin-top: 0.1rem; + margin-bottom: 0.1rem; + } + .chip-others { + margin-left: 0.2rem; + padding: 0 0.8rem; + } + + &.multiple-only { + padding-bottom: 0.3rem; + } + } - &:not(.autocomplete)::selection { - background: none; + &.autocompleted { + padding-bottom: 0.4rem; + } } } - .clear-button { + .vs-select-side { + position: relative; display: flex; - justify-content: center; align-items: center; - padding-right: 0.6rem; - opacity: 0; - transition: opacity 0.4s; - color: var(--vs-input-clearButtonColor, var(--vs-font-color)); - background: none; - border: none; - cursor: pointer; - } + justify-content: flex-end; + width: 2.4rem; - .arrow-box { - display: flex; - align-items: center; - cursor: pointer; - padding-right: 0.6rem; + .clear-button { + display: flex; + justify-content: center; + align-items: center; + padding-right: 0.6rem; + opacity: 1; + transition: opacity 0.4s; + color: var(--vs-input-clearButtonColor, var(--vs-font-color)); + background: none; + border: none; + cursor: pointer; + } - .arrow-icon { - transition: transform 0.2s ease-out; + .arrow-box { + display: flex; + align-items: center; + cursor: pointer; + padding-right: 0.6rem; + + .arrow-icon { + transition: transform 0.2s ease-out; - &.arrow-up { - transform: rotate(180deg); + &.arrow-up { + transform: rotate(180deg); + } } } } @@ -83,9 +106,12 @@ } &.dense { - height: var(--vs-select-height, 2rem); - line-height: var(--vs-select-height, 2rem); + min-height: var(--vs-select-height, 2rem); font-size: var(--vs-select-fontSize, 0.8rem); + + .vs-select-input { + height: var(--vs-select-height, 2rem); + } } &.disabled { @@ -108,49 +134,58 @@ .vs-options-container { z-index: 900; background-color: var(--vs-select-backgroundColor, var(--vs-no-color)); - border: var(--vs-select-border, solid 1px var(--vs-line-color)); + border: var(--vs-select-border, 1px solid var(--vs-line-color)); border-radius: var(--vs-select-borderRadius, calc(var(--vs-radius-ratio) * 0.4rem)); overflow: hidden; + .options-header { + position: relative; + border-bottom: var(--vs-select-border, 1px solid var(--vs-line-color)); + } + + .options-footer { + position: relative; + border-top: var(--vs-select-border, 1px solid var(--vs-line-color)); + } + .vs-select-options { - max-height: 25rem; + max-height: var(--vs-select-optionsHeight, 25rem); overflow: auto; list-style-type: none; outline: none; + } - li { - padding: 0 1.2rem; - height: var(--vs-select-height, 2.4rem); - line-height: var(--vs-select-height, 2.4rem); - color: var(--vs-select-fontColor, var(--vs-font-color)); - cursor: pointer; - user-select: none; - font-size: var(--vs-select-fontSize, 0.9rem); + .vs-option { + position: relative; + display: flex; + align-items: center; + padding: 0 1.2rem; + min-height: var(--vs-select-height, 2.4rem); + background-color: var(--vs-no-color); + color: var(--vs-select-fontColor, var(--vs-font-color)); + cursor: pointer; + user-select: none; + font-size: var(--vs-select-fontSize, 0.9rem); - &:not(.selected).chased { - background-color: var(--vs-select-hoverOptionBackgroundColor, var(--vs-area-bg-hover)); - color: var(--vs-select-hoverOptionColor, var(--vs-font-color)); - } + &.select-all { + border-bottom: var(--vs-select-border, 1px solid var(--vs-line-color)); + } - &.select-all { - background-color: var(--vs-no-color); - padding: 0.4 0.4rem; - height: var(--vs-select-height, 2.4rem); - line-height: var(--vs-select-height, 2.4rem); - } + &.selected { + background-color: var(--vs-select-selectedOptionBackgroundColor, var(--vs-area-bg-active)); + color: var(--vs-select-selectedOptionColor, var(--vs-font-color)); + font-weight: 600; + } - &.selected { - background-color: var(--vs-select-selectedOptionBackgroundColor, var(--vs-area-bg-active)); - color: var(--vs-select-selectedOptionColor, var(--vs-font-color)); - font-weight: 600; - } + &.chased { + background-color: var(--vs-select-hoverOptionBackgroundColor, var(--vs-area-bg-hover)); + color: var(--vs-select-hoverOptionColor, var(--vs-font-color)); } } &.dense { - li { - height: var(--vs-select-height, 2rem); - line-height: var(--vs-select-height, 2rem); + .vs-option { + min-height: var(--vs-select-height, 2rem); font-size: var(--vs-select-fontSize, 0.8rem); } } diff --git a/packages/vlossom/src/components/vs-select/VsSelect.vue b/packages/vlossom/src/components/vs-select/VsSelect.vue index bf5f35b62..060a8a07a 100644 --- a/packages/vlossom/src/components/vs-select/VsSelect.vue +++ b/packages/vlossom/src/components/vs-select/VsSelect.vue @@ -15,86 +15,96 @@
-
-
- - {{ getOptionLabel(selectedOptions[0].value) }} - - - + {{ selectedOptions.length - 1 }} - -
-
- - {{ getOptionLabel(option.value) }} - +
+ + +
+
+ + {{ getOptionLabel(selectedOptions[0].value) }} + + + + {{ selectedOptions.length - 1 }} + +
+
+ + {{ getOptionLabel(option.value) }} + +
- - - - - -
- +
+ + +
+ +
@@ -108,9 +118,33 @@ ]" :style="computedStyleSet" > -
+
+
+ + Select All + +
  • - - Select All - - -
  • -
  • @@ -176,9 +185,11 @@ {{ getOptionLabel(option.value) }}
  • -
  • No Options
  • +
  • + No Options +
-
+
@@ -212,14 +223,13 @@ import { utils } from '@/utils'; import VsWrapper from '@/components/vs-wrapper/VsWrapper.vue'; import VsInputWrapper from '@/components/vs-input-wrapper/VsInputWrapper.vue'; import VsChip from '@/components/vs-chip/VsChip.vue'; -import VsDivider from '@/components/vs-divider/VsDivider.vue'; import type { VsChipStyleSet } from '@/components/vs-chip/types'; const name = VsComponent.VsSelect; export default defineComponent({ name, - components: { VsInputWrapper, VsWrapper, VsChip, VsIcon, VsDivider }, + components: { VsInputWrapper, VsWrapper, VsChip, VsIcon }, props: { ...getInputProps(), ...getInputOptionProps(), @@ -373,6 +383,8 @@ export default defineComponent({ if (autocomplete.value) { autocompleteText.value = ''; } + + closeOptions(); } const { computedMessages, computedState, shake, validate, clear, id } = useInput( @@ -410,12 +422,26 @@ export default defineComponent({ multiple, ); + const inputLabel = computed(() => { + if (focusing.value && autocomplete.value) { + return autocompleteText.value; + } + + if (multiple.value) { + return ''; + } + + return selectedOptions.value[0] ? getOptionLabel(selectedOptions.value[0].value) : ''; + }); + const { isOpen, isClosing, toggleOptions, closeOptions, triggerRef, optionsRef, isVisible, computedPlacement } = useToggleOptions(id, disabled, readonly); const { autocompleteText, filteredOptions, updateAutocompleteText } = useAutocomplete( + autocomplete, computedOptions, getOptionLabel, + inputLabel, isOpen, ); @@ -465,24 +491,9 @@ export default defineComponent({ function onBlur(e: FocusEvent) { focusing.value = false; - if (autocomplete.value) { - autocompleteText.value = inputLabel.value; - } emit('blur', e); } - const inputLabel = computed(() => { - if (focusing.value && autocomplete.value) { - return autocompleteText.value; - } - - if (multiple.value) { - return ''; - } - - return selectedOptions.value[0] ? getOptionLabel(selectedOptions.value[0].value) : ''; - }); - const animationClass = computed(() => { if (isOpen.value) { return computedPlacement.value === 'top' ? 'fade-enter-bottom' : 'fade-enter-top'; diff --git a/packages/vlossom/src/components/vs-select/__tests__/vs-select.test.ts b/packages/vlossom/src/components/vs-select/__tests__/vs-select.test.ts index 94a9508a9..4be6f9a82 100644 --- a/packages/vlossom/src/components/vs-select/__tests__/vs-select.test.ts +++ b/packages/vlossom/src/components/vs-select/__tests__/vs-select.test.ts @@ -31,10 +31,10 @@ describe('vs-select', () => { }); //when - await wrapper.find('input').trigger('click'); + await wrapper.find('.vs-select').trigger('click'); // then - expect(wrapper.findAll('li.option')).toHaveLength(3); + expect(wrapper.findAll('.vs-option')).toHaveLength(3); expect(wrapper.html()).toContain('A'); expect(wrapper.html()).toContain('B'); expect(wrapper.html()).toContain('C'); @@ -60,10 +60,10 @@ describe('vs-select', () => { }); //when - await wrapper.find('input').trigger('click'); + await wrapper.find('.vs-select').trigger('click'); // then - expect(wrapper.findAll('li.option')).toHaveLength(3); + expect(wrapper.findAll('.vs-option')).toHaveLength(3); expect(wrapper.html()).toContain('A'); expect(wrapper.html()).toContain('B'); expect(wrapper.html()).toContain('C'); @@ -123,7 +123,7 @@ describe('vs-select', () => { }); //when - await wrapper.find('input').trigger('click'); + await wrapper.find('.vs-select').trigger('click'); // then expect(wrapper.html()).toContain('header'); @@ -146,7 +146,7 @@ describe('vs-select', () => { }); //when - await wrapper.find('input').trigger('click'); + await wrapper.find('.vs-select').trigger('click'); // then expect(wrapper.html()).toContain('footer'); @@ -166,7 +166,7 @@ describe('vs-select', () => { }); // then - expect(wrapper.find('input').element.value).toBe('A'); + expect(wrapper.vm.inputRef?.value).toBe('A'); }); it('modelValue를 업데이트 할 수 있다', async () => { @@ -185,8 +185,8 @@ describe('vs-select', () => { }); // when - await wrapper.find('input').trigger('click'); - await wrapper.findAll('li.option')[1].trigger('click'); + await wrapper.find('.vs-select').trigger('click'); + await wrapper.findAll('.vs-option')[1].trigger('click'); // then const updateModelValueEvent = wrapper.emitted('update:modelValue'); @@ -208,7 +208,7 @@ describe('vs-select', () => { await wrapper.setProps({ modelValue: 'B' }); // then - expect(wrapper.find('input').element.value).toBe('B'); + expect(wrapper.vm.inputRef?.value).toBe('B'); }); }); @@ -234,7 +234,7 @@ describe('vs-select', () => { }); // then - expect(wrapper.find('input').element.value).toBe('A'); + expect(wrapper.vm.inputRef?.value).toBe('A'); }); it('modelValue를 업데이트 할 수 있다', async () => { @@ -253,8 +253,8 @@ describe('vs-select', () => { }); // when - await wrapper.find('input').trigger('click'); - await wrapper.findAll('li.option')[1].trigger('click'); + await wrapper.find('.vs-select').trigger('click'); + await wrapper.findAll('.vs-option')[1].trigger('click'); // then const updateModelValueEvent = wrapper.emitted('update:modelValue'); @@ -276,7 +276,7 @@ describe('vs-select', () => { await wrapper.setProps({ modelValue: 'b' }); // then - expect(wrapper.find('input').element.value).toBe('B'); + expect(wrapper.vm.inputRef?.value).toBe('B'); }); }); @@ -296,7 +296,7 @@ describe('vs-select', () => { expect(wrapper.findAllComponents({ name: 'VsChip' })).toHaveLength(2); expect(wrapper.findAllComponents({ name: 'VsChip' })[0].html()).toContain('A'); expect(wrapper.findAllComponents({ name: 'VsChip' })[1].html()).toContain('B'); - expect(wrapper.find('input').element.value).toBe(''); + expect(wrapper.find('.vs-select-input').exists()).toBe(false); }); it('modelValue를 업데이트 할 수 있다', async () => { @@ -316,8 +316,8 @@ describe('vs-select', () => { }); // when - await wrapper.find('input').trigger('click'); - await wrapper.findAll('li.option')[2].trigger('click'); + await wrapper.find('.vs-select').trigger('click'); + await wrapper.findAll('.vs-option')[2].trigger('click'); // then const updateModelValueEvent = wrapper.emitted('update:modelValue'); @@ -395,12 +395,12 @@ describe('vs-select', () => { }); // when - await wrapper.find('input').trigger('click'); - await wrapper.findAll('li.option')[1].trigger('click'); + await wrapper.find('.vs-select').trigger('click'); + await wrapper.findAll('.vs-option')[1].trigger('click'); await vi.advanceTimersByTime(500); // then - expect(wrapper.find('input').element.value).toBe('B'); + expect(wrapper.vm.inputRef?.value).toBe('B'); expect(wrapper.find('ul.vs-select-options').exists()).toBe(false); }); @@ -419,11 +419,10 @@ describe('vs-select', () => { }); // when - await wrapper.find('input').trigger('click'); - await wrapper.findAll('li.option')[1].trigger('click'); + await wrapper.find('.vs-select').trigger('click'); + await wrapper.findAll('.vs-option')[1].trigger('click'); // then - expect(wrapper.find('input').element.value).toBe(''); expect(wrapper.findComponent({ name: 'VsChip' }).exists()).toBe(true); expect(wrapper.findComponent({ name: 'VsChip' }).html()).toContain('B'); expect(wrapper.find('ul.vs-select-options').exists()).toBe(true); @@ -446,14 +445,14 @@ describe('vs-select', () => { }); // when - await wrapper.find('input').trigger('click'); - await wrapper.findAll('li.option')[1].trigger('click'); + await wrapper.find('.vs-select').trigger('click'); + await wrapper.findAll('.vs-option')[1].trigger('click'); // then - expect(wrapper.find('input').element.value).toBe(''); expect(wrapper.findAllComponents({ name: 'VsChip' })).toHaveLength(1); expect(wrapper.findComponent({ name: 'VsChip' }).html()).toContain('A'); expect(wrapper.find('ul.vs-select-options').exists()).toBe(true); + expect(wrapper.find('.vs-select-input').exists()).toBe(false); }); it('selectAll이 true일 때 모든 옵션을 선택할 수 있는 옵션을 제공한다', async () => { @@ -474,16 +473,12 @@ describe('vs-select', () => { }); // when - await wrapper.find('input').trigger('click'); - // then - expect(wrapper.find('ul.vs-select-options').html()).toContain('Select All'); + await wrapper.find('.vs-select').trigger('click'); + await wrapper.find('.select-all').trigger('click'); - // when - await wrapper.find('li.option').trigger('click'); // then - const updateModelValueEvent = wrapper.emitted('update:modelValue'); - expect(updateModelValueEvent).toHaveLength(1); - expect(updateModelValueEvent?.[0]).toEqual([['A', 'B', 'C']]); + expect(wrapper.find('.select-all').exists()).toBe(true); + expect(wrapper.vm.inputValue).toEqual(['A', 'B', 'C']); }); }); @@ -502,7 +497,7 @@ describe('vs-select', () => { attachTo: document.body, }); - await wrapper.find('input').trigger('click'); + await wrapper.find('.vs-select').trigger('click'); // when await vi.advanceTimersByTime(0); @@ -529,10 +524,10 @@ describe('vs-select', () => { attachTo: document.body, }); - await wrapper.find('input').trigger('click'); + await wrapper.find('.vs-select').trigger('click'); // when - await wrapper.findAll('ul.vs-select-options li.option')[1].trigger('click'); + await wrapper.findAll('ul.vs-select-options .vs-option')[1].trigger('click'); await nextTick(); // then @@ -608,12 +603,12 @@ describe('vs-select', () => { }); // when - await wrapper.find('input').trigger('click'); - await wrapper.find('input').setValue('ba'); + await wrapper.find('.vs-select').trigger('click'); + await wrapper.find('.vs-select-input').setValue('ba'); await vi.advanceTimersByTime(500); // then - expect(wrapper.findAll('li.option')).toHaveLength(1); + expect(wrapper.findAll('.vs-option')).toHaveLength(1); expect(wrapper.html()).toContain('banana'); }); @@ -637,7 +632,7 @@ describe('vs-select', () => { ]; // when - const input = wrapper.find('input'); + const input = wrapper.find('.vs-select-input'); await input.setValue('apple'); // then @@ -666,23 +661,23 @@ describe('vs-select', () => { describe('keyboard interaction', () => { it('combobox가 focus를 받은 상태에서 Enter 키, Space 바를 누르면 옵션 리스트를 열고 닫을 수 있다', async () => { // when - await wrapper.find('input').trigger('keydown', { code: 'Enter' }); + await wrapper.find('.vs-select-input').trigger('keydown', { code: 'Enter' }); // then expect(wrapper.find('ul.vs-select-options').exists()).toBe(true); // when - await wrapper.find('input').trigger('keydown', { code: 'Enter' }); + await wrapper.find('.vs-select-input').trigger('keydown', { code: 'Enter' }); await vi.advanceTimersByTime(500); // then expect(wrapper.find('ul.vs-select-options').exists()).toBe(false); // when - await wrapper.find('input').trigger('keydown', { code: 'Space' }); + await wrapper.find('.vs-select-input').trigger('keydown', { code: 'Space' }); // then expect(wrapper.find('ul.vs-select-options').exists()).toBe(true); // when - await wrapper.find('input').trigger('keydown', { code: 'Space' }); + await wrapper.find('.vs-select-input').trigger('keydown', { code: 'Space' }); await vi.advanceTimersByTime(500); // then expect(wrapper.find('ul.vs-select-options').exists()).toBe(false); @@ -690,7 +685,7 @@ describe('vs-select', () => { it('combobox가 focus를 받은 상태에서 Arrow Down 키를 누르면 옵션 리스트가 열리고 listbox의 첫번째 옵션으로 focus가 이동한다', async () => { // when - await wrapper.find('input').trigger('keydown', { code: 'ArrowDown' }); + await wrapper.find('.vs-select-input').trigger('keydown', { code: 'ArrowDown' }); // then expect(wrapper.find('ul.vs-select-options').exists()).toBe(true); @@ -700,41 +695,41 @@ describe('vs-select', () => { it('옵션 리스트를 열고 Arrow Down 키를 누르면 listbox의 첫번째 옵션으로 focus가 이동하고 Enter 키를 누르면 그 옵션이 선택된다', async () => { // when - await wrapper.find('input').trigger('keydown', { code: 'Enter' }); - await wrapper.find('input').trigger('keydown', { code: 'ArrowDown' }); + await wrapper.find('.vs-select-input').trigger('keydown', { code: 'Enter' }); + await wrapper.find('.vs-select-input').trigger('keydown', { code: 'ArrowDown' }); // then const firstId = wrapper.find('ul.vs-select-options').find('li').attributes('id'); - expect(wrapper.find('input').attributes('aria-activedescendant')).toBe(firstId); + expect(wrapper.find('.vs-select-input').attributes('aria-activedescendant')).toBe(firstId); // when - await wrapper.find('input').trigger('keydown', { code: 'ArrowDown' }); - await wrapper.find('input').trigger('keydown', { code: 'Enter' }); + await wrapper.find('.vs-select-input').trigger('keydown', { code: 'ArrowDown' }); + await wrapper.find('.vs-select-input').trigger('keydown', { code: 'Enter' }); // then expect(wrapper.emitted('update:modelValue')).toHaveLength(1); expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['B']); - expect(wrapper.find('input').element.value).toBe('B'); + expect(wrapper.vm.inputRef?.value).toBe('B'); }); it('Arrow Up 키를 누르면 위에 있는 옵션으로 이동하고 Space 바를 누르면 그 옵션이 선택된다', async () => { // when - await wrapper.find('input').trigger('click'); - await wrapper.find('input').trigger('keydown', { code: 'ArrowDown' }); - await wrapper.find('input').trigger('keydown', { code: 'ArrowDown' }); - await wrapper.find('input').trigger('keydown', { code: 'ArrowUp' }); - await wrapper.find('input').trigger('keydown', { code: 'Space' }); + await wrapper.find('.vs-select').trigger('click'); + await wrapper.find('.vs-select-input').trigger('keydown', { code: 'ArrowDown' }); + await wrapper.find('.vs-select-input').trigger('keydown', { code: 'ArrowDown' }); + await wrapper.find('.vs-select-input').trigger('keydown', { code: 'ArrowUp' }); + await wrapper.find('.vs-select-input').trigger('keydown', { code: 'Space' }); // then expect(wrapper.emitted('update:modelValue')).toHaveLength(1); expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['A']); - expect(wrapper.find('input').element.value).toBe('A'); + expect(wrapper.vm.inputRef?.value).toBe('A'); }); it('Escape 키를 누르면 옵션 리스트가 닫힌다', async () => { // when - await wrapper.find('input').trigger('click'); - await wrapper.find('input').trigger('keydown', { code: 'Escape' }); + await wrapper.find('.vs-select').trigger('click'); + await wrapper.find('.vs-select-input').trigger('keydown', { code: 'Escape' }); await vi.advanceTimersByTime(500); // then @@ -743,16 +738,16 @@ describe('vs-select', () => { it('Tab 키를 누르면 focus 중인 옵션이 선택되고 옵션 리스트가 닫힌다', async () => { // when - await wrapper.find('input').trigger('click'); - await wrapper.find('input').trigger('keydown', { code: 'ArrowDown' }); - await wrapper.find('input').trigger('keydown', { code: 'ArrowDown' }); - await wrapper.find('input').trigger('keydown', { code: 'Tab' }); + await wrapper.find('.vs-select').trigger('click'); + await wrapper.find('.vs-select-input').trigger('keydown', { code: 'ArrowDown' }); + await wrapper.find('.vs-select-input').trigger('keydown', { code: 'ArrowDown' }); + await wrapper.find('.vs-select-input').trigger('keydown', { code: 'Tab' }); await vi.advanceTimersByTime(500); // then expect(wrapper.emitted('update:modelValue')).toHaveLength(1); expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['B']); - expect(wrapper.find('input').element.value).toBe('B'); + expect(wrapper.vm.inputRef?.value).toBe('B'); expect(wrapper.find('ul.vs-select-options').exists()).toBe(false); }); @@ -761,16 +756,15 @@ describe('vs-select', () => { wrapper.setProps({ multiple: true }); // when - await wrapper.find('input').trigger('click'); - await wrapper.find('input').trigger('keydown', { code: 'ArrowDown' }); - await wrapper.find('input').trigger('keydown', { code: 'ArrowDown' }); - await wrapper.find('input').trigger('keydown', { code: 'Tab' }); + await wrapper.find('.vs-select').trigger('click'); + await wrapper.find('.vs-select-input').trigger('keydown', { code: 'ArrowDown' }); + await wrapper.find('.vs-select-input').trigger('keydown', { code: 'ArrowDown' }); + await wrapper.find('.vs-select-input').trigger('keydown', { code: 'Tab' }); await vi.advanceTimersByTime(500); // then expect(wrapper.emitted('update:modelValue')).toHaveLength(1); expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([['B']]); - expect(wrapper.find('input').element.value).toBe(''); expect(wrapper.find('ul.vs-select-options').exists()).toBe(false); }); }); @@ -778,37 +772,37 @@ describe('vs-select', () => { describe('mouse event', () => { it('옵션 리스트에서 mouse move event가 발생되면 mouse가 올라가 있던 옵션 기준으로 focus가 이동한다', async () => { // when - await wrapper.find('input').trigger('click'); - await wrapper.findAll('li.option')[1].trigger('mousemove'); - await wrapper.find('input').trigger('keydown', { code: 'ArrowDown' }); - await wrapper.find('input').trigger('keydown', { code: 'Enter' }); + await wrapper.find('.vs-select').trigger('click'); + await wrapper.findAll('.vs-option')[1].trigger('mousemove'); + await wrapper.find('.vs-select-input').trigger('keydown', { code: 'ArrowDown' }); + await wrapper.find('.vs-select-input').trigger('keydown', { code: 'Enter' }); // then expect(wrapper.emitted('update:modelValue')).toHaveLength(1); expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['C']); - expect(wrapper.find('input').element.value).toBe('C'); + expect(wrapper.vm.inputRef?.value).toBe('C'); }); }); describe('combobox focus', () => { it('옵션을 선택하고 옵션창이 닫히면 combobox로 focus가 간다', async () => { // when - await wrapper.find('input').trigger('click'); - await wrapper.findAll('li.option')[1].trigger('click'); + await wrapper.find('.vs-select').trigger('click'); + await wrapper.findAll('.vs-option')[1].trigger('click'); await vi.advanceTimersByTime(500); // then - expect(wrapper.find('input').element).toBe(document.activeElement); + expect(wrapper.find('.vs-select-input').element).toBe(document.activeElement); }); it('Escape 키를 눌러서 옵션창이 닫히면 combobox로 focus가 간다', async () => { // when - await wrapper.find('input').trigger('click'); - await wrapper.find('input').trigger('keydown', { code: 'Escape' }); + await wrapper.find('.vs-select').trigger('click'); + await wrapper.find('.vs-select-input').trigger('keydown', { code: 'Escape' }); await vi.advanceTimersByTime(500); // then - expect(wrapper.find('input').element).toBe(document.activeElement); + expect(wrapper.find('.vs-select-input').element).toBe(document.activeElement); }); }); }); @@ -864,7 +858,7 @@ describe('vs-select', () => { }); // then - expect(wrapper.find('input').attributes('aria-label')).toBe('aria-label'); + expect(wrapper.find('.vs-select-input').attributes('aria-label')).toBe('aria-label'); }); }); @@ -878,7 +872,7 @@ describe('vs-select', () => { }); // when - await wrapper.find('input').trigger('focus'); + await wrapper.find('.vs-select-input').trigger('focus'); // then expect(wrapper.emitted('focus')).toHaveLength(1); @@ -893,7 +887,7 @@ describe('vs-select', () => { }); // when - await wrapper.find('input').trigger('blur'); + await wrapper.find('.vs-select-input').trigger('blur'); // then expect(wrapper.emitted('blur')).toHaveLength(1); @@ -913,7 +907,7 @@ describe('vs-select', () => { }); // when - await wrapper.find('input').trigger('mouseover'); + await wrapper.find('.vs-select-input').trigger('mouseover'); await wrapper.find('button.clear-button').trigger('click'); // then diff --git a/packages/vlossom/src/components/vs-select/composables/autocomplete-composable.ts b/packages/vlossom/src/components/vs-select/composables/autocomplete-composable.ts index 672275d44..91ca0a4fc 100644 --- a/packages/vlossom/src/components/vs-select/composables/autocomplete-composable.ts +++ b/packages/vlossom/src/components/vs-select/composables/autocomplete-composable.ts @@ -2,8 +2,10 @@ import { ref, watch, type Ref } from 'vue'; import { utils } from '@/utils'; export function useAutocomplete( + autocomplete: Ref, computedOptions: Ref<{ id: string; value: any }[]>, getOptionLabel: (option: any) => string, + inputLabel: Ref, isOpen: Ref, ) { const autocompleteText = ref(''); @@ -24,9 +26,13 @@ export function useAutocomplete( }, 300), ); - watch(isOpen, (val) => { - if (val) { + watch(isOpen, (opened) => { + if (opened) { filteredOptions.value = [...computedOptions.value]; + } else { + if (autocomplete.value) { + autocompleteText.value = inputLabel.value; + } } }); diff --git a/packages/vlossom/src/components/vs-select/composables/select-option-composable.ts b/packages/vlossom/src/components/vs-select/composables/select-option-composable.ts index 8d5f53739..f95399f6a 100644 --- a/packages/vlossom/src/components/vs-select/composables/select-option-composable.ts +++ b/packages/vlossom/src/components/vs-select/composables/select-option-composable.ts @@ -10,7 +10,7 @@ export function useSelectOption( closeOptions: () => void, autocomplete: Ref, autocompleteText: Ref, - comboboxFocus: () => void, + focusOnInput: () => void, ) { function isSelectedOption(option: any) { if (multiple.value) { @@ -32,12 +32,12 @@ export function useSelectOption( } else { inputValue.value = getOptionValue(option); closeOptions(); - comboboxFocus(); if (autocomplete.value) { autocompleteText.value = getOptionLabel(option); } } + focusOnInput(); } const isAllSelected = computed(() => { @@ -50,6 +50,7 @@ export function useSelectOption( } else { inputValue.value = computedOptions.value.map((option) => getOptionValue(option.value)); } + closeOptions(); } function removeSelected(option: any) { diff --git a/packages/vlossom/src/components/vs-select/composables/toggle-options-composable.ts b/packages/vlossom/src/components/vs-select/composables/toggle-options-composable.ts index afeaeba1f..107ecb37f 100644 --- a/packages/vlossom/src/components/vs-select/composables/toggle-options-composable.ts +++ b/packages/vlossom/src/components/vs-select/composables/toggle-options-composable.ts @@ -35,7 +35,7 @@ export function useToggleOptions(id: string, disabled: Ref, readonly: R const target = e.target as HTMLElement; // check if click outside of select - if (isOpen.value && target.closest(`#${id}`) === null && target.closest('.vs-select-options') === null) { + if (isOpen.value && target.closest(`#${id}`) === null && target.closest('.vs-options-container') === null) { closeOptions(); } } diff --git a/packages/vlossom/src/components/vs-select/types.ts b/packages/vlossom/src/components/vs-select/types.ts index 7b54a176d..837f22980 100644 --- a/packages/vlossom/src/components/vs-select/types.ts +++ b/packages/vlossom/src/components/vs-select/types.ts @@ -11,6 +11,7 @@ export interface VsSelectStyleSet { height?: string; hoverOptionBackgroundColor?: string; hoverOptionColor?: string; + optionsHeight?: string; selectedOptionBackgroundColor?: string; selectedOptionColor?: string; } diff --git a/packages/vlossom/src/components/vs-table/VsTable.scss b/packages/vlossom/src/components/vs-table/VsTable.scss index daf2c502b..46911ae92 100644 --- a/packages/vlossom/src/components/vs-table/VsTable.scss +++ b/packages/vlossom/src/components/vs-table/VsTable.scss @@ -64,6 +64,7 @@ flex-shrink: 0; } .search-input { + font-weight: 400; flex-grow: 1; } } @@ -187,7 +188,7 @@ left: 0; width: 100%; height: 100%; - border: var(--vs-table-hoverBorder, 1px solid var(--vs-font-color)); + border: var(--vs-table-hoverBorder, 1px solid var(--vs-line-color)); pointer-events: none; } @@ -215,9 +216,8 @@ gap: 0.5rem; margin-bottom: 0.6rem; - .pagination-options { - width: 10rem; - height: 100%; + .vs-pagination-options { + margin-left: 2rem; } } diff --git a/packages/vlossom/src/components/vs-table/VsTable.vue b/packages/vlossom/src/components/vs-table/VsTable.vue index 2dcd04740..1648de682 100644 --- a/packages/vlossom/src/components/vs-table/VsTable.vue +++ b/packages/vlossom/src/components/vs-table/VsTable.vue @@ -72,21 +72,21 @@ :edgeButtons="pageEdgeButtons" :color-scheme="colorScheme" /> -
- -
+
diff --git a/packages/vlossom/src/components/vs-table/__tests__/vs-table.test.ts b/packages/vlossom/src/components/vs-table/__tests__/vs-table.test.ts index f2fc4c027..9cf433d47 100644 --- a/packages/vlossom/src/components/vs-table/__tests__/vs-table.test.ts +++ b/packages/vlossom/src/components/vs-table/__tests__/vs-table.test.ts @@ -309,7 +309,7 @@ describe('VsTable', () => { // then const paginationWrapper = wrapper.find('.table-pagination .vs-pagination'); - const selectWrapper = wrapper.find('.pagination-options .vs-select'); + const selectWrapper = wrapper.find('.vs-pagination-options'); expect(paginationWrapper.exists()).toBe(true); expect(selectWrapper.exists()).toBe(true); }); diff --git a/packages/vlossom/src/components/vs-tabs/VsTabs.scss b/packages/vlossom/src/components/vs-tabs/VsTabs.scss index 98f169af9..641a1cf2e 100644 --- a/packages/vlossom/src/components/vs-tabs/VsTabs.scss +++ b/packages/vlossom/src/components/vs-tabs/VsTabs.scss @@ -8,6 +8,8 @@ $fontWeight: var(--vs-tabs-fontWeight, 400); user-select: none; .tabs-container { + position: relative; + width: 100%; overflow-x: auto; padding-bottom: 0.2rem; @@ -16,6 +18,7 @@ $fontWeight: var(--vs-tabs-fontWeight, 400); align-items: center; list-style: none; flex-wrap: nowrap; + width: 100%; &.bottomLine { border-bottom: 1px solid var(--vs-tabs-borderBottomColor, var(--vs-primary-comp-bg));