Skip to content

Commit

Permalink
feat(VsDrawer): add component (#68)
Browse files Browse the repository at this point in the history
  • Loading branch information
gmldus authored Feb 1, 2024
1 parent 57eaba2 commit 6e7c3bc
Show file tree
Hide file tree
Showing 16 changed files with 1,232 additions and 4 deletions.
7 changes: 7 additions & 0 deletions packages/vlossom/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ export { default as VsContainer } from './vs-container/VsContainer.vue';
export { type VsDividerStyleSet } from './vs-divider/types';
export { default as VsDivider } from './vs-divider/VsDivider.vue';

export { type VsDrawerStyleSet } from './vs-drawer/types';
export { default as VsDrawer } from './vs-drawer/VsDrawer.vue';

export { default as VsFocusTrap } from './vs-focus-trap/VsFocusTrap.vue';

export { type VsFooterStyleSet } from './vs-footer/types';
export { default as VsFooter } from './vs-footer/VsFooter.vue';

Expand Down Expand Up @@ -62,6 +67,8 @@ declare module 'vue' {
VsChip: typeof import('./')['VsChip'];
VsContainer: typeof import('./')['VsContainer'];
VsDivider: typeof import('./')['VsDivider'];
VsDrawer: typeof import('./')['VsDrawer'];
VsFocusTrap: typeof import('./')['VsFocusTrap'];
VsFooter: typeof import('./')['VsFooter'];
VsForm: typeof import('./')['VsForm'];
VsHeader: typeof import('./')['VsHeader'];
Expand Down
133 changes: 133 additions & 0 deletions packages/vlossom/src/components/vs-drawer/VsDrawer.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
$sizes: xs, sm, md, lg, xl;

.vs-drawer {
.dimmed {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: var(--vs-drawer-z-index);
background-color: var(--vs-dimmed-backgroundColor);

&.has-container {
position: absolute;
}
}

.vs-drawer-content {
position: fixed;
width: 100%;
height: 100%;
z-index: var(--vs-drawer-z-index);
background-color: var(--vs-drawer-backgroundColor, var(--vs-plain-backgroundColor));
color: var(--vs-drawer-color, var(--vs-font-color));
display: flex;
flex-direction: column;

&.has-container {
position: absolute;
}

.drawer-body {
overflow-y: auto;
flex: 1;

&.hide-scroll {
overflow: scroll;
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
&::-webkit-scrollbar {
display: none;
}
}
}

// placement
&.top {
top: 0;
left: 0;
}

&.right {
top: 0;
right: 0;
}

&.bottom {
bottom: 0;
left: 0;
}

&.left {
top: 0;
left: 0;
}

// size
&.left,
&.right {
max-width: var(--vs-drawer-width, var(--vs-drawer-width-sm));
max-height: var(--vs-drawer-height);

@each $size in $sizes {
&.#{$size} {
max-width: var(--vs-drawer-width-#{$size});
}
}
}

&.top,
&.bottom {
max-width: var(--vs-drawer-width);
max-height: var(--vs-drawer-height, var(--vs-drawer-height-sm));

@each $size in $sizes {
&.#{$size} {
max-height: var(--vs-drawer-height-#{$size});
}
}
}
}
}

.fade-enter-active,
.fade-leave-active {
transition: all 0.3s;
}

.fade-enter-form,
.fade-leave-to {
opacity: 0;
}

.slide-left-enter-active,
.slide-left-leave-active,
.slide-right-enter-active,
.slide-right-leave-active,
.slide-top-enter-active,
.slide-top-leave-active,
.slide-bottom-enter-active,
.slide-bottom-leave-active {
transition: all 0.3s;
}

.slide-left-enter-from,
.slide-left-leave-to {
transform: translateX(-100%);
}

.slide-right-enter-from,
.slide-right-leave-to {
transform: translateX(100%);
}

.slide-bottom-enter-from,
.slide-bottom-leave-to {
transform: translateY(100%);
}

.slide-top-enter-from,
.slide-top-leave-to {
transform: translateY(-100%);
}
153 changes: 153 additions & 0 deletions packages/vlossom/src/components/vs-drawer/VsDrawer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
<template>
<Teleport to="body" :disabled="hasContainer">
<div class="vs-drawer">
<Transition name="fade">
<div
v-if="isOpen && dimmed"
:class="['dimmed', { 'has-container': hasContainer }]"
aria-hidden="true"
@click.stop="clickDimmed()"
/>
</Transition>
<Transition :name="`slide-${placement}`">
<vs-focus-trap v-if="isOpen" :focus-lock="dimmed" :initialFocusRef="initialFocusRef">
<div
:class="[
'vs-drawer-content',
placement,
hasSpecifiedSize ? '' : size,
{ 'has-container': hasContainer },
]"
:style="{ ...customProperties, ...sizeProperty }"
role="dialog"
:aria-labelledby="hasHeader ? 'vs-drawer-title' : undefined"
aria-describedby="vs-drawer-body"
:aria-label="hasHeader ? undefined : 'Dialog'"
:aria-modal="dimmed"
>
<header v-if="hasHeader" id="vs-drawer-title" aria-label="Dialog Header">
<slot name="header" />
</header>

<div :class="['drawer-body', { 'hide-scroll': hideScroll }]" id="vs-drawer-body">
<slot />
</div>

<footer v-if="hasFooter" aria-label="Dialog Footer">
<slot name="footer" />
</footer>
</div>
</vs-focus-trap>
</Transition>
</div>
</Teleport>
</template>

<script lang="ts">
import { defineComponent, ref, toRefs, watch, computed, onMounted, onBeforeUnmount, type PropType } from 'vue';
import { useCustomStyle } from '@/composables';
import { VsComponent, Placement, PLACEMENTS, Size, SIZES } from '@/declaration';
import { VsFocusTrap } from '@/components';
import type { VsDrawerStyleSet } from './types';
const name = VsComponent.VsDrawer;
export default defineComponent({
name,
components: { VsFocusTrap },
props: {
styleSet: { type: [String, Object] as PropType<string | VsDrawerStyleSet>, default: '' },
closeOnDimmedClick: { type: Boolean, default: true },
closeOnEsc: { type: Boolean, default: true },
dimmed: { type: Boolean, default: true },
hasContainer: { type: Boolean, default: false },
hideScroll: { type: Boolean, default: false },
initialFocusRef: { type: [Object, undefined] as PropType<HTMLElement | null>, default: null },
placement: {
type: String as PropType<Placement>,
default: 'left',
validator: (val: Placement) => PLACEMENTS.includes(val),
},
size: { type: String as PropType<Size | string>, default: '' },
// v-model
modelValue: { type: Boolean, default: false },
},
emits: ['update:modelValue'],
setup(props, { emit, slots }) {
const { styleSet, modelValue, closeOnEsc, closeOnDimmedClick, placement, size } = toRefs(props);
const { customProperties } = useCustomStyle<VsDrawerStyleSet>(name, styleSet);
const hasSpecifiedSize = computed(() => size.value && !SIZES.includes(size.value as Size));
const sizeProperty = computed(() => {
if (hasSpecifiedSize.value) {
if (placement.value === 'top' || placement.value === 'bottom') {
return { '--vs-drawer-height': size.value };
}
if (placement.value === 'left' || placement.value === 'right') {
return { '--vs-drawer-width': size.value };
}
}
return {};
});
const hasHeader = computed(() => !!slots['header']);
const hasFooter = computed(() => !!slots['footer']);
const isOpen = ref(modelValue.value);
watch(modelValue, (val) => {
isOpen.value = val;
});
function clickDimmed() {
if (closeOnDimmedClick.value) {
isOpen.value = false;
}
}
function onPressEsc(event: KeyboardEvent) {
if (event.key === 'Escape') {
isOpen.value = false;
}
}
watch(isOpen, (val) => {
if (closeOnEsc.value) {
if (val) {
document.addEventListener('keydown', onPressEsc);
} else {
document.removeEventListener('keydown', onPressEsc);
}
}
emit('update:modelValue', val);
});
onMounted(() => {
if (isOpen.value && closeOnEsc.value) {
document.addEventListener('keydown', onPressEsc);
}
});
onBeforeUnmount(() => {
document.removeEventListener('keydown', onPressEsc);
});
return {
customProperties,
hasSpecifiedSize,
sizeProperty,
isOpen,
clickDimmed,
hasHeader,
hasFooter,
};
},
});
</script>

<style lang="scss" scoped src="./VsDrawer.scss" />
Loading

0 comments on commit 6e7c3bc

Please sign in to comment.