@teammodlr/ui-kit 3.0.0-beta.30 → 3.0.0-beta.33
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/dialog/style/Dialog.css.d.ts +2 -0
- package/lib/components/index.d.ts +1 -1
- 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/index.cjs.js +3 -3
- package/lib/index.es.js +5186 -5520
- package/lib/style.css +1 -1
- package/lib/tsconfig.app.tsbuildinfo +1 -0
- package/package.json +2 -2
- package/src/components/dialog/Dialog.vue +2 -1
- package/src/components/dialog/style/Dialog.css.ts +30 -0
- package/src/components/index.ts +2 -1
- 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/lib/components/hovercard/HoverCard.vue.d.ts +0 -122
- package/lib/components/hovercard/style/HoverCard.css.d.ts +0 -3
- package/lib/components/hovercard/style/index.d.ts +0 -1
- package/src/components/hovercard/HoverCard.vue +0 -163
- package/src/components/hovercard/style/HoverCard.css.ts +0 -175
- package/src/components/hovercard/style/index.ts +0 -1
|
@@ -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>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import MPanelDialog from '../PanelDialog.vue';
|
|
2
|
+
import { MPanelDialogBasic, MPanelDialogModal } from './index';
|
|
3
|
+
import { createStory } from '../../../__storybook__';
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
component: MPanelDialog,
|
|
7
|
+
title: 'Components/PanelDialog',
|
|
8
|
+
argTypes: {
|
|
9
|
+
open: { control: 'boolean' },
|
|
10
|
+
modal: { control: 'boolean' },
|
|
11
|
+
draggable: { control: 'boolean' },
|
|
12
|
+
width: { control: 'text' },
|
|
13
|
+
height: { control: 'text' },
|
|
14
|
+
maxHeight: { control: 'text' },
|
|
15
|
+
handleSelector: { control: 'text' },
|
|
16
|
+
backdrop: { control: 'boolean' },
|
|
17
|
+
focusTrap: { control: 'boolean' }
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const Basic = createStory(MPanelDialogBasic);
|
|
22
|
+
export const Modal = createStory(MPanelDialogModal);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<button @click="open = true">Open modal panel</button>
|
|
4
|
+
<MPanelDialog
|
|
5
|
+
v-model:open="open"
|
|
6
|
+
:modal="true"
|
|
7
|
+
width="520px"
|
|
8
|
+
:draggable="false"
|
|
9
|
+
:backdrop="true"
|
|
10
|
+
>
|
|
11
|
+
<div data-panel-handle style="padding: 12px 16px; border-bottom: 1px solid #e4e2ea; font-weight: 600;">
|
|
12
|
+
Modal panel (drag disabled)
|
|
13
|
+
</div>
|
|
14
|
+
<div style="padding: 16px; display: flex; flex-direction: column; gap: 12px;">
|
|
15
|
+
<p style="margin: 0;">Modal variant blocks background interaction and shows an overlay.</p>
|
|
16
|
+
<MInput placeholder="Focus trap stays inside" />
|
|
17
|
+
<div style="display: flex; gap: 8px; justify-content: flex-end;">
|
|
18
|
+
<button @click="open = false">Close</button>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
</MPanelDialog>
|
|
22
|
+
</div>
|
|
23
|
+
</template>
|
|
24
|
+
|
|
25
|
+
<script lang="ts" setup>
|
|
26
|
+
import { ref } from 'vue';
|
|
27
|
+
import MPanelDialog from '../PanelDialog.vue';
|
|
28
|
+
import MInput from '../../input/Input.vue';
|
|
29
|
+
|
|
30
|
+
const open = ref(false);
|
|
31
|
+
</script>
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { globalStyle, style } from '@vanilla-extract/css';
|
|
2
|
+
import { themeVars } from '../../../styles/theme.css';
|
|
3
|
+
|
|
4
|
+
export const backdrop = style({
|
|
5
|
+
position: 'fixed',
|
|
6
|
+
inset: 0,
|
|
7
|
+
backgroundColor: 'rgba(0,0,0,.45)',
|
|
8
|
+
pointerEvents: 'none'
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export const dialogContent = style({
|
|
12
|
+
position: 'fixed',
|
|
13
|
+
inset: 0,
|
|
14
|
+
display: 'flex',
|
|
15
|
+
alignItems: 'center',
|
|
16
|
+
justifyContent: 'center',
|
|
17
|
+
outline: 'none',
|
|
18
|
+
selectors: {
|
|
19
|
+
'&:focus': {
|
|
20
|
+
outline: 'none'
|
|
21
|
+
},
|
|
22
|
+
'&:focus-visible': {
|
|
23
|
+
outline: 'none'
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export const dialogContentModal = style({
|
|
29
|
+
pointerEvents: 'auto'
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const dialogContentNonModal = style({
|
|
33
|
+
pointerEvents: 'none'
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const focusScope = style({
|
|
37
|
+
display: 'flex',
|
|
38
|
+
alignItems: 'center',
|
|
39
|
+
justifyContent: 'center',
|
|
40
|
+
width: '100%',
|
|
41
|
+
height: '100%',
|
|
42
|
+
outline: 'none',
|
|
43
|
+
selectors: {
|
|
44
|
+
'&:focus': {
|
|
45
|
+
outline: 'none'
|
|
46
|
+
},
|
|
47
|
+
'&:focus-visible': {
|
|
48
|
+
outline: 'none'
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export const focusScopeNonModal = style({
|
|
54
|
+
pointerEvents: 'none'
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export const focusScopeModal = style({
|
|
58
|
+
pointerEvents: 'auto'
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export const panel = style({
|
|
62
|
+
position: 'relative',
|
|
63
|
+
display: 'flex',
|
|
64
|
+
flexDirection: 'column',
|
|
65
|
+
backgroundColor: '#FFF',
|
|
66
|
+
backgroundClip: 'padding-box',
|
|
67
|
+
border: `1px solid ${themeVars.color.gray[300]}`,
|
|
68
|
+
borderRadius: themeVars.rounded.medium,
|
|
69
|
+
boxShadow: `2px 2px 8px rgba(128, 128, 128, 0.1), 0 9px 28px 8px rgba(0,0,0,.05)`,
|
|
70
|
+
overflow: 'hidden',
|
|
71
|
+
outline: 'none',
|
|
72
|
+
pointerEvents: 'auto',
|
|
73
|
+
maxWidth: 'calc(100vw - 32px)',
|
|
74
|
+
selectors: {
|
|
75
|
+
'&:focus': {
|
|
76
|
+
outline: 'none'
|
|
77
|
+
},
|
|
78
|
+
'&:focus-visible': {
|
|
79
|
+
outline: 'none'
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
globalStyle(`${panel} [data-panel-handle]`, {
|
|
85
|
+
cursor: 'grab'
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
globalStyle(`${panel} [data-panel-handle]:active`, {
|
|
89
|
+
cursor: 'grabbing'
|
|
90
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './PanelDialog.css';
|