Skip to content

Commit

Permalink
feat: Use React 19 refs (#1069)
Browse files Browse the repository at this point in the history
This commit updates the code to use the simplified ref handling
introduced in React 19.

- Removed unnecessary `forwardRef` wrappers.
- Passed refs directly as props to function components.

This change simplifies the code and improves readability while taking
advantage of the latest React features.
  • Loading branch information
veslav3 authored Jan 2, 2025
1 parent 72c60ca commit b02f123
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 119 deletions.
82 changes: 43 additions & 39 deletions packages/components-react/src/Alert.tsx
Original file line number Diff line number Diff line change
@@ -1,51 +1,55 @@
import { Heading, Paragraph, Alert as UtrechtAlert } from '@utrecht/component-library-react';
import clsx from 'clsx';
import { ForwardedRef, forwardRef, PropsWithChildren, ReactNode } from 'react';
import { PropsWithChildren, ReactNode, RefObject } from 'react';
import { Icon } from './icon/Icon';

export interface AlertProps {
ref?: RefObject<HTMLDivElement>;
type: 'info' | 'ok' | 'warning' | 'error';
heading?: ReactNode;
headingLevel?: 1 | 2 | 3 | 4 | 5;
textContent?: ReactNode;
}
export const Alert = forwardRef(
(
{ type, heading, headingLevel, textContent, children, ...restProps }: PropsWithChildren<AlertProps>,
ref: ForwardedRef<HTMLDivElement>,
) => {
return (
<UtrechtAlert className="rhc-alert" ref={ref} type={type} {...restProps}>
<div
className={clsx('rhc-alert__icon-container', {
'rhc-alert__icon-container--ok': type === 'ok',
'rhc-alert__icon-container--error': type === 'error',
'rhc-alert__icon-container--warning': type === 'warning',
'rhc-alert__icon-container--info': type === 'info',
})}
>
<Icon
icon={
type === 'info'
? 'info-circle'
: type === 'ok'
? 'circle-check'
: type === 'warning'
? 'let-op'
: 'alert-circle'
}
/>
</div>
<div>
<Heading appearance="utrecht-heading-5" level={headingLevel || 3}>
{heading}
</Heading>
<Paragraph>{textContent}</Paragraph>
{children}
</div>
</UtrechtAlert>
);
},
);
export const Alert = ({
ref,
type,
heading,
headingLevel,
textContent,
children,
...restProps
}: PropsWithChildren<AlertProps>) => {
return (
<UtrechtAlert className="rhc-alert" ref={ref} type={type} {...restProps}>
<div
className={clsx('rhc-alert__icon-container', {
'rhc-alert__icon-container--ok': type === 'ok',
'rhc-alert__icon-container--error': type === 'error',
'rhc-alert__icon-container--warning': type === 'warning',
'rhc-alert__icon-container--info': type === 'info',
})}
>
<Icon
icon={
type === 'info'
? 'info-circle'
: type === 'ok'
? 'circle-check'
: type === 'warning'
? 'let-op'
: 'alert-circle'
}
/>
</div>
<div>
<Heading appearance="utrecht-heading-5" level={headingLevel || 3}>
{heading}
</Heading>
<Paragraph>{textContent}</Paragraph>
{children}
</div>
</UtrechtAlert>
);
};

Alert.displayName = 'Alert';
5 changes: 4 additions & 1 deletion packages/components-react/src/FileInput.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import '@testing-library/jest-dom';

import { fireEvent, render, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { FileInput, FileInputProps } from './FileInput';
Expand All @@ -10,6 +9,7 @@ beforeAll(() => {

describe('File Input tests', () => {
const defaultProps: FileInputProps = {
ref: { current: document.createElement('input') },
buttonText: 'Bestanden kiezen',
maxFileSizeInBytes: 10_485_760,
allowedFileTypes: '.doc,.docx,.xlsx,.pdf,.zip,.jpg,.png,.bmp,.gif',
Expand All @@ -30,6 +30,7 @@ describe('File Input tests', () => {
const mockOnFileChange = jest.fn();

const propsTest: FileInputProps = {
ref: { current: document.createElement('input') },
buttonText: 'Bestanden kiezen',
maxFileSizeInBytes: 10_485_760,
allowedFileTypes: '.doc,.docx,.xlsx,.pdf,.zip,.jpg,.png,.bmp,.gif',
Expand Down Expand Up @@ -57,6 +58,7 @@ describe('File Input tests', () => {
const mockOnFileChange = jest.fn();

const propsTest: FileInputProps = {
ref: { current: document.createElement('input') },
buttonText: 'Bestanden kiezen',
maxFileSizeInBytes: 10_485_760,
allowedFileTypes: '.doc,.docx,.xlsx,.pdf,.zip,.jpg,.png,.bmp,.gif',
Expand Down Expand Up @@ -87,6 +89,7 @@ describe('File Input tests', () => {
const mockOnFileChange = jest.fn();

const propsTest: FileInputProps = {
ref: { current: document.createElement('input') },
buttonText: 'Bestanden kiezen',
maxFileSizeInBytes: 10_485_760,
allowedFileTypes: '.doc,.docx,.xlsx,.pdf,.zip,.jpg,.bmp,.gif', // Removed .png from allow list
Expand Down
127 changes: 62 additions & 65 deletions packages/components-react/src/FileInput.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Paragraph } from '@utrecht/component-library-react';
import { ChangeEvent, ForwardedRef, forwardRef, PropsWithChildren, useRef, useState } from 'react';
import { ChangeEvent, PropsWithChildren, RefObject, useState } from 'react';
import { Button, ButtonProps } from './Button';
import { File } from './File';

export interface FileInputProps extends Omit<ButtonProps, 'appearance'> {
ref: RefObject<HTMLInputElement>;
buttonText: string;
buttonAppearance?: ButtonProps['appearance'];
maxFileSizeInBytes: number;
Expand All @@ -13,72 +14,68 @@ export interface FileInputProps extends Omit<ButtonProps, 'appearance'> {
onValueChange?: (callbackFiles: File[]) => void; // eslint-disable-line no-unused-vars
}

export const FileInput = forwardRef(
(
{
children,
buttonText,
maxFileSizeInBytes,
allowedFileTypes,
buttonAppearance,
fileSizeErrorMessage,
fileTypeErrorMessage,
onValueChange,
}: PropsWithChildren<FileInputProps>,
ref: ForwardedRef<HTMLDivElement>,
) => {
const [files, setFiles] = useState<File[]>([]);
const inputElement = useRef<HTMLInputElement | null>(null);
const onChange = (newFiles: FileList | null) => {
if (newFiles) {
const updatedFiles = [...files, ...Array.from(newFiles)];
setFiles(updatedFiles);
if (onValueChange) {
onValueChange(updatedFiles);
}
export const FileInput = ({
ref,
children,
buttonText,
maxFileSizeInBytes,
allowedFileTypes,
buttonAppearance,
fileSizeErrorMessage,
fileTypeErrorMessage,
onValueChange,
}: PropsWithChildren<FileInputProps>) => {
const [files, setFiles] = useState<File[]>([]);
const inputElement = ref;
const onChange = (newFiles: FileList | null) => {
if (newFiles) {
const updatedFiles = [...files, ...Array.from(newFiles)];
setFiles(updatedFiles);
if (onValueChange) {
onValueChange(updatedFiles);
}
};
}
};

return (
<div className="rhc-file-input" ref={ref}>
{children}
<input
multiple
accept={allowedFileTypes}
ref={inputElement}
style={{ display: 'none' }}
type="file"
onChange={(event: ChangeEvent<HTMLInputElement>) => {
onChange(event.target.files);
}}
/>
<div className="rhc-file-input__button-feedback-container">
<Button
appearance={buttonAppearance ?? 'secondary-action-button'}
onClick={() => inputElement.current && inputElement.current.click()}
>
{buttonText}
</Button>
{files.length === 0 && <Paragraph className="rhc-file-input__feedback">Geen bestand gekozen</Paragraph>}
</div>
<div className="rhc-file-input__files-container">
{files.map((item: File) => {
return (
<File
allowedFileTypes={allowedFileTypes}
file={item}
fileSizeErrorMessage={fileSizeErrorMessage}
fileTypeErrorMessage={fileTypeErrorMessage}
key={files.indexOf(item)}
maxFileSizeInBytes={maxFileSizeInBytes}
onDelete={(fileToRemove: File) => setFiles(files.filter((file) => file !== fileToRemove))}
/>
);
})}
</div>
return (
<div className="rhc-file-input" ref={ref}>
{children}
<input
multiple
accept={allowedFileTypes}
ref={inputElement}
style={{ display: 'none' }}
type="file"
onChange={(event: ChangeEvent<HTMLInputElement>) => {
onChange(event.target.files);
}}
/>
<div className="rhc-file-input__button-feedback-container">
<Button
appearance={buttonAppearance ?? 'secondary-action-button'}
onClick={() => inputElement.current && inputElement.current.click()}
>
{buttonText}
</Button>
{files.length === 0 && <Paragraph className="rhc-file-input__feedback">Geen bestand gekozen</Paragraph>}
</div>
);
},
);
<div className="rhc-file-input__files-container">
{files.map((item: File) => {
return (
<File
allowedFileTypes={allowedFileTypes}
file={item}
fileSizeErrorMessage={fileSizeErrorMessage}
fileTypeErrorMessage={fileTypeErrorMessage}
key={files.indexOf(item)}
maxFileSizeInBytes={maxFileSizeInBytes}
onDelete={(fileToRemove: File) => setFiles(files.filter((file) => file !== fileToRemove))}
/>
);
})}
</div>
</div>
);
};

FileInput.displayName = 'FileInput';
22 changes: 8 additions & 14 deletions packages/storybook/config/Prettify.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,18 @@
import * as prettier from 'prettier';
import pluginHTML from 'prettier/plugins/html';
import { useEffect, useState } from 'react';
import { use } from 'react';

interface Props {
ugly: string;
}

export function Prettify({ ugly }: Props): string {
const [prettySrc, setPrettySrc] = useState('');
useEffect(() => {
const prettify = async () => {
const pretty = await prettier.format(ugly, {
parser: 'html',
plugins: [pluginHTML],
});
setPrettySrc(pretty);
};
const prettify = async () => {
return prettier.format(ugly, {
parser: 'html',
plugins: [pluginHTML],
});
};

prettify();
}, []);

return prettySrc;
return use(prettify());
}

0 comments on commit b02f123

Please sign in to comment.