@agregio-solutions/design-system 1.98.0 → 1.100.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/design-system.cjs +342 -338
- package/dist/design-system.js +3860 -3777
- package/dist/packages/components/Icon/Icon.d.ts +2 -0
- package/dist/packages/components/Icon/components/power_off.d.ts +2 -0
- package/dist/packages/components/Timeline/Timeline.d.ts +14 -2
- package/dist/packages/components/Timeline/components/BlockLineContainer/BlockLineContainer.styled.d.ts +8 -0
- package/dist/packages/components/Timeline/components/BlockLineContainer/utils/assignBlocksToLanes/assignBlocksToLanes.d.ts +18 -0
- package/dist/packages/components/Timeline/components/BlockLineContainer/utils/assignBlocksToLanes/assignBlocksToLanes.test.d.ts +1 -0
- package/dist/packages/components/Timeline/components/BlockLineContainer/utils/getLineHeight/getLineHeight.d.ts +2 -2
- package/dist/packages/components/Timeline/components/GroupLineContainer/GroupLineContainer.d.ts +10 -1
- package/dist/packages/components/Timeline/doc.md +468 -1
- package/dist/packages/components/Timeline/utils/Timeline.types.d.ts +5 -1
- package/package.json +1 -1
|
@@ -125,6 +125,7 @@ import { default as phone_2_full } from './components/phone_2_full';
|
|
|
125
125
|
import { default as play } from './components/play';
|
|
126
126
|
import { default as play_empty } from './components/play_empty';
|
|
127
127
|
import { default as play_full } from './components/play_full';
|
|
128
|
+
import { default as power_off } from './components/power_off';
|
|
128
129
|
import { default as profile } from './components/profile';
|
|
129
130
|
import { default as profile_full } from './components/profile_full';
|
|
130
131
|
import { default as progress } from './components/progress';
|
|
@@ -313,6 +314,7 @@ export declare const AVAILABLE_ICONS: {
|
|
|
313
314
|
play_empty: typeof play_empty;
|
|
314
315
|
play_full: typeof play_full;
|
|
315
316
|
play: typeof play;
|
|
317
|
+
power_off: typeof power_off;
|
|
316
318
|
profile_full: typeof profile_full;
|
|
317
319
|
profile: typeof profile;
|
|
318
320
|
progress: typeof progress;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Line } from './utils/Timeline.types';
|
|
1
|
+
import { ExpandedGroups, Line } from './utils/Timeline.types';
|
|
2
2
|
import { CSSProperties, ReactNode } from 'react';
|
|
3
3
|
export type TimelineProps = {
|
|
4
4
|
/**
|
|
@@ -17,6 +17,18 @@ export type TimelineProps = {
|
|
|
17
17
|
* The configuration of the timeline to display in this line
|
|
18
18
|
*/
|
|
19
19
|
emptyPlaceholder?: ReactNode;
|
|
20
|
+
/**
|
|
21
|
+
* Expanded state of the group lines, keyed by group name (controlled mode).
|
|
22
|
+
* When provided, every group line is controlled and a name missing from the
|
|
23
|
+
* map reads as collapsed. Leave it out to let each group keep its own
|
|
24
|
+
* internal state. Group names must be unique for this to be unambiguous.
|
|
25
|
+
*/
|
|
26
|
+
expandedGroups?: ExpandedGroups;
|
|
27
|
+
/**
|
|
28
|
+
* Called with the complete next map when a group line is expanded or
|
|
29
|
+
* collapsed, so it can be passed straight to a state setter.
|
|
30
|
+
*/
|
|
31
|
+
onExpandedGroupsChange?: (expandedGroups: ExpandedGroups) => void;
|
|
20
32
|
/**
|
|
21
33
|
* The class name of the timeline
|
|
22
34
|
*/
|
|
@@ -26,4 +38,4 @@ export type TimelineProps = {
|
|
|
26
38
|
*/
|
|
27
39
|
style?: CSSProperties;
|
|
28
40
|
};
|
|
29
|
-
export default function Timeline({ emptyPlaceholder, lines, startDate, endDate, ...props }: TimelineProps): import("react").JSX.Element;
|
|
41
|
+
export default function Timeline({ emptyPlaceholder, lines, startDate, endDate, expandedGroups, onExpandedGroupsChange, ...props }: TimelineProps): import("react").JSX.Element;
|
|
@@ -10,6 +10,14 @@ export declare const GroupContainer: import('@emotion/styled').StyledComponent<{
|
|
|
10
10
|
theme?: import('@emotion/react').Theme;
|
|
11
11
|
as?: React.ElementType;
|
|
12
12
|
}, import('react').DetailedHTMLProps<import('react').HTMLAttributes<HTMLDivElement>, HTMLDivElement>, {}>;
|
|
13
|
+
/**
|
|
14
|
+
* One visual row of an overlap group. Its blocks are absolutely positioned
|
|
15
|
+
* inside it, so blocks that don't overlap can share a row.
|
|
16
|
+
*/
|
|
17
|
+
export declare const LaneContainer: import('@emotion/styled').StyledComponent<{
|
|
18
|
+
theme?: import('@emotion/react').Theme;
|
|
19
|
+
as?: React.ElementType;
|
|
20
|
+
}, import('react').DetailedHTMLProps<import('react').HTMLAttributes<HTMLDivElement>, HTMLDivElement>, {}>;
|
|
13
21
|
export declare const Overlay: import('@emotion/styled').StyledComponent<{
|
|
14
22
|
theme?: import('@emotion/react').Theme;
|
|
15
23
|
as?: React.ElementType;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Block } from '../../../../utils/Timeline.types';
|
|
2
|
+
type Lane = Array<Block>;
|
|
3
|
+
/**
|
|
4
|
+
* Packs the blocks of one overlap group into lanes (the visual rows of a line).
|
|
5
|
+
*
|
|
6
|
+
* Blocks sharing a color share a lane as long as they don't overlap each other.
|
|
7
|
+
* The color carries the entity identity in a Timeline, so an entity reads as a
|
|
8
|
+
* single row instead of being spread over several rows by an unrelated block
|
|
9
|
+
* that happens to bridge two of its periods. A color that overlaps itself
|
|
10
|
+
* consumes as many lanes as needed, so a block is never hidden behind another.
|
|
11
|
+
*
|
|
12
|
+
* Lane order is derived from the data alone — earliest start of the color, then
|
|
13
|
+
* the color string — never from the input order: consumers refetch on an
|
|
14
|
+
* interval, and an insertion-ordered layout would make the rows jump on every
|
|
15
|
+
* refresh.
|
|
16
|
+
*/
|
|
17
|
+
export declare function assignBlocksToLanes(blocks: Array<Block>): Array<Lane>;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
type GetLineHeightParams = {
|
|
2
|
-
|
|
2
|
+
nbOfLanes: number;
|
|
3
3
|
isCollapsed: boolean;
|
|
4
4
|
indentLevel: number;
|
|
5
5
|
};
|
|
6
|
-
export default function getLineHeight({
|
|
6
|
+
export default function getLineHeight({ nbOfLanes, isCollapsed, indentLevel, }: GetLineHeightParams): number;
|
|
7
7
|
export {};
|
package/dist/packages/components/Timeline/components/GroupLineContainer/GroupLineContainer.d.ts
CHANGED
|
@@ -5,6 +5,15 @@ interface Props {
|
|
|
5
5
|
lineStartDate: Block["startDate"];
|
|
6
6
|
lineEndDate: Block["endDate"];
|
|
7
7
|
groupHoverText?: GroupHoverTextFormatter;
|
|
8
|
+
/**
|
|
9
|
+
* Whether the group is expanded (controlled). Leave it undefined to let the
|
|
10
|
+
* group keep its own internal state.
|
|
11
|
+
*/
|
|
12
|
+
isExpanded?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Called with the next expanded state whenever the group name is clicked.
|
|
15
|
+
*/
|
|
16
|
+
onExpandedChange?: (isExpanded: boolean) => void;
|
|
8
17
|
}
|
|
9
|
-
export default function GroupLineContainer({ name, subLines, lineStartDate, lineEndDate, groupHoverText, }: Props): import("react").JSX.Element;
|
|
18
|
+
export default function GroupLineContainer({ name, subLines, lineStartDate, lineEndDate, groupHoverText, isExpanded: controlledIsExpanded, onExpandedChange, }: Props): import("react").JSX.Element;
|
|
10
19
|
export {};
|
|
@@ -15,7 +15,8 @@ Base stories:
|
|
|
15
15
|
```tsx
|
|
16
16
|
import { Meta, StoryObj } from "@storybook/react-vite";
|
|
17
17
|
import Timeline from "./Timeline";
|
|
18
|
-
import {
|
|
18
|
+
import { Block } from "./utils/Timeline.types";
|
|
19
|
+
import { within, userEvent, expect, fn } from "storybook/test";
|
|
19
20
|
import MockDate from "mockdate";
|
|
20
21
|
import {
|
|
21
22
|
expectNotPresent,
|
|
@@ -505,6 +506,11 @@ export const GroupSimple: Story = {
|
|
|
505
506
|
await canvas.findByText("Installation 1 PRM_1");
|
|
506
507
|
await canvas.findByTestId("Installation 1 PRM_1 - Indisponibilité 1");
|
|
507
508
|
await canvas.findByTestId("Installation 1 PRM_1 - Activation 1");
|
|
509
|
+
|
|
510
|
+
// Clicking again collapses it back: the group holds its own state when the
|
|
511
|
+
// consumer doesn't control it.
|
|
512
|
+
await user.click(canvas.getByText("Installation 1"));
|
|
513
|
+
await expectNotPresent(() => canvas.queryByText("Installation 1 PRM_1"));
|
|
508
514
|
},
|
|
509
515
|
};
|
|
510
516
|
|
|
@@ -970,6 +976,449 @@ export const GroupFullOverlap: Story = {
|
|
|
970
976
|
await user.click(canvas.getByText("Maison Dieu - PRM[50014414454079]"));
|
|
971
977
|
},
|
|
972
978
|
};
|
|
979
|
+
|
|
980
|
+
const DAY_START = new Date("2024-01-01T00:00:00.000Z");
|
|
981
|
+
const DAY_END = new Date("2024-01-02T00:00:00.000Z");
|
|
982
|
+
|
|
983
|
+
/**
|
|
984
|
+
* Names the merged blocks of a collapsed group row after the block they were
|
|
985
|
+
* merged from, so the stories can target them by `data-testid`. Using the id
|
|
986
|
+
* rather than a formatted date keeps the testids timezone-independent.
|
|
987
|
+
*/
|
|
988
|
+
const groupHoverTextFromFirstBlockId = ({ blocks }: { blocks: Array<Block> }) =>
|
|
989
|
+
blocks[0].id;
|
|
990
|
+
|
|
991
|
+
export const GroupCollapsedSameAggregateTwice: Story = {
|
|
992
|
+
args: {
|
|
993
|
+
startDate: DAY_START,
|
|
994
|
+
endDate: DAY_END,
|
|
995
|
+
lines: [
|
|
996
|
+
{
|
|
997
|
+
type: "group",
|
|
998
|
+
name: "PSN",
|
|
999
|
+
groupHoverText: groupHoverTextFromFirstBlockId,
|
|
1000
|
+
subLines: [
|
|
1001
|
+
{
|
|
1002
|
+
// The reported bug: one aggregate, two solicitations separated by a
|
|
1003
|
+
// 1h30 gap. They must read as a single row.
|
|
1004
|
+
name: "Agrégat A",
|
|
1005
|
+
blocks: [
|
|
1006
|
+
{
|
|
1007
|
+
id: "agregat-a-matin",
|
|
1008
|
+
startDate: new Date("2024-01-01T11:20:00.000Z"),
|
|
1009
|
+
endDate: new Date("2024-01-01T13:00:00.000Z"),
|
|
1010
|
+
color: "var(--color-content-dataviz-purple-3)",
|
|
1011
|
+
hoverText: "Agrégat A - matin",
|
|
1012
|
+
},
|
|
1013
|
+
{
|
|
1014
|
+
id: "agregat-a-apres-midi",
|
|
1015
|
+
startDate: new Date("2024-01-01T14:30:00.000Z"),
|
|
1016
|
+
endDate: new Date("2024-01-01T16:25:00.000Z"),
|
|
1017
|
+
color: "var(--color-content-dataviz-purple-3)",
|
|
1018
|
+
hoverText: "Agrégat A - après-midi",
|
|
1019
|
+
},
|
|
1020
|
+
],
|
|
1021
|
+
},
|
|
1022
|
+
{
|
|
1023
|
+
// Long blocks that bridge the two purple ones into a single overlap
|
|
1024
|
+
// group — the actual cause of the bug.
|
|
1025
|
+
name: "Agrégat B",
|
|
1026
|
+
blocks: [
|
|
1027
|
+
{
|
|
1028
|
+
id: "agregat-b",
|
|
1029
|
+
startDate: new Date("2024-01-01T10:45:00.000Z"),
|
|
1030
|
+
endDate: new Date("2024-01-01T16:20:00.000Z"),
|
|
1031
|
+
color: "var(--color-content-dataviz-azure-3)",
|
|
1032
|
+
hoverText: "Agrégat B",
|
|
1033
|
+
},
|
|
1034
|
+
],
|
|
1035
|
+
},
|
|
1036
|
+
{
|
|
1037
|
+
name: "Agrégat C",
|
|
1038
|
+
blocks: [
|
|
1039
|
+
{
|
|
1040
|
+
id: "agregat-c",
|
|
1041
|
+
startDate: new Date("2024-01-01T11:00:00.000Z"),
|
|
1042
|
+
endDate: new Date("2024-01-01T16:20:00.000Z"),
|
|
1043
|
+
color: "var(--color-content-dataviz-pink-3)",
|
|
1044
|
+
hoverText: "Agrégat C",
|
|
1045
|
+
},
|
|
1046
|
+
],
|
|
1047
|
+
},
|
|
1048
|
+
{
|
|
1049
|
+
name: "Agrégat D",
|
|
1050
|
+
blocks: [
|
|
1051
|
+
{
|
|
1052
|
+
id: "agregat-d",
|
|
1053
|
+
startDate: new Date("2024-01-01T10:45:00.000Z"),
|
|
1054
|
+
endDate: new Date("2024-01-01T16:35:00.000Z"),
|
|
1055
|
+
color: "var(--color-content-dataviz-ocean-3)",
|
|
1056
|
+
hoverText: "Agrégat D",
|
|
1057
|
+
},
|
|
1058
|
+
],
|
|
1059
|
+
},
|
|
1060
|
+
],
|
|
1061
|
+
},
|
|
1062
|
+
],
|
|
1063
|
+
},
|
|
1064
|
+
play: async ({ canvasElement, mount }) => {
|
|
1065
|
+
MockDate.set("2024-01-01T12:00:00.000Z");
|
|
1066
|
+
await mount();
|
|
1067
|
+
const canvas = within(canvasElement);
|
|
1068
|
+
|
|
1069
|
+
const morning = canvas.getByTestId("agregat-a-matin");
|
|
1070
|
+
const afternoon = canvas.getByTestId("agregat-a-apres-midi");
|
|
1071
|
+
|
|
1072
|
+
// Both belong to the same overlap group, bridged by the long blocks…
|
|
1073
|
+
await expect(morning).toHaveAttribute("data-group-index", "0");
|
|
1074
|
+
await expect(afternoon).toHaveAttribute("data-group-index", "0");
|
|
1075
|
+
|
|
1076
|
+
// …and they are two distinct blocks, not a single merged one: the 1h30 gap
|
|
1077
|
+
// between 13:00 and 14:30 stays visible.
|
|
1078
|
+
await expect(morning.getAttribute("data-start")).not.toBe(
|
|
1079
|
+
afternoon.getAttribute("data-start"),
|
|
1080
|
+
);
|
|
1081
|
+
|
|
1082
|
+
// The fix: one lane for the aggregate, instead of one row per solicitation.
|
|
1083
|
+
await expect(morning).toHaveAttribute("data-lane-index", "3");
|
|
1084
|
+
await expect(afternoon).toHaveAttribute("data-lane-index", "3");
|
|
1085
|
+
|
|
1086
|
+
// Lanes are ordered by the earliest start of their color, then by the color
|
|
1087
|
+
// string: azure and ocean both start at 10:45, azure wins the tie.
|
|
1088
|
+
await expect(canvas.getByTestId("agregat-b")).toHaveAttribute(
|
|
1089
|
+
"data-lane-index",
|
|
1090
|
+
"0",
|
|
1091
|
+
);
|
|
1092
|
+
await expect(canvas.getByTestId("agregat-d")).toHaveAttribute(
|
|
1093
|
+
"data-lane-index",
|
|
1094
|
+
"1",
|
|
1095
|
+
);
|
|
1096
|
+
await expect(canvas.getByTestId("agregat-c")).toHaveAttribute(
|
|
1097
|
+
"data-lane-index",
|
|
1098
|
+
"2",
|
|
1099
|
+
);
|
|
1100
|
+
},
|
|
1101
|
+
};
|
|
1102
|
+
|
|
1103
|
+
export const GroupExpandedLanePacking: Story = {
|
|
1104
|
+
args: {
|
|
1105
|
+
startDate: DAY_START,
|
|
1106
|
+
endDate: DAY_END,
|
|
1107
|
+
lines: [
|
|
1108
|
+
{
|
|
1109
|
+
type: "group",
|
|
1110
|
+
name: "PSN",
|
|
1111
|
+
groupHoverText: groupHoverTextFromFirstBlockId,
|
|
1112
|
+
subLines: [
|
|
1113
|
+
{
|
|
1114
|
+
name: "Agrégat A",
|
|
1115
|
+
blocks: [
|
|
1116
|
+
// Same color, disjoint: they share a lane even on a sub-line.
|
|
1117
|
+
{
|
|
1118
|
+
id: "purple-matin",
|
|
1119
|
+
startDate: new Date("2024-01-01T10:00:00.000Z"),
|
|
1120
|
+
endDate: new Date("2024-01-01T12:00:00.000Z"),
|
|
1121
|
+
color: "var(--color-content-dataviz-purple-3)",
|
|
1122
|
+
hoverText: "Sub-line - purple matin",
|
|
1123
|
+
},
|
|
1124
|
+
{
|
|
1125
|
+
id: "purple-apres-midi",
|
|
1126
|
+
startDate: new Date("2024-01-01T12:30:00.000Z"),
|
|
1127
|
+
endDate: new Date("2024-01-01T14:00:00.000Z"),
|
|
1128
|
+
color: "var(--color-content-dataviz-purple-3)",
|
|
1129
|
+
hoverText: "Sub-line - purple après-midi",
|
|
1130
|
+
},
|
|
1131
|
+
// Three more colors, so the sub-line needs four lanes and crosses
|
|
1132
|
+
// its own threshold of three.
|
|
1133
|
+
{
|
|
1134
|
+
id: "azure",
|
|
1135
|
+
startDate: new Date("2024-01-01T10:00:00.000Z"),
|
|
1136
|
+
endDate: new Date("2024-01-01T14:00:00.000Z"),
|
|
1137
|
+
color: "var(--color-content-dataviz-azure-3)",
|
|
1138
|
+
hoverText: "Sub-line - azure",
|
|
1139
|
+
},
|
|
1140
|
+
{
|
|
1141
|
+
id: "ocean",
|
|
1142
|
+
startDate: new Date("2024-01-01T10:00:00.000Z"),
|
|
1143
|
+
endDate: new Date("2024-01-01T14:00:00.000Z"),
|
|
1144
|
+
color: "var(--color-content-dataviz-ocean-3)",
|
|
1145
|
+
hoverText: "Sub-line - ocean",
|
|
1146
|
+
},
|
|
1147
|
+
{
|
|
1148
|
+
id: "pink",
|
|
1149
|
+
startDate: new Date("2024-01-01T10:00:00.000Z"),
|
|
1150
|
+
endDate: new Date("2024-01-01T14:00:00.000Z"),
|
|
1151
|
+
color: "var(--color-content-dataviz-pink-3)",
|
|
1152
|
+
hoverText: "Sub-line - pink",
|
|
1153
|
+
},
|
|
1154
|
+
],
|
|
1155
|
+
},
|
|
1156
|
+
{
|
|
1157
|
+
name: "Agrégat B",
|
|
1158
|
+
blocks: [
|
|
1159
|
+
{
|
|
1160
|
+
id: "brown",
|
|
1161
|
+
startDate: new Date("2024-01-01T11:00:00.000Z"),
|
|
1162
|
+
endDate: new Date("2024-01-01T13:00:00.000Z"),
|
|
1163
|
+
color: "var(--color-content-dataviz-brown-3)",
|
|
1164
|
+
hoverText: "Sub-line - brown",
|
|
1165
|
+
},
|
|
1166
|
+
],
|
|
1167
|
+
},
|
|
1168
|
+
],
|
|
1169
|
+
},
|
|
1170
|
+
],
|
|
1171
|
+
},
|
|
1172
|
+
play: async ({ canvasElement, mount }) => {
|
|
1173
|
+
MockDate.set("2024-01-01T12:00:00.000Z");
|
|
1174
|
+
await mount();
|
|
1175
|
+
const canvas = within(canvasElement);
|
|
1176
|
+
const user = userEvent.setup();
|
|
1177
|
+
|
|
1178
|
+
await user.click(canvas.getByText("PSN"));
|
|
1179
|
+
await canvas.findByText("Agrégat A");
|
|
1180
|
+
|
|
1181
|
+
// A sub-line collapses at three lanes, not five. Purple sorts last, so it
|
|
1182
|
+
// is the lane that gets hidden.
|
|
1183
|
+
await expect(canvas.getByTestId("Sub-line - azure")).toHaveAttribute(
|
|
1184
|
+
"data-lane-index",
|
|
1185
|
+
"0",
|
|
1186
|
+
);
|
|
1187
|
+
await expect(canvas.getByTestId("Sub-line - ocean")).toHaveAttribute(
|
|
1188
|
+
"data-lane-index",
|
|
1189
|
+
"1",
|
|
1190
|
+
);
|
|
1191
|
+
await expect(canvas.getByTestId("Sub-line - pink")).toHaveAttribute(
|
|
1192
|
+
"data-lane-index",
|
|
1193
|
+
"2",
|
|
1194
|
+
);
|
|
1195
|
+
await expectNotPresent(() =>
|
|
1196
|
+
canvas.queryByTestId("Sub-line - purple matin"),
|
|
1197
|
+
);
|
|
1198
|
+
|
|
1199
|
+
await user.click(canvas.getByRole("button", { name: "Afficher plus" }));
|
|
1200
|
+
|
|
1201
|
+
// Once expanded, the two purple blocks share the fourth lane.
|
|
1202
|
+
await expect(canvas.getByTestId("Sub-line - purple matin")).toHaveAttribute(
|
|
1203
|
+
"data-lane-index",
|
|
1204
|
+
"3",
|
|
1205
|
+
);
|
|
1206
|
+
await expect(
|
|
1207
|
+
canvas.getByTestId("Sub-line - purple après-midi"),
|
|
1208
|
+
).toHaveAttribute("data-lane-index", "3");
|
|
1209
|
+
},
|
|
1210
|
+
};
|
|
1211
|
+
|
|
1212
|
+
const TWO_PERIODS = [
|
|
1213
|
+
{
|
|
1214
|
+
startDate: new Date("2024-01-01T10:00:00.000Z"),
|
|
1215
|
+
endDate: new Date("2024-01-01T14:00:00.000Z"),
|
|
1216
|
+
},
|
|
1217
|
+
{
|
|
1218
|
+
startDate: new Date("2024-01-01T15:00:00.000Z"),
|
|
1219
|
+
endDate: new Date("2024-01-01T19:00:00.000Z"),
|
|
1220
|
+
},
|
|
1221
|
+
];
|
|
1222
|
+
|
|
1223
|
+
export const GroupCollapsedExpandButtonCountsLanes: Story = {
|
|
1224
|
+
args: {
|
|
1225
|
+
startDate: DAY_START,
|
|
1226
|
+
endDate: DAY_END,
|
|
1227
|
+
lines: [
|
|
1228
|
+
{
|
|
1229
|
+
type: "group",
|
|
1230
|
+
name: "PSN",
|
|
1231
|
+
groupHoverText: groupHoverTextFromFirstBlockId,
|
|
1232
|
+
subLines: [
|
|
1233
|
+
// Seven blocks, but only five lanes: two aggregates are active over
|
|
1234
|
+
// two disjoint periods each. The collapse threshold is five at this
|
|
1235
|
+
// indent level, so counting blocks would truncate and show the expand
|
|
1236
|
+
// button, while counting lanes shows everything.
|
|
1237
|
+
{
|
|
1238
|
+
name: "Agrégat azure",
|
|
1239
|
+
blocks: [
|
|
1240
|
+
{
|
|
1241
|
+
id: "azure",
|
|
1242
|
+
startDate: new Date("2024-01-01T10:00:00.000Z"),
|
|
1243
|
+
endDate: new Date("2024-01-01T20:00:00.000Z"),
|
|
1244
|
+
color: "var(--color-content-dataviz-azure-3)",
|
|
1245
|
+
hoverText: "Agrégat azure",
|
|
1246
|
+
},
|
|
1247
|
+
],
|
|
1248
|
+
},
|
|
1249
|
+
{
|
|
1250
|
+
name: "Agrégat brown",
|
|
1251
|
+
blocks: TWO_PERIODS.map((period, index) => ({
|
|
1252
|
+
...period,
|
|
1253
|
+
id: `brown-${index + 1}`,
|
|
1254
|
+
color: "var(--color-content-dataviz-brown-3)",
|
|
1255
|
+
hoverText: `Agrégat brown ${index + 1}`,
|
|
1256
|
+
})),
|
|
1257
|
+
},
|
|
1258
|
+
{
|
|
1259
|
+
name: "Agrégat negative",
|
|
1260
|
+
blocks: TWO_PERIODS.map((period, index) => ({
|
|
1261
|
+
...period,
|
|
1262
|
+
id: `negative-${index + 1}`,
|
|
1263
|
+
color: "var(--color-content-dataviz-negative-3)",
|
|
1264
|
+
hoverText: `Agrégat negative ${index + 1}`,
|
|
1265
|
+
})),
|
|
1266
|
+
},
|
|
1267
|
+
{
|
|
1268
|
+
name: "Agrégat neutral",
|
|
1269
|
+
blocks: [
|
|
1270
|
+
{
|
|
1271
|
+
id: "neutral",
|
|
1272
|
+
startDate: new Date("2024-01-01T10:00:00.000Z"),
|
|
1273
|
+
endDate: new Date("2024-01-01T20:00:00.000Z"),
|
|
1274
|
+
color: "var(--color-content-dataviz-neutral-3)",
|
|
1275
|
+
hoverText: "Agrégat neutral",
|
|
1276
|
+
},
|
|
1277
|
+
],
|
|
1278
|
+
},
|
|
1279
|
+
{
|
|
1280
|
+
name: "Agrégat ocean",
|
|
1281
|
+
blocks: [
|
|
1282
|
+
{
|
|
1283
|
+
id: "ocean",
|
|
1284
|
+
startDate: new Date("2024-01-01T10:00:00.000Z"),
|
|
1285
|
+
endDate: new Date("2024-01-01T20:00:00.000Z"),
|
|
1286
|
+
color: "var(--color-content-dataviz-ocean-3)",
|
|
1287
|
+
hoverText: "Agrégat ocean",
|
|
1288
|
+
},
|
|
1289
|
+
],
|
|
1290
|
+
},
|
|
1291
|
+
],
|
|
1292
|
+
},
|
|
1293
|
+
],
|
|
1294
|
+
},
|
|
1295
|
+
play: async ({ canvasElement, mount }) => {
|
|
1296
|
+
MockDate.set("2024-01-01T12:00:00.000Z");
|
|
1297
|
+
await mount();
|
|
1298
|
+
const canvas = within(canvasElement);
|
|
1299
|
+
|
|
1300
|
+
// Five lanes for seven blocks: nothing is truncated, so no expand button.
|
|
1301
|
+
await expectNotPresent(() =>
|
|
1302
|
+
canvas.queryByRole("button", { name: "Afficher plus" }),
|
|
1303
|
+
);
|
|
1304
|
+
|
|
1305
|
+
// Every block stays visible, including the second period of each of the two
|
|
1306
|
+
// aggregates that have one.
|
|
1307
|
+
for (const id of [
|
|
1308
|
+
"azure",
|
|
1309
|
+
"brown-1",
|
|
1310
|
+
"brown-2",
|
|
1311
|
+
"negative-1",
|
|
1312
|
+
"negative-2",
|
|
1313
|
+
"neutral",
|
|
1314
|
+
"ocean",
|
|
1315
|
+
]) {
|
|
1316
|
+
await canvas.findByTestId(id);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
// All five colors start at 10:00, so the color string orders the lanes, and
|
|
1320
|
+
// each aggregate's two periods share its lane.
|
|
1321
|
+
await expect(canvas.getByTestId("azure")).toHaveAttribute(
|
|
1322
|
+
"data-lane-index",
|
|
1323
|
+
"0",
|
|
1324
|
+
);
|
|
1325
|
+
await expect(canvas.getByTestId("brown-1")).toHaveAttribute(
|
|
1326
|
+
"data-lane-index",
|
|
1327
|
+
"1",
|
|
1328
|
+
);
|
|
1329
|
+
await expect(canvas.getByTestId("brown-2")).toHaveAttribute(
|
|
1330
|
+
"data-lane-index",
|
|
1331
|
+
"1",
|
|
1332
|
+
);
|
|
1333
|
+
await expect(canvas.getByTestId("negative-1")).toHaveAttribute(
|
|
1334
|
+
"data-lane-index",
|
|
1335
|
+
"2",
|
|
1336
|
+
);
|
|
1337
|
+
await expect(canvas.getByTestId("negative-2")).toHaveAttribute(
|
|
1338
|
+
"data-lane-index",
|
|
1339
|
+
"2",
|
|
1340
|
+
);
|
|
1341
|
+
await expect(canvas.getByTestId("neutral")).toHaveAttribute(
|
|
1342
|
+
"data-lane-index",
|
|
1343
|
+
"3",
|
|
1344
|
+
);
|
|
1345
|
+
await expect(canvas.getByTestId("ocean")).toHaveAttribute(
|
|
1346
|
+
"data-lane-index",
|
|
1347
|
+
"4",
|
|
1348
|
+
);
|
|
1349
|
+
},
|
|
1350
|
+
};
|
|
1351
|
+
|
|
1352
|
+
export const GroupControlledExpansion: Story = {
|
|
1353
|
+
args: {
|
|
1354
|
+
startDate: new Date("2024-01-01T00:00:00.000Z"),
|
|
1355
|
+
endDate: new Date("2024-01-08T00:00:00.000Z"),
|
|
1356
|
+
// MA is expanded and PSN collapsed from the outside: the consumer owns the state
|
|
1357
|
+
// (e.g. to keep it across data changes that would otherwise remount the group).
|
|
1358
|
+
expandedGroups: { MA: true, PSN: false },
|
|
1359
|
+
onExpandedGroupsChange: fn(),
|
|
1360
|
+
lines: [
|
|
1361
|
+
{
|
|
1362
|
+
type: "group",
|
|
1363
|
+
name: "MA",
|
|
1364
|
+
subLines: [
|
|
1365
|
+
{
|
|
1366
|
+
name: "Agrégat MA 1",
|
|
1367
|
+
blocks: [
|
|
1368
|
+
{
|
|
1369
|
+
id: "ma-1",
|
|
1370
|
+
startDate: new Date("2024-01-02T00:00:00.000Z"),
|
|
1371
|
+
endDate: new Date("2024-01-04T00:00:00.000Z"),
|
|
1372
|
+
color: "var(--color-content-dataviz-azure-3)",
|
|
1373
|
+
hoverText: "Agrégat MA 1 - Sollicitation 1",
|
|
1374
|
+
},
|
|
1375
|
+
],
|
|
1376
|
+
},
|
|
1377
|
+
],
|
|
1378
|
+
},
|
|
1379
|
+
{
|
|
1380
|
+
type: "group",
|
|
1381
|
+
name: "PSN",
|
|
1382
|
+
subLines: [
|
|
1383
|
+
{
|
|
1384
|
+
name: "Agrégat PSN 1",
|
|
1385
|
+
blocks: [
|
|
1386
|
+
{
|
|
1387
|
+
id: "psn-1",
|
|
1388
|
+
startDate: new Date("2024-01-05T00:00:00.000Z"),
|
|
1389
|
+
endDate: new Date("2024-01-07T00:00:00.000Z"),
|
|
1390
|
+
color: "var(--color-content-dataviz-purple-3)",
|
|
1391
|
+
hoverText: "Agrégat PSN 1 - Sollicitation 1",
|
|
1392
|
+
},
|
|
1393
|
+
],
|
|
1394
|
+
},
|
|
1395
|
+
],
|
|
1396
|
+
},
|
|
1397
|
+
],
|
|
1398
|
+
},
|
|
1399
|
+
play: async ({ args, canvasElement, mount }) => {
|
|
1400
|
+
MockDate.set("2024-01-05T12:00:00.000Z");
|
|
1401
|
+
await mount();
|
|
1402
|
+
const canvas = within(canvasElement);
|
|
1403
|
+
const user = userEvent.setup();
|
|
1404
|
+
|
|
1405
|
+
// Expanded state comes from the prop only: no click needed for MA, and PSN stays
|
|
1406
|
+
// collapsed even though both groups render the exact same way.
|
|
1407
|
+
await canvas.findByText("Agrégat MA 1");
|
|
1408
|
+
await expectNotPresent(() => canvas.queryByText("Agrégat PSN 1"));
|
|
1409
|
+
|
|
1410
|
+
await user.click(canvas.getByText("MA"));
|
|
1411
|
+
|
|
1412
|
+
// The callback hands back the whole next map, ready for a state setter.
|
|
1413
|
+
expect(args.onExpandedGroupsChange).toHaveBeenCalledWith({
|
|
1414
|
+
MA: false,
|
|
1415
|
+
PSN: false,
|
|
1416
|
+
});
|
|
1417
|
+
// The story's args are static, so the row must stay expanded: the prop is the
|
|
1418
|
+
// single source of truth, the internal state never shadows it.
|
|
1419
|
+
expect(canvas.getByText("Agrégat MA 1")).toBeInTheDocument();
|
|
1420
|
+
},
|
|
1421
|
+
};
|
|
973
1422
|
```
|
|
974
1423
|
|
|
975
1424
|
## Developer notes
|
|
@@ -1084,6 +1533,24 @@ Give `groupHoverText` a stable identity (`useCallback`, or defined outside the c
|
|
|
1084
1533
|
|
|
1085
1534
|
Clicking the group name **expands** it, revealing each sub-line on its own indented row. Clicking again collapses it back.
|
|
1086
1535
|
|
|
1536
|
+
By default each group owns that state internally. It is therefore lost whenever the group unmounts — which happens more often than it looks, since removing a line shifts its siblings' React keys. Pass `expandedGroups` (a `Record<groupName, boolean>`) and `onExpandedGroupsChange` to hold it yourself instead:
|
|
1537
|
+
|
|
1538
|
+
```tsx
|
|
1539
|
+
const [expandedGroups, setExpandedGroups] = useState({ MA: true });
|
|
1540
|
+
|
|
1541
|
+
<Timeline
|
|
1542
|
+
lines={lines}
|
|
1543
|
+
startDate={startDate}
|
|
1544
|
+
endDate={endDate}
|
|
1545
|
+
expandedGroups={expandedGroups}
|
|
1546
|
+
onExpandedGroupsChange={setExpandedGroups}
|
|
1547
|
+
/>;
|
|
1548
|
+
```
|
|
1549
|
+
|
|
1550
|
+
`onExpandedGroupsChange` receives the complete next map, so it can be handed straight to a state setter. A group name missing from the map reads as collapsed, and the two props go together: providing `expandedGroups` without `onExpandedGroupsChange` freezes every group. Since the map is keyed by name, **group names must be unique**.
|
|
1551
|
+
|
|
1552
|
+
<Canvas of={Timeline.GroupControlledExpansion} />
|
|
1553
|
+
|
|
1087
1554
|
### Simple group (one sub-line)
|
|
1088
1555
|
|
|
1089
1556
|
<Canvas of={Timeline.GroupSimple} />
|
|
@@ -100,8 +100,12 @@ export type GroupLine = {
|
|
|
100
100
|
groupHoverText?: GroupHoverTextFormatter;
|
|
101
101
|
};
|
|
102
102
|
export type Line = SimpleLine | GroupLine;
|
|
103
|
+
/**
|
|
104
|
+
* Expanded state of the group lines, keyed by group line name.
|
|
105
|
+
*/
|
|
106
|
+
export type ExpandedGroups = Record<string, boolean>;
|
|
103
107
|
export declare function isGroupLine(line: Line): line is GroupLine;
|
|
104
108
|
export declare const NAMES_SECTION_WIDTH = 188;
|
|
105
109
|
export declare const lineHeightForIndentLevel: (indentLevel: number) => 40 | 58;
|
|
106
|
-
export declare const
|
|
110
|
+
export declare const maxLanesToDisplayBeforeCollapsingForIndentLevel: (indentLevel: number) => 3 | 5;
|
|
107
111
|
export {};
|
package/package.json
CHANGED