Updated project to React 19

This commit is contained in:
Richard Osborne
2025-12-07 17:32:53 +01:00
parent 2153baf627
commit 8fed72d025
70 changed files with 4534 additions and 5309 deletions

View File

@@ -1,23 +1,24 @@
const path = require('path');
import type { StorybookConfig } from '@storybook/react-webpack5';
import path from 'path';
const editorDir = path.join(__dirname, '../../noodl-editor');
const coreLibDir = path.join(__dirname, '../');
module.exports = {
const config: StorybookConfig = {
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(ts|tsx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'@storybook/preset-create-react-app',
'@storybook/addon-measure'
],
framework: '@storybook/react',
core: {
builder: '@storybook/builder-webpack5'
framework: {
name: '@storybook/react-webpack5',
options: {}
},
webpackFinal: (config) => {
const destinationPath = path.resolve(__dirname, '../../noodl-editor');
const addExternalPath = (rules) => {
const addExternalPath = (rules: any[]) => {
for (let i = 0; i < rules.length; i++) {
const rule = rules[i];
if (rule.test && RegExp(rule.test).test('.tsx')) {
@@ -32,17 +33,20 @@ module.exports = {
}
};
addExternalPath(config.module.rules);
if (config.module?.rules) {
addExternalPath(config.module.rules as any[]);
config.module.rules.push({
test: /\.ts$/,
use: [
{
loader: require.resolve('ts-loader')
}
]
});
config.module.rules.push({
test: /\.ts$/,
use: [
{
loader: require.resolve('ts-loader')
}
]
});
}
config.resolve = config.resolve || {};
config.resolve.alias = {
...config.resolve.alias,
'@noodl-core-ui': path.join(coreLibDir, 'src'),
@@ -56,5 +60,10 @@ module.exports = {
};
return config;
},
typescript: {
reactDocgen: 'react-docgen-typescript'
}
};
export default config;

View File

@@ -2,8 +2,8 @@
"name": "@noodl/noodl-core-ui",
"version": "2.7.0",
"scripts": {
"start": "start-storybook -p 6006 -s public",
"build": "build-storybook -s public"
"start": "storybook dev -p 6006",
"build": "storybook build"
},
"eslintConfig": {
"extends": [
@@ -42,14 +42,20 @@
"react-dom": "19.0.0"
},
"devDependencies": {
"@storybook/addon-essentials": "^8.6.14",
"@storybook/addon-interactions": "^8.6.14",
"@storybook/addon-links": "^8.6.14",
"@storybook/addon-measure": "^8.6.14",
"@storybook/react": "^8.6.14",
"@storybook/react-webpack5": "^8.6.14",
"@types/jest": "^27.5.2",
"@types/node": "^16.11.42",
"@types/react": "19.0.0",
"@types/react-dom": "19.0.0",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"babel-plugin-named-exports-order": "^0.0.2",
"prop-types": "^15.8.1",
"sass": "^1.90.0",
"storybook": "^9.1.3",
"storybook": "^8.6.14",
"ts-loader": "^9.5.4",
"typescript": "^4.9.5",
"web-vitals": "^3.5.2",

View File

@@ -1,5 +1,5 @@
import classNames from 'classnames';
import React, { ChangeEventHandler, cloneElement, FocusEventHandler, MouseEventHandler } from 'react';
import React, { ChangeEventHandler, cloneElement, FocusEventHandler, isValidElement, MouseEventHandler, ReactElement } from 'react';
import { InputNotification } from '@noodl-types/globalInputTypes';
@@ -113,7 +113,7 @@ export function Checkbox({
</div>
)}
{children && <div className={css['ChildContainer']}>{cloneElement(children, { isChecked })}</div>}
{children && isValidElement(children) && <div className={css['ChildContainer']}>{cloneElement(children as ReactElement<{ isChecked?: boolean }>, { isChecked })}</div>}
{label && <InputLabelSection label={label} />}
</label>
);

View File

@@ -89,7 +89,7 @@ export function CoreBaseDialog({
}, 50);
}, [isVisible]);
const dialogRef = useRef<HTMLDivElement>();
const dialogRef = useRef<HTMLDivElement>(null);
const [dialogPosition, setDialogPosition] = useState({
x: 0,
y: 0,

View File

@@ -45,7 +45,7 @@ export function Carousel({ activeIndex, items, indicator }: CarouselProps) {
<div style={{ overflow: 'hidden' }}>
<HStack UNSAFE_style={{ width: items.length * 100 + '%' }}>
{items.map((item, index) => (
<VStack key={index} ref={(ref) => (sliderRefs.current[index] = ref)} UNSAFE_style={{ width: '100%' }}>
<VStack key={index} ref={(ref) => { sliderRefs.current[index] = ref; }} UNSAFE_style={{ width: '100%' }}>
{item.slot}
</VStack>
))}

View File

@@ -91,6 +91,10 @@ export function Columns({
}}
>
{toArray(children).map((child, i) => {
// Skip non-element children (null, undefined, boolean)
if (!React.isValidElement(child)) {
return child;
}
return (
<div
className="column-item"

View File

@@ -1,5 +1,5 @@
import classNames from 'classnames';
import React, { cloneElement, MouseEventHandler } from 'react';
import React, { cloneElement, isValidElement, MouseEventHandler, ReactElement } from 'react';
import { Icon, IconName, IconSize } from '@noodl-core-ui/components/common/Icon';
import { BaseDialog, BaseDialogProps } from '@noodl-core-ui/components/layout/BaseDialog';
@@ -100,7 +100,10 @@ export function MenuDialog({
{item.label}
</Label>
</div>
{item.component && cloneElement(item.component(() => onClose()))}
{item.component && (() => {
const component = item.component(() => onClose());
return isValidElement(component) ? cloneElement(component as ReactElement) : component;
})()}
<div className={css['EndSlot']}>
{item.endSlot && typeof item.endSlot === 'string' && <Text>{item.endSlot}</Text>}
{item.endSlot && typeof item.endSlot !== 'string' && item.endSlot}

View File

@@ -1,92 +1,91 @@
import { Slot } from '@noodl-core-ui/types/global';
import classNames from 'classnames';
import React, { CSSProperties, useLayoutEffect, useRef, useState } from 'react';
import css from './PopupSection.module.scss';
/**
* TODO:
* - Remove style prop and replace with "isInactive" prop
*/
export interface PopupSectionProps {
children?: Slot;
title?: string;
className?: string;
maxContentHeight?: number;
hasBottomBorder?: boolean;
hasYPadding?: boolean;
isCenteringChildren?: boolean;
style?: CSSProperties;
contentContainerStyle?: CSSProperties;
}
export function PopupSection({
children,
title,
className,
maxContentHeight,
hasBottomBorder,
hasYPadding,
isCenteringChildren,
style,
contentContainerStyle
}: PopupSectionProps) {
const contentRef = useRef<HTMLDivElement>();
const [shouldScroll, setShouldScroll] = useState(false);
const [hasBeenCalculated, setHasBeenCalculated] = useState(false);
function checkIfShouldScroll() {
if (!contentRef.current || !maxContentHeight) return false;
if (contentRef.current.getBoundingClientRect().height >= maxContentHeight) return true;
return false;
}
useLayoutEffect(() => {
if (contentRef.current) {
setShouldScroll(checkIfShouldScroll());
setHasBeenCalculated(true);
}
}, [contentRef, children]);
return (
<section
className={classNames([css['Root'], hasBottomBorder && css['has-bottom-border'], className])}
style={{
...style,
overflowY: hasBeenCalculated ? undefined : 'hidden',
height: hasBeenCalculated ? undefined : 0
}}
>
{title && (
<header className={css['Header']}>
<h2 className={css['Title']}>{title}</h2>
</header>
)}
{children ? (
<div
className={classNames([
css['Content'],
hasYPadding && css['has-y-padding'],
isCenteringChildren && css['is-centering-children']
])}
ref={contentRef}
style={{
...contentContainerStyle,
height: shouldScroll ? maxContentHeight : undefined,
// @ts-expect-error
overflowY: shouldScroll ? 'overlay' : undefined
}}
>
{children}
</div>
) : (
<div ref={contentRef} />
)}
</section>
);
}
import { Slot } from '@noodl-core-ui/types/global';
import classNames from 'classnames';
import React, { CSSProperties, useLayoutEffect, useRef, useState } from 'react';
import css from './PopupSection.module.scss';
/**
* TODO:
* - Remove style prop and replace with "isInactive" prop
*/
export interface PopupSectionProps {
children?: Slot;
title?: string;
className?: string;
maxContentHeight?: number;
hasBottomBorder?: boolean;
hasYPadding?: boolean;
isCenteringChildren?: boolean;
style?: CSSProperties;
contentContainerStyle?: CSSProperties;
}
export function PopupSection({
children,
title,
className,
maxContentHeight,
hasBottomBorder,
hasYPadding,
isCenteringChildren,
style,
contentContainerStyle
}: PopupSectionProps) {
const contentRef = useRef<HTMLDivElement>(null);
const [shouldScroll, setShouldScroll] = useState(false);
const [hasBeenCalculated, setHasBeenCalculated] = useState(false);
function checkIfShouldScroll() {
if (!contentRef.current || !maxContentHeight) return false;
if (contentRef.current.getBoundingClientRect().height >= maxContentHeight) return true;
return false;
}
useLayoutEffect(() => {
if (contentRef.current) {
setShouldScroll(checkIfShouldScroll());
setHasBeenCalculated(true);
}
}, [contentRef, children]);
return (
<section
className={classNames([css['Root'], hasBottomBorder && css['has-bottom-border'], className])}
style={{
...style,
overflowY: hasBeenCalculated ? undefined : 'hidden',
height: hasBeenCalculated ? undefined : 0
}}
>
{title && (
<header className={css['Header']}>
<h2 className={css['Title']}>{title}</h2>
</header>
)}
{children ? (
<div
className={classNames([
css['Content'],
hasYPadding && css['has-y-padding'],
isCenteringChildren && css['is-centering-children']
])}
ref={contentRef}
style={{
...contentContainerStyle,
height: shouldScroll ? maxContentHeight : undefined,
overflowY: shouldScroll ? 'overlay' : undefined
}}
>
{children}
</div>
) : (
<div ref={contentRef} />
)}
</section>
);
}

View File

@@ -39,7 +39,7 @@ export function PropertyPanelSelectInput({
hasSmallText
}: PropertyPanelSelectInputProps) {
const [isSelectCollapsed, setIsSelectCollapsed] = useState(true);
const rootRef = useRef<HTMLDivElement>();
const rootRef = useRef<HTMLDivElement>(null);
const displayValue = properties?.options.find((option) => option.value === value)?.label;

View File

@@ -50,7 +50,7 @@ export function PropertyPanelSliderInput({
}
const thumbPercentage = useMemo(
() => linearMap(parseInt(value.toString()), properties.min, properties.max, 0, 100),
() => linearMap(parseInt(value.toString()), Number(properties.min), Number(properties.max), 0, 100),
[value, properties]
);

View File

@@ -1,24 +1,18 @@
import React, {
JSXElementConstructor,
ReactChild,
ReactElement,
ReactFragment,
ReactPortal,
ReactText
} from 'react';
export interface UnsafeStyleProps {
UNSAFE_className?: string;
UNSAFE_style?: React.CSSProperties;
}
// FIXME: add generics to be able to specify what exact components are allowed?
export type SingleSlot =
| ReactElement<TSFixme, TSFixme>
| ReactFragment
| ReactPortal
| boolean
| null
| undefined;
export type Slot = SingleSlot | SingleSlot[];
import React, { ReactElement, ReactPortal } from 'react';
export interface UnsafeStyleProps {
UNSAFE_className?: string;
UNSAFE_style?: React.CSSProperties;
}
// FIXME: add generics to be able to specify what exact components are allowed?
// Note: ReactFragment removed in React 19, using React.ReactNode for fragments
export type SingleSlot =
| ReactElement<TSFixme, TSFixme>
| Iterable<React.ReactNode>
| ReactPortal
| boolean
| null
| undefined;
export type Slot = SingleSlot | SingleSlot[];