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. + + + + {args => } + + 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 */