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 #2441 from teamleadercrm/FRAF-1154-v2
Browse files Browse the repository at this point in the history
Fix DatePickerInput inability to click on MonthPicker in Dialog
  • Loading branch information
qubis741 authored Nov 16, 2022
2 parents b1671d0 + 3078d6e commit 6730a4b
Show file tree
Hide file tree
Showing 8 changed files with 123 additions and 20 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@

### Dependency updates

## [17.0.0] - 2022-11-15

### Added

- `DatePickerInput`: restriction to use `formatDate` if `typeable=true` ([@qubis741](https://github.com/qubis741)) in [#2441](https://github.com/teamleadercrm/ui/pull/2441))

### Fixed

- `DatePickerInput`: inability to click on `MonthPicker` while inside `Dialog` ([@qubis741](https://github.com/qubis741)) in [#2441](https://github.com/teamleadercrm/ui/pull/2441))

## [16.5.0] - 2022-11-10

### Added
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": "16.5.0",
"version": "17.0.0",
"author": "Teamleader <[email protected]>",
"bugs": {
"url": "https://github.com/teamleadercrm/ui/issues"
Expand Down
26 changes: 14 additions & 12 deletions src/components/datepicker/DatePickerInput.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { IconCalendarSmallOutline, IconCloseBadgedSmallFilled } from '@teamleader/ui-icons';
import React, { ReactNode, useEffect, useState } from 'react';
import { DayPickerProps as ReactDayPickerProps } from 'react-day-picker';
import { DayPickerProps as ReactDayPickerProps, Modifier } from 'react-day-picker';
import DatePicker from '.';
import { GenericComponent } from '../../@types/types';
import { SIZES } from '../../constants';
import Box, { pickBoxProps } from '../box';
import { BoxProps } from '../box/Box';
Expand All @@ -13,19 +12,20 @@ import Popover from '../popover';
import { PopoverProps } from '../popover/Popover';
import { formatDate, parseMultiFormatsDate } from './localeUtils';
import theme from './theme.css';
import { isAllowedDate } from './utils';

const DEFAULT_FORMAT = 'dd/MM/yyyy';
const ALLOWED_DATE_FORMATS = [DEFAULT_FORMAT, 'd/M/yyyy', 'dd.MM.yyyy', 'd.M.yyyy', 'dd-MM-yyyy', 'd-M-yyyy'];

export interface DatePickerInputProps extends Omit<BoxProps, 'size' | 'onChange'> {
export interface DatePickerInputProps<IsTypeable extends boolean = true> extends Omit<BoxProps, 'size' | 'onChange'> {
/** A class name for the wrapper to give custom styles. */
className?: string;
/** Object with props for the DatePicker component. */
dayPickerProps?: DayPickerProps;
/** A footer component, rendered at the bottom of the date picker */
footer?: ReactNode;
/** A custom function to format a date if input is not typeable */
formatDate?: (selectedDate: Date, locale: string) => string;
formatDate?: IsTypeable extends true ? never : (selectedDate: Date, locale: string) => string;
/** Object with props for the Input component. */
inputProps?: InputProps;
/** If true, component will be rendered in inverse mode. */
Expand All @@ -51,19 +51,21 @@ export interface DatePickerInputProps extends Omit<BoxProps, 'size' | 'onChange'
/** Whether the input should have button for value clearing. False by default. */
clearable?: boolean;
/** Whether user is able to type into the input. True by default. */
typeable?: boolean;
typeable?: IsTypeable;
/** Error text that is displayed when typed date is invalid. */
errorText?: string;
}

interface DayPickerProps extends Omit<ReactDayPickerProps, 'modifiers'> {
export type AllowedDisabledDays = Modifier | Date[];
interface DayPickerProps extends Omit<ReactDayPickerProps, 'modifiers' | 'disabledDays'> {
numberOfMonths?: number;
showOutsideDays?: boolean;
showWeekNumbers?: boolean;
withMonthPicker?: boolean;
disabledDays?: AllowedDisabledDays;
}

const DatePickerInput: GenericComponent<DatePickerInputProps> = ({
function DatePickerInput<IsTypeable extends boolean = true>({
className,
dayPickerProps,
footer,
Expand All @@ -79,10 +81,10 @@ const DatePickerInput: GenericComponent<DatePickerInputProps> = ({
clearable = false,
onChange,
onBlur,
typeable = true,
typeable = true as IsTypeable,
errorText,
...others
}) => {
}: DatePickerInputProps<IsTypeable>) {
const getFormattedDateString = (date: Date) => {
if (!date) {
return '';
Expand Down Expand Up @@ -129,7 +131,7 @@ const DatePickerInput: GenericComponent<DatePickerInputProps> = ({
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const value = event.target.value;
const date = parseMultiFormatsDate(value, ALLOWED_DATE_FORMATS, locale);
if (date) {
if (date && isAllowedDate(date, dayPickerProps?.disabledDays)) {
setSelectedDate(date);
}
handleInputValueChange(value);
Expand All @@ -139,7 +141,7 @@ const DatePickerInput: GenericComponent<DatePickerInputProps> = ({
inputProps?.onBlur && inputProps.onBlur(event);
if (typeable && !customFormatDate && inputValue) {
const date = parseMultiFormatsDate(inputValue, ALLOWED_DATE_FORMATS, locale);
if (date) {
if (date && isAllowedDate(date, dayPickerProps?.disabledDays)) {
handleInputValueChange(getFormattedDateString(date));
} else {
setDisplayError(true);
Expand Down Expand Up @@ -254,6 +256,6 @@ const DatePickerInput: GenericComponent<DatePickerInputProps> = ({
</Popover>
</Box>
);
};
}

export default DatePickerInput;
30 changes: 30 additions & 0 deletions src/components/datepicker/__tests__/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// @todo uncomment when jest is added
// const testDate = new Date(2022, 10, 15);
// describe('DatePicker utils', () => {
// describe('isAllowedDate', () => {
// test.each([
// { disabledDays: new Date(2022, 10, 16), output: true },
// { disabledDays: new Date(2022, 10, 15), output: false },
// { disabledDays: [new Date(2022, 10, 16), new Date(2022, 10, 14)], output: true },
// { disabledDays: [new Date(2022, 10, 16), new Date(2022, 10, 15)], output: false },
// { disabledDays: [{incorrect: 'value'}], output: true },
// { disabledDays: [new Date(2022, 10, 16), {incorrect: 'value'}], output: true },
// { disabledDays: { from: new Date(2022, 10, 15), to: null }, output: true },
// { disabledDays: { from: null, to: new Date(2022, 10, 15) }, output: true },
// { disabledDays: { from: new Date(2022, 10, 16), to: new Date(2022, 10, 20) }, output: true },
// { disabledDays: { from: new Date(2022, 10, 14), to: new Date(2022, 10, 15) }, output: false },
// { disabledDays: { before: new Date(2022, 10, 14) }, output: true },
// { disabledDays: { before: new Date(2022, 10, 16) }, output: false },
// { disabledDays: { after: new Date(2022, 10, 16) }, output: true },
// { disabledDays: { after: new Date(2022, 10, 14) }, output: false },
// { disabledDays: { before: new Date(2022, 10, 14), after: new Date(2022, 10, 16) }, output: true },
// { disabledDays: { before: new Date(2022, 10, 16), after: new Date(2022, 10, 17) }, output: false },
// { disabledDays: { daysOfWeek: [1, 3, 4] }, output: true },
// { disabledDays: { daysOfWeek: [1, 2, 3, 4] }, output: false },
// { disabledDays: (_date: Date) => false, output: true },
// { disabledDays: (_date: Date) => true, output: false },
// ])('works with disabled days: `$disabledDays`', ({ disabledDays, output }) => {
// expect(isAllowedDate(testDate, disabledDays)).toEqual(output);
// });
// });
// });
5 changes: 2 additions & 3 deletions src/components/datepicker/datePickerInput.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const clearableInputSingleDate: ComponentStory<typeof DatePickerInput> =
numberOfMonths: 1,
showOutsideDays: true,
showWeekNumbers: true,
withMonthPicker: false,
withMonthPicker: true,
}}
inputProps={{
helpText: 'Pick a date',
Expand Down Expand Up @@ -75,9 +75,9 @@ export const inputSingleDateWithCustomFormat: ComponentStory<typeof DatePickerIn
locale="nl-BE"
size="medium"
formatDate={customFormatDate}
typeable={false}
onChange={handleOnChange}
selectedDate={preSelectedDate}
typeable={false}
/>
);
};
Expand All @@ -103,7 +103,6 @@ export const inputSingleDateWithoutTyping: ComponentStory<typeof DatePickerInput
size="medium"
onChange={handleOnChange}
selectedDate={preSelectedDate}
typeable={false}
/>
);
};
53 changes: 52 additions & 1 deletion src/components/datepicker/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { DateUtils, DayModifiers } from 'react-day-picker';
import { BeforeAfterModifier, DateUtils, DayModifiers } from 'react-day-picker';
import { AllowedDisabledDays } from './DatePickerInput';

export const convertModifiersToClassnames = (modifiers: DayModifiers, theme: Record<string, string>) => {
if (!modifiers) {
Expand All @@ -18,3 +19,53 @@ export const isSelectingFirstDay = (from: Date, to: Date, day: Date) => {
const isRangeSelected = from && to;
return !from || isBeforeFirstDay || isRangeSelected;
};

const getDateTimeAtStartOfDay = (date: Date) => {
date.setHours(0, 0, 0, 0);
return date.getTime();
};
export const isAllowedDate = (date: Date, disabledDays?: AllowedDisabledDays) => {
if (!disabledDays) {
return true;
}
const dateTime = getDateTimeAtStartOfDay(date);
if (disabledDays instanceof Date) {
return dateTime !== getDateTimeAtStartOfDay(disabledDays);
}
if (Array.isArray(disabledDays) && disabledDays.length > 0 && disabledDays[0] instanceof Date) {
return !disabledDays.some((disabledDay) => {
return disabledDay instanceof Date ? dateTime === getDateTimeAtStartOfDay(disabledDay) : false;
});
}
if ('from' in disabledDays && disabledDays.from && 'to' in disabledDays && disabledDays.to) {
return !(
getDateTimeAtStartOfDay(disabledDays.from) <= dateTime && dateTime <= getDateTimeAtStartOfDay(disabledDays.to)
);
}
if ('before' in disabledDays && disabledDays.before && !('after' in disabledDays)) {
return getDateTimeAtStartOfDay(disabledDays.before) <= dateTime;
}
if ('after' in disabledDays && disabledDays.after && !('before' in disabledDays)) {
return getDateTimeAtStartOfDay(disabledDays.after) >= dateTime;
}
const disabledDaysBeforeAfter = disabledDays as BeforeAfterModifier;
if (
'before' in disabledDaysBeforeAfter &&
'after' in disabledDaysBeforeAfter &&
disabledDaysBeforeAfter.before &&
disabledDaysBeforeAfter.after
) {
return (
getDateTimeAtStartOfDay(disabledDaysBeforeAfter.before) <= dateTime &&
dateTime <= getDateTimeAtStartOfDay(disabledDaysBeforeAfter.after)
);
}
if ('daysOfWeek' in disabledDays && Array.isArray(disabledDays.daysOfWeek)) {
return !disabledDays.daysOfWeek.includes(date.getDay());
}
if (typeof disabledDays === 'function') {
return !disabledDays(date);
}

return true;
};
8 changes: 7 additions & 1 deletion src/components/select/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ 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 @@ -125,7 +127,10 @@ 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 @@ -466,6 +471,7 @@ function Select<Option extends OptionType, IsMulti extends boolean, IsClearable
ClearIndicator,
DropdownIndicator,
IndicatorSeparator: null,
Input,
...components,
}}
hideSelectedOptions={false}
Expand Down
9 changes: 7 additions & 2 deletions src/utils/useFocusTrap/useFocusTrap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,15 @@ const useFocusTrap = ({
}

const trapFocus: EventListener = (event) => {
const eventTarget = event.target as Element;
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 (!isCkEditorElement && !currentFocusRef.contains(eventTarget)) {
/**
** If focus is called on Select input inside focus trap but not directly (for example DatePickerInput(typeable) Monthly picker),
** we want to keep focus there, not back in our dialog
*/
const isSelect = eventTarget.dataset.isSelect;
if (!isCkEditorElement && !isSelect && !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 6730a4b

Please sign in to comment.