Skip to content

Commit

Permalink
fix: datepicker opening issue on safari
Browse files Browse the repository at this point in the history
affects: @medly-components/core, @medly-components/forms
  • Loading branch information
gmukul01 committed Nov 10, 2024
1 parent 270dab5 commit 9161dc9
Show file tree
Hide file tree
Showing 8 changed files with 2,655 additions and 2,777 deletions.
26 changes: 11 additions & 15 deletions packages/core/src/components/DatePicker/DatePicker.styled.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
/* stylelint-disable property-no-unknown */
import { InjectClassName, Omit, WithThemeProp } from '@medly-components/utils';
import { InjectClassName, WithThemeProp } from '@medly-components/utils';
import styled, { css } from 'styled-components';
import Calendar from '../Calendar';
import { getPosition } from '../Popover/Popup/styled/Popup.styled';
import Popover from '../Popover';
import { InnerWrapper, InputWrapper, OuterWrapper } from '../TextField/Styled';
import { StyleProps } from './types';

type State = 'default' | 'active' | 'error' | 'disabled';

const getStyleForIcon = ({ theme, variant, disabled }: Omit<StyleProps, 'placement'> & WithThemeProp, state: State) => {
const getStyleForIcon = ({ theme, variant, disabled }: StyleProps & WithThemeProp, state: State) => {
const {
icon: { [state]: iconStyle }
} = theme.datePicker[variant];
Expand All @@ -25,7 +24,7 @@ const getStyleForIcon = ({ theme, variant, disabled }: Omit<StyleProps, 'placeme
`;
};

export const DateIconWrapper = styled(InjectClassName)<Omit<StyleProps, 'placement'>>`
export const DateIconWrapper = styled(InjectClassName)<StyleProps>`
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
border-radius: ${({ theme }) => theme.datePicker.borderRadius};
padding: ${({ size }) => (size === 'S' ? '0.6rem' : '0.8rem')};
Expand All @@ -51,22 +50,19 @@ const hiddenInputStyle = css<StyleProps>`
}
`;

export const Wrapper = styled(OuterWrapper)<StyleProps>`
export const Wrapper = styled(Popover)<StyleProps>`
display: ${({ fullWidth }) => (fullWidth ? 'flex' : 'inline-flex')};
width: ${({ fullWidth }) => (fullWidth ? '100%' : 'min-content')};
min-width: ${({ minWidth }) => minWidth};
margin: ${({ theme, fullWidth }) =>
fullWidth ? `${theme.spacing.S2} 0` : `${theme.spacing.S2} ${theme.spacing.S2} ${theme.spacing.S2} 0`};
& > ${OuterWrapper} {
margin: 0;
& > ${InnerWrapper} {
padding-right: 0.8rem;
}
}
${Calendar.Style} {
z-index: 4;
position: absolute;
${getPosition}
top: ${({ size, placement, theme }) =>
(placement === 'bottom' || placement === 'bottom-start' || placement === 'bottom-end') &&
(size === 'S' ? theme.textField.height.S : theme.textField.height.M)};
}
${({ hideInput }) => hideInput && hiddenInputStyle};
`;
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ describe('DatePicker component', () => {
it('should change the size of datepicker based on size prop', () => {
const { container } = render(<DatePicker id="dob" value={null} displayFormat="MM/dd/yyyy" onChange={jest.fn()} size={'S'} />);
fireEvent.click(screen.getByTitle('dob-calendar-icon'));
expect(container.querySelector('#dob-calendar')).toHaveStyle(`top: 4rem`);
expect(container.querySelector('#dob-input-wrapper>div')).toHaveStyle(`height: 4rem`);
});
});
});
211 changes: 32 additions & 179 deletions packages/core/src/components/DatePicker/DatePicker.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,27 @@
import { DateRangeIcon } from '@medly-components/icons';
import {
getFormattedDate,
parseToDate,
useCombinedRefs,
useOuterClickNotifier,
useRunAfterUpdate,
WithStyle
} from '@medly-components/utils';
import { format } from 'date-fns';
import { parseToDate, useCombinedRefs, WithStyle } from '@medly-components/utils';
import type { FC } from 'react';
import React, { forwardRef, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import React, { forwardRef, memo, useCallback, useMemo, useRef, useState } from 'react';
import Calendar from '../Calendar';
import TextField from '../TextField';
import { DateIconWrapper, Wrapper } from './DatePicker.styled';
import datePickerPattern from './datePickerPattern';
import Popover from '../Popover';
import { Wrapper } from './DatePicker.styled';
import { DatePickerTextField } from './DatePickerTextField';
import { DatePickerProps } from './types';

const Component: FC<DatePickerProps> = memo(
forwardRef((props, ref) => {
const {
value,
onChange,
size,
displayFormat,
fullWidth,
minWidth,
required,
disabled,
showDecorators,
errorText,
className,
validator,
popoverPlacement,
minSelectableDate,
maxSelectableDate,
showCalendarIcon,
calendarIconPosition,
defaultMonth,
defaultYear,
hideInput,
Expand All @@ -56,181 +42,48 @@ const Component: FC<DatePickerProps> = memo(
[value, displayFormat]
);

const wrapperRef = useRef<HTMLDivElement>(null),
inputRef = useCombinedRefs<HTMLInputElement>(ref, useRef(null)),
runAfterUpdate = useRunAfterUpdate(),
[textValue, setTextValue] = useState(''),
[isFocused, setFocusedState] = useState(false),
const inputRef = useCombinedRefs<HTMLInputElement>(ref, useRef(null)),
[builtInErrorMessage, setErrorMessage] = useState(''),
[showCalendar, toggleCalendar] = useState(false),
[active, setActive] = useState(false),
isErrorPresent = useMemo(() => !!errorText || !!builtInErrorMessage, [errorText, builtInErrorMessage]),
mask = displayFormat!.replace(new RegExp('\\/|\\-', 'g'), ' $& ').toUpperCase();
isErrorPresent = useMemo(() => !!errorText || !!builtInErrorMessage, [errorText, builtInErrorMessage]);

useEffect(() => {
if (date) {
const cursor = inputRef.current?.selectionStart || 0;
setTextValue(format(date, displayFormat!).replace(new RegExp('\\/|\\-', 'g'), ' $& '));
runAfterUpdate(() => inputRef.current?.setSelectionRange(cursor, cursor));
} else if (!isErrorPresent && !isFocused) {
setTextValue('');
}
}, [date, isFocused, isErrorPresent, displayFormat]);
const onTextChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const inputValue = event.target.value || '',
cursor = event.target.selectionStart || 0,
parsedDate = parseToDate(inputValue, displayFormat!),
isValidDate = parsedDate?.toString() !== 'Invalid Date';

onChange(isValidDate ? (onChangeFormatter ? onChangeFormatter(parsedDate) : parsedDate) : null);

const breakdown = getFormattedDate(inputValue, displayFormat!);
if (breakdown) {
setTextValue(breakdown);
event.target.maxLength = mask!.length;
} else {
setTextValue(inputValue);
event.target.maxLength = mask!.length + 1;
}

runAfterUpdate(() => event.target?.setSelectionRange(cursor, cursor));
isValidDate && validate(event);
},
[displayFormat, onChange]
),
onIconClick = useCallback(
event => {
event.stopPropagation();
if (!disabled) {
toggleCalendar(val => !val);
setActive(true);
inputRef.current?.focus();
}
},
[disabled]
),
validate = useCallback(
(event: React.ChangeEvent<HTMLInputElement>, eventFunc?: any) => {
const inputValue = inputRef.current?.value,
parsedDate = inputValue && parseToDate(inputValue, displayFormat!),
isValidDate = parsedDate?.toString() !== 'Invalid Date',
emptyDateMessage = props.required && !inputValue && 'Please fill in this field',
invalidDateRangeMessage =
(parsedDate! < minSelectableDate! || parsedDate! > maxSelectableDate!) &&
'Please select date from allowed range',
invalidDateMessage = inputValue && !isValidDate && 'Please enter a valid date',
validatorMessage = validator && validator(parsedDate || null, event),
message = validatorMessage || emptyDateMessage || invalidDateRangeMessage || invalidDateMessage || '';

setErrorMessage(message);
inputRef.current?.setCustomValidity(message);
eventFunc && eventFunc(event);
},
[props.required, displayFormat, validator, minSelectableDate, maxSelectableDate]
),
onBlur = useCallback(
(event: React.FocusEvent<HTMLInputElement>) => {
setFocusedState(false);
inputRef.current?.value && validate(event, props.onBlur);
},
[props.onBlur, displayFormat]
),
onInvalid = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => validate(event, props.onInvalid),
[props.onInvalid, displayFormat]
),
onFocus = useCallback(
(event: React.FocusEvent<HTMLInputElement>) => {
setActive(true);
setFocusedState(true);
props.onFocus && props.onFocus(event);
},
[props.onFocus]
),
onDateChange = useCallback(
(dt: Date, e: React.MouseEvent<HTMLButtonElement>) => {
onChange(onChangeFormatter ? onChangeFormatter(dt) : dt);
toggleCalendar(false);
setErrorMessage('');
setActive(false);
setErrorMessage((validator && validator(dt, e)) || '');
},
[onChange]
),
inputValidator = useCallback(() => '', []),
onKeyPress = useCallback(
(event: React.KeyboardEvent) => {
const regex = displayFormat?.includes('/') ? /[0-9/]+/g : /[0-9-]+/g;
if (!regex.test(event.key)) {
event.preventDefault();
}
},
[displayFormat]
);

useOuterClickNotifier((event: any) => {
setActive(false);
toggleCalendar(false);
if (active) {
validate(event, props.onBlur);
props.onBlur && props.onBlur(event);
}
}, wrapperRef);

const dateIconEl = () => (
<DateIconWrapper variant={restProps.variant!} isErrorPresent={isErrorPresent} isActive={active} disabled={disabled} size={size}>
<DateRangeIcon id={`${id}-calendar-icon`} title={`${id}-calendar-icon`} onClick={onIconClick} size={size} />
</DateIconWrapper>
const onDateChange = useCallback(
(dt: Date, e: React.MouseEvent<HTMLButtonElement>) => {
onChange(onChangeFormatter ? onChangeFormatter(dt) : dt);
setErrorMessage('');
setErrorMessage((validator && validator(dt, e)) || '');
},
[onChange]
);

return (
<Wrapper
id={`${id}-datepicker-wrapper`}
ref={wrapperRef}
fullWidth={fullWidth}
minWidth={minWidth}
size={size}
className={className}
variant={restProps.variant!}
placement={popoverPlacement!}
hideInput={hideInput}
interactionType="click"
>
<TextField
errorText={errorText || builtInErrorMessage}
id={id}
<DatePickerTextField
{...props}
ref={inputRef}
required={required}
{...(showCalendarIcon && (calendarIconPosition === 'left' ? { prefix: dateIconEl } : { suffix: dateIconEl }))}
fullWidth
mask={mask}
pattern={datePickerPattern[displayFormat!]}
size={size}
disabled={disabled}
showDecorators={showDecorators}
value={textValue}
onChange={onTextChange}
validator={inputValidator}
onKeyPress={onKeyPress}
maxLength={mask!.length}
{...{ ...restProps, onBlur, onFocus, minWidth, onInvalid }}
builtInErrorMessage={builtInErrorMessage}
setErrorMessage={setErrorMessage}
/>

{showCalendar && (
<Calendar
id={`${id}-calendar`}
date={date}
isErrorPresent={isErrorPresent}
onClose={() => {
setActive(false);
toggleCalendar(false);
}}
onChange={onDateChange}
defaultMonth={defaultMonth}
defaultYear={defaultYear}
minSelectableDate={minSelectableDate!}
maxSelectableDate={maxSelectableDate!}
/>
{!disabled && (
<Popover.Popup placement={popoverPlacement}>
<Calendar
id={`${id}-calendar`}
date={date}
isErrorPresent={isErrorPresent}
onChange={onDateChange}
defaultMonth={defaultMonth}
defaultYear={defaultYear}
minSelectableDate={minSelectableDate!}
maxSelectableDate={maxSelectableDate!}
/>
</Popover.Popup>
)}
</Wrapper>
);
Expand Down
Loading

0 comments on commit 9161dc9

Please sign in to comment.