Skip to content

Commit 68bb834

Browse files
committed
StatsHouse UI: grid dash step 2, plot burger menu
1 parent b96ff58 commit 68bb834

26 files changed

+879
-357
lines changed

statshouse-ui/eslint.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export default tseslint.config({
4545
'react/no-unescaped-entities': 'off',
4646
'no-console': 'warn',
4747
'no-empty': ['error', { allowEmptyCatch: true }],
48+
'no-empty-pattern': ['error', { allowObjectPatternsAsParameters: true }],
4849
'@typescript-eslint/no-unused-vars': [
4950
'warn',
5051
{
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright 2025 V Kontakte LLC
2+
//
3+
// This Source Code Form is subject to the terms of the Mozilla Public
4+
// License, v. 2.0. If a copy of the MPL was not distributed with this
5+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
6+
7+
import cn from 'classnames';
8+
import css from './style.module.css';
9+
import { ReactNode, useEffect, useState } from 'react';
10+
import { Portal } from '@/components/UI/Portal';
11+
import { useRectObserver } from '@/hooks';
12+
13+
const dialogId = 'popper-group';
14+
15+
export type DialogProps = {
16+
open?: boolean;
17+
children?: ReactNode | ((size: { width: number; height: number; maxWidth: number; maxHeight: number }) => ReactNode);
18+
onClose?: () => void;
19+
className?: string;
20+
};
21+
22+
export function Dialog({ children, className, open, onClose }: DialogProps) {
23+
const [wrapper, setWrapper] = useState<HTMLElement | null>(null);
24+
const [targetRect, updateTargetRect] = useRectObserver(wrapper, false, open, false);
25+
26+
useEffect(() => {
27+
updateTargetRect();
28+
}, [open, updateTargetRect]);
29+
30+
useEffect(() => {
31+
if (open) {
32+
document.documentElement.classList.add('modal');
33+
}
34+
return () => {
35+
if (!document.querySelector(`.${css.dialogWrapper}`)) {
36+
document.documentElement.classList.remove('modal');
37+
}
38+
};
39+
}, [open]);
40+
41+
return (
42+
<Portal id={dialogId} className={cn(css.popperGroup)}>
43+
{open && (
44+
<div ref={setWrapper} className={cn(css.dialogWrapper)}>
45+
<div className={css.dialogBackground} onClick={onClose}></div>
46+
<div className={cn(className)}>
47+
{typeof children === 'function'
48+
? children({
49+
height: targetRect.height,
50+
width: targetRect.width,
51+
maxWidth: targetRect.width,
52+
maxHeight: targetRect.height,
53+
})
54+
: children}
55+
</div>
56+
</div>
57+
)}
58+
</Portal>
59+
);
60+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright 2025 V Kontakte LLC
2+
//
3+
// This Source Code Form is subject to the terms of the Mozilla Public
4+
// License, v. 2.0. If a copy of the MPL was not distributed with this
5+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
6+
7+
import { memo } from 'react';
8+
import { Tooltip } from '@/components/UI/Tooltip';
9+
import cn from 'classnames';
10+
import { POPPER_HORIZONTAL, POPPER_VERTICAL } from '@/components/UI/Popper';
11+
import { useStateBoolean } from '@/hooks';
12+
13+
import { DropdownContextProvider } from '@/contexts/DropdownContextProvider';
14+
15+
export type DropdownProps = { className?: string; caption?: React.ReactNode; children?: React.ReactNode };
16+
17+
export const Dropdown = memo(function Dropdown({ className, children, caption }: DropdownProps) {
18+
const [dropdown, setDropdown] = useStateBoolean(false);
19+
20+
return (
21+
<Tooltip
22+
as="button"
23+
type="button"
24+
className={cn(className, 'overflow-auto')}
25+
title={<DropdownContextProvider value={setDropdown}>{children}</DropdownContextProvider>}
26+
open={dropdown}
27+
vertical={POPPER_VERTICAL.outBottom}
28+
horizontal={POPPER_HORIZONTAL.right}
29+
onClick={setDropdown.toggle}
30+
onClickOuter={setDropdown.off}
31+
titleClassName={'p-0 m-0'}
32+
noStyle
33+
>
34+
{caption}
35+
</Tooltip>
36+
);
37+
});

statshouse-ui/src/components/UI/Tooltip.tsx

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import React, { useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
1+
import React, { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
22
import { Popper, POPPER_HORIZONTAL, POPPER_VERTICAL, PopperHorizontal, PopperVertical } from './Popper';
33
import type { JSX } from 'react/jsx-runtime';
44
import { TooltipTitleContent } from './TooltipTitleContent';
5-
import { useOnClickOutside } from '@/hooks';
5+
import { useOnClickOutside, useStateToRef } from '@/hooks';
66

77
import cn from 'classnames';
88
import css from './style.module.css';
@@ -27,6 +27,7 @@ export type TooltipProps<T extends keyof JSX.IntrinsicElements> = {
2727
delay?: number;
2828
delayClose?: number;
2929
onClickOuter?: () => void;
30+
noStyle?: boolean;
3031
} & Omit<JSX.IntrinsicElements[T], 'title'>;
3132

3233
declare function _TooltipFn<T extends keyof JSX.IntrinsicElements>(props: TooltipProps<T>): JSX.Element;
@@ -52,32 +53,36 @@ export const Tooltip = React.forwardRef<Element, TooltipProps<'div'>>(function T
5253
onMouseOut,
5354
onMouseMove,
5455
onClick,
56+
noStyle,
5557
...props
5658
},
5759
ref
5860
) {
5961
const timeoutDelayRef = useRef<NodeJS.Timeout | null>(null);
6062
const [localRef, setLocalRef] = useState<Element | null>(null);
63+
const [open, setOpen] = useState(false);
6164

62-
const targetRef = useRef<Element | null>(null);
65+
const openRef = useStateToRef(open);
66+
const targetRef = useStateToRef(localRef);
6367

6468
useImperativeHandle<Element | null, Element | null>(ref, () => localRef, [localRef]);
6569

6670
const portalRef = useRef(null);
67-
useOnClickOutside(portalRef, () => {
68-
if (outerOpen == null) {
69-
timeoutDelayRef.current = setTimeout(() => {
70-
setOpen(false);
71-
}, delayClose);
72-
}
73-
onClickOuter?.();
74-
});
71+
const innerRef = useMemo(() => [portalRef, targetRef], [targetRef]);
7572

76-
useEffect(() => {
77-
targetRef.current = localRef;
78-
}, [localRef]);
79-
80-
const [open, setOpen] = useState(false);
73+
useOnClickOutside(
74+
innerRef,
75+
useCallback(() => {
76+
if (outerOpen == null) {
77+
timeoutDelayRef.current = setTimeout(() => {
78+
setOpen(false);
79+
}, delayClose);
80+
}
81+
if (openRef.current) {
82+
onClickOuter?.();
83+
}
84+
}, [delayClose, onClickOuter, openRef, outerOpen])
85+
);
8186

8287
useEffect(() => {
8388
if (outerOpen != null) {
@@ -163,8 +168,12 @@ export const Tooltip = React.forwardRef<Element, TooltipProps<'div'>>(function T
163168
vertical={vertical}
164169
show={open}
165170
>
166-
<div ref={portalRef} className={cn(titleClassName, 'card overflow-auto')} onClick={stopPropagation}>
167-
<div className="card-body p-1" style={{ minHeight, minWidth, maxHeight, maxWidth }}>
171+
<div
172+
ref={portalRef}
173+
className={cn(titleClassName, !noStyle && 'card overflow-auto')}
174+
onClick={stopPropagation}
175+
>
176+
<div className={cn(!noStyle && 'card-body p-1')} style={{ minHeight, minWidth, maxHeight, maxWidth }}>
168177
<TooltipTitleContent>{title}</TooltipTitleContent>
169178
</div>
170179
</div>

statshouse-ui/src/components/UI/style.module.css

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,3 +203,26 @@
203203
.selectCursor .hoverVisible {
204204
visibility: visible;
205205
}
206+
207+
.dialogWrapper{
208+
position: fixed;
209+
display: flex;
210+
top: 0;
211+
left: 0;
212+
bottom: 0;
213+
right: 0;
214+
pointer-events: auto;
215+
overflow: hidden;
216+
justify-content: center;
217+
align-items: center;
218+
}
219+
220+
.dialogBackground{
221+
position: absolute;
222+
top: 0;
223+
left: 0;
224+
bottom: 0;
225+
right: 0;
226+
background-color: rgba(0, 0, 0, 0.3);
227+
}
228+
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright 2025 V Kontakte LLC
2+
//
3+
// This Source Code Form is subject to the terms of the Mozilla Public
4+
// License, v. 2.0. If a copy of the MPL was not distributed with this
5+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
6+
7+
import { Button, InputText } from '@/components/UI';
8+
import { Dialog } from '@/components/UI/Dialog';
9+
import { useCallback, useState } from 'react';
10+
11+
export type EditCustomNameDialogProps = {
12+
open?: boolean;
13+
onClose?: () => void;
14+
onChange?: (value?: string) => void;
15+
value: string;
16+
placeholder?: string;
17+
};
18+
19+
export function EditCustomNameDialog({ open, value, placeholder, onClose, onChange }: EditCustomNameDialogProps) {
20+
const [localCustomName, setLocalCustomName] = useState(value || placeholder);
21+
22+
const saveCustomName = useCallback(() => {
23+
onChange?.(localCustomName);
24+
onClose?.();
25+
}, [localCustomName, onChange, onClose]);
26+
27+
return (
28+
<Dialog open={open} onClose={onClose}>
29+
<div className="card">
30+
<div className="card-header">Edit custom name</div>
31+
<div className="card-body">
32+
<div>
33+
<label htmlFor="inputCustomName" className="form-label">
34+
Custom name:
35+
</label>
36+
<InputText
37+
id="inputCustomName"
38+
size={50}
39+
style={{ maxWidth: '80wv' }}
40+
value={localCustomName}
41+
onInput={setLocalCustomName}
42+
placeholder={placeholder}
43+
/>
44+
</div>
45+
</div>
46+
<div className="card-footer d-flex gap-2 justify-content-end">
47+
<Button className="btn btn-outline-primary" type="button" onClick={saveCustomName}>
48+
Save
49+
</Button>
50+
<Button className="btn btn-outline-primary" type="button" onClick={onClose}>
51+
Cancel
52+
</Button>
53+
</div>
54+
</div>
55+
</Dialog>
56+
);
57+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Copyright 2025 V Kontakte LLC
2+
//
3+
// This Source Code Form is subject to the terms of the Mozilla Public
4+
// License, v. 2.0. If a copy of the MPL was not distributed with this
5+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
6+
7+
export * from './EditCustomNameDialog';

0 commit comments

Comments
 (0)