Skip to content

Commit

Permalink
Prepare components to be used with React 18 (#94)
Browse files Browse the repository at this point in the history
* Prepare components to be used with React18

* Change files

* Remove act

* Remove unnecessary waitFor

* Fix test
  • Loading branch information
saskliutas committed Mar 10, 2023
1 parent 126f503 commit d46358e
Show file tree
Hide file tree
Showing 30 changed files with 535 additions and 469 deletions.
2 changes: 1 addition & 1 deletion apps/test-app/frontend/package.json
Expand Up @@ -49,7 +49,7 @@
"html-webpack-plugin": "^5.5.0",
"react": "^17.0.0",
"react-dom": "^17.0.0",
"react-resize-detector": "^6.7.6",
"react-resize-detector": "^8.0.4",
"sass": "^1.58.3",
"sass-loader": "^13.2.0",
"source-map-loader": "^4.0.1",
Expand Down
Expand Up @@ -39,7 +39,7 @@ export function PropertiesWidget(props: Props) {
const [activeMatchIndex, setActiveMatchIndex] = useState(0);
const [activeHighlight, setActiveHighlight] = useState<HighlightInfo>();

const setFilter = useCallback((filter) => {
const setFilter = useCallback((filter: string) => {
if (filter !== filterText)
setFilterText(filter);
}, [filterText]);
Expand Down
Expand Up @@ -41,7 +41,7 @@ function PresentationTable(props: PresentationTableProps) {
rowMapper: mapTableRow,
});

const onSort = useCallback((tableState) => {
const onSort = useCallback((tableState: any) => {
const sortBy = tableState.sortBy[0];
sort(sortBy?.id, sortBy?.desc);
}, [sort]);
Expand Down
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Prepared package to be used with React 18. Updated React peerDependency to \"^17.0.0 || ^18.0.0\"",
"packageName": "@itwin/presentation-components",
"email": "24278440+saskliutas@users.noreply.github.com",
"dependentChangeType": "patch"
}
12 changes: 5 additions & 7 deletions packages/components/package.json
Expand Up @@ -25,7 +25,7 @@
"module": "lib/esm/presentation-components.js",
"types": "lib/cjs/presentation-components.d.ts",
"scripts": {
"build": "npm run -s copy:locale && npm run -s pseudolocalize && npm run -s build:cjs && npm run -s build:esm",
"build": "npm run -s copy:locale && npm run -s build:cjs && npm run -s build:esm",
"build:cjs": "npm run -s copy:cjs && tsc -p tsconfig.cjs.json",
"build:esm": "npm run -s copy:esm && tsc -p tsconfig.esm.json",
"copy:locale": "cpx \"./public/**/*\" ./lib/public",
Expand All @@ -35,7 +35,6 @@
"clean": "rimraf lib",
"cover": "nyc npm -s test",
"lint": "eslint ./src/**/*.{ts,tsx}",
"pseudolocalize": "betools pseudolocalize --englishDir ./lib/public/locales/en --out ./lib/public/locales/en-PSEUDO",
"test": "mocha --config ./.mocharc.json \"./lib/cjs/test/**/*.test.js\"",
"test:watch": "npm -s test -- --reporter min --watch-extensions ts,tsx --watch",
"docs": "npm run -s docs:extract && npm run -s docs:reference && npm run -s docs:changelog",
Expand All @@ -52,8 +51,8 @@
"fast-deep-equal": "^3.1.3",
"fast-sort": "^3.0.2",
"micro-memoize": "^4.0.9",
"react-select": "3.2.0",
"react-select-async-paginate": "0.5.3",
"react-select": "5.7.0",
"react-select-async-paginate": "0.7.2",
"rxjs": "^6.6.2"
},
"peerDependencies": {
Expand All @@ -66,8 +65,8 @@
"@itwin/imodel-components-react": "^4.0.0-dev.11",
"@itwin/presentation-common": "^3.6.0 || ^4.0.0-dev.37",
"@itwin/presentation-frontend": "^3.6.0 || ^4.0.0-dev.37",
"react": "^17.0.0",
"react-dom": "^17.0.0"
"react": "^17.0.0 || ^18.0.0",
"react-dom": "^17.0.0 || ^18.0.0"
},
"devDependencies": {
"@itwin/appui-abstract": "^4.0.0-dev.37",
Expand Down Expand Up @@ -96,7 +95,6 @@
"@types/mocha": "^8.2.2",
"@types/react": "^17.0.37",
"@types/react-dom": "^17.0.0",
"@types/react-select": "3.0.26",
"@types/sinon": "^9.0.0",
"@types/sinon-chai": "^3.2.0",
"chai": "^4.1.2",
Expand Down
Expand Up @@ -6,7 +6,7 @@
* @module Core
*/

import { LegacyRef, MutableRefObject, RefCallback, useCallback, useEffect, useRef, useState } from "react";
import { LegacyRef, MutableRefObject, RefCallback, useCallback, useRef, useState } from "react";
import { Primitives, PrimitiveValue, PropertyDescription, PropertyRecord, PropertyValueFormat } from "@itwin/appui-abstract";
import { IPropertyValueRenderer, PropertyValueRendererManager } from "@itwin/components-react";
import { assert, Guid, GuidString, IDisposable } from "@itwin/core-bentley";
Expand Down Expand Up @@ -164,8 +164,6 @@ export function useResizeObserver<T extends HTMLElement>() {
}
}, []);

useEffect(() => () => { observer.current?.disconnect(); }, []);

return {
ref,
width,
Expand Down
Expand Up @@ -8,7 +8,7 @@

import "./InstanceFilterBuilder.scss";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { ActionMeta } from "react-select";
import { ActionMeta, MultiValue, SingleValue } from "react-select";
import { BehaviorSubject, from, of } from "rxjs";
import { map } from "rxjs/internal/operators/map";
import { switchAll } from "rxjs/internal/operators/switchAll";
Expand Down Expand Up @@ -51,7 +51,7 @@ export interface InstanceFilterBuilderProps extends PropertyFilterBuilderProps {
export function InstanceFilterBuilder(props: InstanceFilterBuilderProps) {
const { selectedClasses, classes, onClassSelected, onClassDeselected, onClearClasses, ...restProps } = props;

const onSelectChange = useCallback((_, action: ActionMeta<ClassInfo>) => {
const onSelectChange = useCallback((_: MultiValue<ClassInfo> | SingleValue<ClassInfo>, action: ActionMeta<ClassInfo>) => {
switch (action.action) {
case "select-option":
action.option && onClassSelected(action.option);
Expand Down Expand Up @@ -191,8 +191,6 @@ function useProperties(propertyInfos: InstanceFilterPropertyInfo[], selectedClas
function useSelectedClasses(classes: ClassInfo[], imodel: IModelConnection, initialClasses?: ClassInfo[]) {
const [selectedClasses, setSelectedClasses] = useState<ClassInfo[]>(initialClasses ?? []);
const [isFilteringClasses, setIsFilteringClasses] = useState(false);
const disposedRef = useRef(false);
useEffect(() => () => { disposedRef.current = true; }, []);

const firstRender = useRef(true);
useEffect(() => {
Expand All @@ -201,36 +199,21 @@ function useSelectedClasses(classes: ClassInfo[], imodel: IModelConnection, init
firstRender.current = false;
}, [classes]);

const onClassSelected = useCallback((info: ClassInfo) => {
setSelectedClasses((prevClasses) => [...prevClasses, info]);
}, []);

const onClassDeselected = useCallback((classInfo: ClassInfo) => {
setSelectedClasses((prevClasses) => prevClasses.filter((info) => info.id !== classInfo.id));
}, []);

const onClearClasses = useCallback(() => {
setSelectedClasses([]);
}, []);

const filterClassesByProperty = useCallback((property: InstanceFilterPropertyInfo) => {
setIsFilteringClasses(true);
void (async () => {
const newSelectedClasses = await computeClassesByProperty(selectedClasses.length === 0 ? classes : selectedClasses, property, imodel);
// istanbul ignore else
if (!disposedRef.current) {
setSelectedClasses(newSelectedClasses);
setIsFilteringClasses(false);
}
setSelectedClasses(newSelectedClasses);
setIsFilteringClasses(false);
})();
}, [selectedClasses, classes, imodel]);

return {
selectedClasses,
isFilteringClasses,
onClassSelected,
onClassDeselected,
onClearClasses,
onClassSelected: useCallback((info: ClassInfo) => { setSelectedClasses((prevClasses) => [...prevClasses, info]); }, []),
onClassDeselected: useCallback((classInfo: ClassInfo) => { setSelectedClasses((prevClasses) => prevClasses.filter((info) => info.id !== classInfo.id)); }, []),
onClearClasses: useCallback(() => { setSelectedClasses([]); }, []),
filterClassesByProperty,
};
}
Expand Down
Expand Up @@ -9,7 +9,8 @@ import "@itwin/itwinui-css/css/button.css";
import "@itwin/itwinui-css/css/menu.css";
import classnames from "classnames";
import Component, {
components, ControlProps, IndicatorProps, MenuProps, MultiValueProps, OptionProps, OptionTypeBase, Props, ValueContainerProps,
ClearIndicatorProps, components, ControlProps, DropdownIndicatorProps, MenuProps, MultiValueGenericProps, MultiValueProps, MultiValueRemoveProps,
OptionProps, Props, ValueContainerProps,
} from "react-select";
import { SvgCaretDown, SvgCheckmarkSmall, SvgCloseSmall } from "@itwin/itwinui-icons-react";
import { useResizeObserver } from "../common/Utils";
Expand All @@ -30,7 +31,8 @@ export function MultiTagSelect<Option>(props: Props<Option>) {
input: (style) => ({ ...style, order: -1, flex: 0 }),
valueContainer: (style) => ({ ...style, padding: 0, flexWrap: "nowrap" }),
indicatorsContainer: () => ({ marginLeft: "auto", display: "flex" }),
multiValue: (style) => ({ ...style, margin: 0 }),
multiValue: () => ({ margin: 0 }),
multiValueLabel: () => ({}),
}}
components={{
Control: TagSelectControl,
Expand All @@ -46,19 +48,19 @@ export function MultiTagSelect<Option>(props: Props<Option>) {
</div>);
}

function TagSelectControl<Option extends OptionTypeBase>({ children, ...props }: ControlProps<Option>) {
function TagSelectControl<TOption, IsMulti extends boolean = boolean>({ children, ...props }: ControlProps<TOption, IsMulti>) {
return <components.Control {...props} className="iui-select-button">
{children}
</components.Control>;
}

function TagSelectMenu<Option extends OptionTypeBase>({ children, ...props }: MenuProps<Option>) {
function TagSelectMenu<TOption, IsMulti extends boolean = boolean>({ children, ...props }: MenuProps<TOption, IsMulti>) {
return <components.Menu {...props} className="iui-menu">
{children}
</components.Menu>;
}

function TagSelectOption<Option extends OptionTypeBase>({ children: _, ...props }: OptionProps<Option>) {
function TagSelectOption<TOption, IsMulti extends boolean = boolean>({ children: _, ...props }: OptionProps<TOption, IsMulti>) {
const className = classnames("iui-menu-item", {
"iui-focused": props.isFocused,
"iui-active": props.isSelected,
Expand All @@ -70,13 +72,13 @@ function TagSelectOption<Option extends OptionTypeBase>({ children: _, ...props
</components.Option>;
}

function TagSelectValueContainer<Option extends OptionTypeBase>({ children, ...props }: ValueContainerProps<Option>) {
function TagSelectValueContainer<TOption, IsMulti extends boolean = boolean>({ children, ...props }: ValueContainerProps<TOption, IsMulti>) {
return <components.ValueContainer {...props} className="iui-tag-container">
{children}
</components.ValueContainer>;
}

function TagMultiValue<Option extends OptionTypeBase>({ children, ...props }: MultiValueProps<Option>) {
function TagMultiValue<TOption, IsMulti extends boolean = boolean>({ children, ...props }: MultiValueProps<TOption, IsMulti>) {
return <components.MultiValue
{...props}
components={{
Expand All @@ -89,33 +91,39 @@ function TagMultiValue<Option extends OptionTypeBase>({ children, ...props }: Mu
</components.MultiValue>;
}

function TagContainer({ children, ...props }: any) {
function TagContainer<TOption, IsMulti extends boolean = boolean>({ children, ...props }: MultiValueGenericProps<TOption, IsMulti>) {
return <components.MultiValueContainer {...props} innerProps={{ ...props.innerProps, className: "iui-tag" }}>
{children}
</components.MultiValueContainer>;
}

function TagLabel({ children, ...props }: any) {
function TagLabel<TOption, IsMulti extends boolean = boolean>({ children, ...props }: MultiValueGenericProps<TOption, IsMulti>) {
return <components.MultiValueLabel {...props} innerProps={{ ...props.innerProps, className: "iui-tag-label" }}>
{children}
</components.MultiValueLabel>;
}

function TagRemove(props: any) {
return <components.MultiValueRemove {...props} innerProps={{ ...props.innerProps, className: "iui-button iui-tag-button", ["data-iui-variant"]: "borderless", ["data-iui-size"]: "small" }}>
function TagRemove<TOption, IsMulti extends boolean = boolean>(props: MultiValueRemoveProps<TOption, IsMulti>) {
const innerProps = {
...props.innerProps,
className: "iui-button iui-tag-button",
["data-iui-variant"]: "borderless",
["data-iui-size"]: "small",
};
return <components.MultiValueRemove {...props} innerProps={innerProps}>
<SvgCloseSmall className="iui-button-icon" aria-hidden />
</components.MultiValueRemove>;
}

function TagSelectDropdownIndicator<Option extends OptionTypeBase>({ children: _, ...props }: IndicatorProps<Option>) {
function TagSelectDropdownIndicator<TOption, IsMulti extends boolean = boolean>({ children: _, ...props }: DropdownIndicatorProps<TOption, IsMulti>) {
return <components.DropdownIndicator {...props} >
<span data-testid="multi-tag-select-dropdownIndicator" className="iui-end-icon iui-actionable" style={{ padding: 0 }}>
<SvgCaretDown />
</span>
</components.DropdownIndicator>;
}

function TagSelectClearIndicator<Option extends OptionTypeBase>({ children: _, ...props }: IndicatorProps<Option>) {
function TagSelectClearIndicator<TOption, IsMulti extends boolean = boolean>({ children: _, ...props }: ClearIndicatorProps<TOption, IsMulti>) {
return <components.ClearIndicator {...props} >
<span data-testid="multi-tag-select-clearIndicator" className="iui-end-icon iui-actionable" style={{ padding: 0 }}>
<SvgCloseSmall aria-hidden />
Expand Down
Expand Up @@ -9,7 +9,7 @@ import "@itwin/itwinui-css/css/menu.css";
import classnames from "classnames";
import { Children, forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from "react";
import {
components, ControlProps, IndicatorContainerProps, IndicatorProps, MenuProps, OptionProps, OptionTypeBase, ValueContainerProps,
components, ControlProps, DropdownIndicatorProps, IndicatorsContainerProps, MenuProps, OptionProps, SingleValue, ValueContainerProps,
} from "react-select";
import { AsyncPaginate } from "react-select-async-paginate";
import { PropertyDescription, PropertyRecord, PropertyValue, PropertyValueFormat } from "@itwin/appui-abstract";
Expand Down Expand Up @@ -43,8 +43,9 @@ export const NavigationPropertyTargetSelector = forwardRef<NavigationPropertyTar

const [selectedTarget, setSelectedTarget] = useState(() => getNavigationTargetFromPropertyRecord(propertyRecord));

const onChange = useCallback((target?: NavigationPropertyTarget) => {
setSelectedTarget(target);
const onChange = useCallback((target: SingleValue<NavigationPropertyTarget>) => {
// istanbul ignore next
setSelectedTarget(target === null ? undefined : target);
target && onCommit && onCommit({ propertyRecord, newValue: getPropertyValue(target) });
}, [propertyRecord, onCommit]);

Expand Down Expand Up @@ -75,7 +76,7 @@ export const NavigationPropertyTargetSelector = forwardRef<NavigationPropertyTar
getOptionValue={(option: NavigationPropertyTarget) => option.key.id}
hideSelectedOptions={false}
debounceTimeout={500}
loadOptions={loadTargets}
loadOptions={async (inputValue, options) => loadTargets(inputValue, options.length)}
cacheUniqs={[loadTargets]}
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={setFocus}
Expand Down Expand Up @@ -127,25 +128,25 @@ function getNavigationTargetFromPropertyRecord(record: PropertyRecord): Navigati
return { key: value.value as InstanceKey, label: LabelDefinition.fromLabelString(value.displayValue) };
}

function TargetSelectControl<TOption extends OptionTypeBase>({ children, ...props }: ControlProps<TOption>) {
function TargetSelectControl<TOption, IsMulti extends boolean = boolean>({ children, ...props }: ControlProps<TOption, IsMulti>) {
return <components.Control {...props} className="iui-input-with-icon" >
{children}
</components.Control>;
}

function TargetSelectValueContainer<TOption extends OptionTypeBase>({ children, ...props }: ValueContainerProps<TOption>) {
function TargetSelectValueContainer<TOption, IsMulti extends boolean = boolean>({ children, ...props }: ValueContainerProps<TOption, IsMulti>) {
return <components.ValueContainer {...props} className="iui-select-button">
{children}
</components.ValueContainer>;
}

function TargetSelectMenu<TOption extends OptionTypeBase>({ children, ...props }: MenuProps<TOption>) {
function TargetSelectMenu<TOption, IsMulti extends boolean = boolean>({ children, ...props }: MenuProps<TOption, IsMulti>) {
return <components.Menu {...props} className="iui-menu" >
{children}
</components.Menu>;
}

function TargetSelectOption<TOption extends OptionTypeBase>({ children: _, ...props }: OptionProps<TOption>) {
function TargetSelectOption<TOption, IsMulti extends boolean = boolean>({ children: _, ...props }: OptionProps<TOption, IsMulti>) {
const className = classnames("iui-menu-item", {
"iui-focused": props.isFocused,
"iui-active": props.isSelected,
Expand All @@ -156,11 +157,11 @@ function TargetSelectOption<TOption extends OptionTypeBase>({ children: _, ...pr
</components.Option>;
}

function TargetSelectIndicatorsContainer<TOption extends OptionTypeBase>({ children }: IndicatorContainerProps<TOption>) {
return Children.toArray(children).pop();
function TargetSelectIndicatorsContainer<TOption, IsMulti extends boolean = boolean>({ children }: IndicatorsContainerProps<TOption, IsMulti>) {
return <>{Children.toArray(children).pop()}</>;
}

function TargetSelectDropdownIndicator<TOption extends OptionTypeBase>(props: IndicatorProps<TOption>) {
function TargetSelectDropdownIndicator<TOption, IsMulti extends boolean = boolean>(props: DropdownIndicatorProps<TOption, IsMulti>) {
return <components.DropdownIndicator {...props} className="iui-end-icon iui-actionable" >
<SvgCaretDownSmall />
</components.DropdownIndicator>;
Expand Down
Expand Up @@ -35,7 +35,7 @@ export interface UseNavigationPropertyTargetsLoaderProps {
/** @internal */
export function useNavigationPropertyTargetsLoader(props: UseNavigationPropertyTargetsLoaderProps) {
const { imodel, ruleset } = props;
const loadTargets = useCallback(async (filter: string, loadedOptions: NavigationPropertyTarget[]): Promise<NavigationPropertyTargetsResult> => {
const loadTargets = useCallback(async (filter: string, loadedOptionsCount: number): Promise<NavigationPropertyTargetsResult> => {
if (!ruleset) {
return { options: [], hasMore: false };
}
Expand All @@ -48,7 +48,7 @@ export function useNavigationPropertyTargetsLoader(props: UseNavigationPropertyT
contentFlags: ContentFlags.ShowLabels | ContentFlags.NoFields,
fieldsFilterExpression: filter ? `/DisplayLabel/ ~ \"%${filter}%\"` : undefined,
},
paging: { start: loadedOptions.length, size: NAVIGATION_PROPERTY_TARGETS_BATCH_SIZE },
paging: { start: loadedOptionsCount, size: NAVIGATION_PROPERTY_TARGETS_BATCH_SIZE },
});

return {
Expand Down

0 comments on commit d46358e

Please sign in to comment.