diff --git a/apps/example/e2e/datetimepicker.spec.ts b/apps/example/e2e/datetimepicker.spec.ts
new file mode 100644
index 00000000..09ad458c
--- /dev/null
+++ b/apps/example/e2e/datetimepicker.spec.ts
@@ -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();
+ });
+});
diff --git a/apps/example/src/views/DateTimePickerView.vue b/apps/example/src/views/DateTimePickerView.vue
index 199ce732..7b58afc8 100644
--- a/apps/example/src/views/DateTimePickerView.vue
+++ b/apps/example/src/views/DateTimePickerView.vue
@@ -1,6 +1,6 @@
@@ -24,5 +28,38 @@ const timePickers = ref([{
v-bind="objectOmit(field, ['modelValue'])"
/>
+
+
+ Inside Parent Menu
+
+
+
+
+ Open parent menu
+
+
+
+
+
+
+
diff --git a/packages/ui-library/src/components/date-time-picker/RuiDateTimePicker.spec.ts b/packages/ui-library/src/components/date-time-picker/RuiDateTimePicker.spec.ts
index 7d30b72c..a6c4740b 100644
--- a/packages/ui-library/src/components/date-time-picker/RuiDateTimePicker.spec.ts
+++ b/packages/ui-library/src/components/date-time-picker/RuiDateTimePicker.spec.ts
@@ -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);
+ });
+ });
});
diff --git a/packages/ui-library/src/components/date-time-picker/RuiDateTimePicker.stories.ts b/packages/ui-library/src/components/date-time-picker/RuiDateTimePicker.stories.ts
index 0082a30e..fa74b72f 100644
--- a/packages/ui-library/src/components/date-time-picker/RuiDateTimePicker.stories.ts
+++ b/packages/ui-library/src/components/date-time-picker/RuiDateTimePicker.stories.ts
@@ -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';
@@ -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(false);
+ const pickerMenuOpen = ref(false);
+ return { args, open, pickerMenuOpen };
+ },
+ template: `
+
+
+ Open parent menu
+
+
+
+
+
+
`,
+ }),
+});
+
export default meta;
diff --git a/packages/ui-library/src/components/date-time-picker/RuiDateTimePicker.vue b/packages/ui-library/src/components/date-time-picker/RuiDateTimePicker.vue
index b3f5a2f7..92196499 100644
--- a/packages/ui-library/src/components/date-time-picker/RuiDateTimePicker.vue
+++ b/packages/ui-library/src/components/date-time-picker/RuiDateTimePicker.vue
@@ -50,6 +50,7 @@ defineOptions({
});
const modelValue = defineModel>({ required: true });
+const menuOpen = defineModel('menuOpen', { default: false });
const {
disabled = false,
@@ -284,6 +285,12 @@ function arrowClicked(event: MouseEvent): void {
event.stopPropagation();
}
}
+
+const anyMenuOpen = computed(() => get(isOpen) || get(calendarMenuOpen));
+
+watch(anyMenuOpen, (value) => {
+ set(menuOpen, value);
+});