lilact 0.10.2 → 0.11.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.
@@ -37,9 +37,9 @@ const ENTERING = "entering";
37
37
  const ENTERED = "entered";
38
38
  const EXITING = "exiting";
39
39
 
40
- import {setTimeout, clearTimeout} from "./timers.jsx"
41
- import {Children} from "./misc.jsx"
42
- import {useEffect} from "./hooks.jsx"
40
+ import { setTimeout, clearTimeout} from "./timers.jsx"
41
+ import { Children } from "./misc.jsx"
42
+ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "./hooks.jsx"
43
43
 
44
44
  /**
45
45
  * Transition component that manages enter/exit lifecycle and calls callbacks based on state changes.
@@ -200,7 +200,7 @@ export function Transition({
200
200
  */
201
201
  export function CSSTransition({
202
202
  in: inProp,
203
- timeout = defaultTransitionTimeout,
203
+ timeout = Lilact.defaultTransitionTimeout,
204
204
  classNames = "fade",
205
205
  mountOnEnter = false,
206
206
  unmountOnExit = false,
@@ -244,6 +244,182 @@ export function CSSTransition({
244
244
  }
245
245
 
246
246
 
247
+
248
+ /**
249
+ * Switches between keyed children with `CSSTransition`.
250
+ * Ensures each child key gets its own `CSSTransition` instance to avoid state loss.
251
+ *
252
+ * @param {object} props
253
+ * @param {Array} props.children
254
+ * The set of children to switch between. Each child should have a stable `key`
255
+ * (or your `key` will fall back to its index).
256
+ * @param {*} props.activeKey
257
+ * The key of the currently active child.
258
+ * @param {"out-in" | "in-out"} [props.mode="out-in"]
259
+ * Controls sequencing when `activeKey` changes:
260
+ * - `"out-in"`: outgoing transitions out first, incoming transitions in after exit completes.
261
+ * - `"in-out"`: incoming transitions in first, outgoing transitions out after the incoming finishes entering
262
+ * (relies on `onEntered`).
263
+ * @param {number} [props.timeout=Lilact.defaultTransitionTimeout]
264
+ * Transition timeout passed to each `CSSTransition`.
265
+ * @param {string} [props.classNames="switch"]
266
+ * Base className(s) used by `CSSTransition` to apply enter/exit classes.
267
+ * @param {boolean} [props.mountOnEnter=false]
268
+ * Whether to mount the transitioning child only when it begins entering.
269
+ * @param {boolean} [props.unmountOnExit=false]
270
+ * Whether to unmount the child after it finishes exiting.
271
+ * @param {boolean} [props.appear=false]
272
+ * Whether to run the initial transition on first mount.
273
+ *
274
+ * @param {(isAppearing: boolean) => void} [props.onExited]
275
+ * Called when the outgoing child finishes exiting.
276
+ * @param {(isAppearing: boolean) => void} [props.onEnter]
277
+ * Called when a child begins entering.
278
+ * @param {(isAppearing: boolean) => void} [props.onExiting]
279
+ * Called when a child begins exiting.
280
+ * @param {(isAppearing: boolean) => void} [props.onEntered]
281
+ * Called when a child finishes entering. Used to implement `"in-out"` sequencing.
282
+ *
283
+ * @param {object} [props.csstProps]
284
+ * Any additional props are forwarded to each `CSSTransition` instance.
285
+ */
286
+
287
+
288
+ export function SwitchTransition({
289
+ children,
290
+ activeKey,
291
+ mode = "out-in", // "out-in" | "in-out"
292
+
293
+ timeout = Lilact.defaultTransitionTimeout,
294
+ classNames = "switch",
295
+
296
+ mountOnEnter = false,
297
+ unmountOnExit = false,
298
+ appear = false,
299
+
300
+ // callbacks (optional)
301
+ onExited,
302
+ onEnter,
303
+ onExiting,
304
+ onEntered,
305
+
306
+ ...csstProps
307
+ }) {
308
+ const childArray = useMemo(() => Children.toArray(children), [children]);
309
+
310
+ // Outgoing key (the one that should exit) and phase flags
311
+ const [exitingKey, setExitingKey] = useState(null);
312
+ const [exitStarted, setExitStarted] = useState(false);
313
+
314
+ // Controls whether the incoming (activeKey) is currently "in"
315
+ const [enterAllowed, setEnterAllowed] = useState(true);
316
+
317
+ // Refs to avoid stale closures in callbacks
318
+ const prevKeyRef = useRef(activeKey);
319
+ const activeKeyRef = useRef(activeKey);
320
+ const exitingKeyRef = useRef(exitingKey);
321
+
322
+ useLayoutEffect(() => {
323
+ activeKeyRef.current = activeKey;
324
+ }, [activeKey]);
325
+
326
+ useLayoutEffect(() => {
327
+ exitingKeyRef.current = exitingKey;
328
+ }, [exitingKey]);
329
+
330
+ useLayoutEffect(() => {
331
+ const prevKey = prevKeyRef.current;
332
+ if (prevKey === activeKey) return;
333
+
334
+ prevKeyRef.current = activeKey;
335
+
336
+ // Start a new switch
337
+ setExitingKey(prevKey);
338
+
339
+ if (mode === "out-in") {
340
+ // Incoming blocked; outgoing should exit immediately.
341
+ setEnterAllowed(false);
342
+ setExitStarted(true); // outgoing in: true -> false => triggers exit
343
+ } else {
344
+ // Incoming allowed immediately; outgoing should exit only after incoming has entered.
345
+ setEnterAllowed(true);
346
+ setExitStarted(false); // outgoing stays in:true until we flip it later
347
+ }
348
+ }, [activeKey, mode]);
349
+
350
+ const handleExited = (key) => (node, isAppearing) => {
351
+ if (typeof onExited === "function") onExited(node, isAppearing);
352
+
353
+ if (key === exitingKeyRef.current) {
354
+ setExitingKey(null);
355
+
356
+ // out-in: once outgoing is fully exited, allow incoming to start
357
+ if (mode === "out-in") {
358
+ setExitStarted(false);
359
+ setEnterAllowed(true);
360
+ }
361
+ // in-out: incoming already entered; just clean up outgoing
362
+ if (mode === "in-out") {
363
+ setExitStarted(false);
364
+ }
365
+ }
366
+ };
367
+
368
+ const handleEntered = (key) => (node, isAppearing) => {
369
+ if (typeof onEntered === "function") onEntered(node, isAppearing);
370
+
371
+ if (mode === "in-out") {
372
+ // Only start outgoing exit when the CURRENT incoming (activeKey) finishes entering.
373
+ if (key === activeKeyRef.current && exitingKeyRef.current != null) {
374
+ setExitStarted(true); // outgoing in:true -> false => triggers exit
375
+ }
376
+ }
377
+ };
378
+
379
+ return (
380
+ <div style={{ position: "relative" }}>
381
+ {childArray.map((child, index) => {
382
+ const key = child?.props?.key || index;
383
+
384
+ const isIncoming = key === activeKey;
385
+ const isOutgoing = key === exitingKey;
386
+
387
+ // Core rule:
388
+ // - Incoming: in = enterAllowed
389
+ // - Outgoing: in = !exitStarted
390
+ // - Others: in = false
391
+ const inProp = isIncoming
392
+ ? enterAllowed
393
+ : isOutgoing
394
+ ? !exitStarted
395
+ : false;
396
+
397
+ return (
398
+ <CSSTransition
399
+ key={key}
400
+ in={inProp}
401
+ timeout={timeout}
402
+ classNames={classNames}
403
+ mountOnEnter={mountOnEnter}
404
+ unmountOnExit={unmountOnExit}
405
+ appear={appear}
406
+ onEnter={onEnter}
407
+ onExiting={onExiting}
408
+ onEntered={handleEntered(key)}
409
+ onExited={handleExited(key)}
410
+ {...csstProps}
411
+ >
412
+ <div style={{ position: "absolute", inset: 0 }}>
413
+ {child}
414
+ </div>
415
+ </CSSTransition>
416
+ );
417
+ })}
418
+ </div>
419
+ );
420
+ }
421
+
422
+
247
423
  /**
248
424
  * Lilact doesn't need TransitionGroup, so it is the same as a fragment.
249
425
  * In Lilact all the transitions and timeouts are automatically grouped.