routie dev init since i didn't adhere to any proper guidance up until now
This commit is contained in:
+100
@@ -0,0 +1,100 @@
|
||||
@layer vuetify-components {
|
||||
.v-dialog {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: auto;
|
||||
}
|
||||
.v-dialog > .v-overlay__content {
|
||||
max-height: calc(100% - 48px);
|
||||
width: calc(100% - 48px);
|
||||
max-width: calc(100% - 48px);
|
||||
margin: 24px;
|
||||
}
|
||||
.v-dialog > .v-overlay__content,
|
||||
.v-dialog > .v-overlay__content > form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
.v-dialog > .v-overlay__content > .v-card,
|
||||
.v-dialog > .v-overlay__content > .v-sheet,
|
||||
.v-dialog > .v-overlay__content > form > .v-card,
|
||||
.v-dialog > .v-overlay__content > form > .v-sheet {
|
||||
--v-scrollbar-offset: 0px;
|
||||
border-radius: 4px;
|
||||
overflow-y: auto;
|
||||
flex: 1 1 var(--v-card-height, 100%);
|
||||
}
|
||||
.v-dialog > .v-overlay__content > .v-card,
|
||||
.v-dialog > .v-overlay__content > .v-sheet,
|
||||
.v-dialog > .v-overlay__content > form > .v-card,
|
||||
.v-dialog > .v-overlay__content > form > .v-sheet {
|
||||
box-shadow: 0px 4px 4px 0px rgba(var(--v-shadow-color), var(--v-shadow-key-opacity, 0.3)), 0px 8px 12px 6px rgba(var(--v-shadow-color), var(--v-shadow-ambient-opacity, 0.15));
|
||||
--v-elevation-overlay: color-mix(in srgb, var(--v-elevation-overlay-color) 10%, transparent);
|
||||
}
|
||||
.v-dialog > .v-overlay__content > .v-card,
|
||||
.v-dialog > .v-overlay__content > form > .v-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.v-dialog > .v-overlay__content > .v-card > .v-card-item,
|
||||
.v-dialog > .v-overlay__content > form > .v-card > .v-card-item {
|
||||
padding: 16px 24px;
|
||||
}
|
||||
.v-dialog > .v-overlay__content > .v-card > .v-card-item + .v-card-text,
|
||||
.v-dialog > .v-overlay__content > form > .v-card > .v-card-item + .v-card-text {
|
||||
padding-top: 0;
|
||||
}
|
||||
.v-dialog > .v-overlay__content > .v-card > .v-card-text,
|
||||
.v-dialog > .v-overlay__content > form > .v-card > .v-card-text {
|
||||
font-size: inherit;
|
||||
letter-spacing: 0.03125em;
|
||||
line-height: inherit;
|
||||
padding: 16px 24px 24px;
|
||||
}
|
||||
.v-dialog > .v-overlay__content > .v-card > .v-card-actions,
|
||||
.v-dialog > .v-overlay__content > form > .v-card > .v-card-actions {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.v-dialog--fullscreen {
|
||||
--v-scrollbar-offset: 0px;
|
||||
}
|
||||
.v-dialog--fullscreen > .v-overlay__content {
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
overflow-y: auto;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
.v-dialog--fullscreen > .v-overlay__content > .v-card,
|
||||
.v-dialog--fullscreen > .v-overlay__content > .v-sheet,
|
||||
.v-dialog--fullscreen > .v-overlay__content > form > .v-card,
|
||||
.v-dialog--fullscreen > .v-overlay__content > form > .v-sheet {
|
||||
min-height: 100%;
|
||||
min-width: 100%;
|
||||
border-radius: 0;
|
||||
}
|
||||
.v-dialog--scrollable > .v-overlay__content > form,
|
||||
.v-dialog--scrollable > .v-overlay__content > form > .v-card {
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
.v-dialog--scrollable > .v-overlay__content,
|
||||
.v-dialog--scrollable > .v-overlay__content > .v-card,
|
||||
.v-dialog--scrollable > .v-overlay__content > form,
|
||||
.v-dialog--scrollable > .v-overlay__content > form > .v-card {
|
||||
display: flex;
|
||||
flex: 1 1 var(--v-card-height, 100%);
|
||||
flex-direction: column;
|
||||
}
|
||||
.v-dialog--scrollable > .v-overlay__content > .v-card > .v-card-text,
|
||||
.v-dialog--scrollable > .v-overlay__content > form > .v-card > .v-card-text {
|
||||
backface-visibility: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
+3434
File diff suppressed because it is too large
Load Diff
+105
@@ -0,0 +1,105 @@
|
||||
import { createVNode as _createVNode, mergeProps as _mergeProps } from "vue";
|
||||
// Styles
|
||||
import "./VDialog.css";
|
||||
|
||||
// Components
|
||||
import { VDialogTransition } from "../transitions/index.js";
|
||||
import { VDefaultsProvider } from "../VDefaultsProvider/index.js";
|
||||
import { VOverlay } from "../VOverlay/index.js";
|
||||
import { makeVOverlayProps } from "../VOverlay/VOverlay.js"; // Composables
|
||||
import { forwardRefs } from "../../composables/forwardRefs.js";
|
||||
import { useProxiedModel } from "../../composables/proxiedModel.js";
|
||||
import { useScopeId } from "../../composables/scopeId.js"; // Utilities
|
||||
import { mergeProps, nextTick, ref, watch } from 'vue';
|
||||
import { genericComponent, omit, propsFactory, useRender } from "../../util/index.js"; // Types
|
||||
export const makeVDialogProps = propsFactory({
|
||||
fullscreen: Boolean,
|
||||
scrollable: Boolean,
|
||||
...omit(makeVOverlayProps({
|
||||
captureFocus: true,
|
||||
origin: 'center center',
|
||||
scrollStrategy: 'block',
|
||||
transition: {
|
||||
component: VDialogTransition
|
||||
},
|
||||
zIndex: 2400,
|
||||
retainFocus: true
|
||||
}), ['disableInitialFocus'])
|
||||
}, 'VDialog');
|
||||
export const VDialog = genericComponent()({
|
||||
name: 'VDialog',
|
||||
props: makeVDialogProps(),
|
||||
emits: {
|
||||
'update:modelValue': value => true,
|
||||
afterEnter: () => true,
|
||||
afterLeave: () => true
|
||||
},
|
||||
setup(props, {
|
||||
emit,
|
||||
slots
|
||||
}) {
|
||||
const isActive = useProxiedModel(props, 'modelValue');
|
||||
const {
|
||||
scopeId
|
||||
} = useScopeId();
|
||||
const overlay = ref();
|
||||
function onAfterEnter() {
|
||||
emit('afterEnter');
|
||||
if ((props.scrim || props.retainFocus) && overlay.value?.contentEl && !overlay.value.contentEl.contains(document.activeElement)) {
|
||||
overlay.value.contentEl.focus({
|
||||
preventScroll: true
|
||||
});
|
||||
}
|
||||
}
|
||||
function onAfterLeave() {
|
||||
emit('afterLeave');
|
||||
}
|
||||
watch(isActive, async val => {
|
||||
if (!val) {
|
||||
await nextTick();
|
||||
overlay.value.activatorEl?.focus({
|
||||
preventScroll: true
|
||||
});
|
||||
}
|
||||
});
|
||||
useRender(() => {
|
||||
const overlayProps = VOverlay.filterProps(props);
|
||||
const activatorProps = mergeProps({
|
||||
'aria-haspopup': 'dialog'
|
||||
}, props.activatorProps);
|
||||
const contentProps = mergeProps({
|
||||
tabindex: -1
|
||||
}, props.contentProps);
|
||||
return _createVNode(VOverlay, _mergeProps({
|
||||
"ref": overlay,
|
||||
"class": ['v-dialog', {
|
||||
'v-dialog--fullscreen': props.fullscreen,
|
||||
'v-dialog--scrollable': props.scrollable
|
||||
}, props.class],
|
||||
"style": props.style
|
||||
}, overlayProps, {
|
||||
"modelValue": isActive.value,
|
||||
"onUpdate:modelValue": $event => isActive.value = $event,
|
||||
"aria-modal": "true",
|
||||
"activatorProps": activatorProps,
|
||||
"contentProps": contentProps,
|
||||
"height": !props.fullscreen ? props.height : undefined,
|
||||
"width": !props.fullscreen ? props.width : undefined,
|
||||
"maxHeight": !props.fullscreen ? props.maxHeight : undefined,
|
||||
"maxWidth": !props.fullscreen ? props.maxWidth : undefined,
|
||||
"role": "dialog",
|
||||
"onAfterEnter": onAfterEnter,
|
||||
"onAfterLeave": onAfterLeave
|
||||
}, scopeId), {
|
||||
activator: slots.activator,
|
||||
default: (...args) => _createVNode(VDefaultsProvider, {
|
||||
"root": "VDialog"
|
||||
}, {
|
||||
default: () => [slots.default?.(...args)]
|
||||
})
|
||||
});
|
||||
});
|
||||
return forwardRefs({}, overlay);
|
||||
}
|
||||
});
|
||||
//# sourceMappingURL=VDialog.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
+90
@@ -0,0 +1,90 @@
|
||||
@use '../../styles/tools'
|
||||
@use './variables' as *
|
||||
|
||||
@include tools.layer('components')
|
||||
.v-dialog
|
||||
align-items: center
|
||||
justify-content: center
|
||||
margin: auto
|
||||
|
||||
> .v-overlay__content
|
||||
max-height: calc(100% - #{$dialog-margin * 2})
|
||||
width: calc(100% - #{$dialog-margin * 2})
|
||||
max-width: calc(100% - #{$dialog-margin * 2})
|
||||
margin: $dialog-margin
|
||||
|
||||
&,
|
||||
> form
|
||||
display: flex
|
||||
flex-direction: column
|
||||
min-height: 0
|
||||
|
||||
> .v-card,
|
||||
> .v-sheet
|
||||
--v-scrollbar-offset: 0px
|
||||
border-radius: $dialog-border-radius
|
||||
overflow-y: auto
|
||||
flex: 1 1 var(--v-card-height, 100%)
|
||||
|
||||
@include tools.elevation($dialog-elevation)
|
||||
|
||||
> .v-card
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
> .v-card-item
|
||||
padding: $dialog-card-header-padding
|
||||
|
||||
+ .v-card-text
|
||||
padding-top: $dialog-card-header-text-padding-top
|
||||
|
||||
> .v-card-text
|
||||
font-size: inherit
|
||||
letter-spacing: $dialog-card-text-letter-spacing
|
||||
line-height: inherit
|
||||
padding: $dialog-card-text-padding
|
||||
|
||||
> .v-card-actions
|
||||
justify-content: $dialog-card-actions-justify
|
||||
|
||||
.v-dialog--fullscreen
|
||||
--v-scrollbar-offset: 0px
|
||||
|
||||
> .v-overlay__content
|
||||
border-radius: 0
|
||||
margin: 0
|
||||
padding: 0
|
||||
width: 100%
|
||||
height: 100%
|
||||
max-width: 100%
|
||||
max-height: 100%
|
||||
overflow-y: auto
|
||||
top: 0
|
||||
left: 0
|
||||
|
||||
&,
|
||||
> form
|
||||
> .v-card,
|
||||
> .v-sheet
|
||||
min-height: 100%
|
||||
min-width: 100%
|
||||
border-radius: 0
|
||||
|
||||
.v-dialog--scrollable > .v-overlay__content
|
||||
> form
|
||||
&,
|
||||
> .v-card
|
||||
max-height: 100%
|
||||
max-width: 100%
|
||||
|
||||
&,
|
||||
> form
|
||||
&,
|
||||
> .v-card
|
||||
display: flex
|
||||
flex: 1 1 var(--v-card-height, 100%)
|
||||
flex-direction: column
|
||||
|
||||
> .v-card > .v-card-text
|
||||
backface-visibility: hidden
|
||||
overflow-y: auto
|
||||
Generated
Vendored
+217
@@ -0,0 +1,217 @@
|
||||
import { createTextVNode as _createTextVNode, createElementVNode as _createElementVNode, createVNode as _createVNode } from "vue";
|
||||
// Components
|
||||
import { VDialog } from "../VDialog.js"; // Utilities
|
||||
import { commands, render, screen, userEvent, wait } from '@test';
|
||||
import { h, nextTick, ref } from 'vue';
|
||||
import { createMemoryHistory, createRouter } from 'vue-router';
|
||||
|
||||
// Tests
|
||||
describe('VDialog', () => {
|
||||
it('should render correctly', async () => {
|
||||
const model = ref(false);
|
||||
render(() => _createElementVNode("div", null, [_createVNode(VDialog, {
|
||||
"modelValue": model.value,
|
||||
"onUpdate:modelValue": $event => model.value = $event,
|
||||
"data-testid": "dialog"
|
||||
}, {
|
||||
default: () => [_createElementVNode("div", {
|
||||
"data-testid": "content"
|
||||
}, [_createTextVNode("Content")])]
|
||||
})]));
|
||||
expect(screen.queryByTestId('dialog')).toBeNull();
|
||||
model.value = true;
|
||||
await nextTick();
|
||||
await expect(screen.findByTestId('dialog')).resolves.toBeVisible();
|
||||
await expect.element(await screen.findByTestId('content')).toBeVisible();
|
||||
await commands.click(0, 0);
|
||||
await expect.poll(() => model.value).toBeFalsy();
|
||||
await expect.poll(() => screen.queryByTestId('dialog')).toBeNull();
|
||||
await expect.poll(() => screen.queryByTestId('content')).toBeNull();
|
||||
});
|
||||
it('should emit afterLeave', async () => {
|
||||
const model = ref(true);
|
||||
const onAfterLeave = vi.fn();
|
||||
render(() => _createElementVNode("div", null, [_createVNode(VDialog, {
|
||||
"modelValue": model.value,
|
||||
"onUpdate:modelValue": $event => model.value = $event,
|
||||
"onAfterLeave": onAfterLeave
|
||||
}, {
|
||||
default: () => [_createElementVNode("div", {
|
||||
"data-test": "content"
|
||||
}, [_createTextVNode("Content")])]
|
||||
})]));
|
||||
await commands.click(0, 0);
|
||||
await expect.poll(() => onAfterLeave).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('should focus on the last element when shift + tab key is pressed on the first element', async () => {
|
||||
const model = ref(true);
|
||||
render(() => _createElementVNode("div", null, [_createVNode(VDialog, {
|
||||
"modelValue": model.value,
|
||||
"onUpdate:modelValue": $event => model.value = $event,
|
||||
"persistent": true
|
||||
}, {
|
||||
default: () => [_createElementVNode("div", null, [_createElementVNode("button", {
|
||||
"data-testid": "first"
|
||||
}, [_createTextVNode("First")]), _createElementVNode("button", {
|
||||
"data-testid": "last"
|
||||
}, [_createTextVNode("Last")])])]
|
||||
})]));
|
||||
const first = screen.getByCSS('button[data-testid="first"]');
|
||||
const last = screen.getByCSS('button[data-testid="last"]');
|
||||
first.focus();
|
||||
await expect.poll(() => document.activeElement).toBe(first);
|
||||
await userEvent.tab({
|
||||
shift: true
|
||||
});
|
||||
await expect.poll(() => document.activeElement).toBe(last);
|
||||
});
|
||||
it('should focus on the first element when Tab key is pressed on the last element', async () => {
|
||||
const model = ref(true);
|
||||
render(() => _createElementVNode("div", null, [_createVNode(VDialog, {
|
||||
"modelValue": model.value,
|
||||
"onUpdate:modelValue": $event => model.value = $event
|
||||
}, {
|
||||
default: () => [_createElementVNode("div", null, [_createElementVNode("button", {
|
||||
"data-testid": "first"
|
||||
}, [_createTextVNode("First")]), _createElementVNode("button", {
|
||||
"data-testid": "last"
|
||||
}, [_createTextVNode("Last")])])]
|
||||
})]));
|
||||
const first = screen.getByCSS('button[data-testid="first"]');
|
||||
const last = screen.getByCSS('button[data-testid="last"]');
|
||||
last.focus();
|
||||
await expect.poll(() => document.activeElement).toBe(last);
|
||||
await userEvent.tab();
|
||||
await expect.poll(() => document.activeElement).toBe(first);
|
||||
});
|
||||
describe('routing back', () => {
|
||||
function createTestRouter() {
|
||||
return createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [{
|
||||
path: '/',
|
||||
component: {
|
||||
setup: () => () => h('h1', 'home')
|
||||
}
|
||||
}, {
|
||||
path: '/page1',
|
||||
component: {
|
||||
setup: () => () => h('h1', 'page1')
|
||||
}
|
||||
}, {
|
||||
path: '/page2',
|
||||
component: {
|
||||
setup: () => () => h('h1', 'page2')
|
||||
}
|
||||
}, {
|
||||
path: '/page3',
|
||||
component: {
|
||||
setup: () => () => h('h1', 'page3')
|
||||
}
|
||||
}]
|
||||
});
|
||||
}
|
||||
async function simulateBackNavigation(router) {
|
||||
router.back();
|
||||
for (let i = 0; i < 10; i++) await nextTick(); // flush microtasks
|
||||
window.dispatchEvent(new PopStateEvent('popstate', {
|
||||
state: {}
|
||||
}));
|
||||
await wait();
|
||||
}
|
||||
it('should block back with persistent dialog, allow after close, and block again when reopened', async () => {
|
||||
const router = createTestRouter();
|
||||
await router.push('/page1');
|
||||
await router.push('/page2');
|
||||
await router.push('/page3');
|
||||
const model = ref(true);
|
||||
render(() => _createVNode(VDialog, {
|
||||
"modelValue": model.value,
|
||||
"onUpdate:modelValue": $event => model.value = $event,
|
||||
"persistent": true,
|
||||
"data-testid": "dialog"
|
||||
}, {
|
||||
default: () => [_createElementVNode("div", {
|
||||
"data-testid": "content"
|
||||
}, [_createTextVNode("Content")])]
|
||||
}), {
|
||||
global: {
|
||||
plugins: [router]
|
||||
}
|
||||
});
|
||||
await expect(screen.findByTestId('dialog')).resolves.toBeVisible();
|
||||
await wait();
|
||||
|
||||
// 1st back: persistent dialog blocks navigation
|
||||
await simulateBackNavigation(router);
|
||||
expect(model.value).toBe(true);
|
||||
expect(router.currentRoute.value.path).toBe('/page3');
|
||||
|
||||
// close the dialog
|
||||
model.value = false;
|
||||
await nextTick();
|
||||
|
||||
// 2nd back: no dialog blocking, navigation proceeds
|
||||
await simulateBackNavigation(router);
|
||||
expect(router.currentRoute.value.path).toBe('/page2');
|
||||
|
||||
// reopen
|
||||
model.value = true;
|
||||
await nextTick();
|
||||
await expect(screen.findByTestId('dialog')).resolves.toBeVisible();
|
||||
await wait();
|
||||
|
||||
// 3rd back: persistent dialog blocks again
|
||||
await simulateBackNavigation(router);
|
||||
expect(model.value).toBe(true);
|
||||
expect(router.currentRoute.value.path).toBe('/page2');
|
||||
});
|
||||
it('should close non-persistent dialog on back and block navigation', async () => {
|
||||
const router = createTestRouter();
|
||||
await router.push('/page1');
|
||||
await router.push('/page2');
|
||||
await router.push('/page3');
|
||||
const model = ref(false);
|
||||
render(() => _createVNode(VDialog, {
|
||||
"modelValue": model.value,
|
||||
"onUpdate:modelValue": $event => model.value = $event,
|
||||
"data-testid": "dialog"
|
||||
}, {
|
||||
default: () => [_createElementVNode("div", {
|
||||
"data-testid": "content"
|
||||
}, [_createTextVNode("Content")])]
|
||||
}), {
|
||||
global: {
|
||||
plugins: [router]
|
||||
}
|
||||
});
|
||||
|
||||
// Open dialog
|
||||
model.value = true;
|
||||
await nextTick();
|
||||
await expect(screen.findByTestId('dialog')).resolves.toBeVisible();
|
||||
await wait();
|
||||
|
||||
// 1st back: dialog closes but route stays (navigation blocked)
|
||||
await simulateBackNavigation(router);
|
||||
await expect.poll(() => model.value).toBeFalsy();
|
||||
expect(router.currentRoute.value.path).toBe('/page3');
|
||||
|
||||
// 2nd back: no dialog, navigation proceeds
|
||||
await simulateBackNavigation(router);
|
||||
expect(router.currentRoute.value.path).toBe('/page2');
|
||||
|
||||
// reopen
|
||||
model.value = true;
|
||||
await nextTick();
|
||||
await expect(screen.findByTestId('dialog')).resolves.toBeVisible();
|
||||
await wait();
|
||||
|
||||
// 3rd back: dialog closes again, route stays
|
||||
await simulateBackNavigation(router);
|
||||
await expect.poll(() => model.value).toBeFalsy();
|
||||
expect(router.currentRoute.value.path).toBe('/page2');
|
||||
});
|
||||
});
|
||||
});
|
||||
//# sourceMappingURL=VDialog.spec.browser.js.map
|
||||
Generated
Vendored
+1
File diff suppressed because one or more lines are too long
+14
@@ -0,0 +1,14 @@
|
||||
@use 'sass:map';
|
||||
@use '../../styles/settings';
|
||||
@use '../../styles/tools';
|
||||
|
||||
// Defaults
|
||||
$dialog-elevation: 5 !default;
|
||||
$dialog-border-radius: settings.$border-radius-root !default;
|
||||
$dialog-margin: 24px !default;
|
||||
|
||||
$dialog-card-actions-justify: flex-end !default;
|
||||
$dialog-card-header-padding: 16px 24px !default;
|
||||
$dialog-card-header-text-padding-top: 0 !default;
|
||||
$dialog-card-text-padding: 16px 24px 24px !default;
|
||||
$dialog-card-text-letter-spacing: tools.map-deep-get(settings.$typography, 'body-large', 'letter-spacing') !default;
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { VDialog } from './VDialog.js';
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export { VDialog } from "./VDialog.js";
|
||||
//# sourceMappingURL=index.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.js","names":["VDialog"],"sources":["../../../src/components/VDialog/index.ts"],"sourcesContent":["export { VDialog } from './VDialog'\n"],"mappings":"SAASA,OAAO","ignoreList":[]}
|
||||
Reference in New Issue
Block a user