Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions apps/example/e2e/datetimepicker.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { expect, test } from '@playwright/test';

test.describe('datetimepicker inside parent menu', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/datetimepickers');
});

test.afterEach(async ({ page }) => {
await page.keyboard.press('Escape');
await page.keyboard.press('Escape');
await page.keyboard.press('Escape');
});

test('parent menu stays open while calendar sub-menu is open and click-outside fires', async ({ page }) => {
await page.getByTestId('parent-menu-activator').click();
const parentContent = page.getByTestId('parent-menu-content');
await expect(parentContent).toBeVisible();

// Open the date-time picker (its own RuiMenu)
await parentContent.getByRole('textbox').click();

// Open the calendar's teleported month/year sub-menu by clicking the header title
await page.getByTestId('header-title').click();

// Click somewhere outside the calendar sub-menu but on the page body —
// because the picker exposes menu-open and the page binds :persistent to it,
// the parent menu must NOT close.
await page.mouse.click(5, 5);

await expect(parentContent).toBeVisible();
});

test('parent menu stays open while only the picker menu is open and click-outside fires', async ({ page }) => {
await page.getByTestId('parent-menu-activator').click();
const parentContent = page.getByTestId('parent-menu-content');
await expect(parentContent).toBeVisible();

// Open the picker but do NOT open the calendar's year/month sub-menu
await parentContent.getByRole('textbox').click();

// Click outside — picker exposes menu-open while its own menu is open,
// so parent stays open.
await page.mouse.click(5, 5);

await expect(parentContent).toBeVisible();
});

test('parent menu closes on outside click when no picker overlay is open', async ({ page }) => {
await page.getByTestId('parent-menu-activator').click();
const parentContent = page.getByTestId('parent-menu-content');
await expect(parentContent).toBeVisible();

// Click outside without opening the picker / calendar sub-menu
await page.mouse.click(5, 5);

await expect(parentContent).toBeHidden();
});
});
39 changes: 38 additions & 1 deletion apps/example/src/views/DateTimePickerView.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import type { ExtractPropTypes } from 'vue';
import { RuiDateTimePicker } from '@rotki/ui-library';
import { RuiButton, RuiDateTimePicker, RuiMenu } from '@rotki/ui-library';
import { objectOmit } from '@vueuse/shared';
import ComponentView from '@/components/ComponentView.vue';

Expand All @@ -9,6 +9,10 @@ type RuiDateTimePickerProps = ExtractPropTypes<typeof RuiDateTimePicker['props']
const timePickers = ref<RuiDateTimePickerProps[]>([{
modelValue: new Date(2023, 0, 2, 20, 20),
}]);

const parentMenuOpen = ref<boolean>(false);
const pickerMenuOpen = ref<boolean>(false);
const insideMenuValue = ref<Date | undefined>(new Date(2023, 0, 2, 20, 20));
</script>

