Skip to content

Commit 85faa8f

Browse files
committed
ui-manchette: fix waypoint menu positioning
- Move the menu next to its related waypoint so it can always be positioned relatively to it - To keep osrd-ui as neutral as possible, the menu has no styles, they need to be passed by props
1 parent 233f908 commit 85faa8f

File tree

9 files changed

+126
-107
lines changed

9 files changed

+126
-107
lines changed

ui-manchette/src/components/Manchette.tsx

+4-6
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ import cx from 'classnames';
55

66
import { INITIAL_OP_LIST_HEIGHT, MAX_ZOOM_Y, MIN_ZOOM_Y } from './consts';
77
import OperationalPointList from './OperationalPointList';
8-
import type { StyledOperationalPointType } from '../types';
8+
import type { StyledOperationalPointType, WaypointMenuData } from '../types';
99

1010
type ManchetteProps = {
1111
operationalPoints: StyledOperationalPointType[];
12-
activeOperationalPointId?: string;
12+
waypointMenuData: WaypointMenuData;
1313
zoomYIn: () => void;
1414
zoomYOut: () => void;
1515
resetZoom: () => void;
@@ -26,10 +26,9 @@ const Manchette = ({
2626
resetZoom,
2727
yZoom = 1,
2828
operationalPoints,
29-
activeOperationalPointId,
29+
waypointMenuData,
3030
isProportional = true,
3131
toggleMode,
32-
children,
3332
}: ManchetteProps) => (
3433
<div className="manchette-container">
3534
<div
@@ -38,9 +37,8 @@ const Manchette = ({
3837
>
3938
<OperationalPointList
4039
operationalPoints={operationalPoints}
41-
activeOperationalPointId={activeOperationalPointId}
40+
waypointMenuData={waypointMenuData}
4241
/>
43-
{children}
4442
</div>
4543
<div className="manchette-actions">
4644
<div className="zoom-buttons">

ui-manchette/src/components/OperationalPoint.tsx

+10-2
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,23 @@ import React, { useRef } from 'react';
22

33
import cx from 'classnames';
44

5-
import { type StyledOperationalPointType } from '../types';
5+
import type { WaypointMenuItem, StyledOperationalPointType } from '../types';
66
import '@osrd-project/ui-core/dist/theme.css';
77
import { positionMmToKm } from '../utils';
8+
import WaypointMenu from './WaypointMenu';
89

910
type OperationalPointProps = {
1011
operationalPoint: StyledOperationalPointType;
1112
isActive: boolean;
13+
waypointMenuItems?: WaypointMenuItem[];
14+
waypointMenuClassName?: string;
1215
};
1316

1417
const OperationalPoint = ({
1518
operationalPoint: { extensions, id, position, display, onClick },
1619
isActive,
20+
waypointMenuItems,
21+
waypointMenuClassName,
1722
}: OperationalPointProps) => {
1823
const opRef = useRef<HTMLDivElement>(null);
1924

@@ -27,7 +32,7 @@ const OperationalPoint = ({
2732
id={id}
2833
ref={opRef}
2934
onClick={() => {
30-
if (onClick) onClick(id, opRef.current);
35+
if (onClick) onClick(`${id}-${position}`); // to handle waypoints with same id
3136
}}
3237
>
3338
<div className="op-position justify-self-start text-end">{positionMmToKm(position)}</div>
@@ -39,6 +44,9 @@ const OperationalPoint = ({
3944

4045
<div className="op-type"></div>
4146
<div className="op-separator"></div>
47+
{isActive && waypointMenuItems && (
48+
<WaypointMenu items={waypointMenuItems} className={waypointMenuClassName} />
49+
)}
4250
</div>
4351
);
4452
};

ui-manchette/src/components/OperationalPointList.tsx

+9-4
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import React from 'react';
22

33
import OperationalPoint from './OperationalPoint';
4-
import { type StyledOperationalPointType } from '../types';
4+
import type { WaypointMenuData, StyledOperationalPointType } from '../types';
55

66
type OperationalPointListProps = {
77
operationalPoints: StyledOperationalPointType[];
8-
activeOperationalPointId?: string;
8+
waypointMenuData: WaypointMenuData;
99
};
1010

1111
const OperationalPointList = ({
1212
operationalPoints,
13-
activeOperationalPointId,
13+
waypointMenuData: { activeOperationalPointId, waypointMenuItems, waypointMenuClassName },
1414
}: OperationalPointListProps) => (
1515
<div className="operational-point-list ">
1616
{operationalPoints.map((op) => (
@@ -19,7 +19,12 @@ const OperationalPointList = ({
1919
className="operational-point-wrapper flex flex-col justify-start"
2020
style={op.styles}
2121
>
22-
<OperationalPoint operationalPoint={op} isActive={activeOperationalPointId === op.id} />
22+
<OperationalPoint
23+
operationalPoint={op}
24+
isActive={activeOperationalPointId === `${op.id}-${op.position}`}
25+
waypointMenuItems={waypointMenuItems}
26+
waypointMenuClassName={waypointMenuClassName}
27+
/>
2328
</div>
2429
))}
2530
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import React, { type HTMLAttributes } from 'react';
2+
3+
import { type WaypointMenuItem } from '../types';
4+
5+
type WaypointMenuProps = HTMLAttributes<unknown> & {
6+
items: WaypointMenuItem[];
7+
};
8+
9+
/**
10+
* An optional menu to handle some actions when clicking on a waypoint
11+
*
12+
* The menu doesn't have any styles by default so the user can customize it as needed
13+
*
14+
* Styles need to be send from the application with props
15+
*/
16+
const WaypointMenu = ({ items, className }: WaypointMenuProps) => (
17+
<div className={className}>
18+
{items.map(({ title, icon, onClick }) => (
19+
<button
20+
key={title}
21+
type="button"
22+
onClick={(e) => {
23+
onClick(e);
24+
}}
25+
>
26+
<span>{icon}</span>
27+
<span>{title}</span>
28+
</button>
29+
))}
30+
</div>
31+
);
32+
33+
export default WaypointMenu;

ui-manchette/src/stories/Manchette.stories.tsx

+18-31
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
1-
import React, { useRef, useState } from 'react';
1+
import React, { useState } from 'react';
22

33
import '@osrd-project/ui-core/dist/theme.css';
44
import '@osrd-project/ui-manchette/dist/theme.css';
55
import { EyeClosed, Telescope } from '@osrd-project/ui-icons';
66
import type { Meta, StoryObj } from '@storybook/react';
77

88
import { SAMPLE_PATH_PROPERTIES_DATA } from './assets/sampleData';
9-
import Menu, { type MenuItem } from './components/Menu';
109
import Manchette from '../components/Manchette';
11-
import { type StyledOperationalPointType } from '../types';
10+
import type { WaypointMenuItem, StyledOperationalPointType } from '../types';
1211

13-
const OperationalPointListData: StyledOperationalPointType[] =
12+
const operationalPointListData: StyledOperationalPointType[] =
1413
SAMPLE_PATH_PROPERTIES_DATA.operational_points?.map((op) => ({ ...op, display: true })) ?? [];
1514

1615
const meta: Meta<typeof Manchette> = {
@@ -33,59 +32,47 @@ const meta: Meta<typeof Manchette> = {
3332
};
3433

3534
const ManchetteWithWaypointMenu = () => {
36-
const [menuPosition, setMenuPosition] = useState<{ top: number; left: number } | null>(null);
3735
const [activeOperationalPointId, setActiveOperationalPointId] = useState<string>();
3836

39-
const menuRef = useRef<HTMLDivElement>(null);
40-
41-
const menuItems: MenuItem[] = [
37+
const menuItems: WaypointMenuItem[] = [
4238
{
4339
title: 'Action 1',
4440
icon: <EyeClosed />,
45-
onClick: () => {
46-
setMenuPosition(null);
41+
onClick: (e: React.MouseEvent) => {
42+
e.stopPropagation();
4743
setActiveOperationalPointId(undefined);
4844
},
4945
},
5046
{
5147
title: 'Action 2',
5248
icon: <Telescope />,
53-
onClick: () => {
54-
setMenuPosition(null);
49+
onClick: (e: React.MouseEvent) => {
50+
e.stopPropagation();
5551
setActiveOperationalPointId(undefined);
5652
},
5753
},
5854
];
5955

60-
const handleWaypointClick = (id: string, ref: HTMLDivElement | null) => {
61-
if (!ref) return;
62-
const position = ref.getBoundingClientRect();
63-
setMenuPosition({ top: position.bottom - 2, left: position.left });
56+
const handleWaypointClick = (id: string) => {
6457
setActiveOperationalPointId(id);
6558
};
6659

6760
return (
6861
<Manchette
69-
operationalPoints={OperationalPointListData.map((op) => ({
62+
operationalPoints={operationalPointListData.map((op) => ({
7063
...op,
71-
onClick: (id, ref) => {
72-
handleWaypointClick(id, ref);
73-
},
64+
onClick: handleWaypointClick,
7465
}))}
7566
zoomYIn={() => {}}
7667
zoomYOut={() => {}}
7768
resetZoom={() => {}}
7869
toggleMode={() => {}}
79-
activeOperationalPointId={activeOperationalPointId}
80-
>
81-
{menuPosition && (
82-
<Menu
83-
menuRef={menuRef}
84-
items={menuItems}
85-
style={{ width: '305px', top: menuPosition.top, left: menuPosition.left }}
86-
/>
87-
)}
88-
</Manchette>
70+
waypointMenuData={{
71+
activeOperationalPointId,
72+
waypointMenuItems: menuItems,
73+
waypointMenuClassName: 'menu',
74+
}}
75+
/>
8976
);
9077
};
9178

@@ -94,7 +81,7 @@ type Story = StoryObj<typeof ManchetteWithWaypointMenu>;
9481

9582
export const Default: Story = {
9683
args: {
97-
operationalPoints: OperationalPointListData,
84+
operationalPoints: operationalPointListData,
9885
},
9986
};
10087

ui-manchette/src/stories/components/Menu.tsx

-26
This file was deleted.

ui-manchette/src/styles/manchette.css

+39-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,44 @@
11
.manchette-container {
2+
.menu {
3+
position: absolute;
4+
z-index: 10;
5+
@apply bg-grey-5;
6+
width: 305px;
7+
top: 30px;
8+
left: 0;
9+
border: 0.0625rem solid #b6b2af;
10+
border-radius: 0.5rem;
11+
box-shadow:
12+
0 0.375rem 1.3125rem -0.3125rem rgba(101, 169, 232, 0.341810533216783),
13+
0 1rem 1.875rem -0.3125rem rgba(0, 0, 0, 0.2),
14+
0 0.1875rem 0.3125rem -0.125rem rgba(0, 0, 0, 0.1),
15+
inset 0 0 0 0.0625rem #ffffff;
16+
17+
/* .menu-item { */
18+
button {
19+
width: 100%;
20+
height: 2.75rem;
21+
font-size: 0.875rem;
22+
display: flex;
23+
align-items: center;
24+
25+
&:not(:first-child) {
26+
border-top: 0.0625rem solid #ffffff;
27+
}
28+
29+
&:not(:only-of-type, :last-child) {
30+
border-bottom: 0.0625rem solid #d3d1cf;
31+
}
32+
33+
/* .icon { */
34+
span:first-child {
35+
@apply text-primary-40;
36+
padding-inline: 1rem;
37+
}
38+
}
39+
}
240
.manchette-actions {
41+
z-index: 11;
342
align-items: center;
443
background-color: rgba(250, 249, 245, 0.6);
544
border-top: solid 1px;
@@ -16,7 +55,6 @@
1655
backdrop-filter: blur(8px);
1756
-webkit-backdrop-filter: blur(8px);
1857

19-
2058
.zoom-buttons {
2159
display: flex;
2260
align-items: center;

ui-manchette/src/styles/menu.css

-36
This file was deleted.

ui-manchette/src/types.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,19 @@ export type OperationalPointType = ArrayElement<PathProperties['operational_poin
5151
export type StyledOperationalPointType = OperationalPointType & {
5252
styles?: CSSProperties;
5353
display?: boolean;
54-
onClick?: (opId: string, opRef: HTMLDivElement | null) => void;
54+
onClick?: (opId: string) => void;
55+
};
56+
57+
export type WaypointMenuData = {
58+
activeOperationalPointId?: string;
59+
waypointMenuItems?: WaypointMenuItem[];
60+
waypointMenuClassName?: string;
61+
};
62+
63+
export type WaypointMenuItem = {
64+
title: string;
65+
icon: React.ReactNode;
66+
onClick: (e: React.MouseEvent) => void;
5567
};
5668

5769
export type ProjectPathTrainResult = {

0 commit comments

Comments
 (0)