Skip to content

Commit 4c887be

Browse files
authored
feat(datepickers): provide appendToNode prop for handling DatePicker calendar rendering (#1955)
1 parent 821d0f2 commit 4c887be

File tree

6 files changed

+134
-25
lines changed

6 files changed

+134
-25
lines changed

packages/datepickers/demo/datePicker.stories.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import README from '../README.md';
2323
message: 'Message'
2424
}}
2525
argTypes={{
26+
appendToNode: { control: false },
2627
value: { control: 'date' },
2728
minValue: { control: 'date' },
2829
maxValue: { control: 'date' },
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Meta, Canvas, Story } from '@storybook/addon-docs';
2+
import { CalendarStory } from './stories/CalendarStory';
3+
4+
<Meta title="Packages/DatePickers/[patterns]" />
5+
6+
# Patterns
7+
8+
## Render calendar in a root level React portal
9+
10+
The `appendToNode` property can be used to render the calendar in a different
11+
DOM location than inline with the DatePicker component. This is done via React
12+
portals under the hood.
13+
14+
You typically will need to set this property if you are using the `DatePicker`
15+
inside an element with `overflow: hidden` / `auto` / `scroll` CSS styles.
16+
17+
See in this example, that the calendar is currently getting cropped. Enable the
18+
`appendToNode` property to see the full calendar.
19+
20+
<Canvas>
21+
<Story
22+
name="Calendar"
23+
args={{ appendToNode: false }}
24+
argTypes={{ appendToNode: { control: 'boolean' } }}
25+
>
26+
{args => <CalendarStory {...args} />}
27+
</Story>
28+
</Canvas>
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* Copyright Zendesk, Inc.
3+
*
4+
* Use of this source code is governed under the Apache License, Version 2.0
5+
* found at http://www.apache.org/licenses/LICENSE-2.0.
6+
*/
7+
8+
import React, { useRef } from 'react';
9+
import { StoryFn } from '@storybook/react';
10+
import styled from 'styled-components';
11+
import { DatePicker } from '@zendeskgarden/react-datepickers';
12+
import { Field, Input } from '@zendeskgarden/react-forms';
13+
import { Paragraph } from '@zendeskgarden/react-typography';
14+
import { getColor } from '@zendeskgarden/react-theming';
15+
16+
interface IArgs {
17+
appendToNode: boolean;
18+
}
19+
20+
export const StyledContainer = styled.div`
21+
position: relative;
22+
border: ${p => p.theme.borders.sm};
23+
border-radius: ${p => p.theme.borderRadii.md};
24+
border-color: ${p => getColor({ theme: p.theme, variable: 'border.default' })};
25+
padding: ${p => p.theme.space.md};
26+
max-height: 300px;
27+
overflow: clip;
28+
`;
29+
30+
export const CalendarStory: StoryFn<IArgs> = ({ appendToNode }) => {
31+
const portalNode = useRef<HTMLDivElement>(null);
32+
33+
return (
34+
<>
35+
<div ref={portalNode} />
36+
<StyledContainer>
37+
<Field>
38+
<Field.Label>Calendar portal pattern</Field.Label>
39+
<DatePicker appendToNode={appendToNode ? portalNode.current || undefined : undefined}>
40+
<Input />
41+
</DatePicker>
42+
</Field>
43+
<Paragraph style={{ marginTop: 20 }}>
44+
Turnip greens yarrow ricebean rutabaga endive cauliflower sea lettuce kohlrabi amaranth
45+
water spinach avocado daikon napa cabbage asparagus winter purslane kale. Celery potato
46+
scallion desert raisin horseradish spinach carrot soko. Lotus root water spinach fennel
47+
kombu maize bamboo shoot green bean swiss chard seakale pumpkin onion chickpea gram corn
48+
pea. Brussels sprout coriander water chestnut gourd swiss chard wakame kohlrabi beetroot
49+
carrot watercress. Corn amaranth salsify bunya nuts nori azuki bean chickweed potato bell
50+
pepper artichoke.
51+
</Paragraph>
52+
</StyledContainer>
53+
</>
54+
);
55+
};

packages/datepickers/src/elements/DatePicker/DatePicker.spec.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,5 +456,21 @@ describe('DatePicker', () => {
456456

457457
expect(getByTestId('datepicker-menu')).toHaveAttribute('data-test-rtl', 'true');
458458
});
459+
460+
it('portals as expected', () => {
461+
const { container, rerender } = render(<Example />);
462+
const selector = '[data-test-id="datepicker-menu"]';
463+
464+
expect(container.querySelector(selector)).not.toBeNull();
465+
466+
const node = document.createElement('DIV');
467+
468+
document.body.appendChild(node);
469+
470+
rerender(<Example appendToNode={node} />);
471+
472+
expect(container.querySelector(selector)).toBeNull();
473+
expect(node.querySelector(selector)).not.toBeNull();
474+
});
459475
});
460476
});

