Skip to content

Commit

Permalink
Merge pull request #405 from AnnMarieW/update-date-components
Browse files Browse the repository at this point in the history
Update date components
  • Loading branch information
AnnMarieW authored Nov 14, 2024
2 parents 961dff5 + 799f613 commit 6dcf035
Show file tree
Hide file tree
Showing 5 changed files with 282 additions and 55 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

# Unreleased

### Added
- New additons to Date components #405 by @AnnMarieW
- `DatePicker` - The `debounce` prop can now be `True` `False` or `number` of ms delay before updating. When True, the value updates on blur.
- Added missing `highlightToday` prop on all Date components with calendars.
- `DateInput` updates properly now when clearable=True

- Enabled custom icons in `Checkbox` Added `icon` and `indeterminateIcon` props #408 by @snehilvj

Expand Down
41 changes: 27 additions & 14 deletions src/ts/components/dates/DateInput.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CalendarLevel, DateInput as MantineDateInput } from "@mantine/dates";
import { useDebouncedValue, useDidUpdate } from "@mantine/hooks";
import { useDebouncedValue, useDidUpdate, useFocusWithin } from "@mantine/hooks";
import { BoxProps } from "props/box";
import { DashBaseProps, PersistenceProps, DebounceProps } from "props/dash";
import {
Expand Down Expand Up @@ -50,6 +50,8 @@ interface Props
level?: CalendarLevel;
/** Specifies days that should be disabled */
disabledDates?: string[];
/** Determines whether today should be highlighted with a border, false by default */
highlightToday?: boolean;
}

/** DateInput */
Expand All @@ -75,12 +77,21 @@ const DateInput = (props: Props) => {
const debounceValue = typeof debounce === 'number' ? debounce : 0;
const [debounced] = useDebouncedValue(date, debounceValue);

const { ref, focused } = useFocusWithin();

useDidUpdate(() => {
if (typeof debounce === 'number' || debounce === false) {
setProps({ value: dateToString(date) });
}

// Ensure the value prop is updated when the date is cleared by clicking the "X" button,
// even if the input does not have focus.
if (!focused && debounce === true) {
setProps({ value: dateToString(date)})
}
}, [debounced]);


useDidUpdate(() => {
setDate(stringToDate(value));
}, [value]);
Expand All @@ -107,19 +118,21 @@ const DateInput = (props: Props) => {
};

return (
<MantineDateInput
data-dash-is-loading={
(loading_state && loading_state.is_loading) || undefined
}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
onChange={setDate}
value={date}
minDate={stringToDate(minDate)}
maxDate={stringToDate(maxDate)}
excludeDate={isExcluded}
{...others}
/>
<div ref={ref}>
<MantineDateInput
data-dash-is-loading={
(loading_state && loading_state.is_loading) || undefined
}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
onChange={setDate}
value={date}
minDate={stringToDate(minDate)}
maxDate={stringToDate(maxDate)}
excludeDate={isExcluded}
{...others}
/>
</div>
);
};

Expand Down
104 changes: 64 additions & 40 deletions src/ts/components/dates/DatePicker.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,32 @@
import { DatePickerInput } from "@mantine/dates";
import { useDebouncedValue, useDidUpdate } from "@mantine/hooks";
import { BoxProps } from "props/box";
import { DashBaseProps, PersistenceProps } from "props/dash";
import { DateInputSharedProps, DatePickerBaseProps } from "props/dates";
import { StylesApiProps } from "props/styles";
import React, { useState } from "react";
import { DatePickerInput } from '@mantine/dates';
import { useDebouncedValue, useDidUpdate, useFocusWithin } from '@mantine/hooks';
import { BoxProps } from 'props/box';
import { DashBaseProps, PersistenceProps } from 'props/dash';
import { DateInputSharedProps, DatePickerBaseProps } from 'props/dates';
import { StylesApiProps } from 'props/styles';
import React, { useState } from 'react';
import {
isDisabled,
stringToDate,
toDates,
toStrings,
} from "../../utils/dates";
} from '../../utils/dates';

