Skip to content

Commit 4e50dcb

Browse files
committed
added button click handler ref, to click button from outside
1 parent a979d62 commit 4e50dcb

File tree

6 files changed

+86
-24
lines changed

6 files changed

+86
-24
lines changed

src/Annotator/index.tsx

+19-5
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ import {
55
MainLayoutState,
66
RegionAllowedActions,
77
} from "../MainLayout/types.ts";
8-
import { ComponentType, FunctionComponent, useEffect, useReducer } from "react";
8+
import { ComponentType, forwardRef, FunctionComponent, useEffect, useImperativeHandle, useReducer, useRef } from "react";
99

1010
import type { KeypointsDefinition } from "../types/region-tools.ts";
11-
import MainLayout from "../MainLayout/index.tsx";
11+
import MainLayout, { MainLayoutRef } from "../MainLayout/index.tsx";
1212
import SettingsProvider from "../SettingsProvider/index.tsx";
1313
import combineReducers from "./reducers/combine-reducers.ts";
1414
import generalReducer from "./reducers/general-reducer.ts";
@@ -54,7 +54,11 @@ export type AnnotatorProps = {
5454
onPrevImage?: (state: MainLayoutState) => void;
5555
};
5656

57-
export const Annotator = ({
57+
export interface AnnotatorRef {
58+
clickHeaderButton: (name: string) => void;
59+
}
60+
61+
export const Annotator = forwardRef<AnnotatorRef, AnnotatorProps>(({
5862
images,
5963
allowedArea,
6064
selectedImage = images && images.length > 0 ? 0 : undefined,
@@ -98,14 +102,23 @@ export const Annotator = ({
98102
hideFullScreen,
99103
hideSave,
100104
allowComments,
101-
}: AnnotatorProps) => {
105+
}, ref) => {
102106
if (typeof selectedImage === "string") {
103107
selectedImage = (images || []).findIndex(
104108
(img) => img.name === selectedImage
105109
);
106110

107111
if (selectedImage === -1) selectedImage = undefined;
108112
}
113+
114+
const mainLayoutRef = useRef<MainLayoutRef>(null);
115+
116+
useImperativeHandle(ref, () => ({
117+
clickHeaderButton(name: string) {
118+
mainLayoutRef.current?.clickHeaderButton(name);
119+
},
120+
}));
121+
109122
const combinedReducers = combineReducers(imageReducer, generalReducer) as (
110123
state: MainLayoutState,
111124
action: Action
@@ -186,6 +199,7 @@ export const Annotator = ({
186199
return (
187200
<SettingsProvider>
188201
<MainLayout
202+
ref={mainLayoutRef}
189203
RegionEditLabel={RegionEditLabel}
190204
alwaysShowNextButton={Boolean(onNextImage)}
191205
alwaysShowPrevButton={Boolean(onPrevImage)}
@@ -203,6 +217,6 @@ export const Annotator = ({
203217
/>
204218
</SettingsProvider>
205219
);
206-
};
220+
});
207221

208222
export default Annotator;

src/MainLayout/index.tsx

+20-4
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import { Action, AnnotatorToolEnum, MainLayoutState } from "./types.ts";
44
import { FullScreen, useFullScreenHandle } from "react-full-screen";
55
import {
66
ComponentType,
7+
forwardRef,
78
FunctionComponent,
89
MouseEvent,
910
MouseEventHandler,
1011
ReactElement,
1112
useCallback,
13+
useImperativeHandle,
1214
useMemo,
1315
useRef,
1416
} from "react";
@@ -32,7 +34,7 @@ import { HotKeys } from "react-hotkeys";
3234
import { grey } from "@mui/material/colors";
3335
import { notEmpty } from "../utils/not-empty.ts";
3436
import { ALL_TOOLS } from "./all-tools-list.ts";
35-
import Workspace from "../workspace/Workspace/index.tsx";
37+
import Workspace, { WorkspaceRef } from "../workspace/Workspace/index.tsx";
3638
import { tss } from "tss-react/mui";
3739
import { RegionLabelProps } from "../RegionLabel/index.tsx";
3840
import SettingsDialog from "../SettingsDialog/index.tsx";
@@ -94,7 +96,11 @@ type Props = {
9496
hideSave?: boolean;
9597
};
9698

97-
export const MainLayout = ({
99+
export interface MainLayoutRef {
100+
clickHeaderButton: (name: string) => void;
101+
}
102+
103+
export const MainLayout = forwardRef<MainLayoutRef, Props>(({
98104
state,
99105
dispatch,
100106
RegionEditLabel,
@@ -107,14 +113,23 @@ export const MainLayout = ({
107113
hideSettings = false,
108114
hideFullScreen = false,
109115
hideSave = false,
110-
}: Props) => {
116+
}, ref) => {
111117
const { classes } = useStyles();
112118
const settings = useSettings();
113119
const fullScreenHandle = useFullScreenHandle();
114120

115121
const memoizedActionFns = useRef<Record<string, (...args: any[]) => void>>(
116122
{}
117123
);
124+
125+
const workSpaceRef = useRef<WorkspaceRef>(null);
126+
127+
useImperativeHandle(ref, () => ({
128+
clickHeaderButton(name: string) {
129+
workSpaceRef.current?.clickHeaderButton(name);
130+
},
131+
}));
132+
118133
const action = (type: Action["type"], ...params: Array<any>) => {
119134
const fnKey = `${type}(${params.join(",")})`;
120135
if (memoizedActionFns.current[fnKey])
@@ -366,6 +381,7 @@ export const MainLayout = ({
366381
)}
367382
>
368383
<Workspace
384+
ref={workSpaceRef}
369385
allowFullscreen
370386
iconDictionary={iconDictionary}
371387
hideHeader={hideHeader}
@@ -400,6 +416,6 @@ export const MainLayout = ({
400416
</FullScreenContainer>
401417
</ThemeProvider>
402418
);
403-
};
419+
});
404420

405421
export default MainLayout;

src/lib.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Annotator from "./Annotator";
22

33
// re-export types
4-
export type { AnnotatorProps } from "./Annotator";
4+
export type { AnnotatorProps, AnnotatorRef } from "./Annotator";
55
export type {
66
MainLayoutState,
77
MainLayoutImageAnnotationState,

src/workspace/Header/index.tsx

+24-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import HeaderButton from "../HeaderButton/index.js";
22
import Box from "@mui/material/Box";
33
import { createTheme, styled, ThemeProvider } from "@mui/material/styles";
4-
import { ReactNode } from "react";
4+
import { forwardRef, ReactNode, useImperativeHandle, useRef } from "react";
55

66
const theme = createTheme();
77

@@ -22,19 +22,39 @@ interface HeaderProps {
2222
onClickItem: (item: { name: string }) => void;
2323
}
2424

25-
export const Header = ({
25+
export type HeaderRef = {
26+
clickButtonByName: (name: string) => void;
27+
};
28+
29+
export const Header = forwardRef<HeaderRef, HeaderProps>(({
2630
leftSideContent = null,
2731
hideHeaderText = false,
2832
items,
2933
onClickItem,
30-
}: HeaderProps) => {
34+
}, ref) => {
35+
const buttonRefs = useRef<Record<string, HTMLButtonElement | null>>({});
36+
37+
useImperativeHandle(ref, () => ({
38+
clickButtonByName(name: string) {
39+
const button = buttonRefs.current[name];
40+
if (button) {
41+
button.click();
42+
} else {
43+
console.warn(`No button found with name: ${name}`);
44+
}
45+
},
46+
}));
47+
3148
return (
3249
<ThemeProvider theme={theme}>
3350
<Container>
3451
<Box flexGrow={1}>{leftSideContent}</Box>
3552
{items.map((item, index) => (
3653
<HeaderButton
3754
key={`${item.name}-${index}`}
55+
ref={(el: HTMLButtonElement | null) => {
56+
buttonRefs.current[item.name] = el;
57+
}}
3858
hideText={hideHeaderText}
3959
onClick={() => onClickItem(item)}
4060
{...item}
@@ -43,6 +63,6 @@ export const Header = ({
4363
</Container>
4464
</ThemeProvider>
4565
);
46-
};
66+
});
4767

4868
export default Header;

src/workspace/HeaderButton/index.tsx

+5-5
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { useIconDictionary } from "../icon-dictionary.ts";
66
import { iconMapping } from "../icon-mapping.ts";
77
import { colors, SvgIconTypeMap } from "@mui/material";
88
import { OverridableComponent } from "@mui/material/OverridableComponent";
9-
import { ReactNode } from "react";
9+
import { forwardRef, ReactNode } from "react";
1010

1111
const theme = createTheme();
1212
const defaultNameIconMapping = iconMapping;
@@ -63,17 +63,17 @@ interface HeaderButtonProps {
6363
hideText?: boolean;
6464
}
6565

66-
export const HeaderButton = ({
66+
export const HeaderButton = forwardRef<HTMLButtonElement, HeaderButtonProps>(({
6767
name,
6868
icon,
6969
disabled,
7070
onClick,
7171
hideText = false,
72-
}: HeaderButtonProps) => {
72+
}, ref) => {
7373
const customIconMapping = useIconDictionary();
7474
return (
7575
<ThemeProvider key={name} theme={theme}>
76-
<StyledButton onClick={onClick} disabled={disabled}>
76+
<StyledButton onClick={onClick} disabled={disabled} ref={ref}>
7777
<ButtonInnerContent>
7878
<IconContainer textHidden={hideText}>
7979
{icon || getIcon(name, customIconMapping)}
@@ -87,6 +87,6 @@ export const HeaderButton = ({
8787
</StyledButton>
8888
</ThemeProvider>
8989
);
90-
};
90+
});
9191

9292
export default HeaderButton;

src/workspace/Workspace/index.tsx

+17-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { CSSProperties, ReactElement } from "react";
1+
import { CSSProperties, forwardRef, ReactElement, useImperativeHandle, useRef } from "react";
22
import { createTheme, styled, ThemeProvider } from "@mui/material/styles";
3-
import Header from "../Header/index.tsx";
3+
import Header, { HeaderRef } from "../Header/index.tsx";
44
import RightSidebar from "../RightSidebar/index.tsx";
55
import WorkContainer from "../WorkContainer/index.tsx";
66
import { IconDictionaryContext } from "../icon-dictionary.ts";
@@ -29,6 +29,10 @@ const SidebarsAndContent = styled("div")(() => ({
2929
maxWidth: "100vw",
3030
}));
3131

32+
export interface WorkspaceRef {
33+
clickHeaderButton: (name: string) => void;
34+
}
35+
3236
export interface WorkspaceProps {
3337
style?: CSSProperties;
3438
allowFullscreen?: boolean;
@@ -50,7 +54,7 @@ export interface WorkspaceProps {
5054
children: ReactElement;
5155
}
5256

53-
export const Workspace = ({
57+
export const Workspace = forwardRef<WorkspaceRef, WorkspaceProps>(({
5458
style = {},
5559
iconSidebarItems = [],
5660
selectedTools = ["select"],
@@ -64,15 +68,23 @@ export const Workspace = ({
6468
hideHeader = false,
6569
hideHeaderText = false,
6670
children,
67-
}: WorkspaceProps) => {
71+
}, ref) => {
6872
const [sidebarAndContentRef, sidebarAndContent] =
6973
useMeasure<HTMLDivElement>();
74+
const headerRef = useRef<HeaderRef>(null);
75+
76+
useImperativeHandle(ref, () => ({
77+
clickHeaderButton(name: string) {
78+
headerRef.current?.clickButtonByName(name);
79+
},
80+
}));
7081
return (
7182
<ThemeProvider theme={theme}>
7283
<IconDictionaryContext.Provider value={iconDictionary}>
7384
<Container style={style}>
7485
{!hideHeader && (
7586
<Header
87+
ref={headerRef}
7688
hideHeaderText={hideHeaderText}
7789
leftSideContent={headerLeftSide}
7890
onClickItem={onClickHeaderItem}
@@ -101,6 +113,6 @@ export const Workspace = ({
101113
</IconDictionaryContext.Provider>
102114
</ThemeProvider>
103115
);
104-
};
116+
});
105117

106118
export default Workspace;

0 commit comments

Comments
 (0)