Skip to content

Commit

Permalink
Merge pull request #642 from sebgroup/develop
Browse files Browse the repository at this point in the history
release new beta
  • Loading branch information
kherP authored Jul 30, 2021
2 parents 3a8bc83 + b54a108 commit 47b6c4e
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 59 deletions.
26 changes: 26 additions & 0 deletions lib/src/Datepicker/Datepicker.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ describe("Component: Datepicker", () => {
beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
jest.clearAllMocks();
});

afterEach(() => {
Expand Down Expand Up @@ -95,34 +96,55 @@ describe("Component: Datepicker", () => {

it("Should fire change event with null when component value is out of range and with latest value when in range", () => {
const [min, max]: [Date, Date] = [new Date(props.value.getFullYear() - 10, 1, 1), new Date(props.value.getFullYear() + 10, 1, 1)];
const year: number = props.value.getFullYear();
act(() => {
render(<Datepicker {...props} min={max} />, container);
});
const yearElement: HTMLInputElement = container.querySelector("input");
act(() => {
Simulate.change(yearElement, { target: { value: new Date(year, 1, 1) } } as any);
});

expect(props.onChange).toHaveBeenCalledWith(null);

act(() => {
render(<Datepicker {...props} max={min} />, container);
});

act(() => {
Simulate.change(yearElement, { target: { value: new Date(year + 11, 1, 1) } } as any);
});

expect(props.onChange).toHaveBeenCalledWith(null);

act(() => {
render(<Datepicker {...props} min={min} max={min} />, container);
});

act(() => {
Simulate.change(yearElement, { target: { value: new Date(year, 1, 1) } } as any);
});

expect(props.onChange).toHaveBeenCalledWith(null);

act(() => {
render(<Datepicker {...props} min={max} max={max} />, container);
});

act(() => {
Simulate.change(yearElement, { target: { value: new Date(year, 1, 1) } } as any);
});

expect(props.onChange).toHaveBeenCalledWith(null);

act(() => {
render(<Datepicker {...props} min={min} max={max} />, container);
});

act(() => {
Simulate.change(yearElement, { target: { value: new Date(year, 1, 1) } } as any);
});

expect(props.onChange).toHaveBeenCalled();
const tzoffset: number = new Date().getTimezoneOffset() * 60000;
const expectedDate: string = new Date(Date.now() - tzoffset).toISOString()?.substr(0, 10) || "";
Expand All @@ -132,6 +154,10 @@ describe("Component: Datepicker", () => {
render(<Datepicker {...props} min={min} max={max} forceCustom />, container);
});

act(() => {
Simulate.change(container.querySelector("input.seb-datepicker-custom-year"), { target: { value: year } } as any);
});

expect(props.onChange).toHaveBeenCalled();
expect(container.querySelector<HTMLInputElement>("input.seb-datepicker-custom-day").value).toEqual(props.value.getDate().toString());
expect(container.querySelector<HTMLInputElement>("select.seb-datepicker-custom-month").value).toEqual(`${props.value.getMonth() + 1}`);
Expand Down
105 changes: 73 additions & 32 deletions lib/src/Datepicker/Datepicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,14 @@ interface UnitNames {
year: string;
}

type InputRenderType = "custom" | "date" | "month";

export const Datepicker: React.FunctionComponent<DatepickerProps> = React.forwardRef(
(
{ monthPicker, forceCustom, className, value, min, max, disabled, onChange, localeCode = "en", wrapperProps, customPickerSelectProps, ...props }: DatepickerProps,
ref: React.ForwardedRef<HTMLInputElement>
): React.ReactElement<void> => {
const [renderType, setRenderType] = React.useState<InputRenderType>("date");
const isValidDate = React.useCallback((d: Date): boolean => {
return !!(d && d instanceof Date && !isNaN(d.getTime()));
}, []);
Expand Down Expand Up @@ -82,6 +85,21 @@ export const Datepicker: React.FunctionComponent<DatepickerProps> = React.forwar
}
}, []);

const onCustomDatepickerChange = React.useCallback(
(day: number, month: number, year: number) => {
day = monthPicker ? 1 : day;
const dateString: string = `${padNumber(year, true)}-${padNumber(month)}-${padNumber(day)}`;
const date: Date = new Date(dateString);
// as long as all custom input fields are not null and is valid date, fire onChange
if (!!day && !!month && !!year && isValidDate(date) && isDateInRange(date, min, max)) {
onChange(date);
} else {
onChange(null);
}
},
[isDateInRange, onChange, min, max, monthPicker]
);

const initCustomDay = React.useCallback(
(value: Date, monthPicker: boolean): number => {
const inputRawValue: string = getInputRawValue(value, monthPicker);
Expand All @@ -95,13 +113,6 @@ export const Datepicker: React.FunctionComponent<DatepickerProps> = React.forwar

const [customDay, setCustomDay] = React.useState<number>(initCustomDay(value, monthPicker));

const handleChangeCustomDay = (e: React.ChangeEvent<HTMLInputElement>): void => {
if (!monthPicker) {
const v: number = e.target?.value && !Number.isNaN(Number(e.target?.value)) ? Number(e.target.value) : null;
setCustomDay(v);
}
};

const initCustomMonth = React.useCallback(
(value: Date, monthPicker: boolean): number => {
const inputRawValue: string = getInputRawValue(value, monthPicker);
Expand All @@ -115,11 +126,6 @@ export const Datepicker: React.FunctionComponent<DatepickerProps> = React.forwar

const [customMonth, setCustomMonth] = React.useState<number>(initCustomMonth(value, monthPicker));

const handleChangeCustomMonth = (e: React.ChangeEvent<HTMLSelectElement>): void => {
const v: number = e.target?.value && !Number.isNaN(Number(e.target?.value)) ? Number(e.target.value) : null;
setCustomMonth(v);
};

const initCustomYear = React.useCallback(
(value: Date, monthPicker: boolean): number => {
const inputRawValue: string = getInputRawValue(value, monthPicker);
Expand All @@ -133,24 +139,40 @@ export const Datepicker: React.FunctionComponent<DatepickerProps> = React.forwar

const [customYear, setCustomYear] = React.useState<number>(initCustomYear(value, monthPicker));

const handleChangeCustomYear = (e: React.ChangeEvent<HTMLInputElement>): void => {
const v: number = e.target?.value && !Number.isNaN(Number(e.target?.value)) ? Number(e.target.value) : null;
setCustomYear(v);
};
const handleChangeCustomDay = React.useCallback(
(e: React.ChangeEvent<HTMLInputElement>): void => {
if (!monthPicker) {
const v: number = e.target?.value && !Number.isNaN(Number(e.target?.value)) ? Number(e.target.value) : null;
setCustomDay(() => {
onCustomDatepickerChange(v, customMonth, customYear);
return v;
});
}
},
[customMonth, customYear, onCustomDatepickerChange]
);

React.useEffect(() => {
const day: number = monthPicker ? 1 : customDay;
const month: number = customMonth;
const year: number = customYear;
const dateString: string = `${padNumber(year, true)}-${padNumber(month)}-${padNumber(day)}`;
const date: Date = new Date(dateString);
const m: number = date.getMonth() + 1;
if (date.getFullYear() === year && m === month && date.getDate() === day) {
isDateInRange(date, min, max) ? onChange(date) : onChange(null);
} else {
onChange(null);
}
}, [monthPicker, customDay, customMonth, customYear, min, max]);
const handleChangeCustomMonth = React.useCallback(
(e: React.ChangeEvent<HTMLSelectElement>): void => {
const v: number = e.target?.value && !Number.isNaN(Number(e.target?.value)) ? Number(e.target.value) : null;
setCustomMonth(() => {
onCustomDatepickerChange(customDay, v, customYear);
return v;
});
},
[customDay, customYear, onCustomDatepickerChange]
);

const handleChangeCustomYear = React.useCallback(
(e: React.ChangeEvent<HTMLInputElement>): void => {
const v: number = e.target?.value && !Number.isNaN(Number(e.target?.value)) ? Number(e.target.value) : null;
setCustomYear(() => {
onCustomDatepickerChange(customDay, customMonth, v);
return v;
});
},
[customDay, customMonth, onCustomDatepickerChange]
);

const getRelativeTimeFormat = React.useCallback((code: string): any => {
if ((Intl as any)["RelativeTimeFormat"]) {
Expand Down Expand Up @@ -239,7 +261,11 @@ export const Datepicker: React.FunctionComponent<DatepickerProps> = React.forwar
const handleOnChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
const { value: changeEventValue } = e.target;
const value: Date = new Date(changeEventValue);
onChange(value);
if (isDateInRange(value, min, max)) {
onChange(value);
return;
}
onChange(null);
};

const renderCustomDatepicker = (value: Date, monthPicker: boolean, customPickerOrder: string[], unitNames: UnitNames, disabled: boolean, monthNames: string[]) => {
Expand Down Expand Up @@ -312,7 +338,22 @@ export const Datepicker: React.FunctionComponent<DatepickerProps> = React.forwar
);
};

if (monthPicker && !forceCustom && supportsInputOfType("month")) {
React.useEffect(() => {
setRenderType(() => {
if (forceCustom) {
return "custom";
}
if (monthPicker && supportsInputOfType("month")) {
return "month";
}
if (supportsInputOfType("date")) {
return "date";
}
return "custom";
});
}, [forceCustom, monthPicker]);

if (renderType === "month") {
return (
<input
{...props}
Expand All @@ -326,7 +367,7 @@ export const Datepicker: React.FunctionComponent<DatepickerProps> = React.forwar
onChange={handleOnChange}
/>
);
} else if (!forceCustom && supportsInputOfType("date")) {
} else if (renderType === "date") {
return (
<input
{...props}
Expand Down
2 changes: 1 addition & 1 deletion lib/src/Dropdown/Dropdown.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ describe("Component: Dropdown", () => {
select.append(option);
});

expect(getValueOfMultipleSelect(select)).toEqual(["2", "3"]);
expect(getValueOfMultipleSelect(Array.from(select.options))).toEqual(["2", "3"]);
});
});
});
60 changes: 38 additions & 22 deletions lib/src/Dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ const defaultText: Required<DropdownText> = {
search: "Search...",
};

export function getValueOfMultipleSelect(select: HTMLSelectElement): string[] {
return Array.from(select.options)
export function getValueOfMultipleSelect(selectOptions: Array<HTMLOptionElement>): string[] {
return Array.from(selectOptions)
.filter((option) => option.selected)
.map((option) => option.value);
}
Expand Down Expand Up @@ -60,8 +60,8 @@ export const Dropdown: React.FC<DropdownProps> = React.forwardRef(
const [searchKeyword, setSearchKeyword] = React.useState<string>("");
const [menuStyle, setMenuStyle] = React.useState<React.CSSProperties>({});
const [label, setLabel] = React.useState<string>();

const selectRef = useCombinedRefs<HTMLSelectElement>(ref);
const [selectRef, setSelectRef] = React.useState<HTMLSelectElement>(null);
const [selectRefOptions, setSelectRefOptions] = React.useState<Array<HTMLOptionElement>>([]);
const searchRef = React.useRef<HTMLInputElement>();
const menuRef = React.useRef<HTMLDivElement>();
const dropdownRef = React.useRef<HTMLDivElement>();
Expand All @@ -71,41 +71,39 @@ export const Dropdown: React.FC<DropdownProps> = React.forwardRef(
const handleChange = React.useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
if (props.multiple) {
const current = Array.from(selectRef.current.options).find((option) => option.value == e.target.value);
const current = selectRefOptions.find((option) => option.value == e.target.value);
current.selected = !current.selected;
} else {
selectRef.current.value = e.target.value;
selectRef.value = e.target.value;
setShow(false);
}
selectRef.current.dispatchEvent(new Event("change", { bubbles: true }));
props.multiple && onMultipleChange && onMultipleChange(getValueOfMultipleSelect(selectRef.current));
selectRef.dispatchEvent(new Event("change", { bubbles: true }));
props.multiple && onMultipleChange && onMultipleChange(getValueOfMultipleSelect(selectRefOptions));
},
[isMobile, props.multiple, onMultipleChange]
[isMobile, props.multiple, onMultipleChange, selectRefOptions]
);

const selectAll = React.useCallback(
(forceValue?: boolean | React.ChangeEvent<HTMLInputElement>) => {
Array.from(selectRef.current.options).forEach((_, i) => {
const option = selectRef.current.options.item(i);
selectRefOptions.forEach((option: HTMLOptionElement) => {
if (!option.disabled) {
option.selected = typeof forceValue === "boolean" ? forceValue : !allSelected;
} else {
option.selected = false;
}
});
typeof forceValue === "boolean" && (selectRef.current.value = "");
selectRef.current.dispatchEvent(new Event("change", { bubbles: true }));
props.multiple && onMultipleChange && onMultipleChange(getValueOfMultipleSelect(selectRef.current));
typeof forceValue === "boolean" && (selectRef.value = "");
selectRef.dispatchEvent(new Event("change", { bubbles: true }));
props.multiple && onMultipleChange && onMultipleChange(getValueOfMultipleSelect(selectRefOptions));
},
[allSelected, props.multiple]
[allSelected, props.multiple, selectRefOptions, selectRef]
);

const isAllSelected = (): boolean => {
return Array.from(selectRef.current.options).every((_, i) => {
const option: HTMLOptionElement = selectRef.current.options.item(i);
const isAllSelected = React.useCallback((): boolean => {
return selectRefOptions.every((option: HTMLOptionElement) => {
return option.disabled ? true : option.selected;
});
};
}, [selectRefOptions]);

const toggleMenu = React.useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
Expand All @@ -130,7 +128,7 @@ export const Dropdown: React.FC<DropdownProps> = React.forwardRef(

const onChange = React.useCallback(
(event: React.ChangeEvent<HTMLSelectElement>) => {
props.multiple && onMultipleChange && onMultipleChange(getValueOfMultipleSelect(event.target));
props.multiple && onMultipleChange && onMultipleChange(getValueOfMultipleSelect(Array.from(event.target.options)));
props.onChange && props.onChange(event);
},
[props.multiple, props.onChange, onMultipleChange]
Expand Down Expand Up @@ -198,14 +196,32 @@ export const Dropdown: React.FC<DropdownProps> = React.forwardRef(
return list?.length ? list : searchKeyword ? <p>{text.noResult || defaultText.noResult}</p> : <p>{text.emptyList || defaultText.emptyList}</p>;
};

const measuredSelectRef = React.useCallback((node: HTMLSelectElement) => {
if (typeof ref === "function") {
// to pass ref back to parents
ref(node);
} else if (!!ref) {
(ref as any).current = node;
}
if (node !== null) {
setSelectRef(node);
}
}, []);

React.useEffect(() => {
!isMobile && props.multiple && setAllSelected(isAllSelected());
}, [props.value]);
}, [props.value, props.multiple, isAllSelected]);

React.useEffect(() => {
!searchable && setSearchKeyword("");
}, [searchable]);

React.useEffect(() => {
if (!!selectRef) {
setSelectRefOptions(Array.from(selectRef.options));
}
}, [selectRef]);

React.useEffect(() => {
if (!isMobile) {
const detectBlur = (event: MouseEvent) => {
Expand Down Expand Up @@ -303,7 +319,7 @@ export const Dropdown: React.FC<DropdownProps> = React.forwardRef(
</div>
)}
<FeedbackIndicator type={indicator?.type} message={indicator?.message}>
<select {...props} ref={selectRef} onChange={onChange} className={classnames("custom-select", props.className)} hidden={!isMobile}>
<select {...props} ref={measuredSelectRef} onChange={onChange} className={classnames("custom-select", props.className)} hidden={!isMobile}>
{/* select always picks the first item by default. Therefore the first needs to be initialized here */}
{!props.value && (
<option disabled value="" hidden>
Expand Down
2 changes: 1 addition & 1 deletion lib/src/Modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export const Modal: React.FC<ModalProps> = React.memo(
// Escape key listner
React.useEffect(() => {
function keyupListener(e: KeyboardEvent) {
e.key.toLowerCase() === "escape" && onEscape(e);
e.key?.toLowerCase() === "escape" && onEscape(e);
}

if (onEscape && toggle) {
Expand Down
Loading

0 comments on commit 47b6c4e

Please sign in to comment.