jbpenrath
2025-01-07 23:28:47 +01:00
committed by Jean-Baptiste PENRATH
parent 0f6a8dfa72
commit 56d9ed88f0
27 changed files with 1497 additions and 1546 deletions

View File

@@ -0,0 +1,5 @@
---
"@openfun/cunningham-react": major
---
Upgrade to React 19

View File

@@ -2,14 +2,16 @@ 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"> & { export type ButtonElement = HTMLButtonElement & HTMLAnchorElement;
export type ButtonProps = Omit<DomProps, "color"> &
RefAttributes<ButtonElement> & {
size?: "medium" | "small" | "nano"; size?: "medium" | "small" | "nano";
color?: color?:
| "primary" | "primary"
@@ -22,13 +24,9 @@ export type ButtonProps = Omit<DomProps, "color"> & {
iconPosition?: "left" | "right"; iconPosition?: "left" | "right";
active?: boolean; active?: boolean;
fullWidth?: boolean; fullWidth?: boolean;
}; };
export type ButtonElement = HTMLButtonElement & HTMLAnchorElement; export const Button = ({
export const Button = forwardRef<ButtonElement, ButtonProps>(
(
{
children, children,
color = "primary", color = "primary",
size = "medium", size = "medium",
@@ -37,10 +35,9 @@ export const Button = forwardRef<ButtonElement, ButtonProps>(
active, active,
className, className,
fullWidth, fullWidth,
...props
},
ref, ref,
) => { ...props
}: ButtonProps) => {
const classes = [ const classes = [
"c__button", "c__button",
"c__button--" + color, "c__button--" + color,
@@ -77,5 +74,4 @@ export const Button = forwardRef<ButtonElement, ButtonProps>(
{!!icon && iconPosition === "right" && iconElement} {!!icon && iconPosition === "right" && iconElement}
</>, </>,
); );
}, };
);

View File

@@ -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,15 +16,19 @@ 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 = "",
checked,
label,
ref, ref,
) => { ...props
const inputRef = useRef<HTMLInputElement>(); }: CheckboxProps) => {
const inputRef = useRef<HTMLInputElement>(null);
const [value, setValue] = useState<boolean>(!!checked); const [value, setValue] = useState<boolean>(!!checked);
useEffect(() => { useEffect(() => {
@@ -69,7 +73,7 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
if (typeof ref === "function") { if (typeof ref === "function") {
ref(checkboxRef); ref(checkboxRef);
} }
inputRef.current = checkboxRef || undefined; inputRef.current = checkboxRef || null;
}} }}
/> />
<Indeterminate /> <Indeterminate />
@@ -80,8 +84,7 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
</Field> </Field>
</label> </label>
); );
}, };
);
export const CheckboxGroup = ({ export const CheckboxGroup = ({
children, children,

View File

@@ -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,24 +83,22 @@ const DropdownValues = ({ options, downShift }: DropdownValuesProps) => (
</div> </div>
); );
interface CalendarAuxProps extends CalendarAria { type CalendarAuxProps = CalendarAria &
RefAttributes<HTMLDivElement> & {
minYear?: number; minYear?: number;
maxYear?: number; maxYear?: number;
state: RangeCalendarState | CalendarState; state: RangeCalendarState | CalendarState;
} };
const CalendarAux = forwardRef( const CalendarAux = ({
(
{
state, state,
minYear = 1900, // in gregorian calendar. minYear = 1900, // in gregorian calendar.
maxYear = 2050, // in gregorian calendar. maxYear = 2050, // in gregorian calendar.
prevButtonProps, prevButtonProps,
nextButtonProps, nextButtonProps,
calendarProps, calendarProps,
}: CalendarAuxProps, ref,
ref: Ref<HTMLDivElement>, }: CalendarAuxProps) => {
) => {
const { t } = useCunningham(); const { t } = useCunningham();
const useTimeZoneFormatter = (formatOptions: DateFormatterOptions) => { const useTimeZoneFormatter = (formatOptions: DateFormatterOptions) => {
@@ -310,8 +308,7 @@ const CalendarAux = forwardRef(
onClick={() => state.focusPreviousSection(true)} onClick={() => state.focusPreviousSection(true)}
disabled={ disabled={
!!state.minValue && !!state.minValue &&
state.minValue.year > state.minValue.year > state.focusedDate.add({ years: -1 }).year
state.focusedDate.add({ years: -1 }).year
} }
aria-label={t( aria-label={t(
"components.forms.date_picker.previous_year_button_aria_label", "components.forms.date_picker.previous_year_button_aria_label",
@@ -355,8 +352,7 @@ const CalendarAux = forwardRef(
<DropdownValues options={yearItems} downShift={downshiftYear} /> <DropdownValues options={yearItems} downShift={downshiftYear} />
</div> </div>
); );
}, };
);
export const Calendar = (props: CalendarProps<DateValue>) => { export const Calendar = (props: CalendarProps<DateValue>) => {
const { locale } = useLocale(); const { locale } = useLocale();

View File

@@ -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,9 +54,7 @@ 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, className,
pickerState, pickerState,
pickerProps, pickerProps,
@@ -70,16 +68,14 @@ const DatePickerAux = forwardRef(
disabled = false, disabled = false,
optionalClassName, optionalClassName,
isRange, isRange,
ref,
...props ...props
}: DatePickerAuxProps, }: DatePickerAuxProps) => {
ref: Ref<HTMLDivElement>,
) => {
const { t, currentLocale } = useCunningham(); const { t, currentLocale } = useCunningham();
const pickerRef = useRef<HTMLDivElement>(null); 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],
); );
@@ -98,9 +94,7 @@ const DatePickerAux = forwardRef(
"c__date-picker--invalid": isDateInvalid, "c__date-picker--invalid": isDateInvalid,
"c__date-picker--success": props.state === "success", "c__date-picker--success": props.state === "success",
"c__date-picker--focused": "c__date-picker--focused":
!isDateInvalid && !isDateInvalid && !disabled && (pickerState.isOpen || isFocused),
!disabled &&
(pickerState.isOpen || isFocused),
})} })}
> >
<div <div
@@ -201,7 +195,6 @@ const DatePickerAux = forwardRef(
</Field> </Field>
</I18nProvider> </I18nProvider>
); );
}, };
);
export default DatePickerAux; export default DatePickerAux;

