💥(react) upgrade to React 19
https://react.dev/blog/2024/04/25/react-19-upgrade-guide https://react.dev/blog/2024/12/05/react-19
This commit is contained in:
committed by
Jean-Baptiste PENRATH
parent
0f6a8dfa72
commit
56d9ed88f0
5
.changeset/gorgeous-clocks-wash.md
Normal file
5
.changeset/gorgeous-clocks-wash.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@openfun/cunningham-react": major
|
||||||
|
---
|
||||||
|
|
||||||
|
Upgrade to React 19
|
||||||
@@ -2,80 +2,76 @@ import React, {
|
|||||||
AnchorHTMLAttributes,
|
AnchorHTMLAttributes,
|
||||||
ButtonHTMLAttributes,
|
ButtonHTMLAttributes,
|
||||||
createElement,
|
createElement,
|
||||||
forwardRef,
|
|
||||||
ReactNode,
|
ReactNode,
|
||||||
|
RefAttributes,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
|
||||||
type DomProps = ButtonHTMLAttributes<HTMLButtonElement> &
|
type DomProps = ButtonHTMLAttributes<HTMLButtonElement> &
|
||||||
AnchorHTMLAttributes<HTMLAnchorElement>;
|
AnchorHTMLAttributes<HTMLAnchorElement>;
|
||||||
|
|
||||||
export type ButtonProps = Omit<DomProps, "color"> & {
|
|
||||||
size?: "medium" | "small" | "nano";
|
|
||||||
color?:
|
|
||||||
| "primary"
|
|
||||||
| "primary-text"
|
|
||||||
| "secondary"
|
|
||||||
| "tertiary"
|
|
||||||
| "tertiary-text"
|
|
||||||
| "danger";
|
|
||||||
icon?: ReactNode;
|
|
||||||
iconPosition?: "left" | "right";
|
|
||||||
active?: boolean;
|
|
||||||
fullWidth?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ButtonElement = HTMLButtonElement & HTMLAnchorElement;
|
export type ButtonElement = HTMLButtonElement & HTMLAnchorElement;
|
||||||
|
export type ButtonProps = Omit<DomProps, "color"> &
|
||||||
|
RefAttributes<ButtonElement> & {
|
||||||
|
size?: "medium" | "small" | "nano";
|
||||||
|
color?:
|
||||||
|
| "primary"
|
||||||
|
| "primary-text"
|
||||||
|
| "secondary"
|
||||||
|
| "tertiary"
|
||||||
|
| "tertiary-text"
|
||||||
|
| "danger";
|
||||||
|
icon?: ReactNode;
|
||||||
|
iconPosition?: "left" | "right";
|
||||||
|
active?: boolean;
|
||||||
|
fullWidth?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export const Button = forwardRef<ButtonElement, ButtonProps>(
|
export const Button = ({
|
||||||
(
|
children,
|
||||||
|
color = "primary",
|
||||||
|
size = "medium",
|
||||||
|
iconPosition = "left",
|
||||||
|
icon,
|
||||||
|
active,
|
||||||
|
className,
|
||||||
|
fullWidth,
|
||||||
|
ref,
|
||||||
|
...props
|
||||||
|
}: ButtonProps) => {
|
||||||
|
const classes = [
|
||||||
|
"c__button",
|
||||||
|
"c__button--" + color,
|
||||||
|
"c__button--" + size,
|
||||||
|
className,
|
||||||
|
];
|
||||||
|
if (icon && children) {
|
||||||
|
classes.push("c__button--with-icon--" + iconPosition);
|
||||||
|
}
|
||||||
|
if (icon && !children) {
|
||||||
|
classes.push("c__button--icon-only");
|
||||||
|
}
|
||||||
|
if (active) {
|
||||||
|
classes.push("c__button--active");
|
||||||
|
}
|
||||||
|
if (fullWidth) {
|
||||||
|
classes.push("c__button--full-width");
|
||||||
|
}
|
||||||
|
if (["primary-text", "tertiary-text"].includes(color)) {
|
||||||
|
classes.push("c__button--text");
|
||||||
|
}
|
||||||
|
const iconElement = <span className="c__button__icon">{icon}</span>;
|
||||||
|
const tagName = props.href ? "a" : "button";
|
||||||
|
return createElement(
|
||||||
|
tagName,
|
||||||
{
|
{
|
||||||
children,
|
className: classes.join(" "),
|
||||||
color = "primary",
|
...props,
|
||||||
size = "medium",
|
ref,
|
||||||
iconPosition = "left",
|
|
||||||
icon,
|
|
||||||
active,
|
|
||||||
className,
|
|
||||||
fullWidth,
|
|
||||||
...props
|
|
||||||
},
|
},
|
||||||
ref,
|
<>
|
||||||
) => {
|
{!!icon && iconPosition === "left" && iconElement}
|
||||||
const classes = [
|
{children}
|
||||||
"c__button",
|
{!!icon && iconPosition === "right" && iconElement}
|
||||||
"c__button--" + color,
|
</>,
|
||||||
"c__button--" + size,
|
);
|
||||||
className,
|
};
|
||||||
];
|
|
||||||
if (icon && children) {
|
|
||||||
classes.push("c__button--with-icon--" + iconPosition);
|
|
||||||
}
|
|
||||||
if (icon && !children) {
|
|
||||||
classes.push("c__button--icon-only");
|
|
||||||
}
|
|
||||||
if (active) {
|
|
||||||
classes.push("c__button--active");
|
|
||||||
}
|
|
||||||
if (fullWidth) {
|
|
||||||
classes.push("c__button--full-width");
|
|
||||||
}
|
|
||||||
if (["primary-text", "tertiary-text"].includes(color)) {
|
|
||||||
classes.push("c__button--text");
|
|
||||||
}
|
|
||||||
const iconElement = <span className="c__button__icon">{icon}</span>;
|
|
||||||
const tagName = props.href ? "a" : "button";
|
|
||||||
return createElement(
|
|
||||||
tagName,
|
|
||||||
{
|
|
||||||
className: classes.join(" "),
|
|
||||||
...props,
|
|
||||||
ref,
|
|
||||||
},
|
|
||||||
<>
|
|
||||||
{!!icon && iconPosition === "left" && iconElement}
|
|
||||||
{children}
|
|
||||||
{!!icon && iconPosition === "right" && iconElement}
|
|
||||||
</>,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import React, {
|
import React, {
|
||||||
InputHTMLAttributes,
|
InputHTMLAttributes,
|
||||||
PropsWithChildren,
|
PropsWithChildren,
|
||||||
forwardRef,
|
|
||||||
useEffect,
|
useEffect,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
|
RefAttributes,
|
||||||
} from "react";
|
} from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { Field, FieldProps } from ":/components/Forms/Field";
|
import { Field, FieldProps } from ":/components/Forms/Field";
|
||||||
@@ -16,72 +16,75 @@ export type CheckboxOnlyProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type CheckboxProps = InputHTMLAttributes<HTMLInputElement> &
|
export type CheckboxProps = InputHTMLAttributes<HTMLInputElement> &
|
||||||
|
RefAttributes<HTMLInputElement> &
|
||||||
FieldProps &
|
FieldProps &
|
||||||
CheckboxOnlyProps;
|
CheckboxOnlyProps;
|
||||||
|
|
||||||
export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
|
export const Checkbox = ({
|
||||||
(
|
indeterminate,
|
||||||
{ indeterminate, className = "", checked, label, ...props }: CheckboxProps,
|
className = "",
|
||||||
ref,
|
checked,
|
||||||
) => {
|
label,
|
||||||
const inputRef = useRef<HTMLInputElement>();
|
ref,
|
||||||
const [value, setValue] = useState<boolean>(!!checked);
|
...props
|
||||||
|
}: CheckboxProps) => {
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [value, setValue] = useState<boolean>(!!checked);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setValue(!!checked);
|
setValue(!!checked);
|
||||||
}, [checked]);
|
}, [checked]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (inputRef.current) {
|
if (inputRef.current) {
|
||||||
inputRef.current.indeterminate = !!indeterminate;
|
inputRef.current.indeterminate = !!indeterminate;
|
||||||
}
|
}
|
||||||
}, [indeterminate]);
|
}, [indeterminate]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
compact,
|
compact,
|
||||||
fullWidth,
|
fullWidth,
|
||||||
rightText,
|
rightText,
|
||||||
state,
|
state,
|
||||||
text,
|
text,
|
||||||
textItems,
|
textItems,
|
||||||
...inputProps
|
...inputProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
className={classNames("c__checkbox", className, {
|
className={classNames("c__checkbox", className, {
|
||||||
"c__checkbox--disabled": props.disabled,
|
"c__checkbox--disabled": props.disabled,
|
||||||
"c__checkbox--full-width": props.fullWidth,
|
"c__checkbox--full-width": props.fullWidth,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Field compact={true} {...props}>
|
<Field compact={true} {...props}>
|
||||||
<div className="c__checkbox__container">
|
<div className="c__checkbox__container">
|
||||||
<div className="c__checkbox__wrapper">
|
<div className="c__checkbox__wrapper">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setValue(e.target.checked);
|
setValue(e.target.checked);
|
||||||
props.onChange?.(e);
|
props.onChange?.(e);
|
||||||
}}
|
}}
|
||||||
checked={value}
|
checked={value}
|
||||||
ref={(checkboxRef) => {
|
ref={(checkboxRef) => {
|
||||||
if (typeof ref === "function") {
|
if (typeof ref === "function") {
|
||||||
ref(checkboxRef);
|
ref(checkboxRef);
|
||||||
}
|
}
|
||||||
inputRef.current = checkboxRef || undefined;
|
inputRef.current = checkboxRef || null;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Indeterminate />
|
<Indeterminate />
|
||||||
<Checkmark />
|
<Checkmark />
|
||||||
</div>
|
|
||||||
{label && <div className="c__checkbox__label">{label}</div>}
|
|
||||||
</div>
|
</div>
|
||||||
</Field>
|
{label && <div className="c__checkbox__label">{label}</div>}
|
||||||
</label>
|
</div>
|
||||||
);
|
</Field>
|
||||||
},
|
</label>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const CheckboxGroup = ({
|
export const CheckboxGroup = ({
|
||||||
children,
|
children,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { forwardRef, Ref, useMemo, useRef, useState } from "react";
|
import React, { RefAttributes, useMemo, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
CalendarDate,
|
CalendarDate,
|
||||||
createCalendar,
|
createCalendar,
|
||||||
@@ -83,280 +83,276 @@ const DropdownValues = ({ options, downShift }: DropdownValuesProps) => (
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
interface CalendarAuxProps extends CalendarAria {
|
type CalendarAuxProps = CalendarAria &
|
||||||
minYear?: number;
|
RefAttributes<HTMLDivElement> & {
|
||||||
maxYear?: number;
|
minYear?: number;
|
||||||
state: RangeCalendarState | CalendarState;
|
maxYear?: number;
|
||||||
}
|
state: RangeCalendarState | CalendarState;
|
||||||
|
};
|
||||||
|
|
||||||
const CalendarAux = forwardRef(
|
const CalendarAux = ({
|
||||||
(
|
state,
|
||||||
{
|
minYear = 1900, // in gregorian calendar.
|
||||||
state,
|
maxYear = 2050, // in gregorian calendar.
|
||||||
minYear = 1900, // in gregorian calendar.
|
prevButtonProps,
|
||||||
maxYear = 2050, // in gregorian calendar.
|
nextButtonProps,
|
||||||
prevButtonProps,
|
calendarProps,
|
||||||
nextButtonProps,
|
ref,
|
||||||
calendarProps,
|
}: CalendarAuxProps) => {
|
||||||
}: CalendarAuxProps,
|
const { t } = useCunningham();
|
||||||
ref: Ref<HTMLDivElement>,
|
|
||||||
) => {
|
|
||||||
const { t } = useCunningham();
|
|
||||||
|
|
||||||
const useTimeZoneFormatter = (formatOptions: DateFormatterOptions) => {
|
const useTimeZoneFormatter = (formatOptions: DateFormatterOptions) => {
|
||||||
return useDateFormatter({
|
return useDateFormatter({
|
||||||
...formatOptions,
|
...formatOptions,
|
||||||
timeZone: state.timeZone,
|
timeZone: state.timeZone,
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const monthItemsFormatter = useTimeZoneFormatter({ month: "long" });
|
|
||||||
const selectedMonthItemFormatter = useTimeZoneFormatter({ month: "short" });
|
|
||||||
const yearItemsFormatter = useTimeZoneFormatter({ year: "numeric" });
|
|
||||||
const [showGrid, setShowGrid] = useState(true);
|
|
||||||
|
|
||||||
const monthItems: Array<Option> = useMemo(() => {
|
|
||||||
// Note that in some calendar systems, such as the Hebrew, the number of months may differ between years.
|
|
||||||
const numberOfMonths = state.focusedDate.calendar.getMonthsInYear(
|
|
||||||
state.focusedDate,
|
|
||||||
);
|
|
||||||
return range(1, numberOfMonths).map((monthNumber) => {
|
|
||||||
const date = state.focusedDate.set({ month: monthNumber });
|
|
||||||
return {
|
|
||||||
value: monthNumber,
|
|
||||||
label: monthItemsFormatter.format(date.toDate(state.timeZone)),
|
|
||||||
disabled:
|
|
||||||
(!!state.minValue && state.minValue.month > monthNumber) ||
|
|
||||||
(!!state.maxValue && state.maxValue.month < monthNumber),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}, [state.maxValue, state.minValue, state.focusedDate.year]);
|
|
||||||
|
|
||||||
const yearItems: Array<Option> = useMemo(() => {
|
|
||||||
const calendarCurrentUser = createCalendar(
|
|
||||||
new Intl.DateTimeFormat().resolvedOptions().calendar,
|
|
||||||
);
|
|
||||||
const minDate = toCalendar(
|
|
||||||
new CalendarDate(new GregorianCalendar(), minYear, 1, 1),
|
|
||||||
calendarCurrentUser,
|
|
||||||
);
|
|
||||||
const maxDate = toCalendar(
|
|
||||||
new CalendarDate(new GregorianCalendar(), maxYear, 12, 31),
|
|
||||||
calendarCurrentUser,
|
|
||||||
);
|
|
||||||
return range(minDate.year, maxDate.year).map((yearNumber) => {
|
|
||||||
const date = state.focusedDate.set({ year: yearNumber });
|
|
||||||
return {
|
|
||||||
value: yearNumber,
|
|
||||||
label: yearItemsFormatter.format(date.toDate(state.timeZone)),
|
|
||||||
disabled:
|
|
||||||
(!!state.minValue && state.minValue.year > yearNumber) ||
|
|
||||||
(!!state.maxValue && state.maxValue.year < yearNumber),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}, [state.focusedDate, state.timeZone, state.maxValue, state.minValue]);
|
|
||||||
|
|
||||||
const useDownshiftSelect = (
|
|
||||||
key: string,
|
|
||||||
items: Array<Option>,
|
|
||||||
): UseSelectReturnValue<Option> => {
|
|
||||||
return useSelect({
|
|
||||||
items,
|
|
||||||
itemToString: optionToString,
|
|
||||||
onSelectedItemChange: (e: UseSelectStateChange<Option>) => {
|
|
||||||
const updatedFocusedDate = state.focusedDate.set({
|
|
||||||
[key]: e?.selectedItem?.value,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We need to hide the grid before updated the focused date because if the mouse hovers a cell it will
|
|
||||||
* automatically internally call the focusCell method which sets the focused date to the hovered cell date.
|
|
||||||
*
|
|
||||||
* (Current year = 2024) The steps are:
|
|
||||||
* 1 - Select year 2050 in the dropdown.
|
|
||||||
* 2 - Hide the dropdown
|
|
||||||
* 3 - state.setFocusedDate(2050)
|
|
||||||
* 3 - Mouse hovers a cell in the grid before the state takes into account the new focused date ( 2050 ).
|
|
||||||
* 4 - focusCell is called with the current year (2024) overriding the previous call with year=2050
|
|
||||||
*
|
|
||||||
* The resulting bug will be the year 2024 being selected in the grid instead of 2050.
|
|
||||||
*
|
|
||||||
* So instead why first hide the grid, wait for the state to be updated, set the focused date to 2050, and
|
|
||||||
* then show the grid again. This way we will prevent the mouse from hovering a cell before the state is updated.
|
|
||||||
*/
|
|
||||||
setShowGrid(false);
|
|
||||||
setTimeout(() => {
|
|
||||||
state.setFocusedDate(updatedFocusedDate);
|
|
||||||
setTimeout(() => {
|
|
||||||
setShowGrid(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const downshiftMonth = useDownshiftSelect("month", monthItems);
|
|
||||||
const downshiftYear = useDownshiftSelect("year", yearItems);
|
|
||||||
|
|
||||||
// isDisabled, onPress and onFocusChange props don't exist on the <Button /> component.
|
|
||||||
// remove them to avoid any warning.
|
|
||||||
const {
|
|
||||||
isDisabled: isPrevButtonDisabled,
|
|
||||||
onPress: onPressPrev,
|
|
||||||
onFocusChange: onFocusChangePrev,
|
|
||||||
...prevButtonOtherProps
|
|
||||||
} = prevButtonProps;
|
|
||||||
const {
|
|
||||||
isDisabled: isNextButtonDisabled,
|
|
||||||
onPress: onPressNext,
|
|
||||||
onFocusChange: onFocusChangeNext,
|
|
||||||
...nextButtonOtherProps
|
|
||||||
} = nextButtonProps;
|
|
||||||
|
|
||||||
const getToggleButtonProps = (
|
|
||||||
key: string,
|
|
||||||
items: Array<Option>,
|
|
||||||
downshift: UseSelectReturnValue<Option>,
|
|
||||||
) => ({
|
|
||||||
...downshift.getToggleButtonProps(),
|
|
||||||
onClick: () => {
|
|
||||||
const selectedItem = items.find(
|
|
||||||
(item) => item.value === state.focusedDate[key as keyof CalendarDate],
|
|
||||||
);
|
|
||||||
if (selectedItem) {
|
|
||||||
downshift.selectItem(selectedItem);
|
|
||||||
}
|
|
||||||
downshift.toggleMenu();
|
|
||||||
},
|
|
||||||
"aria-label": t(
|
|
||||||
`components.forms.date_picker.${key}_select_button_aria_label`,
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
const monthItemsFormatter = useTimeZoneFormatter({ month: "long" });
|
||||||
<div className="c__calendar">
|
const selectedMonthItemFormatter = useTimeZoneFormatter({ month: "short" });
|
||||||
<div
|
const yearItemsFormatter = useTimeZoneFormatter({ year: "numeric" });
|
||||||
ref={ref}
|
const [showGrid, setShowGrid] = useState(true);
|
||||||
{...calendarProps}
|
|
||||||
// We need to remove the id from the calendar when the dropdowns are open to avoid having the following bug:
|
const monthItems: Array<Option> = useMemo(() => {
|
||||||
// 1 - Open the calendar
|
// Note that in some calendar systems, such as the Hebrew, the number of months may differ between years.
|
||||||
// 2 - Select a start date
|
const numberOfMonths = state.focusedDate.calendar.getMonthsInYear(
|
||||||
// 3 - Click on the dropdown to select another year
|
state.focusedDate,
|
||||||
// 4 - BUG: The calendar closes abruptly.
|
|
||||||
//
|
|
||||||
// This way caused by this internal call from Spectrum: https://github.com/adobe/react-spectrum/blob/cdb2fe21213d9e8b03d782d82bda07742ab3afbd/packages/%40react-aria/calendar/src/useRangeCalendar.ts#L59
|
|
||||||
//
|
|
||||||
// So instead we decided to remove the id of the calendar when the dropdowns are open and add it back when
|
|
||||||
// the dropdowns are closed in order to make this condition fail: https://github.com/adobe/react-spectrum/blob/cdb2fe21213d9e8b03d782d82bda07742ab3afbd/packages/%40react-aria/calendar/src/useRangeCalendar.ts#L55
|
|
||||||
// This way the `body` variable will never be found.
|
|
||||||
id={
|
|
||||||
!downshiftMonth.isOpen && !downshiftYear.isOpen
|
|
||||||
? calendarProps.id
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
className={classNames("c__calendar__wrapper", {
|
|
||||||
"c__calendar__wrapper--opened":
|
|
||||||
!downshiftMonth.isOpen && !downshiftYear.isOpen,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<div className="c__calendar__wrapper__header">
|
|
||||||
<div className="c__calendar__wrapper__header__actions">
|
|
||||||
<Button
|
|
||||||
color="tertiary-text"
|
|
||||||
size="small"
|
|
||||||
icon={<span className="material-icons">navigate_before</span>}
|
|
||||||
{...{
|
|
||||||
...prevButtonOtherProps,
|
|
||||||
"aria-label": t(
|
|
||||||
"components.forms.date_picker.previous_month_button_aria_label",
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
disabled={isPrevButtonDisabled}
|
|
||||||
onClick={() => state.focusPreviousSection()}
|
|
||||||
type="button"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
className="c__calendar__wrapper__header__actions__dropdown"
|
|
||||||
color="tertiary-text"
|
|
||||||
size="small"
|
|
||||||
iconPosition="right"
|
|
||||||
icon={<span className="material-icons">arrow_drop_down</span>}
|
|
||||||
type="button"
|
|
||||||
{...getToggleButtonProps("month", monthItems, downshiftMonth)}
|
|
||||||
>
|
|
||||||
{selectedMonthItemFormatter.format(
|
|
||||||
state.focusedDate.toDate(state.timeZone),
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
color="tertiary-text"
|
|
||||||
size="small"
|
|
||||||
icon={<span className="material-icons">navigate_next</span>}
|
|
||||||
type="button"
|
|
||||||
{...{
|
|
||||||
...nextButtonOtherProps,
|
|
||||||
"aria-label": t(
|
|
||||||
"components.forms.date_picker.next_month_button_aria_label",
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
disabled={isNextButtonDisabled}
|
|
||||||
onClick={() => state.focusNextSection()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="c__calendar__wrapper__header__actions">
|
|
||||||
<Button
|
|
||||||
color="tertiary-text"
|
|
||||||
size="small"
|
|
||||||
icon={<span className="material-icons">navigate_before</span>}
|
|
||||||
onClick={() => state.focusPreviousSection(true)}
|
|
||||||
disabled={
|
|
||||||
!!state.minValue &&
|
|
||||||
state.minValue.year >
|
|
||||||
state.focusedDate.add({ years: -1 }).year
|
|
||||||
}
|
|
||||||
aria-label={t(
|
|
||||||
"components.forms.date_picker.previous_year_button_aria_label",
|
|
||||||
)}
|
|
||||||
type="button"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
className="c__calendar__wrapper__header__actions__dropdown"
|
|
||||||
color="tertiary-text"
|
|
||||||
size="small"
|
|
||||||
iconPosition="right"
|
|
||||||
icon={<span className="material-icons">arrow_drop_down</span>}
|
|
||||||
type="button"
|
|
||||||
{...getToggleButtonProps("year", yearItems, downshiftYear)}
|
|
||||||
>
|
|
||||||
{yearItemsFormatter.format(
|
|
||||||
state.focusedDate.toDate(state.timeZone),
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
color="tertiary-text"
|
|
||||||
size="small"
|
|
||||||
icon={<span className="material-icons">navigate_next</span>}
|
|
||||||
onClick={() => state.focusNextSection(true)}
|
|
||||||
disabled={
|
|
||||||
!!state.maxValue &&
|
|
||||||
state.maxValue.year < state.focusedDate.add({ years: 1 }).year
|
|
||||||
}
|
|
||||||
aria-label={t(
|
|
||||||
"components.forms.date_picker.next_year_button_aria_label",
|
|
||||||
)}
|
|
||||||
type="button"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{!downshiftMonth.isOpen && !downshiftYear.isOpen && showGrid && (
|
|
||||||
<CalendarGrid state={state} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<DropdownValues options={monthItems} downShift={downshiftMonth} />
|
|
||||||
<DropdownValues options={yearItems} downShift={downshiftYear} />
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
},
|
return range(1, numberOfMonths).map((monthNumber) => {
|
||||||
);
|
const date = state.focusedDate.set({ month: monthNumber });
|
||||||
|
return {
|
||||||
|
value: monthNumber,
|
||||||
|
label: monthItemsFormatter.format(date.toDate(state.timeZone)),
|
||||||
|
disabled:
|
||||||
|
(!!state.minValue && state.minValue.month > monthNumber) ||
|
||||||
|
(!!state.maxValue && state.maxValue.month < monthNumber),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [state.maxValue, state.minValue, state.focusedDate.year]);
|
||||||
|
|
||||||
|
const yearItems: Array<Option> = useMemo(() => {
|
||||||
|
const calendarCurrentUser = createCalendar(
|
||||||
|
new Intl.DateTimeFormat().resolvedOptions().calendar,
|
||||||
|
);
|
||||||
|
const minDate = toCalendar(
|
||||||
|
new CalendarDate(new GregorianCalendar(), minYear, 1, 1),
|
||||||
|
calendarCurrentUser,
|
||||||
|
);
|
||||||
|
const maxDate = toCalendar(
|
||||||
|
new CalendarDate(new GregorianCalendar(), maxYear, 12, 31),
|
||||||
|
calendarCurrentUser,
|
||||||
|
);
|
||||||
|
return range(minDate.year, maxDate.year).map((yearNumber) => {
|
||||||
|
const date = state.focusedDate.set({ year: yearNumber });
|
||||||
|
return {
|
||||||
|
value: yearNumber,
|
||||||
|
label: yearItemsFormatter.format(date.toDate(state.timeZone)),
|
||||||
|
disabled:
|
||||||
|
(!!state.minValue && state.minValue.year > yearNumber) ||
|
||||||
|
(!!state.maxValue && state.maxValue.year < yearNumber),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [state.focusedDate, state.timeZone, state.maxValue, state.minValue]);
|
||||||
|
|
||||||
|
const useDownshiftSelect = (
|
||||||
|
key: string,
|
||||||
|
items: Array<Option>,
|
||||||
|
): UseSelectReturnValue<Option> => {
|
||||||
|
return useSelect({
|
||||||
|
items,
|
||||||
|
itemToString: optionToString,
|
||||||
|
onSelectedItemChange: (e: UseSelectStateChange<Option>) => {
|
||||||
|
const updatedFocusedDate = state.focusedDate.set({
|
||||||
|
[key]: e?.selectedItem?.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We need to hide the grid before updated the focused date because if the mouse hovers a cell it will
|
||||||
|
* automatically internally call the focusCell method which sets the focused date to the hovered cell date.
|
||||||
|
*
|
||||||
|
* (Current year = 2024) The steps are:
|
||||||
|
* 1 - Select year 2050 in the dropdown.
|
||||||
|
* 2 - Hide the dropdown
|
||||||
|
* 3 - state.setFocusedDate(2050)
|
||||||
|
* 3 - Mouse hovers a cell in the grid before the state takes into account the new focused date ( 2050 ).
|
||||||
|
* 4 - focusCell is called with the current year (2024) overriding the previous call with year=2050
|
||||||
|
*
|
||||||
|
* The resulting bug will be the year 2024 being selected in the grid instead of 2050.
|
||||||
|
*
|
||||||
|
* So instead why first hide the grid, wait for the state to be updated, set the focused date to 2050, and
|
||||||
|
* then show the grid again. This way we will prevent the mouse from hovering a cell before the state is updated.
|
||||||
|
*/
|
||||||
|
setShowGrid(false);
|
||||||
|
setTimeout(() => {
|
||||||
|
state.setFocusedDate(updatedFocusedDate);
|
||||||
|
setTimeout(() => {
|
||||||
|
setShowGrid(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const downshiftMonth = useDownshiftSelect("month", monthItems);
|
||||||
|
const downshiftYear = useDownshiftSelect("year", yearItems);
|
||||||
|
|
||||||
|
// isDisabled, onPress and onFocusChange props don't exist on the <Button /> component.
|
||||||
|
// remove them to avoid any warning.
|
||||||
|
const {
|
||||||
|
isDisabled: isPrevButtonDisabled,
|
||||||
|
onPress: onPressPrev,
|
||||||
|
onFocusChange: onFocusChangePrev,
|
||||||
|
...prevButtonOtherProps
|
||||||
|
} = prevButtonProps;
|
||||||
|
const {
|
||||||
|
isDisabled: isNextButtonDisabled,
|
||||||
|
onPress: onPressNext,
|
||||||
|
onFocusChange: onFocusChangeNext,
|
||||||
|
...nextButtonOtherProps
|
||||||
|
} = nextButtonProps;
|
||||||
|
|
||||||
|
const getToggleButtonProps = (
|
||||||
|
key: string,
|
||||||
|
items: Array<Option>,
|
||||||
|
downshift: UseSelectReturnValue<Option>,
|
||||||
|
) => ({
|
||||||
|
...downshift.getToggleButtonProps(),
|
||||||
|
onClick: () => {
|
||||||
|
const selectedItem = items.find(
|
||||||
|
(item) => item.value === state.focusedDate[key as keyof CalendarDate],
|
||||||
|
);
|
||||||
|
if (selectedItem) {
|
||||||
|
downshift.selectItem(selectedItem);
|
||||||
|
}
|
||||||
|
downshift.toggleMenu();
|
||||||
|
},
|
||||||
|
"aria-label": t(
|
||||||
|
`components.forms.date_picker.${key}_select_button_aria_label`,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="c__calendar">
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
{...calendarProps}
|
||||||
|
// We need to remove the id from the calendar when the dropdowns are open to avoid having the following bug:
|
||||||
|
// 1 - Open the calendar
|
||||||
|
// 2 - Select a start date
|
||||||
|
// 3 - Click on the dropdown to select another year
|
||||||
|
// 4 - BUG: The calendar closes abruptly.
|
||||||
|
//
|
||||||
|
// This way caused by this internal call from Spectrum: https://github.com/adobe/react-spectrum/blob/cdb2fe21213d9e8b03d782d82bda07742ab3afbd/packages/%40react-aria/calendar/src/useRangeCalendar.ts#L59
|
||||||
|
//
|
||||||
|
// So instead we decided to remove the id of the calendar when the dropdowns are open and add it back when
|
||||||
|
// the dropdowns are closed in order to make this condition fail: https://github.com/adobe/react-spectrum/blob/cdb2fe21213d9e8b03d782d82bda07742ab3afbd/packages/%40react-aria/calendar/src/useRangeCalendar.ts#L55
|
||||||
|
// This way the `body` variable will never be found.
|
||||||
|
id={
|
||||||
|
!downshiftMonth.isOpen && !downshiftYear.isOpen
|
||||||
|
? calendarProps.id
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
className={classNames("c__calendar__wrapper", {
|
||||||
|
"c__calendar__wrapper--opened":
|
||||||
|
!downshiftMonth.isOpen && !downshiftYear.isOpen,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className="c__calendar__wrapper__header">
|
||||||
|
<div className="c__calendar__wrapper__header__actions">
|
||||||
|
<Button
|
||||||
|
color="tertiary-text"
|
||||||
|
size="small"
|
||||||
|
icon={<span className="material-icons">navigate_before</span>}
|
||||||
|
{...{
|
||||||
|
...prevButtonOtherProps,
|
||||||
|
"aria-label": t(
|
||||||
|
"components.forms.date_picker.previous_month_button_aria_label",
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
disabled={isPrevButtonDisabled}
|
||||||
|
onClick={() => state.focusPreviousSection()}
|
||||||
|
type="button"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
className="c__calendar__wrapper__header__actions__dropdown"
|
||||||
|
color="tertiary-text"
|
||||||
|
size="small"
|
||||||
|
iconPosition="right"
|
||||||
|
icon={<span className="material-icons">arrow_drop_down</span>}
|
||||||
|
type="button"
|
||||||
|
{...getToggleButtonProps("month", monthItems, downshiftMonth)}
|
||||||
|
>
|
||||||
|
{selectedMonthItemFormatter.format(
|
||||||
|
state.focusedDate.toDate(state.timeZone),
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="tertiary-text"
|
||||||
|
size="small"
|
||||||
|
icon={<span className="material-icons">navigate_next</span>}
|
||||||
|
type="button"
|
||||||
|
{...{
|
||||||
|
...nextButtonOtherProps,
|
||||||
|
"aria-label": t(
|
||||||
|
"components.forms.date_picker.next_month_button_aria_label",
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
disabled={isNextButtonDisabled}
|
||||||
|
onClick={() => state.focusNextSection()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="c__calendar__wrapper__header__actions">
|
||||||
|
<Button
|
||||||
|
color="tertiary-text"
|
||||||
|
size="small"
|
||||||
|
icon={<span className="material-icons">navigate_before</span>}
|
||||||
|
onClick={() => state.focusPreviousSection(true)}
|
||||||
|
disabled={
|
||||||
|
!!state.minValue &&
|
||||||
|
state.minValue.year > state.focusedDate.add({ years: -1 }).year
|
||||||
|
}
|
||||||
|
aria-label={t(
|
||||||
|
"components.forms.date_picker.previous_year_button_aria_label",
|
||||||
|
)}
|
||||||
|
type="button"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
className="c__calendar__wrapper__header__actions__dropdown"
|
||||||
|
color="tertiary-text"
|
||||||
|
size="small"
|
||||||
|
iconPosition="right"
|
||||||
|
icon={<span className="material-icons">arrow_drop_down</span>}
|
||||||
|
type="button"
|
||||||
|
{...getToggleButtonProps("year", yearItems, downshiftYear)}
|
||||||
|
>
|
||||||
|
{yearItemsFormatter.format(
|
||||||
|
state.focusedDate.toDate(state.timeZone),
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="tertiary-text"
|
||||||
|
size="small"
|
||||||
|
icon={<span className="material-icons">navigate_next</span>}
|
||||||
|
onClick={() => state.focusNextSection(true)}
|
||||||
|
disabled={
|
||||||
|
!!state.maxValue &&
|
||||||
|
state.maxValue.year < state.focusedDate.add({ years: 1 }).year
|
||||||
|
}
|
||||||
|
aria-label={t(
|
||||||
|
"components.forms.date_picker.next_year_button_aria_label",
|
||||||
|
)}
|
||||||
|
type="button"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!downshiftMonth.isOpen && !downshiftYear.isOpen && showGrid && (
|
||||||
|
<CalendarGrid state={state} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<DropdownValues options={monthItems} downShift={downshiftMonth} />
|
||||||
|
<DropdownValues options={yearItems} downShift={downshiftYear} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const Calendar = (props: CalendarProps<DateValue>) => {
|
export const Calendar = (props: CalendarProps<DateValue>) => {
|
||||||
const { locale } = useLocale();
|
const { locale } = useLocale();
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import React, {
|
import React, {
|
||||||
forwardRef,
|
|
||||||
PropsWithChildren,
|
PropsWithChildren,
|
||||||
Ref,
|
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
|
RefAttributes,
|
||||||
} from "react";
|
} from "react";
|
||||||
import {
|
import {
|
||||||
DateRangePickerState,
|
DateRangePickerState,
|
||||||
@@ -36,7 +35,8 @@ export type DatePickerAuxSubProps = FieldProps & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type DatePickerAuxProps = PropsWithChildren &
|
export type DatePickerAuxProps = PropsWithChildren &
|
||||||
DatePickerAuxSubProps & {
|
DatePickerAuxSubProps &
|
||||||
|
RefAttributes<HTMLDivElement> & {
|
||||||
pickerState: DateRangePickerState | DatePickerState;
|
pickerState: DateRangePickerState | DatePickerState;
|
||||||
pickerProps: Pick<
|
pickerProps: Pick<
|
||||||
DateRangePickerAria | DatePickerAria,
|
DateRangePickerAria | DatePickerAria,
|
||||||
@@ -54,154 +54,147 @@ export type DatePickerAuxProps = PropsWithChildren &
|
|||||||
* This component is used by date and date range picker components.
|
* This component is used by date and date range picker components.
|
||||||
* It contains the common logic between the two.
|
* It contains the common logic between the two.
|
||||||
*/
|
*/
|
||||||
const DatePickerAux = forwardRef(
|
const DatePickerAux = ({
|
||||||
(
|
className,
|
||||||
{
|
pickerState,
|
||||||
className,
|
pickerProps,
|
||||||
pickerState,
|
onClear,
|
||||||
pickerProps,
|
isFocused,
|
||||||
onClear,
|
labelAsPlaceholder,
|
||||||
isFocused,
|
calendar,
|
||||||
labelAsPlaceholder,
|
children,
|
||||||
calendar,
|
name,
|
||||||
children,
|
locale,
|
||||||
name,
|
disabled = false,
|
||||||
locale,
|
optionalClassName,
|
||||||
disabled = false,
|
isRange,
|
||||||
optionalClassName,
|
ref,
|
||||||
isRange,
|
...props
|
||||||
...props
|
}: DatePickerAuxProps) => {
|
||||||
}: DatePickerAuxProps,
|
const { t, currentLocale } = useCunningham();
|
||||||
ref: Ref<HTMLDivElement>,
|
const pickerRef = useRef<HTMLDivElement>(null);
|
||||||
) => {
|
|
||||||
const { t, currentLocale } = useCunningham();
|
|
||||||
const pickerRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const isDateInvalid = useMemo(
|
const isDateInvalid = useMemo(
|
||||||
() =>
|
() => pickerState.validationState === "invalid" || props.state === "error",
|
||||||
pickerState.validationState === "invalid" || props.state === "error",
|
[pickerState.validationState, props.state],
|
||||||
[pickerState.validationState, props.state],
|
);
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<I18nProvider locale={locale || currentLocale}>
|
<I18nProvider locale={locale || currentLocale}>
|
||||||
<Field
|
<Field
|
||||||
{...props}
|
{...props}
|
||||||
className={classNames(className, {
|
className={classNames(className, {
|
||||||
"c__date-picker__range__container": isRange,
|
"c__date-picker__range__container": isRange,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={pickerRef}
|
||||||
|
className={classNames(["c__date-picker", optionalClassName], {
|
||||||
|
"c__date-picker--disabled": disabled,
|
||||||
|
"c__date-picker--invalid": isDateInvalid,
|
||||||
|
"c__date-picker--success": props.state === "success",
|
||||||
|
"c__date-picker--focused":
|
||||||
|
!isDateInvalid && !disabled && (pickerState.isOpen || isFocused),
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref={pickerRef}
|
className={classNames("c__date-picker__wrapper", {
|
||||||
className={classNames(["c__date-picker", optionalClassName], {
|
"c__date-picker__wrapper--clickable": labelAsPlaceholder,
|
||||||
"c__date-picker--disabled": disabled,
|
|
||||||
"c__date-picker--invalid": isDateInvalid,
|
|
||||||
"c__date-picker--success": props.state === "success",
|
|
||||||
"c__date-picker--focused":
|
|
||||||
!isDateInvalid &&
|
|
||||||
!disabled &&
|
|
||||||
(pickerState.isOpen || isFocused),
|
|
||||||
})}
|
})}
|
||||||
|
ref={ref}
|
||||||
|
{...pickerProps.groupProps}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => !pickerState.isOpen && pickerState.open()}
|
||||||
>
|
>
|
||||||
<div
|
{"dateRange" in pickerState ? (
|
||||||
className={classNames("c__date-picker__wrapper", {
|
<>
|
||||||
"c__date-picker__wrapper--clickable": labelAsPlaceholder,
|
|
||||||
})}
|
|
||||||
ref={ref}
|
|
||||||
{...pickerProps.groupProps}
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
onClick={() => !pickerState.isOpen && pickerState.open()}
|
|
||||||
>
|
|
||||||
{"dateRange" in pickerState ? (
|
|
||||||
<>
|
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
name={name && `${name}_start`}
|
|
||||||
value={convertDateValueToString(
|
|
||||||
pickerState.value?.start ?? null,
|
|
||||||
props.timezone,
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
name={name && `${name}_end`}
|
|
||||||
value={convertDateValueToString(
|
|
||||||
pickerState.value?.end ?? null,
|
|
||||||
props.timezone,
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<input
|
<input
|
||||||
type="hidden"
|
type="hidden"
|
||||||
name={name}
|
name={name && `${name}_start`}
|
||||||
value={convertDateValueToString(
|
value={convertDateValueToString(
|
||||||
pickerState.value,
|
pickerState.value?.start ?? null,
|
||||||
props.timezone,
|
props.timezone,
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
<input
|
||||||
<div className="c__date-picker__wrapper__icon">
|
type="hidden"
|
||||||
<Button
|
name={name && `${name}_end`}
|
||||||
type="button"
|
value={convertDateValueToString(
|
||||||
onKeyDown={(e) => {
|
pickerState.value?.end ?? null,
|
||||||
if (e.key === "Enter") {
|
props.timezone,
|
||||||
pickerState.toggle();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onClick={pickerState.toggle}
|
|
||||||
aria-label={t(
|
|
||||||
pickerState.isOpen
|
|
||||||
? "components.forms.date_picker.toggle_button_aria_label_close"
|
|
||||||
: "components.forms.date_picker.toggle_button_aria_label_open",
|
|
||||||
)}
|
)}
|
||||||
color="tertiary-text"
|
|
||||||
size="small"
|
|
||||||
className="c__date-picker__wrapper__toggle"
|
|
||||||
icon={
|
|
||||||
<span className="material-icons icon">calendar_today</span>
|
|
||||||
}
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</>
|
||||||
{children}
|
) : (
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name={name}
|
||||||
|
value={convertDateValueToString(
|
||||||
|
pickerState.value,
|
||||||
|
props.timezone,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="c__date-picker__wrapper__icon">
|
||||||
<Button
|
<Button
|
||||||
className={classNames("c__date-picker__inner__action", {
|
type="button"
|
||||||
"c__date-picker__inner__action--empty": !pickerState.value,
|
|
||||||
"c__date-picker__inner__action--hidden":
|
|
||||||
labelAsPlaceholder || disabled,
|
|
||||||
})}
|
|
||||||
color="tertiary-text"
|
|
||||||
size="nano"
|
|
||||||
icon={<span className="material-icons">close</span>}
|
|
||||||
onClick={onClear}
|
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
onClear();
|
pickerState.toggle();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onClick={pickerState.toggle}
|
||||||
aria-label={t(
|
aria-label={t(
|
||||||
"components.forms.date_picker.clear_button_aria_label",
|
pickerState.isOpen
|
||||||
|
? "components.forms.date_picker.toggle_button_aria_label_close"
|
||||||
|
: "components.forms.date_picker.toggle_button_aria_label_open",
|
||||||
)}
|
)}
|
||||||
|
color="tertiary-text"
|
||||||
|
size="small"
|
||||||
|
className="c__date-picker__wrapper__toggle"
|
||||||
|
icon={
|
||||||
|
<span className="material-icons icon">calendar_today</span>
|
||||||
|
}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
type="button"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{pickerState.isOpen && (
|
{children}
|
||||||
<Popover
|
<Button
|
||||||
parentRef={pickerRef}
|
className={classNames("c__date-picker__inner__action", {
|
||||||
onClickOutside={pickerState.close}
|
"c__date-picker__inner__action--empty": !pickerState.value,
|
||||||
borderless
|
"c__date-picker__inner__action--hidden":
|
||||||
>
|
labelAsPlaceholder || disabled,
|
||||||
{calendar}
|
})}
|
||||||
</Popover>
|
color="tertiary-text"
|
||||||
)}
|
size="nano"
|
||||||
|
icon={<span className="material-icons">close</span>}
|
||||||
|
onClick={onClear}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
onClear();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
aria-label={t(
|
||||||
|
"components.forms.date_picker.clear_button_aria_label",
|
||||||
|
)}
|
||||||
|
disabled={disabled}
|
||||||
|
type="button"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Field>
|
{pickerState.isOpen && (
|
||||||
</I18nProvider>
|
<Popover
|
||||||
);
|
parentRef={pickerRef}
|
||||||
},
|
onClickOutside={pickerState.close}
|
||||||
);
|
borderless
|
||||||
|
>
|
||||||
|
{calendar}
|
||||||
|
</Popover>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Field>
|
||||||
|
</I18nProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default DatePickerAux;
|
export default DatePickerAux;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, {
|
import React, {
|
||||||
forwardRef,
|
|
||||||
PropsWithChildren,
|
PropsWithChildren,
|
||||||
|
RefAttributes,
|
||||||
useEffect,
|
useEffect,
|
||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
useRef,
|
useRef,
|
||||||
@@ -15,147 +15,141 @@ import {
|
|||||||
FileUploaderRefType,
|
FileUploaderRefType,
|
||||||
} from ":/components/Forms/FileUploader/index";
|
} from ":/components/Forms/FileUploader/index";
|
||||||
|
|
||||||
interface DropZoneProps extends FileUploaderProps, PropsWithChildren {
|
type DropZoneProps = FileUploaderProps &
|
||||||
files: File[];
|
RefAttributes<FileUploaderRefType> &
|
||||||
}
|
PropsWithChildren<{
|
||||||
|
files: File[];
|
||||||
|
}>;
|
||||||
|
|
||||||
export const DropZone = forwardRef<FileUploaderRefType, DropZoneProps>(
|
export const DropZone = ({
|
||||||
(
|
multiple,
|
||||||
{
|
name,
|
||||||
multiple,
|
state,
|
||||||
name,
|
icon,
|
||||||
state,
|
animateIcon,
|
||||||
icon,
|
successIcon,
|
||||||
animateIcon,
|
uploadingIcon,
|
||||||
successIcon,
|
text,
|
||||||
uploadingIcon,
|
bigText,
|
||||||
text,
|
files,
|
||||||
bigText,
|
onFilesChange,
|
||||||
files,
|
children,
|
||||||
onFilesChange,
|
ref,
|
||||||
children,
|
...props
|
||||||
...props
|
}: DropZoneProps) => {
|
||||||
}: DropZoneProps,
|
const [dragActive, setDragActive] = useState(false);
|
||||||
ref,
|
const container = useRef<HTMLLabelElement>(null);
|
||||||
) => {
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const [dragActive, setDragActive] = useState(false);
|
const { t } = useCunningham();
|
||||||
const container = useRef<HTMLLabelElement>(null);
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
|
||||||
const { t } = useCunningham();
|
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
get input() {
|
get input() {
|
||||||
return inputRef.current;
|
return inputRef.current;
|
||||||
},
|
},
|
||||||
reset() {
|
reset() {
|
||||||
onFilesChange?.({ target: { value: [] } });
|
onFilesChange?.({ target: { value: [] } });
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!inputRef.current) {
|
if (!inputRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
replaceInputFilters(inputRef.current, files);
|
replaceInputFilters(inputRef.current, files);
|
||||||
}, [files]);
|
}, [files]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onFilesChange?.({ target: { value: files ?? [] } });
|
onFilesChange?.({ target: { value: files ?? [] } });
|
||||||
}, [files]);
|
}, [files]);
|
||||||
|
|
||||||
const renderIcon = () => {
|
const renderIcon = () => {
|
||||||
if (state === "success") {
|
if (state === "success") {
|
||||||
return successIcon ?? <span className="material-icons">done</span>;
|
return successIcon ?? <span className="material-icons">done</span>;
|
||||||
}
|
}
|
||||||
if (state === "uploading") {
|
if (state === "uploading") {
|
||||||
return React.cloneElement(uploadingIcon ?? <Loader size="small" />, {
|
return React.cloneElement(uploadingIcon ?? <Loader size="small" />, {
|
||||||
"aria-label": t("components.forms.file_uploader.uploading"),
|
"aria-label": t("components.forms.file_uploader.uploading"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return icon ?? <span className="material-icons">upload</span>;
|
return icon ?? <span className="material-icons">upload</span>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderCaption = () => {
|
|
||||||
if (state === "uploading") {
|
|
||||||
return t("components.forms.file_uploader.uploading");
|
|
||||||
}
|
|
||||||
if (bigText) {
|
|
||||||
return bigText;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{t("components.forms.file_uploader.caption")}
|
|
||||||
<span>{t("components.forms.file_uploader.browse_files")}</span>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
const renderCaption = () => {
|
||||||
|
if (state === "uploading") {
|
||||||
|
return t("components.forms.file_uploader.uploading");
|
||||||
|
}
|
||||||
|
if (bigText) {
|
||||||
|
return bigText;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<label
|
<>
|
||||||
className={classNames(
|
{t("components.forms.file_uploader.caption")}
|
||||||
"c__file-uploader",
|
<span>{t("components.forms.file_uploader.browse_files")}</span>
|
||||||
"c__file-uploader--" + state,
|
</>
|
||||||
{
|
|
||||||
"c__file-uploader--active": dragActive,
|
|
||||||
"c__file-uploader--animate-icon": animateIcon,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
onDragEnter={() => {
|
|
||||||
setDragActive(true);
|
|
||||||
}}
|
|
||||||
onDragLeave={(e) => {
|
|
||||||
/**
|
|
||||||
* This condition is important because onDragLeave is called when the cursor goes over
|
|
||||||
* a child of the current node, which is not intuitive. So here we need to make sure that
|
|
||||||
* the relatedTarget is not a child of the current node.
|
|
||||||
*/
|
|
||||||
if (!container.current!.contains(e.relatedTarget as Node)) {
|
|
||||||
setDragActive(false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onDragOver={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
}}
|
|
||||||
onDrop={(e) => {
|
|
||||||
// To prevent a new tab to open.
|
|
||||||
e.preventDefault();
|
|
||||||
const newFiles = Array.from(e.dataTransfer.files);
|
|
||||||
if (inputRef.current) {
|
|
||||||
inputRef.current.files = e.dataTransfer.files;
|
|
||||||
onFilesChange?.({ target: { value: [...newFiles] } });
|
|
||||||
}
|
|
||||||
setDragActive(false);
|
|
||||||
}}
|
|
||||||
ref={container}
|
|
||||||
>
|
|
||||||
<div className="c__file-uploader__inner">
|
|
||||||
<div className="c__file-uploader__inner__icon">{renderIcon()}</div>
|
|
||||||
{children ?? (
|
|
||||||
<>
|
|
||||||
<div className="c__file-uploader__inner__caption">
|
|
||||||
{renderCaption()}
|
|
||||||
</div>
|
|
||||||
{text && (
|
|
||||||
<div className="c__file-uploader__inner__text">{text}</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
name={name}
|
|
||||||
ref={inputRef}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (e.target.files) {
|
|
||||||
onFilesChange?.({ target: { value: [...e.target.files] } });
|
|
||||||
} else {
|
|
||||||
onFilesChange?.({ target: { value: [] } });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
multiple={multiple}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
);
|
);
|
||||||
},
|
};
|
||||||
);
|
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
className={classNames("c__file-uploader", "c__file-uploader--" + state, {
|
||||||
|
"c__file-uploader--active": dragActive,
|
||||||
|
"c__file-uploader--animate-icon": animateIcon,
|
||||||
|
})}
|
||||||
|
onDragEnter={() => {
|
||||||
|
setDragActive(true);
|
||||||
|
}}
|
||||||
|
onDragLeave={(e) => {
|
||||||
|
/**
|
||||||
|
* This condition is important because onDragLeave is called when the cursor goes over
|
||||||
|
* a child of the current node, which is not intuitive. So here we need to make sure that
|
||||||
|
* the relatedTarget is not a child of the current node.
|
||||||
|
*/
|
||||||
|
if (!container.current!.contains(e.relatedTarget as Node)) {
|
||||||
|
setDragActive(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onDragOver={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
}}
|
||||||
|
onDrop={(e) => {
|
||||||
|
// To prevent a new tab to open.
|
||||||
|
e.preventDefault();
|
||||||
|
const newFiles = Array.from(e.dataTransfer.files);
|
||||||
|
if (inputRef.current) {
|
||||||
|
inputRef.current.files = e.dataTransfer.files;
|
||||||
|
onFilesChange?.({ target: { value: [...newFiles] } });
|
||||||
|
}
|
||||||
|
setDragActive(false);
|
||||||
|
}}
|
||||||
|
ref={container}
|
||||||
|
>
|
||||||
|
<div className="c__file-uploader__inner">
|
||||||
|
<div className="c__file-uploader__inner__icon">{renderIcon()}</div>
|
||||||
|
{children ?? (
|
||||||
|
<>
|
||||||
|
<div className="c__file-uploader__inner__caption">
|
||||||
|
{renderCaption()}
|
||||||
|
</div>
|
||||||
|
{text && (
|
||||||
|
<div className="c__file-uploader__inner__text">{text}</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
name={name}
|
||||||
|
ref={inputRef}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.files) {
|
||||||
|
onFilesChange?.({ target: { value: [...e.target.files] } });
|
||||||
|
} else {
|
||||||
|
onFilesChange?.({ target: { value: [] } });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
multiple={multiple}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
import React, { forwardRef, useEffect, useMemo, useState } from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
import { useCunningham } from ":/components/Provider";
|
import { useCunningham } from ":/components/Provider";
|
||||||
import { Button } from ":/components/Button";
|
import { Button } from ":/components/Button";
|
||||||
import {
|
import { FileUploaderProps } from ":/components/Forms/FileUploader/index";
|
||||||
FileUploaderProps,
|
|
||||||
FileUploaderRefType,
|
|
||||||
} from ":/components/Forms/FileUploader/index";
|
|
||||||
import { DropZone } from ":/components/Forms/FileUploader/DropZone";
|
import { DropZone } from ":/components/Forms/FileUploader/DropZone";
|
||||||
|
|
||||||
export const FileUploaderMono = forwardRef<
|
export const FileUploaderMono = ({
|
||||||
FileUploaderRefType,
|
fakeDefaultFiles,
|
||||||
FileUploaderProps
|
ref,
|
||||||
>(({ fakeDefaultFiles, ...props }, ref) => {
|
...props
|
||||||
|
}: FileUploaderProps) => {
|
||||||
const { t } = useCunningham();
|
const { t } = useCunningham();
|
||||||
const [file, setFile] = useState<File | undefined>(
|
const [file, setFile] = useState<File | undefined>(
|
||||||
fakeDefaultFiles && fakeDefaultFiles.length > 0
|
fakeDefaultFiles && fakeDefaultFiles.length > 0
|
||||||
@@ -91,4 +89,4 @@ export const FileUploaderMono = forwardRef<
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
import React, { forwardRef, useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useCunningham } from ":/components/Provider";
|
import { useCunningham } from ":/components/Provider";
|
||||||
import { formatBytes } from ":/components/Forms/FileUploader/utils";
|
import { formatBytes } from ":/components/Forms/FileUploader/utils";
|
||||||
import { Button } from ":/components/Button";
|
import { Button } from ":/components/Button";
|
||||||
import {
|
import { FileUploaderProps } from ":/components/Forms/FileUploader/index";
|
||||||
FileUploaderProps,
|
|
||||||
FileUploaderRefType,
|
|
||||||
} from ":/components/Forms/FileUploader/index";
|
|
||||||
import { DropZone } from ":/components/Forms/FileUploader/DropZone";
|
import { DropZone } from ":/components/Forms/FileUploader/DropZone";
|
||||||
|
|
||||||
export const FileUploaderMulti = forwardRef<
|
export const FileUploaderMulti = ({
|
||||||
FileUploaderRefType,
|
fullWidth,
|
||||||
FileUploaderProps
|
fakeDefaultFiles,
|
||||||
>(({ fullWidth, fakeDefaultFiles, ...props }, ref) => {
|
ref,
|
||||||
|
...props
|
||||||
|
}: FileUploaderProps) => {
|
||||||
const { t } = useCunningham();
|
const { t } = useCunningham();
|
||||||
const [files, setFiles] = useState<File[]>(fakeDefaultFiles || []);
|
const [files, setFiles] = useState<File[]>(fakeDefaultFiles || []);
|
||||||
|
|
||||||
@@ -59,4 +58,4 @@ export const FileUploaderMulti = forwardRef<
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import React, { forwardRef, InputHTMLAttributes, ReactElement } from "react";
|
import React, { InputHTMLAttributes, ReactElement, RefAttributes } from "react";
|
||||||
import { Field, FieldProps, FieldState } from ":/components/Forms/Field";
|
import { Field, FieldProps, FieldState } from ":/components/Forms/Field";
|
||||||
import { FileUploaderMulti } from ":/components/Forms/FileUploader/FileUploaderMulti";
|
import { FileUploaderMulti } from ":/components/Forms/FileUploader/FileUploaderMulti";
|
||||||
import { FileUploaderMono } from ":/components/Forms/FileUploader/FileUploaderMono";
|
import { FileUploaderMono } from ":/components/Forms/FileUploader/FileUploaderMono";
|
||||||
|
|
||||||
export interface FileUploaderProps
|
export interface FileUploaderProps
|
||||||
extends Omit<FieldProps, "state">,
|
extends Omit<FieldProps, "state">,
|
||||||
InputHTMLAttributes<HTMLInputElement> {
|
InputHTMLAttributes<HTMLInputElement>,
|
||||||
|
RefAttributes<FileUploaderRefType> {
|
||||||
state?: FieldState | "uploading" | undefined;
|
state?: FieldState | "uploading" | undefined;
|
||||||
multiple?: boolean;
|
multiple?: boolean;
|
||||||
icon?: ReactElement;
|
icon?: ReactElement;
|
||||||
@@ -25,16 +26,18 @@ export interface FileUploaderRefType {
|
|||||||
reset: () => void;
|
reset: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FileUploader = forwardRef<FileUploaderRefType, FileUploaderProps>(
|
export const FileUploader = ({
|
||||||
({ fullWidth, ...props }, ref) => {
|
fullWidth,
|
||||||
return (
|
ref,
|
||||||
<Field fullWidth={fullWidth} className={props.className}>
|
...props
|
||||||
{props.multiple ? (
|
}: FileUploaderProps) => {
|
||||||
<FileUploaderMulti {...props} ref={ref} />
|
return (
|
||||||
) : (
|
<Field fullWidth={fullWidth} className={props.className}>
|
||||||
<FileUploaderMono {...props} ref={ref} />
|
{props.multiple ? (
|
||||||
)}
|
<FileUploaderMulti {...props} ref={ref} />
|
||||||
</Field>
|
) : (
|
||||||
);
|
<FileUploaderMono {...props} ref={ref} />
|
||||||
},
|
)}
|
||||||
);
|
</Field>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import React, { forwardRef } from "react";
|
import React from "react";
|
||||||
import { Input, InputProps } from ":/components/Forms/Input/index";
|
import { Input, InputProps } from ":/components/Forms/Input/index";
|
||||||
import { Button } from ":/components/Button";
|
import { Button } from ":/components/Button";
|
||||||
import { useCunningham } from ":/components/Provider";
|
import { useCunningham } from ":/components/Provider";
|
||||||
|
|
||||||
export const InputPassword = forwardRef<
|
export const InputPassword = (props: Omit<InputProps, "rightIcon">) => {
|
||||||
HTMLInputElement,
|
|
||||||
Omit<InputProps, "rightIcon">
|
|
||||||
>((props: InputProps, ref) => {
|
|
||||||
const [showPassword, setShowPassword] = React.useState(false);
|
const [showPassword, setShowPassword] = React.useState(false);
|
||||||
const { className, ...otherProps } = props;
|
const { className, ...otherProps } = props;
|
||||||
const customClassName = "c__input--password";
|
const customClassName = "c__input--password";
|
||||||
@@ -14,7 +11,6 @@ export const InputPassword = forwardRef<
|
|||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
ref={ref}
|
|
||||||
className={className + " " + customClassName}
|
className={className + " " + customClassName}
|
||||||
type={showPassword ? "text" : "password"}
|
type={showPassword ? "text" : "password"}
|
||||||
rightIcon={
|
rightIcon={
|
||||||
@@ -38,4 +34,4 @@ export const InputPassword = forwardRef<
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, {
|
import React, {
|
||||||
forwardRef,
|
|
||||||
InputHTMLAttributes,
|
InputHTMLAttributes,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
|
RefAttributes,
|
||||||
useEffect,
|
useEffect,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
@@ -20,123 +20,118 @@ export type InputOnlyProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type InputProps = InputHTMLAttributes<HTMLInputElement> &
|
export type InputProps = InputHTMLAttributes<HTMLInputElement> &
|
||||||
|
RefAttributes<HTMLInputElement> &
|
||||||
FieldProps &
|
FieldProps &
|
||||||
InputOnlyProps;
|
InputOnlyProps;
|
||||||
|
|
||||||
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
export const Input = ({
|
||||||
(
|
className,
|
||||||
{
|
defaultValue,
|
||||||
className,
|
label,
|
||||||
defaultValue,
|
id,
|
||||||
label,
|
icon,
|
||||||
id,
|
rightIcon,
|
||||||
icon,
|
charCounter,
|
||||||
rightIcon,
|
charCounterMax,
|
||||||
charCounter,
|
ref,
|
||||||
charCounterMax,
|
...props
|
||||||
...props
|
}: InputProps) => {
|
||||||
}: InputProps,
|
const classes = ["c__input"];
|
||||||
ref,
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
) => {
|
const [inputFocus, setInputFocus] = useState(false);
|
||||||
const classes = ["c__input"];
|
const [value, setValue] = useState(defaultValue || props.value || "");
|
||||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
const [labelAsPlaceholder, setLabelAsPlaceholder] = useState(!value);
|
||||||
const [inputFocus, setInputFocus] = useState(false);
|
const idToUse = useRef(id || randomString());
|
||||||
const [value, setValue] = useState(defaultValue || props.value || "");
|
const rightTextToUse = charCounter
|
||||||
const [labelAsPlaceholder, setLabelAsPlaceholder] = useState(!value);
|
? `${value.toString().length}/${charCounterMax}`
|
||||||
const idToUse = useRef(id || randomString());
|
: props.rightText;
|
||||||
const rightTextToUse = charCounter
|
|
||||||
? `${value.toString().length}/${charCounterMax}`
|
|
||||||
: props.rightText;
|
|
||||||
|
|
||||||
const updateLabel = () => {
|
const updateLabel = () => {
|
||||||
if (inputFocus) {
|
if (inputFocus) {
|
||||||
setLabelAsPlaceholder(false);
|
setLabelAsPlaceholder(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setLabelAsPlaceholder(!value);
|
setLabelAsPlaceholder(!value);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateLabel();
|
updateLabel();
|
||||||
}, [inputFocus, value]);
|
}, [inputFocus, value]);
|
||||||
|
|
||||||
// If the input is used as a controlled component, we need to update the local value.
|
// If the input is used as a controlled component, we need to update the local value.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (defaultValue !== undefined) {
|
if (defaultValue !== undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setValue(props.value || "");
|
setValue(props.value || "");
|
||||||
}, [props.value]);
|
}, [props.value]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
compact,
|
compact,
|
||||||
fullWidth,
|
fullWidth,
|
||||||
rightText,
|
rightText,
|
||||||
state,
|
state,
|
||||||
text,
|
text,
|
||||||
textItems,
|
textItems,
|
||||||
...inputProps
|
...inputProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Field {...props} rightText={rightTextToUse} className={className}>
|
<Field {...props} rightText={rightTextToUse} className={className}>
|
||||||
{/* We disabled linting for this specific line because we consider that the onClick props is only used for */}
|
{/* We disabled linting for this specific line because we consider that the onClick props is only used for */}
|
||||||
{/* mouse users, so this do not engender any issue for accessibility. */}
|
{/* mouse users, so this do not engender any issue for accessibility. */}
|
||||||
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
|
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"c__input__wrapper",
|
"c__input__wrapper",
|
||||||
props.state && "c__input__wrapper--" + props.state,
|
props.state && "c__input__wrapper--" + props.state,
|
||||||
{
|
{
|
||||||
"c__input__wrapper--disabled": props.disabled,
|
"c__input__wrapper--disabled": props.disabled,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
inputRef.current?.focus();
|
inputRef.current?.focus();
|
||||||
}}
|
}}
|
||||||
|
>
|
||||||
|
{!!icon && <div className="c__input__icon-left">{icon}</div>}
|
||||||
|
<LabelledBox
|
||||||
|
label={label}
|
||||||
|
htmlFor={idToUse.current}
|
||||||
|
labelAsPlaceholder={labelAsPlaceholder}
|
||||||
|
disabled={props.disabled}
|
||||||
>
|
>
|
||||||
{!!icon && <div className="c__input__icon-left">{icon}</div>}
|
<input
|
||||||
<LabelledBox
|
type="text"
|
||||||
label={label}
|
className={classes.join(" ")}
|
||||||
htmlFor={idToUse.current}
|
{...inputProps}
|
||||||
labelAsPlaceholder={labelAsPlaceholder}
|
id={idToUse.current}
|
||||||
disabled={props.disabled}
|
value={value}
|
||||||
>
|
onFocus={(e) => {
|
||||||
<input
|
setInputFocus(true);
|
||||||
type="text"
|
props.onFocus?.(e);
|
||||||
className={classes.join(" ")}
|
}}
|
||||||
{...inputProps}
|
onBlur={(e) => {
|
||||||
id={idToUse.current}
|
setInputFocus(false);
|
||||||
value={value}
|
props.onBlur?.(e);
|
||||||
onFocus={(e) => {
|
}}
|
||||||
setInputFocus(true);
|
onChange={(e) => {
|
||||||
props.onFocus?.(e);
|
setValue(e.target.value);
|
||||||
}}
|
props.onChange?.(e);
|
||||||
onBlur={(e) => {
|
}}
|
||||||
setInputFocus(false);
|
ref={(inputTextRef) => {
|
||||||
props.onBlur?.(e);
|
if (ref) {
|
||||||
}}
|
if (typeof ref === "function") {
|
||||||
onChange={(e) => {
|
ref(inputTextRef);
|
||||||
setValue(e.target.value);
|
} else {
|
||||||
props.onChange?.(e);
|
ref.current = inputTextRef;
|
||||||
}}
|
|
||||||
ref={(inputTextRef) => {
|
|
||||||
if (ref) {
|
|
||||||
if (typeof ref === "function") {
|
|
||||||
ref(inputTextRef);
|
|
||||||
} else {
|
|
||||||
ref.current = inputTextRef;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
inputRef.current = inputTextRef;
|
}
|
||||||
}}
|
inputRef.current = inputTextRef;
|
||||||
/>
|
}}
|
||||||
</LabelledBox>
|
/>
|
||||||
{!!rightIcon && (
|
</LabelledBox>
|
||||||
<div className="c__input__icon-right">{rightIcon}</div>
|
{!!rightIcon && <div className="c__input__icon-right">{rightIcon}</div>}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</Field>
|
||||||
</Field>
|
);
|
||||||
);
|
};
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import React, {
|
import React, {
|
||||||
InputHTMLAttributes,
|
InputHTMLAttributes,
|
||||||
PropsWithChildren,
|
PropsWithChildren,
|
||||||
forwardRef,
|
|
||||||
ReactNode,
|
ReactNode,
|
||||||
|
RefAttributes,
|
||||||
} from "react";
|
} from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { Field, FieldProps } from ":/components/Forms/Field";
|
import { Field, FieldProps } from ":/components/Forms/Field";
|
||||||
@@ -12,38 +12,37 @@ export type RadioOnlyProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type RadioProps = InputHTMLAttributes<HTMLInputElement> &
|
export type RadioProps = InputHTMLAttributes<HTMLInputElement> &
|
||||||
|
RefAttributes<HTMLInputElement> &
|
||||||
FieldProps &
|
FieldProps &
|
||||||
RadioOnlyProps;
|
RadioOnlyProps;
|
||||||
|
|
||||||
export const Radio = forwardRef<HTMLInputElement, RadioProps>(
|
export const Radio = ({ className, label, ref, ...props }: RadioProps) => {
|
||||||
({ className, label, ...props }: RadioProps, ref) => {
|
const {
|
||||||
const {
|
compact,
|
||||||
compact,
|
fullWidth,
|
||||||
fullWidth,
|
rightText,
|
||||||
rightText,
|
state,
|
||||||
state,
|
text,
|
||||||
text,
|
textItems,
|
||||||
textItems,
|
...inputProps
|
||||||
...inputProps
|
} = props;
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
className={classNames("c__checkbox", "c__radio", className, {
|
className={classNames("c__checkbox", "c__radio", className, {
|
||||||
"c__checkbox--disabled": props.disabled,
|
"c__checkbox--disabled": props.disabled,
|
||||||
"c__checkbox--full-width": props.fullWidth,
|
"c__checkbox--full-width": props.fullWidth,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Field compact={true} {...props}>
|
<Field compact={true} {...props}>
|
||||||
<div className="c__checkbox__container">
|
<div className="c__checkbox__container">
|
||||||
<input type="radio" {...inputProps} ref={ref} />
|
<input type="radio" {...inputProps} ref={ref} />
|
||||||
{label && <div className="c__checkbox__label">{label}</div>}
|
{label && <div className="c__checkbox__label">{label}</div>}
|
||||||
</div>
|
</div>
|
||||||
</Field>
|
</Field>
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
},
|
};
|
||||||
);
|
|
||||||
|
|
||||||
export const RadioGroup = ({
|
export const RadioGroup = ({
|
||||||
className,
|
className,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { forwardRef, PropsWithChildren, ReactNode } from "react";
|
import React, { PropsWithChildren, ReactNode, RefAttributes } from "react";
|
||||||
import { SelectMulti } from ":/components/Forms/Select/multi";
|
import { SelectMulti } from ":/components/Forms/Select/multi";
|
||||||
import { SelectMono } from ":/components/Forms/Select/mono";
|
import { SelectMono } from ":/components/Forms/Select/mono";
|
||||||
import { FieldProps } from ":/components/Forms/Field";
|
import { FieldProps } from ":/components/Forms/Field";
|
||||||
@@ -28,6 +28,7 @@ export interface SelectHandle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type SelectProps = PropsWithChildren &
|
export type SelectProps = PropsWithChildren &
|
||||||
|
RefAttributes<SelectHandle> &
|
||||||
FieldProps & {
|
FieldProps & {
|
||||||
label: string;
|
label: string;
|
||||||
hideLabel?: boolean;
|
hideLabel?: boolean;
|
||||||
@@ -53,16 +54,12 @@ export type SelectProps = PropsWithChildren &
|
|||||||
target: { value: string | undefined };
|
target: { value: string | undefined };
|
||||||
}) => void;
|
}) => void;
|
||||||
};
|
};
|
||||||
export const Select = forwardRef<SelectHandle, SelectProps>((props, ref) => {
|
export const Select = (props: SelectProps) => {
|
||||||
if (props.defaultValue && props.value) {
|
if (props.defaultValue && props.value) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"You cannot use both defaultValue and value props on Select component",
|
"You cannot use both defaultValue and value props on Select component",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return props.multi ? (
|
return props.multi ? <SelectMulti {...props} /> : <SelectMono {...props} />;
|
||||||
<SelectMulti {...props} ref={ref} />
|
};
|
||||||
) : (
|
|
||||||
<SelectMono {...props} ref={ref} />
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
import React, {
|
import React, { useEffect, useImperativeHandle, useRef, useState } from "react";
|
||||||
forwardRef,
|
|
||||||
useEffect,
|
|
||||||
useImperativeHandle,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { useCombobox } from "downshift";
|
import { useCombobox } from "downshift";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useCunningham } from ":/components/Provider";
|
import { useCunningham } from ":/components/Provider";
|
||||||
@@ -15,142 +9,143 @@ import {
|
|||||||
SelectMonoAux,
|
SelectMonoAux,
|
||||||
SubProps,
|
SubProps,
|
||||||
} from ":/components/Forms/Select/mono-common";
|
} from ":/components/Forms/Select/mono-common";
|
||||||
import { SelectHandle } from ":/components/Forms/Select";
|
|
||||||
import { isOptionWithRender } from ":/components/Forms/Select/utils";
|
import { isOptionWithRender } from ":/components/Forms/Select/utils";
|
||||||
|
|
||||||
export const SelectMonoSearchable = forwardRef<SelectHandle, SubProps>(
|
export const SelectMonoSearchable = ({
|
||||||
({ showLabelWhenSelected = true, ...props }, ref) => {
|
showLabelWhenSelected = true,
|
||||||
const { t } = useCunningham();
|
ref,
|
||||||
const [optionsToDisplay, setOptionsToDisplay] = useState(props.options);
|
...props
|
||||||
const [hasInputFocused, setHasInputFocused] = useState(false);
|
}: SubProps) => {
|
||||||
const [inputFilter, setInputFilter] = useState<string>();
|
const { t } = useCunningham();
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const [optionsToDisplay, setOptionsToDisplay] = useState(props.options);
|
||||||
const downshiftReturn = useCombobox({
|
const [hasInputFocused, setHasInputFocused] = useState(false);
|
||||||
...props.downshiftProps,
|
const [inputFilter, setInputFilter] = useState<string>();
|
||||||
items: optionsToDisplay,
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
itemToString: optionToString,
|
const downshiftReturn = useCombobox({
|
||||||
onInputValueChange: (e) => {
|
...props.downshiftProps,
|
||||||
setInputFilter(e.inputValue);
|
items: optionsToDisplay,
|
||||||
if (!e.inputValue) {
|
itemToString: optionToString,
|
||||||
downshiftReturn.selectItem(null);
|
onInputValueChange: (e) => {
|
||||||
}
|
setInputFilter(e.inputValue);
|
||||||
},
|
if (!e.inputValue) {
|
||||||
});
|
downshiftReturn.selectItem(null);
|
||||||
const [labelAsPlaceholder, setLabelAsPlaceholder] = useState(
|
|
||||||
!downshiftReturn.selectedItem,
|
|
||||||
);
|
|
||||||
useEffect(() => {
|
|
||||||
if (hasInputFocused || downshiftReturn.inputValue) {
|
|
||||||
setLabelAsPlaceholder(false);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
setLabelAsPlaceholder(!downshiftReturn.selectedItem);
|
},
|
||||||
}, [
|
});
|
||||||
downshiftReturn.selectedItem,
|
const [labelAsPlaceholder, setLabelAsPlaceholder] = useState(
|
||||||
hasInputFocused,
|
!downshiftReturn.selectedItem,
|
||||||
downshiftReturn.inputValue,
|
);
|
||||||
]);
|
useEffect(() => {
|
||||||
|
if (hasInputFocused || downshiftReturn.inputValue) {
|
||||||
|
setLabelAsPlaceholder(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLabelAsPlaceholder(!downshiftReturn.selectedItem);
|
||||||
|
}, [
|
||||||
|
downshiftReturn.selectedItem,
|
||||||
|
hasInputFocused,
|
||||||
|
downshiftReturn.inputValue,
|
||||||
|
]);
|
||||||
|
|
||||||
// Similar to: useKeepSelectedItemInSyncWithOptions ( see docs )
|
// Similar to: useKeepSelectedItemInSyncWithOptions ( see docs )
|
||||||
// The only difference is that it does not apply when there is an inputFilter. ( See below why )
|
// The only difference is that it does not apply when there is an inputFilter. ( See below why )
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// If there is an inputFilter, using selectItem will trigger onInputValueChange that will sets inputFilter to
|
// If there is an inputFilter, using selectItem will trigger onInputValueChange that will sets inputFilter to
|
||||||
// empty, and then ignoring the existing filter and displaying all options.
|
// empty, and then ignoring the existing filter and displaying all options.
|
||||||
if (inputFilter) {
|
if (inputFilter) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const optionToSelect = props.options.find(
|
const optionToSelect = props.options.find(
|
||||||
(option) => optionToValue(option) === props.value,
|
(option) => optionToValue(option) === props.value,
|
||||||
|
);
|
||||||
|
downshiftReturn.selectItem(optionToSelect ?? null);
|
||||||
|
}, [props.value, props.options, inputFilter]);
|
||||||
|
|
||||||
|
// Even there is already a value selected, when opening the combobox menu we want to display all available choices.
|
||||||
|
useEffect(() => {
|
||||||
|
if (downshiftReturn.isOpen) {
|
||||||
|
setOptionsToDisplay(
|
||||||
|
inputFilter
|
||||||
|
? props.options.filter(getOptionsFilter(inputFilter))
|
||||||
|
: props.options,
|
||||||
);
|
);
|
||||||
downshiftReturn.selectItem(optionToSelect ?? null);
|
} else {
|
||||||
}, [props.value, props.options, inputFilter]);
|
setInputFilter(undefined);
|
||||||
|
}
|
||||||
|
}, [downshiftReturn.isOpen, props.options, inputFilter]);
|
||||||
|
|
||||||
// Even there is already a value selected, when opening the combobox menu we want to display all available choices.
|
useImperativeHandle(ref, () => ({
|
||||||
useEffect(() => {
|
blur: () => {
|
||||||
if (downshiftReturn.isOpen) {
|
downshiftReturn.closeMenu();
|
||||||
setOptionsToDisplay(
|
inputRef.current?.blur();
|
||||||
inputFilter
|
},
|
||||||
? props.options.filter(getOptionsFilter(inputFilter))
|
}));
|
||||||
: props.options,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
setInputFilter(undefined);
|
|
||||||
}
|
|
||||||
}, [downshiftReturn.isOpen, props.options, inputFilter]);
|
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useEffect(() => {
|
||||||
blur: () => {
|
props.onSearchInputChange?.({ target: { value: inputFilter } });
|
||||||
downshiftReturn.closeMenu();
|
}, [inputFilter]);
|
||||||
inputRef.current?.blur();
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
useEffect(() => {
|
const onInputBlur = () => {
|
||||||
props.onSearchInputChange?.({ target: { value: inputFilter } });
|
setHasInputFocused(false);
|
||||||
}, [inputFilter]);
|
if (downshiftReturn.selectedItem) {
|
||||||
|
// Here the goal is to make sure that when the input in blurred then the input value
|
||||||
|
// has exactly the selectedItem label. Which is not the case by default.
|
||||||
|
downshiftReturn.selectItem(downshiftReturn.selectedItem);
|
||||||
|
} else {
|
||||||
|
// We want the input to be empty when no item is selected.
|
||||||
|
downshiftReturn.setInputValue("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onInputBlur = () => {
|
const inputProps = downshiftReturn.getInputProps({
|
||||||
setHasInputFocused(false);
|
ref: inputRef,
|
||||||
if (downshiftReturn.selectedItem) {
|
disabled: props.disabled,
|
||||||
// Here the goal is to make sure that when the input in blurred then the input value
|
});
|
||||||
// has exactly the selectedItem label. Which is not the case by default.
|
|
||||||
downshiftReturn.selectItem(downshiftReturn.selectedItem);
|
|
||||||
} else {
|
|
||||||
// We want the input to be empty when no item is selected.
|
|
||||||
downshiftReturn.setInputValue("");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const inputProps = downshiftReturn.getInputProps({
|
const renderCustomSelectedOption = !showLabelWhenSelected;
|
||||||
ref: inputRef,
|
|
||||||
disabled: props.disabled,
|
|
||||||
});
|
|
||||||
|
|
||||||
const renderCustomSelectedOption = !showLabelWhenSelected;
|
return (
|
||||||
|
<SelectMonoAux
|
||||||
return (
|
{...props}
|
||||||
<SelectMonoAux
|
downshiftReturn={{
|
||||||
{...props}
|
...downshiftReturn,
|
||||||
downshiftReturn={{
|
wrapperProps: {
|
||||||
...downshiftReturn,
|
onClick: () => {
|
||||||
wrapperProps: {
|
inputRef.current?.focus();
|
||||||
onClick: () => {
|
// This is important because if we don't check that: when clicking on the toggle button
|
||||||
inputRef.current?.focus();
|
// when the menu is open, it will close and reopen immediately.
|
||||||
// This is important because if we don't check that: when clicking on the toggle button
|
if (!downshiftReturn.isOpen) {
|
||||||
// when the menu is open, it will close and reopen immediately.
|
downshiftReturn.openMenu();
|
||||||
if (!downshiftReturn.isOpen) {
|
}
|
||||||
downshiftReturn.openMenu();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
toggleButtonProps: downshiftReturn.getToggleButtonProps({
|
},
|
||||||
disabled: props.disabled,
|
toggleButtonProps: downshiftReturn.getToggleButtonProps({
|
||||||
"aria-label": t("components.forms.select.toggle_button_aria_label"),
|
disabled: props.disabled,
|
||||||
}),
|
"aria-label": t("components.forms.select.toggle_button_aria_label"),
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
labelAsPlaceholder={labelAsPlaceholder}
|
||||||
|
options={optionsToDisplay}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
{...inputProps}
|
||||||
|
className={classNames({
|
||||||
|
"c__select__inner__value__input--hidden":
|
||||||
|
renderCustomSelectedOption && !hasInputFocused,
|
||||||
|
})}
|
||||||
|
onFocus={() => {
|
||||||
|
setHasInputFocused(true);
|
||||||
}}
|
}}
|
||||||
labelAsPlaceholder={labelAsPlaceholder}
|
onBlur={() => {
|
||||||
options={optionsToDisplay}
|
onInputBlur();
|
||||||
>
|
}}
|
||||||
<input
|
/>
|
||||||
{...inputProps}
|
|
||||||
className={classNames({
|
|
||||||
"c__select__inner__value__input--hidden":
|
|
||||||
renderCustomSelectedOption && !hasInputFocused,
|
|
||||||
})}
|
|
||||||
onFocus={() => {
|
|
||||||
setHasInputFocused(true);
|
|
||||||
}}
|
|
||||||
onBlur={() => {
|
|
||||||
onInputBlur();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{renderCustomSelectedOption &&
|
{renderCustomSelectedOption &&
|
||||||
!hasInputFocused &&
|
!hasInputFocused &&
|
||||||
downshiftReturn.selectedItem &&
|
downshiftReturn.selectedItem &&
|
||||||
isOptionWithRender(downshiftReturn.selectedItem) &&
|
isOptionWithRender(downshiftReturn.selectedItem) &&
|
||||||
downshiftReturn.selectedItem.render()}
|
downshiftReturn.selectedItem.render()}
|
||||||
</SelectMonoAux>
|
</SelectMonoAux>
|
||||||
);
|
);
|
||||||
},
|
};
|
||||||
);
|
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
import { useSelect, UseSelectReturnValue } from "downshift";
|
import { useSelect, UseSelectReturnValue } from "downshift";
|
||||||
import React, {
|
import React, { useEffect, useImperativeHandle, useRef } from "react";
|
||||||
forwardRef,
|
|
||||||
useEffect,
|
|
||||||
useImperativeHandle,
|
|
||||||
useRef,
|
|
||||||
} from "react";
|
|
||||||
import {
|
import {
|
||||||
optionToString,
|
optionToString,
|
||||||
optionToValue,
|
optionToValue,
|
||||||
SelectMonoAux,
|
SelectMonoAux,
|
||||||
SubProps,
|
SubProps,
|
||||||
} from ":/components/Forms/Select/mono-common";
|
} from ":/components/Forms/Select/mono-common";
|
||||||
import { Option, SelectHandle, SelectProps } from ":/components/Forms/Select";
|
import { Option, SelectProps } from ":/components/Forms/Select";
|
||||||
import { SelectedOption } from ":/components/Forms/Select/utils";
|
import { SelectedOption } from ":/components/Forms/Select/utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -32,40 +27,38 @@ const useKeepSelectedItemInSyncWithOptions = (
|
|||||||
}, [props.value, props.options]);
|
}, [props.value, props.options]);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SelectMonoSimple = forwardRef<SelectHandle, SubProps>(
|
export const SelectMonoSimple = ({ ref, ...props }: SubProps) => {
|
||||||
(props, ref) => {
|
const downshiftReturn = useSelect({
|
||||||
const downshiftReturn = useSelect({
|
...props.downshiftProps,
|
||||||
...props.downshiftProps,
|
items: props.options,
|
||||||
items: props.options,
|
itemToString: optionToString,
|
||||||
itemToString: optionToString,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
useKeepSelectedItemInSyncWithOptions(downshiftReturn, props);
|
useKeepSelectedItemInSyncWithOptions(downshiftReturn, props);
|
||||||
|
|
||||||
const wrapperRef = useRef<HTMLElement>(null);
|
const wrapperRef = useRef<HTMLElement>(null);
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
blur: () => {
|
blur: () => {
|
||||||
downshiftReturn.closeMenu();
|
downshiftReturn.closeMenu();
|
||||||
wrapperRef.current?.blur();
|
wrapperRef.current?.blur();
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectMonoAux
|
<SelectMonoAux
|
||||||
{...props}
|
{...props}
|
||||||
downshiftReturn={{
|
downshiftReturn={{
|
||||||
...downshiftReturn,
|
...downshiftReturn,
|
||||||
wrapperProps: downshiftReturn.getToggleButtonProps({
|
wrapperProps: downshiftReturn.getToggleButtonProps({
|
||||||
disabled: props.disabled,
|
disabled: props.disabled,
|
||||||
ref: wrapperRef,
|
ref: wrapperRef,
|
||||||
}),
|
}),
|
||||||
toggleButtonProps: {},
|
toggleButtonProps: {},
|
||||||
}}
|
}}
|
||||||
labelAsPlaceholder={!downshiftReturn.selectedItem}
|
labelAsPlaceholder={!downshiftReturn.selectedItem}
|
||||||
>
|
>
|
||||||
<SelectedOption option={downshiftReturn.selectedItem} {...props} />
|
<SelectedOption option={downshiftReturn.selectedItem} {...props} />
|
||||||
</SelectMonoAux>
|
</SelectMonoAux>
|
||||||
);
|
);
|
||||||
},
|
};
|
||||||
);
|
|
||||||
|
|||||||
@@ -1,67 +1,63 @@
|
|||||||
import React, { forwardRef, useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { UseSelectStateChange } from "downshift";
|
import { UseSelectStateChange } from "downshift";
|
||||||
import { optionToValue, SubProps } from ":/components/Forms/Select/mono-common";
|
import { optionToValue, SubProps } from ":/components/Forms/Select/mono-common";
|
||||||
import { SelectMonoSearchable } from ":/components/Forms/Select/mono-searchable";
|
import { SelectMonoSearchable } from ":/components/Forms/Select/mono-searchable";
|
||||||
import { SelectMonoSimple } from ":/components/Forms/Select/mono-simple";
|
import { SelectMonoSimple } from ":/components/Forms/Select/mono-simple";
|
||||||
import { Option, SelectHandle, SelectProps } from ":/components/Forms/Select";
|
import { Option, SelectProps } from ":/components/Forms/Select";
|
||||||
|
|
||||||
export const SelectMono = forwardRef<SelectHandle, SelectProps>(
|
export const SelectMono = (props: SelectProps) => {
|
||||||
(props, ref) => {
|
const defaultSelectedItem = props.defaultValue
|
||||||
const defaultSelectedItem = props.defaultValue
|
? props.options.find(
|
||||||
? props.options.find(
|
(option) => optionToValue(option) === props.defaultValue,
|
||||||
(option) => optionToValue(option) === props.defaultValue,
|
)
|
||||||
)
|
: undefined;
|
||||||
: undefined;
|
const [value, setValue] = useState(
|
||||||
const [value, setValue] = useState(
|
defaultSelectedItem ? optionToValue(defaultSelectedItem) : props.value,
|
||||||
defaultSelectedItem ? optionToValue(defaultSelectedItem) : props.value,
|
);
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This useEffect is used to update the local value when the component is controlled.
|
* This useEffect is used to update the local value when the component is controlled.
|
||||||
* The defaultValue is used only on first render.
|
* The defaultValue is used only on first render.
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (props.defaultValue) {
|
if (props.defaultValue) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
setValue(props.value);
|
||||||
|
}, [props.value, props.defaultValue]);
|
||||||
|
|
||||||
|
const commonDownshiftProps: SubProps["downshiftProps"] = {
|
||||||
|
initialSelectedItem: defaultSelectedItem,
|
||||||
|
onSelectedItemChange: (e: UseSelectStateChange<Option>) => {
|
||||||
|
const eventCmp = e.selectedItem ? optionToValue(e.selectedItem) : null;
|
||||||
|
const valueCmp = value ?? null;
|
||||||
|
// We make sure to not trigger a onChange event if the value are not different.
|
||||||
|
// This could happen on first render when the component is controlled, the value will be
|
||||||
|
// set inside a useEffect down in SelectMonoSearchable or SelectMonoSimple. So that means the
|
||||||
|
// downshift component will always render empty the first time.
|
||||||
|
if (eventCmp !== valueCmp) {
|
||||||
|
setValue(eventCmp || undefined);
|
||||||
|
props.onChange?.({
|
||||||
|
target: {
|
||||||
|
value: e.selectedItem ? optionToValue(e.selectedItem) : undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
setValue(props.value);
|
},
|
||||||
}, [props.value, props.defaultValue]);
|
isItemDisabled: (item) => !!item.disabled,
|
||||||
|
};
|
||||||
|
|
||||||
const commonDownshiftProps: SubProps["downshiftProps"] = {
|
return props.searchable ? (
|
||||||
initialSelectedItem: defaultSelectedItem,
|
<SelectMonoSearchable
|
||||||
onSelectedItemChange: (e: UseSelectStateChange<Option>) => {
|
{...props}
|
||||||
const eventCmp = e.selectedItem ? optionToValue(e.selectedItem) : null;
|
downshiftProps={commonDownshiftProps}
|
||||||
const valueCmp = value ?? null;
|
value={value}
|
||||||
// We make sure to not trigger a onChange event if the value are not different.
|
/>
|
||||||
// This could happen on first render when the component is controlled, the value will be
|
) : (
|
||||||
// set inside a useEffect down in SelectMonoSearchable or SelectMonoSimple. So that means the
|
<SelectMonoSimple
|
||||||
// downshift component will always render empty the first time.
|
{...props}
|
||||||
if (eventCmp !== valueCmp) {
|
downshiftProps={commonDownshiftProps}
|
||||||
setValue(eventCmp || undefined);
|
value={value}
|
||||||
props.onChange?.({
|
/>
|
||||||
target: {
|
);
|
||||||
value: e.selectedItem ? optionToValue(e.selectedItem) : undefined,
|
};
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
isItemDisabled: (item) => !!item.disabled,
|
|
||||||
};
|
|
||||||
|
|
||||||
return props.searchable ? (
|
|
||||||
<SelectMonoSearchable
|
|
||||||
{...props}
|
|
||||||
downshiftProps={commonDownshiftProps}
|
|
||||||
value={value}
|
|
||||||
ref={ref}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<SelectMonoSimple
|
|
||||||
{...props}
|
|
||||||
downshiftProps={commonDownshiftProps}
|
|
||||||
value={value}
|
|
||||||
ref={ref}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { SelectMenu } from ":/components/Forms/Select/select-menu";
|
|||||||
|
|
||||||
export const SelectMultiMenu = (
|
export const SelectMultiMenu = (
|
||||||
props: SelectMultiAuxProps & {
|
props: SelectMultiAuxProps & {
|
||||||
selectRef: React.RefObject<HTMLDivElement>;
|
selectRef: React.RefObject<HTMLDivElement | null>;
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
const { t } = useCunningham();
|
const { t } = useCunningham();
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
import React, {
|
import React, { useEffect, useImperativeHandle, useRef, useState } from "react";
|
||||||
forwardRef,
|
|
||||||
useEffect,
|
|
||||||
useImperativeHandle,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { useCombobox, useMultipleSelection } from "downshift";
|
import { useCombobox, useMultipleSelection } from "downshift";
|
||||||
import { optionToString } from ":/components/Forms/Select/mono-common";
|
import { optionToString } from ":/components/Forms/Select/mono-common";
|
||||||
import {
|
import {
|
||||||
@@ -12,162 +6,155 @@ import {
|
|||||||
SelectMultiAux,
|
SelectMultiAux,
|
||||||
SubProps,
|
SubProps,
|
||||||
} from ":/components/Forms/Select/multi-common";
|
} from ":/components/Forms/Select/multi-common";
|
||||||
import { SelectHandle } from ":/components/Forms/Select/index";
|
|
||||||
|
|
||||||
export const SelectMultiSearchable = forwardRef<SelectHandle, SubProps>(
|
export const SelectMultiSearchable = ({ ref, ...props }: SubProps) => {
|
||||||
(props, ref) => {
|
const [inputValue, setInputValue] = React.useState<string>();
|
||||||
const [inputValue, setInputValue] = React.useState<string>();
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const options = React.useMemo(
|
||||||
const options = React.useMemo(
|
() =>
|
||||||
() =>
|
props.options.filter(
|
||||||
props.options.filter(
|
getMultiOptionsFilter(props.selectedItems, inputValue),
|
||||||
getMultiOptionsFilter(props.selectedItems, inputValue),
|
),
|
||||||
),
|
[props.selectedItems, inputValue],
|
||||||
[props.selectedItems, inputValue],
|
);
|
||||||
);
|
const [hasInputFocused, setHasInputFocused] = useState(false);
|
||||||
const [hasInputFocused, setHasInputFocused] = useState(false);
|
const useMultipleSelectionReturn = useMultipleSelection({
|
||||||
const useMultipleSelectionReturn = useMultipleSelection({
|
selectedItems: props.selectedItems,
|
||||||
selectedItems: props.selectedItems,
|
onStateChange({ selectedItems: newSelectedItems, type }) {
|
||||||
onStateChange({ selectedItems: newSelectedItems, type }) {
|
switch (type) {
|
||||||
switch (type) {
|
case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownBackspace:
|
||||||
case useMultipleSelection.stateChangeTypes
|
case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownDelete:
|
||||||
.SelectedItemKeyDownBackspace:
|
case useMultipleSelection.stateChangeTypes.DropdownKeyDownBackspace:
|
||||||
case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownDelete:
|
case useMultipleSelection.stateChangeTypes.FunctionRemoveSelectedItem:
|
||||||
case useMultipleSelection.stateChangeTypes.DropdownKeyDownBackspace:
|
props.onSelectedItemsChange(newSelectedItems ?? []);
|
||||||
case useMultipleSelection.stateChangeTypes.FunctionRemoveSelectedItem:
|
break;
|
||||||
props.onSelectedItemsChange(newSelectedItems ?? []);
|
default:
|
||||||
break;
|
break;
|
||||||
default:
|
}
|
||||||
break;
|
},
|
||||||
}
|
});
|
||||||
},
|
const downshiftReturn = useCombobox({
|
||||||
});
|
items: options,
|
||||||
const downshiftReturn = useCombobox({
|
itemToString: optionToString,
|
||||||
items: options,
|
defaultHighlightedIndex: 0, // after selection, highlight the first item.
|
||||||
itemToString: optionToString,
|
selectedItem: null, // Important, without this we are not able to re-select the last removed option.
|
||||||
defaultHighlightedIndex: 0, // after selection, highlight the first item.
|
stateReducer: (state, actionAndChanges) => {
|
||||||
selectedItem: null, // Important, without this we are not able to re-select the last removed option.
|
const { changes, type } = actionAndChanges;
|
||||||
stateReducer: (state, actionAndChanges) => {
|
switch (type) {
|
||||||
const { changes, type } = actionAndChanges;
|
case useCombobox.stateChangeTypes.InputKeyDownEnter:
|
||||||
switch (type) {
|
case useCombobox.stateChangeTypes.ItemClick:
|
||||||
case useCombobox.stateChangeTypes.InputKeyDownEnter:
|
return {
|
||||||
case useCombobox.stateChangeTypes.ItemClick:
|
...changes,
|
||||||
return {
|
isOpen: true, // keep the menu open after selection.
|
||||||
...changes,
|
highlightedIndex: 0, // with the first option highlighted.
|
||||||
isOpen: true, // keep the menu open after selection.
|
};
|
||||||
highlightedIndex: 0, // with the first option highlighted.
|
default:
|
||||||
};
|
return changes;
|
||||||
default:
|
}
|
||||||
return changes;
|
},
|
||||||
}
|
onStateChange: ({
|
||||||
},
|
inputValue: newInputValue,
|
||||||
onStateChange: ({
|
type,
|
||||||
inputValue: newInputValue,
|
selectedItem: newSelectedItem,
|
||||||
type,
|
}) => {
|
||||||
selectedItem: newSelectedItem,
|
switch (type) {
|
||||||
}) => {
|
case useCombobox.stateChangeTypes.InputKeyDownEnter:
|
||||||
switch (type) {
|
case useCombobox.stateChangeTypes.ItemClick:
|
||||||
case useCombobox.stateChangeTypes.InputKeyDownEnter:
|
case useCombobox.stateChangeTypes.InputBlur:
|
||||||
case useCombobox.stateChangeTypes.ItemClick:
|
if (newSelectedItem && !newSelectedItem.disabled) {
|
||||||
case useCombobox.stateChangeTypes.InputBlur:
|
props.onSelectedItemsChange([
|
||||||
if (newSelectedItem && !newSelectedItem.disabled) {
|
...props.selectedItems,
|
||||||
props.onSelectedItemsChange([
|
newSelectedItem,
|
||||||
...props.selectedItems,
|
]);
|
||||||
newSelectedItem,
|
setInputValue(undefined);
|
||||||
]);
|
|
||||||
setInputValue(undefined);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case useCombobox.stateChangeTypes.InputChange:
|
|
||||||
setInputValue(newInputValue);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
isItemDisabled: (item) => !!item.disabled,
|
|
||||||
});
|
|
||||||
|
|
||||||
const inputProps = downshiftReturn.getInputProps({
|
|
||||||
...useMultipleSelectionReturn.getDropdownProps({
|
|
||||||
preventKeyAction: downshiftReturn.isOpen,
|
|
||||||
ref: inputRef,
|
|
||||||
disabled: props.disabled,
|
|
||||||
}),
|
|
||||||
value: inputValue,
|
|
||||||
});
|
|
||||||
// We want to extend the default behavior of the input onKeyDown.
|
|
||||||
const { onKeyDown } = inputProps;
|
|
||||||
inputProps.onKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {
|
|
||||||
switch (event.code) {
|
|
||||||
case "Backspace":
|
|
||||||
if (!inputValue) {
|
|
||||||
props.onSelectedItemsChange(props.selectedItems.slice(0, -1));
|
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
case useCombobox.stateChangeTypes.InputChange:
|
||||||
|
setInputValue(newInputValue);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
onKeyDown?.(event);
|
},
|
||||||
};
|
isItemDisabled: (item) => !!item.disabled,
|
||||||
|
});
|
||||||
|
|
||||||
const [labelAsPlaceholder, setLabelAsPlaceholder] = useState(true);
|
const inputProps = downshiftReturn.getInputProps({
|
||||||
useEffect(() => {
|
...useMultipleSelectionReturn.getDropdownProps({
|
||||||
if (hasInputFocused || inputValue) {
|
preventKeyAction: downshiftReturn.isOpen,
|
||||||
setLabelAsPlaceholder(false);
|
ref: inputRef,
|
||||||
return;
|
disabled: props.disabled,
|
||||||
}
|
}),
|
||||||
setLabelAsPlaceholder(props.selectedItems.length === 0);
|
value: inputValue,
|
||||||
}, [props.selectedItems, hasInputFocused, inputValue]);
|
});
|
||||||
|
// We want to extend the default behavior of the input onKeyDown.
|
||||||
|
const { onKeyDown } = inputProps;
|
||||||
|
inputProps.onKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {
|
||||||
|
switch (event.code) {
|
||||||
|
case "Backspace":
|
||||||
|
if (!inputValue) {
|
||||||
|
props.onSelectedItemsChange(props.selectedItems.slice(0, -1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onKeyDown?.(event);
|
||||||
|
};
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
const [labelAsPlaceholder, setLabelAsPlaceholder] = useState(true);
|
||||||
blur: () => {
|
useEffect(() => {
|
||||||
downshiftReturn.closeMenu();
|
if (hasInputFocused || inputValue) {
|
||||||
inputRef.current?.blur();
|
setLabelAsPlaceholder(false);
|
||||||
},
|
return;
|
||||||
}));
|
}
|
||||||
|
setLabelAsPlaceholder(props.selectedItems.length === 0);
|
||||||
|
}, [props.selectedItems, hasInputFocused, inputValue]);
|
||||||
|
|
||||||
useEffect(() => {
|
useImperativeHandle(ref, () => ({
|
||||||
props.onSearchInputChange?.({ target: { value: inputValue } });
|
blur: () => {
|
||||||
}, [inputValue]);
|
downshiftReturn.closeMenu();
|
||||||
|
inputRef.current?.blur();
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
return (
|
useEffect(() => {
|
||||||
<SelectMultiAux
|
props.onSearchInputChange?.({ target: { value: inputValue } });
|
||||||
{...props}
|
}, [inputValue]);
|
||||||
monoline={false}
|
|
||||||
options={options}
|
return (
|
||||||
labelAsPlaceholder={labelAsPlaceholder}
|
<SelectMultiAux
|
||||||
selectedItems={props.selectedItems}
|
{...props}
|
||||||
downshiftReturn={{
|
monoline={false}
|
||||||
...downshiftReturn,
|
options={options}
|
||||||
wrapperProps: {
|
labelAsPlaceholder={labelAsPlaceholder}
|
||||||
onClick: () => {
|
selectedItems={props.selectedItems}
|
||||||
inputRef.current?.focus();
|
downshiftReturn={{
|
||||||
// This is important because if we don't check that: when clicking on the toggle button
|
...downshiftReturn,
|
||||||
// when the menu is open, it will close and reopen immediately.
|
wrapperProps: {
|
||||||
if (!downshiftReturn.isOpen) {
|
onClick: () => {
|
||||||
downshiftReturn.openMenu();
|
inputRef.current?.focus();
|
||||||
}
|
// This is important because if we don't check that: when clicking on the toggle button
|
||||||
},
|
// when the menu is open, it will close and reopen immediately.
|
||||||
},
|
if (!downshiftReturn.isOpen) {
|
||||||
toggleButtonProps: downshiftReturn.getToggleButtonProps(),
|
|
||||||
}}
|
|
||||||
useMultipleSelectionReturn={useMultipleSelectionReturn}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="c__select__inner__value__input"
|
|
||||||
data-value={inputValue}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
{...inputProps}
|
|
||||||
onFocus={() => {
|
|
||||||
setHasInputFocused(true);
|
|
||||||
downshiftReturn.openMenu();
|
downshiftReturn.openMenu();
|
||||||
}}
|
}
|
||||||
onBlur={() => {
|
},
|
||||||
setHasInputFocused(false);
|
},
|
||||||
}}
|
toggleButtonProps: downshiftReturn.getToggleButtonProps(),
|
||||||
size={4}
|
}}
|
||||||
/>
|
useMultipleSelectionReturn={useMultipleSelectionReturn}
|
||||||
</span>
|
>
|
||||||
</SelectMultiAux>
|
<span className="c__select__inner__value__input" data-value={inputValue}>
|
||||||
);
|
<input
|
||||||
},
|
{...inputProps}
|
||||||
);
|
onFocus={() => {
|
||||||
|
setHasInputFocused(true);
|
||||||
|
downshiftReturn.openMenu();
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
setHasInputFocused(false);
|
||||||
|
}}
|
||||||
|
size={4}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</SelectMultiAux>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { forwardRef, useImperativeHandle, useRef } from "react";
|
import React, { useImperativeHandle, useRef } from "react";
|
||||||
import { useMultipleSelection, useSelect } from "downshift";
|
import { useMultipleSelection, useSelect } from "downshift";
|
||||||
import {
|
import {
|
||||||
getMultiOptionsFilter,
|
getMultiOptionsFilter,
|
||||||
@@ -9,127 +9,121 @@ import {
|
|||||||
optionsEqual,
|
optionsEqual,
|
||||||
optionToString,
|
optionToString,
|
||||||
} from ":/components/Forms/Select/mono-common";
|
} from ":/components/Forms/Select/mono-common";
|
||||||
import { Option, SelectHandle } from ":/components/Forms/Select/index";
|
import { Option } from ":/components/Forms/Select/index";
|
||||||
|
|
||||||
export const SelectMultiSimple = forwardRef<SelectHandle, SubProps>(
|
export const SelectMultiSimple = ({ ref, ...props }: SubProps) => {
|
||||||
(props, ref) => {
|
const isSelected = (option: Option) =>
|
||||||
const isSelected = (option: Option) =>
|
!!props.selectedItems.find((selectedItem) =>
|
||||||
!!props.selectedItems.find((selectedItem) =>
|
optionsEqual(selectedItem, option),
|
||||||
optionsEqual(selectedItem, option),
|
|
||||||
);
|
|
||||||
|
|
||||||
const options = React.useMemo(() => {
|
|
||||||
if (props.monoline) {
|
|
||||||
return props.options.map((option) => ({
|
|
||||||
...option,
|
|
||||||
highlighted: isSelected(option),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
return props.options.filter(
|
|
||||||
getMultiOptionsFilter(props.selectedItems, ""),
|
|
||||||
);
|
|
||||||
}, [props.selectedItems]);
|
|
||||||
|
|
||||||
const useMultipleSelectionReturn = useMultipleSelection({
|
|
||||||
selectedItems: props.selectedItems,
|
|
||||||
onStateChange({ selectedItems: newSelectedItems, type }) {
|
|
||||||
switch (type) {
|
|
||||||
case useMultipleSelection.stateChangeTypes
|
|
||||||
.SelectedItemKeyDownBackspace:
|
|
||||||
case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownDelete:
|
|
||||||
case useMultipleSelection.stateChangeTypes.DropdownKeyDownBackspace:
|
|
||||||
case useMultipleSelection.stateChangeTypes.FunctionRemoveSelectedItem:
|
|
||||||
props.onSelectedItemsChange(newSelectedItems ?? []);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const downshiftReturn = useSelect({
|
|
||||||
items: options,
|
|
||||||
itemToString: optionToString,
|
|
||||||
selectedItem: null, // Important, without this we are not able to re-select the last removed option.
|
|
||||||
defaultHighlightedIndex: 0, // after selection, highlight the first item.
|
|
||||||
stateReducer: (state, actionAndChanges) => {
|
|
||||||
const { changes, type } = actionAndChanges;
|
|
||||||
switch (type) {
|
|
||||||
case useSelect.stateChangeTypes.ToggleButtonKeyDownEnter:
|
|
||||||
case useSelect.stateChangeTypes.ToggleButtonKeyDownSpaceButton:
|
|
||||||
case useSelect.stateChangeTypes.ItemClick:
|
|
||||||
return {
|
|
||||||
...changes,
|
|
||||||
isOpen: true, // keep the menu open after selection.
|
|
||||||
highlightedIndex: state.highlightedIndex, // avoid automatic scroll up on click.
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return changes;
|
|
||||||
},
|
|
||||||
onStateChange: ({ type, selectedItem: newSelectedItem }) => {
|
|
||||||
switch (type) {
|
|
||||||
case useSelect.stateChangeTypes.ToggleButtonKeyDownEnter:
|
|
||||||
case useSelect.stateChangeTypes.ToggleButtonKeyDownSpaceButton:
|
|
||||||
case useSelect.stateChangeTypes.ItemClick:
|
|
||||||
if (!newSelectedItem) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (isSelected(newSelectedItem)) {
|
|
||||||
// Remove the item if it is already selected.
|
|
||||||
props.onSelectedItemsChange(
|
|
||||||
props.selectedItems.filter(
|
|
||||||
(selectedItem) =>
|
|
||||||
!optionsEqual(selectedItem, newSelectedItem),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
props.onSelectedItemsChange([
|
|
||||||
...props.selectedItems,
|
|
||||||
newSelectedItem,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
isItemDisabled: (item) => !!item.disabled,
|
|
||||||
});
|
|
||||||
|
|
||||||
const toggleRef = useRef<HTMLElement>(null);
|
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
|
||||||
blur: () => {
|
|
||||||
downshiftReturn.closeMenu();
|
|
||||||
toggleRef.current?.blur();
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SelectMultiAux
|
|
||||||
{...props}
|
|
||||||
options={options}
|
|
||||||
labelAsPlaceholder={props.selectedItems.length === 0}
|
|
||||||
selectedItems={props.selectedItems}
|
|
||||||
selectedItemsStyle={props.monoline ? "text" : "pills"}
|
|
||||||
menuOptionsStyle={props.monoline ? "checkbox" : "plain"}
|
|
||||||
downshiftReturn={{
|
|
||||||
...downshiftReturn,
|
|
||||||
toggleButtonProps: downshiftReturn.getToggleButtonProps({
|
|
||||||
...useMultipleSelectionReturn.getDropdownProps({
|
|
||||||
preventKeyAction: downshiftReturn.isOpen,
|
|
||||||
ref: toggleRef,
|
|
||||||
}),
|
|
||||||
disabled: props.disabled,
|
|
||||||
onClick: (e: React.MouseEvent): void => {
|
|
||||||
// As the wrapper also has an onClick handler, we need to stop the event propagation here on it will toggle
|
|
||||||
// twice the menu opening which will ... do nothing :).
|
|
||||||
e.stopPropagation();
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}}
|
|
||||||
useMultipleSelectionReturn={useMultipleSelectionReturn}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
},
|
|
||||||
);
|
const options = React.useMemo(() => {
|
||||||
|
if (props.monoline) {
|
||||||
|
return props.options.map((option) => ({
|
||||||
|
...option,
|
||||||
|
highlighted: isSelected(option),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return props.options.filter(getMultiOptionsFilter(props.selectedItems, ""));
|
||||||
|
}, [props.selectedItems]);
|
||||||
|
|
||||||
|
const useMultipleSelectionReturn = useMultipleSelection({
|
||||||
|
selectedItems: props.selectedItems,
|
||||||
|
onStateChange({ selectedItems: newSelectedItems, type }) {
|
||||||
|
switch (type) {
|
||||||
|
case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownBackspace:
|
||||||
|
case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownDelete:
|
||||||
|
case useMultipleSelection.stateChangeTypes.DropdownKeyDownBackspace:
|
||||||
|
case useMultipleSelection.stateChangeTypes.FunctionRemoveSelectedItem:
|
||||||
|
props.onSelectedItemsChange(newSelectedItems ?? []);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const downshiftReturn = useSelect({
|
||||||
|
items: options,
|
||||||
|
itemToString: optionToString,
|
||||||
|
selectedItem: null, // Important, without this we are not able to re-select the last removed option.
|
||||||
|
defaultHighlightedIndex: 0, // after selection, highlight the first item.
|
||||||
|
stateReducer: (state, actionAndChanges) => {
|
||||||
|
const { changes, type } = actionAndChanges;
|
||||||
|
switch (type) {
|
||||||
|
case useSelect.stateChangeTypes.ToggleButtonKeyDownEnter:
|
||||||
|
case useSelect.stateChangeTypes.ToggleButtonKeyDownSpaceButton:
|
||||||
|
case useSelect.stateChangeTypes.ItemClick:
|
||||||
|
return {
|
||||||
|
...changes,
|
||||||
|
isOpen: true, // keep the menu open after selection.
|
||||||
|
highlightedIndex: state.highlightedIndex, // avoid automatic scroll up on click.
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return changes;
|
||||||
|
},
|
||||||
|
onStateChange: ({ type, selectedItem: newSelectedItem }) => {
|
||||||
|
switch (type) {
|
||||||
|
case useSelect.stateChangeTypes.ToggleButtonKeyDownEnter:
|
||||||
|
case useSelect.stateChangeTypes.ToggleButtonKeyDownSpaceButton:
|
||||||
|
case useSelect.stateChangeTypes.ItemClick:
|
||||||
|
if (!newSelectedItem) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (isSelected(newSelectedItem)) {
|
||||||
|
// Remove the item if it is already selected.
|
||||||
|
props.onSelectedItemsChange(
|
||||||
|
props.selectedItems.filter(
|
||||||
|
(selectedItem) => !optionsEqual(selectedItem, newSelectedItem),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
props.onSelectedItemsChange([
|
||||||
|
...props.selectedItems,
|
||||||
|
newSelectedItem,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isItemDisabled: (item) => !!item.disabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleRef = useRef<HTMLElement>(null);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
blur: () => {
|
||||||
|
downshiftReturn.closeMenu();
|
||||||
|
toggleRef.current?.blur();
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SelectMultiAux
|
||||||
|
{...props}
|
||||||
|
options={options}
|
||||||
|
labelAsPlaceholder={props.selectedItems.length === 0}
|
||||||
|
selectedItems={props.selectedItems}
|
||||||
|
selectedItemsStyle={props.monoline ? "text" : "pills"}
|
||||||
|
menuOptionsStyle={props.monoline ? "checkbox" : "plain"}
|
||||||
|
downshiftReturn={{
|
||||||
|
...downshiftReturn,
|
||||||
|
toggleButtonProps: downshiftReturn.getToggleButtonProps({
|
||||||
|
...useMultipleSelectionReturn.getDropdownProps({
|
||||||
|
preventKeyAction: downshiftReturn.isOpen,
|
||||||
|
ref: toggleRef,
|
||||||
|
}),
|
||||||
|
disabled: props.disabled,
|
||||||
|
onClick: (e: React.MouseEvent): void => {
|
||||||
|
// As the wrapper also has an onClick handler, we need to stop the event propagation here on it will toggle
|
||||||
|
// twice the menu opening which will ... do nothing :).
|
||||||
|
e.stopPropagation();
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
useMultipleSelectionReturn={useMultipleSelectionReturn}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,73 +1,65 @@
|
|||||||
import React, { forwardRef, useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { optionToValue } from ":/components/Forms/Select/mono-common";
|
import { optionToValue } from ":/components/Forms/Select/mono-common";
|
||||||
import { SelectMultiSearchable } from ":/components/Forms/Select/multi-searchable";
|
import { SelectMultiSearchable } from ":/components/Forms/Select/multi-searchable";
|
||||||
import { SelectMultiSimple } from ":/components/Forms/Select/multi-simple";
|
import { SelectMultiSimple } from ":/components/Forms/Select/multi-simple";
|
||||||
import { SubProps } from ":/components/Forms/Select/multi-common";
|
import { SubProps } from ":/components/Forms/Select/multi-common";
|
||||||
import {
|
import { Option, SelectProps } from ":/components/Forms/Select/index";
|
||||||
Option,
|
|
||||||
SelectHandle,
|
|
||||||
SelectProps,
|
|
||||||
} from ":/components/Forms/Select/index";
|
|
||||||
|
|
||||||
export type SelectMultiProps = Omit<SelectProps, "onChange"> & {
|
export type SelectMultiProps = Omit<SelectProps, "onChange"> & {
|
||||||
onChange?: (event: { target: { value: string[] } }) => void;
|
onChange?: (event: { target: { value: string[] } }) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SelectMulti = forwardRef<SelectHandle, SelectMultiProps>(
|
export const SelectMulti = (props: SelectMultiProps) => {
|
||||||
(props, ref) => {
|
const getSelectedItemsFromProps = () => {
|
||||||
const getSelectedItemsFromProps = () => {
|
const valueToUse = props.defaultValue ?? props.value ?? [];
|
||||||
const valueToUse = props.defaultValue ?? props.value ?? [];
|
return props.options.filter((option) =>
|
||||||
return props.options.filter((option) =>
|
(valueToUse as string[]).includes(optionToValue(option)),
|
||||||
(valueToUse as string[]).includes(optionToValue(option)),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const [selectedItems, setSelectedItems] = React.useState<Option[]>(
|
|
||||||
getSelectedItemsFromProps(),
|
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// If the component is used as a controlled component, we need to update the local value when the value prop changes.
|
const [selectedItems, setSelectedItems] = React.useState<Option[]>(
|
||||||
useEffect(() => {
|
getSelectedItemsFromProps(),
|
||||||
// Means it is not controlled.
|
);
|
||||||
if (props.defaultValue !== undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setSelectedItems(getSelectedItemsFromProps());
|
|
||||||
}, [JSON.stringify(props.value)]);
|
|
||||||
|
|
||||||
// If the component is used as an uncontrolled component, we need to update the parent value when the local value changes.
|
// If the component is used as a controlled component, we need to update the local value when the value prop changes.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
props.onChange?.({ target: { value: selectedItems.map(optionToValue) } });
|
// Means it is not controlled.
|
||||||
}, [JSON.stringify(selectedItems)]);
|
if (props.defaultValue !== undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSelectedItems(getSelectedItemsFromProps());
|
||||||
|
}, [JSON.stringify(props.value)]);
|
||||||
|
|
||||||
const onSelectedItemsChange: SubProps["onSelectedItemsChange"] = (
|
// If the component is used as an uncontrolled component, we need to update the parent value when the local value changes.
|
||||||
newSelectedItems,
|
useEffect(() => {
|
||||||
) => {
|
props.onChange?.({ target: { value: selectedItems.map(optionToValue) } });
|
||||||
setSelectedItems(newSelectedItems);
|
}, [JSON.stringify(selectedItems)]);
|
||||||
};
|
|
||||||
|
|
||||||
const defaultProps: Partial<SelectMultiProps> = {
|
const onSelectedItemsChange: SubProps["onSelectedItemsChange"] = (
|
||||||
selectedItemsStyle: "pills",
|
newSelectedItems,
|
||||||
menuOptionsStyle: "plain",
|
) => {
|
||||||
clearable: true,
|
setSelectedItems(newSelectedItems);
|
||||||
};
|
};
|
||||||
|
|
||||||
return props.searchable ? (
|
const defaultProps: Partial<SelectMultiProps> = {
|
||||||
<SelectMultiSearchable
|
selectedItemsStyle: "pills",
|
||||||
{...defaultProps}
|
menuOptionsStyle: "plain",
|
||||||
{...props}
|
clearable: true,
|
||||||
selectedItems={selectedItems}
|
};
|
||||||
onSelectedItemsChange={onSelectedItemsChange}
|
|
||||||
ref={ref}
|
return props.searchable ? (
|
||||||
/>
|
<SelectMultiSearchable
|
||||||
) : (
|
{...defaultProps}
|
||||||
<SelectMultiSimple
|
{...props}
|
||||||
{...defaultProps}
|
selectedItems={selectedItems}
|
||||||
{...props}
|
onSelectedItemsChange={onSelectedItemsChange}
|
||||||
selectedItems={selectedItems}
|
/>
|
||||||
onSelectedItemsChange={onSelectedItemsChange}
|
) : (
|
||||||
ref={ref}
|
<SelectMultiSimple
|
||||||
/>
|
{...defaultProps}
|
||||||
);
|
{...props}
|
||||||
},
|
selectedItems={selectedItems}
|
||||||
);
|
onSelectedItemsChange={onSelectedItemsChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { SelectMultiAuxProps } from ":/components/Forms/Select/multi-common";
|
|||||||
|
|
||||||
export interface SelectDropdownProps extends PropsWithChildren {
|
export interface SelectDropdownProps extends PropsWithChildren {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
selectRef: React.RefObject<HTMLDivElement>;
|
selectRef: React.RefObject<HTMLDivElement | null>;
|
||||||
menuOptionsStyle?: SelectProps["menuOptionsStyle"];
|
menuOptionsStyle?: SelectProps["menuOptionsStyle"];
|
||||||
downshiftReturn:
|
downshiftReturn:
|
||||||
| SelectAuxProps["downshiftReturn"]
|
| SelectAuxProps["downshiftReturn"]
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { InputHTMLAttributes, forwardRef } from "react";
|
import React, { InputHTMLAttributes, RefAttributes } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { Field, FieldProps } from ":/components/Forms/Field";
|
import { Field, FieldProps } from ":/components/Forms/Field";
|
||||||
|
|
||||||
@@ -8,47 +8,51 @@ export type SwitchOnlyProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type SwitchProps = InputHTMLAttributes<HTMLInputElement> &
|
export type SwitchProps = InputHTMLAttributes<HTMLInputElement> &
|
||||||
|
RefAttributes<HTMLInputElement> &
|
||||||
FieldProps &
|
FieldProps &
|
||||||
SwitchOnlyProps;
|
SwitchOnlyProps;
|
||||||
|
|
||||||
export const Switch = forwardRef<HTMLInputElement, SwitchProps>(
|
export const Switch = ({
|
||||||
({ label, labelSide = "left", ...props }: SwitchProps, ref) => {
|
label,
|
||||||
const {
|
labelSide = "left",
|
||||||
compact,
|
ref,
|
||||||
className,
|
...props
|
||||||
fullWidth,
|
}: SwitchProps) => {
|
||||||
rightText,
|
const {
|
||||||
state,
|
compact,
|
||||||
text,
|
className,
|
||||||
textItems,
|
fullWidth,
|
||||||
...inputProps
|
rightText,
|
||||||
} = props;
|
state,
|
||||||
|
text,
|
||||||
|
textItems,
|
||||||
|
...inputProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
const { className: excludeClassName, ...fieldProps } = props;
|
const { className: excludeClassName, ...fieldProps } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"c__checkbox",
|
"c__checkbox",
|
||||||
"c__switch",
|
"c__switch",
|
||||||
"c__switch--" + labelSide,
|
"c__switch--" + labelSide,
|
||||||
className,
|
className,
|
||||||
{
|
{
|
||||||
"c__checkbox--disabled": props.disabled,
|
"c__checkbox--disabled": props.disabled,
|
||||||
"c__switch--full-width": props.fullWidth,
|
"c__switch--full-width": props.fullWidth,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Field compact={true} {...fieldProps}>
|
<Field compact={true} {...fieldProps}>
|
||||||
<div className="c__checkbox__container">
|
<div className="c__checkbox__container">
|
||||||
{label && <div className="c__checkbox__label">{label}</div>}
|
{label && <div className="c__checkbox__label">{label}</div>}
|
||||||
<div className="c__switch__rail__wrapper">
|
<div className="c__switch__rail__wrapper">
|
||||||
<input type="checkbox" {...inputProps} ref={ref} />
|
<input type="checkbox" {...inputProps} ref={ref} />
|
||||||
<div className="c__switch__rail" />
|
<div className="c__switch__rail" />
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Field>
|
</div>
|
||||||
</label>
|
</Field>
|
||||||
);
|
</label>
|
||||||
},
|
);
|
||||||
);
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, {
|
import React, {
|
||||||
forwardRef,
|
RefAttributes,
|
||||||
TextareaHTMLAttributes,
|
TextareaHTMLAttributes,
|
||||||
useEffect,
|
useEffect,
|
||||||
useRef,
|
useRef,
|
||||||
@@ -11,94 +11,101 @@ import { LabelledBox } from ":/components/Forms/LabelledBox";
|
|||||||
import { randomString } from ":/utils";
|
import { randomString } from ":/utils";
|
||||||
|
|
||||||
export type TextAreaProps = TextareaHTMLAttributes<HTMLTextAreaElement> &
|
export type TextAreaProps = TextareaHTMLAttributes<HTMLTextAreaElement> &
|
||||||
|
RefAttributes<HTMLTextAreaElement> &
|
||||||
FieldProps & {
|
FieldProps & {
|
||||||
label?: string;
|
label?: string;
|
||||||
charCounter?: boolean;
|
charCounter?: boolean;
|
||||||
charCounterMax?: number;
|
charCounterMax?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
|
export const TextArea = ({
|
||||||
({ label, id, defaultValue, charCounter, charCounterMax, ...props }, ref) => {
|
label,
|
||||||
const areaRef = useRef<HTMLTextAreaElement | null>(null);
|
id,
|
||||||
const [inputFocus, setInputFocus] = useState(false);
|
defaultValue,
|
||||||
const [value, setValue] = useState(defaultValue || props.value || "");
|
charCounter,
|
||||||
const [labelAsPlaceholder, setLabelAsPlaceholder] = useState(!value);
|
charCounterMax,
|
||||||
const idToUse = useRef(id || randomString());
|
ref,
|
||||||
const rightTextToUse = charCounter
|
...props
|
||||||
? `${value.toString().length}/${charCounterMax}`
|
}: TextAreaProps) => {
|
||||||
: props.rightText;
|
const areaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||||
|
const [inputFocus, setInputFocus] = useState(false);
|
||||||
|
const [value, setValue] = useState(defaultValue || props.value || "");
|
||||||
|
const [labelAsPlaceholder, setLabelAsPlaceholder] = useState(!value);
|
||||||
|
const idToUse = useRef(id || randomString());
|
||||||
|
const rightTextToUse = charCounter
|
||||||
|
? `${value.toString().length}/${charCounterMax}`
|
||||||
|
: props.rightText;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (inputFocus) {
|
if (inputFocus) {
|
||||||
setLabelAsPlaceholder(false);
|
setLabelAsPlaceholder(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setLabelAsPlaceholder(!value);
|
setLabelAsPlaceholder(!value);
|
||||||
}, [inputFocus, value]);
|
}, [inputFocus, value]);
|
||||||
|
|
||||||
// If the input is used as a controlled component, we need to update the local value.
|
// If the input is used as a controlled component, we need to update the local value.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (defaultValue !== undefined) {
|
if (defaultValue !== undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setValue(props.value || "");
|
setValue(props.value || "");
|
||||||
}, [props.value]);
|
}, [props.value]);
|
||||||
|
|
||||||
const { fullWidth, rightText, text, textItems, className, ...areaProps } =
|
const { fullWidth, rightText, text, textItems, className, ...areaProps } =
|
||||||
props;
|
props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Field
|
<Field
|
||||||
{...props}
|
{...props}
|
||||||
className={classNames("c__field--textarea", className)}
|
className={classNames("c__field--textarea", className)}
|
||||||
rightText={rightTextToUse}
|
rightText={rightTextToUse}
|
||||||
|
>
|
||||||
|
{/* We disabled linting for this specific line because we consider that the onClick props is only used for */}
|
||||||
|
{/* mouse users, so this do not engender any issue for accessibility. */}
|
||||||
|
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
|
||||||
|
<div
|
||||||
|
className={classNames("c__textarea__wrapper", {
|
||||||
|
"c__textarea__wrapper--disabled": props.disabled,
|
||||||
|
})}
|
||||||
|
onClick={() => areaRef.current?.focus()}
|
||||||
>
|
>
|
||||||
{/* We disabled linting for this specific line because we consider that the onClick props is only used for */}
|
<LabelledBox
|
||||||
{/* mouse users, so this do not engender any issue for accessibility. */}
|
label={label}
|
||||||
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
|
htmlFor={idToUse.current}
|
||||||
<div
|
labelAsPlaceholder={labelAsPlaceholder}
|
||||||
className={classNames("c__textarea__wrapper", {
|
disabled={props.disabled}
|
||||||
"c__textarea__wrapper--disabled": props.disabled,
|
|
||||||
})}
|
|
||||||
onClick={() => areaRef.current?.focus()}
|
|
||||||
>
|
>
|
||||||
<LabelledBox
|
<textarea
|
||||||
label={label}
|
className="c__textarea"
|
||||||
htmlFor={idToUse.current}
|
{...areaProps}
|
||||||
labelAsPlaceholder={labelAsPlaceholder}
|
id={idToUse.current}
|
||||||
disabled={props.disabled}
|
onFocus={(e) => {
|
||||||
>
|
setInputFocus(true);
|
||||||
<textarea
|
props.onFocus?.(e);
|
||||||
className="c__textarea"
|
}}
|
||||||
{...areaProps}
|
onBlur={(e) => {
|
||||||
id={idToUse.current}
|
setInputFocus(false);
|
||||||
onFocus={(e) => {
|
props.onBlur?.(e);
|
||||||
setInputFocus(true);
|
}}
|
||||||
props.onFocus?.(e);
|
value={value}
|
||||||
}}
|
onChange={(e) => {
|
||||||
onBlur={(e) => {
|
setValue(e.target.value);
|
||||||
setInputFocus(false);
|
props.onChange?.(e);
|
||||||
props.onBlur?.(e);
|
}}
|
||||||
}}
|
ref={(nativeRef) => {
|
||||||
value={value}
|
if (ref) {
|
||||||
onChange={(e) => {
|
if (typeof ref === "function") {
|
||||||
setValue(e.target.value);
|
ref(nativeRef);
|
||||||
props.onChange?.(e);
|
} else {
|
||||||
}}
|
ref.current = nativeRef;
|
||||||
ref={(nativeRef) => {
|
|
||||||
if (ref) {
|
|
||||||
if (typeof ref === "function") {
|
|
||||||
ref(nativeRef);
|
|
||||||
} else {
|
|
||||||
ref.current = nativeRef;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
areaRef.current = nativeRef;
|
}
|
||||||
}}
|
areaRef.current = nativeRef;
|
||||||
/>
|
}}
|
||||||
</LabelledBox>
|
/>
|
||||||
</div>
|
</LabelledBox>
|
||||||
</Field>
|
</div>
|
||||||
);
|
</Field>
|
||||||
},
|
);
|
||||||
);
|
};
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import classNames from "classnames";
|
|||||||
import { useHandleClickOutside } from ":/hooks/useHandleClickOutside";
|
import { useHandleClickOutside } from ":/hooks/useHandleClickOutside";
|
||||||
|
|
||||||
export type PopoverProps = PropsWithChildren & {
|
export type PopoverProps = PropsWithChildren & {
|
||||||
parentRef: RefObject<HTMLDivElement>;
|
parentRef: RefObject<HTMLDivElement | null>;
|
||||||
onClickOutside: () => void;
|
onClickOutside: () => void;
|
||||||
borderless?: boolean;
|
borderless?: boolean;
|
||||||
};
|
};
|
||||||
@@ -22,7 +22,7 @@ export const Popover = ({
|
|||||||
}: PopoverProps) => {
|
}: PopoverProps) => {
|
||||||
const popoverRef = useRef<HTMLDivElement>(null);
|
const popoverRef = useRef<HTMLDivElement>(null);
|
||||||
useHandleClickOutside(parentRef, onClickOutside);
|
useHandleClickOutside(parentRef, onClickOutside);
|
||||||
const timeout = useRef<ReturnType<typeof setTimeout>>();
|
const timeout = useRef<ReturnType<typeof setTimeout>>(null);
|
||||||
const [topPosition, setTopPosition] = useState<number | undefined>();
|
const [topPosition, setTopPosition] = useState<number | undefined>();
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export interface ToastProps extends PropsWithChildren {
|
|||||||
export const Toast = (props: ToastProps) => {
|
export const Toast = (props: ToastProps) => {
|
||||||
const [animateDisappear, setAnimateDisappear] = React.useState(false);
|
const [animateDisappear, setAnimateDisappear] = React.useState(false);
|
||||||
const container = useRef<HTMLDivElement>(null);
|
const container = useRef<HTMLDivElement>(null);
|
||||||
const disappearTimeout = useRef<NodeJS.Timeout>();
|
const disappearTimeout = useRef<NodeJS.Timeout>(null);
|
||||||
|
|
||||||
// Register a timeout to remove the toast after the duration.
|
// Register a timeout to remove the toast after the duration.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -34,7 +34,7 @@ export const Toast = (props: ToastProps) => {
|
|||||||
}
|
}
|
||||||
disappearTimeout.current = setTimeout(async () => {
|
disappearTimeout.current = setTimeout(async () => {
|
||||||
setAnimateDisappear(true);
|
setAnimateDisappear(true);
|
||||||
disappearTimeout.current = undefined;
|
disappearTimeout.current = null;
|
||||||
}, props.duration);
|
}, props.duration);
|
||||||
return () => {
|
return () => {
|
||||||
if (disappearTimeout.current) {
|
if (disappearTimeout.current) {
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import React, { PropsWithChildren, ReactElement, ReactNode } from "react";
|
import React, {
|
||||||
|
PropsWithChildren,
|
||||||
|
ReactElement,
|
||||||
|
ReactNode,
|
||||||
|
RefObject,
|
||||||
|
} from "react";
|
||||||
import { OverlayArrow } from "react-aria-components";
|
import { OverlayArrow } from "react-aria-components";
|
||||||
import {
|
import {
|
||||||
mergeProps,
|
mergeProps,
|
||||||
@@ -72,7 +77,11 @@ export const Tooltip = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{React.cloneElement(
|
{React.cloneElement(
|
||||||
React.Children.toArray(props.children)[0] as ReactElement,
|
React.Children.toArray(props.children)[0] as ReactElement<
|
||||||
|
typeof useTooltipTriggerRes.triggerProps & {
|
||||||
|
ref: RefObject<ReactElement | null>;
|
||||||
|
}
|
||||||
|
>,
|
||||||
{
|
{
|
||||||
ref,
|
ref,
|
||||||
...useTooltipTriggerRes.triggerProps,
|
...useTooltipTriggerRes.triggerProps,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { RefObject, useEffect } from "react";
|
import { RefObject, useEffect } from "react";
|
||||||
|
|
||||||
export const useHandleClickOutside = (
|
export const useHandleClickOutside = (
|
||||||
ref: RefObject<HTMLDivElement>,
|
ref: RefObject<HTMLDivElement | null>,
|
||||||
onClickOutside: any,
|
onClickOutside: any,
|
||||||
) => {
|
) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user