diff --git a/packages/vlossom/package.json b/packages/vlossom/package.json
index 7f00818cb..1db479e2a 100644
--- a/packages/vlossom/package.json
+++ b/packages/vlossom/package.json
@@ -44,9 +44,9 @@
"typings": "./dist/index.d.ts",
"exports": {
".": {
+ "types": "./dist/index.d.ts",
"import": "./dist/vlossom.es.js",
- "require": "./dist/vlossom.umd.js",
- "types": "./dist/index.d.ts"
+ "require": "./dist/vlossom.umd.js"
},
"./styles": "./dist/style.css"
},
diff --git a/packages/vlossom/src/components/index.ts b/packages/vlossom/src/components/index.ts
index ec8e5cd99..a2c90f4a2 100644
--- a/packages/vlossom/src/components/index.ts
+++ b/packages/vlossom/src/components/index.ts
@@ -59,7 +59,6 @@ export { default as VsLoading } from './vs-loading/VsLoading.vue';
export { default as VsMessage } from './vs-message/VsMessage.vue';
-export { type VsModalStyleSet } from './vs-modal/types';
export { default as VsModal } from './vs-modal/VsModal.vue';
export { type VsNoticeStyleSet } from './vs-notice/types';
diff --git a/packages/vlossom/src/components/vs-confirm/VsConfirm.scss b/packages/vlossom/src/components/vs-confirm/VsConfirm.scss
deleted file mode 100644
index 265dc1539..000000000
--- a/packages/vlossom/src/components/vs-confirm/VsConfirm.scss
+++ /dev/null
@@ -1,27 +0,0 @@
-.vs-confirm-text {
- position: relative;
- display: flex;
- align-items: center;
- justify-content: center;
- width: 100%;
- height: 100%;
- flex: 1;
- color: var(--vs-font-color);
- text-align: center;
-}
-
-.vs-confirm-footer {
- align-items: center;
- display: flex;
- justify-content: center;
- padding-bottom: 1rem;
- width: 100%;
-
- .vs-ok-button {
- min-width: 9rem;
- }
-
- .vs-cancel-button {
- min-width: 9rem;
- }
-}
diff --git a/packages/vlossom/src/components/vs-confirm/VsConfirm.vue b/packages/vlossom/src/components/vs-confirm/VsConfirm.vue
deleted file mode 100644
index f604ceaf9..000000000
--- a/packages/vlossom/src/components/vs-confirm/VsConfirm.vue
+++ /dev/null
@@ -1,89 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/packages/vlossom/src/components/vs-confirm/__tests__/vs-confirm.test.ts b/packages/vlossom/src/components/vs-confirm/__tests__/vs-confirm.test.ts
deleted file mode 100644
index 8e8e887d5..000000000
--- a/packages/vlossom/src/components/vs-confirm/__tests__/vs-confirm.test.ts
+++ /dev/null
@@ -1,102 +0,0 @@
-import { describe, it, expect, vi } from 'vitest';
-import { mount } from '@vue/test-utils';
-import { store } from '@/stores';
-import VsConfirm from './../VsConfirm.vue';
-
-describe('vs-confirm', () => {
- const text = 'This is Confirm Text';
-
- it('text를 렌더할 수 있다', async () => {
- // given
- const wrapper = mount(VsConfirm, {
- props: {
- modelValue: true,
- text,
- },
- global: {
- stubs: ['Teleport'],
- },
- });
-
- // then
- expect(wrapper.find('.vs-confirm-text').html()).toContain(text);
- });
-
- it('ok-button의 텍스트를 설정할 수 있다', () => {
- // given
- const okText = 'YES';
- const wrapper = mount(VsConfirm, {
- props: {
- modelValue: true,
- text,
- okText,
- },
- global: {
- stubs: ['Teleport'],
- },
- });
-
- // then
- expect(wrapper.find('.vs-ok-button').text()).toContain(okText);
- });
-
- it('cancel-button의 텍스트를 설정할 수 있다', () => {
- // given
- const cancelText = 'NO';
- const wrapper = mount(VsConfirm, {
- props: {
- modelValue: true,
- text,
- cancelText,
- },
- global: {
- stubs: ['Teleport'],
- },
- });
-
- // then
- expect(wrapper.find('.vs-cancel-button').text()).toContain(cancelText);
- });
-
- it('ok 버튼을 클릭하면 resolve 가 true 로 이행되고 confirm이 닫힌다', async () => {
- // given
- const spy = vi.spyOn(store.confirm, 'executeResolve');
- const wrapper = mount(VsConfirm, {
- props: {
- modelValue: true,
- text,
- },
- global: {
- stubs: ['Teleport'],
- },
- });
-
- // when
- await wrapper.find('.vs-ok-button').trigger('click');
-
- // then
- expect(wrapper.vm.isOpen).toBe(false);
- expect(spy).toHaveBeenCalledWith(true);
- });
-
- it('cancel 버튼을 클릭하면 resolve 가 false 로 이행되고 confirm이 닫힌다', async () => {
- // given
- const spy = vi.spyOn(store.confirm, 'executeResolve');
- const wrapper = mount(VsConfirm, {
- props: {
- modelValue: true,
- text,
- },
- global: {
- stubs: ['Teleport'],
- },
- });
-
- // when
- await wrapper.find('.vs-cancel-button').trigger('click');
-
- // then
- expect(wrapper.vm.isOpen).toBe(false);
- expect(spy).toHaveBeenCalledWith(false);
- });
-});
diff --git a/packages/vlossom/src/components/vs-confirm/stories/VsConfirm.stories.ts b/packages/vlossom/src/components/vs-confirm/stories/VsConfirm.stories.ts
deleted file mode 100644
index c1c85ea01..000000000
--- a/packages/vlossom/src/components/vs-confirm/stories/VsConfirm.stories.ts
+++ /dev/null
@@ -1,224 +0,0 @@
-import { colorScheme, getColorSchemeTemplate, chromaticParameters, size } from '@/storybook';
-import { ref } from 'vue';
-import { useVlossom } from '@/vlossom-framework';
-import VsButton from '@/components/vs-button/VsButton.vue';
-import VsModal from '@/components/vs-modal/VsModal.vue';
-import VsForm from '@/components/vs-form/VsForm.vue';
-import VsInput from '@/components/vs-input/VsInput.vue';
-import VsDivider from '@/components/vs-divider/VsDivider.vue';
-
-import type { ConfirmOptions } from '@/plugins';
-import type { Meta, StoryObj } from '@storybook/vue3';
-
-const meta: Meta = {
- title: 'Plugins/Confirm',
- render: (args: any) => ({
- components: { VsButton, VsDivider },
- setup() {
- const $vs = useVlossom();
- const showResult = ref(false);
- const result = ref(false);
-
- async function confirm() {
- result.value = await $vs.confirm.open(args.text, {
- okText: args.okText,
- cancelText: args.cancelText,
- size: args.size,
- colorScheme: args.colorScheme,
- closeOnEsc: args.closeOnEsc,
- });
- showResult.value = true;
- }
- return { args, confirm, showResult, result };
- },
- template: `
-
-
Open Confirm
-
-
Result : {{ result }}
-
- `,
- }),
- tags: ['autodocs'],
-};
-
-export default meta;
-type OpenStory = StoryObj<{ text: string } | ConfirmOptions>;
-
-export const Default: OpenStory = {
- args: {
- text: 'Are you sure?',
- okText: '',
- cancelText: '',
- closeOnEsc: true,
- },
- argTypes: {
- size,
- colorScheme,
- },
-};
-
-export const ColorScheme: OpenStory = {
- render: (args: any) => ({
- components: { VsButton },
- setup() {
- return { args };
- },
- template: `
-
- ${getColorSchemeTemplate(`
-
-
- Open Confirm ( {{'{{ color }}'.toUpperCase()}} )
-
-
- `)}
-
- `,
- }),
- args: {
- text: 'Are you sure?',
- okText: '',
- cancelText: '',
- },
- argTypes: {
- size,
- colorScheme,
- },
- parameters: {
- chromatic: chromaticParameters.theme,
- },
-};
-
-export const HtmlText: OpenStory = {
- render: (args: any) => ({
- components: { VsButton },
- setup() {
- return { args };
- },
- template: `
- Confirm
- `,
- }),
- args: {
- text: ' Are you sure?
',
- okText: '',
- cancelText: '',
- },
- argTypes: {
- size,
- colorScheme,
- },
-};
-
-export const NestedWithModal: OpenStory = {
- render: (args: any) => ({
- components: { VsButton, VsModal, VsForm, VsInput },
- setup() {
- const $vs = useVlossom();
-
- const isModalOpen = ref(false);
- const formRef = ref('formRef');
- const form = ref({ name: '', email: '' });
-
- const result = ref(false);
-
- function openModal() {
- isModalOpen.value = true;
- form.value = { name: '', email: '' };
- }
-
- function closeModal() {
- isModalOpen.value = false;
- }
-
- async function submit() {
- if (!(await (formRef.value as any)?.validate())) {
- $vs.toast.warn('Invalid Form');
- return;
- }
-
- if (!(await $vs.confirm.open(args.text))) {
- return;
- }
-
- try {
- setTimeout(() => {
- $vs.toast.success('Request Success');
- }, 500);
- closeModal();
- } catch (error) {
- $vs.toast.error(error as Error);
- }
- }
-
- return {
- args,
- isModalOpen,
- formRef,
- form,
- openModal,
- closeModal,
- submit,
- result,
- };
- },
- template: `
-
-
Open Modal
-
-
-
- Modal
-
-
-
-
-
-
-
- Submit
- Cancel
-
-
-
- `,
- }),
- args: {
- text: 'Are you sure?',
- okText: '',
- cancelText: '',
- },
- argTypes: {
- size,
- colorScheme,
- },
-};
-
-type PromptStory = StoryObj<{ text: string; confirmText: string }>;
-
-export const Prompt: PromptStory = {
- render: (args: any) => ({
- components: { VsButton, VsDivider },
- setup() {
- const $vs = useVlossom();
- const showResult = ref(false);
- const result = ref(false);
-
- async function prompt() {
- result.value = await $vs.confirm.prompt(args.text, args.confirmText);
- showResult.value = true;
- }
- return { args, prompt, showResult, result };
- },
- template: `
- Open Prompt
-
- Result : {{ result }}
- `,
- }),
- args: {
- text: "Type 'ABC'",
- confirmText: 'ABC',
- },
-};
diff --git a/packages/vlossom/src/components/vs-drawer/VsDrawer.vue b/packages/vlossom/src/components/vs-drawer/VsDrawer.vue
index bca56c682..5016d5ba5 100644
--- a/packages/vlossom/src/components/vs-drawer/VsDrawer.vue
+++ b/packages/vlossom/src/components/vs-drawer/VsDrawer.vue
@@ -32,11 +32,11 @@ import {
watch,
computed,
getCurrentInstance,
- ComputedRef,
- nextTick,
type PropType,
+ type Ref,
+ type ComputedRef,
} from 'vue';
-import { useColorScheme, useEscClose, useLayout, useBodyScroll, useStyleSet } from '@/composables';
+import { useColorScheme, useLayout, useStyleSet, useOverlay } from '@/composables';
import {
VsComponent,
Placement,
@@ -48,12 +48,14 @@ import {
VS_LAYOUT,
DRAWER_SIZE,
MODAL_DURATION,
- type ColorScheme,
- type CssPosition,
+ VS_OVERLAY_OPEN,
+ VS_OVERLAY_CLOSE,
type SizeProp,
+ Focusable,
} from '@/declaration';
import { utils } from '@/utils';
import { VsFocusTrap } from '@/nodes';
+import { getOverlayProps } from '@/models';
import type { VsDrawerStyleSet } from './types';
@@ -62,60 +64,53 @@ export default defineComponent({
name,
components: { VsFocusTrap },
props: {
- colorScheme: { type: String as PropType },
- styleSet: { type: [String, Object] as PropType },
- closeOnDimmedClick: { type: Boolean, default: true },
- closeOnEsc: { type: Boolean, default: true },
+ ...getOverlayProps(),
dimmed: { type: Boolean, default: false },
- focusLock: { type: Boolean, default: true },
- hideScroll: { type: Boolean, default: false },
- initialFocusRef: {
- type: Object as PropType,
- default: null,
- },
+ escClose: { type: Boolean, default: false },
+ fixed: { type: Boolean, default: false },
open: { type: Boolean, default: false },
placement: {
type: String as PropType>,
default: 'left',
validator: (val: Placement) => utils.props.checkPropExist(name, 'placement', PLACEMENTS, val),
},
- position: { type: String as PropType, default: 'absolute' },
size: { type: [String, Number] as PropType, default: 'sm' },
- useLayoutPadding: { type: Boolean, default: false },
+ useLayoutPadding: { type: Boolean, default: true },
// v-model
modelValue: { type: Boolean, default: false },
},
- emits: ['update:modelValue'],
+ emits: ['update:modelValue', 'open', 'close'],
setup(props, { emit }) {
const {
colorScheme,
styleSet,
modelValue,
- closeOnDimmedClick,
- closeOnEsc,
+ id,
+ callbacks,
+ dimClose,
dimmed,
+ fixed,
open,
placement,
- position,
size,
useLayoutPadding,
+ escClose,
} = toRefs(props);
const { colorSchemeClass } = useColorScheme(name, colorScheme);
const { computedStyleSet: drawerStyleSet } = useStyleSet(name, styleSet);
- const id = utils.string.createID();
- const isOpen = ref(open.value || modelValue.value);
- const focusTrapRef = ref(null);
+ const focusTrapRef: Ref = ref(null);
const positionStyle = computed(() => {
- const style: { [key: string]: string | number } = { position: position.value };
+ const position = fixed.value ? 'fixed' : 'absolute';
+ const style: { [key: string]: string | number } = { position };
- if (position.value === 'absolute') {
- style['--vs-drawer-zIndex'] = LAYOUT_Z_INDEX;
- } else if (position.value === 'fixed') {
- style['--vs-drawer-zIndex'] = APP_LAYOUT_Z_INDEX;
+ if (position === 'absolute') {
+ style['--vs-drawer-zIndex'] = LAYOUT_Z_INDEX - 5;
+ } else if (position === 'fixed') {
+ style['--vs-drawer-zIndex'] = APP_LAYOUT_Z_INDEX - 5;
}
return style;
@@ -143,35 +138,20 @@ export default defineComponent({
};
});
- watch(modelValue, (val) => {
- isOpen.value = val;
+ const initialOpen = open.value || modelValue.value;
+ const needScrollLock = computed(() => dimmed.value && fixed.value);
+ const computedCallbacks = computed(() => {
+ return {
+ ...callbacks.value,
+ [VS_OVERLAY_OPEN]: () => {
+ focusTrapRef.value?.focus();
+ },
+ [VS_OVERLAY_CLOSE]: () => {
+ focusTrapRef.value?.blur();
+ },
+ };
});
-
- const bodyScroll = useBodyScroll();
- watch(
- isOpen,
- (val) => {
- const needScrollLock = dimmed.value && position.value === 'fixed';
- if (val) {
- if (needScrollLock) {
- bodyScroll.lock();
- }
- nextTick(() => {
- (focusTrapRef.value as any)?.focus();
- });
- } else {
- if (needScrollLock) {
- bodyScroll.unlock();
- }
- nextTick(() => {
- (focusTrapRef.value as any)?.blur();
- });
- }
-
- emit('update:modelValue', val);
- },
- { immediate: true },
- );
+ const { isOpen, close } = useOverlay(id, initialOpen, needScrollLock, computedCallbacks, escClose);
// only for vs-layout children
const { getDefaultLayoutProvide } = useLayout();
@@ -214,13 +194,18 @@ export default defineComponent({
});
function onClickDimmed() {
- if (closeOnDimmedClick.value) {
- isOpen.value = false;
+ if (dimClose.value) {
+ close();
}
}
- useEscClose(id, closeOnEsc, isOpen, () => {
- isOpen.value = false;
+ watch(modelValue, (o) => {
+ isOpen.value = o;
+ });
+
+ watch(isOpen, (o) => {
+ emit('update:modelValue', o);
+ emit(o ? 'open' : 'close');
});
return {
diff --git a/packages/vlossom/src/components/vs-drawer/__tests__/vs-drawer.test.ts b/packages/vlossom/src/components/vs-drawer/__tests__/vs-drawer.test.ts
index 9a53cbfb2..99e271558 100644
--- a/packages/vlossom/src/components/vs-drawer/__tests__/vs-drawer.test.ts
+++ b/packages/vlossom/src/components/vs-drawer/__tests__/vs-drawer.test.ts
@@ -109,7 +109,7 @@ describe('vs-drawer', () => {
props: {
modelValue: true,
dimmed: true,
- closeOnDimmedClick: true,
+ dimClose: true,
},
});
@@ -121,13 +121,13 @@ describe('vs-drawer', () => {
expect(wrapper.vm.isOpen).toBe(false);
});
- it('close-on-dimmed-click prop을 false로 전달하면 dimmed 영역을 클릭해도 drawer가 닫히지 않는다', async () => {
+ it('dimClose prop을 false로 전달하면 dimmed 영역을 클릭해도 drawer가 닫히지 않는다', async () => {
// given
const wrapper = mount(VsDrawer, {
props: {
modelValue: true,
dimmed: true,
- closeOnDimmedClick: false,
+ dimClose: false,
},
});
@@ -139,41 +139,6 @@ describe('vs-drawer', () => {
});
});
- describe('close on esc key', () => {
- it('esc key를 누르면 drawer가 닫힌다', async () => {
- // given
- const wrapper = mount(VsDrawer, {
- props: {
- modelValue: true,
- },
- attachTo: document.body,
- });
-
- // when
- await wrapper.trigger('keydown.Escape');
-
- // then
- expect(wrapper.vm.isOpen).toBe(false);
- });
-
- it('close-on-esc-key prop을 false로 전달하면 esc key를 눌러도 drawer가 닫히지 않는다', async () => {
- // given
- const wrapper = mount(VsDrawer, {
- props: {
- modelValue: true,
- closeOnEsc: false,
- },
- attachTo: document.body,
- });
-
- // when
- await wrapper.trigger('keydown.Esc');
-
- // then
- expect(wrapper.vm.isOpen).toBe(true);
- });
- });
-
describe('focus trap', () => {
it('focus trap이 적용되어 있다', async () => {
// given
diff --git a/packages/vlossom/src/components/vs-modal/VsModal.vue b/packages/vlossom/src/components/vs-modal/VsModal.vue
index 41afb2207..d7f55bc0c 100644
--- a/packages/vlossom/src/components/vs-modal/VsModal.vue
+++ b/packages/vlossom/src/components/vs-modal/VsModal.vue
@@ -1,162 +1,78 @@
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
diff --git a/packages/vlossom/src/components/vs-modal/__tests__/vs-modal.test.ts b/packages/vlossom/src/components/vs-modal/__tests__/vs-modal.test.ts
deleted file mode 100644
index a04e04252..000000000
--- a/packages/vlossom/src/components/vs-modal/__tests__/vs-modal.test.ts
+++ /dev/null
@@ -1,342 +0,0 @@
-import { describe, expect, it } from 'vitest';
-import { mount } from '@vue/test-utils';
-import VsModal from './../VsModal.vue';
-
-describe('vs-modal', () => {
- describe('v-model', () => {
- it('modelValue가 false이면 modal이 열리지 않는다', async () => {
- // given
- const wrapper = mount(VsModal, {
- props: {
- modelValue: false,
- },
- global: {
- stubs: ['Teleport'],
- },
- });
-
- // then
- expect(wrapper.find('.vs-modal').exists()).toBe(false);
- expect(wrapper.vm.isOpen).toBe(false);
- });
-
- it('modelValue가 true이면 modal이 열린다', async () => {
- // given
- const wrapper = mount(VsModal, {
- props: {
- modelValue: false,
- },
- global: {
- stubs: ['Teleport'],
- },
- });
-
- expect(wrapper.find('.vs-modal').exists()).toBe(false);
-
- // when
- await wrapper.setProps({ modelValue: true });
-
- // then
- expect(wrapper.find('.vs-modal').exists()).toBe(true);
- expect(wrapper.vm.isOpen).toBe(true);
- });
- });
-
- describe('slot', () => {
- it('default slot을 전달하면 slot 컨텐츠가 렌더링 된다', () => {
- // given
- const wrapper = mount(VsModal, {
- props: {
- modelValue: true,
- },
- slots: {
- default: 'Content',
- },
- global: {
- stubs: ['Teleport'],
- },
- });
-
- // then
- expect(wrapper.html()).toContain('Content');
- });
-
- it('header slot을 전달하면 header 영역에 slot 컨텐츠가 렌더링 된다', () => {
- // given
- const wrapper = mount(VsModal, {
- props: {
- modelValue: true,
- },
- slots: {
- header: 'Header',
- },
- global: {
- stubs: ['Teleport'],
- },
- });
-
- // then
- expect(wrapper.find('.vs-modal-header').exists()).toBe(true);
- expect(wrapper.find('.vs-modal-header').text()).toBe('Header');
- });
-
- it('footer slot을 전달하면 footer 영역에 slot 컨텐츠가 렌더링 된다', () => {
- // given
- const wrapper = mount(VsModal, {
- props: {
- modelValue: true,
- },
- slots: {
- footer: 'Footer',
- },
- global: {
- stubs: ['Teleport'],
- },
- });
-
- // then
- expect(wrapper.find('.vs-modal-footer').exists()).toBe(true);
- expect(wrapper.find('.vs-modal-footer').text()).toBe('Footer');
- });
- });
-
- describe('aria attributes', () => {
- it('header slot이 있는 경우 modal의 aria-lablledby 속성 값이 의 id 가 된다', () => {
- // given
- const wrapper = mount(VsModal, {
- props: {
- modelValue: true,
- },
- slots: {
- header: 'Header',
- },
- global: {
- stubs: ['Teleport'],
- },
- });
-
- // then
- const modal = wrapper.find('.vs-modal-wrap');
- expect(modal.attributes('aria-labelledby')).toBe(wrapper.find('header').attributes('id'));
- expect(modal.attributes('aria-label')).toBe(undefined);
- });
-
- it('header slot이 없는 경우 modal의 aria-lablledby 속성이 없고 대신 aria-label 속성 값이 Modal이 된다', () => {
- // given
- const wrapper = mount(VsModal, {
- props: {
- modelValue: true,
- },
- global: {
- stubs: ['Teleport'],
- },
- });
-
- // then
- const modal = wrapper.find('.vs-modal-wrap');
- expect(modal.attributes('aria-labelledby')).toBe(undefined);
- expect(modal.attributes('aria-label')).toBe('Modal');
- });
-
- it('modal body 요소의 id 값이 modal의 aria-describedby 속성 값이 된다', () => {
- // given
- const wrapper = mount(VsModal, {
- props: {
- modelValue: true,
- },
- slots: {
- default: 'Content',
- },
- global: {
- stubs: ['Teleport'],
- },
- });
-
- // then
- const modal = wrapper.find('.vs-modal-wrap');
- expect(modal.attributes('aria-describedby')).toBe(wrapper.find('.vs-modal-body').attributes('id'));
- });
-
- it('aria-modal 속성이 있다', async () => {
- // given
- const wrapper = mount(VsModal, {
- props: {
- modelValue: true,
- },
- global: {
- stubs: ['Teleport'],
- },
- });
-
- // then
- const modal = wrapper.find('.vs-modal-wrap');
- expect(modal.attributes('aria-modal')).toBe('true');
- });
- });
-
- describe('has container', () => {
- it('has container prop을 전달하면 teleport가 비활성화된다', () => {
- // given
- // stub teleport X
- const wrapper = mount(VsModal, {
- props: {
- modelValue: true,
- hasContainer: true,
- },
- });
-
- // then
- expect(wrapper.find('div').exists()).toBe(true);
- });
- });
-
- describe('dimmed', () => {
- it('dimmed 영역이 존재한다', () => {
- // given
- const wrapper = mount(VsModal, {
- props: {
- modelValue: true,
- },
- global: {
- stubs: ['Teleport'],
- },
- });
-
- // then
- expect(wrapper.find('.vs-modal-dimmed').exists()).toBe(true);
- });
-
- it('dimmed 영역 클릭 시 modal이 닫힌다', async () => {
- // given
- const wrapper = mount(VsModal, {
- props: {
- modelValue: true,
- },
- global: {
- stubs: ['Teleport'],
- },
- });
-
- // when
- await wrapper.find('.vs-modal-dimmed').trigger('click');
-
- // then
- expect(wrapper.vm.isOpen).toBe(false);
- });
-
- it('close-on-dimmed-click prop을 false로 전달하면 dimmed 영역을 클릭해도 modal이 닫히지 않는다', async () => {
- // given
- const wrapper = mount(VsModal, {
- props: {
- modelValue: true,
- closeOnDimmedClick: false,
- },
- global: {
- stubs: ['Teleport'],
- },
- });
-
- // when
- await wrapper.find('.vs-modal-dimmed').trigger('click');
-
- // then
- expect(wrapper.vm.isOpen).toBe(true);
- });
- });
-
- describe('close on esc key', () => {
- it('esc key를 누르면 modal이 닫힌다', async () => {
- // given
- const wrapper = mount(VsModal, {
- props: {
- modelValue: true,
- },
- global: {
- stubs: ['Teleport'],
- },
- attachTo: document.body,
- });
-
- // when
- await wrapper.trigger('keydown.Escape');
-
- // then
- expect(wrapper.vm.isOpen).toBe(false);
- });
-
- it('close-on-esc prop을 false로 전달하면 esc key를 눌러도 modal이 닫히지 않는다', async () => {
- // given
- const wrapper = mount(VsModal, {
- props: {
- modelValue: true,
- closeOnEsc: false,
- },
- global: {
- stubs: ['Teleport'],
- },
- attachTo: document.body,
- });
-
- // when
- await wrapper.trigger('keydown.Esc');
-
- // then
- expect(wrapper.vm.isOpen).toBe(true);
- });
- });
-
- describe('focus trap', () => {
- it('focus trap이 적용되어 있다', async () => {
- // given
- const wrapper = mount(VsModal, {
- props: {
- modelValue: true,
- },
- global: {
- stubs: ['Teleport'],
- },
- });
-
- // then
- expect(wrapper.findComponent({ name: 'VsFocusTrap' }).exists()).toBe(true);
- });
- });
-
- describe('size', () => {
- describe('size prop이 style set 보다 우선된다', () => {
- it('width', () => {
- // given
- const wrapper = mount(VsModal, {
- props: {
- modelValue: true,
- size: '270px',
- styleSet: { width: '320px' },
- },
- global: {
- stubs: ['Teleport'],
- },
- });
-
- // then
- expect(wrapper.find('.vs-modal').attributes('style')).toContain('--vs-modal-width: 270px;');
- });
-
- it('height', () => {
- // given
- const wrapper = mount(VsModal, {
- props: {
- modelValue: true,
- size: '270px',
- styleSet: { height: '320px' },
- },
- global: {
- stubs: ['Teleport'],
- },
- });
-
- // then
- expect(wrapper.find('.vs-modal').attributes('style')).toContain('--vs-modal-height: 270px;');
- });
- });
- });
-});
diff --git a/packages/vlossom/src/components/vs-modal/stories/VsModal.chromatic.stories.ts b/packages/vlossom/src/components/vs-modal/stories/VsModal.chromatic.stories.ts
index 007121da5..3b1638b8f 100644
--- a/packages/vlossom/src/components/vs-modal/stories/VsModal.chromatic.stories.ts
+++ b/packages/vlossom/src/components/vs-modal/stories/VsModal.chromatic.stories.ts
@@ -54,9 +54,6 @@ const meta: Meta = {
size,
colorScheme,
},
- args: {
- hasContainer: true,
- },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
diff --git a/packages/vlossom/src/components/vs-modal/stories/VsModal.stories.ts b/packages/vlossom/src/components/vs-modal/stories/VsModal.stories.ts
index a6c487fff..0a968d217 100644
--- a/packages/vlossom/src/components/vs-modal/stories/VsModal.stories.ts
+++ b/packages/vlossom/src/components/vs-modal/stories/VsModal.stories.ts
@@ -212,9 +212,6 @@ export const HasContainer: Story = {
`,
}),
- args: {
- hasContainer: true,
- },
};
export const HideScroll: Story = {
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 336e3f930..401e1756e 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
@@ -847,6 +847,7 @@ describe('vs-select', () => {
const wrapper = mount(VsSelect, {
props: {
ariaLabel: 'aria-label',
+ options: ['A', 'B', 'C'],
},
});
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 430ad985e..03a8ab2e8 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
@@ -1,12 +1,12 @@
-import { ref, watch, onBeforeUnmount, type Ref } from 'vue';
-import { useOverlay, usePositioning } from '@/composables';
+import { ref, watch, onBeforeMount, onBeforeUnmount, type Ref } from 'vue';
+import { useOverlayDom, usePositioning } from '@/composables';
export function useToggleOptions(id: Ref, disabled: Ref, readonly: Ref) {
- useOverlay();
-
const isOpen = ref(false);
const isClosing = ref(false);
+ const { appendOverlayDom } = useOverlayDom();
+
function toggleOptions() {
if (disabled.value || readonly.value) {
return;
@@ -68,6 +68,8 @@ export function useToggleOptions(id: Ref, disabled: Ref, readon
}
});
+ onBeforeMount(appendOverlayDom);
+
onBeforeUnmount(() => {
document.removeEventListener('click', onOutsideClick, true);
});
diff --git a/packages/vlossom/src/components/vs-tooltip/VsTooltip.vue b/packages/vlossom/src/components/vs-tooltip/VsTooltip.vue
index ebc6e6f2b..8c635fc7b 100644
--- a/packages/vlossom/src/components/vs-tooltip/VsTooltip.vue
+++ b/packages/vlossom/src/components/vs-tooltip/VsTooltip.vue
@@ -30,10 +30,21 @@
+
+
diff --git a/packages/vlossom/src/nodes/vs-confirm/types.ts b/packages/vlossom/src/nodes/vs-confirm/types.ts
new file mode 100644
index 000000000..1a21b2338
--- /dev/null
+++ b/packages/vlossom/src/nodes/vs-confirm/types.ts
@@ -0,0 +1,27 @@
+import type { ModalOptions } from '@/declaration';
+
+export interface VsCofirmationStyleSet {
+ backgroundColor?: string;
+ borderRadius?: string;
+ boxShadow?: string;
+ fontColor?: string;
+ fontSize?: string;
+ fontWeight?: string | number;
+ height?: string;
+ width?: string;
+ padding?: string;
+ zIndex?: string | number;
+ // TODO: add button style object (224926)
+}
+
+export interface ConfirmOptions extends Omit, 'component'> {
+ okText?: string;
+ cancelText?: string;
+}
+
+export interface PromptOptions extends ConfirmOptions {
+ inputType?: string;
+ inputLabel?: string;
+ inputMaxLength?: number;
+ inputPlaceholder?: string;
+}
diff --git a/packages/vlossom/src/nodes/vs-content-renderer/VsContentRenderer.vue b/packages/vlossom/src/nodes/vs-content-renderer/VsContentRenderer.vue
new file mode 100644
index 000000000..bbb0b922e
--- /dev/null
+++ b/packages/vlossom/src/nodes/vs-content-renderer/VsContentRenderer.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
diff --git a/packages/vlossom/src/components/vs-modal/VsModal.scss b/packages/vlossom/src/nodes/vs-modal-node/VsModalNode.scss
similarity index 82%
rename from packages/vlossom/src/components/vs-modal/VsModal.scss
rename to packages/vlossom/src/nodes/vs-modal-node/VsModalNode.scss
index c4e42fd1b..96227e1ae 100644
--- a/packages/vlossom/src/components/vs-modal/VsModal.scss
+++ b/packages/vlossom/src/nodes/vs-modal-node/VsModalNode.scss
@@ -1,7 +1,7 @@
@use '@/styles/variables' as *;
@use '@/styles/mixin' as *;
-.vs-modal {
+.vs-modal-node {
position: fixed;
top: 0;
left: 0;
@@ -56,9 +56,11 @@
.vs-modal-contents {
position: relative;
display: flex;
+ flex-direction: column;
+ overflow: auto;
width: 100%;
height: 100%;
- padding: var(--vs-modal-padding, 2rem);
+ padding: var(--vs-modal-padding, 1.8rem 2.4rem);
color: var(--vs-modal-fontColor, var(--vs-font-color));
font-size: var(--vs-modal-fontSize, var(--vs-font-size));
font-weight: var(--vs-modal-fontWeight, 400);
@@ -76,7 +78,6 @@
.vs-modal-body {
position: relative;
flex: auto;
- overflow: auto;
&.hide-scroll {
@include hide-scroll;
@@ -105,25 +106,25 @@
}
@container (min-width: 640px) {
- .vs-modal .vs-modal-wrap .vs-modal-contents {
- padding: var(--vs-modal-padding, 2.8rem);
+ .vs-modal-node .vs-modal-wrap .vs-modal-contents {
+ padding: var(--vs-modal-padding, 2.2rem 2.8rem);
}
}
@container (min-width: 768px) {
- .vs-modal .vs-modal-wrap .vs-modal-contents {
- padding: var(--vs-modal-padding, 3.6rem);
+ .vs-modal-node .vs-modal-wrap .vs-modal-contents {
+ padding: var(--vs-modal-padding, 2.6rem 3.2rem);
}
}
@container (min-width: 1024px) {
- .vs-modal .vs-modal-wrap .vs-modal-contents {
- padding: var(--vs-modal-padding, 4.8rem);
+ .vs-modal-node .vs-modal-wrap .vs-modal-contents {
+ padding: var(--vs-modal-padding, 3rem 3.6rem);
}
}
@container (min-width: 1280px) {
- .vs-modal .vs-modal-wrap .vs-modal-contents {
- padding: var(--vs-modal-padding, 6rem);
+ .vs-modal-node .vs-modal-wrap .vs-modal-contents {
+ padding: var(--vs-modal-padding, 3.6rem 4.8rem);
}
}
diff --git a/packages/vlossom/src/nodes/vs-modal-node/VsModalNode.vue b/packages/vlossom/src/nodes/vs-modal-node/VsModalNode.vue
new file mode 100644
index 000000000..207b7657e
--- /dev/null
+++ b/packages/vlossom/src/nodes/vs-modal-node/VsModalNode.vue
@@ -0,0 +1,148 @@
+
+
+
+
+
+
+
diff --git a/packages/vlossom/src/nodes/vs-modal-node/VsModalView.vue b/packages/vlossom/src/nodes/vs-modal-node/VsModalView.vue
new file mode 100644
index 000000000..79c36b1a8
--- /dev/null
+++ b/packages/vlossom/src/nodes/vs-modal-node/VsModalView.vue
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/vlossom/src/components/vs-modal/types.ts b/packages/vlossom/src/nodes/vs-modal-node/types.ts
similarity index 62%
rename from packages/vlossom/src/components/vs-modal/types.ts
rename to packages/vlossom/src/nodes/vs-modal-node/types.ts
index d5c7b01a8..fb43d8b60 100644
--- a/packages/vlossom/src/components/vs-modal/types.ts
+++ b/packages/vlossom/src/nodes/vs-modal-node/types.ts
@@ -1,4 +1,6 @@
-export interface VsModalStyleSet {
+import { ModalOptions } from '@/declaration';
+
+export interface VsModalNodeStyleSet {
backgroundColor?: string;
borderRadius?: string;
boxShadow?: string;
@@ -10,3 +12,5 @@ export interface VsModalStyleSet {
padding?: string;
zIndex?: string | number;
}
+
+export type VsModalOptions = ModalOptions;
diff --git a/packages/vlossom/src/plugins/confirm-plugin/__tests__/confirm-plugin.test.ts b/packages/vlossom/src/plugins/confirm-plugin/__tests__/confirm-plugin.test.ts
deleted file mode 100644
index 7e3192e3a..000000000
--- a/packages/vlossom/src/plugins/confirm-plugin/__tests__/confirm-plugin.test.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import { describe, vi, it, expect } from 'vitest';
-import { store } from '@/stores';
-import * as confirmPlugin from '@/plugins/confirm-plugin';
-
-const { confirmPlugin: confirm } = confirmPlugin;
-
-describe('confirm-plugin', () => {
- describe('confirmPlugin', () => {
- it('open 메서드가 Promise.resolve(true)를 리턴할 수 있다', async () => {
- // given
- store.confirm.setResolve(vi.fn());
- const openPromise = confirm.open('Are you sure?');
-
- // when
- store.confirm.executeResolve(true);
-
- // then
- expect(await openPromise).toBe(true);
- });
-
- it('open 메서드가 Promise.resolve(false)를 리턴할 수 있다', async () => {
- // given
- store.confirm.setResolve(vi.fn());
-
- // when
- const openPromise = confirm.open('Are you sure?');
- store.confirm.executeResolve(false);
-
- // then
- expect(await openPromise).toBe(false);
- });
-
- it('prompt 메서드로 윈도우 프롬프트 입력값과 confirmText 를 비교할 수 있다', async () => {
- // given
- const confirmText = 'YES';
- const promptText = 'YES';
- const promptSpy = vi.spyOn(window, 'prompt').mockReturnValue(promptText);
-
- // when
- const promptPromise = confirm.prompt('type "ABC"', confirmText);
-
- // then
- expect(promptSpy).toBeCalledWith('type "ABC"');
- expect(await promptPromise).toBe(true);
- });
- });
-});
diff --git a/packages/vlossom/src/plugins/confirm-plugin/confirm-plugin.ts b/packages/vlossom/src/plugins/confirm-plugin/confirm-plugin.ts
index 0615eea81..6c65f3c96 100644
--- a/packages/vlossom/src/plugins/confirm-plugin/confirm-plugin.ts
+++ b/packages/vlossom/src/plugins/confirm-plugin/confirm-plugin.ts
@@ -1,33 +1,55 @@
-import { render, h } from 'vue';
-import { store } from '@/stores';
-import { utils } from '@/utils';
-import VsConfirm from '@/components/vs-confirm/VsConfirm.vue';
-
-import type { ConfirmOptions, ConfirmPlugin } from './types';
-
-function renderConfirm(text: string, confirmOptions: ConfirmOptions) {
- const body = document?.body;
- if (!body) {
- utils.log.error('vs-confirm', 'body not found');
- return;
- }
-
- const confirmView = h(VsConfirm, { text, ...confirmOptions });
- const confirmViewRoot = document.createElement('div');
- render(confirmView, confirmViewRoot);
-}
+import { Component, h } from 'vue';
+import { ConfirmPlugin } from './types';
+import { VsConfirmation, type ConfirmOptions } from '@/nodes';
+import { VS_CONFIRM_CANCEL, VS_CONFIRM_OK } from '@/declaration';
+import { modalPlugin } from '@/plugins';
+import { useContentRenderer } from '@/composables';
export const confirmPlugin: ConfirmPlugin = {
- open: (text: string, confirmOptions: ConfirmOptions = {}): Promise => {
- renderConfirm(text, confirmOptions);
-
+ open: (content: string | Component, confirmOptions: ConfirmOptions = {}): Promise => {
return new Promise((resolve) => {
- store.confirm.setResolve(resolve);
+ const { okText, cancelText, size = 'xs', callbacks = {} } = confirmOptions;
+ const { getRenderedContent } = useContentRenderer();
+ const modalId = modalPlugin.open({
+ ...confirmOptions,
+ component: h(
+ VsConfirmation,
+ { okText, cancelText },
+ {
+ default: () => {
+ if (typeof content === 'string') {
+ return getRenderedContent(content);
+ }
+
+ return h(content);
+ },
+ },
+ ),
+ size,
+ callbacks: {
+ ...callbacks,
+ [VS_CONFIRM_OK]: () => {
+ resolve(true);
+ modalPlugin.closeWithId(modalId);
+ },
+ [VS_CONFIRM_CANCEL]: () => {
+ resolve(false);
+ modalPlugin.closeWithId(modalId);
+ },
+ 'key-Enter': () => {
+ resolve(true);
+ modalPlugin.closeWithId(modalId);
+ },
+ 'key-Escape': () => {
+ resolve(false);
+ modalPlugin.closeWithId(modalId);
+ },
+ },
+ });
});
},
- prompt(text: string, confirmText: string) {
- const promptText = window.prompt(text);
-
+ prompt(content: string, confirmText: string /*promptOptions: PromptOptions = {}*/) {
+ const promptText = window.prompt(content);
return new Promise((resolve) => {
resolve(promptText === confirmText);
});
diff --git a/packages/vlossom/src/plugins/confirm-plugin/types.ts b/packages/vlossom/src/plugins/confirm-plugin/types.ts
index 6d6e89230..cb8acaaf6 100644
--- a/packages/vlossom/src/plugins/confirm-plugin/types.ts
+++ b/packages/vlossom/src/plugins/confirm-plugin/types.ts
@@ -1,23 +1,7 @@
-import type { ColorScheme, SizeProp } from '@/declaration';
-import { type VsModalStyleSet } from '@/components/vs-modal/types';
-
-export interface ConfirmOptions {
- colorScheme?: ColorScheme;
- styleSet?: string | VsModalStyleSet;
- closeOnDimmedClick?: boolean;
- closeOnEsc?: boolean;
- dimmed?: boolean;
- focusLock?: boolean;
- hasContainer?: boolean;
- hideScroll?: boolean;
- initialFocusRef?: HTMLElement | null;
- size?: SizeProp | { width?: SizeProp; height?: SizeProp };
-
- okText?: string;
- cancelText?: string;
-}
+import { Component } from 'vue';
+import type { ConfirmOptions, PromptOptions } from '@/nodes';
export interface ConfirmPlugin {
- open: (text: string, confirmOptions?: ConfirmOptions) => Promise;
- prompt: (text: string, confirmText: string) => Promise;
+ open: (content: string | Component, confirmOptions?: ConfirmOptions) => Promise;
+ prompt: (content: string, confirmText: string, promptOptions?: PromptOptions) => Promise;
}
diff --git a/packages/vlossom/src/plugins/index.ts b/packages/vlossom/src/plugins/index.ts
index 09dd461ef..f1172f329 100644
--- a/packages/vlossom/src/plugins/index.ts
+++ b/packages/vlossom/src/plugins/index.ts
@@ -1,2 +1,3 @@
-export * from './toast-plugin';
export * from './confirm-plugin';
+export * from './modal-plugin';
+export * from './toast-plugin';
diff --git a/packages/vlossom/src/plugins/modal-plugin/index.ts b/packages/vlossom/src/plugins/modal-plugin/index.ts
new file mode 100644
index 000000000..7a3c60693
--- /dev/null
+++ b/packages/vlossom/src/plugins/modal-plugin/index.ts
@@ -0,0 +1,2 @@
+export * from './modal-plugin';
+export * from './types';
diff --git a/packages/vlossom/src/plugins/modal-plugin/modal-plugin.ts b/packages/vlossom/src/plugins/modal-plugin/modal-plugin.ts
new file mode 100644
index 000000000..905392ba5
--- /dev/null
+++ b/packages/vlossom/src/plugins/modal-plugin/modal-plugin.ts
@@ -0,0 +1,41 @@
+import { h, render } from 'vue';
+import { ModalPlugin } from './types';
+import { utils } from '@/utils';
+import { store } from '@/stores';
+import { VsModalView, VsModalOptions } from '@/nodes';
+
+export const modalPlugin: ModalPlugin = {
+ open(options: VsModalOptions) {
+ const { id = utils.string.createID(), container = 'body' } = options;
+ const containerElement = document.querySelector(container);
+ if (!containerElement) {
+ utils.log.error('vs-modal', `container not found: ${container}`);
+ return '';
+ }
+
+ const modalView = h(VsModalView, { container });
+ render(modalView, containerElement);
+
+ store.modal.push({ ...options, id });
+ return id;
+ },
+ emit(eventName: string, ...args: any[]) {
+ const lastOverlayId = store.overlay.getLastOverlayId();
+ return store.overlay.run(lastOverlayId, eventName, ...args);
+ },
+ emitWithId(id: string, eventName: string, ...args: any[]) {
+ return store.overlay.run(id, eventName, ...args);
+ },
+ close(...args: any[]) {
+ store.modal.pop();
+ return store.overlay.pop(...args);
+ },
+ closeWithId(id: string, ...args: any[]) {
+ store.modal.remove(id);
+ return store.overlay.remove(id, ...args);
+ },
+ clear(...args: any[]) {
+ store.modal.clear();
+ store.overlay.clear(...args);
+ },
+};
diff --git a/packages/vlossom/src/plugins/modal-plugin/types.ts b/packages/vlossom/src/plugins/modal-plugin/types.ts
new file mode 100644
index 000000000..e4fa6ec28
--- /dev/null
+++ b/packages/vlossom/src/plugins/modal-plugin/types.ts
@@ -0,0 +1,10 @@
+import { VsModalOptions } from '@/nodes';
+
+export interface ModalPlugin {
+ open(options: VsModalOptions): string;
+ emit(event: string, ...args: any[]): void | Promise;
+ emitWithId(id: string, event: string, ...args: any[]): void | Promise;
+ close(...args: any[]): void | Promise;
+ closeWithId(id: string, ...args: any[]): void | Promise;
+ clear(...args: any[]): void | Promise;
+}
diff --git a/packages/vlossom/src/stores/__tests__/confirm-store.test.ts b/packages/vlossom/src/stores/__tests__/confirm-store.test.ts
deleted file mode 100644
index 7a5fa03e7..000000000
--- a/packages/vlossom/src/stores/__tests__/confirm-store.test.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import { describe, expect, it, vi } from 'vitest';
-import { ConfirmStore } from './../confirm-store';
-
-describe('confirm store', () => {
- it('confirm store의 상태를 가져올 수 있다', () => {
- // given
- const store = new ConfirmStore();
-
- // when
- const result = store.getState();
-
- // then
- expect(result).toEqual({
- resolve: null,
- });
- });
-
- describe('setResolve', () => {
- it('resolve를 설정할 수 있다', () => {
- // given
- const store = new ConfirmStore();
- const resolve = vi.fn();
-
- // when
- store.setResolve(resolve);
-
- // then
- expect(store.getState().resolve).not.toBeNull();
- });
- });
-
- describe('executeResolve', () => {
- it('resolve를 이행할 수 있다', async () => {
- // given
- const store = new ConfirmStore();
- const resolve = vi.fn();
- store.setResolve(resolve);
-
- // when
- store.executeResolve(true);
-
- // then
- expect(resolve).toBeCalledWith(true);
- });
- });
-});
diff --git a/packages/vlossom/src/stores/__tests__/esc-stack-store.test.ts b/packages/vlossom/src/stores/__tests__/esc-stack-store.test.ts
deleted file mode 100644
index d5b514cf3..000000000
--- a/packages/vlossom/src/stores/__tests__/esc-stack-store.test.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { describe, it, expect } from 'vitest';
-import { EscStackStore } from './../esc-stack-store';
-
-describe('esc stack store', () => {
- it('stack에서 가장 위에 있는 esc stack의 id를 가져올 수 있다', () => {
- // given
- const store = new EscStackStore();
- store.push('dialog1');
- store.push('dialog2');
-
- // when
- const result = store.getTopId();
- // then
- expect(result).toEqual('dialog2');
-
- // when
- store.pop();
-
- // then
- expect(store.getTopId()).toEqual('dialog1');
- });
-});
diff --git a/packages/vlossom/src/stores/__tests__/store.test.ts b/packages/vlossom/src/stores/__tests__/store.test.ts
index be35d180d..5d47c26c3 100644
--- a/packages/vlossom/src/stores/__tests__/store.test.ts
+++ b/packages/vlossom/src/stores/__tests__/store.test.ts
@@ -1,8 +1,8 @@
import { describe, it, expect } from 'vitest';
import { VsStore } from './../index';
-import { ConfirmStore } from './../confirm-store';
-import { EscStackStore } from './../esc-stack-store';
+import { ModalStore } from '../modal-store';
import { OptionStore } from './../option-store';
+import { OverlayStore } from '../overlay-store';
import { ToastStore } from './../toast-store';
describe('Vlossom store', () => {
@@ -12,9 +12,9 @@ describe('Vlossom store', () => {
// then
expect(Object.keys(store).length).toBe(4);
- expect(store.confirm).toBeInstanceOf(ConfirmStore);
- expect(store.escStack).toBeInstanceOf(EscStackStore);
+ expect(store.modal).toBeInstanceOf(ModalStore);
expect(store.option).toBeInstanceOf(OptionStore);
+ expect(store.overlay).toBeInstanceOf(OverlayStore);
expect(store.toast).toBeInstanceOf(ToastStore);
});
});
diff --git a/packages/vlossom/src/stores/confirm-store.ts b/packages/vlossom/src/stores/confirm-store.ts
deleted file mode 100644
index e0725e24c..000000000
--- a/packages/vlossom/src/stores/confirm-store.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { reactive } from 'vue';
-
-interface ConfirmState {
- resolve: ((result: boolean) => void) | null;
-}
-
-export class ConfirmStore {
- private state: ConfirmState = reactive({
- resolve: null,
- });
-
- getState() {
- return this.state;
- }
-
- setResolve(resolve: (result: boolean) => void) {
- this.state.resolve = resolve;
- }
-
- executeResolve(result: boolean) {
- if (this.state.resolve) {
- this.state.resolve(result);
- }
- }
-}
diff --git a/packages/vlossom/src/stores/esc-stack-store.ts b/packages/vlossom/src/stores/esc-stack-store.ts
deleted file mode 100644
index caaaedd63..000000000
--- a/packages/vlossom/src/stores/esc-stack-store.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-export class EscStackStore {
- private idStack: string[] = [];
-
- push(id: string) {
- this.idStack.push(id);
- }
-
- pop() {
- return this.idStack.pop();
- }
-
- remove(id: string) {
- const index = this.idStack.indexOf(id);
- if (index >= 0) {
- this.idStack.splice(index, 1);
- }
- }
-
- getTopId() {
- return this.idStack[this.idStack.length - 1];
- }
-}
diff --git a/packages/vlossom/src/stores/index.ts b/packages/vlossom/src/stores/index.ts
index 78c2fa558..7410634d2 100644
--- a/packages/vlossom/src/stores/index.ts
+++ b/packages/vlossom/src/stores/index.ts
@@ -1,11 +1,11 @@
-import { ConfirmStore } from './confirm-store';
-import { EscStackStore } from './esc-stack-store';
+import { ModalStore } from './modal-store';
+import { OverlayStore } from './overlay-store';
import { OptionStore } from './option-store';
import { ToastStore } from './toast-store';
export class VsStore {
- private _confirm: ConfirmStore | null = null;
- private _escStack: EscStackStore | null = null;
+ private _modal: ModalStore | null = null;
+ private _overlay: OverlayStore | null = null;
private _option: OptionStore | null = null;
private _toast: ToastStore | null = null;
@@ -15,12 +15,6 @@ export class VsStore {
}
return this._option;
}
- public get escStack() {
- if (!this._escStack) {
- this._escStack = new EscStackStore();
- }
- return this._escStack;
- }
public get toast() {
if (!this._toast) {
@@ -29,11 +23,18 @@ export class VsStore {
return this._toast;
}
- public get confirm() {
- if (!this._confirm) {
- this._confirm = new ConfirmStore();
+ public get modal() {
+ if (!this._modal) {
+ this._modal = new ModalStore();
+ }
+ return this._modal;
+ }
+
+ public get overlay() {
+ if (!this._overlay) {
+ this._overlay = new OverlayStore();
}
- return this._confirm;
+ return this._overlay;
}
}
diff --git a/packages/vlossom/src/stores/modal-store.ts b/packages/vlossom/src/stores/modal-store.ts
new file mode 100644
index 000000000..a4cc57558
--- /dev/null
+++ b/packages/vlossom/src/stores/modal-store.ts
@@ -0,0 +1,36 @@
+import { computed, ComputedRef, Ref, ref } from 'vue';
+import { VsModalOptions } from '@/nodes';
+export class ModalStore {
+ public readonly modals: Ref = ref([]);
+ public readonly modalsByContainer: ComputedRef<{ [container: string]: VsModalOptions[] }> = computed(() => {
+ const modalsByContainer: { [container: string]: VsModalOptions[] } = {};
+ this.modals.value.forEach((modal) => {
+ const { container = 'body' } = modal;
+ if (!modalsByContainer[container]) {
+ modalsByContainer[container] = [];
+ }
+ modalsByContainer[container].push(modal);
+ });
+ return modalsByContainer;
+ });
+
+ push(options: VsModalOptions) {
+ if (!options.id) {
+ return;
+ }
+
+ this.modals.value.push(options);
+ }
+
+ pop() {
+ this.modals.value.pop();
+ }
+
+ remove(id: string) {
+ this.modals.value = this.modals.value.filter(({ id: modalId }) => modalId !== id);
+ }
+
+ clear() {
+ this.modals.value = [];
+ }
+}
diff --git a/packages/vlossom/src/stores/overlay-store.ts b/packages/vlossom/src/stores/overlay-store.ts
new file mode 100644
index 000000000..93ae9fde0
--- /dev/null
+++ b/packages/vlossom/src/stores/overlay-store.ts
@@ -0,0 +1,75 @@
+import { reactive, Ref } from 'vue';
+import { OverlayCallbacks, VS_OVERLAY_CLOSE, VS_OVERLAY_OPEN } from '@/declaration';
+
+export class OverlayStore {
+ // overlay tuple: [id, { [eventName: callback }]
+ public overlays: [string, Ref][] = reactive([]);
+
+ constructor() {
+ document.addEventListener('keydown', (event: KeyboardEvent) => {
+ if (this.overlays.length === 0) {
+ return;
+ }
+
+ const keyEventName = `key-${event.key}`;
+ const [lastOverlayId, callbacks] = this.overlays[this.overlays.length - 1];
+ if (!callbacks.value[keyEventName]) {
+ return;
+ }
+
+ // Prevent default action for registered key event (ex. enter, esc)
+ event.preventDefault();
+
+ this.run(lastOverlayId, keyEventName, event);
+ });
+ }
+
+ getLastOverlayId() {
+ return this.overlays.length > 0 ? this.overlays[this.overlays.length - 1][0] : '';
+ }
+
+ async run(id: string, eventName: string, ...args: any[]): Promise {
+ const index = this.overlays.findIndex(([overlayId]) => overlayId === id);
+ if (index === -1) {
+ return;
+ }
+ const [, callbacks] = this.overlays[index];
+ return await callbacks.value[eventName]?.(...args);
+ }
+
+ push(id: string, callbacks: Ref) {
+ this.overlays.push([id, callbacks]);
+ this.run(id, VS_OVERLAY_OPEN);
+ return this.run(id, 'open');
+ }
+
+ pop(...args: any[]) {
+ const popped = this.overlays.pop();
+ if (!popped) {
+ return;
+ }
+ const [poppedId] = popped;
+ this.run(poppedId, VS_OVERLAY_CLOSE, ...args);
+ return this.run(poppedId, 'close', ...args);
+ }
+
+ remove(id: string, ...args: any[]) {
+ const index = this.overlays.findIndex(([stackId]) => stackId === id);
+ if (index === -1) {
+ return;
+ }
+ const [popped] = this.overlays.splice(index, 1);
+ if (!popped) {
+ return;
+ }
+ const [poppedId] = popped;
+ this.run(poppedId, VS_OVERLAY_CLOSE, ...args);
+ return this.run(poppedId, 'close', ...args);
+ }
+
+ clear(...args: any[]) {
+ while (this.overlays.length > 0) {
+ this.pop(...args);
+ }
+ }
+}
diff --git a/packages/vlossom/src/storybook/args.ts b/packages/vlossom/src/storybook/args.ts
index a6ae1ba13..0bfb14028 100644
--- a/packages/vlossom/src/storybook/args.ts
+++ b/packages/vlossom/src/storybook/args.ts
@@ -1,4 +1,4 @@
-import { COLORS, CSS_POSITION, PLACEMENTS, SIZES } from '@/declaration';
+import { COLORS, PLACEMENTS, SIZES } from '@/declaration';
export const colorScheme = {
control: 'select' as any,
@@ -37,11 +37,6 @@ export function numberArray(length: number, multiSelect: boolean = false) {
};
}
-export const cssPosition = {
- control: 'select' as any,
- options: CSS_POSITION,
-};
-
export function getMetaArguments(componentProps: { [key: string]: any }, originalArgs: { [key: string]: any } = {}) {
const metaArgs: { [key: string]: any } = {};
Object.keys(componentProps).forEach((prop) => {
diff --git a/packages/vlossom/src/storybook/style-sets.ts b/packages/vlossom/src/storybook/style-sets.ts
index 797cbea08..3fc52193b 100644
--- a/packages/vlossom/src/storybook/style-sets.ts
+++ b/packages/vlossom/src/storybook/style-sets.ts
@@ -16,7 +16,6 @@ import type {
VsInputStyleSet,
VsLabelValueStyleSet,
VsLoadingStyleSet,
- VsModalStyleSet,
VsNoticeStyleSet,
VsPageStyleSet,
VsPaginationStyleSet,
@@ -34,6 +33,7 @@ import type {
VsThemeButtonStyleSet,
VsTooltipStyleSet,
} from '@/components';
+import type { VsModalNodeStyleSet } from '@/nodes';
const vsAccordion: VsAccordionStyleSet = {
backgroundColor: '#f5f5f5',
@@ -152,7 +152,7 @@ const vsLoading: VsLoadingStyleSet = {
barWidth: '16%',
};
-const vsModal: VsModalStyleSet = {
+const vsModal: VsModalNodeStyleSet = {
backgroundColor: '#FFF6E9',
fontColor: '#0D9276',
fontSize: '1.2rem',
diff --git a/packages/vlossom/src/utils/object.ts b/packages/vlossom/src/utils/object.ts
index 873d93903..c2375da8d 100644
--- a/packages/vlossom/src/utils/object.ts
+++ b/packages/vlossom/src/utils/object.ts
@@ -14,12 +14,9 @@ export const objectUtil = {
omit,
pick,
pickWithPath(object: Record, keys: string[]): Pick {
- return keys.reduce(
- (acc, key) => {
- acc[key] = this.get(object, key);
- return acc;
- },
- {} as Record,
- );
+ return keys.reduce((acc, key) => {
+ acc[key] = this.get(object, key);
+ return acc;
+ }, {} as Record);
},
};
diff --git a/packages/vlossom/src/vlossom-framework.ts b/packages/vlossom/src/vlossom-framework.ts
index 930cabce9..4dc58173b 100644
--- a/packages/vlossom/src/vlossom-framework.ts
+++ b/packages/vlossom/src/vlossom-framework.ts
@@ -1,10 +1,10 @@
import { store } from './stores';
-import * as vsPlugins from './plugins';
import { utils } from './utils';
+import { modalPlugin, toastPlugin, confirmPlugin } from '@/plugins';
import type { App } from 'vue';
import type { VlossomOptions, VsComponent, VsNode } from '@/declaration';
-import type { ToastPlugin, ConfirmPlugin } from './plugins';
+import type { ToastPlugin, ConfirmPlugin, ModalPlugin } from './plugins';
export class Vlossom {
constructor(options?: VlossomOptions) {
@@ -82,9 +82,11 @@ export class Vlossom {
this.theme = this.theme === 'dark' ? 'light' : 'dark';
}
- public toast: ToastPlugin = vsPlugins.toastPlugin;
+ public toast: ToastPlugin = toastPlugin;
- public confirm: ConfirmPlugin = vsPlugins.confirmPlugin;
+ public confirm: ConfirmPlugin = confirmPlugin;
+
+ public modal: ModalPlugin = modalPlugin;
}
let vlossom: Vlossom;