react-native-roster 0.0.1 → 0.2.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/README.md +209 -32
- package/dist/src/components/primitives/portal.d.ts +12 -0
- package/dist/src/components/primitives/portal.d.ts.map +1 -0
- package/dist/src/components/primitives/portal.js +41 -0
- package/dist/src/components/primitives/portal.js.map +1 -0
- package/dist/src/components/roster/body-layout.d.ts +9 -0
- package/dist/src/components/roster/body-layout.d.ts.map +1 -0
- package/dist/src/components/roster/body-layout.js +15 -0
- package/dist/src/components/roster/body-layout.js.map +1 -0
- package/dist/src/components/roster/index.d.ts +2 -1
- package/dist/src/components/roster/index.d.ts.map +1 -1
- package/dist/src/components/roster/index.js +34 -27
- package/dist/src/components/roster/index.js.map +1 -1
- package/dist/src/components/roster/lanes/lane.d.ts +2 -2
- package/dist/src/components/roster/lanes/lane.d.ts.map +1 -1
- package/dist/src/components/roster/lanes/lane.js +4 -8
- package/dist/src/components/roster/lanes/lane.js.map +1 -1
- package/dist/src/components/roster/parts/body.d.ts +3 -3
- package/dist/src/components/roster/parts/body.d.ts.map +1 -1
- package/dist/src/components/roster/parts/body.js +5 -20
- package/dist/src/components/roster/parts/body.js.map +1 -1
- package/dist/src/components/roster/parts/header-cell.d.ts +2 -4
- package/dist/src/components/roster/parts/header-cell.d.ts.map +1 -1
- package/dist/src/components/roster/parts/header-cell.js.map +1 -1
- package/dist/src/components/roster/parts/header.d.ts +1 -1
- package/dist/src/components/roster/parts/header.d.ts.map +1 -1
- package/dist/src/components/roster/parts/header.js +2 -2
- package/dist/src/components/roster/parts/header.js.map +1 -1
- package/dist/src/components/roster/parts/lane-label-column.d.ts +1 -1
- package/dist/src/components/roster/parts/lane-label-column.d.ts.map +1 -1
- package/dist/src/components/roster/parts/lane-label-column.js +2 -2
- package/dist/src/components/roster/parts/lane-label-column.js.map +1 -1
- package/dist/src/components/roster/parts/lane-list.d.ts +7 -0
- package/dist/src/components/roster/parts/lane-list.d.ts.map +1 -0
- package/dist/src/components/roster/parts/lane-list.js +26 -0
- package/dist/src/components/roster/parts/lane-list.js.map +1 -0
- package/dist/src/components/roster/roster.types.d.ts +48 -21
- package/dist/src/components/roster/roster.types.d.ts.map +1 -1
- package/dist/src/components/roster/selection/selection-layout.d.ts +4 -0
- package/dist/src/components/roster/selection/selection-layout.d.ts.map +1 -0
- package/dist/src/components/roster/selection/selection-layout.js +82 -0
- package/dist/src/components/roster/selection/selection-layout.js.map +1 -0
- package/dist/src/components/roster/selection/selection-layout.types.d.ts +25 -0
- package/dist/src/components/roster/selection/selection-layout.types.d.ts.map +1 -0
- package/dist/src/components/roster/selection/selection-layout.types.js +3 -0
- package/dist/src/components/roster/selection/selection-layout.types.js.map +1 -0
- package/dist/src/components/roster/selection/selection-layout.web.d.ts +4 -0
- package/dist/src/components/roster/selection/selection-layout.web.d.ts.map +1 -0
- package/dist/src/components/roster/selection/selection-layout.web.js +52 -0
- package/dist/src/components/roster/selection/selection-layout.web.js.map +1 -0
- package/dist/src/components/roster/use-roster-press.d.ts +6 -0
- package/dist/src/components/roster/use-roster-press.d.ts.map +1 -0
- package/dist/src/components/roster/use-roster-press.js +61 -0
- package/dist/src/components/roster/use-roster-press.js.map +1 -0
- package/dist/src/components/roster/use-roster.d.ts.map +1 -1
- package/dist/src/components/roster/use-roster.js +40 -30
- package/dist/src/components/roster/use-roster.js.map +1 -1
- package/dist/src/components/roster/utils/body-content-key.d.ts +1 -1
- package/dist/src/components/roster/utils/body-content-key.d.ts.map +1 -1
- package/dist/src/components/roster/utils/body-content-key.js +10 -10
- package/dist/src/components/roster/utils/body-content-key.js.map +1 -1
- package/dist/src/components/schedule/days/day.d.ts +2 -2
- package/dist/src/components/schedule/days/day.d.ts.map +1 -1
- package/dist/src/components/schedule/days/day.js +2 -11
- package/dist/src/components/schedule/days/day.js.map +1 -1
- package/dist/src/components/schedule/days/header-layout.d.ts +1 -1
- package/dist/src/components/schedule/days/header-layout.d.ts.map +1 -1
- package/dist/src/components/schedule/days/parts/column.d.ts +1 -1
- package/dist/src/components/schedule/days/parts/column.d.ts.map +1 -1
- package/dist/src/components/schedule/days/parts/column.js +4 -8
- package/dist/src/components/schedule/days/parts/column.js.map +1 -1
- package/dist/src/components/schedule/index.d.ts.map +1 -1
- package/dist/src/components/schedule/index.js +7 -8
- package/dist/src/components/schedule/index.js.map +1 -1
- package/dist/src/components/schedule/schedule.types.d.ts +17 -17
- package/dist/src/components/schedule/schedule.types.d.ts.map +1 -1
- package/dist/src/core/index.d.ts +1 -0
- package/dist/src/core/index.d.ts.map +1 -1
- package/dist/src/core/index.js +3 -1
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/core/source.d.ts +4 -0
- package/dist/src/core/source.d.ts.map +1 -0
- package/dist/src/core/source.js +9 -0
- package/dist/src/core/source.js.map +1 -0
- package/dist/src/index.d.ts +6 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +10 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/nativewind/index.d.ts +28 -27
- package/dist/src/nativewind/index.d.ts.map +1 -1
- package/docs/migrations.md +61 -0
- package/llms.txt +112 -30
- package/package.json +12 -5
- package/src/components/layers/README.md +7 -3
- package/src/components/primitives/README.md +13 -2
- package/src/components/primitives/portal.tsx +48 -0
- package/src/components/roster/README.md +62 -5
- package/src/components/roster/body-layout.tsx +38 -0
- package/src/components/roster/index.tsx +109 -46
- package/src/components/roster/lanes/lane.tsx +16 -13
- package/src/components/roster/parts/body.tsx +17 -84
- package/src/components/roster/parts/header-cell.tsx +2 -2
- package/src/components/roster/parts/header.tsx +7 -2
- package/src/components/roster/parts/lane-label-column.tsx +2 -2
- package/src/components/roster/parts/lane-list.tsx +76 -0
- package/src/components/roster/roster.types.ts +47 -17
- package/src/components/roster/selection/selection-layout.tsx +90 -0
- package/src/components/roster/selection/selection-layout.types.ts +20 -0
- package/src/components/roster/selection/selection-layout.web.tsx +88 -0
- package/src/components/roster/use-roster-press.ts +74 -0
- package/src/components/roster/use-roster.ts +57 -44
- package/src/components/roster/utils/body-content-key.ts +13 -13
- package/src/components/schedule/README.md +8 -3
- package/src/components/schedule/days/day.tsx +31 -18
- package/src/components/schedule/days/header-layout.tsx +1 -1
- package/src/components/schedule/days/parts/column.tsx +10 -12
- package/src/components/schedule/index.tsx +20 -21
- package/src/components/schedule/schedule.types.ts +13 -13
- package/src/core/README.md +4 -0
- package/src/core/index.ts +1 -0
- package/src/core/source.ts +9 -0
- package/src/index.ts +7 -0
|
@@ -2,17 +2,74 @@
|
|
|
2
2
|
|
|
3
3
|
This feature is about a roster; its children are lanes.
|
|
4
4
|
|
|
5
|
-
- Root entry exports: `Roster`, `useRoster`, the `RosterXxx` parts, and every
|
|
6
|
-
`XxxInput`
|
|
5
|
+
- Root entry exports: `Roster`, `useRoster`, `RosterBodyLayout`, `RosterLaneList`, the other `RosterXxx` parts, and every
|
|
6
|
+
`XxxInput` slot input type from `roster.types.ts`
|
|
7
7
|
|
|
8
8
|
- `index.tsx`: the chassis; calls `useRoster`, branches on `status`, composes zones
|
|
9
|
-
- `use-roster.ts`: the hook; owns window, projection, scroll, geometry, and
|
|
9
|
+
- `use-roster.ts`: the hook; owns window, projection, scroll, geometry, and selection
|
|
10
|
+
- `use-roster-press.ts`: isolates the stable press ref so React Compiler can retain derived body inputs
|
|
10
11
|
- `layout.tsx`: arranges corner, header, label column, and body regions only
|
|
11
|
-
- `roster.types.ts`: props, model, and every
|
|
12
|
+
- `roster.types.ts`: props, model, and every slot input type
|
|
13
|
+
- `body-layout.tsx`: arranges grid and list nodes with horizontal scroll wiring
|
|
14
|
+
- `parts/lane-list.tsx`: `RosterLaneList` owns LegendList and its lane render callback
|
|
12
15
|
- `parts/`: collection-level parts (body, header, header cell, grid, corner, label column, empty)
|
|
13
16
|
- `lanes/`: `LaneRow`, the interval-hover platform pair, and lane-local parts
|
|
14
17
|
- `utils/`: ticks and the body content key
|
|
15
18
|
|
|
16
|
-
Interval and gap
|
|
19
|
+
Interval and gap components live in `../layers`; press geometry and `regionStyle`
|
|
17
20
|
in `../primitives`. Zone contracts are documented in the README's Roster zones
|
|
18
21
|
section and in `llms.txt`. Tests: `test/components/roster`.
|
|
22
|
+
|
|
23
|
+
`RosterBody` gates
|
|
24
|
+
measurement and composes the body layout and lane list. The `onRowRender` prop on
|
|
25
|
+
`RosterBody` and `RosterLaneList` is a development-only Profiler hook used by the
|
|
26
|
+
gallery, not a supported customization point. Production Profiler callbacks are disabled.
|
|
27
|
+
|
|
28
|
+
Input-bearing slots accept component types and mount in the data-owning parts.
|
|
29
|
+
The chassis binds defaults once; emptyZone and cornerZone accept nodes. The body
|
|
30
|
+
content key tracks interval and gap component identity, including class components.
|
|
31
|
+
|
|
32
|
+
`selection/` holds the runtime-swappable presentation strategies (native popover, web popover) for one `SelectionLayoutProps` contract.
|
|
33
|
+
|
|
34
|
+
- `selection/selection-layout.types.ts`: exported `SelectionLayoutProps`, with body
|
|
35
|
+
and detail nodes, targetBounds, open, dismissal, host name, and shared scroll
|
|
36
|
+
limited to x, y, headerStyle, and labelStyle
|
|
37
|
+
- `selection/selection-layout.tsx`: exported native `RosterSelectionPopover`, local
|
|
38
|
+
portal host, body press passthrough, hardware back, measured viewport clamping, and shared scroll positioning
|
|
39
|
+
- `selection/selection-layout.web.tsx`: Radix presentation with a browser remap
|
|
40
|
+
|
|
41
|
+
`intervalDetailComponent` receives `IntervalDetailInput`, the interval input plus absolute
|
|
42
|
+
`start`, `end`, and `viewTimezone`; presses still invoke onIntervalPress.
|
|
43
|
+
`selectable?: boolean` belongs only to the hook input, `RosterInput`, and defaults to false.
|
|
44
|
+
It is excluded from `RosterProps`.
|
|
45
|
+
`intervalDetailComponent`, `selectionLayout`, and `portalHost` belong to `RosterProps`
|
|
46
|
+
because the chassis mounts the content and layout. The chassis passes
|
|
47
|
+
`selectable: Boolean(intervalDetailComponent)` to `useRoster`.
|
|
48
|
+
`selectionLayout` defaults once in the chassis and wraps the body
|
|
49
|
+
before RosterLayout receives bodyZone. The selection never enters the body content
|
|
50
|
+
key. Missing lane/layer/bounds clear selection; valid selections use current data.
|
|
51
|
+
Pressing the selected interval again dismisses it; pressing another interval
|
|
52
|
+
switches selection. Cell and gap presses dismiss selection while still firing
|
|
53
|
+
`onCellPress` and `onGapPress`. These rules live in the hook and apply on native
|
|
54
|
+
and web; web outside press and Escape still dismiss.
|
|
55
|
+
On web, the layout captures document.activeElement before opening autofocus and
|
|
56
|
+
recaptures the focused lane when targetBounds changes while open. It uses onCloseAutoFocus to return focus after Escape only, leaving outside pointer focus intact. The browser's focus-visible ring styling is left to the host page.
|
|
57
|
+
`portalHost` defaults to a per-roster useId name. The interval-detail fixture switches
|
|
58
|
+
to an inspector column with the same node contract. Schedule does not yet support selection.
|
|
59
|
+
|
|
60
|
+
The private reconcileSelection helper resolves the stored absolute bounds against the
|
|
61
|
+
current lanes and window each render, matching source identity sets and choosing the
|
|
62
|
+
nearest bounds with a total difference of at most 1 ms for projection roundoff. Target bounds
|
|
63
|
+
include the lane offset and interval inset. Native details clamp horizontally and use the
|
|
64
|
+
top edge when neither below nor above fits. The browser gate isolates selection updates from Pressable state,
|
|
65
|
+
and separately exercises complete pointer presses and outside dismissal.
|
|
66
|
+
|
|
67
|
+
Native has no intercepting dismissal overlay; body presses reach the hook, and the detail
|
|
68
|
+
card captures its own presses. Web excludes the body wrapper from Radix outside
|
|
69
|
+
dismissal, leaving body presses to the hook while preserving true outside presses and Escape.
|
|
70
|
+
The hook toggles the selected interval closed, switches to another interval, and dismisses
|
|
71
|
+
on cell or gap presses while preserving their callbacks. The body restores horizontal
|
|
72
|
+
and vertical offsets from shared values on remount when selectionLayout changes.
|
|
73
|
+
A mount effect calls the horizontal ScrollView ref's scrollTo without animation;
|
|
74
|
+
native also retains contentOffset. LegendList restores initialScrollOffset through
|
|
75
|
+
its own web mount effect and native initial offset.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type ReactNode, useEffect, useState } from 'react';
|
|
2
|
+
import { ScrollView, View } from 'react-native';
|
|
3
|
+
import type { BodyInput } from './roster.types';
|
|
4
|
+
|
|
5
|
+
export function RosterBodyLayout({
|
|
6
|
+
scroll,
|
|
7
|
+
contentWidth,
|
|
8
|
+
viewport,
|
|
9
|
+
gridZone,
|
|
10
|
+
listZone,
|
|
11
|
+
}: Pick<BodyInput, 'scroll' | 'contentWidth' | 'viewport'> & {
|
|
12
|
+
/** Noninteractive grid behind the lane list. */
|
|
13
|
+
gridZone: ReactNode;
|
|
14
|
+
/** Virtualized lane collection within the measured viewport. */
|
|
15
|
+
listZone: ReactNode;
|
|
16
|
+
}) {
|
|
17
|
+
const [contentOffset] = useState(() => ({ x: scroll.x.get(), y: 0 }));
|
|
18
|
+
const bodyRef = scroll.bodyRef;
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
bodyRef.current?.scrollTo({ x: contentOffset.x, animated: false });
|
|
21
|
+
}, [bodyRef, contentOffset]);
|
|
22
|
+
return (
|
|
23
|
+
<ScrollView
|
|
24
|
+
testID="roster-horizontal-scroll"
|
|
25
|
+
ref={scroll.bodyRef}
|
|
26
|
+
contentOffset={contentOffset}
|
|
27
|
+
horizontal
|
|
28
|
+
onScroll={scroll.onBodyScroll}
|
|
29
|
+
scrollEventThrottle={16}
|
|
30
|
+
style={{ flex: 1 }}
|
|
31
|
+
>
|
|
32
|
+
<View style={{ width: contentWidth, height: viewport.height }}>
|
|
33
|
+
{gridZone}
|
|
34
|
+
{listZone}
|
|
35
|
+
</View>
|
|
36
|
+
</ScrollView>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type ReactNode, useId } from 'react';
|
|
2
|
+
import { hasSource } from '../../core';
|
|
1
3
|
import { RosterGap } from '../layers/parts/gap';
|
|
2
4
|
import { RosterInterval } from '../layers/parts/interval';
|
|
3
5
|
import { RosterLaneLabel } from './lanes/parts/lane-label';
|
|
@@ -10,10 +12,17 @@ import { RosterHeader } from './parts/header';
|
|
|
10
12
|
import { RosterHeaderCell } from './parts/header-cell';
|
|
11
13
|
import { RosterLaneLabelColumn } from './parts/lane-label-column';
|
|
12
14
|
import type { RosterProps } from './roster.types';
|
|
15
|
+
import { RosterSelectionPopover } from './selection/selection-layout';
|
|
16
|
+
import type { SelectionLayoutProps } from './selection/selection-layout.types';
|
|
13
17
|
import { useRoster } from './use-roster';
|
|
14
18
|
|
|
19
|
+
const defaultEmptyZone = <RosterEmpty />;
|
|
20
|
+
const defaultCornerZone = <RosterCorner />;
|
|
21
|
+
|
|
15
22
|
export function Roster(props: RosterProps) {
|
|
16
23
|
const {
|
|
24
|
+
selection,
|
|
25
|
+
dismissSelection,
|
|
17
26
|
status,
|
|
18
27
|
orderedLanes,
|
|
19
28
|
laneState,
|
|
@@ -26,23 +35,47 @@ export function Roster(props: RosterProps) {
|
|
|
26
35
|
contentWidth,
|
|
27
36
|
viewport,
|
|
28
37
|
onLayout,
|
|
29
|
-
} = useRoster(props);
|
|
38
|
+
} = useRoster({ ...props, selectable: Boolean(props.intervalDetailComponent) });
|
|
30
39
|
const {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
intervalDetailComponent: IntervalDetailComponent,
|
|
41
|
+
selectionLayout: SelectionLayout = RosterSelectionPopover,
|
|
42
|
+
emptyZone = defaultEmptyZone,
|
|
43
|
+
cornerZone = defaultCornerZone,
|
|
44
|
+
headerComponent: HeaderComponent = RosterHeader,
|
|
45
|
+
laneLabelColumnComponent: LaneLabelColumnComponent = RosterLaneLabelColumn,
|
|
46
|
+
bodyComponent: BodyComponent = RosterBody,
|
|
47
|
+
headerCellComponent = RosterHeaderCell,
|
|
48
|
+
laneLabelComponent = RosterLaneLabel,
|
|
49
|
+
intervalComponent = RosterInterval,
|
|
50
|
+
gapComponent = RosterGap,
|
|
51
|
+
gridComponent = RosterGrid,
|
|
41
52
|
incompleteLabel = 'Availability may be incomplete',
|
|
42
53
|
neverSetLabel = 'No availability set',
|
|
43
54
|
} = props;
|
|
44
|
-
|
|
55
|
+
const hostId = useId();
|
|
56
|
+
if (status === 'empty') return emptyZone;
|
|
45
57
|
if (window.start === window.end) return null;
|
|
58
|
+
let targetBounds: SelectionLayoutProps['targetBounds'] = null;
|
|
59
|
+
if (selection) {
|
|
60
|
+
targetBounds = {
|
|
61
|
+
x: selection.rect.x,
|
|
62
|
+
y:
|
|
63
|
+
orderedLanes.findIndex((lane) => lane.id === selection.lane.id) * projection.rowHeight +
|
|
64
|
+
selection.rect.y,
|
|
65
|
+
width: selection.rect.width,
|
|
66
|
+
height: selection.rect.height,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
let contentZone: ReactNode = null;
|
|
70
|
+
if (selection && IntervalDetailComponent) {
|
|
71
|
+
contentZone = (
|
|
72
|
+
<IntervalDetailComponent
|
|
73
|
+
{...selection}
|
|
74
|
+
viewTimezone={projection.viewTimezone}
|
|
75
|
+
highlighted={hasSource(selection.rect.sources, props.highlightSource)}
|
|
76
|
+
/>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
46
79
|
return (
|
|
47
80
|
<RosterLayout
|
|
48
81
|
style={props.style}
|
|
@@ -51,40 +84,70 @@ export function Roster(props: RosterProps) {
|
|
|
51
84
|
bodyStyle={props.bodyStyle}
|
|
52
85
|
laneLabelWidth={props.laneLabelWidth}
|
|
53
86
|
onLayout={onLayout}
|
|
54
|
-
cornerZone={cornerZone
|
|
55
|
-
headerZone={
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
87
|
+
cornerZone={cornerZone}
|
|
88
|
+
headerZone={
|
|
89
|
+
<HeaderComponent
|
|
90
|
+
ticks={ticks}
|
|
91
|
+
projection={projection}
|
|
92
|
+
scroll={scroll}
|
|
93
|
+
contentWidth={contentWidth}
|
|
94
|
+
headerCellComponent={headerCellComponent}
|
|
95
|
+
/>
|
|
96
|
+
}
|
|
97
|
+
laneLabelColumnZone={
|
|
98
|
+
<LaneLabelColumnComponent
|
|
99
|
+
labels={orderedLanes.map((lane) => {
|
|
100
|
+
const state = laneState.get(lane.id) ?? {
|
|
101
|
+
flag: 'none',
|
|
102
|
+
complete: true,
|
|
103
|
+
};
|
|
104
|
+
return {
|
|
105
|
+
lane,
|
|
106
|
+
...state,
|
|
107
|
+
viewTimezone: projection.viewTimezone,
|
|
108
|
+
incompleteLabel,
|
|
109
|
+
neverSetLabel,
|
|
110
|
+
};
|
|
111
|
+
})}
|
|
112
|
+
projection={projection}
|
|
113
|
+
scroll={scroll}
|
|
114
|
+
laneLabelComponent={laneLabelComponent}
|
|
115
|
+
/>
|
|
116
|
+
}
|
|
117
|
+
bodyZone={
|
|
118
|
+
<SelectionLayout
|
|
119
|
+
portalHost={props.portalHost ?? `roster-${hostId}`}
|
|
120
|
+
open={selection !== null}
|
|
121
|
+
onDismiss={dismissSelection}
|
|
122
|
+
scroll={{
|
|
123
|
+
x: scroll.x,
|
|
124
|
+
y: scroll.y,
|
|
125
|
+
headerStyle: scroll.headerStyle,
|
|
126
|
+
labelStyle: scroll.labelStyle,
|
|
127
|
+
}}
|
|
128
|
+
targetBounds={targetBounds}
|
|
129
|
+
contentZone={contentZone}
|
|
130
|
+
anchorZone={
|
|
131
|
+
<BodyComponent
|
|
132
|
+
lanes={orderedLanes}
|
|
133
|
+
window={window}
|
|
134
|
+
geometryFor={geometryFor}
|
|
135
|
+
projection={projection}
|
|
136
|
+
scroll={scroll}
|
|
137
|
+
press={press}
|
|
138
|
+
ticks={ticks}
|
|
139
|
+
contentWidth={contentWidth}
|
|
140
|
+
viewport={viewport}
|
|
141
|
+
intervalComponent={intervalComponent}
|
|
142
|
+
gapComponent={gapComponent}
|
|
143
|
+
gridComponent={gridComponent}
|
|
144
|
+
highlightSource={props.highlightSource}
|
|
145
|
+
onIntervalHover={props.onIntervalHover}
|
|
146
|
+
incompleteLabel={incompleteLabel}
|
|
147
|
+
/>
|
|
148
|
+
}
|
|
149
|
+
/>
|
|
150
|
+
}
|
|
88
151
|
/>
|
|
89
152
|
);
|
|
90
153
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Fragment } from 'react';
|
|
2
2
|
import { Pressable } from 'react-native';
|
|
3
3
|
import type { Lane, LaneGeometry } from '../../../core';
|
|
4
|
+
import { hasSource } from '../../../core';
|
|
4
5
|
import { pressPoint } from '../../primitives/press-point';
|
|
5
6
|
import type { BodyInput } from '../roster.types';
|
|
6
7
|
import { intervalHoverProps } from './interval-hover';
|
|
@@ -8,7 +9,12 @@ import { RosterIncomplete } from './parts/incomplete';
|
|
|
8
9
|
|
|
9
10
|
export type LaneRowProps = Pick<
|
|
10
11
|
BodyInput,
|
|
11
|
-
|
|
12
|
+
| 'press'
|
|
13
|
+
| 'intervalComponent'
|
|
14
|
+
| 'gapComponent'
|
|
15
|
+
| 'highlightSource'
|
|
16
|
+
| 'onIntervalHover'
|
|
17
|
+
| 'incompleteLabel'
|
|
12
18
|
> & {
|
|
13
19
|
lane: Lane;
|
|
14
20
|
geometry: LaneGeometry;
|
|
@@ -22,8 +28,8 @@ export function LaneRow({
|
|
|
22
28
|
width,
|
|
23
29
|
rowHeight,
|
|
24
30
|
press,
|
|
25
|
-
|
|
26
|
-
|
|
31
|
+
intervalComponent: IntervalComponent,
|
|
32
|
+
gapComponent: GapComponent,
|
|
27
33
|
highlightSource,
|
|
28
34
|
onIntervalHover,
|
|
29
35
|
incompleteLabel = 'Availability may be incomplete',
|
|
@@ -47,15 +53,12 @@ export function LaneRow({
|
|
|
47
53
|
.filter((rect) => rect.layerId === layer.id)
|
|
48
54
|
.map((rect) => (
|
|
49
55
|
<Fragment key={`interval-${rect.x}:${rect.width}`}>
|
|
50
|
-
|
|
51
|
-
rect
|
|
52
|
-
layer
|
|
53
|
-
lane
|
|
54
|
-
highlighted
|
|
55
|
-
|
|
56
|
-
source.kind === highlightSource?.kind && source.id === highlightSource.id,
|
|
57
|
-
),
|
|
58
|
-
})}
|
|
56
|
+
<IntervalComponent
|
|
57
|
+
rect={rect}
|
|
58
|
+
layer={layer}
|
|
59
|
+
lane={lane}
|
|
60
|
+
highlighted={hasSource(rect.sources, highlightSource)}
|
|
61
|
+
/>
|
|
59
62
|
</Fragment>
|
|
60
63
|
))}
|
|
61
64
|
{geometry.gapRects
|
|
@@ -78,7 +81,7 @@ export function LaneRow({
|
|
|
78
81
|
zIndex: rect.z,
|
|
79
82
|
}}
|
|
80
83
|
>
|
|
81
|
-
|
|
84
|
+
<GapComponent rect={rect} layer={layer} lane={lane} />
|
|
82
85
|
</Pressable>
|
|
83
86
|
))}
|
|
84
87
|
</Fragment>
|
|
@@ -1,91 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { ScrollView, View } from 'react-native';
|
|
4
|
-
import { LaneRow } from '../lanes/lane';
|
|
1
|
+
import type { ProfilerOnRenderCallback } from 'react';
|
|
2
|
+
import { RosterBodyLayout } from '../body-layout';
|
|
5
3
|
import type { BodyInput } from '../roster.types';
|
|
6
|
-
import {
|
|
4
|
+
import { RosterLaneList } from './lane-list';
|
|
7
5
|
|
|
8
|
-
export function RosterBody(
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
ticks,
|
|
15
|
-
contentWidth,
|
|
16
|
-
viewport,
|
|
17
|
-
window,
|
|
18
|
-
intervalZone,
|
|
19
|
-
gapZone,
|
|
20
|
-
gridZone,
|
|
21
|
-
highlightSource,
|
|
22
|
-
onIntervalHover,
|
|
23
|
-
incompleteLabel,
|
|
24
|
-
onRowRender,
|
|
25
|
-
}: BodyInput & {
|
|
26
|
-
/** Optional per-lane commit observer; used by the development gallery's Profiler gate. */
|
|
27
|
-
onRowRender?: ProfilerOnRenderCallback;
|
|
28
|
-
}) {
|
|
6
|
+
export function RosterBody(
|
|
7
|
+
props: BodyInput & {
|
|
8
|
+
/** Development-only Profiler hook used by the gallery; not a supported customization point. */
|
|
9
|
+
onRowRender?: ProfilerOnRenderCallback;
|
|
10
|
+
},
|
|
11
|
+
) {
|
|
12
|
+
const { viewport, scroll, contentWidth, ticks, gridComponent: GridComponent } = props;
|
|
29
13
|
// LegendList requires a measured viewport and does not support static rendering.
|
|
30
14
|
if (viewport.width <= 0 || viewport.height <= 0) return null;
|
|
31
15
|
return (
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
>
|
|
40
|
-
<View style={{ width: contentWidth, height: viewport.height }}>
|
|
41
|
-
{gridZone({ ticks, contentWidth })}
|
|
42
|
-
<LegendList
|
|
43
|
-
testID="roster-vertical-scroll"
|
|
44
|
-
data={lanes}
|
|
45
|
-
extraData={bodyContentKey({
|
|
46
|
-
window,
|
|
47
|
-
projection,
|
|
48
|
-
highlightSource,
|
|
49
|
-
intervalZone,
|
|
50
|
-
gapZone,
|
|
51
|
-
onIntervalHover,
|
|
52
|
-
incompleteLabel,
|
|
53
|
-
})}
|
|
54
|
-
keyExtractor={(lane) => lane.id}
|
|
55
|
-
estimatedListSize={{ width: contentWidth, height: viewport.height }}
|
|
56
|
-
estimatedItemSize={projection.rowHeight}
|
|
57
|
-
getFixedItemSize={() => projection.rowHeight}
|
|
58
|
-
drawDistance={0}
|
|
59
|
-
recycleItems={false}
|
|
60
|
-
maintainVisibleContentPosition={false}
|
|
61
|
-
onScroll={scroll.onVerticalScroll}
|
|
62
|
-
scrollEventThrottle={16}
|
|
63
|
-
style={{ flex: 1 }}
|
|
64
|
-
renderItem={({ item }) => {
|
|
65
|
-
const row = (
|
|
66
|
-
<LaneRow
|
|
67
|
-
lane={item}
|
|
68
|
-
geometry={geometryFor(item)}
|
|
69
|
-
width={contentWidth}
|
|
70
|
-
rowHeight={projection.rowHeight}
|
|
71
|
-
press={press}
|
|
72
|
-
intervalZone={intervalZone}
|
|
73
|
-
gapZone={gapZone}
|
|
74
|
-
highlightSource={highlightSource}
|
|
75
|
-
onIntervalHover={onIntervalHover}
|
|
76
|
-
incompleteLabel={incompleteLabel}
|
|
77
|
-
/>
|
|
78
|
-
);
|
|
79
|
-
if (onRowRender)
|
|
80
|
-
return (
|
|
81
|
-
<Profiler id={item.id} onRender={onRowRender}>
|
|
82
|
-
{row}
|
|
83
|
-
</Profiler>
|
|
84
|
-
);
|
|
85
|
-
return row;
|
|
86
|
-
}}
|
|
87
|
-
/>
|
|
88
|
-
</View>
|
|
89
|
-
</ScrollView>
|
|
16
|
+
<RosterBodyLayout
|
|
17
|
+
scroll={scroll}
|
|
18
|
+
contentWidth={contentWidth}
|
|
19
|
+
viewport={viewport}
|
|
20
|
+
gridZone={<GridComponent ticks={ticks} contentWidth={contentWidth} />}
|
|
21
|
+
listZone={<RosterLaneList {...props} />}
|
|
22
|
+
/>
|
|
90
23
|
);
|
|
91
24
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Text } from 'react-native';
|
|
2
|
-
import type {
|
|
2
|
+
import type { HeaderCellInput } from '../roster.types';
|
|
3
3
|
|
|
4
|
-
export function RosterHeaderCell({ tick }:
|
|
4
|
+
export function RosterHeaderCell({ tick }: HeaderCellInput) {
|
|
5
5
|
return (
|
|
6
6
|
<Text numberOfLines={1} style={{ color: '#475569', fontSize: 11, padding: 4 }}>
|
|
7
7
|
{tick.label}
|
|
@@ -2,7 +2,12 @@ import { View } from 'react-native';
|
|
|
2
2
|
import Animated from 'react-native-reanimated';
|
|
3
3
|
import type { HeaderInput } from '../roster.types';
|
|
4
4
|
|
|
5
|
-
export function RosterHeader({
|
|
5
|
+
export function RosterHeader({
|
|
6
|
+
ticks,
|
|
7
|
+
contentWidth,
|
|
8
|
+
scroll,
|
|
9
|
+
headerCellComponent: HeaderCellComponent,
|
|
10
|
+
}: HeaderInput) {
|
|
6
11
|
return (
|
|
7
12
|
<Animated.View
|
|
8
13
|
testID="roster-header"
|
|
@@ -10,7 +15,7 @@ export function RosterHeader({ ticks, contentWidth, scroll, headerCellZone }: He
|
|
|
10
15
|
>
|
|
11
16
|
{ticks.map((tick) => (
|
|
12
17
|
<View key={tick.time} style={{ position: 'absolute', left: tick.x, top: 0 }}>
|
|
13
|
-
|
|
18
|
+
<HeaderCellComponent tick={tick} />
|
|
14
19
|
</View>
|
|
15
20
|
))}
|
|
16
21
|
</Animated.View>
|
|
@@ -6,7 +6,7 @@ export function RosterLaneLabelColumn({
|
|
|
6
6
|
labels,
|
|
7
7
|
projection,
|
|
8
8
|
scroll,
|
|
9
|
-
|
|
9
|
+
laneLabelComponent: LaneLabelComponent,
|
|
10
10
|
}: LabelColumnInput) {
|
|
11
11
|
return (
|
|
12
12
|
<Animated.View
|
|
@@ -23,7 +23,7 @@ export function RosterLaneLabelColumn({
|
|
|
23
23
|
width: '100%',
|
|
24
24
|
}}
|
|
25
25
|
>
|
|
26
|
-
{
|
|
26
|
+
<LaneLabelComponent {...label} />
|
|
27
27
|
</View>
|
|
28
28
|
))}
|
|
29
29
|
</Animated.View>
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { LegendList } from '@legendapp/list';
|
|
2
|
+
import { Profiler, type ProfilerOnRenderCallback, useState } from 'react';
|
|
3
|
+
import { LaneRow } from '../lanes/lane';
|
|
4
|
+
import type { BodyInput } from '../roster.types';
|
|
5
|
+
import { bodyContentKey } from '../utils/body-content-key';
|
|
6
|
+
|
|
7
|
+
export function RosterLaneList({
|
|
8
|
+
lanes,
|
|
9
|
+
geometryFor,
|
|
10
|
+
projection,
|
|
11
|
+
scroll,
|
|
12
|
+
press,
|
|
13
|
+
contentWidth,
|
|
14
|
+
viewport,
|
|
15
|
+
window,
|
|
16
|
+
intervalComponent,
|
|
17
|
+
gapComponent,
|
|
18
|
+
highlightSource,
|
|
19
|
+
onIntervalHover,
|
|
20
|
+
incompleteLabel,
|
|
21
|
+
onRowRender,
|
|
22
|
+
}: Omit<BodyInput, 'ticks' | 'gridComponent'> & {
|
|
23
|
+
/** Development-only Profiler hook used by the gallery; not a supported customization point. */
|
|
24
|
+
onRowRender?: ProfilerOnRenderCallback;
|
|
25
|
+
}) {
|
|
26
|
+
const [initialScrollOffset] = useState(() => scroll.y.get());
|
|
27
|
+
return (
|
|
28
|
+
<LegendList
|
|
29
|
+
testID="roster-vertical-scroll"
|
|
30
|
+
initialScrollOffset={initialScrollOffset}
|
|
31
|
+
data={lanes}
|
|
32
|
+
extraData={bodyContentKey({
|
|
33
|
+
window,
|
|
34
|
+
projection,
|
|
35
|
+
highlightSource,
|
|
36
|
+
intervalComponent,
|
|
37
|
+
gapComponent,
|
|
38
|
+
onIntervalHover,
|
|
39
|
+
incompleteLabel,
|
|
40
|
+
})}
|
|
41
|
+
keyExtractor={(lane) => lane.id}
|
|
42
|
+
estimatedListSize={{ width: contentWidth, height: viewport.height }}
|
|
43
|
+
estimatedItemSize={projection.rowHeight}
|
|
44
|
+
getFixedItemSize={() => projection.rowHeight}
|
|
45
|
+
drawDistance={0}
|
|
46
|
+
recycleItems={false}
|
|
47
|
+
maintainVisibleContentPosition={false}
|
|
48
|
+
onScroll={scroll.onVerticalScroll}
|
|
49
|
+
scrollEventThrottle={16}
|
|
50
|
+
style={{ flex: 1 }}
|
|
51
|
+
renderItem={({ item }) => {
|
|
52
|
+
const row = (
|
|
53
|
+
<LaneRow
|
|
54
|
+
lane={item}
|
|
55
|
+
geometry={geometryFor(item)}
|
|
56
|
+
width={contentWidth}
|
|
57
|
+
rowHeight={projection.rowHeight}
|
|
58
|
+
press={press}
|
|
59
|
+
intervalComponent={intervalComponent}
|
|
60
|
+
gapComponent={gapComponent}
|
|
61
|
+
highlightSource={highlightSource}
|
|
62
|
+
onIntervalHover={onIntervalHover}
|
|
63
|
+
incompleteLabel={incompleteLabel}
|
|
64
|
+
/>
|
|
65
|
+
);
|
|
66
|
+
if (onRowRender)
|
|
67
|
+
return (
|
|
68
|
+
<Profiler id={item.id} onRender={onRowRender}>
|
|
69
|
+
{row}
|
|
70
|
+
</Profiler>
|
|
71
|
+
);
|
|
72
|
+
return row;
|
|
73
|
+
}}
|
|
74
|
+
/>
|
|
75
|
+
);
|
|
76
|
+
}
|