Skip to content

Commit

Permalink
feat: react 19 (#1050)
Browse files Browse the repository at this point in the history
* feat: react 19

* fix: react select e2e tests

* fix: transition in React dev mode
  • Loading branch information
quentinderoubaix authored Dec 12, 2024
1 parent c7e05dc commit aa20258
Show file tree
Hide file tree
Showing 24 changed files with 3,215 additions and 1,245 deletions.
302 changes: 143 additions & 159 deletions demo/src/lib/stackblitz/react-bootstrap/package-lock.json

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions demo/src/lib/stackblitz/react-bootstrap/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@
"devDependencies": {
"@amadeus-it-group/tansu": "^2.0.0",
"@floating-ui/dom": "^1.6.12",
"@types/react": "^18.3.14",
"@types/react-dom": "^18.3.2",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^4.3.4",
"bootstrap-icons": "^1.11.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"sass": "^1.82.0",
"tslib": "^2.8.1",
"typescript": "~5.6.3",
Expand Down
304 changes: 143 additions & 161 deletions demo/src/lib/stackblitz/react-daisyui/package-lock.json

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions demo/src/lib/stackblitz/react-daisyui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@
"devDependencies": {
"@amadeus-it-group/tansu": "^2.0.0",
"@floating-ui/dom": "^1.6.12",
"@types/react": "^18.3.14",
"@types/react-dom": "^18.3.2",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
"daisyui": "^4.12.20",
"postcss": "^8.4.49",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tailwindcss": "^3.4.16",
"tslib": "^2.8.1",
"typescript": "~5.6.3",
Expand Down
3,545 changes: 2,759 additions & 786 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
"@eslint/js": "^9.16.0",
"@playwright/test": "^1.49.0",
"@types/node": "^22.10.1",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@types/uuid": "^10.0.0",
"@vitest/browser": "^2.1.8",
"@vitest/eslint-plugin": "^1.1.14",
Expand Down
8 changes: 6 additions & 2 deletions react/bootstrap/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,14 @@
"@agnos-ui/react-headless": "0.0.0",
"classnames": "^2.5.1"
},
"devDependencies": {
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0"
},
"peerDependencies": {
"@amadeus-it-group/tansu": "^2.0.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"sideEffects": false
}
62 changes: 29 additions & 33 deletions react/bootstrap/src/components/accordion/accordion.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {Slot} from '@agnos-ui/react-headless/slot';
import type {Directive} from '@agnos-ui/react-headless/types';
import {classDirective, useDirectives} from '@agnos-ui/react-headless/utils/directive';
import type {ForwardRefExoticComponent, ForwardedRef, PropsWithChildren, RefAttributes} from 'react';
import {createContext, forwardRef, useContext, useEffect, useImperativeHandle} from 'react';
import type {ForwardedRef, PropsWithChildren, Ref} from 'react';
import {createContext, useContext, useEffect, useImperativeHandle} from 'react';
import {useWidgetWithConfig} from '../../config';
import type {AccordionApi, AccordionItemApi, AccordionItemContext, AccordionItemProps, AccordionProps} from './accordion.gen';
import {createAccordion} from './accordion.gen';
Expand Down Expand Up @@ -45,7 +45,7 @@ export const AccordionItemDefaultSlotStructure = (slotContext: AccordionItemCont
* AccordionItem component is a part of the Accordion component suite.
*
* @param props - The properties for the AccordionItem component.
* @param ref - The forwarded ref to the AccordionItemApi.
* @param props.ref - The forwarded ref to the AccordionItemApi.
*
* @returns The rendered AccordionItem component.
* @remarks
Expand All @@ -60,24 +60,22 @@ export const AccordionItemDefaultSlotStructure = (slotContext: AccordionItemCont
* @see {@link useWidgetWithConfig}
* @see {@link useDirectives}
*/
export const AccordionItem: ForwardRefExoticComponent<Partial<AccordionItemProps> & RefAttributes<AccordionItemApi>> = forwardRef(
function AccordionItem(props: Partial<AccordionItemProps>, ref: ForwardedRef<AccordionItemApi>) {
const {registerItem} = useContext(AccordionDIContext);
const widgetContext = useWidgetWithConfig(registerItem!, props, null, {
structure: AccordionItemDefaultSlotStructure,
});
const {state, api, directives} = widgetContext;
useImperativeHandle(ref, () => api, [api]);
useEffect(() => {
api.initDone();
}, [api]);
return (
<div {...useDirectives([classDirective, `accordion-item ${state.className}`], directives.itemDirective)}>
<Slot slotContent={state.structure} props={widgetContext} />
</div>
);
},
);
export function AccordionItem(props: Partial<AccordionItemProps> & {ref?: Ref<AccordionItemApi>}) {
const {registerItem} = useContext(AccordionDIContext);
const widgetContext = useWidgetWithConfig(registerItem!, props, null, {
structure: AccordionItemDefaultSlotStructure,
});
const {state, api, directives} = widgetContext;
useImperativeHandle(props.ref, () => api, [api]);
useEffect(() => {
api.initDone();
}, [api]);
return (
<div {...useDirectives([classDirective, `accordion-item ${state.className}`], directives.itemDirective)}>
<Slot slotContent={state.structure} props={widgetContext} />
</div>
);
}

/**
* Accordion component that provides a collapsible content container.
Expand All @@ -87,19 +85,17 @@ export const AccordionItem: ForwardRefExoticComponent<Partial<AccordionItemProps
* {@link https://react.dev/reference/react/useImperativeHandle | useImperativeHandle} to bind the widget API to the ref.
*
* @param props - The properties for the Accordion component.
* @param ref - The ref to be forwarded to the Accordion API.
* @param props.ref - The ref to be forwarded to the Accordion API.
*
* @returns The rendered Accordion component.
*
*/
export const Accordion: ForwardRefExoticComponent<PropsWithChildren<Partial<AccordionProps>> & RefAttributes<AccordionApi>> = forwardRef(
function Accordion(props: PropsWithChildren<Partial<AccordionProps>>, ref: ForwardedRef<AccordionApi>) {
const widget = useWidgetWithConfig(createAccordion, props, 'accordion');
useImperativeHandle(ref, () => widget.api, [widget.api]);
return (
<AccordionDIContext.Provider value={widget.api}>
<div {...useDirectives([classDirective, 'accordion'], widget.directives.accordionDirective)}>{props.children}</div>
</AccordionDIContext.Provider>
);
},
);
export function Accordion(props: PropsWithChildren<Partial<AccordionProps>> & {ref?: ForwardedRef<AccordionApi>}) {
const widget = useWidgetWithConfig(createAccordion, props, 'accordion');
useImperativeHandle(props.ref, () => widget.api, [widget.api]);
return (
<AccordionDIContext value={widget.api}>
<div {...useDirectives([classDirective, 'accordion'], widget.directives.accordionDirective)}>{props.children}</div>
</AccordionDIContext>
);
}
15 changes: 6 additions & 9 deletions react/bootstrap/src/components/alert/alert.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {Slot} from '@agnos-ui/react-headless/slot';
import {classDirective, useDirectives} from '@agnos-ui/react-headless/utils/directive';
import type {ForwardedRef, ForwardRefExoticComponent, RefAttributes} from 'react';
import {forwardRef, useImperativeHandle} from 'react';
import type {Ref} from 'react';
import {useImperativeHandle} from 'react';
import {useWidgetWithConfig} from '../../config';
import type {AlertApi, AlertContext, AlertProps} from './alert.gen';
import {createAlert} from './alert.gen';
Expand Down Expand Up @@ -45,17 +45,14 @@ const AlertElement = (slotContext: AlertContext) => (
* and the {@link https://react.dev/reference/react/useImperativeHandle | useImperativeHandle} hook to expose the widget's API via the ref.
*
* @param props - Partial properties of the AlertProps interface.
* @param ref - Forwarded reference to the AlertApi.
* @param props.ref - Forwarded reference to the AlertApi.
*
* @returns A JSX element that conditionally renders the AlertElement based on the widget's hidden state.
*/
export const Alert: ForwardRefExoticComponent<Partial<AlertProps> & RefAttributes<AlertApi>> = forwardRef(function Alert(
props: Partial<AlertProps>,
ref: ForwardedRef<AlertApi>,
) {
export function Alert(props: Partial<AlertProps> & {ref?: Ref<AlertApi>}) {
const widgetContext = useWidgetWithConfig(createAlert, props, 'alert', {
structure: AlertDefaultSlotStructure,
});
useImperativeHandle(ref, () => widgetContext.api, [widgetContext.api]);
useImperativeHandle(props.ref, () => widgetContext.api, [widgetContext.api]);
return <>{!widgetContext.state.hidden && <AlertElement {...widgetContext} />}</>;
});
}
18 changes: 8 additions & 10 deletions react/bootstrap/src/components/collapse/collapse.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type {ForwardedRef, ForwardRefExoticComponent, PropsWithChildren, RefAttributes} from 'react';
import {forwardRef, useImperativeHandle} from 'react';
import type {PropsWithChildren, Ref} from 'react';
import {useImperativeHandle} from 'react';
import {useWidgetWithConfig} from '../../config';
import type {CollapseApi, CollapseProps} from './collapse.gen';
import {createCollapse} from './collapse.gen';
Expand All @@ -13,15 +13,13 @@ import {useDirectives} from '@agnos-ui/react-headless/utils/directive';
*
* @param props - The properties for the Collapse component.
* @param props.children - The child elements to be rendered inside the collapsible container.
* @param ref - A ref object to access the Collapse API.
* @param props.ref - A ref object to access the Collapse API.
*
* @returns A div element with transition directives applied, containing the child elements.
*/
export const Collapse: ForwardRefExoticComponent<PropsWithChildren<Partial<CollapseProps>> & RefAttributes<CollapseApi>> = forwardRef(
function Collapse(props: PropsWithChildren<Partial<CollapseProps>>, ref: ForwardedRef<CollapseApi>) {
const {api, directives} = useWidgetWithConfig(createCollapse, props, 'collapse');
useImperativeHandle(ref, () => api, [api]);
export function Collapse(props: PropsWithChildren<Partial<CollapseProps>> & {ref?: Ref<CollapseApi>}) {
const {api, directives} = useWidgetWithConfig(createCollapse, props, 'collapse');
useImperativeHandle(props.ref, () => api, [api]);

return <div {...useDirectives(directives.collapseDirective)}>{props.children}</div>;
},
);
return <div {...useDirectives(directives.collapseDirective)}>{props.children}</div>;
}
14 changes: 7 additions & 7 deletions react/bootstrap/src/components/modal/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import type {Directive} from '@agnos-ui/react-headless/types';
import {classDirective, useDirective, useDirectives} from '@agnos-ui/react-headless/utils/directive';
import {Portal} from '@agnos-ui/react-headless/utils/portal';
import classNames from 'classnames';
import type {Ref, RefAttributes} from 'react';
import {forwardRef, useImperativeHandle} from 'react';
import type {Ref} from 'react';
import {useImperativeHandle} from 'react';
import ReactDOM from 'react-dom/client';
import {useWidgetWithConfig} from '../../config';
import type {ModalApi, ModalContext, ModalProps} from './modal.gen';
Expand Down Expand Up @@ -71,28 +71,28 @@ const ModalElement = <Data,>(slotContext: ModalContext<Data>) => {
};

/**
* A Modal component that uses a forwardRef to expose its API.
* A Modal component
*
* @template Data - The type of data that the modal will handle.
*
* @param props - The properties for the Modal component.
* @param ref - A ref to access the Modal API.
* @param props.ref - A ref to access the Modal API.
*
* @returns The rendered Modal component.
*/
export const Modal = forwardRef(function Modal<Data>(props: Partial<ModalProps<Data>>, ref: Ref<ModalApi<Data>>) {
export function Modal<Data>(props: Partial<ModalProps<Data>> & {ref?: Ref<ModalApi<Data>>}) {
const widgetContext = useWidgetWithConfig(createModal<Data>, props, 'modal', {
header: ModalDefaultSlotHeader,
structure: ModalDefaultSlotStructure,
});
useImperativeHandle(ref, () => widgetContext.api, [widgetContext.api]);
useImperativeHandle(props.ref, () => widgetContext.api, [widgetContext.api]);
return (
<Portal container={widgetContext.state.container}>
{!widgetContext.state.backdropHidden && <BackdropElement {...widgetContext} />}
{!widgetContext.state.hidden && <ModalElement {...widgetContext} />}
</Portal>
);
}) as <Data>(props: Partial<ModalProps<Data>> & RefAttributes<ModalApi<Data>>) => JSX.Element;
}

/**
* Opens a modal dialog with the specified options.
Expand Down
7 changes: 5 additions & 2 deletions react/bootstrap/src/components/pagination/pagination.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import {Slot} from '@agnos-ui/react-headless/slot';
import classNames from 'classnames';
import {useWidgetWithConfig} from '../../config';
import {NavButton, PageItem} from './pageItem';
import type {PaginationContext, PaginationProps} from './pagination.gen';
import type {PaginationApi, PaginationContext, PaginationProps} from './pagination.gen';
import {createPagination} from './pagination.gen';
import {type Ref, useImperativeHandle} from 'react';

/**
* Renders the default slot pages for the pagination component.
Expand Down Expand Up @@ -114,13 +115,15 @@ export const PaginationDefaultSlotStructure = (slotContext: PaginationContext) =
* It uses the {@link useWidgetWithConfig} hook to create a pagination widget with the provided props.
*
* @param props - The properties for the Pagination component.
* @param props.ref - Forwarded reference to the PaginationApi.
* @returns The rendered pagination navigation element.
*/
export function Pagination(props: Partial<PaginationProps>) {
export function Pagination(props: Partial<PaginationProps> & {ref?: Ref<PaginationApi>}) {
const widgetContext = useWidgetWithConfig(createPagination, props, 'pagination', {
pagesDisplay: PaginationDefaultSlotPages,
structure: PaginationDefaultSlotStructure,
});
useImperativeHandle(props.ref, () => widgetContext.api, [widgetContext.api]);

return (
<nav aria-label={widgetContext.state.ariaLabel}>
Expand Down
15 changes: 8 additions & 7 deletions react/bootstrap/src/components/rating/rating.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
import {Slot} from '@agnos-ui/react-headless/slot';
import {classDirective, useDirective, useDirectives} from '@agnos-ui/react-headless/utils/directive';
import React from 'react';
import {type Ref, useImperativeHandle, Fragment} from 'react';
import {useWidgetWithConfig} from '../../config';
import type {RatingDirectives, RatingProps, RatingState, StarContext} from './rating.gen';
import type {RatingApi, RatingDirectives, RatingProps, RatingState, StarContext} from './rating.gen';
import {createRating} from './rating.gen';

function Star({star, state, directive}: {star: StarContext; state: RatingState; directive: RatingDirectives['starDirective']}) {
const arg = {index: star.index};
return (
<React.Fragment key={star.index}>
<Fragment key={star.index}>
<span className="visually-hidden">({star.index < state.visibleRating ? '*' : ' '})</span>
<span {...useDirective(directive, arg)}>
<span {...useDirective(directive, {index: star.index})}>
<Slot slotContent={state.star} props={star}></Slot>
</span>
</React.Fragment>
</Fragment>
);
}

Expand All @@ -28,11 +27,13 @@ function Star({star, state, directive}: {star: StarContext; state: RatingState;
* It applies directives to the container and individual stars for styling and behavior.
*
*/
export function Rating(props: Partial<RatingProps>) {
export function Rating(props: Partial<RatingProps> & {ref?: Ref<RatingApi>}) {
const {
state,
directives: {containerDirective, starDirective},
api,
} = useWidgetWithConfig(createRating, props, 'rating');
useImperativeHandle(props.ref, () => api, [api]);

return (
<div {...useDirectives([classDirective, 'd-inline-flex'], containerDirective)}>
Expand Down
8 changes: 5 additions & 3 deletions react/bootstrap/src/components/select/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import {Slot} from '@agnos-ui/react-headless/slot';
import {classDirective, useDirective, useDirectives} from '@agnos-ui/react-headless/utils/directive';
import classNames from 'classnames';
import {useWidgetWithConfig} from '../../config';
import type {ItemContext, SelectContext, SelectItemContext, SelectProps, SelectWidget} from './select.gen';
import type {ItemContext, SelectApi, SelectContext, SelectItemContext, SelectProps, SelectWidget} from './select.gen';
import {createSelect} from './select.gen';
import {type Ref, useImperativeHandle} from 'react';

function DefaultBadge<Item>(slotContext: SelectItemContext<Item>) {
return <>{'' + slotContext.itemContext.item}</>;
Expand Down Expand Up @@ -63,11 +64,12 @@ function Rows<Item>({slotContext}: {slotContext: SelectContext<Item>; menuId: st
* This component uses a widget context to manage its state and directives. It supports
* custom badge labels and item labels through the widget configuration.
*/
export function Select<Item>(props: Partial<SelectProps<Item>>) {
export function Select<Item>(props: Partial<SelectProps<Item>> & {ref?: Ref<SelectApi<Item>>}) {
const widgetContext = useWidgetWithConfig<SelectWidget<Item>>(createSelect, props, 'select', {
badgeLabel: DefaultBadge,
itemLabel: DefaultItem,
});
useImperativeHandle(props.ref, () => widgetContext.api, [widgetContext.api]);
const {
state: {id, visibleItems, filterText, open, className},
directives: {hasFocusDirective, referenceDirective, inputContainerDirective, inputDirective},
Expand All @@ -78,7 +80,7 @@ export function Select<Item>(props: Partial<SelectProps<Item>>) {
<div {...useDirectives([classDirective, `au-select dropdown border border-1 p-1 mb-3 d-block ${className}`], referenceDirective)}>
<div {...useDirectives([classDirective, 'd-flex align-items-center flex-wrap gap-1'], hasFocusDirective, inputContainerDirective)}>
<Badges slotContext={widgetContext}></Badges>
<input value={filterText} {...useDirective(inputDirective)} onChange={() => {}} />
<input type="text" value={filterText} {...useDirective(inputDirective)} onChange={() => {}} />
</div>
{open && visibleItems.length > 0 && <Rows slotContext={widgetContext} menuId={menuId} />}
</div>
Expand Down
14 changes: 5 additions & 9 deletions react/bootstrap/src/components/toast/toast.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {Slot} from '@agnos-ui/react-headless/slot';
import {classDirective, useDirectives} from '@agnos-ui/react-headless/utils/directive';
import type {ForwardRefExoticComponent, RefAttributes} from 'react';
import {forwardRef, useImperativeHandle} from 'react';
import {type Ref, useImperativeHandle} from 'react';
import {useWidgetWithConfig} from '../../config';
import type {ToastApi, ToastContext, ToastProps} from './toast.gen';
import {createToast} from './toast.gen';
Expand Down Expand Up @@ -59,19 +58,16 @@ const ToastElement = (slotContext: ToastContext) => (
* to expose the widget's API through the forwarded ref.
*
* @param props - Partial properties of `ToastProps` to configure the toast widget.
* @param ref - Ref to expose the Toast API.
* @param props.ref - Ref to expose the Toast API.
*
* @returns A JSX element that conditionally renders the `ToastElement` based on the widget's state.
*/
export const Toast: ForwardRefExoticComponent<Partial<ToastProps> & RefAttributes<ToastApi>> = forwardRef(function Toast(
props: Partial<ToastProps>,
ref,
) {
export function Toast(props: Partial<ToastProps> & {ref?: Ref<ToastApi>}) {
const widgetContext = useWidgetWithConfig(createToast, props, 'toast', {
structure: ToastDefaultSlotStructure,
children: props.children,
});
useImperativeHandle(ref, () => widgetContext.api, [widgetContext.api]);
useImperativeHandle(props.ref, () => widgetContext.api, [widgetContext.api]);

return <>{!widgetContext.state.hidden && <ToastElement {...widgetContext} />}</>;
});
}
Loading

0 comments on commit aa20258

Please sign in to comment.