Skip to content

Commit

Permalink
DatePicker: support SheetMobile for mobile (#3798)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlbertCarreras authored Oct 11, 2024
1 parent 093bf52 commit 97a781c
Show file tree
Hide file tree
Showing 11 changed files with 1,077 additions and 8 deletions.
2 changes: 1 addition & 1 deletion docs/examples/datepicker/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default function Example() {
onChange={({ value }) => setDateValue(value)}
value={dateValue}
/>
</Flex>{' '}
</Flex>
</Box>
);
}
19 changes: 19 additions & 0 deletions docs/examples/datepicker/mobile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useState } from 'react';
import { DeviceTypeProvider } from 'gestalt';
import { DatePicker } from 'gestalt-datepicker';

export default function Example() {
const [dateValue, setDateValue] = useState<Date | null>(new Date(1985, 6, 4));
return (
<DeviceTypeProvider deviceType="mobile">
<DatePicker
disableMobileUI={false}
helperText="Select a date"
id="main"
label="Delivery date"
onChange={({ value }) => setDateValue(value)}
value={dateValue}
/>
</DeviceTypeProvider>
);
}
2 changes: 2 additions & 0 deletions docs/examples/defaultlabelprovider/translations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ const labels = {
iconAccessibilityLabelSuccess: myI18nTranslator('Success'),
},
DatePicker: {
accessibilityDismissButtonLabel: myI18nTranslator('Dismiss date picker'),
dismissButton: myI18nTranslator('Close'),
openCalendar: myI18nTranslator('Open calendar'),
previousMonth: myI18nTranslator('Navigate to previou month'),
nextMonth: myI18nTranslator('Navigate to next month'),
Expand Down
17 changes: 17 additions & 0 deletions docs/pages/web/datepicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import enabled from '../../examples/datepicker/enabled';
import error from '../../examples/datepicker/error';
import helperText from '../../examples/datepicker/helperText';
import main from '../../examples/datepicker/main';
import mobile from '../../examples/datepicker/mobile';
import preselected from '../../examples/datepicker/preselected';
import range from '../../examples/datepicker/range';
import readOnly from '../../examples/datepicker/readOnly';
Expand Down Expand Up @@ -391,6 +392,22 @@ Read-only TextFields are used to present information to the user without allowin
)}
</CombinationNew>
</MainSection.Subsection>

<MainSection.Subsection
description={`DatePicker requires [DeviceTypeProvider](/web/utilities/devicetypeprovider) to enable its mobile user interface. The example below shows the mobile platform UI and its implementation.
For mobile, DatePicker is displayed in a SheetMobile.
`}
title="Mobile"
>
<MainSection.Card
cardSize="lg"
sandpackExample={
<SandpackExample code={mobile} layout="mobileRow" name="Mobile example" />
}
/>
</MainSection.Subsection>

