Skip to content

Commit 0f2e138

Browse files
authored
fix(ui mode): filter dialog positioning (#37619)
1 parent 2900aeb commit 0f2e138

File tree

6 files changed

+66
-96
lines changed

6 files changed

+66
-96
lines changed

packages/html-reporter/src/headerView.tsx

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -123,21 +123,24 @@ const SettingsButton: React.FC = () => {
123123
const [darkMode, setDarkMode] = useDarkModeSetting();
124124
const [mergeFiles, setMergeFiles] = useSetting('mergeFiles', false);
125125

126-
return <div
127-
role='button'
128-
ref={settingsRef}
129-
style={{ cursor: 'pointer' }}
130-
className='subnav-item'
131-
title='Settings'
132-
onClick={e => {
133-
setSettingsOpen(!settingsOpen);
134-
e.preventDefault();
135-
}}
136-
onMouseDown={preventDefault}>
126+
return <>
127+
<div
128+
role='button'
129+
ref={settingsRef}
130+
style={{ cursor: 'pointer' }}
131+
className='subnav-item'
132+
title='Settings'
133+
onClick={e => {
134+
setSettingsOpen(!settingsOpen);
135+
e.preventDefault();
136+
}}
137+
onMouseDown={preventDefault}>
138+
{icons.settings()}
139+
</div>
137140
<Dialog
138141
open={settingsOpen}
139-
width={150}
140-
verticalOffset={8}
142+
minWidth={150}
143+
verticalOffset={4}
141144
requestClose={() => setSettingsOpen(false)}
142145
anchor={settingsRef}
143146
dataTestId='settings-dialog'
@@ -151,8 +154,7 @@ const SettingsButton: React.FC = () => {
151154
Merge files
152155
</label>
153156
</Dialog>
154-
{icons.settings()}
155-
</div>;
157+
</>;
156158
};
157159

158160
const preventDefault = (e: any) => {

packages/injected/src/highlight.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ export class Highlight {
215215
};
216216
}
217217

218+
// Note: there is a copy of this method in dialog.tsx. Please fix bugs in both places.
218219
tooltipPosition(box: DOMRect, tooltipElement: HTMLElement) {
219220
const tooltipWidth = tooltipElement.offsetWidth;
220221
const tooltipHeight = tooltipElement.offsetHeight;

packages/recorder/src/recorder.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,6 @@ export const Recorder: React.FC<RecorderProps> = ({
198198
<Dialog
199199
style={{ padding: '4px 8px' }}
200200
open={settingsOpen}
201-
width={200}
202201
verticalOffset={8}
203202
requestClose={() => setSettingsOpen(false)}
204203
anchor={settingsButtonRef}

packages/web/src/components/dialogToolbarButton.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ export const DialogToolbarButton: React.FC<React.PropsWithChildren<DialogToolbar
4141
padding: '4px 8px'
4242
}}
4343
open={open}
44-
width={200}
4544
// TODO: Temporary spacing until design of toolbar buttons is revisited
4645
verticalOffset={8}
4746
requestClose={() => setOpen(false)}

packages/web/src/shared/dialog.tsx

Lines changed: 32 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@
1616

1717
import * as React from 'react';
1818

19+
import { useMeasureForRef } from '../uiUtils';
20+
1921
export interface DialogProps {
2022
className?: string;
2123
style?: React.CSSProperties;
2224
open: boolean;
2325
isModal?: boolean;
24-
width?: number;
26+
minWidth?: number;
2527
verticalOffset?: number;
2628
requestClose?: () => void;
2729
anchor?: React.RefObject<HTMLElement|null>;
@@ -33,7 +35,7 @@ export const Dialog: React.FC<React.PropsWithChildren<DialogProps>> = ({
3335
style: externalStyle,
3436
open,
3537
isModal,
36-
width,
38+
minWidth,
3739
verticalOffset,
3840
requestClose,
3941
anchor,
@@ -44,22 +46,9 @@ export const Dialog: React.FC<React.PropsWithChildren<DialogProps>> = ({
4446

4547
// eslint-disable-next-line @typescript-eslint/no-unused-vars
4648
const [_, setRecalculateDimensionsCount] = React.useState(0);
47-
48-
let style: React.CSSProperties | undefined = externalStyle;
49-
50-
if (anchor?.current) {
51-
const bounds = anchor.current.getBoundingClientRect();
52-
53-
style = {
54-
position: 'fixed',
55-
margin: 0,
56-
top: bounds.bottom + (verticalOffset ?? 0),
57-
left: buildTopLeftCoord(bounds, width ?? 0),
58-
width,
59-
zIndex: 110, // on top of split view resizer
60-
...externalStyle
61-
};
62-
}
49+
const dialogMeasure = useMeasureForRef(dialogRef);
50+
const anchorMeasure = useMeasureForRef(anchor);
51+
const position = dialogPosition(dialogMeasure, anchorMeasure, verticalOffset);
6352

6453
React.useEffect(() => {
6554
const onClick = (event: MouseEvent) => {
@@ -113,53 +102,34 @@ export const Dialog: React.FC<React.PropsWithChildren<DialogProps>> = ({
113102
}, [open, isModal]);
114103

115104
return (
116-
<dialog ref={dialogRef} style={style} className={className} data-testid={dataTestId}>
105+
<dialog ref={dialogRef} style={{
106+
position: 'fixed',
107+
margin: 0,
108+
zIndex: 110, // on top of split view resizer
109+
top: position.top,
110+
left: position.left,
111+
minWidth: minWidth || 0,
112+
...externalStyle,
113+
}} className={className} data-testid={dataTestId}>
117114
{children}
118115
</dialog>
119116
);
120117
};
121118

122-
const buildTopLeftCoord = (bounds: DOMRect, width: number): number => {
123-
const leftAlignCoord = buildTopLeftCoordWithAlignment(bounds, width, 'left');
124-
125-
if (leftAlignCoord.inBounds)
126-
return leftAlignCoord.value;
127-
128-
const rightAlignCoord = buildTopLeftCoordWithAlignment(
129-
bounds,
130-
width,
131-
'right'
132-
);
133-
134-
if (rightAlignCoord.inBounds)
135-
return rightAlignCoord.value;
136-
137-
return leftAlignCoord.value;
138-
};
139-
140-
const buildTopLeftCoordWithAlignment = (
141-
bounds: DOMRect,
142-
width: number,
143-
alignment: 'left' | 'right'
144-
): {
145-
value: number;
146-
inBounds: boolean;
147-
} => {
148-
const maxLeft = document.documentElement.clientWidth;
149-
150-
if (alignment === 'left') {
151-
const value = bounds.left;
152-
153-
return {
154-
value,
155-
inBounds: value + width <= maxLeft,
156-
};
157-
} else {
158-
const value = bounds.right - width;
159-
160-
return {
161-
value,
162-
inBounds: bounds.right - width >= 0,
163-
};
119+
// Note: there is a copy of this method in highlight.ts. Please fix bugs in both places.
120+
function dialogPosition(dialogBox: DOMRect, anchorBox: DOMRect, verticalOffset = 4, horizontalOffset = 4): { top: number, left: number } {
121+
let left = Math.max(horizontalOffset, anchorBox.left);
122+
if (left + dialogBox.width > window.innerWidth - horizontalOffset)
123+
left = window.innerWidth - dialogBox.width - horizontalOffset;
124+
let top = Math.max(0, anchorBox.bottom) + verticalOffset;
125+
if (top + dialogBox.height > window.innerHeight - verticalOffset) {
126+
// If can't fit below, either position above...
127+
if (Math.max(0, anchorBox.top) > dialogBox.height + verticalOffset) {
128+
top = Math.max(0, anchorBox.top) - dialogBox.height - verticalOffset;
129+
} else {
130+
// Or on top in case of large element
131+
top = window.innerHeight - verticalOffset - dialogBox.height;
132+
}
164133
}
165-
};
134+
return { left, top };
135+
}

packages/web/src/uiUtils.ts

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616

1717
import React from 'react';
1818

19-
import type { EffectCallback } from 'react';
20-
2119
// Recalculates the value when dependencies change.
2220
export function useAsyncMemo<T>(fn: () => Promise<T>, deps: React.DependencyList, initialValue: T, resetValue?: T) {
2321
const [value, setValue] = React.useState<T>(initialValue);
@@ -37,28 +35,29 @@ export function useAsyncMemo<T>(fn: () => Promise<T>, deps: React.DependencyList
3735
return value;
3836
}
3937

40-
// Tracks the element size and returns it's contentRect (always has x=0, y=0).
38+
// Tracks the element's bounding box.
4139
export function useMeasure<T extends Element>() {
4240
const ref = React.useRef<T | null>(null);
41+
return [useMeasureForRef(ref), ref] as const;
42+
}
43+
44+
export function useMeasureForRef<T extends Element>(ref?: React.RefObject<T | null>) {
4345
const [measure, setMeasure] = React.useState(new DOMRect(0, 0, 10, 10));
4446
React.useLayoutEffect(() => {
45-
const target = ref.current;
47+
const target = ref?.current;
4648
if (!target)
4749
return;
48-
49-
const bounds = target.getBoundingClientRect();
50-
51-
setMeasure(new DOMRect(0, 0, bounds.width, bounds.height));
52-
53-
const resizeObserver = new ResizeObserver((entries: any) => {
54-
const entry = entries[entries.length - 1];
55-
if (entry && entry.contentRect)
56-
setMeasure(entry.contentRect);
57-
});
50+
const update = () => setMeasure(target.getBoundingClientRect());
51+
update();
52+
const resizeObserver = new ResizeObserver(update);
5853
resizeObserver.observe(target);
59-
return () => resizeObserver.disconnect();
54+
window.addEventListener('resize', update);
55+
return () => {
56+
resizeObserver.disconnect();
57+
window.removeEventListener('resize', update);
58+
};
6059
}, [ref]);
61-
return [measure, ref] as const;
60+
return measure;
6261
}
6362

6463
export function msToString(ms: number): string {
@@ -233,7 +232,7 @@ export const kWebLinkRe = new RegExp('(?:[a-zA-Z][a-zA-Z0-9+.-]{2,}:\\/\\/|www\\
233232
* If `trigger` is called while a flash is ongoing, the ongoing flash will be cancelled and after 50ms a new flash is started.
234233
* @returns [flash, trigger]
235234
*/
236-
export function useFlash(): [boolean, EffectCallback] {
235+
export function useFlash(): [boolean, React.EffectCallback] {
237236
const [flash, setFlash] = React.useState(false);
238237
const trigger = React.useCallback<React.EffectCallback>(() => {
239238
const timeouts: any[] = [];

0 commit comments

Comments
 (0)