Skip to content

Commit de82dff

Browse files
committed
expose closeDrawer func to ApplicationDrawer
1 parent ba8f57e commit de82dff

File tree

8 files changed

+222
-121
lines changed

8 files changed

+222
-121
lines changed

workspaces/lightspeed/packages/app/src/components/Root/ApplicationDrawer.tsx

Lines changed: 78 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
useCallback,
2121
useMemo,
2222
useEffect,
23+
useRef,
2324
} from 'react';
2425
import { CustomDrawer } from './CustomDrawer';
2526

@@ -33,6 +34,7 @@ export interface DrawerPartialState {
3334
isDrawerOpen: boolean;
3435
drawerWidth: number;
3536
setDrawerWidth: (width: number) => void;
37+
closeDrawer: () => void;
3638
}
3739

3840
/**
@@ -69,12 +71,12 @@ export interface ApplicationDrawerProps {
6971
drawerContents: DrawerContentType[];
7072
/**
7173
* Array of state exposer components from drawer plugins
72-
* These are typically mounted via `application/drawer-state` mount point
74+
* These are typically mounted via `application/internal/drawer-state` mount point
7375
*
7476
* In RHDH dynamic plugins, this would come from:
7577
* ```yaml
7678
* mountPoints:
77-
* - mountPoint: application/drawer-state
79+
* - mountPoint: application/internal/drawer-state
7880
* importName: TestDrawerStateExposer
7981
* ```
8082
*/
@@ -85,55 +87,85 @@ export const ApplicationDrawer = ({
8587
drawerContents,
8688
stateExposers = [],
8789
}: ApplicationDrawerProps) => {
88-
// Collect drawer states from all state exposers
89-
const [drawerStates, setDrawerStates] = useState<
90-
Record<string, DrawerPartialState>
91-
>({});
92-
93-
// Callback for state exposers to report their state
94-
const handleStateChange = useCallback((state: DrawerPartialState) => {
95-
setDrawerStates(prev => {
96-
// Only update if something actually changed
97-
const existing = prev[state.id];
98-
if (
99-
existing &&
100-
existing.isDrawerOpen === state.isDrawerOpen &&
101-
existing.drawerWidth === state.drawerWidth &&
102-
existing.setDrawerWidth === state.setDrawerWidth
90+
const drawerStatesRef = useRef<Map<string, DrawerPartialState>>(new Map());
91+
const [, forceUpdate] = useState({});
92+
const [activeDrawerId, setActiveDrawerId] = useState<string | null>(null);
93+
94+
const handleStateChange = useCallback(
95+
(state: DrawerPartialState) => {
96+
const prev = drawerStatesRef.current.get(state.id);
97+
const hasChanged =
98+
!prev ||
99+
prev.isDrawerOpen !== state.isDrawerOpen ||
100+
prev.drawerWidth !== state.drawerWidth ||
101+
prev.setDrawerWidth !== state.setDrawerWidth;
102+
103+
// If drawer just opened then make it the active drawer
104+
if (!prev?.isDrawerOpen && state.isDrawerOpen) {
105+
setActiveDrawerId(state.id);
106+
}
107+
// If drawer just closed and it was the active one, clear active drawer
108+
else if (
109+
prev?.isDrawerOpen &&
110+
!state.isDrawerOpen &&
111+
state.id === activeDrawerId
103112
) {
104-
return prev;
113+
setActiveDrawerId(null);
114+
}
115+
116+
drawerStatesRef.current.set(state.id, state);
117+
118+
if (hasChanged) {
119+
forceUpdate({});
105120
}
106-
return { ...prev, [state.id]: state };
107-
});
108-
}, []);
109-
110-
// Convert states record to array
111-
const statesArray = useMemo(
112-
() => Object.values(drawerStates),
113-
[drawerStates],
121+
},
122+
[activeDrawerId],
123+
);
124+
125+
const drawerStates = Array.from(drawerStatesRef.current.values());
126+
127+
const allDrawers = useMemo(
128+
() =>
129+
drawerStates
130+
.map(state => {
131+
const content = drawerContents.find(c => c.id === state.id);
132+
if (!content) return null;
133+
134+
return {
135+
state,
136+
Component: content.Component,
137+
priority: content.priority,
138+
};
139+
})
140+
.filter(Boolean),
141+
[drawerStates, drawerContents],
114142
);
115143

116-
// Get active drawer - find the open drawer with highest priority
117-
const activeDrawer = useMemo(() => {
118-
return statesArray
119-
.filter(state => state.isDrawerOpen)
120-
.map(state => {
121-
const content = drawerContents.find(c => c.id === state.id);
122-
if (!content) return null;
123-
return { ...state, ...content };
124-
})
125-
.filter(Boolean)
126-
.sort((a, b) => (b?.priority ?? -1) - (a?.priority ?? -1))[0];
127-
}, [statesArray, drawerContents]);
144+
const activeDrawer =
145+
allDrawers.find(d => d?.state.id === activeDrawerId) || null;
146+
147+
// Close all other drawers when one becomes active
148+
useEffect(() => {
149+
if (activeDrawerId) {
150+
drawerStates.forEach(state => {
151+
if (state.id !== activeDrawerId && state.isDrawerOpen) {
152+
state.closeDrawer();
153+
}
154+
});
155+
}
156+
}, [activeDrawerId, drawerStates]);
128157

129158
// Manage CSS classes and variables for layout adjustments
130159
useEffect(() => {
131160
if (activeDrawer) {
132-
const className = `docked-drawer-open`;
133-
const cssVar = `--docked-drawer-width`;
161+
const className = 'docked-drawer-open';
162+
const cssVar = '--docked-drawer-width';
134163

135164
document.body.classList.add(className);
136-
document.body.style.setProperty(cssVar, `${activeDrawer.drawerWidth}px`);
165+
document.body.style.setProperty(
166+
cssVar,
167+
`${activeDrawer.state.drawerWidth}px`,
168+
);
137169

138170
return () => {
139171
document.body.classList.remove(className);
@@ -142,32 +174,24 @@ export const ApplicationDrawer = ({
142174
}
143175
return undefined;
144176
// eslint-disable-next-line react-hooks/exhaustive-deps
145-
}, [activeDrawer?.id, activeDrawer?.drawerWidth]);
146-
147-
// Wrapper to handle the width change callback type
148-
const handleWidthChange = useCallback(
149-
(width: number) => {
150-
activeDrawer?.setDrawerWidth(width);
151-
},
152-
[activeDrawer],
153-
);
177+
}, [activeDrawer?.state.id, activeDrawer?.state.drawerWidth]);
154178

155179
return (
156180
<>
157181
{/* Render all state exposers - they return null but report their state */}
158182
{stateExposers.map(({ Component }, index) => (
159183
<Component
160-
key={`${index}-${Component.displayName}`}
184+
key={`drawer-${Component.displayName || index}`}
161185
onStateChange={handleStateChange}
162186
/>
163187
))}
164188

165189
{/* Render the active drawer */}
166190
{activeDrawer && (
167191
<CustomDrawer
168-
isDrawerOpen
169-
drawerWidth={activeDrawer.drawerWidth}
170-
onWidthChange={handleWidthChange}
192+
isDrawerOpen={activeDrawer.state.isDrawerOpen}
193+
drawerWidth={activeDrawer.state.drawerWidth}
194+
onWidthChange={activeDrawer.state.setDrawerWidth}
171195
>
172196
<activeDrawer.Component />
173197
</CustomDrawer>

workspaces/lightspeed/plugins/lightspeed/report.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export type DrawerState = {
7171
isDrawerOpen: boolean;
7272
drawerWidth: number;
7373
setDrawerWidth: (width: number) => void;
74+
closeDrawer: () => void;
7475
};
7576

7677
// @public

workspaces/lightspeed/plugins/lightspeed/src/components/LightspeedDrawerProvider.tsx

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,24 @@ export const LightspeedDrawerProvider = ({ children }: PropsWithChildren) => {
6666
} else {
6767
setCurrentConversationIdState(undefined);
6868
}
69-
setDisplayModeState(ChatbotDisplayMode.embedded);
69+
// Update this to fullscreen only when it is not already in the docked mode
70+
setDisplayModeState(prev => {
71+
if (prev === ChatbotDisplayMode.docked) {
72+
return prev; // Don't override docked mode
73+
}
74+
return ChatbotDisplayMode.embedded;
75+
});
7076
setIsOpen(true);
71-
} else if (displayModeState === ChatbotDisplayMode.embedded) {
72-
setDisplayModeState(ChatbotDisplayMode.default);
77+
} else {
78+
// When leaving lightspeed route, update this only when the current mode is fullscreen
79+
setDisplayModeState(prev => {
80+
if (prev === ChatbotDisplayMode.embedded) {
81+
return ChatbotDisplayMode.default;
82+
}
83+
return prev;
84+
});
7385
}
74-
}, [isLightspeedRoute, location.pathname, displayModeState]);
86+
}, [isLightspeedRoute, location.pathname]);
7587

7688
// Open chatbot in overlay mode
7789
const openChatbot = useCallback(() => {

workspaces/lightspeed/plugins/lightspeed/src/components/LightspeedDrawerStateExposer.tsx

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { useEffect } from 'react';
17+
import { useCallback, useEffect, useRef } from 'react';
1818

1919
import { ChatbotDisplayMode } from '@patternfly/chatbot';
2020

@@ -29,6 +29,7 @@ export type DrawerState = {
2929
isDrawerOpen: boolean;
3030
drawerWidth: number;
3131
setDrawerWidth: (width: number) => void;
32+
closeDrawer: () => void;
3233
};
3334

3435
/**
@@ -53,15 +54,27 @@ export type DrawerStateExposerProps = {
5354
export const LightspeedDrawerStateExposer = ({
5455
onStateChange,
5556
}: DrawerStateExposerProps) => {
56-
const { displayMode, drawerWidth, setDrawerWidth } =
57+
const { displayMode, drawerWidth, setDrawerWidth, toggleChatbot } =
5758
useLightspeedDrawerContext();
59+
60+
const isDrawerOpen = displayMode === ChatbotDisplayMode.docked;
61+
62+
const toggleChatbotRef = useRef(toggleChatbot);
63+
toggleChatbotRef.current = toggleChatbot;
64+
65+
const closeDrawer = useCallback(() => {
66+
toggleChatbotRef.current();
67+
}, []);
68+
5869
useEffect(() => {
5970
onStateChange({
6071
id: 'lightspeed',
61-
isDrawerOpen: displayMode === ChatbotDisplayMode.docked,
72+
isDrawerOpen,
6273
drawerWidth,
6374
setDrawerWidth,
75+
closeDrawer,
6476
});
65-
}, [displayMode, drawerWidth, onStateChange, setDrawerWidth]);
77+
}, [isDrawerOpen, drawerWidth, setDrawerWidth, closeDrawer, onStateChange]);
78+
6679
return null;
6780
};

workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/LightspeedDrawerStateExposer.test.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,11 @@ import {
2626
describe('LightspeedDrawerStateExposer', () => {
2727
const mockSetDrawerWidth = jest.fn();
2828
const mockOnStateChange = jest.fn();
29+
const mockToggleChatbot = jest.fn();
2930

3031
const createContextValue = (overrides = {}) => ({
3132
isChatbotActive: false,
32-
toggleChatbot: jest.fn(),
33+
toggleChatbot: mockToggleChatbot,
3334
displayMode: ChatbotDisplayMode.default,
3435
setDisplayMode: jest.fn(),
3536
drawerWidth: 500,
@@ -62,6 +63,7 @@ describe('LightspeedDrawerStateExposer', () => {
6263
isDrawerOpen: false,
6364
drawerWidth: 500,
6465
setDrawerWidth: mockSetDrawerWidth,
66+
closeDrawer: expect.any(Function),
6567
});
6668
});
6769
});
@@ -216,4 +218,14 @@ describe('LightspeedDrawerStateExposer', () => {
216218
expect(callArg.setDrawerWidth).toBe(mockSetDrawerWidth);
217219
});
218220
});
221+
222+
it('should call toggleChatbot when closeDrawer is invoked', async () => {
223+
renderWithContext(createContextValue());
224+
225+
await waitFor(() => {
226+
const callArg = mockOnStateChange.mock.calls[0][0] as DrawerState;
227+
callArg.closeDrawer();
228+
expect(mockToggleChatbot).toHaveBeenCalledTimes(1);
229+
});
230+
});
219231
});

0 commit comments

Comments
 (0)