Skip to content

Commit afccefb

Browse files
authored
Adjusted use of BaseComponent and always forwarding ref to it (#867)
1 parent 008be37 commit afccefb

File tree

19 files changed

+502
-452
lines changed

19 files changed

+502
-452
lines changed

.vscode/launch.json

+27-27
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,31 @@
11
{
2-
"configurations": [
3-
{
4-
"name": "Python: Remote Attach",
5-
"type": "python",
6-
"request": "attach",
7-
"connect": { "host": "localhost", "port": 5678 },
8-
"pathMappings": [
2+
"configurations": [
93
{
10-
"localRoot": "${workspaceFolder}/backend_py/primary",
11-
"remoteRoot": "/home/appuser/backend_py/primary"
4+
"name": "Python: Remote Attach",
5+
"type": "python",
6+
"request": "attach",
7+
"connect": { "host": "localhost", "port": 5678 },
8+
"pathMappings": [
9+
{
10+
"localRoot": "${workspaceFolder}/backend_py/primary",
11+
"remoteRoot": "/home/appuser/backend_py/primary"
12+
}
13+
]
14+
},
15+
{
16+
"name": "TS: Launch Chrome and Attach",
17+
"request": "launch",
18+
"type": "chrome",
19+
"webRoot": "${workspaceFolder}/frontend",
20+
"url": "http://localhost:8080"
21+
},
22+
{
23+
"name": "Run script",
24+
"type": "node",
25+
"cwd": "${workspaceFolder}/frontend",
26+
"request": "launch",
27+
"program": "${workspaceFolder}/frontend/scripts/add-api-suffix.cjs",
28+
"args": ["--suffix", "'__api'", "--dir", "'src/api/'"]
1229
}
13-
]
14-
},
15-
{
16-
"name": "TS: Launch Chrome and Attach",
17-
"request": "launch",
18-
"type": "chrome",
19-
"webRoot": "${workspaceFolder}/frontend",
20-
"url": "http://localhost:8080"
21-
},
22-
{
23-
"name": "Run script",
24-
"type": "node",
25-
"cwd": "${workspaceFolder}/frontend",
26-
"request": "launch",
27-
"program": "${workspaceFolder}/frontend/scripts/add-api-suffix.cjs",
28-
"args": ["--suffix", "'__api'", "--dir", "'src/api/'"]
29-
}
30-
]
30+
]
3131
}

frontend/src/framework/components/RealizationPicker/realizationPicker.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ export type RealizationPickerProps = {
197197
onChange?: (realizationPickerSelection: RealizationPickerSelection) => void;
198198
} & BaseComponentProps;
199199

200-
export const RealizationPicker: React.FC<RealizationPickerProps> = (props) => {
200+
function RealizationPickerComponent(props: RealizationPickerProps, ref: React.ForwardedRef<HTMLDivElement>) {
201201
const [selections, setSelections] = React.useState<Selection[]>(
202202
props.initialRangeTags
203203
? [...props.initialRangeTags].map((rangeTag) => {
@@ -410,7 +410,7 @@ export const RealizationPicker: React.FC<RealizationPickerProps> = (props) => {
410410
const numSelectedRealizations = calcUniqueSelections(selections, props.validRealizations).length;
411411

412412
return (
413-
<BaseComponent disabled={props.disabled}>
413+
<BaseComponent ref={ref} disabled={props.disabled}>
414414
<div className="relative border border-gray-300 rounded p-2 pr-6 min-h-[3rem]">
415415
<ul className="flex flex-wrap items-center cursor-text gap-1 h-full" onPointerDown={handlePointerDown}>
416416
{selections.map((selection) => (
@@ -450,6 +450,6 @@ export const RealizationPicker: React.FC<RealizationPickerProps> = (props) => {
450450
</div>
451451
</BaseComponent>
452452
);
453-
};
453+
}
454454

455-
RealizationPicker.displayName = "RealizationPicker";
455+
export const RealizationPicker = React.forwardRef(RealizationPickerComponent);

frontend/src/lib/components/BaseComponent/baseComponent.tsx

+11-5
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,23 @@ import { resolveClassNames } from "@lib/utils/resolveClassNames";
55
export type BaseComponentProps = {
66
disabled?: boolean;
77
children?: React.ReactNode;
8+
className?: string;
9+
style?: React.CSSProperties;
810
};
911

1012
export const BaseComponent = React.forwardRef((props: BaseComponentProps, ref: React.ForwardedRef<HTMLDivElement>) => {
1113
return (
1214
<div
1315
ref={ref}
14-
className={resolveClassNames({
15-
"opacity-50": props.disabled,
16-
"pointer-events-none": props.disabled,
17-
"cursor-default": props.disabled,
18-
})}
16+
className={resolveClassNames(
17+
{
18+
"opacity-50": props.disabled,
19+
"pointer-events-none": props.disabled,
20+
"cursor-default": props.disabled,
21+
},
22+
props.className ?? ""
23+
)}
24+
style={props.style}
1925
>
2026
{props.children}
2127
</div>

frontend/src/lib/components/Button/button.tsx

+11-6
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,15 @@ export type ButtonProps = {
1111
endIcon?: React.ReactNode;
1212
color?: "primary" | "danger" | "success" | "secondary";
1313
size?: "small" | "medium" | "large";
14+
buttonRef?: React.Ref<HTMLButtonElement>;
1415
} & ButtonUnstyledProps;
1516

16-
export const Button = React.forwardRef((props: ButtonProps, ref: React.ForwardedRef<HTMLButtonElement>) => {
17-
const { disabled, variant, children, startIcon, endIcon, color, ...rest } = props;
17+
function ButtonComponent(props: ButtonProps, ref: React.ForwardedRef<HTMLDivElement>) {
18+
const { disabled, variant, children, startIcon, endIcon, color, buttonRef, ...rest } = props;
19+
20+
const internalRef = React.useRef<HTMLButtonElement>(null);
21+
React.useImperativeHandle<HTMLButtonElement | null, HTMLButtonElement | null>(buttonRef, () => internalRef.current);
22+
1823
const classNames = [
1924
"inline-flex",
2025
"items-center",
@@ -73,10 +78,10 @@ export const Button = React.forwardRef((props: ButtonProps, ref: React.Forwarded
7378
);
7479

7580
return (
76-
<BaseComponent disabled={disabled}>
81+
<BaseComponent disabled={disabled} ref={ref}>
7782
<ButtonUnstyled
7883
{...rest}
79-
ref={ref}
84+
ref={buttonRef}
8085
slotProps={{
8186
root: {
8287
className: resolveClassNames(...classNames),
@@ -87,6 +92,6 @@ export const Button = React.forwardRef((props: ButtonProps, ref: React.Forwarded
8792
</ButtonUnstyled>
8893
</BaseComponent>
8994
);
90-
});
95+
}
9196

92-
Button.displayName = "Button";
97+
export const Button = React.forwardRef(ButtonComponent);

frontend/src/lib/components/Checkbox/checkbox.tsx

+28-30
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export type CheckboxProps = {
1515
onChange?: (event: React.ChangeEvent<HTMLInputElement>, checked: boolean) => void;
1616
} & BaseComponentProps;
1717

18-
export const Checkbox: React.FC<CheckboxProps> = (props) => {
18+
function CheckboxComponent(props: CheckboxProps, ref: React.ForwardedRef<HTMLDivElement>) {
1919
const { onChange } = props;
2020

2121
const [checked, setChecked] = React.useState<boolean>(props.checked ?? false);
@@ -36,36 +36,34 @@ export const Checkbox: React.FC<CheckboxProps> = (props) => {
3636
);
3737

3838
return (
39-
<BaseComponent disabled={props.disabled}>
40-
<div className="flex gap-2 items-center">
41-
<input
42-
id={props.id ?? id.current}
43-
name={props.name}
44-
ref={(el) => el && (el.indeterminate = props.indeterminate ?? false)}
45-
type="checkbox"
46-
checked={checked}
47-
onChange={handleChange}
48-
className={resolveClassNames(
49-
"w-4",
50-
"h-4",
51-
"text-blue-600",
52-
"border-gray-300",
53-
"rounded",
54-
"focus:ring-blue-500",
55-
"cursor-pointer"
56-
)}
57-
/>
58-
{props.label && (
59-
<label
60-
htmlFor={props.id ?? id.current}
61-
className={resolveClassNames("block", "text-gray-900", "cursor-pointer")}
62-
>
63-
{props.label}
64-
</label>
39+
<BaseComponent ref={ref} disabled={props.disabled} className="flex gap-2 items-center">
40+
<input
41+
id={props.id ?? id.current}
42+
name={props.name}
43+
ref={(el) => el && (el.indeterminate = props.indeterminate ?? false)}
44+
type="checkbox"
45+
checked={checked}
46+
onChange={handleChange}
47+
className={resolveClassNames(
48+
"w-4",
49+
"h-4",
50+
"text-blue-600",
51+
"border-gray-300",
52+
"rounded",
53+
"focus:ring-blue-500",
54+
"cursor-pointer"
6555
)}
66-
</div>
56+
/>
57+
{props.label && (
58+
<label
59+
htmlFor={props.id ?? id.current}
60+
className={resolveClassNames("block", "text-gray-900", "cursor-pointer")}
61+
>
62+
{props.label}
63+
</label>
64+
)}
6765
</BaseComponent>
6866
);
69-
};
67+
}
7068

71-
Checkbox.displayName = "Checkbox";
69+
export const Checkbox = React.forwardRef(CheckboxComponent);

frontend/src/lib/components/CollapsibleGroup/collapsibleGroup.tsx

+22-24
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export type CollapsibleGroupProps = {
1313
onChange?: (expanded: boolean) => void;
1414
} & BaseComponentProps;
1515

16-
export const CollapsibleGroup: React.FC<CollapsibleGroupProps> = (props) => {
16+
function CollapsibleGroupComponent(props: CollapsibleGroupProps, ref: React.ForwardedRef<HTMLDivElement>) {
1717
const [expanded, setExpanded] = React.useState(props.expanded ?? false);
1818
const [prevExpanded, setPrevExpanded] = React.useState<boolean | undefined>(props.expanded);
1919

@@ -28,30 +28,28 @@ export const CollapsibleGroup: React.FC<CollapsibleGroupProps> = (props) => {
2828
};
2929

3030
return (
31-
<BaseComponent disabled={props.disabled}>
32-
<div className="shadow">
33-
<div
34-
className={resolveClassNames(
35-
"flex flex-row justify-between items-center bg-slate-100 cursor-pointer p-2 select-none gap-2",
36-
{ "border-b": expanded }
37-
)}
38-
onClick={handleClick}
39-
title={expanded ? "Collapse" : "Expand"}
40-
>
41-
{props.icon && React.cloneElement(props.icon, { className: "w-4 h-4" })}
42-
<h3 className="text-sm font-semibold flex-grow leading-none">{props.title}</h3>
43-
{expanded ? <ExpandLess fontSize="small" /> : <ExpandMore fontSize="small" />}
44-
</div>
45-
<div
46-
className={resolveClassNames("p-2", {
47-
hidden: !expanded,
48-
})}
49-
>
50-
{props.children}
51-
</div>
31+
<BaseComponent ref={ref} disabled={props.disabled} className="shadow">
32+
<div
33+
className={resolveClassNames(
34+
"flex flex-row justify-between items-center bg-slate-100 cursor-pointer p-2 select-none gap-2",
35+
{ "border-b": expanded }
36+
)}
37+
onClick={handleClick}
38+
title={expanded ? "Collapse" : "Expand"}
39+
>
40+
{props.icon && React.cloneElement(props.icon, { className: "w-4 h-4" })}
41+
<h3 className="text-sm font-semibold flex-grow leading-none">{props.title}</h3>
42+
{expanded ? <ExpandLess fontSize="small" /> : <ExpandMore fontSize="small" />}
43+
</div>
44+
<div
45+
className={resolveClassNames("p-2", {
46+
hidden: !expanded,
47+
})}
48+
>
49+
{props.children}
5250
</div>
5351
</BaseComponent>
5452
);
55-
};
53+
}
5654

57-
CollapsibleGroup.displayName = "CollapsibleGroup";
55+
export const CollapsibleGroup = React.forwardRef(CollapsibleGroupComponent);

frontend/src/lib/components/Dropdown/dropdown.tsx

+7-5
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ function makeOptionListItems<TValue>(options: DropdownOption<TValue>[]): OptionL
7878
return optionsWithSeperators;
7979
}
8080

81-
export function Dropdown<TValue = string>(props: DropdownProps<TValue>) {
81+
function DropdownComponent<TValue = string>(props: DropdownProps<TValue>, ref: React.ForwardedRef<HTMLDivElement>) {
8282
const { onChange } = props;
8383

8484
const valueWithDefault = props.value ?? null;
@@ -103,7 +103,7 @@ export function Dropdown<TValue = string>(props: DropdownProps<TValue>) {
103103
const [startIndex, setStartIndex] = React.useState<number>(0);
104104
const [keyboardFocus, setKeyboardFocus] = React.useState<boolean>(false);
105105

106-
const inputRef = React.useRef<HTMLInputElement>(null);
106+
const inputRef = React.useRef<HTMLDivElement>(null);
107107
const dropdownRef = React.useRef<HTMLDivElement>(null);
108108
const debounceTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
109109

@@ -482,7 +482,7 @@ export function Dropdown<TValue = string>(props: DropdownProps<TValue>) {
482482
}
483483

484484
return (
485-
<BaseComponent disabled={props.disabled}>
485+
<BaseComponent ref={ref} disabled={props.disabled}>
486486
<div style={{ width: props.width }} id={props.wrapperId} className="flex hover input-comp rounded">
487487
<div className="flex-grow">
488488
<Input
@@ -569,6 +569,10 @@ export function Dropdown<TValue = string>(props: DropdownProps<TValue>) {
569569
);
570570
}
571571

572+
export const Dropdown = React.forwardRef(DropdownComponent) as <TValue = string>(
573+
props: DropdownProps<TValue> & { ref?: React.Ref<HTMLDivElement> }
574+
) => React.ReactElement;
575+
572576
type OptionProps<TValue> = DropdownOption<TValue> & {
573577
isSelected: boolean;
574578
isFocused: boolean;
@@ -613,5 +617,3 @@ function SeparatorItem(props: { text: string }): React.ReactNode {
613617
</div>
614618
);
615619
}
616-
617-
Dropdown.displayName = "Dropdown";

frontend/src/lib/components/HoldPressedIntervalCallbackButton/holdPressedIntervalCallbackButton.tsx

+8-2
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@ import React from "react";
22

33
import { Button, ButtonProps } from "../Button/button";
44

5-
export type HoldPressedIntervalCallbackButtonProps = ButtonProps & {
5+
export type HoldPressedIntervalCallbackButtonProps = Omit<ButtonProps, "ref"> & {
66
onHoldPressedIntervalCallback: () => void;
77
};
88

9-
export function HoldPressedIntervalCallbackButton(props: HoldPressedIntervalCallbackButtonProps): React.ReactNode {
9+
function HoldPressedIntervalCallbackButtonComponent(
10+
props: HoldPressedIntervalCallbackButtonProps,
11+
ref: React.ForwardedRef<HTMLDivElement>
12+
): React.ReactNode {
1013
const { onHoldPressedIntervalCallback, ...other } = props;
1114

1215
const timeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
@@ -39,10 +42,13 @@ export function HoldPressedIntervalCallbackButton(props: HoldPressedIntervalCall
3942
return (
4043
<Button
4144
{...other}
45+
ref={ref}
4246
onClick={handleClick}
4347
onPointerDown={handlePointerDown}
4448
onPointerUp={handlePointerUp}
4549
onPointerLeave={handlePointerUp}
4650
/>
4751
);
4852
}
53+
54+
export const HoldPressedIntervalCallbackButton = React.forwardRef(HoldPressedIntervalCallbackButtonComponent);

0 commit comments

Comments
 (0)