diff --git a/packages/datepickers/demo/datePicker.stories.mdx b/packages/datepickers/demo/datePicker.stories.mdx
index 83e067e80ce..5a3066065c0 100644
--- a/packages/datepickers/demo/datePicker.stories.mdx
+++ b/packages/datepickers/demo/datePicker.stories.mdx
@@ -23,6 +23,7 @@ import README from '../README.md';
message: 'Message'
}}
argTypes={{
+ appendToNode: { control: false },
value: { control: 'date' },
minValue: { control: 'date' },
maxValue: { control: 'date' },
diff --git a/packages/datepickers/demo/~patterns/patterns.stories.mdx b/packages/datepickers/demo/~patterns/patterns.stories.mdx
new file mode 100644
index 00000000000..e3d2da1b2f0
--- /dev/null
+++ b/packages/datepickers/demo/~patterns/patterns.stories.mdx
@@ -0,0 +1,28 @@
+import { Meta, Canvas, Story } from '@storybook/addon-docs';
+import { CalendarStory } from './stories/CalendarStory';
+
+
+
+# Patterns
+
+## Render calendar in a root level React portal
+
+The `appendToNode` property can be used to render the calendar in a different
+DOM location than inline with the DatePicker component. This is done via React
+portals under the hood.
+
+You typically will need to set this property if you are using the `DatePicker`
+inside an element with `overflow: hidden` / `auto` / `scroll` CSS styles.
+
+See in this example, that the calendar is currently getting cropped. Enable the
+`appendToNode` property to see the full calendar.
+
+
diff --git a/packages/datepickers/demo/~patterns/stories/CalendarStory.tsx b/packages/datepickers/demo/~patterns/stories/CalendarStory.tsx
new file mode 100644
index 00000000000..b4decd3cd38
--- /dev/null
+++ b/packages/datepickers/demo/~patterns/stories/CalendarStory.tsx
@@ -0,0 +1,55 @@
+/**
+ * Copyright Zendesk, Inc.
+ *
+ * Use of this source code is governed under the Apache License, Version 2.0
+ * found at http://www.apache.org/licenses/LICENSE-2.0.
+ */
+
+import React, { useRef } from 'react';
+import { StoryFn } from '@storybook/react';
+import styled from 'styled-components';
+import { DatePicker } from '@zendeskgarden/react-datepickers';
+import { Field, Input } from '@zendeskgarden/react-forms';
+import { Paragraph } from '@zendeskgarden/react-typography';
+import { getColor } from '@zendeskgarden/react-theming';
+
+interface IArgs {
+ appendToNode: boolean;
+}
+
+export const StyledContainer = styled.div`
+ position: relative;
+ border: ${p => p.theme.borders.sm};
+ border-radius: ${p => p.theme.borderRadii.md};
+ border-color: ${p => getColor({ theme: p.theme, variable: 'border.default' })};
+ padding: ${p => p.theme.space.md};
+ max-height: 300px;
+ overflow: clip;
+`;
+
+export const CalendarStory: StoryFn = ({ appendToNode }) => {
+ const portalNode = useRef(null);
+
+ return (
+ <>
+
+
+
+ Calendar portal pattern
+
+
+
+
+
+ Turnip greens yarrow ricebean rutabaga endive cauliflower sea lettuce kohlrabi amaranth
+ water spinach avocado daikon napa cabbage asparagus winter purslane kale. Celery potato
+ scallion desert raisin horseradish spinach carrot soko. Lotus root water spinach fennel
+ kombu maize bamboo shoot green bean swiss chard seakale pumpkin onion chickpea gram corn
+ pea. Brussels sprout coriander water chestnut gourd swiss chard wakame kohlrabi beetroot
+ carrot watercress. Corn amaranth salsify bunya nuts nori azuki bean chickweed potato bell
+ pepper artichoke.
+
+
+ >
+ );
+};
diff --git a/packages/datepickers/src/elements/DatePicker/DatePicker.spec.tsx b/packages/datepickers/src/elements/DatePicker/DatePicker.spec.tsx
index ca003d2a957..14111eea627 100644
--- a/packages/datepickers/src/elements/DatePicker/DatePicker.spec.tsx
+++ b/packages/datepickers/src/elements/DatePicker/DatePicker.spec.tsx
@@ -456,5 +456,21 @@ describe('DatePicker', () => {
expect(getByTestId('datepicker-menu')).toHaveAttribute('data-test-rtl', 'true');
});
+
+ it('portals as expected', () => {
+ const { container, rerender } = render();
+ const selector = '[data-test-id="datepicker-menu"]';
+
+ expect(container.querySelector(selector)).not.toBeNull();
+
+ const node = document.createElement('DIV');
+
+ document.body.appendChild(node);
+
+ rerender();
+
+ expect(container.querySelector(selector)).toBeNull();
+ expect(node.querySelector(selector)).not.toBeNull();
+ });
});
});
diff --git a/packages/datepickers/src/elements/DatePicker/DatePicker.tsx b/packages/datepickers/src/elements/DatePicker/DatePicker.tsx
index 1bf8a813dfa..c8529ac2b74 100644
--- a/packages/datepickers/src/elements/DatePicker/DatePicker.tsx
+++ b/packages/datepickers/src/elements/DatePicker/DatePicker.tsx
@@ -15,6 +15,7 @@ import React, {
useMemo,
forwardRef
} from 'react';
+import { createPortal } from 'react-dom';
import PropTypes from 'prop-types';
import { mergeRefs } from 'react-merge-refs';
import { ThemeContext } from 'styled-components';
@@ -34,6 +35,7 @@ const PLACEMENT_DEFAULT = 'bottom-start';
*/
export const DatePicker = forwardRef((props, calendarRef) => {
const {
+ appendToNode,
children,
placement: _placement,
zIndex,
@@ -124,6 +126,34 @@ export const DatePicker = forwardRef((props, c
dispatch({ type: 'CONTROLLED_LOCALE_CHANGE' });
}, [locale]);
+ const Node = (
+
+ {!!(state.isOpen || isVisible) && (
+
+
+
+ )}
+
+ );
+
return (
<>
((props, c
ref={mergeRefs([triggerRef, Child.ref ? Child.ref : null])}
/>
-
- {!!(state.isOpen || isVisible) && (
-
-
-
- )}
-
+ {appendToNode ? createPortal(Node, appendToNode) : Node}
>
);
@@ -167,6 +173,7 @@ export const DatePicker = forwardRef((props, c
DatePicker.displayName = 'DatePicker';
DatePicker.propTypes = {
+ appendToNode: PropTypes.any,
value: PropTypes.any,
onChange: PropTypes.any,
formatDate: PropTypes.func,
diff --git a/packages/datepickers/src/types/index.ts b/packages/datepickers/src/types/index.ts
index 9b09db0c086..255c31d22ea 100644
--- a/packages/datepickers/src/types/index.ts
+++ b/packages/datepickers/src/types/index.ts
@@ -15,6 +15,8 @@ export const PLACEMENT = ['auto', ...BASE_PLACEMENT] as const;
export type GardenPlacement = (typeof PLACEMENT)[number];
export interface IDatePickerProps extends Omit, 'onChange'> {
+ /** Appends the calendar to the element provided */
+ appendToNode?: Element | DocumentFragment;
/**
* Sets the selected date
*/