|
3 | 3 | createRowLayoutController, |
4 | 4 | type DomLayoutColumn, |
5 | 5 | } from "@pretable-internal/renderer-dom"; |
| 6 | +import { ɵqueriesSemanticallyEqual } from "@pretable-internal/row-model/query-equality"; |
6 | 7 | import { |
7 | 8 | ɵcreateGridUiCore as createGridUiCore, |
8 | 9 | type PretableGridUiState, |
@@ -1038,21 +1039,93 @@ export function usePretableModelInternal< |
1038 | 1039 | previousPresentationColumns.current = columns; |
1039 | 1040 | }, [columns, stores.autoWidths, stores.gridCore]); |
1040 | 1041 |
|
| 1042 | + const viewportAuthorityRef = useRef<{ |
| 1043 | + readonly gridScrollTop: number; |
| 1044 | + readonly controllerScrollTop: number; |
| 1045 | + readonly viewportHeight: number; |
| 1046 | + readonly overscan: number; |
| 1047 | + readonly observedQuery: typeof observedQuery; |
| 1048 | + readonly renderColumns: typeof renderColumns; |
| 1049 | + } | null>(null); |
| 1050 | + const controllerScrollTop = renderControllerSnapshot.scrollTop; |
| 1051 | + const viewportOverscan = options.overscan ?? 6; |
1041 | 1052 | useLayoutEffect(() => { |
1042 | | - if (renderControllerSnapshot.status.kind === "disposed") return; |
1043 | | - stores.controller.setColumns(renderColumns); |
1044 | | - stores.controller.setViewport({ |
1045 | | - scrollTop: gridSnapshot.viewport.scrollTop, |
| 1053 | + if (stores.controller.getState().status.kind === "disposed") return; |
| 1054 | + |
| 1055 | + const previous = viewportAuthorityRef.current; |
| 1056 | + const gridChanged = |
| 1057 | + previous === null || |
| 1058 | + previous.gridScrollTop !== gridSnapshot.viewport.scrollTop; |
| 1059 | + const viewportShapeChanged = |
| 1060 | + previous === null || |
| 1061 | + previous.viewportHeight !== gridSnapshot.viewport.height || |
| 1062 | + previous.overscan !== viewportOverscan; |
| 1063 | + const queryChanged = |
| 1064 | + previous !== null && |
| 1065 | + !ɵqueriesSemanticallyEqual(previous.observedQuery, observedQuery); |
| 1066 | + const columnsChanged = |
| 1067 | + previous === null || previous.renderColumns !== renderColumns; |
| 1068 | + const controllerChanged = |
| 1069 | + previous !== null && previous.controllerScrollTop !== controllerScrollTop; |
| 1070 | + |
| 1071 | + // Record the committed pair before publishing either side. A synchronous |
| 1072 | + // external-store notification can render immediately, and that render |
| 1073 | + // must classify the publication as the echo of this decision rather than |
| 1074 | + // as a second source of authority. |
| 1075 | + viewportAuthorityRef.current = { |
| 1076 | + gridScrollTop: gridSnapshot.viewport.scrollTop, |
| 1077 | + controllerScrollTop, |
1046 | 1078 | viewportHeight: gridSnapshot.viewport.height, |
1047 | | - overscan: options.overscan ?? 6, |
1048 | | - }); |
| 1079 | + overscan: viewportOverscan, |
| 1080 | + observedQuery, |
| 1081 | + renderColumns, |
| 1082 | + }; |
| 1083 | + |
| 1084 | + if (columnsChanged) { |
| 1085 | + stores.controller.setColumns(renderColumns); |
| 1086 | + } |
| 1087 | + |
| 1088 | + if (gridChanged || viewportShapeChanged || queryChanged) { |
| 1089 | + // A real grid/DOM change is an input. When it races an anchored |
| 1090 | + // controller publication, the newer external input deliberately wins. |
| 1091 | + // A semantic query change also starts from the grid's position: sort, |
| 1092 | + // filter and grouping transitions keep the user's DOM offset rather |
| 1093 | + // than following an old row to its new dataset rank. |
| 1094 | + stores.controller.setViewport({ |
| 1095 | + scrollTop: gridSnapshot.viewport.scrollTop, |
| 1096 | + viewportHeight: gridSnapshot.viewport.height, |
| 1097 | + overscan: viewportOverscan, |
| 1098 | + }); |
| 1099 | + return; |
| 1100 | + } |
| 1101 | + |
| 1102 | + if ( |
| 1103 | + controllerChanged && |
| 1104 | + controllerScrollTop !== gridSnapshot.viewport.scrollTop |
| 1105 | + ) { |
| 1106 | + // Anchor restoration is a controller output, not permission to re-feed |
| 1107 | + // the grid's previous offset. Publish it outward so the grid and DOM |
| 1108 | + // converge on the controller before another viewport input is possible. |
| 1109 | + const viewport = stores.gridCore.getState().viewport; |
| 1110 | + viewportAuthorityRef.current = { |
| 1111 | + ...viewportAuthorityRef.current, |
| 1112 | + gridScrollTop: controllerScrollTop, |
| 1113 | + controllerScrollTop, |
| 1114 | + }; |
| 1115 | + stores.gridCore.setViewport({ |
| 1116 | + ...viewport, |
| 1117 | + scrollTop: controllerScrollTop, |
| 1118 | + }); |
| 1119 | + } |
1049 | 1120 | }, [ |
| 1121 | + controllerScrollTop, |
1050 | 1122 | gridSnapshot.viewport.height, |
1051 | 1123 | gridSnapshot.viewport.scrollTop, |
1052 | | - options.overscan, |
| 1124 | + observedQuery, |
1053 | 1125 | renderColumns, |
1054 | | - renderControllerSnapshot.status.kind, |
1055 | 1126 | stores.controller, |
| 1127 | + stores.gridCore, |
| 1128 | + viewportOverscan, |
1056 | 1129 | ]); |
1057 | 1130 |
|
1058 | 1131 | const observedRevision = renderControllerSnapshot.observedRevision; |
|
0 commit comments