@trackunit/react-modal 2.2.7 → 2.2.8-alpha-360ef2f17bc.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.
Files changed (3) hide show
  1. package/index.cjs.js +54 -26
  2. package/index.esm.js +54 -26
  3. package/package.json +6 -6
package/index.cjs.js CHANGED
@@ -326,6 +326,12 @@ const Modal = ({ children, container, dismiss, isOpen, mode, requestClose, role
326
326
  // In card mode, there is no close animation so notify useModal immediately
327
327
  // so it can call closeModal() without delay.
328
328
  // In sheet mode, Sheet's onCloseComplete handles this instead.
329
+ //
330
+ // Must stay a passive effect (useWatch): useModal arms the close safety
331
+ // timeout in a layout effect, which has to flush before onCloseComplete
332
+ // cancels it. Moving this to a layout effect would run it first (child
333
+ // effects run before parent effects within a phase), leaving the timeout
334
+ // armed with nothing to cancel it.
329
335
  reactComponents.useWatch({
330
336
  value: isOpen,
331
337
  onChange: (current, prev) => {
@@ -1014,33 +1020,55 @@ const useModal = (props) => {
1014
1020
  void closeModal();
1015
1021
  }
1016
1022
  }, [stopCloseTimeout, closeModal]);
1017
- // Sync controlled isOpen state with iframe size controller.
1018
- // Defers rendering until the iframe has finished resizing to fullscreen
1019
- // so the modal animation plays after the resize, not during it.
1020
- reactComponents.useWatch({
1021
- value: controlledIsOpen,
1022
- immediate: true,
1023
- skip: typeof controlledIsOpen !== "boolean",
1024
- onChange: (current, prev) => {
1025
- if (current && !prev) {
1026
- const id = ++openIdRef.current;
1027
- const result = openModal();
1028
- if (result instanceof Promise) {
1029
- setOpenDeferred(true);
1030
- void result.then(() => {
1031
- if (openIdRef.current === id) {
1032
- setOpenDeferred(false);
1033
- }
1034
- });
1023
+ // Asks the host iframe to resize to fullscreen. While the resize is running,
1024
+ // rendering is deferred so the open animation plays after the resize, not
1025
+ // during it.
1026
+ const beginControlledOpen = react.useCallback(() => {
1027
+ const id = ++openIdRef.current;
1028
+ const result = openModal();
1029
+ if (result instanceof Promise) {
1030
+ setOpenDeferred(true);
1031
+ void result.then(() => {
1032
+ if (openIdRef.current === id) {
1033
+ setOpenDeferred(false);
1035
1034
  }
1036
- }
1037
- else if (!current && prev) {
1038
- openIdRef.current++;
1039
- setOpenDeferred(false);
1040
- startPendingCloseModal();
1041
- }
1042
- },
1043
- });
1035
+ });
1036
+ }
1037
+ }, [openModal]);
1038
+ // Arms the pending close, so closeModal() resizes the iframe back once the
1039
+ // close animation completes (or the safety timeout fires as a fallback).
1040
+ const beginControlledClose = react.useCallback(() => {
1041
+ openIdRef.current++;
1042
+ setOpenDeferred(false);
1043
+ startPendingCloseModal();
1044
+ }, [startPendingCloseModal]);
1045
+ // Reacts to changes of the controlled isOpen prop.
1046
+ //
1047
+ // Must be a layout effect: layout effects flush before ALL passive effects,
1048
+ // so the close safety timeout is armed (or re-armed, after modal.close()'s
1049
+ // synchronous arm in handleClose) before the child Modal's passive effect
1050
+ // cancels it via onCloseComplete. Within the same phase child effects run
1051
+ // first, so this ordering only holds while Modal calls onCloseComplete from
1052
+ // a passive effect. With a passive effect here, the arm came after the
1053
+ // cancellation and the timeout fired on every controlled card-mode close
1054
+ // (sheet-mode closes usually completed later via transitionend).
1055
+ const prevControlledIsOpenRef = react.useRef(null);
1056
+ react.useLayoutEffect(() => {
1057
+ if (typeof controlledIsOpen !== "boolean") {
1058
+ return;
1059
+ }
1060
+ const wasOpen = prevControlledIsOpenRef.current;
1061
+ prevControlledIsOpenRef.current = controlledIsOpen;
1062
+ if (controlledIsOpen === wasOpen) {
1063
+ return;
1064
+ }
1065
+ if (controlledIsOpen) {
1066
+ beginControlledOpen();
1067
+ }
1068
+ else if (wasOpen) {
1069
+ beginControlledClose();
1070
+ }
1071
+ }, [controlledIsOpen, beginControlledOpen, beginControlledClose]);
1044
1072
  // Cleanup: notify context when component unmounts while modal is open or mid-close.
1045
1073
  // Calls closeModal() directly (no animation to wait for during unmount).
1046
1074
  react.useEffect(() => {
package/index.esm.js CHANGED
@@ -324,6 +324,12 @@ const Modal = ({ children, container, dismiss, isOpen, mode, requestClose, role
324
324
  // In card mode, there is no close animation so notify useModal immediately
325
325
  // so it can call closeModal() without delay.
326
326
  // In sheet mode, Sheet's onCloseComplete handles this instead.
327
+ //
328
+ // Must stay a passive effect (useWatch): useModal arms the close safety
329
+ // timeout in a layout effect, which has to flush before onCloseComplete
330
+ // cancels it. Moving this to a layout effect would run it first (child
331
+ // effects run before parent effects within a phase), leaving the timeout
332
+ // armed with nothing to cancel it.
327
333
  useWatch({
328
334
  value: isOpen,
329
335
  onChange: (current, prev) => {
@@ -1012,33 +1018,55 @@ const useModal = (props) => {
1012
1018
  void closeModal();
1013
1019
  }
1014
1020
  }, [stopCloseTimeout, closeModal]);
1015
- // Sync controlled isOpen state with iframe size controller.
1016
- // Defers rendering until the iframe has finished resizing to fullscreen
1017
- // so the modal animation plays after the resize, not during it.
1018
- useWatch({
1019
- value: controlledIsOpen,
1020
- immediate: true,
1021
- skip: typeof controlledIsOpen !== "boolean",
1022
- onChange: (current, prev) => {
1023
- if (current && !prev) {
1024
- const id = ++openIdRef.current;
1025
- const result = openModal();
1026
- if (result instanceof Promise) {
1027
- setOpenDeferred(true);
1028
- void result.then(() => {
1029
- if (openIdRef.current === id) {
1030
- setOpenDeferred(false);
1031
- }
1032
- });
1021
+ // Asks the host iframe to resize to fullscreen. While the resize is running,
1022
+ // rendering is deferred so the open animation plays after the resize, not
1023
+ // during it.
1024
+ const beginControlledOpen = useCallback(() => {
1025
+ const id = ++openIdRef.current;
1026
+ const result = openModal();
1027
+ if (result instanceof Promise) {
1028
+ setOpenDeferred(true);
1029
+ void result.then(() => {
1030
+ if (openIdRef.current === id) {
1031
+ setOpenDeferred(false);
1033
1032
  }
1034
- }
1035
- else if (!current && prev) {
1036
- openIdRef.current++;
1037
- setOpenDeferred(false);
1038
- startPendingCloseModal();
1039
- }
1040
- },
1041
- });
1033
+ });
1034
+ }
1035
+ }, [openModal]);
1036
+ // Arms the pending close, so closeModal() resizes the iframe back once the
1037
+ // close animation completes (or the safety timeout fires as a fallback).
1038
+ const beginControlledClose = useCallback(() => {
1039
+ openIdRef.current++;
1040
+ setOpenDeferred(false);
1041
+ startPendingCloseModal();
1042
+ }, [startPendingCloseModal]);
1043
+ // Reacts to changes of the controlled isOpen prop.
1044
+ //
1045
+ // Must be a layout effect: layout effects flush before ALL passive effects,
1046
+ // so the close safety timeout is armed (or re-armed, after modal.close()'s
1047
+ // synchronous arm in handleClose) before the child Modal's passive effect
1048
+ // cancels it via onCloseComplete. Within the same phase child effects run
1049
+ // first, so this ordering only holds while Modal calls onCloseComplete from
1050
+ // a passive effect. With a passive effect here, the arm came after the
1051
+ // cancellation and the timeout fired on every controlled card-mode close
1052
+ // (sheet-mode closes usually completed later via transitionend).
1053
+ const prevControlledIsOpenRef = useRef(null);
1054
+ useLayoutEffect(() => {
1055
+ if (typeof controlledIsOpen !== "boolean") {
1056
+ return;
1057
+ }
1058
+ const wasOpen = prevControlledIsOpenRef.current;
1059
+ prevControlledIsOpenRef.current = controlledIsOpen;
1060
+ if (controlledIsOpen === wasOpen) {
1061
+ return;
1062
+ }
1063
+ if (controlledIsOpen) {
1064
+ beginControlledOpen();
1065
+ }
1066
+ else if (wasOpen) {
1067
+ beginControlledClose();
1068
+ }
1069
+ }, [controlledIsOpen, beginControlledOpen, beginControlledClose]);
1042
1070
  // Cleanup: notify context when component unmounts while modal is open or mid-close.
1043
1071
  // Calls closeModal() directly (no animation to wait for during unmount).
1044
1072
  useEffect(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/react-modal",
3
- "version": "2.2.7",
3
+ "version": "2.2.8-alpha-360ef2f17bc.0",
4
4
  "repository": "https://github.com/Trackunit/manager",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "engines": {
@@ -8,12 +8,12 @@
8
8
  },
9
9
  "dependencies": {
10
10
  "@floating-ui/react": "^0.26.25",
11
- "@trackunit/react-components": "2.2.6",
12
- "@trackunit/css-class-variance-utilities": "1.13.64",
13
- "@trackunit/shared-utils": "1.15.67",
11
+ "@trackunit/react-components": "2.2.7-alpha-360ef2f17bc.0",
12
+ "@trackunit/css-class-variance-utilities": "1.13.65-alpha-360ef2f17bc.0",
13
+ "@trackunit/shared-utils": "1.15.68-alpha-360ef2f17bc.0",
14
14
  "@floating-ui/react-dom": "2.1.2",
15
- "@trackunit/react-core-contexts-api": "1.17.73",
16
- "@trackunit/i18n-library-translation": "2.1.5"
15
+ "@trackunit/react-core-contexts-api": "1.17.74-alpha-360ef2f17bc.0",
16
+ "@trackunit/i18n-library-translation": "2.1.6-alpha-360ef2f17bc.0"
17
17
  },
18
18
  "peerDependencies": {
19
19
  "@tanstack/react-router": "^1.114.29",