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
1 change: 1 addition & 0 deletions packages/datepickers/demo/datePicker.stories.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import README from '../README.md';
message: 'Message'
}}
argTypes={{
appendToNode: { control: false },
value: { control: 'date' },
minValue: { control: 'date' },
maxValue: { control: 'date' },
Expand Down
28 changes: 28 additions & 0 deletions packages/datepickers/demo/~patterns/patterns.stories.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Meta, Canvas, Story } from '@storybook/addon-docs';
import { CalendarStory } from './stories/CalendarStory';

<Meta title="Packages/DatePickers/[patterns]" />

# 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.

<Canvas>
<Story
name="Calendar"
args={{ appendToNode: false }}
argTypes={{ appendToNode: { control: 'boolean' } }}
>
{args => <CalendarStory {...args} />}
</Story>
</Canvas>
55 changes: 55 additions & 0 deletions packages/datepickers/demo/~patterns/stories/CalendarStory.tsx
Original file line number Diff line number Diff line change
@@ -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<IArgs> = ({ appendToNode }) => {
const portalNode = useRef<HTMLDivElement>(null);

return (
<>
<div ref={portalNode} />
<StyledContainer>
<Field>
<Field.Label>Calendar portal pattern</Field.Label>
<DatePicker appendToNode={appendToNode ? portalNode.current || undefined : undefined}>
<Input />
</DatePicker>
</Field>
<Paragraph style={{ marginTop: 20 }}>
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.
</Paragraph>
</StyledContainer>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -456,5 +456,21 @@ describe('DatePicker', () => {

expect(getByTestId('datepicker-menu')).toHaveAttribute('data-test-rtl', 'true');
});

it('portals as expected', () => {
const { container, rerender } = render(<Example />);
const selector = '[data-test-id="datepicker-menu"]';

expect(container.querySelector(selector)).not.toBeNull();

const node = document.createElement('DIV');

document.body.appendChild(node);

rerender(<Example appendToNode={node} />);

expect(container.querySelector(selector)).toBeNull();
expect(node.querySelector(selector)).not.toBeNull();
});
});
});
57 changes: 32 additions & 25 deletions packages/datepickers/src/elements/DatePicker/DatePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -34,6 +35,7 @@ const PLACEMENT_DEFAULT = 'bottom-start';
*/
export const DatePicker = forwardRef<HTMLDivElement, IDatePickerProps>((props, calendarRef) => {
const {
appendToNode,
children,
placement: _placement,
zIndex,
Expand Down Expand Up @@ -124,6 +126,34 @@ export const DatePicker = forwardRef<HTMLDivElement, IDatePickerProps>((props, c
dispatch({ type: 'CONTROLLED_LOCALE_CHANGE' });
}, [locale]);

const Node = (
<StyledMenuWrapper
ref={floatingRef}
style={{ transform }}
$isAnimated={!!isAnimated && (state.isOpen || isVisible)}
$placement={placement}
$zIndex={zIndex}
aria-hidden={!state.isOpen || undefined}
data-test-id="datepicker-menu"
data-test-open={state.isOpen}
data-test-rtl={theme.rtl}
>
{!!(state.isOpen || isVisible) && (
<StyledMenu {...menuProps}>
<Calendar
ref={calendarRef}
isCompact={isCompact}
value={value}
minValue={minValue}
maxValue={maxValue}
locale={locale}
weekStartsOn={weekStartsOn}
/>
</StyledMenu>
)}
</StyledMenuWrapper>
);

return (
<>
<Input
Expand All @@ -134,31 +164,7 @@ export const DatePicker = forwardRef<HTMLDivElement, IDatePickerProps>((props, c
ref={mergeRefs([triggerRef, Child.ref ? Child.ref : null])}
/>
<DatePickerContext.Provider value={contextValue}>
<StyledMenuWrapper
ref={floatingRef}
style={{ transform }}
$isAnimated={!!isAnimated && (state.isOpen || isVisible)}
$placement={placement}
$zIndex={zIndex}
aria-hidden={!state.isOpen || undefined}
data-test-id="datepicker-menu"
data-test-open={state.isOpen}
data-test-rtl={theme.rtl}
>
{!!(state.isOpen || isVisible) && (
<StyledMenu {...menuProps}>
<Calendar
ref={calendarRef}
isCompact={isCompact}
value={value}
minValue={minValue}
maxValue={maxValue}
locale={locale}
weekStartsOn={weekStartsOn}
/>
</StyledMenu>
)}
</StyledMenuWrapper>
{appendToNode ? createPortal(Node, appendToNode) : Node}
</DatePickerContext.Provider>
</>
);
Expand All @@ -167,6 +173,7 @@ export const DatePicker = forwardRef<HTMLDivElement, IDatePickerProps>((props, c
DatePicker.displayName = 'DatePicker';

DatePicker.propTypes = {
appendToNode: PropTypes.any,
value: PropTypes.any,
onChange: PropTypes.any,
formatDate: PropTypes.func,
Expand Down
2 changes: 2 additions & 0 deletions packages/datepickers/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export const PLACEMENT = ['auto', ...BASE_PLACEMENT] as const;
export type GardenPlacement = (typeof PLACEMENT)[number];

export interface IDatePickerProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {
/** Appends the calendar to the element provided */
appendToNode?: Element | DocumentFragment;
/**
* Sets the selected date
*/
Expand Down