diff --git a/specifyweb/frontend/js_src/lib/components/QueryBuilder/Header.tsx b/specifyweb/frontend/js_src/lib/components/QueryBuilder/Header.tsx
index 34d9609c5a2..4af326a6e43 100644
--- a/specifyweb/frontend/js_src/lib/components/QueryBuilder/Header.tsx
+++ b/specifyweb/frontend/js_src/lib/components/QueryBuilder/Header.tsx
@@ -47,7 +47,7 @@ export function QueryHeader({
onTriedToSave: handleTriedToSave,
onSaved: handleSaved,
isSplit,
- canSplit,
+ canUseHorizontalSplit,
isHorizontal,
onToggleSplit,
onToggleOrientation,
@@ -67,7 +67,7 @@ export function QueryHeader({
readonly onTriedToSave: () => void;
readonly onSaved: () => void;
readonly isSplit: boolean;
- readonly canSplit: boolean;
+ readonly canUseHorizontalSplit: boolean;
readonly isHorizontal: boolean;
readonly onToggleSplit: () => void;
readonly onToggleOrientation: () => void;
@@ -135,13 +135,9 @@ export function QueryHeader({
) : undefined}
{hasPermission('/querybuilder/query', 'execute') && (
<>
-
+
diff --git a/specifyweb/frontend/js_src/lib/components/QueryBuilder/QueryBuilderResults.tsx b/specifyweb/frontend/js_src/lib/components/QueryBuilder/QueryBuilderResults.tsx
index 315c80185a7..671786dd0bf 100644
--- a/specifyweb/frontend/js_src/lib/components/QueryBuilder/QueryBuilderResults.tsx
+++ b/specifyweb/frontend/js_src/lib/components/QueryBuilder/QueryBuilderResults.tsx
@@ -1,7 +1,7 @@
import React from 'react';
import { commonText } from '../../localization/common';
-import { localized, type RA } from '../../utils/types';
+import { type RA, localized } from '../../utils/types';
import { BatchEditFromQuery } from '../BatchEdit';
import type { SerializedResource } from '../DataModel/helperTypes';
import type { SpecifyResource } from '../DataModel/legacyTypes';
@@ -34,6 +34,8 @@ export function QueryBuilderResults({
resultsRef,
isSplit,
isHorizontal,
+ maximumPrimaryPaneWidth,
+ splitViewRef,
onReRun: handleReRun,
onResults: handleResults,
onSelected: handleSelected,
@@ -61,6 +63,8 @@ export function QueryBuilderResults({
>;
readonly isSplit: boolean;
readonly isHorizontal: boolean;
+ readonly maximumPrimaryPaneWidth: number;
+ readonly splitViewRef: React.RefCallback
;
readonly onReRun: () => void;
readonly onResults?: (results: RA) => void;
readonly onSelected: (ids: RA) => void;
@@ -149,8 +153,10 @@ export function QueryBuilderResults({
resultsRef={resultsRef}
selectedRows={[selectedRows, setSelectedRows]}
isSplit={isSplit}
+ splitContainerRef={splitViewRef}
splitHorizontal={isHorizontal}
splitPane={recordPreview}
+ splitPrimaryPaneMaxWidth={`${maximumPrimaryPaneWidth}px`}
table={table}
onReRun={handleReRun}
onResults={handleResults}
diff --git a/specifyweb/frontend/js_src/lib/components/QueryBuilder/ResultsWrapper.tsx b/specifyweb/frontend/js_src/lib/components/QueryBuilder/ResultsWrapper.tsx
index d235b929fdf..07dbd3ea1d3 100644
--- a/specifyweb/frontend/js_src/lib/components/QueryBuilder/ResultsWrapper.tsx
+++ b/specifyweb/frontend/js_src/lib/components/QueryBuilder/ResultsWrapper.tsx
@@ -37,7 +37,9 @@ export function QueryResultsWrapper({
onReRun: handleReRun,
refreshToken,
splitPane,
+ splitContainerRef,
splitHorizontal,
+ splitPrimaryPaneMaxWidth,
isSplit,
...props
}: ResultsProps & {
@@ -49,7 +51,9 @@ export function QueryResultsWrapper({
readonly restoreScrollTopRef?: React.MutableRefObject;
readonly refreshToken?: number;
readonly splitPane?: JSX.Element;
+ readonly splitContainerRef?: React.RefCallback;
readonly splitHorizontal?: boolean;
+ readonly splitPrimaryPaneMaxWidth?: string;
readonly isSplit?: boolean;
readonly onReRun: () => void;
}): JSX.Element | null {
@@ -79,14 +83,20 @@ export function QueryResultsWrapper({
return splitPane === undefined ? (
queryResults
) : (
-
+
+
+
);
}
diff --git a/specifyweb/frontend/js_src/lib/components/QueryBuilder/SplitView.tsx b/specifyweb/frontend/js_src/lib/components/QueryBuilder/SplitView.tsx
index 3a2e0890cd9..8f3f7a2784e 100644
--- a/specifyweb/frontend/js_src/lib/components/QueryBuilder/SplitView.tsx
+++ b/specifyweb/frontend/js_src/lib/components/QueryBuilder/SplitView.tsx
@@ -1,9 +1,9 @@
-import React from 'react';
import Splitter from 'm-react-splitters';
+import React from 'react';
-import { Button } from '../Atoms/Button';
-import { treeText } from '../../localization/tree';
import { useTriggerState } from '../../hooks/useTriggerState';
+import { treeText } from '../../localization/tree';
+import { Button } from '../Atoms/Button';
export function useSplitViewOrientation(defaultHorizontal = true): {
readonly isHorizontal: boolean;
@@ -60,6 +60,7 @@ export function SplitView({
primaryPane,
secondaryPane,
primaryPaneKey,
+ primaryPaneMaxWidth,
secondaryPaneKey,
isHorizontal,
isSplit = true,
@@ -67,10 +68,21 @@ export function SplitView({
readonly primaryPane: JSX.Element;
readonly secondaryPane: JSX.Element;
readonly primaryPaneKey: string;
+ readonly primaryPaneMaxWidth?: string;
readonly secondaryPaneKey: string;
readonly isHorizontal: boolean;
readonly isSplit?: boolean;
}): JSX.Element {
+ const splitterRef = React.useRef | null>(
+ null
+ );
+ const previousIsHorizontal = React.useRef(isHorizontal);
+ React.useLayoutEffect(() => {
+ if (previousIsHorizontal.current !== isHorizontal)
+ splitterRef.current?.setState({ primaryPane: undefined });
+ previousIsHorizontal.current = isHorizontal;
+ }, [isHorizontal]);
+
return (
dispatch({ type: 'RunQueryAction' })}
diff --git a/specifyweb/frontend/js_src/lib/components/QueryBuilder/__tests__/SplitView.test.tsx b/specifyweb/frontend/js_src/lib/components/QueryBuilder/__tests__/SplitView.test.tsx
new file mode 100644
index 00000000000..f3a54609a66
--- /dev/null
+++ b/specifyweb/frontend/js_src/lib/components/QueryBuilder/__tests__/SplitView.test.tsx
@@ -0,0 +1,49 @@
+import { render } from '@testing-library/react';
+import React from 'react';
+
+import { SplitView } from '../SplitView';
+
+const mockSetState = jest.fn();
+
+jest.mock('m-react-splitters', () => {
+ const actualReact = jest.requireActual
('react');
+ const MockSplitter = actualReact.forwardRef(function MockSplitter(
+ { children }: { readonly children?: React.ReactNode },
+ ref: React.ForwardedRef<{ readonly setState: typeof mockSetState }>
+ ): JSX.Element {
+ actualReact.useImperativeHandle(
+ ref,
+ () => ({ setState: mockSetState }),
+ []
+ );
+ return {children}
;
+ });
+ return { __esModule: true, default: MockSplitter };
+});
+
+const primaryPane = ;
+const secondaryPane = ;
+
+test('clears the dragged pane size when the orientation changes', () => {
+ const { rerender } = render(
+
+ );
+ expect(mockSetState).not.toHaveBeenCalled();
+
+ rerender(
+
+ );
+ expect(mockSetState).toHaveBeenCalledWith({ primaryPane: undefined });
+});
diff --git a/specifyweb/frontend/js_src/lib/components/QueryBuilder/useQuerySplitView.ts b/specifyweb/frontend/js_src/lib/components/QueryBuilder/useQuerySplitView.ts
index 1f4b055a45f..93771e3a6b7 100644
--- a/specifyweb/frontend/js_src/lib/components/QueryBuilder/useQuerySplitView.ts
+++ b/specifyweb/frontend/js_src/lib/components/QueryBuilder/useQuerySplitView.ts
@@ -1,12 +1,10 @@
import React from 'react';
+import { useResponsiveSplitView } from '../../hooks/useResponsiveSplitView';
import type { RA } from '../../utils/types';
-import { listen } from '../../utils/events';
+import { userPreferences } from '../Preferences/userPreferences';
import { queryIdField, type QueryResultRow } from './Results';
import { useSplitViewOrientation } from './SplitView';
-import { userPreferences } from '../Preferences/userPreferences';
-
-const SMALL_SCREEN_WIDTH = 768;
export function useQuerySplitView(
resultsRef: React.MutableRefObject<
@@ -21,7 +19,9 @@ export function useQuerySplitView(
readonly selectedIndex: number;
readonly setSelectedIndex: React.Dispatch>;
readonly isSplit: boolean;
- readonly canSplit: boolean;
+ readonly canUseHorizontalSplit: boolean;
+ readonly splitViewRef: React.RefCallback;
+ readonly maximumPrimaryPaneWidth: number;
readonly isHorizontal: boolean;
readonly toggleSplit: () => void;
readonly toggleOrientation: () => void;
@@ -42,19 +42,15 @@ export function useQuerySplitView(
'splitViewOrientation'
);
const [rawIsSplit, setIsSplit] = React.useState(splitViewByDefault);
- const [canSplit, setCanSplit] = React.useState(
- window.innerWidth >= SMALL_SCREEN_WIDTH
- );
- React.useEffect(() => {
- const handleResize = (): void =>
- setCanSplit(window.innerWidth >= SMALL_SCREEN_WIDTH);
- handleResize();
- return listen(window, 'resize', handleResize);
- }, []);
- const isSplit = rawIsSplit && canSplit;
- const { isHorizontal, toggleOrientation } = useSplitViewOrientation(
- splitViewOrientation === 'horizontal'
- );
+ const isSplit = rawIsSplit;
+ const { isHorizontal: preferredIsHorizontal, toggleOrientation } =
+ useSplitViewOrientation(splitViewOrientation === 'horizontal');
+ const {
+ canUseHorizontalSplit,
+ containerRef: splitViewRef,
+ isHorizontal,
+ maximumPrimaryPaneWidth,
+ } = useResponsiveSplitView(preferredIsHorizontal);
const selectFirstResult = React.useCallback((): boolean => {
const firstId = resultsRef.current?.find(
@@ -120,7 +116,9 @@ export function useQuerySplitView(
selectedIndex,
setSelectedIndex,
isSplit,
- canSplit,
+ canUseHorizontalSplit,
+ splitViewRef,
+ maximumPrimaryPaneWidth,
isHorizontal,
toggleSplit,
toggleOrientation,
diff --git a/specifyweb/frontend/js_src/lib/hooks/useResponsiveSplitView.ts b/specifyweb/frontend/js_src/lib/hooks/useResponsiveSplitView.ts
new file mode 100644
index 00000000000..a5fe60a3273
--- /dev/null
+++ b/specifyweb/frontend/js_src/lib/hooks/useResponsiveSplitView.ts
@@ -0,0 +1,46 @@
+import React from 'react';
+
+import { listen } from '../utils/events';
+
+export const minimumSplitPaneWidth = 768;
+export const splitViewHandleWidth = 10;
+const minimumCombinedPaneWidth = minimumSplitPaneWidth * 2;
+const combinedHandleAllowance = splitViewHandleWidth * 2;
+export const minimumHorizontalSplitWidth =
+ minimumCombinedPaneWidth + combinedHandleAllowance;
+
+export function useResponsiveSplitView(preferredHorizontal: boolean): {
+ readonly canUseHorizontalSplit: boolean;
+ readonly containerRef: React.RefCallback;
+ readonly isHorizontal: boolean;
+ readonly maximumPrimaryPaneWidth: number;
+} {
+ const [container, setContainer] = React.useState(null);
+ const containerRef = React.useCallback(setContainer, [setContainer]);
+ const [containerWidth, setContainerWidth] = React.useState(0);
+
+ React.useEffect(() => {
+ if (container === null) return undefined;
+
+ const updateWidth = (): void => setContainerWidth(container.clientWidth);
+ updateWidth();
+ const observer = new ResizeObserver(updateWidth);
+ observer.observe(container);
+ const removeResizeListener = listen(window, 'resize', updateWidth);
+ return (): void => {
+ observer.disconnect();
+ removeResizeListener();
+ };
+ }, [container]);
+
+ const canUseHorizontalSplit = containerWidth >= minimumHorizontalSplitWidth;
+ return {
+ canUseHorizontalSplit,
+ containerRef,
+ isHorizontal: preferredHorizontal && canUseHorizontalSplit,
+ maximumPrimaryPaneWidth: Math.max(
+ 0,
+ containerWidth - minimumSplitPaneWidth - splitViewHandleWidth
+ ),
+ };
+}