packages/datepickers/src/elements/DatePicker/DatePicker.tsx

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import React, {
1515
useMemo,
1616
forwardRef
1717
} from 'react';
18+
import { createPortal } from 'react-dom';
1819
import PropTypes from 'prop-types';
1920
import { mergeRefs } from 'react-merge-refs';
2021
import { ThemeContext } from 'styled-components';
@@ -34,6 +35,7 @@ const PLACEMENT_DEFAULT = 'bottom-start';
3435
*/
3536
export const DatePicker = forwardRef<HTMLDivElement, IDatePickerProps>((props, calendarRef) => {
3637
const {
38+
appendToNode,
3739
children,
3840
placement: _placement,
3941
zIndex,
@@ -124,6 +126,34 @@ export const DatePicker = forwardRef<HTMLDivElement, IDatePickerProps>((props, c
124126
dispatch({ type: 'CONTROLLED_LOCALE_CHANGE' });
125127
}, [locale]);
126128

129+
const Node = (
130+
<StyledMenuWrapper
131+
ref={floatingRef}
132+
style={{ transform }}
133+
$isAnimated={!!isAnimated && (state.isOpen || isVisible)}
134+
$placement={placement}
135+
$zIndex={zIndex}
136+
aria-hidden={!state.isOpen || undefined}
137+
data-test-id="datepicker-menu"
138+
data-test-open={state.isOpen}
139+
data-test-rtl={theme.rtl}
140+
>
141+
{!!(state.isOpen || isVisible) && (
142+
<StyledMenu {...menuProps}>
143+
<Calendar
144+
ref={calendarRef}
145+
isCompact={isCompact}
146+
value={value}
147+
minValue={minValue}
148+
maxValue={maxValue}
149+
locale={locale}
150+
weekStartsOn={weekStartsOn}
151+
/>
152+
</StyledMenu>
153+
)}
154+
</StyledMenuWrapper>
155+
);
156+
127157
return (
128158
<>
129159
<Input
@@ -134,31 +164,7 @@ export const DatePicker = forwardRef<HTMLDivElement, IDatePickerProps>((props, c
134164
ref={mergeRefs([triggerRef, Child.ref ? Child.ref : null])}
135165
/>
136166
<DatePickerContext.Provider value={contextValue}>
137-
<StyledMenuWrapper
138-
ref={floatingRef}
139-
style={{ transform }}
140-
$isAnimated={!!isAnimated && (state.isOpen || isVisible)}
141-
$placement={placement}
142-
$zIndex={zIndex}
143-
aria-hidden={!state.isOpen || undefined}
144-
data-test-id="datepicker-menu"
145-
data-test-open={state.isOpen}
146-
data-test-rtl={theme.rtl}
147-
>
148-
{!!(state.isOpen || isVisible) && (
149-
<StyledMenu {...menuProps}>
150-
<Calendar
151-
ref={calendarRef}
152-
isCompact={isCompact}
153-
value={value}
154-
minValue={minValue}
155-
maxValue={maxValue}
156-
locale={locale}
157-
weekStartsOn={weekStartsOn}
158-
/>
159-
</StyledMenu>
160-
)}
161-
</StyledMenuWrapper>
167+
{appendToNode ? createPortal(Node, appendToNode) : Node}
162168
</DatePickerContext.Provider>
163169
</>
164170
);
@@ -167,6 +173,7 @@ export const DatePicker = forwardRef<HTMLDivElement, IDatePickerProps>((props, c
167173
DatePicker.displayName = 'DatePicker';
168174

169175
DatePicker.propTypes = {
176+
appendToNode: PropTypes.any,
170177
value: PropTypes.any,
171178
onChange: PropTypes.any,
172179
formatDate: PropTypes.func,

packages/datepickers/src/types/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ export const PLACEMENT = ['auto', ...BASE_PLACEMENT] as const;
1515
export type GardenPlacement = (typeof PLACEMENT)[number];
1616

1717
export interface IDatePickerProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {
18+
/** Appends the calendar to the element provided */
19+
appendToNode?: Element | DocumentFragment;
1820
/**
1921
* Sets the selected date
2022
*/

0 commit comments

Comments
 (0)