interface Props
extends DashBaseProps,
PersistenceProps,
BoxProps,
DateInputSharedProps,
DatePickerBaseProps,
StylesApiProps {
/** Dayjs format to display input value, "MMMM D, YYYY" by default */
interface Props extends DashBaseProps, PersistenceProps, BoxProps, DateInputSharedProps, DatePickerBaseProps, StylesApiProps {
/** Dayjs format to display input value, "MMMM D, YYYY" by default */
valueFormat?: string;
/** Specifies days that should be disabled */
disabledDates?: string[];
/** Determines whether today should be highlighted with a border, false by default */
highlightToday?: boolean;
/** An integer that represents the number of times that this element has been submitted */
n_submit?: number;
/** Debounce time in ms */
debounce?: number;
/**
* (boolean | number; default False): If True, changes to input will be sent back to the Dash server only on enter or when losing focus.
* If it's False, it will send the value back on every change. If a number, it will not send anything back to the Dash server until
* the user has stopped typing for that number of milliseconds.
*/
debounce?: boolean | number;
}

/** DatePicker */
Expand All @@ -35,6 +35,7 @@ const DatePicker = (props: Props) => {
setProps,
loading_state,
n_submit,
type,
value,
debounce,
minDate,
Expand All @@ -47,48 +48,71 @@ const DatePicker = (props: Props) => {
} = props;

const [date, setDate] = useState(toDates(value));
const [debounced] = useDebouncedValue(date, debounce);

const debounceValue = typeof debounce === 'number' ? debounce : 0;
const [debounced] = useDebouncedValue(date, debounceValue);
const { ref, focused } = useFocusWithin();

useDidUpdate(() => {
setProps({ value: toStrings(date) });
if (typeof debounce === 'number' || debounce === false) {
setProps({ value: toStrings(date) });
}
}, [debounced]);

useDidUpdate(() => {
setDate(toDates(value));
// Clears value when X is clicked
if (focused) {
setProps({ value: toStrings(date) });
}
}, [date]);

useDidUpdate(() => {
// If type is multiple or range, sets default value to a list
setDate(type !== 'default' && !value ? [] : toDates(value));
}, [value]);

const handleKeyDown = (ev) => {
if (ev.key === "Enter") {
const handleKeyDown = (ev: React.KeyboardEvent) => {
// Enter key opens calendar, so don't call setProps
if (ev.key === 'Enter') {
setProps({ n_submit: n_submit + 1 });
}
};

const isExcluded = (date: Date) => {
return isDisabled(date, disabledDates || []);
const handleBlur = () => {
// Don't include n_blur counter because onBlur is called when the calendar is opened
if (debounce === true) {
setProps({ value: toStrings(date) });
}
};

const isExcluded = (date: Date) => isDisabled(date, disabledDates || []);

return (
<DatePickerInput
data-dash-is-loading={
(loading_state && loading_state.is_loading) || undefined
}
wrapperProps={{ autoComplete: "off" }}
onKeyDown={handleKeyDown}
onChange={setDate}
value={date}
minDate={stringToDate(minDate)}
maxDate={stringToDate(maxDate)}
excludeDate={isExcluded}
{...others}
/>
<div ref={ref}>
<DatePickerInput
data-dash-is-loading={
(loading_state && loading_state.is_loading) || undefined
}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
onChange={setDate}
value={date}
type={type}
minDate={stringToDate(minDate)}
maxDate={stringToDate(maxDate)}
excludeDate={isExcluded}
{...others}
/>
</div>
);
};

DatePicker.defaultProps = {
persisted_props: ["value"],
persistence_type: "local",
persisted_props: ['value'],
persistence_type: 'local',
debounce: 0,
n_submit: 0,
type: 'default',
};

export default DatePicker;
2 changes: 2 additions & 0 deletions src/ts/components/dates/DateTimePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ interface Props
n_submit?: number;
/** Debounce time in ms */
debounce?: number;
/** Determines whether today should be highlighted with a border, false by default */
highlightToday?: boolean;
}

/** DateTimePicker */
Expand Down
Loading

0 comments on commit 6dcf035

Please sign in to comment.