@teammodlr/ui-kit 3.0.0-beta.2 → 3.0.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/components/colorpicker/ColorPicker.vue.d.ts +0 -1
- package/lib/components/colorpicker/style/ColorPicker.css.d.ts +1 -0
- package/lib/components/dialog/Dialog.vue.d.ts +6 -2
- package/lib/components/dialogprovider/types.d.ts +2 -0
- package/lib/components/drawer/Drawer.vue.d.ts +142 -0
- package/lib/components/drawer/style/Drawer.css.d.ts +33 -0
- package/lib/components/drawer/style/index.d.ts +1 -0
- package/lib/components/index.d.ts +2 -0
- package/lib/components/messageprovider/MessageProvider.vue.d.ts +2 -0
- package/lib/components/messageprovider/style/Toaster.css.d.ts +1 -0
- package/lib/components/popover/Popover.vue.d.ts +113 -0
- package/lib/components/popover/style/Popover.css.d.ts +4 -0
- package/lib/components/popover/style/index.d.ts +1 -0
- package/lib/index.cjs.js +3 -3
- package/lib/index.es.js +5825 -5297
- package/lib/style.css +1 -1
- package/package.json +1 -1
- package/src/components/cardcontextmenu/style/Dropdown.css.ts +7 -0
- package/src/components/colorpicker/ColorPicker.vue +8 -11
- package/src/components/colorpicker/examples/basic.vue +8 -17
- package/src/components/colorpicker/style/ColorPicker.css.ts +6 -1
- package/src/components/dialog/Dialog.vue +44 -13
- package/src/components/dialog/style/Dialog.css.ts +15 -1
- package/src/components/dialogprovider/DialogProvider.vue +7 -1
- package/src/components/dialogprovider/types.ts +2 -0
- package/src/components/drawer/Drawer.vue +276 -0
- package/src/components/drawer/examples/basic.vue +15 -0
- package/src/components/drawer/examples/custom-title.vue +21 -0
- package/src/components/drawer/examples/index.stories.ts +24 -0
- package/src/components/drawer/examples/index.ts +7 -0
- package/src/components/drawer/examples/no-mask.vue +15 -0
- package/src/components/drawer/examples/no-outside-close.vue +15 -0
- package/src/components/drawer/examples/placements.vue +28 -0
- package/src/components/drawer/examples/with-footer.vue +21 -0
- package/src/components/drawer/examples/with-trigger.vue +13 -0
- package/src/components/drawer/style/Drawer.css.ts +208 -0
- package/src/components/drawer/style/index.ts +1 -0
- package/src/components/dropdown/Dropdown.vue +1 -1
- package/src/components/dropdown/style/Dropdown.css.ts +16 -0
- package/src/components/index.ts +2 -0
- package/src/components/messageprovider/MessageProvider.vue +36 -7
- package/src/components/messageprovider/style/Toaster.css.ts +24 -0
- package/src/components/modal/style/Modal.css.ts +26 -2
- package/src/components/popover/Popover.vue +155 -0
- package/src/components/popover/examples/arrow.vue +47 -0
- package/src/components/popover/examples/basic.vue +15 -0
- package/src/components/popover/examples/index.stories.ts +19 -0
- package/src/components/popover/examples/index.ts +4 -0
- package/src/components/popover/examples/placement.vue +59 -0
- package/src/components/popover/examples/variant.vue +30 -0
- package/src/components/popover/style/Popover.css.ts +203 -0
- package/src/components/popover/style/index.ts +1 -0
- package/src/components/tab/Tab.vue +1 -1
- package/src/components/tab/style/Tab.css.ts +6 -0
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
<DialogOverlay v-if="isOpen && modal" force-mount :class="backdropClasses" style="z-index: 10" />
|
|
7
7
|
</Transition>
|
|
8
8
|
<Transition type="animation" :enter-active-class="modalScale.enterActive" :leave-active-class="modalScale.leaveActive"
|
|
9
|
+
@after-leave="handleAfterLeave"
|
|
9
10
|
:enter-from-class="modalScale.enterFrom" :leave-to-class="modalScale.leaveTo">
|
|
10
11
|
|
|
11
12
|
<DialogContent
|
|
@@ -15,12 +16,12 @@
|
|
|
15
16
|
:style="computedStyle"
|
|
16
17
|
:disable-outside-pointer-events="modal"
|
|
17
18
|
@open-auto-focus="handleOpenAutoFocus"
|
|
18
|
-
@escape-key-down
|
|
19
|
-
@pointer-down-outside
|
|
20
|
-
@focus-outside="
|
|
21
|
-
@interact-outside
|
|
19
|
+
@escape-key-down="handleEscapeKeyDown"
|
|
20
|
+
@pointer-down-outside="handleOutsideEvent"
|
|
21
|
+
@focus-outside="handleOutsideEvent"
|
|
22
|
+
@interact-outside="handleOutsideEvent"
|
|
22
23
|
>
|
|
23
|
-
<FocusScope :trapped="
|
|
24
|
+
<FocusScope :trapped="activeFocusTrap" :loop="activeFocusTrap">
|
|
24
25
|
<slot v-if="$slots.container" name="container" :close="close"></slot>
|
|
25
26
|
<template v-else>
|
|
26
27
|
<div :class="style.content">
|
|
@@ -64,6 +65,7 @@ import {
|
|
|
64
65
|
import {
|
|
65
66
|
computed,
|
|
66
67
|
defineComponent,
|
|
68
|
+
onUnmounted,
|
|
67
69
|
provide,
|
|
68
70
|
ref,
|
|
69
71
|
shallowRef,
|
|
@@ -125,7 +127,7 @@ export default defineComponent({
|
|
|
125
127
|
},
|
|
126
128
|
},
|
|
127
129
|
// Eventually remove 'open', i lazy cant be assed for now
|
|
128
|
-
emits: ['onOpen', 'onClose', 'update:open', 'open'],
|
|
130
|
+
emits: ['onOpen', 'onClose', 'update:open', 'open', 'close'],
|
|
129
131
|
setup(props, { emit, expose }) {
|
|
130
132
|
const wrapper = ref<HTMLElement | null>(null);
|
|
131
133
|
const dialog = shallowRef<HTMLElement | ComponentPublicInstance | null>(null);
|
|
@@ -135,6 +137,19 @@ export default defineComponent({
|
|
|
135
137
|
const zIndex = ref(props.zIndex ?? nextZIndex());
|
|
136
138
|
provide(ZINDEX, zIndex);
|
|
137
139
|
const hasOpened = ref(false);
|
|
140
|
+
const focusTrapOverride = ref<boolean | null>(null);
|
|
141
|
+
const activeFocusTrap = computed(() => focusTrapOverride.value ?? props.focusTrap);
|
|
142
|
+
|
|
143
|
+
// reka-ui's FocusScope captures `trapped` at mount time and never re-checks it.
|
|
144
|
+
// We register this listener before FocusScope mounts (children mount after parent setup)
|
|
145
|
+
// so it fires first and blocks reka-ui's listener from stealing focus when trap is off.
|
|
146
|
+
const focusTrapBlocker = (event: FocusEvent) => {
|
|
147
|
+
if (!activeFocusTrap.value) {
|
|
148
|
+
event.stopImmediatePropagation();
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
document.addEventListener('focusin', focusTrapBlocker, true);
|
|
152
|
+
onUnmounted(() => document.removeEventListener('focusin', focusTrapBlocker, true));
|
|
138
153
|
|
|
139
154
|
const isDragging = ref<boolean>(false);
|
|
140
155
|
|
|
@@ -221,6 +236,16 @@ export default defineComponent({
|
|
|
221
236
|
handleOpenChange(false);
|
|
222
237
|
}
|
|
223
238
|
|
|
239
|
+
function handleEscapeKeyDown(event: KeyboardEvent) {
|
|
240
|
+
event.preventDefault();
|
|
241
|
+
event.stopPropagation();
|
|
242
|
+
close();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function handleAfterLeave() {
|
|
246
|
+
emit('close');
|
|
247
|
+
}
|
|
248
|
+
|
|
224
249
|
function onCloseEmit() {
|
|
225
250
|
emit('onClose');
|
|
226
251
|
if (props.onClose) {
|
|
@@ -234,12 +259,10 @@ export default defineComponent({
|
|
|
234
259
|
zIndex.value = nextZIndex();
|
|
235
260
|
}
|
|
236
261
|
|
|
237
|
-
function
|
|
238
|
-
if (
|
|
239
|
-
|
|
262
|
+
function handleOutsideEvent(event: Event) {
|
|
263
|
+
if (activeFocusTrap.value) {
|
|
264
|
+
event.preventDefault();
|
|
240
265
|
}
|
|
241
|
-
|
|
242
|
-
event.preventDefault();
|
|
243
266
|
}
|
|
244
267
|
|
|
245
268
|
function handleOpenAutoFocus(event: Event) {
|
|
@@ -340,8 +363,13 @@ export default defineComponent({
|
|
|
340
363
|
{ immediate: true }
|
|
341
364
|
);
|
|
342
365
|
|
|
366
|
+
function setFocusTrap(value: boolean) {
|
|
367
|
+
focusTrapOverride.value = value;
|
|
368
|
+
}
|
|
369
|
+
|
|
343
370
|
expose({
|
|
344
371
|
close,
|
|
372
|
+
setFocusTrap,
|
|
345
373
|
});
|
|
346
374
|
|
|
347
375
|
return {
|
|
@@ -360,11 +388,14 @@ export default defineComponent({
|
|
|
360
388
|
wrapperClasses,
|
|
361
389
|
wrapperStyle,
|
|
362
390
|
close,
|
|
391
|
+
handleAfterLeave,
|
|
392
|
+
handleEscapeKeyDown,
|
|
363
393
|
onEnter,
|
|
364
394
|
handleOpenChange,
|
|
365
395
|
handleOpenAutoFocus,
|
|
366
|
-
|
|
367
|
-
isOpen: computed(() => props.open)
|
|
396
|
+
handleOutsideEvent,
|
|
397
|
+
isOpen: computed(() => props.open),
|
|
398
|
+
activeFocusTrap,
|
|
368
399
|
};
|
|
369
400
|
},
|
|
370
401
|
});
|
|
@@ -60,7 +60,16 @@ export const content = style({
|
|
|
60
60
|
border: `1px solid ${themeVars.color.gray[300]}`,
|
|
61
61
|
borderRadius: themeVars.rounded.medium,
|
|
62
62
|
pointerEvents: 'auto',
|
|
63
|
-
boxShadow: `2px 2px 8px rgba(128, 128, 128, 0.1), 0 9px 28px 8px rgba(0,0,0,.05)
|
|
63
|
+
boxShadow: `2px 2px 8px rgba(128, 128, 128, 0.1), 0 9px 28px 8px rgba(0,0,0,.05)`,
|
|
64
|
+
selectors: {
|
|
65
|
+
'&:focus': {
|
|
66
|
+
outline: 'none'
|
|
67
|
+
},
|
|
68
|
+
'&:focus-visible': {
|
|
69
|
+
outline: 'none',
|
|
70
|
+
boxShadow: `2px 2px 8px rgba(128, 128, 128, 0.1), 0 9px 28px 8px rgba(0,0,0,.05)`
|
|
71
|
+
}
|
|
72
|
+
}
|
|
64
73
|
});
|
|
65
74
|
|
|
66
75
|
export const header = style({
|
|
@@ -119,6 +128,11 @@ export const close = style([
|
|
|
119
128
|
':focus': {
|
|
120
129
|
backgroundColor: themeVars.color.gray[150]
|
|
121
130
|
},
|
|
131
|
+
':focus-visible': {
|
|
132
|
+
outline: 'none',
|
|
133
|
+
boxShadow: 'none',
|
|
134
|
+
backgroundColor: themeVars.color.gray[150]
|
|
135
|
+
},
|
|
122
136
|
'::before': {
|
|
123
137
|
content: '""',
|
|
124
138
|
position: 'absolute',
|
|
@@ -39,6 +39,10 @@ export default defineComponent({
|
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
function setFocusTrap(key: string, enabled: boolean) {
|
|
43
|
+
dialogRefs.value[key]?.setFocusTrap(enabled);
|
|
44
|
+
}
|
|
45
|
+
|
|
42
46
|
function create(key: string, options: DialogOptions = {}): DialogReactive {
|
|
43
47
|
if (dialogMap[key]) {
|
|
44
48
|
// Merge new options into existing reactive options
|
|
@@ -72,6 +76,7 @@ export default defineComponent({
|
|
|
72
76
|
handleClose(key);
|
|
73
77
|
}
|
|
74
78
|
},
|
|
79
|
+
setFocusTrap: (enabled: boolean) => setFocusTrap(key, enabled),
|
|
75
80
|
};
|
|
76
81
|
|
|
77
82
|
dialogMap[key] = dialog;
|
|
@@ -122,7 +127,8 @@ export default defineComponent({
|
|
|
122
127
|
remove,
|
|
123
128
|
hasOpenDialog: () => hasOpenDialog.value,
|
|
124
129
|
openDialogCount: () => openDialogCount.value,
|
|
125
|
-
openDialogKeys: () => openDialogKeys.value
|
|
130
|
+
openDialogKeys: () => openDialogKeys.value,
|
|
131
|
+
setFocusTrap,
|
|
126
132
|
};
|
|
127
133
|
|
|
128
134
|
provide('m-dialog', api);
|
|
@@ -19,6 +19,7 @@ export interface DialogReactive {
|
|
|
19
19
|
options: DialogOptions;
|
|
20
20
|
open: () => void;
|
|
21
21
|
close: () => void;
|
|
22
|
+
setFocusTrap: (enabled: boolean) => void;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export interface DialogApiInjection {
|
|
@@ -27,4 +28,5 @@ export interface DialogApiInjection {
|
|
|
27
28
|
hasOpenDialog: () => boolean;
|
|
28
29
|
openDialogCount: () => number;
|
|
29
30
|
openDialogKeys: () => string[];
|
|
31
|
+
setFocusTrap: (key: string, enabled: boolean) => void;
|
|
30
32
|
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<DialogRoot :open="isOpen" modal @update:open="handleOpenChange">
|
|
3
|
+
<DialogTrigger v-if="hasTrigger" as-child>
|
|
4
|
+
<slot name="trigger" />
|
|
5
|
+
</DialogTrigger>
|
|
6
|
+
<DialogPortal to="body">
|
|
7
|
+
<Transition
|
|
8
|
+
appear
|
|
9
|
+
type="animation"
|
|
10
|
+
:enter-active-class="fade.enterActive"
|
|
11
|
+
:leave-active-class="fade.leaveActive"
|
|
12
|
+
:enter-from-class="fade.enterFrom"
|
|
13
|
+
:leave-to-class="fade.leaveTo"
|
|
14
|
+
>
|
|
15
|
+
<DialogOverlay
|
|
16
|
+
v-if="isOpen && showMask"
|
|
17
|
+
force-mount
|
|
18
|
+
:class="style.mask"
|
|
19
|
+
:style="{ zIndex }"
|
|
20
|
+
/>
|
|
21
|
+
</Transition>
|
|
22
|
+
<Transition
|
|
23
|
+
appear
|
|
24
|
+
type="animation"
|
|
25
|
+
:enter-active-class="currentTransition.enterActive"
|
|
26
|
+
:leave-active-class="currentTransition.leaveActive"
|
|
27
|
+
:enter-from-class="currentTransition.enterFrom"
|
|
28
|
+
:leave-to-class="currentTransition.leaveTo"
|
|
29
|
+
>
|
|
30
|
+
<DialogContent
|
|
31
|
+
v-if="isOpen"
|
|
32
|
+
force-mount
|
|
33
|
+
:class="[style.placement[placement]]"
|
|
34
|
+
:style="contentStyle"
|
|
35
|
+
:disable-outside-pointer-events="true"
|
|
36
|
+
@open-auto-focus="handleOpenAutoFocus"
|
|
37
|
+
@escape-key-down="onEscapeKeyDown"
|
|
38
|
+
@pointer-down-outside="onPointerDownOutside"
|
|
39
|
+
@interact-outside="onInteractOutside"
|
|
40
|
+
>
|
|
41
|
+
<FocusScope :trapped="focusTrap" :loop="focusTrap">
|
|
42
|
+
<div v-if="hasTitle || showClose" :class="style.header">
|
|
43
|
+
<DialogTitle as-child>
|
|
44
|
+
<div :class="style.title">
|
|
45
|
+
<slot name="title">{{ title }}</slot>
|
|
46
|
+
</div>
|
|
47
|
+
</DialogTitle>
|
|
48
|
+
<DialogClose v-if="showClose" as-child>
|
|
49
|
+
<button :class="style.close" aria-label="Close" />
|
|
50
|
+
</DialogClose>
|
|
51
|
+
</div>
|
|
52
|
+
<div :class="style.body" ref="bodyRef">
|
|
53
|
+
<slot />
|
|
54
|
+
</div>
|
|
55
|
+
<div v-if="hasFooter" :class="style.footer">
|
|
56
|
+
<slot name="footer" />
|
|
57
|
+
</div>
|
|
58
|
+
</FocusScope>
|
|
59
|
+
</DialogContent>
|
|
60
|
+
</Transition>
|
|
61
|
+
</DialogPortal>
|
|
62
|
+
</DialogRoot>
|
|
63
|
+
</template>
|
|
64
|
+
|
|
65
|
+
<script lang="ts">
|
|
66
|
+
import type { CSSProperties, PropType } from 'vue';
|
|
67
|
+
import {
|
|
68
|
+
DialogClose,
|
|
69
|
+
DialogContent,
|
|
70
|
+
DialogOverlay,
|
|
71
|
+
DialogPortal,
|
|
72
|
+
DialogRoot,
|
|
73
|
+
DialogTitle,
|
|
74
|
+
DialogTrigger,
|
|
75
|
+
FocusScope
|
|
76
|
+
} from 'reka-ui';
|
|
77
|
+
import { computed, defineComponent, provide, ref, watch } from 'vue';
|
|
78
|
+
import * as style from './style';
|
|
79
|
+
import { fade } from '../../styles/transitions.css';
|
|
80
|
+
import { useZIndex, ZINDEX } from '../../composables/useZIndex';
|
|
81
|
+
|
|
82
|
+
type Placement = 'left' | 'right' | 'top' | 'bottom';
|
|
83
|
+
|
|
84
|
+
export default defineComponent({
|
|
85
|
+
name: 'MDrawer',
|
|
86
|
+
components: {
|
|
87
|
+
DialogClose,
|
|
88
|
+
DialogContent,
|
|
89
|
+
DialogOverlay,
|
|
90
|
+
DialogPortal,
|
|
91
|
+
DialogRoot,
|
|
92
|
+
DialogTitle,
|
|
93
|
+
DialogTrigger,
|
|
94
|
+
FocusScope
|
|
95
|
+
},
|
|
96
|
+
props: {
|
|
97
|
+
open: {
|
|
98
|
+
type: Boolean,
|
|
99
|
+
default: undefined
|
|
100
|
+
},
|
|
101
|
+
placement: {
|
|
102
|
+
type: String as PropType<Placement>,
|
|
103
|
+
default: 'right'
|
|
104
|
+
},
|
|
105
|
+
width: {
|
|
106
|
+
type: [String, Number],
|
|
107
|
+
default: '378px'
|
|
108
|
+
},
|
|
109
|
+
height: {
|
|
110
|
+
type: [String, Number],
|
|
111
|
+
default: '378px'
|
|
112
|
+
},
|
|
113
|
+
title: {
|
|
114
|
+
type: String,
|
|
115
|
+
default: ''
|
|
116
|
+
},
|
|
117
|
+
showClose: {
|
|
118
|
+
type: Boolean,
|
|
119
|
+
default: true
|
|
120
|
+
},
|
|
121
|
+
showMask: {
|
|
122
|
+
type: Boolean,
|
|
123
|
+
default: true
|
|
124
|
+
},
|
|
125
|
+
outsideClose: {
|
|
126
|
+
type: Boolean,
|
|
127
|
+
default: true
|
|
128
|
+
},
|
|
129
|
+
focusTrap: {
|
|
130
|
+
type: Boolean,
|
|
131
|
+
default: true
|
|
132
|
+
},
|
|
133
|
+
zIndex: {
|
|
134
|
+
type: Number
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
emits: ['open', 'close', 'update:open'],
|
|
138
|
+
setup(props, { emit, slots }) {
|
|
139
|
+
const { nextZIndex } = useZIndex();
|
|
140
|
+
const zIndex = ref(props.zIndex ?? nextZIndex());
|
|
141
|
+
provide(ZINDEX, zIndex);
|
|
142
|
+
|
|
143
|
+
const bodyRef = ref<HTMLElement | null>(null);
|
|
144
|
+
const hasFooter = computed(() => !!slots.footer);
|
|
145
|
+
const hasTitle = computed(() => !!slots.title || !!props.title);
|
|
146
|
+
const hasTrigger = computed(() => !!slots.trigger);
|
|
147
|
+
|
|
148
|
+
const internalOpen = ref(false);
|
|
149
|
+
|
|
150
|
+
const isOpen = computed({
|
|
151
|
+
get: () => {
|
|
152
|
+
if (props.open !== undefined) return props.open;
|
|
153
|
+
return internalOpen.value;
|
|
154
|
+
},
|
|
155
|
+
set: (value: boolean) => {
|
|
156
|
+
if (props.open !== undefined) {
|
|
157
|
+
emit('update:open', value);
|
|
158
|
+
} else {
|
|
159
|
+
internalOpen.value = value;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const isHorizontal = computed(() =>
|
|
165
|
+
props.placement === 'left' || props.placement === 'right'
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const contentStyle = computed<CSSProperties>(() => {
|
|
169
|
+
const s: CSSProperties = { zIndex: zIndex.value };
|
|
170
|
+
if (isHorizontal.value) {
|
|
171
|
+
const w = props.width;
|
|
172
|
+
s.width = typeof w === 'number' ? `${w}px` : w;
|
|
173
|
+
} else {
|
|
174
|
+
const h = props.height;
|
|
175
|
+
s.height = typeof h === 'number' ? `${h}px` : h;
|
|
176
|
+
}
|
|
177
|
+
return s;
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const currentTransition = computed(() => style.slideTransition[props.placement]);
|
|
181
|
+
|
|
182
|
+
const focusableSelector = [
|
|
183
|
+
'[autofocus]',
|
|
184
|
+
'input:not([type="hidden"])',
|
|
185
|
+
'textarea',
|
|
186
|
+
'select',
|
|
187
|
+
'[contenteditable="true"]',
|
|
188
|
+
'button',
|
|
189
|
+
'a[href]',
|
|
190
|
+
'[tabindex]:not([tabindex="-1"])'
|
|
191
|
+
].join(', ');
|
|
192
|
+
|
|
193
|
+
function getFirstFocusable() {
|
|
194
|
+
if (!bodyRef.value) return null;
|
|
195
|
+
return Array.from(
|
|
196
|
+
bodyRef.value.querySelectorAll<HTMLElement>(focusableSelector)
|
|
197
|
+
).find(el =>
|
|
198
|
+
!el.matches(':disabled') &&
|
|
199
|
+
el.getAttribute('aria-disabled') !== 'true' &&
|
|
200
|
+
el.getAttribute('aria-hidden') !== 'true' &&
|
|
201
|
+
el.getClientRects().length > 0
|
|
202
|
+
) ?? null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const handleOpenAutoFocus = (e: Event) => {
|
|
206
|
+
const first = getFirstFocusable();
|
|
207
|
+
if (first) {
|
|
208
|
+
e.preventDefault();
|
|
209
|
+
first.focus();
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const close = (e: MouseEvent | KeyboardEvent | undefined) => {
|
|
214
|
+
emit('close', e);
|
|
215
|
+
isOpen.value = false;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const onEscapeKeyDown = (e: KeyboardEvent) => {
|
|
219
|
+
e.stopPropagation();
|
|
220
|
+
close(e);
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const onPointerDownOutside = (e: Event) => {
|
|
224
|
+
if (!props.outsideClose) e.preventDefault();
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const onInteractOutside = (e: Event) => {
|
|
228
|
+
if (!props.outsideClose) {
|
|
229
|
+
e.preventDefault();
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
close(e as MouseEvent | KeyboardEvent);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const handleOpenChange = (value: boolean) => {
|
|
236
|
+
if (!value && isOpen.value) {
|
|
237
|
+
close(undefined);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
isOpen.value = value;
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
watch(
|
|
244
|
+
() => isOpen.value,
|
|
245
|
+
(next, prev) => {
|
|
246
|
+
if (next && !prev) {
|
|
247
|
+
zIndex.value = props.zIndex ?? nextZIndex();
|
|
248
|
+
emit('open');
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
const open = () => { isOpen.value = true; };
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
style,
|
|
257
|
+
fade,
|
|
258
|
+
isOpen,
|
|
259
|
+
zIndex,
|
|
260
|
+
hasFooter,
|
|
261
|
+
hasTitle,
|
|
262
|
+
hasTrigger,
|
|
263
|
+
bodyRef,
|
|
264
|
+
contentStyle,
|
|
265
|
+
currentTransition,
|
|
266
|
+
open,
|
|
267
|
+
close,
|
|
268
|
+
handleOpenAutoFocus,
|
|
269
|
+
handleOpenChange,
|
|
270
|
+
onEscapeKeyDown,
|
|
271
|
+
onPointerDownOutside,
|
|
272
|
+
onInteractOutside
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
</script>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<MDrawer ref="drawer" title="Basic Drawer">
|
|
3
|
+
<p style="margin: 0">This is the drawer body. It slides in from the right by default.</p>
|
|
4
|
+
</MDrawer>
|
|
5
|
+
|
|
6
|
+
<MButton @click="drawer?.open()">Open Drawer</MButton>
|
|
7
|
+
</template>
|
|
8
|
+
|
|
9
|
+
<script lang="ts" setup>
|
|
10
|
+
import { ref } from 'vue';
|
|
11
|
+
import MDrawer from '../Drawer.vue';
|
|
12
|
+
import MButton from '../../button/Button.vue';
|
|
13
|
+
|
|
14
|
+
const drawer = ref<InstanceType<typeof MDrawer> | null>(null);
|
|
15
|
+
</script>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<MDrawer ref="drawer">
|
|
3
|
+
<template #title>
|
|
4
|
+
<span style="display: flex; align-items: center; gap: 6px">
|
|
5
|
+
<span style="font-size: 1.1em">⚙️</span>
|
|
6
|
+
Settings
|
|
7
|
+
</span>
|
|
8
|
+
</template>
|
|
9
|
+
<p style="margin: 0">Custom title slot with an icon.</p>
|
|
10
|
+
</MDrawer>
|
|
11
|
+
|
|
12
|
+
<MButton @click="drawer?.open()">Open Drawer</MButton>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<script lang="ts" setup>
|
|
16
|
+
import { ref } from 'vue';
|
|
17
|
+
import MDrawer from '../Drawer.vue';
|
|
18
|
+
import MButton from '../../button/Button.vue';
|
|
19
|
+
|
|
20
|
+
const drawer = ref<InstanceType<typeof MDrawer> | null>(null);
|
|
21
|
+
</script>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import MDrawer from '../Drawer.vue';
|
|
2
|
+
import {
|
|
3
|
+
MDrawerBasic,
|
|
4
|
+
MDrawerPlacements,
|
|
5
|
+
MDrawerNoMask,
|
|
6
|
+
MDrawerWithFooter,
|
|
7
|
+
MDrawerCustomTitle,
|
|
8
|
+
MDrawerWithTrigger,
|
|
9
|
+
MDrawerNoOutsideClose
|
|
10
|
+
} from './index';
|
|
11
|
+
import { createStory } from '../../../__storybook__';
|
|
12
|
+
|
|
13
|
+
export default {
|
|
14
|
+
component: MDrawer,
|
|
15
|
+
title: 'Components/Drawer'
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const Basic = createStory(MDrawerBasic);
|
|
19
|
+
export const WithTrigger = createStory(MDrawerWithTrigger);
|
|
20
|
+
export const Placements = createStory(MDrawerPlacements);
|
|
21
|
+
export const NoMask = createStory(MDrawerNoMask);
|
|
22
|
+
export const WithFooter = createStory(MDrawerWithFooter);
|
|
23
|
+
export const CustomTitle = createStory(MDrawerCustomTitle);
|
|
24
|
+
export const NoOutsideClose = createStory(MDrawerNoOutsideClose);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { default as MDrawerBasic } from './basic.vue';
|
|
2
|
+
export { default as MDrawerPlacements } from './placements.vue';
|
|
3
|
+
export { default as MDrawerNoMask } from './no-mask.vue';
|
|
4
|
+
export { default as MDrawerWithFooter } from './with-footer.vue';
|
|
5
|
+
export { default as MDrawerCustomTitle } from './custom-title.vue';
|
|
6
|
+
export { default as MDrawerWithTrigger } from './with-trigger.vue';
|
|
7
|
+
export { default as MDrawerNoOutsideClose } from './no-outside-close.vue';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<MDrawer v-model:open="isOpen" title="No Backdrop" :show-mask="false">
|
|
3
|
+
<p style="margin: 0">This drawer has no backdrop overlay.</p>
|
|
4
|
+
</MDrawer>
|
|
5
|
+
|
|
6
|
+
<MButton @click="isOpen = true">Open (no backdrop)</MButton>
|
|
7
|
+
</template>
|
|
8
|
+
|
|
9
|
+
<script lang="ts" setup>
|
|
10
|
+
import { ref } from 'vue';
|
|
11
|
+
import MDrawer from '../Drawer.vue';
|
|
12
|
+
import MButton from '../../button/Button.vue';
|
|
13
|
+
|
|
14
|
+
const isOpen = ref(false);
|
|
15
|
+
</script>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<MDrawer ref="drawer" title="Persistent Drawer" :outside-close="false">
|
|
3
|
+
<p style="margin: 0">Clicking outside won't close this drawer. Use the × button or Escape.</p>
|
|
4
|
+
</MDrawer>
|
|
5
|
+
|
|
6
|
+
<MButton @click="drawer?.open()">Open Drawer</MButton>
|
|
7
|
+
</template>
|
|
8
|
+
|
|
9
|
+
<script lang="ts" setup>
|
|
10
|
+
import { ref } from 'vue';
|
|
11
|
+
import MDrawer from '../Drawer.vue';
|
|
12
|
+
import MButton from '../../button/Button.vue';
|
|
13
|
+
|
|
14
|
+
const drawer = ref<InstanceType<typeof MDrawer> | null>(null);
|
|
15
|
+
</script>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<MDrawer v-model:open="isOpen" :placement="activePlacement" :title="`Drawer — ${activePlacement}`">
|
|
3
|
+
<p style="margin: 0">Sliding in from the <strong>{{ activePlacement }}</strong>.</p>
|
|
4
|
+
</MDrawer>
|
|
5
|
+
|
|
6
|
+
<div style="display: flex; gap: 8px; flex-wrap: wrap">
|
|
7
|
+
<MButton @click="open('left')">Left</MButton>
|
|
8
|
+
<MButton @click="open('right')">Right</MButton>
|
|
9
|
+
<MButton @click="open('top')">Top</MButton>
|
|
10
|
+
<MButton @click="open('bottom')">Bottom</MButton>
|
|
11
|
+
</div>
|
|
12
|
+
</template>
|
|
13
|
+
|
|
14
|
+
<script lang="ts" setup>
|
|
15
|
+
import { ref } from 'vue';
|
|
16
|
+
import MDrawer from '../Drawer.vue';
|
|
17
|
+
import MButton from '../../button/Button.vue';
|
|
18
|
+
|
|
19
|
+
type Placement = 'left' | 'right' | 'top' | 'bottom';
|
|
20
|
+
|
|
21
|
+
const isOpen = ref(false);
|
|
22
|
+
const activePlacement = ref<Placement>('right');
|
|
23
|
+
|
|
24
|
+
function open(placement: Placement) {
|
|
25
|
+
activePlacement.value = placement;
|
|
26
|
+
isOpen.value = true;
|
|
27
|
+
}
|
|
28
|
+
</script>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<MDrawer v-model:open="isOpen" title="Drawer with Footer">
|
|
3
|
+
<p style="margin: 0">Drawer body content goes here.</p>
|
|
4
|
+
<template #footer>
|
|
5
|
+
<div style="display: flex; gap: 8px; justify-content: flex-end; width: 100%">
|
|
6
|
+
<MButton @click="isOpen = false">Cancel</MButton>
|
|
7
|
+
<MButton variant="primary" @click="isOpen = false">Save</MButton>
|
|
8
|
+
</div>
|
|
9
|
+
</template>
|
|
10
|
+
</MDrawer>
|
|
11
|
+
|
|
12
|
+
<MButton @click="isOpen = true">Open Drawer</MButton>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<script lang="ts" setup>
|
|
16
|
+
import { ref } from 'vue';
|
|
17
|
+
import MDrawer from '../Drawer.vue';
|
|
18
|
+
import MButton from '../../button/Button.vue';
|
|
19
|
+
|
|
20
|
+
const isOpen = ref(false);
|
|
21
|
+
</script>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<MDrawer title="Profile">
|
|
3
|
+
<template #trigger>
|
|
4
|
+
<MButton>Edit profile</MButton>
|
|
5
|
+
</template>
|
|
6
|
+
<p style="margin: 0">Trigger slot — no ref or v-model needed.</p>
|
|
7
|
+
</MDrawer>
|
|
8
|
+
</template>
|
|
9
|
+
|
|
10
|
+
<script lang="ts" setup>
|
|
11
|
+
import MDrawer from '../Drawer.vue';
|
|
12
|
+
import MButton from '../../button/Button.vue';
|
|
13
|
+
</script>
|