View File

@@ -1,6 +1,6 @@
import React, { import React, {
forwardRef,
PropsWithChildren, PropsWithChildren,
RefAttributes,
useEffect, useEffect,
useImperativeHandle, useImperativeHandle,
useRef, useRef,
@@ -15,13 +15,13 @@ import {
FileUploaderRefType, FileUploaderRefType,
} from ":/components/Forms/FileUploader/index"; } from ":/components/Forms/FileUploader/index";
interface DropZoneProps extends FileUploaderProps, PropsWithChildren { type DropZoneProps = FileUploaderProps &
RefAttributes<FileUploaderRefType> &
PropsWithChildren<{
files: File[]; files: File[];
} }>;
export const DropZone = forwardRef<FileUploaderRefType, DropZoneProps>( export const DropZone = ({
(
{
multiple, multiple,
name, name,
state, state,
@@ -34,10 +34,9 @@ export const DropZone = forwardRef<FileUploaderRefType, DropZoneProps>(
files, files,
onFilesChange, onFilesChange,
children, children,
...props
}: DropZoneProps,
ref, ref,
) => { ...props
}: DropZoneProps) => {
const [dragActive, setDragActive] = useState(false); const [dragActive, setDragActive] = useState(false);
const container = useRef<HTMLLabelElement>(null); const container = useRef<HTMLLabelElement>(null);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
@@ -92,14 +91,10 @@ export const DropZone = forwardRef<FileUploaderRefType, DropZoneProps>(
return ( return (
<label <label
className={classNames( className={classNames("c__file-uploader", "c__file-uploader--" + state, {
"c__file-uploader",
"c__file-uploader--" + state,
{
"c__file-uploader--active": dragActive, "c__file-uploader--active": dragActive,
"c__file-uploader--animate-icon": animateIcon, "c__file-uploader--animate-icon": animateIcon,
}, })}
)}
onDragEnter={() => { onDragEnter={() => {
setDragActive(true); setDragActive(true);
}} }}
@@ -157,5 +152,4 @@ export const DropZone = forwardRef<FileUploaderRefType, DropZoneProps>(
</div> </div>
</label> </label>
); );
}, };
);

View File

@@ -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<
)} )}
</> </>
); );
}); };

View File

@@ -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<
)} )}
</> </>
); );
}); };

View File

@@ -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,8 +26,11 @@ export interface FileUploaderRefType {
reset: () => void; reset: () => void;
} }
export const FileUploader = forwardRef<FileUploaderRefType, FileUploaderProps>( export const FileUploader = ({
({ fullWidth, ...props }, ref) => { fullWidth,
ref,
...props
}: FileUploaderProps) => {
return ( return (
<Field fullWidth={fullWidth} className={props.className}> <Field fullWidth={fullWidth} className={props.className}>
{props.multiple ? ( {props.multiple ? (
@@ -36,5 +40,4 @@ export const FileUploader = forwardRef<FileUploaderRefType, FileUploaderProps>(
)} )}
</Field> </Field>
); );
}, };
);

View File

@@ -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<
} }
/> />
); );
}); };

