@agregio-solutions/design-system 1.97.1 → 1.99.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 +326 -322
- package/dist/design-system.js +4811 -4736
- 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 +12 -2
- package/dist/packages/components/Timeline/doc.md +530 -1
- package/dist/packages/components/Timeline/utils/Timeline.types.d.ts +32 -1
- package/dist/packages/components/Timeline/utils/mergeBlocksByColor/mergeBlocksByColor.d.ts +2 -2
- package/package.json +1 -1
|
@@ -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
|
@@ -1,9 +1,19 @@
|
|
|
1
|
-
import { Block, SubLine } from '../../utils/Timeline.types';
|
|
1
|
+
import { Block, GroupHoverTextFormatter, SubLine } from '../../utils/Timeline.types';
|
|
2
2
|
interface Props {
|
|
3
3
|
name: string;
|
|
4
4
|
subLines: Array<SubLine>;
|
|
5
5
|
lineStartDate: Block["startDate"];
|
|
6
6
|
lineEndDate: Block["endDate"];
|
|
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;
|
|
7
17
|
}
|
|
8
|
-
export default function GroupLineContainer({ name, subLines, lineStartDate, lineEndDate, }: Props): import("react").JSX.Element;
|
|
18
|
+
export default function GroupLineContainer({ name, subLines, lineStartDate, lineEndDate, groupHoverText, isExpanded: controlledIsExpanded, onExpandedChange, }: Props): import("react").JSX.Element;
|
|
9
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
|
|
|
@@ -704,6 +710,52 @@ export const GroupWithOverflow: Story = {
|
|
|
704
710
|
},
|
|
705
711
|
};
|
|
706
712
|
|
|
713
|
+
export const GroupCustomHoverText: Story = {
|
|
714
|
+
args: {
|
|
715
|
+
startDate: new Date("2024-01-01T00:00:00.000Z"),
|
|
716
|
+
endDate: new Date("2024-01-08T00:00:00.000Z"),
|
|
717
|
+
lines: [
|
|
718
|
+
{
|
|
719
|
+
type: "group",
|
|
720
|
+
name: "Installation 5",
|
|
721
|
+
groupHoverText: ({ blocks }) => `${blocks.length} indisponibilité(s)`,
|
|
722
|
+
subLines: [
|
|
723
|
+
{
|
|
724
|
+
name: "Installation 5 PRM_1",
|
|
725
|
+
blocks: [
|
|
726
|
+
{
|
|
727
|
+
id: "unavailability-1",
|
|
728
|
+
startDate: new Date("2024-01-01T00:00:00.000Z"),
|
|
729
|
+
endDate: new Date("2024-01-03T00:00:00.000Z"),
|
|
730
|
+
color: "var(--color-content-dataviz-pink-3)",
|
|
731
|
+
hoverText: "Installation 5 PRM_1 - Indisponibilité 1",
|
|
732
|
+
},
|
|
733
|
+
],
|
|
734
|
+
},
|
|
735
|
+
{
|
|
736
|
+
name: "Installation 5 PRM_2",
|
|
737
|
+
blocks: [
|
|
738
|
+
{
|
|
739
|
+
id: "unavailability-1",
|
|
740
|
+
startDate: new Date("2024-01-02T00:00:00.000Z"),
|
|
741
|
+
endDate: new Date("2024-01-04T00:00:00.000Z"),
|
|
742
|
+
color: "var(--color-content-dataviz-pink-3)",
|
|
743
|
+
hoverText: "Installation 5 PRM_2 - Indisponibilité 1",
|
|
744
|
+
},
|
|
745
|
+
],
|
|
746
|
+
},
|
|
747
|
+
],
|
|
748
|
+
},
|
|
749
|
+
],
|
|
750
|
+
},
|
|
751
|
+
play: async ({ canvasElement, mount }) => {
|
|
752
|
+
MockDate.set("2024-01-05T12:00:00.000Z");
|
|
753
|
+
await mount();
|
|
754
|
+
const canvas = within(canvasElement);
|
|
755
|
+
await canvas.findByTestId("2 indisponibilité(s)");
|
|
756
|
+
},
|
|
757
|
+
};
|
|
758
|
+
|
|
707
759
|
export const MixedSimpleAndGroup: Story = {
|
|
708
760
|
args: {
|
|
709
761
|
startDate: new Date("2024-01-01T00:00:00.000Z"),
|
|
@@ -924,6 +976,449 @@ export const GroupFullOverlap: Story = {
|
|
|
924
976
|
await user.click(canvas.getByText("Maison Dieu - PRM[50014414454079]"));
|
|
925
977
|
},
|
|
926
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
|
+
};
|
|
927
1422
|
```
|
|
928
1423
|
|
|
929
1424
|
## Developer notes
|
|
@@ -1018,10 +1513,44 @@ Use `type: "group"` with a `subLines` array instead of a `blocks` array.
|
|
|
1018
1513
|
|
|
1019
1514
|
When collapsed, the group displays a **merged view** of all sub-lines' blocks. Blocks sharing the same color are merged into a single block spanning their combined range. This gives a quick overview of activity across all sub-lines.
|
|
1020
1515
|
|
|
1516
|
+
By default, hovering a merged block shows its date range. Provide a `groupHoverText` function on the group line to customize this text. It receives the merged interval's `startDate`, `endDate`, `color`, and the original `blocks` that were merged into it:
|
|
1517
|
+
|
|
1518
|
+
```ts
|
|
1519
|
+
{
|
|
1520
|
+
type: "group",
|
|
1521
|
+
name: "Installation 3",
|
|
1522
|
+
groupHoverText: ({ startDate, endDate, blocks }) =>
|
|
1523
|
+
`${blocks.length} events between ${startDate} and ${endDate}`,
|
|
1524
|
+
subLines: [ /* ... */ ],
|
|
1525
|
+
}
|
|
1526
|
+
```
|
|
1527
|
+
|
|
1528
|
+
Give `groupHoverText` a stable identity (`useCallback`, or defined outside the component) — it's used as a dependency to memoize the merge computation, so a new function on every render defeats that memoization.
|
|
1529
|
+
|
|
1530
|
+
<Canvas of={Timeline.GroupCustomHoverText} />
|
|
1531
|
+
|
|
1021
1532
|
### Expanded state
|
|
1022
1533
|
|
|
1023
1534
|
Clicking the group name **expands** it, revealing each sub-line on its own indented row. Clicking again collapses it back.
|
|
1024
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
|
+
|
|
1025
1554
|
### Simple group (one sub-line)
|
|
1026
1555
|
|
|
1027
1556
|
<Canvas of={Timeline.GroupSimple} />
|