<template>
Expand All @@ -24,5 +28,38 @@ const timePickers = ref<RuiDateTimePickerProps[]>([{
v-bind="objectOmit(field, ['modelValue'])"
/>
</div>
<div
class="mt-8"
data-id="picker-inside-menu-section"
>
<h3 class="text-lg font-semibold mb-4">
Inside Parent Menu
</h3>
<RuiMenu
v-model="parentMenuOpen"
:persistent="pickerMenuOpen"
:close-on-content-click="false"
>
<template #activator="{ attrs }">
<RuiButton
v-bind="attrs"
data-id="parent-menu-activator"
>
Open parent menu
</RuiButton>
</template>
<div
class="p-4 w-[360px]"
data-id="parent-menu-content"
>
<RuiDateTimePicker
v-model="insideMenuValue"
v-model:menu-open="pickerMenuOpen"
data-id="picker-inside-menu"
variant="outlined"
/>
</div>
</RuiMenu>
</div>
</ComponentView>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -2377,4 +2377,53 @@ describe('components/date-time-picker/RuiDateTimePicker.vue', () => {
expect(wrapper.text()).toContain('This hint should be visible');
});
});

describe('menu-open model', () => {
it('emits update:menuOpen when the picker menu opens and closes', async () => {
wrapper = createWrapper({
props: {
modelValue: new Date(),
},
});

await vi.runOnlyPendingTimersAsync();
await wrapper.find('[data-id="activator"]').trigger('click');
await vi.runOnlyPendingTimersAsync();

let events = wrapper.emitted('update:menuOpen');
expect(events?.at(-1)).toEqual([true]);

await wrapper.find('[data-id="append"]').trigger('click');
await vi.runOnlyPendingTimersAsync();

events = wrapper.emitted('update:menuOpen');
expect(events?.at(-1)).toEqual([false]);
});

it('stays open while the calendar sub-menu opens after the picker is open', async () => {
wrapper = createWrapper({
props: {
modelValue: new Date(),
},
});

await vi.runOnlyPendingTimersAsync();
await wrapper.find('[data-id="activator"]').trigger('click');
await vi.runOnlyPendingTimersAsync();

const menu = wrapper.findComponent({ name: 'RuiDateTimePickerMenu' });
assert(menu.exists());

menu.vm.$emit('update:calendarMenuOpen', true);
await nextTick();
await vi.runOnlyPendingTimersAsync();
const menuAfter = wrapper.findComponent({ name: 'RuiDateTimePickerMenu' });
menuAfter.vm.$emit('update:calendarMenuOpen', false);
await nextTick();
await vi.runOnlyPendingTimersAsync();

const events = wrapper.emitted('update:menuOpen');
expect(events?.every(e => e[0] === true)).toBe(true);
});
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { ComponentPropsAndSlots } from '@storybook/vue3-vite';
import { expect, waitFor, within } from 'storybook/test';
import RuiButton from '@/components/buttons/button/RuiButton.vue';
import RuiMenu from '@/components/overlays/menu/RuiMenu.vue';
import { TimeAccuracy } from '@/consts/time-accuracy';
import preview from '~/.storybook/preview';
import RuiDateTimePicker from './RuiDateTimePicker.vue';
Expand Down Expand Up @@ -174,4 +176,33 @@ export const Required = meta.story({
},
});

export const InsideParentMenu = meta.story({
args: {
modelValue: new Date(),
variant: 'outlined',
},
render: args => ({
components: { RuiButton, RuiDateTimePicker, RuiMenu },
setup() {
const open = ref<boolean>(false);
const pickerMenuOpen = ref<boolean>(false);
return { args, open, pickerMenuOpen };
},
template: `<div class="p-8">
<RuiMenu v-model="open" :persistent="pickerMenuOpen" :close-on-content-click="false">
<template #activator="{ attrs }">
<RuiButton v-bind="attrs">Open parent menu</RuiButton>
</template>
<div class="p-4 w-[360px]">
<RuiDateTimePicker
v-bind="args"
v-model="args.modelValue"
v-model:menu-open="pickerMenuOpen"
/>
</div>
</RuiMenu>
</div>`,
}),
});

export default meta;
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
});

const modelValue = defineModel<ModelValueType<DateTimeModelType>>({ required: true });
const menuOpen = defineModel<boolean>('menuOpen', { default: false });

const {
disabled = false,
Expand All @@ -68,7 +69,7 @@
errorMessages = [],
successMessages = [],
required = false,
} = defineProps<RuiDateTimePickerProps>();

Check warning on line 72 in packages/ui-library/src/components/date-time-picker/RuiDateTimePicker.vue

View workflow job for this annotation

GitHub Actions / ci

Component has too many props (16). Maximum allowed is 6

Check warning on line 72 in packages/ui-library/src/components/date-time-picker/RuiDateTimePicker.vue

View workflow job for this annotation

GitHub Actions / ci

Component has too many props (16). Maximum allowed is 6

defineSlots<{
'menu-content': () => any;
Expand Down Expand Up @@ -284,6 +285,12 @@
event.stopPropagation();
}
}

const anyMenuOpen = computed<boolean>(() => get(isOpen) || get(calendarMenuOpen));

watch(anyMenuOpen, (value) => {
set(menuOpen, value);
});
</script>

<template>
Expand Down
Loading