View File

@@ -1,7 +1,7 @@
import React, { import React, {
forwardRef,
InputHTMLAttributes, InputHTMLAttributes,
ReactNode, ReactNode,
RefAttributes,
useEffect, useEffect,
useRef, useRef,
useState, useState,
@@ -20,12 +20,11 @@ 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, className,
defaultValue, defaultValue,
label, label,
@@ -34,10 +33,9 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
rightIcon, rightIcon,
charCounter, charCounter,
charCounterMax, charCounterMax,
...props
}: InputProps,
ref, ref,
) => { ...props
}: InputProps) => {
const classes = ["c__input"]; const classes = ["c__input"];
const inputRef = useRef<HTMLInputElement | null>(null); const inputRef = useRef<HTMLInputElement | null>(null);
const [inputFocus, setInputFocus] = useState(false); const [inputFocus, setInputFocus] = useState(false);
@@ -132,11 +130,8 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
}} }}
/> />
</LabelledBox> </LabelledBox>
{!!rightIcon && ( {!!rightIcon && <div className="c__input__icon-right">{rightIcon}</div>}
<div className="c__input__icon-right">{rightIcon}</div>
)}
</div> </div>
</Field> </Field>
); );
}, };
);

View File

@@ -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,11 +12,11 @@ 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,
@@ -42,8 +42,7 @@ export const Radio = forwardRef<HTMLInputElement, RadioProps>(
</Field> </Field>
</label> </label>
); );
}, };
);
export const RadioGroup = ({ export const RadioGroup = ({
className, className,

View File

@@ -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} />
);
});

View File

@@ -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,11 +9,13 @@ 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,
ref,
...props
}: SubProps) => {
const { t } = useCunningham(); const { t } = useCunningham();
const [optionsToDisplay, setOptionsToDisplay] = useState(props.options); const [optionsToDisplay, setOptionsToDisplay] = useState(props.options);
const [hasInputFocused, setHasInputFocused] = useState(false); const [hasInputFocused, setHasInputFocused] = useState(false);
@@ -152,5 +148,4 @@ export const SelectMonoSearchable = forwardRef<SelectHandle, SubProps>(
downshiftReturn.selectedItem.render()} downshiftReturn.selectedItem.render()}
</SelectMonoAux> </SelectMonoAux>
); );
}, };
);

View File

@@ -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,8 +27,7 @@ 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,
@@ -67,5 +61,4 @@ export const SelectMonoSimple = forwardRef<SelectHandle, SubProps>(
<SelectedOption option={downshiftReturn.selectedItem} {...props} /> <SelectedOption option={downshiftReturn.selectedItem} {...props} />
</SelectMonoAux> </SelectMonoAux>
); );
}, };
);

View File

@@ -1,12 +1,11 @@
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,
@@ -53,15 +52,12 @@ export const SelectMono = forwardRef<SelectHandle, SelectProps>(
{...props} {...props}
downshiftProps={commonDownshiftProps} downshiftProps={commonDownshiftProps}
value={value} value={value}
ref={ref}
/> />
) : ( ) : (
<SelectMonoSimple <SelectMonoSimple
{...props} {...props}
downshiftProps={commonDownshiftProps} downshiftProps={commonDownshiftProps}
value={value} value={value}
ref={ref}
/> />
); );
}, };
);

View File

@@ -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();

View File