<MainSection.Subsection
badge="experimental"
description={`DatePicker consumes external handlers from [GlobalEventsHandlerProvider](/web/utilities/globaleventshandlerprovider).
Expand Down
27 changes: 26 additions & 1 deletion packages/gestalt-datepicker/src/DatePicker.jsdom.test.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
import { useState } from 'react';
import { act, fireEvent, render, screen } from '@testing-library/react';
import { DeviceTypeProvider } from 'gestalt';
import DatePicker from './DatePicker';

const initialDate = new Date(2018, 11, 14);

function DatePickerWrap({ showMonthYearDropdown }: { showMonthYearDropdown?: boolean }) {
function DatePickerWrap({
showMonthYearDropdown,
disableMobileUI,
label,
}: {
showMonthYearDropdown?: boolean;
disableMobileUI?: boolean;
label?: string;
}) {
const [date, setDate] = useState<Date | null>(initialDate);

return (
<DatePicker
disableMobileUI={disableMobileUI}
id="fake_id"
label={label}
onChange={({ value }: any) => setDate(value)}
selectLists={showMonthYearDropdown ? ['year', 'month'] : undefined}
value={date}
Expand Down Expand Up @@ -140,4 +151,18 @@ describe('DatePicker', () => {
expect(screen.queryAllByRole('option', { name: 'January' })).toHaveLength(1);
expect(screen.queryAllByRole('option', { name: '2017' })).toHaveLength(1);
});

test('Mobile Datepicker renders', async () => {
const { baseElement } = render(
<DeviceTypeProvider deviceType="mobile">
<DatePickerWrap disableMobileUI={false} label="select" />
</DeviceTypeProvider>,
);

fireEvent.focus(screen.getByDisplayValue('12/14/2018'));

expect(baseElement).toMatchSnapshot();

expect(screen.getByText('Close')).toBeInTheDocument();
});
});
144 changes: 142 additions & 2 deletions packages/gestalt-datepicker/src/DatePicker.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,33 @@
import { forwardRef, ReactElement, useEffect, useImperativeHandle, useRef } from 'react';
import {
forwardRef,
Fragment,
ReactElement,
useEffect,
useImperativeHandle,
useRef,
useState,
} from 'react';
import { Locale } from 'date-fns/locale';
import { useGlobalEventsHandler } from 'gestalt';
import {
Button,
Flex,
Layer,
SheetMobile,
useDefaultLabel,
useDeviceType,
useGlobalEventsHandler,
} from 'gestalt';
import InternalDatePicker from './DatePicker/InternalDatePicker';

interface Indexable {
index(): number;
}

export type Props = {
/**
* DatePicker can adapt to mobile devices to [SheetMobile](https://gestalt.pinterest.systems/web/sheetmobile). Mobile adaptation is disabled by default. Set to 'false' to enable SheetMobile in mobile devices. See the [mobile variant](https://gestalt.pinterest.systems/web/datepicker#Mobile) to learn more.
*/
disableMobileUI?: boolean;
/**
* When disabled, DatePicker looks inactive and cannot be interacted with. See the [disabled example](https://gestalt.pinterest.systems/web/datepicker#States) to learn more.
*/
Expand Down Expand Up @@ -66,6 +90,14 @@ export type Props = {
* Placeholder text shown if the user has not yet input a value. The default placeholder value shows the date format for each locale, e.g. MM/DD/YYYY.
*/
placeholder?: string;
/**
* Callback fired when SheetMobile's in & out animations end. See [SheetMobile's animation variant](https://gestalt.pinterest.systems/web/sheetmobile#Animation) to learn more.
*/
mobileOnAnimationEnd?: (arg1: { animationState: 'in' | 'out' }) => void;
/**
* An object representing the zIndex value of the SheetMobile where DatePicker is built upon on mobile. Learn more about [zIndex classes](https://gestalt.pinterest.systems/web/zindex_classes)
*/
mobileZIndex?: Indexable;
/**
* Required for date range selection. End date on a date range selection. See the [date range example](https://gestalt.pinterest.systems/web/datepicker#Date-range) to learn more.
*/
Expand Down Expand Up @@ -107,6 +139,7 @@ export type Props = {
const DatePickerWithForwardRef = forwardRef<HTMLInputElement, Props>(function DatePicker(
{
disabled,
disableMobileUI = false,
errorMessage,
excludeDates,
helperText,
Expand All @@ -117,6 +150,8 @@ const DatePickerWithForwardRef = forwardRef<HTMLInputElement, Props>(function Da
localeData,
maxDate,
minDate,
mobileZIndex,
mobileOnAnimationEnd,
name,
nextRef,
onChange,
Expand All @@ -139,10 +174,115 @@ const DatePickerWithForwardRef = forwardRef<HTMLInputElement, Props>(function Da
datePickerHandlers: undefined,
};

const { accessibilityDismissButtonLabel, dismissButton } = useDefaultLabel('DatePicker');

const [showMobileCalendar, setShowMobileCalendar] = useState<boolean>(false);

const deviceType = useDeviceType();
const isMobile = deviceType === 'mobile';

useEffect(() => {
if (datePickerHandlers?.onRender) datePickerHandlers?.onRender();
}, [datePickerHandlers]);

if (isMobile && !disableMobileUI) {
return (
<Fragment>
<InternalDatePicker
ref={innerInputRef}
disabled={disabled}
errorMessage={errorMessage}
excludeDates={excludeDates}
helperText={helperText}
id={id}
idealDirection={idealDirection}
includeDates={includeDates}
inputOnly
label={label}
localeData={localeData}
maxDate={maxDate}
minDate={minDate}
name={name}
nextRef={nextRef}
onChange={onChange}
onFocus={() => setShowMobileCalendar(true)}
placeholder={placeholder}
rangeEndDate={rangeEndDate}
rangeSelector={rangeSelector}
rangeStartDate={rangeStartDate}
readOnly={readOnly}
selectLists={selectLists}
value={value}
/>
{showMobileCalendar ? (
<Layer zIndex={mobileZIndex}>
<SheetMobile
footer={
<SheetMobile.DismissingElement>
{({ onDismissStart }) => (
<Flex
alignItems="center"
direction="column"
gap={4}
justifyContent="center"
width="100%"
>
<Button
accessibilityLabel={accessibilityDismissButtonLabel}
color="gray"
onClick={() => onDismissStart()}
size="lg"
text={dismissButton}
/>
</Flex>
)}
</SheetMobile.DismissingElement>
}
heading=""
onAnimationEnd={mobileOnAnimationEnd}
onDismiss={() => setShowMobileCalendar(false)}
padding="none"
showDismissButton={false}
size="auto"
>
<SheetMobile.DismissingElement>
{({ onDismissStart }) => (
<Flex
alignItems="center"
direction="column"
gap={4}
justifyContent="center"
width="100%"
>
<InternalDatePicker
errorMessage={errorMessage}
excludeDates={excludeDates}
id={id}
idealDirection={idealDirection}
includeDates={includeDates}
inline
localeData={localeData}
maxDate={maxDate}
minDate={minDate}
nextRef={nextRef}
onChange={onChange}
onSelect={() => onDismissStart()}
rangeEndDate={rangeEndDate}
rangeSelector={rangeSelector}
rangeStartDate={rangeStartDate}
selectLists={selectLists}
value={value}
/>
</Flex>
)}
</SheetMobile.DismissingElement>
</SheetMobile>
</Layer>
) : null}
</Fragment>
);
}

return (
<InternalDatePicker
ref={innerInputRef}
Expand Down
4 changes: 4 additions & 0 deletions packages/gestalt-datepicker/src/DatePicker/DateInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type InjectedProps = {
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
onClick?: () => void;
onFocus?: (event: React.FocusEvent<HTMLInputElement>) => void;
onPassthroughFocus?: () => void;
onKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => void;
placeholder?: string;
readOnly?: boolean;
Expand All @@ -44,6 +45,7 @@ const DateInputWithForwardRef = forwardRef<HTMLInputElement, Props>(function Dat
onClick,
onBlur,
onFocus,
onPassthroughFocus,
onKeyDown,
placeholder,
readOnly,
Expand Down Expand Up @@ -77,6 +79,7 @@ const DateInputWithForwardRef = forwardRef<HTMLInputElement, Props>(function Dat
onBlur={(data) => onBlur?.(data.event)}
onChange={(data) => onChange?.(data.event)}
onFocus={(data) => {
onPassthroughFocus?.();
onFocus?.(data.event);
onClick?.();
}}
Expand Down Expand Up @@ -109,6 +112,7 @@ const DateInputWithForwardRef = forwardRef<HTMLInputElement, Props>(function Dat
onBlur={(data) => onBlur?.(data.event)}
onChange={(data) => onChange?.(data.event)}
onFocus={(data) => {
onPassthroughFocus?.();
onFocus?.(data.event);
onClick?.();
}}
Expand Down
Loading

0 comments on commit 97a781c

Please sign in to comment.