@teammodlr/ui-kit 3.0.0-beta.9 → 3.0.0
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/checkbox/Checkbox.vue.d.ts +4 -4
- package/lib/components/clearbutton/ClearButton.vue.d.ts +7 -0
- package/lib/components/clearbutton/style/ClearButton.css.d.ts +2 -0
- package/lib/components/clearbutton/style/index.d.ts +1 -0
- 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/style/Dialog.css.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/hovercard/HoverCard.vue.d.ts +122 -0
- package/lib/components/hovercard/style/HoverCard.css.d.ts +3 -0
- package/lib/components/hovercard/style/index.d.ts +1 -0
- package/lib/components/index.d.ts +3 -0
- package/lib/components/input/Input.vue.d.ts +20 -5
- package/lib/components/input/style/Input.css.d.ts +4 -0
- package/lib/components/paneldialog/PanelDialog.vue.d.ts +167 -0
- package/lib/components/paneldialog/style/PanelDialog.css.d.ts +8 -0
- package/lib/components/paneldialog/style/index.d.ts +1 -0
- package/lib/components/popover/Popover.vue.d.ts +24 -0
- package/lib/components/radio/Radio.vue.d.ts +5 -5
- package/lib/components/radiobutton/RadioButton.vue.d.ts +4 -4
- package/lib/components/select/Select.vue.d.ts +72 -91
- package/lib/components/select/style/Select.css.d.ts +9 -1
- package/lib/components/switch/Switch.vue.d.ts +4 -4
- package/lib/components/textarea/Textarea.vue.d.ts +16 -5
- package/lib/components/textarea/style/Textarea.css.d.ts +2 -0
- package/lib/composables/useInput.d.ts +9 -2
- package/lib/index.cjs.js +3 -3
- package/lib/index.es.js +8491 -6431
- package/lib/style.css +1 -1
- package/lib/tsconfig.app.tsbuildinfo +1 -0
- package/package.json +7 -4
- package/src/components/cardcontextmenu/style/Dropdown.css.ts +7 -0
- package/src/components/clearbutton/ClearButton.vue +39 -0
- package/src/components/clearbutton/style/ClearButton.css.ts +37 -0
- package/src/components/clearbutton/style/index.ts +1 -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 +2 -5
- package/src/components/dialog/style/Dialog.css.ts +30 -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/hovercard/HoverCard.vue +163 -0
- package/src/components/hovercard/style/HoverCard.css.ts +175 -0
- package/src/components/hovercard/style/index.ts +1 -0
- package/src/components/index.ts +3 -0
- package/src/components/input/Input.vue +52 -4
- package/src/components/input/examples/clearable.vue +18 -0
- package/src/components/input/examples/index.stories.ts +3 -1
- package/src/components/input/examples/index.ts +1 -0
- package/src/components/input/style/Input.css.ts +19 -0
- package/src/components/messageprovider/MessageProvider.vue +33 -7
- package/src/components/paneldialog/PanelDialog.vue +500 -0
- package/src/components/paneldialog/examples/basic.vue +36 -0
- package/src/components/paneldialog/examples/index.stories.ts +22 -0
- package/src/components/paneldialog/examples/index.ts +2 -0
- package/src/components/paneldialog/examples/modal.vue +31 -0
- package/src/components/paneldialog/style/PanelDialog.css.ts +90 -0
- package/src/components/paneldialog/style/index.ts +1 -0
- package/src/components/popover/Popover.vue +6 -1
- package/src/components/select/Select.vue +122 -42
- package/src/components/select/examples/clearable.vue +18 -0
- package/src/components/select/examples/fill.vue +52 -0
- package/src/components/select/examples/index.stories.ts +3 -1
- package/src/components/select/examples/index.ts +2 -0
- package/src/components/select/style/Select.css.ts +140 -24
- package/src/components/tab/Tab.vue +1 -1
- package/src/components/tab/style/Tab.css.ts +6 -0
- package/src/components/textarea/Textarea.vue +40 -4
- package/src/components/textarea/examples/clearable.vue +16 -0
- package/src/components/textarea/examples/index.stories.ts +2 -1
- package/src/components/textarea/examples/index.ts +1 -0
- package/src/components/textarea/style/Textarea.css.ts +10 -0
- package/src/composables/useInput.ts +10 -2
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div style="display: flex; flex-direction: column; gap: 12px; max-width: 320px;">
|
|
3
|
+
<MInput v-model="value" placeholder="Type something..." clearable />
|
|
4
|
+
<MInput v-model="value" placeholder="With a right icon" clearable>
|
|
5
|
+
<template #icon-right>
|
|
6
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" style="width: 20px; height: 20px;">
|
|
7
|
+
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" clip-rule="evenodd" />
|
|
8
|
+
</svg>
|
|
9
|
+
</template>
|
|
10
|
+
</MInput>
|
|
11
|
+
</div>
|
|
12
|
+
</template>
|
|
13
|
+
|
|
14
|
+
<script lang="ts" setup>
|
|
15
|
+
import { ref } from 'vue';
|
|
16
|
+
|
|
17
|
+
const value = ref('Clear me');
|
|
18
|
+
</script>
|
|
@@ -5,7 +5,8 @@ import {
|
|
|
5
5
|
MInputPrefix,
|
|
6
6
|
MInputLeftIcon,
|
|
7
7
|
MInputRightIcon,
|
|
8
|
-
MInputSizes
|
|
8
|
+
MInputSizes,
|
|
9
|
+
MInputClearable
|
|
9
10
|
} from './index';
|
|
10
11
|
|
|
11
12
|
import { createStory, sizeArgType } from '../../../__storybook__';
|
|
@@ -24,3 +25,4 @@ export const Prefix = createStory(MInputPrefix);
|
|
|
24
25
|
export const Sizes = createStory(MInputSizes);
|
|
25
26
|
export const LeftIcon = createStory(MInputLeftIcon);
|
|
26
27
|
export const RightIcon = createStory(MInputRightIcon);
|
|
28
|
+
export const Clearable = createStory(MInputClearable);
|
|
@@ -4,3 +4,4 @@ export { default as MInputPrefix } from './prefix.vue';
|
|
|
4
4
|
export { default as MInputSizes } from './sizes.vue';
|
|
5
5
|
export { default as MInputLeftIcon } from './left-icon.vue';
|
|
6
6
|
export { default as MInputRightIcon } from './right-icon.vue';
|
|
7
|
+
export { default as MInputClearable } from './clearable.vue';
|
|
@@ -251,6 +251,25 @@ export const hasRightIcon = style({
|
|
|
251
251
|
paddingRight: '2.25rem'
|
|
252
252
|
});
|
|
253
253
|
|
|
254
|
+
export const hasClear = style({
|
|
255
|
+
paddingRight: '1.75rem'
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
export const hasClearAndRightIcon = style({
|
|
259
|
+
paddingRight: '4rem'
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
export const clear = style({
|
|
263
|
+
position: 'absolute',
|
|
264
|
+
top: '50%',
|
|
265
|
+
right: themeVars.space[1],
|
|
266
|
+
transform: 'translateY(-50%)'
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
export const clearWithRightIcon = style({
|
|
270
|
+
right: '2.25rem'
|
|
271
|
+
});
|
|
272
|
+
|
|
254
273
|
export const prefix = style({
|
|
255
274
|
pointerEvents: 'none',
|
|
256
275
|
marginRight: themeVars.space[3],
|
|
@@ -64,7 +64,7 @@ export default defineComponent({
|
|
|
64
64
|
},
|
|
65
65
|
},
|
|
66
66
|
setup(props) {
|
|
67
|
-
const activeMessages = new Map<string, { stop: () => void; closed: boolean; onClose?: () => void }>();
|
|
67
|
+
const activeMessages = new Map<string, { stop: () => void; closed: boolean; currentId: string; onClose?: () => void }>();
|
|
68
68
|
|
|
69
69
|
const sonnerPosition = computed(() => placementMap[props.placement]);
|
|
70
70
|
|
|
@@ -100,9 +100,9 @@ export default defineComponent({
|
|
|
100
100
|
return timeout ?? props.timeout;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
function
|
|
103
|
+
function createToast(message: PrivateMessageReactive, sonnerId: string) {
|
|
104
104
|
const options: ExternalToast = {
|
|
105
|
-
id:
|
|
105
|
+
id: sonnerId,
|
|
106
106
|
duration: normalizeDuration(message.timeout),
|
|
107
107
|
closeButton: message.closeButton ?? message.closable,
|
|
108
108
|
richColors: message.richColors,
|
|
@@ -133,6 +133,23 @@ export default defineComponent({
|
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
function syncToast(message: PrivateMessageReactive) {
|
|
137
|
+
const active = activeMessages.get(message.id);
|
|
138
|
+
if (!active) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const oldId = active.currentId;
|
|
143
|
+
const newId = Math.random().toString(16).slice(2, 14);
|
|
144
|
+
active.currentId = newId;
|
|
145
|
+
|
|
146
|
+
toast.dismiss(oldId);
|
|
147
|
+
|
|
148
|
+
requestAnimationFrame(() => {
|
|
149
|
+
createToast(message, newId);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
136
153
|
function closeMessage(id: string, invokeOnClose: boolean) {
|
|
137
154
|
const active = activeMessages.get(id);
|
|
138
155
|
if (!active || active.closed) {
|
|
@@ -164,20 +181,29 @@ export default defineComponent({
|
|
|
164
181
|
type: options.type,
|
|
165
182
|
onClose: options.onClose,
|
|
166
183
|
destroy: () => {
|
|
184
|
+
const active = activeMessages.get(id);
|
|
185
|
+
const sonnerId = active?.currentId ?? id;
|
|
167
186
|
closeMessage(id, false);
|
|
168
|
-
toast.dismiss(
|
|
187
|
+
toast.dismiss(sonnerId);
|
|
169
188
|
},
|
|
170
189
|
}) as PrivateMessageReactive;
|
|
171
190
|
|
|
191
|
+
const sonnerId = Math.random().toString(16).slice(2, 14);
|
|
192
|
+
activeMessages.set(id, { stop: () => {}, closed: false, currentId: sonnerId, onClose: options.onClose });
|
|
193
|
+
|
|
172
194
|
const stop = watch(
|
|
173
195
|
() => [message.content, message.type, message.timeout, message.closable, message.closeButton, message.richColors, sonnerPosition.value],
|
|
174
|
-
() => {
|
|
175
|
-
|
|
196
|
+
(_, oldVal) => {
|
|
197
|
+
if (!oldVal) {
|
|
198
|
+
createToast(message, activeMessages.get(id)!.currentId);
|
|
199
|
+
} else {
|
|
200
|
+
syncToast(message);
|
|
201
|
+
}
|
|
176
202
|
},
|
|
177
203
|
{ immediate: true }
|
|
178
204
|
);
|
|
179
205
|
|
|
180
|
-
activeMessages.
|
|
206
|
+
activeMessages.get(id)!.stop = stop;
|
|
181
207
|
|
|
182
208
|
return message;
|
|
183
209
|
}
|
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<DialogRoot :open="isOpen" :modal="modal" @update:open="handleOpenChange">
|
|
3
|
+
<DialogPortal to="body">
|
|
4
|
+
<Transition
|
|
5
|
+
type="animation"
|
|
6
|
+
:enter-active-class="fade.enterActive"
|
|
7
|
+
:leave-active-class="fade.leaveActive"
|
|
8
|
+
:enter-from-class="fade.enterFrom"
|
|
9
|
+
:leave-to-class="fade.leaveTo"
|
|
10
|
+
>
|
|
11
|
+
<DialogOverlay v-if="isOpen && modal" force-mount :class="style.backdrop" :style="{ zIndex, pointerEvents: 'auto' }" />
|
|
12
|
+
</Transition>
|
|
13
|
+
<Transition
|
|
14
|
+
type="animation"
|
|
15
|
+
:enter-active-class="modalScale.enterActive"
|
|
16
|
+
:leave-active-class="modalScale.leaveActive"
|
|
17
|
+
:enter-from-class="modalScale.enterFrom"
|
|
18
|
+
:leave-to-class="modalScale.leaveTo"
|
|
19
|
+
@after-leave="handleAfterLeave"
|
|
20
|
+
>
|
|
21
|
+
<DialogContent
|
|
22
|
+
v-if="isOpen"
|
|
23
|
+
ref="dialogContent"
|
|
24
|
+
force-mount
|
|
25
|
+
:class="contentClasses"
|
|
26
|
+
:style="contentStyle"
|
|
27
|
+
:disable-outside-pointer-events="modal"
|
|
28
|
+
@open-auto-focus="handleOpenAutoFocus"
|
|
29
|
+
@escape-key-down="handleEscapeKeyDown"
|
|
30
|
+
@pointer-down-outside.prevent
|
|
31
|
+
@focus-outside="handleFocusOutside"
|
|
32
|
+
@interact-outside.prevent
|
|
33
|
+
>
|
|
34
|
+
<FocusScope :trapped="focusTrap" :loop="focusTrap" :class="focusScopeClasses">
|
|
35
|
+
<slot name="backdrop" />
|
|
36
|
+
<div v-if="backdrop && !$slots.backdrop" :class="[style.backdrop, backdropClass]" />
|
|
37
|
+
<div
|
|
38
|
+
ref="panel"
|
|
39
|
+
:class="[style.panel, panelClass]"
|
|
40
|
+
:style="panelComputedStyle"
|
|
41
|
+
@pointerdown="onPanelPointerDown"
|
|
42
|
+
>
|
|
43
|
+
<slot :close="close" />
|
|
44
|
+
</div>
|
|
45
|
+
</FocusScope>
|
|
46
|
+
</DialogContent>
|
|
47
|
+
</Transition>
|
|
48
|
+
</DialogPortal>
|
|
49
|
+
</DialogRoot>
|
|
50
|
+
</template>
|
|
51
|
+
<script lang="ts">
|
|
52
|
+
import { fade, modalScale } from '../../styles/transitions.css';
|
|
53
|
+
import {
|
|
54
|
+
DialogContent,
|
|
55
|
+
DialogOverlay,
|
|
56
|
+
DialogPortal,
|
|
57
|
+
DialogRoot,
|
|
58
|
+
FocusScope
|
|
59
|
+
} from 'reka-ui';
|
|
60
|
+
import {
|
|
61
|
+
computed,
|
|
62
|
+
defineComponent,
|
|
63
|
+
nextTick,
|
|
64
|
+
onBeforeUnmount,
|
|
65
|
+
provide,
|
|
66
|
+
ref,
|
|
67
|
+
shallowRef,
|
|
68
|
+
watch,
|
|
69
|
+
type ComponentPublicInstance,
|
|
70
|
+
type CSSProperties,
|
|
71
|
+
type PropType
|
|
72
|
+
} from 'vue';
|
|
73
|
+
import type { ComputedRef } from 'vue';
|
|
74
|
+
import { useZIndex, ZINDEX } from '../../composables/useZIndex';
|
|
75
|
+
import { useEventListener } from '@vueuse/core';
|
|
76
|
+
import * as style from './style';
|
|
77
|
+
|
|
78
|
+
function toCssSize(value: number | string | undefined): string | undefined {
|
|
79
|
+
if (value === undefined) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return typeof value === 'number' ? `${value}px` : value;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function resolveElement(
|
|
87
|
+
value: HTMLElement | ComponentPublicInstance | null | undefined
|
|
88
|
+
): HTMLElement | null {
|
|
89
|
+
if (!value) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (value instanceof HTMLElement) {
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if ('$el' in value && value.$el instanceof HTMLElement) {
|
|
98
|
+
return value.$el;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export default defineComponent({
|
|
105
|
+
name: 'PanelDialog',
|
|
106
|
+
components: {
|
|
107
|
+
DialogContent,
|
|
108
|
+
DialogOverlay,
|
|
109
|
+
DialogPortal,
|
|
110
|
+
DialogRoot,
|
|
111
|
+
FocusScope
|
|
112
|
+
},
|
|
113
|
+
props: {
|
|
114
|
+
open: {
|
|
115
|
+
type: Boolean,
|
|
116
|
+
default: false
|
|
117
|
+
},
|
|
118
|
+
modal: {
|
|
119
|
+
type: Boolean,
|
|
120
|
+
default: false
|
|
121
|
+
},
|
|
122
|
+
width: {
|
|
123
|
+
type: [Number, String] as PropType<number | string>,
|
|
124
|
+
default: '500px'
|
|
125
|
+
},
|
|
126
|
+
height: {
|
|
127
|
+
type: [Number, String] as PropType<number | string>,
|
|
128
|
+
default: undefined
|
|
129
|
+
},
|
|
130
|
+
maxHeight: {
|
|
131
|
+
type: [Number, String] as PropType<number | string>,
|
|
132
|
+
default: '85vh'
|
|
133
|
+
},
|
|
134
|
+
draggable: {
|
|
135
|
+
type: Boolean,
|
|
136
|
+
default: true
|
|
137
|
+
},
|
|
138
|
+
handleSelector: {
|
|
139
|
+
type: String,
|
|
140
|
+
default: '[data-panel-handle]'
|
|
141
|
+
},
|
|
142
|
+
backdrop: {
|
|
143
|
+
type: Boolean,
|
|
144
|
+
default: false
|
|
145
|
+
},
|
|
146
|
+
backdropClass: {
|
|
147
|
+
type: String,
|
|
148
|
+
default: ''
|
|
149
|
+
},
|
|
150
|
+
panelClass: {
|
|
151
|
+
type: [String, Array, Object] as PropType<string | unknown[] | Record<string, unknown>>,
|
|
152
|
+
default: ''
|
|
153
|
+
},
|
|
154
|
+
panelStyle: {
|
|
155
|
+
type: Object as PropType<CSSProperties>,
|
|
156
|
+
default: undefined
|
|
157
|
+
},
|
|
158
|
+
zIndex: {
|
|
159
|
+
type: Number
|
|
160
|
+
},
|
|
161
|
+
focusTrap: {
|
|
162
|
+
type: Boolean,
|
|
163
|
+
default: true
|
|
164
|
+
},
|
|
165
|
+
onClose: {
|
|
166
|
+
type: Function as PropType<() => void>,
|
|
167
|
+
default: () => {}
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
emits: ['onOpen', 'onClose', 'update:open', 'open', 'close'],
|
|
171
|
+
setup(props, { emit, expose }) {
|
|
172
|
+
const dialogContent = shallowRef<HTMLElement | ComponentPublicInstance | null>(null);
|
|
173
|
+
const panel = ref<HTMLElement | null>(null);
|
|
174
|
+
const { nextZIndex, current: maxZIndex } = useZIndex();
|
|
175
|
+
const zIndex = ref(props.zIndex ?? nextZIndex());
|
|
176
|
+
provide(ZINDEX, zIndex);
|
|
177
|
+
const hasOpened = ref(false);
|
|
178
|
+
|
|
179
|
+
const offsetX = ref(0);
|
|
180
|
+
const offsetY = ref(0);
|
|
181
|
+
const isDragging = ref(false);
|
|
182
|
+
|
|
183
|
+
const dialogContentElement = computed(() => resolveElement(dialogContent.value));
|
|
184
|
+
|
|
185
|
+
const isOpen = computed(() => props.open);
|
|
186
|
+
|
|
187
|
+
const contentClasses = computed(() => [
|
|
188
|
+
style.dialogContent,
|
|
189
|
+
props.modal ? style.dialogContentModal : style.dialogContentNonModal
|
|
190
|
+
]);
|
|
191
|
+
|
|
192
|
+
const contentStyle: ComputedRef<CSSProperties> = computed(() => ({
|
|
193
|
+
zIndex: zIndex.value
|
|
194
|
+
}));
|
|
195
|
+
|
|
196
|
+
const focusScopeClasses = computed(() => [
|
|
197
|
+
style.focusScope,
|
|
198
|
+
props.modal ? style.focusScopeModal : style.focusScopeNonModal
|
|
199
|
+
]);
|
|
200
|
+
|
|
201
|
+
const panelComputedStyle: ComputedRef<CSSProperties> = computed(() => ({
|
|
202
|
+
...props.panelStyle,
|
|
203
|
+
width: toCssSize(props.width),
|
|
204
|
+
height: toCssSize(props.height),
|
|
205
|
+
maxHeight: toCssSize(props.maxHeight),
|
|
206
|
+
transform: `translate(${offsetX.value}px, ${offsetY.value}px)`
|
|
207
|
+
}));
|
|
208
|
+
|
|
209
|
+
const focusableSelector = [
|
|
210
|
+
'[autofocus]',
|
|
211
|
+
'input:not([type="hidden"])',
|
|
212
|
+
'textarea',
|
|
213
|
+
'select',
|
|
214
|
+
'[contenteditable="true"]',
|
|
215
|
+
'button',
|
|
216
|
+
'a[href]',
|
|
217
|
+
'[tabindex]:not([tabindex="-1"])'
|
|
218
|
+
].join(', ');
|
|
219
|
+
|
|
220
|
+
function close(): void {
|
|
221
|
+
handleOpenChange(false);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function handleEscapeKeyDown(event: KeyboardEvent): void {
|
|
225
|
+
event.preventDefault();
|
|
226
|
+
event.stopPropagation();
|
|
227
|
+
close();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function handleAfterLeave(): void {
|
|
231
|
+
emit('close');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function onCloseEmit(): void {
|
|
235
|
+
emit('onClose');
|
|
236
|
+
if (props.onClose) {
|
|
237
|
+
props.onClose();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function onEnter(): void {
|
|
242
|
+
emit('onOpen');
|
|
243
|
+
emit('open');
|
|
244
|
+
zIndex.value = props.zIndex ?? nextZIndex();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function handleFocusOutside(event: Event): void {
|
|
248
|
+
event.preventDefault();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function handleOpenAutoFocus(event: Event): void {
|
|
252
|
+
const element = dialogContentElement.value ?? panel.value;
|
|
253
|
+
|
|
254
|
+
if (!element) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const firstFocusableElement = Array.from(
|
|
259
|
+
element.querySelectorAll<HTMLElement>(focusableSelector)
|
|
260
|
+
).find(candidate => {
|
|
261
|
+
return (
|
|
262
|
+
!candidate.matches(':disabled') &&
|
|
263
|
+
candidate.getAttribute('aria-disabled') !== 'true' &&
|
|
264
|
+
candidate.getAttribute('aria-hidden') !== 'true' &&
|
|
265
|
+
candidate.getClientRects().length > 0
|
|
266
|
+
);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
if (!firstFocusableElement) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
event.preventDefault();
|
|
274
|
+
firstFocusableElement.focus();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function handleOpenChange(value: boolean): void {
|
|
278
|
+
if (!value && props.open) {
|
|
279
|
+
emit('update:open', false);
|
|
280
|
+
onCloseEmit();
|
|
281
|
+
hasOpened.value = false;
|
|
282
|
+
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
emit('update:open', value);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function clampOffset(): void {
|
|
290
|
+
const element = panel.value;
|
|
291
|
+
|
|
292
|
+
if (!element) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const rect = element.getBoundingClientRect();
|
|
297
|
+
let dx = 0;
|
|
298
|
+
let dy = 0;
|
|
299
|
+
|
|
300
|
+
if (rect.width < window.innerWidth) {
|
|
301
|
+
if (rect.left < 0) {
|
|
302
|
+
dx = -rect.left;
|
|
303
|
+
} else if (rect.right > window.innerWidth) {
|
|
304
|
+
dx = window.innerWidth - rect.right;
|
|
305
|
+
}
|
|
306
|
+
} else if (rect.left !== 0) {
|
|
307
|
+
dx = -rect.left;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (rect.height < window.innerHeight) {
|
|
311
|
+
if (rect.top < 0) {
|
|
312
|
+
dy = -rect.top;
|
|
313
|
+
} else if (rect.bottom > window.innerHeight) {
|
|
314
|
+
dy = window.innerHeight - rect.bottom;
|
|
315
|
+
}
|
|
316
|
+
} else if (rect.top !== 0) {
|
|
317
|
+
dy = -rect.top;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (dx !== 0 || dy !== 0) {
|
|
321
|
+
offsetX.value += dx;
|
|
322
|
+
offsetY.value += dy;
|
|
323
|
+
element.style.transform = `translate(${offsetX.value}px, ${offsetY.value}px)`;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function resetPosition(): void {
|
|
328
|
+
offsetX.value = 0;
|
|
329
|
+
offsetY.value = 0;
|
|
330
|
+
|
|
331
|
+
if (panel.value) {
|
|
332
|
+
panel.value.style.transform = 'translate(0px, 0px)';
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
interface DragState {
|
|
337
|
+
startX: number;
|
|
338
|
+
startY: number;
|
|
339
|
+
baseX: number;
|
|
340
|
+
baseY: number;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
let dragState: DragState | null = null;
|
|
344
|
+
|
|
345
|
+
function onPointerMove(event: PointerEvent): void {
|
|
346
|
+
if (!dragState || !panel.value) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const element = panel.value;
|
|
351
|
+
const renderedRect = element.getBoundingClientRect();
|
|
352
|
+
const renderedX = offsetX.value;
|
|
353
|
+
const renderedY = offsetY.value;
|
|
354
|
+
let nextX = dragState.baseX + (event.clientX - dragState.startX);
|
|
355
|
+
let nextY = dragState.baseY + (event.clientY - dragState.startY);
|
|
356
|
+
const dx = nextX - renderedX;
|
|
357
|
+
const dy = nextY - renderedY;
|
|
358
|
+
const nextRect = {
|
|
359
|
+
left: renderedRect.left + dx,
|
|
360
|
+
right: renderedRect.right + dx,
|
|
361
|
+
top: renderedRect.top + dy,
|
|
362
|
+
bottom: renderedRect.bottom + dy,
|
|
363
|
+
width: renderedRect.width,
|
|
364
|
+
height: renderedRect.height
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
if (nextRect.width < window.innerWidth) {
|
|
368
|
+
if (nextRect.left < 0) {
|
|
369
|
+
nextX -= nextRect.left;
|
|
370
|
+
} else if (nextRect.right > window.innerWidth) {
|
|
371
|
+
nextX -= nextRect.right - window.innerWidth;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (nextRect.height < window.innerHeight) {
|
|
376
|
+
if (nextRect.top < 0) {
|
|
377
|
+
nextY -= nextRect.top;
|
|
378
|
+
} else if (nextRect.bottom > window.innerHeight) {
|
|
379
|
+
nextY -= nextRect.bottom - window.innerHeight;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
offsetX.value = nextX;
|
|
384
|
+
offsetY.value = nextY;
|
|
385
|
+
element.style.transform = `translate(${nextX}px, ${nextY}px)`;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function onPointerUp(): void {
|
|
389
|
+
dragState = null;
|
|
390
|
+
isDragging.value = false;
|
|
391
|
+
window.removeEventListener('pointermove', onPointerMove);
|
|
392
|
+
window.removeEventListener('pointerup', onPointerUp);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function onPanelPointerDown(event: PointerEvent): void {
|
|
396
|
+
if (!props.draggable || !panel.value) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (event.button !== 0) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const target = event.target as HTMLElement | null;
|
|
405
|
+
|
|
406
|
+
if (!target || typeof target.closest !== 'function') {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (props.handleSelector && !target.closest(props.handleSelector)) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (
|
|
415
|
+
target.closest(
|
|
416
|
+
'button, input, select, textarea, a[href], [contenteditable="true"]'
|
|
417
|
+
)
|
|
418
|
+
) {
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (zIndex.value < maxZIndex.value) {
|
|
423
|
+
zIndex.value = nextZIndex();
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
dragState = {
|
|
427
|
+
startX: event.clientX,
|
|
428
|
+
startY: event.clientY,
|
|
429
|
+
baseX: offsetX.value,
|
|
430
|
+
baseY: offsetY.value
|
|
431
|
+
};
|
|
432
|
+
isDragging.value = true;
|
|
433
|
+
window.addEventListener('pointermove', onPointerMove);
|
|
434
|
+
window.addEventListener('pointerup', onPointerUp);
|
|
435
|
+
event.preventDefault();
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function onWindowResize(): void {
|
|
439
|
+
if (props.open) {
|
|
440
|
+
nextTick(() => clampOffset());
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
useEventListener(window, 'resize', onWindowResize);
|
|
445
|
+
|
|
446
|
+
onBeforeUnmount(() => {
|
|
447
|
+
window.removeEventListener('pointermove', onPointerMove);
|
|
448
|
+
window.removeEventListener('pointerup', onPointerUp);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
watch(
|
|
452
|
+
() => props.open,
|
|
453
|
+
async newVal => {
|
|
454
|
+
if (newVal) {
|
|
455
|
+
await nextTick();
|
|
456
|
+
resetPosition();
|
|
457
|
+
nextTick(() => clampOffset());
|
|
458
|
+
zIndex.value = props.zIndex ?? nextZIndex();
|
|
459
|
+
if (!hasOpened.value) {
|
|
460
|
+
onEnter();
|
|
461
|
+
hasOpened.value = true;
|
|
462
|
+
}
|
|
463
|
+
} else {
|
|
464
|
+
hasOpened.value = false;
|
|
465
|
+
if (dragState) {
|
|
466
|
+
onPointerUp();
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
},
|
|
470
|
+
{ immediate: true }
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
expose({
|
|
474
|
+
close
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
return {
|
|
478
|
+
dialogContent,
|
|
479
|
+
panel,
|
|
480
|
+
zIndex,
|
|
481
|
+
fade,
|
|
482
|
+
modalScale,
|
|
483
|
+
style,
|
|
484
|
+
isOpen,
|
|
485
|
+
contentClasses,
|
|
486
|
+
contentStyle,
|
|
487
|
+
focusScopeClasses,
|
|
488
|
+
panelComputedStyle,
|
|
489
|
+
isDragging,
|
|
490
|
+
close,
|
|
491
|
+
handleAfterLeave,
|
|
492
|
+
handleEscapeKeyDown,
|
|
493
|
+
handleOpenChange,
|
|
494
|
+
handleOpenAutoFocus,
|
|
495
|
+
handleFocusOutside,
|
|
496
|
+
onPanelPointerDown
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
</script>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<button @click="open = true">Open panel</button>
|
|
4
|
+
<MPanelDialog
|
|
5
|
+
v-model:open="open"
|
|
6
|
+
:modal="false"
|
|
7
|
+
width="500px"
|
|
8
|
+
:draggable="true"
|
|
9
|
+
@onClose="closed"
|
|
10
|
+
>
|
|
11
|
+
<div data-panel-handle style="padding: 12px 16px; border-bottom: 1px solid #e4e2ea; font-weight: 600;">
|
|
12
|
+
Draggable panel (grab here)
|
|
13
|
+
</div>
|
|
14
|
+
<div style="padding: 16px; display: flex; flex-direction: column; gap: 12px;">
|
|
15
|
+
<p style="margin: 0;">Non-modal floating panel. Drag by the header. Background stays interactive.</p>
|
|
16
|
+
<MInput placeholder="Focus me — blue ring must remain" />
|
|
17
|
+
<input type="text" placeholder="Native input" />
|
|
18
|
+
<div style="display: flex; gap: 8px; justify-content: flex-end;">
|
|
19
|
+
<button @click="open = false">Close</button>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
</MPanelDialog>
|
|
23
|
+
</div>
|
|
24
|
+
</template>
|
|
25
|
+
|
|
26
|
+
<script lang="ts" setup>
|
|
27
|
+
import { ref } from 'vue';
|
|
28
|
+
import MPanelDialog from '../PanelDialog.vue';
|
|
29
|
+
import MInput from '../../input/Input.vue';
|
|
30
|
+
|
|
31
|
+
const open = ref(false);
|
|
32
|
+
|
|
33
|
+
function closed(): void {
|
|
34
|
+
console.log('closed');
|
|
35
|
+
}
|
|
36
|
+
</script>
|