Skip to content
This repository has been archived by the owner on Feb 1, 2024. It is now read-only.

Commit

Permalink
Merge pull request #2448 from teamleadercrm/FRAF-1154-v4
Browse files Browse the repository at this point in the history
Properly call `onChange` in DatePickerInput
  • Loading branch information
qubis741 authored Nov 21, 2022
2 parents bdf03d4 + e8b97f3 commit 7ec493d
Show file tree
Hide file tree
Showing 8 changed files with 90 additions and 40 deletions.
14 changes: 10 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

### Added

- `MarketingMenuItem`: Added new component ([@lorgan3](https://github.com/lorgan3)) in ([#2452](https://github.com/teamleadercrm/ui/pull/2452))

### Changed

### Deprecated
Expand All @@ -12,10 +10,18 @@

### Fixed

- `NumericInput`: Make sure the stepper `onMouseDown` loops stop when the minimum or maximum value is reached ([@kristofcolpaert](https://github.com/kristofcolpaert)) in ([#2451](https://github.com/teamleadercrm/ui/pull/2451))

### Dependency updates

## [17.0.3] - 2022-11-21

### Added

- `MarketingMenuItem`: Added new component ([@lorgan3](https://github.com/lorgan3)) in ([#2452](https://github.com/teamleadercrm/ui/pull/2452))
### Fixed

- `NumericInput`: Make sure the stepper `onMouseDown` loops stop when the minimum or maximum value is reached ([@kristofcolpaert](https://github.com/kristofcolpaert)) in ([#2451](https://github.com/teamleadercrm/ui/pull/2451))
- `DatePickerInput`: overall functionality ([@qubis741](https://github.com/qubis741)) in [#2448](https://github.com/teamleadercrm/ui/pull/2448))

## [17.0.2] - 2022-11-16

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@teamleader/ui",
"description": "Teamleader UI library",
"version": "17.0.2",
"version": "17.0.3",
"author": "Teamleader <development@teamleader.eu>",
"bugs": {
"url": "https://github.com/teamleadercrm/ui/issues"
Expand Down
2 changes: 1 addition & 1 deletion src/components/datepicker/DatePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ const DatePicker: GenericComponent<DatePickerProps> = ({
);

return (
<Box {...pickBoxProps(others)}>
<Box {...pickBoxProps(others)} data-teamleader-ui="date-picker">
<DayPicker
{...others}
localeUtils={localeUtils}
Expand Down
88 changes: 67 additions & 21 deletions src/components/datepicker/DatePickerInput.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { IconCalendarSmallOutline, IconCloseBadgedSmallFilled } from '@teamleader/ui-icons';
import React, { ReactNode, useEffect, useState } from 'react';
import React, { ReactNode, useCallback, useEffect, useState } from 'react';
import { DayPickerProps as ReactDayPickerProps, Modifier } from 'react-day-picker';
import DatePicker from '.';
import { SIZES } from '../../constants';
Expand All @@ -10,7 +10,7 @@ import Input from '../input';
import { InputProps } from '../input/Input';
import Popover from '../popover';
import { PopoverProps } from '../popover/Popover';
import { formatDate, parseMultiFormatsDate } from './localeUtils';
import { formatDate, isValidDate, parseMultiFormatsDate } from './localeUtils';
import theme from './theme.css';
import { isAllowedDate } from './utils';

Expand All @@ -33,13 +33,13 @@ export interface DatePickerInputProps<IsTypeable extends boolean = true> extends
/** The language ISO locale code ('en-GB', 'nl-BE', 'fr-FR',...). */
locale?: string;
/** Callback function that is fired when the date has changed. */
onChange?: (selectedDate: Date | undefined) => void;
onChange?: (selectedDate: IsTypeable extends true ? Date | string | undefined : Date | undefined) => void;
/** Callback function that is fired when the popover with the calendar gets closed (unfocused) */
onBlur?: () => void;
/** Object with props for the Popover component. */
popoverProps?: PopoverProps;
/** The current selected date. */
selectedDate?: Date;
/** The current selected value. */
selectedDate?: IsTypeable extends true ? Date | string : Date;
/** Size of the Input & DatePicker components. */
size?: Exclude<typeof SIZES[number], 'tiny' | 'smallest' | 'hero' | 'fullscreen'>;
/** Overridable size of the Input component. */
Expand Down Expand Up @@ -83,6 +83,7 @@ function DatePickerInput<IsTypeable extends boolean = true>({
onBlur,
typeable = true as IsTypeable,
errorText,
selectedDate: preselectedDate,
...others
}: DatePickerInputProps<IsTypeable>) {
const getFormattedDateString = (date: Date) => {
Expand All @@ -96,15 +97,68 @@ function DatePickerInput<IsTypeable extends boolean = true>({

return customFormatDate(date, locale);
};

const [isPopoverActive, setIsPopoverActive] = useState(false);
const [popoverAnchorEl, setPopoverAnchorEl] = useState<Element | null>(null);
const [selectedDate, setSelectedDate] = useState<Date | undefined>(others.selectedDate);
const [selectedDate, setSelectedDate] = useState<Date | undefined>(
preselectedDate ? new Date(preselectedDate) : undefined,
);
const [displayError, setDisplayError] = useState(false);
const [inputValue, setInputValue] = useState(others.selectedDate ? getFormattedDateString(others.selectedDate) : '');
const [inputValue, setInputValue] = useState<string>(
preselectedDate ? (isValidDate(preselectedDate) ? getFormattedDateString(preselectedDate) : preselectedDate) : '',
);

const handleInputValueChange = (value: string) => {
setDisplayError(false);
setInputValue(value);
};
// Special handling of closing popover, where we also call onChange prop to ensure proper value is in argument
const closePopover = useCallback(
(value: Date | undefined | false) => {
setIsPopoverActive(false);
// Date - on day click
if (value) {
onChange && onChange(value);
return;
}
// Clear click
if (value === undefined) {
onChange && onChange(undefined);
return;
}
// Blurred from input, not focused on datepicker
if (value === false) {
if (typeable && !customFormatDate && inputValue) {
const date = parseMultiFormatsDate(inputValue, ALLOWED_DATE_FORMATS, locale);
if (date && isAllowedDate(date, dayPickerProps?.disabledDays)) {
onChange && onChange(date);
} else {
// Conditional typing of arguments somehow doesn't work inside of component
// @ts-ignore
onChange && onChange(inputValue);
}
} else {
onChange && onChange(selectedDate);
}
}
},
[inputValue, selectedDate],
);
useEffect(() => {
if (!preselectedDate) {
handleInputValueChange('');
setSelectedDate(undefined);
/*
** Preselected date can be invalid (or string), when typed date is invalid and it's value is passed in prop. For that case
** we need to check it here and set selectedDate and inputValue accordingly
*/
} else if (isValidDate(preselectedDate)) {
handleInputValueChange(getFormattedDateString(preselectedDate));
setSelectedDate(preselectedDate);
} else {
setSelectedDate(undefined);
}
}, [preselectedDate]);

const handleInputFocus = (event: React.FocusEvent<HTMLElement>) => {
if (inputProps?.readOnly) {
Expand Down Expand Up @@ -143,7 +197,6 @@ function DatePickerInput<IsTypeable extends boolean = true>({
const date = parseMultiFormatsDate(inputValue, ALLOWED_DATE_FORMATS, locale);
if (date && isAllowedDate(date, dayPickerProps?.disabledDays)) {
handleInputValueChange(getFormattedDateString(date));
onChange && onChange(date);
} else {
setDisplayError(true);
}
Expand All @@ -152,14 +205,13 @@ function DatePickerInput<IsTypeable extends boolean = true>({

const handlePopoverClose = () => {
onBlur && onBlur();
setIsPopoverActive(false);
closePopover(false);
};

const handleDatePickerDateChange = (date: Date) => {
setIsPopoverActive(false);
closePopover(date);
setSelectedDate(date);
handleInputValueChange(getFormattedDateString(date));
onChange && onChange(date);
};

const renderIcon = () => {
Expand All @@ -173,10 +225,8 @@ function DatePickerInput<IsTypeable extends boolean = true>({
const handleClear = (event: MouseEvent) => {
// Prevents opening datepicker on clicking of this
event.preventDefault();
setIsPopoverActive(false);
setSelectedDate(undefined);
closePopover(undefined);
handleInputValueChange('');
onChange && onChange(undefined);
};

const renderClearIcon = () => {
Expand All @@ -192,12 +242,8 @@ function DatePickerInput<IsTypeable extends boolean = true>({
) : null;
};

useEffect(() => {
setSelectedDate(others.selectedDate);
}, [others.selectedDate]);

const boxProps = pickBoxProps(others);
const inputError = displayError ? errorText || true : false;
const internalError = displayError ? errorText || true : false;
return (
<Box className={className} {...boxProps}>
<Input
Expand All @@ -207,8 +253,8 @@ function DatePickerInput<IsTypeable extends boolean = true>({
size={inputSize || size}
width="120px"
noInputStyling={!typeable}
error={inputError}
{...inputProps}
error={inputProps?.error || internalError}
onClick={handleInputClick}
onFocus={handleInputFocus}
onBlur={handleInputBlur}
Expand All @@ -233,7 +279,7 @@ function DatePickerInput<IsTypeable extends boolean = true>({
<DatePicker
className={theme[`is-${datePickerSize || size}`]}
onChange={handleDatePickerDateChange}
selectedDate={selectedDate as Date}
selectedDate={selectedDate}
size={datePickerSize || size}
{...dayPickerProps}
/>
Expand Down
6 changes: 3 additions & 3 deletions src/components/datepicker/datePickerInput.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ export default {
} as ComponentMeta<typeof DatePickerInput>;

export const defaultStory: ComponentStory<typeof DatePickerInput> = (args) => {
const handleOnChange = (selectedDate: Date | undefined) => {
const handleOnChange = (selectedDate: Date | undefined | string) => {
console.log('Selected date', selectedDate);
};

return <DatePickerInput onChange={handleOnChange} {...args} />;
};

export const clearableInputSingleDate: ComponentStory<typeof DatePickerInput> = () => {
const handleOnChange = (selectedDate: Date | undefined) => {
const handleOnChange = (selectedDate: Date | undefined | string) => {
console.log('Selected date', selectedDate);
};

Expand Down Expand Up @@ -83,7 +83,7 @@ export const inputSingleDateWithCustomFormat: ComponentStory<typeof DatePickerIn
};

export const inputSingleDateWithoutTyping: ComponentStory<typeof DatePickerInput> = () => {
const handleOnChange = (selectedDate: Date | undefined) => {
const handleOnChange = (selectedDate: Date | undefined | string) => {
console.log('Selected date', selectedDate);
};

Expand Down
2 changes: 2 additions & 0 deletions src/components/datepicker/localeUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,5 @@ export default {
getMonths,
parseDate,
};

export const isValidDate = (date: any): date is Date => DateTime.fromJSDate(date).isValid;
8 changes: 1 addition & 7 deletions src/components/select/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ import ReactSelect, {
Props,
StylesConfig,
ValueContainerProps,
components as ReactSelectComponents,
InputProps,
} from 'react-select';
import ReactCreatableSelect from 'react-select/creatable';
import SelectType from 'react-select/dist/declarations/src/Select';
Expand Down Expand Up @@ -130,10 +128,7 @@ const ClearIndicator = <Option extends OptionType, IsMulti extends boolean>(
</Icon>
);
};
// For setting data attribute that is detected in `useFocusTrap`
const Input = <Option extends OptionType, IsMulti extends boolean>(inputProps: InputProps<Option, IsMulti>) => {
return <ReactSelectComponents.Input data-is-select="true" {...inputProps} />;
};

export const selectOverlayNode = document.createElement('div');
selectOverlayNode.setAttribute('data-teamleader-ui', 'select-overlay');

Expand Down Expand Up @@ -502,7 +497,6 @@ function Select<Option extends OptionType, IsMulti extends boolean, IsClearable
ClearIndicator,
DropdownIndicator,
IndicatorSeparator: null,
Input,
...components,
}}
hideSelectedOptions={false}
Expand Down
8 changes: 5 additions & 3 deletions src/utils/useFocusTrap/useFocusTrap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,14 @@ const useFocusTrap = ({
const eventTarget = event.target as HTMLElement;
// If focus is called on CkeEditor Element (for example Link dialog), we want to keep focus there, not back in our dialog
const isCkEditorElement = eventTarget.className.includes('cke');

/**
** If focus is called on Select input inside focus trap but not directly (for example DatePickerInput(typeable) Monthly picker),
** If focus is called on DatePicker inside focus trap but not directly (for example DatePickerInput(typeable) Monthly picker, Year Picker),
** we want to keep focus there, not back in our dialog
*/
const isSelect = eventTarget.dataset.isSelect;
if (!isCkEditorElement && !isSelect && !currentFocusRef.contains(eventTarget)) {
const isDatePickerElement = !!eventTarget.closest('[data-teamleader-ui="date-picker"]');
const isException = [isCkEditorElement, isDatePickerElement].some(Boolean);
if (!isException && !currentFocusRef.contains(eventTarget)) {
if (document.activeElement === event.target) {
if (event.target === topFocusBumperRef.current) {
// Reset the focus to the last element when focusing in reverse (shift-tab)
Expand Down

0 comments on commit 7ec493d

Please sign in to comment.