@@ -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,10 +6,8 @@ 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(
@@ -30,8 +22,7 @@ export const SelectMultiSearchable = forwardRef<SelectHandle, SubProps>(
selectedItems: props.selectedItems, selectedItems: props.selectedItems,
onStateChange({ selectedItems: newSelectedItems, type }) { onStateChange({ selectedItems: newSelectedItems, type }) {
switch (type) { switch (type) {
case useMultipleSelection.stateChangeTypes case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownBackspace:
.SelectedItemKeyDownBackspace:
case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownDelete: case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownDelete:
case useMultipleSelection.stateChangeTypes.DropdownKeyDownBackspace: case useMultipleSelection.stateChangeTypes.DropdownKeyDownBackspace:
case useMultipleSelection.stateChangeTypes.FunctionRemoveSelectedItem: case useMultipleSelection.stateChangeTypes.FunctionRemoveSelectedItem:
@@ -151,10 +142,7 @@ export const SelectMultiSearchable = forwardRef<SelectHandle, SubProps>(
}} }}
useMultipleSelectionReturn={useMultipleSelectionReturn} useMultipleSelectionReturn={useMultipleSelectionReturn}
> >
<span <span className="c__select__inner__value__input" data-value={inputValue}>
className="c__select__inner__value__input"
data-value={inputValue}
>
<input <input
{...inputProps} {...inputProps}
onFocus={() => { onFocus={() => {
@@ -169,5 +157,4 @@ export const SelectMultiSearchable = forwardRef<SelectHandle, SubProps>(
</span> </span>
</SelectMultiAux> </SelectMultiAux>
); );
}, };
);

View File

@@ -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,10 +9,9 @@ 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),
@@ -25,17 +24,14 @@ export const SelectMultiSimple = forwardRef<SelectHandle, SubProps>(
highlighted: isSelected(option), highlighted: isSelected(option),
})); }));
} }
return props.options.filter( return props.options.filter(getMultiOptionsFilter(props.selectedItems, ""));
getMultiOptionsFilter(props.selectedItems, ""),
);
}, [props.selectedItems]); }, [props.selectedItems]);
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 case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownBackspace:
.SelectedItemKeyDownBackspace:
case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownDelete: case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownDelete:
case useMultipleSelection.stateChangeTypes.DropdownKeyDownBackspace: case useMultipleSelection.stateChangeTypes.DropdownKeyDownBackspace:
case useMultipleSelection.stateChangeTypes.FunctionRemoveSelectedItem: case useMultipleSelection.stateChangeTypes.FunctionRemoveSelectedItem:
@@ -78,8 +74,7 @@ export const SelectMultiSimple = forwardRef<SelectHandle, SubProps>(
// Remove the item if it is already selected. // Remove the item if it is already selected.
props.onSelectedItemsChange( props.onSelectedItemsChange(
props.selectedItems.filter( props.selectedItems.filter(
(selectedItem) => (selectedItem) => !optionsEqual(selectedItem, newSelectedItem),
!optionsEqual(selectedItem, newSelectedItem),
), ),
); );
} else { } else {
@@ -131,5 +126,4 @@ export const SelectMultiSimple = forwardRef<SelectHandle, SubProps>(
useMultipleSelectionReturn={useMultipleSelectionReturn} useMultipleSelectionReturn={useMultipleSelectionReturn}
/> />
); );
}, };
);

View File

@@ -1,20 +1,15 @@
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) =>
@@ -58,7 +53,6 @@ export const SelectMulti = forwardRef<SelectHandle, SelectMultiProps>(
{...props} {...props}
selectedItems={selectedItems} selectedItems={selectedItems}
onSelectedItemsChange={onSelectedItemsChange} onSelectedItemsChange={onSelectedItemsChange}
ref={ref}
/> />
) : ( ) : (
<SelectMultiSimple <SelectMultiSimple
@@ -66,8 +60,6 @@ export const SelectMulti = forwardRef<SelectHandle, SelectMultiProps>(
{...props} {...props}
selectedItems={selectedItems} selectedItems={selectedItems}
onSelectedItemsChange={onSelectedItemsChange} onSelectedItemsChange={onSelectedItemsChange}
ref={ref}
/> />
); );
}, };
);

View File

@@ -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"]

View File

@@ -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,11 +8,16 @@ 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,
labelSide = "left",
ref,
...props
}: SwitchProps) => {
const { const {
compact, compact,
className, className,
@@ -50,5 +55,4 @@ export const Switch = forwardRef<HTMLInputElement, SwitchProps>(
</Field> </Field>
</label> </label>
); );
}, };
);

View File

@@ -1,5 +1,5 @@
import React, { import React, {
forwardRef, RefAttributes,
TextareaHTMLAttributes, TextareaHTMLAttributes,
useEffect, useEffect,
useRef, useRef,
@@ -11,14 +11,22 @@ 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,
id,
defaultValue,
charCounter,
charCounterMax,
ref,
...props
}: TextAreaProps) => {
const areaRef = useRef<HTMLTextAreaElement | null>(null); const areaRef = useRef<HTMLTextAreaElement | null>(null);
const [inputFocus, setInputFocus] = useState(false); const [inputFocus, setInputFocus] = useState(false);
const [value, setValue] = useState(defaultValue || props.value || ""); const [value, setValue] = useState(defaultValue || props.value || "");
@@ -100,5 +108,4 @@ export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
</div> </div>
</Field> </Field>
); );
}, };
);

View File

@@ -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(() => {

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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(() => {