Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DatePicker: support SheetMobile for mobile #3798

Merged
merged 14 commits into from
Oct 11, 2024
Merged
Show file tree
Hide file tree
Changes from 10 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
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>
);
}
18 changes: 18 additions & 0 deletions docs/examples/datepicker/mobile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
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
26 changes: 25 additions & 1 deletion packages/gestalt-datepicker/src/DatePicker.jsdom.test.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
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,

Check failure on line 11 in packages/gestalt-datepicker/src/DatePicker.jsdom.test.tsx

View workflow job for this annotation

GitHub Actions / Tests

'label' is defined but never used. Allowed unused args must match /^_/u
}: {
showMonthYearDropdown?: boolean;
disableMobileUI?: boolean;
label?: string;
}) {
const [date, setDate] = useState<Date | null>(initialDate);

return (
<DatePicker
disableMobileUI={disableMobileUI}
id="fake_id"
onChange={({ value }: any) => setDate(value)}
selectLists={showMonthYearDropdown ? ['year', 'month'] : undefined}
Expand Down Expand Up @@ -140,4 +150,18 @@
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(screen.getByText('Close')).toBeInTheDocument();

expect(baseElement).toMatchSnapshot();
});
});
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
Loading