@seed-design/stackflow 1.1.23 → 2.0.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/lib/components/AppBar/AppBar.d.ts +0 -4
- package/lib/primitive/AppScreen/AppScreen.cjs +17 -6
- package/lib/primitive/AppScreen/AppScreen.js +18 -7
- package/lib/primitive/AppScreen/useAppScreen.cjs +2 -0
- package/lib/primitive/AppScreen/useAppScreen.d.ts +2 -1
- package/lib/primitive/AppScreen/useAppScreen.js +3 -1
- package/lib/primitive/GlobalInteraction/dom.cjs +29 -0
- package/lib/primitive/GlobalInteraction/dom.d.ts +17 -0
- package/lib/primitive/GlobalInteraction/dom.js +29 -1
- package/lib/primitive/GlobalInteraction/useGlobalInteraction.cjs +7 -0
- package/lib/primitive/GlobalInteraction/useGlobalInteraction.js +8 -1
- package/package.json +6 -5
- package/src/components/AppBar/AppBar.tsx +1 -6
- package/src/primitive/AppScreen/AppScreen.tsx +31 -8
- package/src/primitive/AppScreen/useAppScreen.tsx +3 -1
- package/src/primitive/GlobalInteraction/animation.ts +5 -2
- package/src/primitive/GlobalInteraction/dom.ts +29 -0
- package/src/primitive/GlobalInteraction/useGlobalInteraction.ts +23 -0
|
@@ -9,10 +9,6 @@ export declare const AppBarPropsProvider: ({ children, value }: {
|
|
|
9
9
|
value: Partial< AppBarVariant>;
|
|
10
10
|
}) => import("react/jsx-runtime").JSX.Element;
|
|
11
11
|
export interface AppBarProps extends AppBarVariantProps, AppBarPrimitive.RootProps, BoxBackgroundProps {
|
|
12
|
-
/**
|
|
13
|
-
* @deprecated SEED React 1.3에서 제거될 예정인 옵션입니다. 옵션 제거 이후에는 AppBar 하단 구분선이 표시되지 않습니다.
|
|
14
|
-
*/
|
|
15
|
-
divider?: AppBarVariantProps["divider"];
|
|
16
12
|
}
|
|
17
13
|
export declare const AppBarRoot: ForwardRefExoticComponent<AppBarProps & RefAttributes<HTMLDivElement>>;
|
|
18
14
|
export interface AppBarLeftProps extends AppBarPrimitive.LeftProps {
|
|
@@ -6,6 +6,7 @@ let _seed_design_dom_utils = require("@seed-design/dom-utils");
|
|
|
6
6
|
let _seed_design_react_primitive = require("@seed-design/react-primitive");
|
|
7
7
|
let react = require("react");
|
|
8
8
|
let _radix_ui_react_compose_refs = require("@radix-ui/react-compose-refs");
|
|
9
|
+
let _radix_ui_react_focus_scope = require("@radix-ui/react-focus-scope");
|
|
9
10
|
let _stackflow_react_ui_core = require("@stackflow/react-ui-core");
|
|
10
11
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
11
12
|
//#region src/primitive/AppScreen/AppScreen.tsx
|
|
@@ -20,12 +21,21 @@ var AppScreenRoot = (0, react.forwardRef)((props, ref) => {
|
|
|
20
21
|
onSwipeBackStart
|
|
21
22
|
});
|
|
22
23
|
(0, _stackflow_react_ui_core.usePreventTouchDuringTransition)({ ref: innerRef });
|
|
24
|
+
(0, react.useEffect)(() => {
|
|
25
|
+
if (api.activity?.transitionState === "enter-done") api.layerRef.current?.focus();
|
|
26
|
+
}, [api.activity?.transitionState, api.layerRef]);
|
|
23
27
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_useAppScreenContext.AppScreenProvider, {
|
|
24
28
|
value: api,
|
|
25
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_radix_ui_react_focus_scope.FocusScope, {
|
|
30
|
+
asChild: true,
|
|
31
|
+
trapped: true,
|
|
32
|
+
loop: true,
|
|
33
|
+
onMountAutoFocus: (e) => e.preventDefault(),
|
|
34
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_seed_design_react_primitive.Primitive.div, {
|
|
35
|
+
ref: (0, _radix_ui_react_compose_refs.composeRefs)(innerRef, ref),
|
|
36
|
+
"data-stackflow-component-name": "AppScreen",
|
|
37
|
+
...(0, _seed_design_dom_utils.mergeProps)(api.activityProps, otherProps)
|
|
38
|
+
})
|
|
29
39
|
})
|
|
30
40
|
});
|
|
31
41
|
});
|
|
@@ -47,9 +57,10 @@ var AppScreenEdge = (0, react.forwardRef)((props, ref) => {
|
|
|
47
57
|
});
|
|
48
58
|
AppScreenEdge.displayName = "AppScreenEdge";
|
|
49
59
|
var AppScreenLayer = (0, react.forwardRef)((props, ref) => {
|
|
50
|
-
const { layerProps } = require_useAppScreenContext.useAppScreenContext();
|
|
60
|
+
const { layerProps, layerRef } = require_useAppScreenContext.useAppScreenContext();
|
|
51
61
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_seed_design_react_primitive.Primitive.div, {
|
|
52
|
-
ref,
|
|
62
|
+
ref: (0, _radix_ui_react_compose_refs.composeRefs)(ref, layerRef),
|
|
63
|
+
tabIndex: -1,
|
|
53
64
|
...(0, _seed_design_dom_utils.mergeProps)(layerProps, props)
|
|
54
65
|
});
|
|
55
66
|
});
|
|
@@ -3,8 +3,9 @@ import { useAppScreen } from "./useAppScreen.js";
|
|
|
3
3
|
import { AppScreenProvider, useAppScreenContext } from "./useAppScreenContext.js";
|
|
4
4
|
import { mergeProps } from "@seed-design/dom-utils";
|
|
5
5
|
import { Primitive } from "@seed-design/react-primitive";
|
|
6
|
-
import { forwardRef, useRef } from "react";
|
|
6
|
+
import { forwardRef, useEffect, useRef } from "react";
|
|
7
7
|
import { composeRefs } from "@radix-ui/react-compose-refs";
|
|
8
|
+
import { FocusScope } from "@radix-ui/react-focus-scope";
|
|
8
9
|
import { usePreventTouchDuringTransition } from "@stackflow/react-ui-core";
|
|
9
10
|
import { jsx } from "react/jsx-runtime";
|
|
10
11
|
//#region src/primitive/AppScreen/AppScreen.tsx
|
|
@@ -19,12 +20,21 @@ var AppScreenRoot = forwardRef((props, ref) => {
|
|
|
19
20
|
onSwipeBackStart
|
|
20
21
|
});
|
|
21
22
|
usePreventTouchDuringTransition({ ref: innerRef });
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (api.activity?.transitionState === "enter-done") api.layerRef.current?.focus();
|
|
25
|
+
}, [api.activity?.transitionState, api.layerRef]);
|
|
22
26
|
return /* @__PURE__ */ jsx(AppScreenProvider, {
|
|
23
27
|
value: api,
|
|
24
|
-
children: /* @__PURE__ */ jsx(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
children: /* @__PURE__ */ jsx(FocusScope, {
|
|
29
|
+
asChild: true,
|
|
30
|
+
trapped: true,
|
|
31
|
+
loop: true,
|
|
32
|
+
onMountAutoFocus: (e) => e.preventDefault(),
|
|
33
|
+
children: /* @__PURE__ */ jsx(Primitive.div, {
|
|
34
|
+
ref: composeRefs(innerRef, ref),
|
|
35
|
+
"data-stackflow-component-name": "AppScreen",
|
|
36
|
+
...mergeProps(api.activityProps, otherProps)
|
|
37
|
+
})
|
|
28
38
|
})
|
|
29
39
|
});
|
|
30
40
|
});
|
|
@@ -46,9 +56,10 @@ var AppScreenEdge = forwardRef((props, ref) => {
|
|
|
46
56
|
});
|
|
47
57
|
AppScreenEdge.displayName = "AppScreenEdge";
|
|
48
58
|
var AppScreenLayer = forwardRef((props, ref) => {
|
|
49
|
-
const { layerProps } = useAppScreenContext();
|
|
59
|
+
const { layerProps, layerRef } = useAppScreenContext();
|
|
50
60
|
return /* @__PURE__ */ jsx(Primitive.div, {
|
|
51
|
-
ref,
|
|
61
|
+
ref: composeRefs(ref, layerRef),
|
|
62
|
+
tabIndex: -1,
|
|
52
63
|
...mergeProps(layerProps, props)
|
|
53
64
|
});
|
|
54
65
|
});
|
|
@@ -9,6 +9,7 @@ let _stackflow_react_ui_core = require("@stackflow/react-ui-core");
|
|
|
9
9
|
//#region src/primitive/AppScreen/useAppScreen.tsx
|
|
10
10
|
function useAppScreen(props) {
|
|
11
11
|
const activity = (0, _stackflow_react_ui_core.useNullableActivity)();
|
|
12
|
+
const layerRef = (0, react.useRef)(null);
|
|
12
13
|
const transitionState = activity?.transitionState ?? "enter-done";
|
|
13
14
|
const { activityProps, layerProps, edgeProps } = require_useSwipeBack.useSwipeBack(props);
|
|
14
15
|
const zIndexBase = require_useActivityZIndexBase.useActivityZIndexBase();
|
|
@@ -24,6 +25,7 @@ function useAppScreen(props) {
|
|
|
24
25
|
]);
|
|
25
26
|
return (0, react.useMemo)(() => ({
|
|
26
27
|
activity,
|
|
28
|
+
layerRef,
|
|
27
29
|
stateProps,
|
|
28
30
|
activityProps: (0, _seed_design_dom_utils.elementProps)({
|
|
29
31
|
"data-part": require_anatomy.appScreenAnatomy.activity,
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { UseSwipeBackProps } from '../GlobalInteraction/useSwipeBack';
|
|
2
2
|
import { Activity } from '@stackflow/core';
|
|
3
|
-
import { HTMLAttributes, CSSProperties, AriaRole, ReactNode, ClipboardEventHandler, CompositionEventHandler, FocusEventHandler, FormEventHandler, InputEventHandler, ReactEventHandler, KeyboardEventHandler, MouseEventHandler, DragEventHandler, TouchEventHandler, PointerEventHandler, UIEventHandler, WheelEventHandler, AnimationEventHandler, ToggleEventHandler, TransitionEventHandler } from 'react';
|
|
3
|
+
import { RefObject, HTMLAttributes, CSSProperties, AriaRole, ReactNode, ClipboardEventHandler, CompositionEventHandler, FocusEventHandler, FormEventHandler, InputEventHandler, ReactEventHandler, KeyboardEventHandler, MouseEventHandler, DragEventHandler, TouchEventHandler, PointerEventHandler, UIEventHandler, WheelEventHandler, AnimationEventHandler, ToggleEventHandler, TransitionEventHandler } from 'react';
|
|
4
4
|
export interface UseAppScreenProps extends UseSwipeBackProps {
|
|
5
5
|
}
|
|
6
6
|
export type UseAppScreenReturn = ReturnType<typeof useAppScreen>;
|
|
7
7
|
export declare function useAppScreen(props: UseAppScreenProps): {
|
|
8
8
|
activity: Activity | null;
|
|
9
|
+
layerRef: RefObject<HTMLDivElement | null>;
|
|
9
10
|
stateProps: HTMLAttributes<HTMLElement>;
|
|
10
11
|
activityProps: {
|
|
11
12
|
slot?: string | undefined | undefined;
|
|
@@ -3,11 +3,12 @@ import { useSwipeBack } from "../GlobalInteraction/useSwipeBack.js";
|
|
|
3
3
|
import { useActivityZIndexBase } from "../../hooks/useActivityZIndexBase.js";
|
|
4
4
|
import { appScreenAnatomy } from "./anatomy.js";
|
|
5
5
|
import { dataAttr, elementProps } from "@seed-design/dom-utils";
|
|
6
|
-
import { useMemo } from "react";
|
|
6
|
+
import { useMemo, useRef } from "react";
|
|
7
7
|
import { useNullableActivity } from "@stackflow/react-ui-core";
|
|
8
8
|
//#region src/primitive/AppScreen/useAppScreen.tsx
|
|
9
9
|
function useAppScreen(props) {
|
|
10
10
|
const activity = useNullableActivity();
|
|
11
|
+
const layerRef = useRef(null);
|
|
11
12
|
const transitionState = activity?.transitionState ?? "enter-done";
|
|
12
13
|
const { activityProps, layerProps, edgeProps } = useSwipeBack(props);
|
|
13
14
|
const zIndexBase = useActivityZIndexBase();
|
|
@@ -23,6 +24,7 @@ function useAppScreen(props) {
|
|
|
23
24
|
]);
|
|
24
25
|
return useMemo(() => ({
|
|
25
26
|
activity,
|
|
27
|
+
layerRef,
|
|
26
28
|
stateProps,
|
|
27
29
|
activityProps: elementProps({
|
|
28
30
|
"data-part": appScreenAnatomy.activity,
|
|
@@ -82,6 +82,34 @@ function clearAllStyles(t) {
|
|
|
82
82
|
for (const el of all) clearStyles(el);
|
|
83
83
|
}
|
|
84
84
|
/**
|
|
85
|
+
* Remove all leftover inline styles from the top activity once the
|
|
86
|
+
* transition has settled (globalTransitionState === "idle").
|
|
87
|
+
*
|
|
88
|
+
* The top + behind pair model only handles the single immediately adjacent
|
|
89
|
+
* behind layer. When transitions overlap (concurrent pop, swipe-back race,
|
|
90
|
+
* etc.) the landing screen can get stuck with the temporary styles of a
|
|
91
|
+
* "behind" or an "exiting top" — the layer stays at -30% and shifts by 1/3,
|
|
92
|
+
* or the appBar root stays at opacity 0 (setPostExitPositions) and the whole
|
|
93
|
+
* app bar disappears.
|
|
94
|
+
*
|
|
95
|
+
* At idle the top must always be in a clean default state (setIdlePositions
|
|
96
|
+
* only clears the top and styles the behind), so wipe any leftover inline
|
|
97
|
+
* back to the CSS defaults (layer 0%, appBar visible). The behind is left
|
|
98
|
+
* untouched, and it's a no-op if already clean.
|
|
99
|
+
*/
|
|
100
|
+
function clearTopActivityStyles(stackEl) {
|
|
101
|
+
const t = findTransitionTargets(stackEl);
|
|
102
|
+
const topParts = [
|
|
103
|
+
t.topLayer,
|
|
104
|
+
t.topDim,
|
|
105
|
+
t.topTitle,
|
|
106
|
+
t.topAppBarRoot,
|
|
107
|
+
t.topAppBarBackground,
|
|
108
|
+
...t.topIcons
|
|
109
|
+
];
|
|
110
|
+
for (const el of topParts) clearStyles(el);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
85
113
|
* Set idle positions after push completes.
|
|
86
114
|
* Pattern: clear everything first, then set only non-default positions.
|
|
87
115
|
*/
|
|
@@ -139,6 +167,7 @@ function applySwipeStyles(t, displacement, ratio) {
|
|
|
139
167
|
//#endregion
|
|
140
168
|
exports.applySwipeStyles = applySwipeStyles;
|
|
141
169
|
exports.clearAllStyles = clearAllStyles;
|
|
170
|
+
exports.clearTopActivityStyles = clearTopActivityStyles;
|
|
142
171
|
exports.findTransitionTargets = findTransitionTargets;
|
|
143
172
|
exports.readTransitionStyle = readTransitionStyle;
|
|
144
173
|
exports.setIdlePositions = setIdlePositions;
|
|
@@ -30,6 +30,23 @@ export declare function setOpacity(el: HTMLElement | null, value: string): void;
|
|
|
30
30
|
* This is the foundation — call before setting any specific positions.
|
|
31
31
|
*/
|
|
32
32
|
export declare function clearAllStyles(t: TransitionTargets): void;
|
|
33
|
+
/**
|
|
34
|
+
* Remove all leftover inline styles from the top activity once the
|
|
35
|
+
* transition has settled (globalTransitionState === "idle").
|
|
36
|
+
*
|
|
37
|
+
* The top + behind pair model only handles the single immediately adjacent
|
|
38
|
+
* behind layer. When transitions overlap (concurrent pop, swipe-back race,
|
|
39
|
+
* etc.) the landing screen can get stuck with the temporary styles of a
|
|
40
|
+
* "behind" or an "exiting top" — the layer stays at -30% and shifts by 1/3,
|
|
41
|
+
* or the appBar root stays at opacity 0 (setPostExitPositions) and the whole
|
|
42
|
+
* app bar disappears.
|
|
43
|
+
*
|
|
44
|
+
* At idle the top must always be in a clean default state (setIdlePositions
|
|
45
|
+
* only clears the top and styles the behind), so wipe any leftover inline
|
|
46
|
+
* back to the CSS defaults (layer 0%, appBar visible). The behind is left
|
|
47
|
+
* untouched, and it's a no-op if already clean.
|
|
48
|
+
*/
|
|
49
|
+
export declare function clearTopActivityStyles(stackEl: HTMLElement): void;
|
|
33
50
|
/**
|
|
34
51
|
* Set idle positions after push completes.
|
|
35
52
|
* Pattern: clear everything first, then set only non-default positions.
|
|
@@ -82,6 +82,34 @@ function clearAllStyles(t) {
|
|
|
82
82
|
for (const el of all) clearStyles(el);
|
|
83
83
|
}
|
|
84
84
|
/**
|
|
85
|
+
* Remove all leftover inline styles from the top activity once the
|
|
86
|
+
* transition has settled (globalTransitionState === "idle").
|
|
87
|
+
*
|
|
88
|
+
* The top + behind pair model only handles the single immediately adjacent
|
|
89
|
+
* behind layer. When transitions overlap (concurrent pop, swipe-back race,
|
|
90
|
+
* etc.) the landing screen can get stuck with the temporary styles of a
|
|
91
|
+
* "behind" or an "exiting top" — the layer stays at -30% and shifts by 1/3,
|
|
92
|
+
* or the appBar root stays at opacity 0 (setPostExitPositions) and the whole
|
|
93
|
+
* app bar disappears.
|
|
94
|
+
*
|
|
95
|
+
* At idle the top must always be in a clean default state (setIdlePositions
|
|
96
|
+
* only clears the top and styles the behind), so wipe any leftover inline
|
|
97
|
+
* back to the CSS defaults (layer 0%, appBar visible). The behind is left
|
|
98
|
+
* untouched, and it's a no-op if already clean.
|
|
99
|
+
*/
|
|
100
|
+
function clearTopActivityStyles(stackEl) {
|
|
101
|
+
const t = findTransitionTargets(stackEl);
|
|
102
|
+
const topParts = [
|
|
103
|
+
t.topLayer,
|
|
104
|
+
t.topDim,
|
|
105
|
+
t.topTitle,
|
|
106
|
+
t.topAppBarRoot,
|
|
107
|
+
t.topAppBarBackground,
|
|
108
|
+
...t.topIcons
|
|
109
|
+
];
|
|
110
|
+
for (const el of topParts) clearStyles(el);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
85
113
|
* Set idle positions after push completes.
|
|
86
114
|
* Pattern: clear everything first, then set only non-default positions.
|
|
87
115
|
*/
|
|
@@ -137,4 +165,4 @@ function applySwipeStyles(t, displacement, ratio) {
|
|
|
137
165
|
for (const icon of t.behindIcons) setOpacity(icon, `${ratio}`);
|
|
138
166
|
}
|
|
139
167
|
//#endregion
|
|
140
|
-
export { applySwipeStyles, clearAllStyles, findTransitionTargets, readTransitionStyle, setIdlePositions, setOpacity, setPostExitPositions, setTransform };
|
|
168
|
+
export { applySwipeStyles, clearAllStyles, clearTopActivityStyles, findTransitionTargets, readTransitionStyle, setIdlePositions, setOpacity, setPostExitPositions, setTransform };
|
|
@@ -4,6 +4,7 @@ const require_useTopActivity = require("../private/useTopActivity.cjs");
|
|
|
4
4
|
const require_dom = require("./dom.cjs");
|
|
5
5
|
const require_animation = require("./animation.cjs");
|
|
6
6
|
let react = require("react");
|
|
7
|
+
let _stackflow_react = require("@stackflow/react");
|
|
7
8
|
//#region src/primitive/GlobalInteraction/useGlobalInteraction.ts
|
|
8
9
|
var IDLE_CONTEXT = Object.freeze({
|
|
9
10
|
x0: 0,
|
|
@@ -135,6 +136,7 @@ function useGlobalInteraction() {
|
|
|
135
136
|
setSwipeBackState
|
|
136
137
|
]);
|
|
137
138
|
const topActivity = require_useTopActivity.useTopActivity();
|
|
139
|
+
const stack = (0, _stackflow_react.useStack)();
|
|
138
140
|
const prevTransitionStateRef = (0, react.useRef)(topActivity.transitionState);
|
|
139
141
|
(0, react.useLayoutEffect)(() => {
|
|
140
142
|
const prev = prevTransitionStateRef.current;
|
|
@@ -183,6 +185,11 @@ function useGlobalInteraction() {
|
|
|
183
185
|
stopRunningAnims,
|
|
184
186
|
cancelPendingPushRAF
|
|
185
187
|
]);
|
|
188
|
+
(0, react.useLayoutEffect)(() => {
|
|
189
|
+
if (stack?.globalTransitionState !== "idle") return;
|
|
190
|
+
const stackEl = stackRef.current;
|
|
191
|
+
if (stackEl) require_dom.clearTopActivityStyles(stackEl);
|
|
192
|
+
}, [stack?.globalTransitionState]);
|
|
186
193
|
(0, react.useEffect)(() => {
|
|
187
194
|
return () => {
|
|
188
195
|
cancelPendingPushRAF();
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { useTopActivity } from "../private/useTopActivity.js";
|
|
3
|
-
import { applySwipeStyles, clearAllStyles, findTransitionTargets, readTransitionStyle, setIdlePositions, setPostExitPositions } from "./dom.js";
|
|
3
|
+
import { applySwipeStyles, clearAllStyles, clearTopActivityStyles, findTransitionTargets, readTransitionStyle, setIdlePositions, setPostExitPositions } from "./dom.js";
|
|
4
4
|
import { animateSwipeCancel, animateSwipeComplete, animateTransition, cancelAll, scrubAppBarBackground } from "./animation.js";
|
|
5
5
|
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from "react";
|
|
6
|
+
import { useStack } from "@stackflow/react";
|
|
6
7
|
//#region src/primitive/GlobalInteraction/useGlobalInteraction.ts
|
|
7
8
|
var IDLE_CONTEXT = Object.freeze({
|
|
8
9
|
x0: 0,
|
|
@@ -134,6 +135,7 @@ function useGlobalInteraction() {
|
|
|
134
135
|
setSwipeBackState
|
|
135
136
|
]);
|
|
136
137
|
const topActivity = useTopActivity();
|
|
138
|
+
const stack = useStack();
|
|
137
139
|
const prevTransitionStateRef = useRef(topActivity.transitionState);
|
|
138
140
|
useLayoutEffect(() => {
|
|
139
141
|
const prev = prevTransitionStateRef.current;
|
|
@@ -182,6 +184,11 @@ function useGlobalInteraction() {
|
|
|
182
184
|
stopRunningAnims,
|
|
183
185
|
cancelPendingPushRAF
|
|
184
186
|
]);
|
|
187
|
+
useLayoutEffect(() => {
|
|
188
|
+
if (stack?.globalTransitionState !== "idle") return;
|
|
189
|
+
const stackEl = stackRef.current;
|
|
190
|
+
if (stackEl) clearTopActivityStyles(stackEl);
|
|
191
|
+
}, [stack?.globalTransitionState]);
|
|
185
192
|
useEffect(() => {
|
|
186
193
|
return () => {
|
|
187
194
|
cancelPendingPushRAF();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seed-design/stackflow",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/daangn/seed-design.git",
|
|
@@ -28,15 +28,16 @@
|
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@radix-ui/react-compose-refs": "^1.1.2",
|
|
31
|
+
"@radix-ui/react-focus-scope": "^1.1.4",
|
|
31
32
|
"@radix-ui/react-slot": "^1.2.3",
|
|
32
33
|
"@radix-ui/react-use-callback-ref": "^1.1.1",
|
|
33
|
-
"@seed-design/dom-utils": "
|
|
34
|
-
"@seed-design/react-primitive": "
|
|
34
|
+
"@seed-design/dom-utils": "^2.0.0",
|
|
35
|
+
"@seed-design/react-primitive": "^2.0.0",
|
|
35
36
|
"@stackflow/react-ui-core": "^1.3.3",
|
|
36
37
|
"clsx": "^2.1.1"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|
|
39
|
-
"@seed-design/css": "
|
|
40
|
+
"@seed-design/css": "2.0.0",
|
|
40
41
|
"@stackflow/core": "^1.2.0",
|
|
41
42
|
"@stackflow/react": "^1.5.1",
|
|
42
43
|
"@types/react": "^19.2.2",
|
|
@@ -49,7 +50,7 @@
|
|
|
49
50
|
"vite-plugin-dts": "^5.0.0"
|
|
50
51
|
},
|
|
51
52
|
"peerDependencies": {
|
|
52
|
-
"@seed-design/css": "
|
|
53
|
+
"@seed-design/css": "^2.0.0",
|
|
53
54
|
"@stackflow/core": ">=1.1.0",
|
|
54
55
|
"@stackflow/react": ">=1.4.1",
|
|
55
56
|
"react": ">=18.0.0",
|
|
@@ -26,12 +26,7 @@ export const AppBarPropsProvider = PropsProvider;
|
|
|
26
26
|
export interface AppBarProps
|
|
27
27
|
extends AppBarVariantProps,
|
|
28
28
|
AppBarPrimitive.RootProps,
|
|
29
|
-
BoxBackgroundProps {
|
|
30
|
-
/**
|
|
31
|
-
* @deprecated SEED React 1.3에서 제거될 예정인 옵션입니다. 옵션 제거 이후에는 AppBar 하단 구분선이 표시되지 않습니다.
|
|
32
|
-
*/
|
|
33
|
-
divider?: AppBarVariantProps["divider"];
|
|
34
|
-
}
|
|
29
|
+
BoxBackgroundProps {}
|
|
35
30
|
|
|
36
31
|
export const AppBarRoot = forwardRef<HTMLDivElement, AppBarProps>((props, ref) => {
|
|
37
32
|
const { style: boxStyle, restProps: propsWithoutBoxProps } = useBoxBackgroundProps(props);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { composeRefs } from "@radix-ui/react-compose-refs";
|
|
2
|
+
import { FocusScope } from "@radix-ui/react-focus-scope";
|
|
2
3
|
import { mergeProps } from "@seed-design/dom-utils";
|
|
3
4
|
import { Primitive, type PrimitiveProps } from "@seed-design/react-primitive";
|
|
4
5
|
import { usePreventTouchDuringTransition } from "@stackflow/react-ui-core";
|
|
5
|
-
import { forwardRef, useRef } from "react";
|
|
6
|
+
import { forwardRef, useEffect, useRef } from "react";
|
|
6
7
|
import { useAppScreen, type UseAppScreenProps } from "./useAppScreen";
|
|
7
8
|
import { AppScreenProvider, useAppScreenContext } from "./useAppScreenContext";
|
|
8
9
|
|
|
@@ -32,13 +33,29 @@ export const AppScreenRoot = forwardRef<HTMLDivElement, AppScreenRootProps>((pro
|
|
|
32
33
|
ref: innerRef as React.RefObject<HTMLDivElement>,
|
|
33
34
|
});
|
|
34
35
|
|
|
36
|
+
// Focus the layer once after the enter animation completes.
|
|
37
|
+
// onMountAutoFocus fires during enter-active and interrupts the CSS animation, so we
|
|
38
|
+
// defer until transitionState reaches "enter-done". This only happens once per activity
|
|
39
|
+
// lifecycle — subsequent isTop changes (e.g. upper activity pop) don't change
|
|
40
|
+
// transitionState, so the effect won't re-fire.
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (api.activity?.transitionState === "enter-done") {
|
|
43
|
+
api.layerRef.current?.focus();
|
|
44
|
+
}
|
|
45
|
+
}, [api.activity?.transitionState, api.layerRef]);
|
|
46
|
+
|
|
35
47
|
return (
|
|
36
48
|
<AppScreenProvider value={api}>
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
49
|
+
{/* onMountAutoFocus is prevented because it fires during enter-active and
|
|
50
|
+
interrupts the CSS animation. Focus is handled by the useEffect above instead,
|
|
51
|
+
which defers until transitionState reaches "enter-done". */}
|
|
52
|
+
<FocusScope asChild trapped loop onMountAutoFocus={(e) => e.preventDefault()}>
|
|
53
|
+
<Primitive.div
|
|
54
|
+
ref={composeRefs(innerRef, ref)}
|
|
55
|
+
data-stackflow-component-name="AppScreen"
|
|
56
|
+
{...mergeProps(api.activityProps, otherProps)}
|
|
57
|
+
/>
|
|
58
|
+
</FocusScope>
|
|
42
59
|
</AppScreenProvider>
|
|
43
60
|
);
|
|
44
61
|
});
|
|
@@ -65,8 +82,14 @@ AppScreenEdge.displayName = "AppScreenEdge";
|
|
|
65
82
|
export interface AppScreenLayerProps extends PrimitiveProps, React.HTMLAttributes<HTMLDivElement> {}
|
|
66
83
|
|
|
67
84
|
export const AppScreenLayer = forwardRef<HTMLDivElement, AppScreenLayerProps>((props, ref) => {
|
|
68
|
-
const { layerProps } = useAppScreenContext();
|
|
85
|
+
const { layerProps, layerRef } = useAppScreenContext();
|
|
69
86
|
|
|
70
|
-
return
|
|
87
|
+
return (
|
|
88
|
+
<Primitive.div
|
|
89
|
+
ref={composeRefs(ref, layerRef)}
|
|
90
|
+
tabIndex={-1}
|
|
91
|
+
{...mergeProps(layerProps, props)}
|
|
92
|
+
/>
|
|
93
|
+
);
|
|
71
94
|
});
|
|
72
95
|
AppScreenLayer.displayName = "AppScreenLayer";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { dataAttr, elementProps } from "@seed-design/dom-utils";
|
|
2
2
|
import { useNullableActivity } from "@stackflow/react-ui-core";
|
|
3
|
-
import { useMemo } from "react";
|
|
3
|
+
import { useMemo, useRef } from "react";
|
|
4
4
|
import { type UseSwipeBackProps, useSwipeBack } from "../GlobalInteraction/useSwipeBack";
|
|
5
5
|
import { useActivityZIndexBase } from "../../hooks";
|
|
6
6
|
import { appScreenAnatomy } from "./anatomy";
|
|
@@ -11,6 +11,7 @@ export type UseAppScreenReturn = ReturnType<typeof useAppScreen>;
|
|
|
11
11
|
|
|
12
12
|
export function useAppScreen(props: UseAppScreenProps) {
|
|
13
13
|
const activity = useNullableActivity();
|
|
14
|
+
const layerRef = useRef<HTMLDivElement>(null);
|
|
14
15
|
|
|
15
16
|
const transitionState = activity?.transitionState ?? "enter-done";
|
|
16
17
|
|
|
@@ -37,6 +38,7 @@ export function useAppScreen(props: UseAppScreenProps) {
|
|
|
37
38
|
return useMemo(
|
|
38
39
|
() => ({
|
|
39
40
|
activity,
|
|
41
|
+
layerRef,
|
|
40
42
|
stateProps,
|
|
41
43
|
activityProps: elementProps({
|
|
42
44
|
"data-part": appScreenAnatomy.activity,
|
|
@@ -138,10 +138,13 @@ function collectAnimations(anims: (Animation | null)[], durationMs: number): Ani
|
|
|
138
138
|
|
|
139
139
|
// ─── iOS Slide ──────────────────────────────────────────────────────────────
|
|
140
140
|
|
|
141
|
-
|
|
141
|
+
// Intersect with the DOM `Keyframe` so values of this type satisfy the
|
|
142
|
+
// `[property: string]` index signature `el.animate()` expects, while still
|
|
143
|
+
// guaranteeing `opacity` and `transform` are present.
|
|
144
|
+
type TitleKeyframe = Keyframe & {
|
|
142
145
|
opacity: string;
|
|
143
146
|
transform: string;
|
|
144
|
-
}
|
|
147
|
+
};
|
|
145
148
|
|
|
146
149
|
interface IosPositions {
|
|
147
150
|
topLayer: string;
|
|
@@ -127,6 +127,35 @@ export function clearAllStyles(t: TransitionTargets) {
|
|
|
127
127
|
for (const el of all) clearStyles(el);
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Remove all leftover inline styles from the top activity once the
|
|
132
|
+
* transition has settled (globalTransitionState === "idle").
|
|
133
|
+
*
|
|
134
|
+
* The top + behind pair model only handles the single immediately adjacent
|
|
135
|
+
* behind layer. When transitions overlap (concurrent pop, swipe-back race,
|
|
136
|
+
* etc.) the landing screen can get stuck with the temporary styles of a
|
|
137
|
+
* "behind" or an "exiting top" — the layer stays at -30% and shifts by 1/3,
|
|
138
|
+
* or the appBar root stays at opacity 0 (setPostExitPositions) and the whole
|
|
139
|
+
* app bar disappears.
|
|
140
|
+
*
|
|
141
|
+
* At idle the top must always be in a clean default state (setIdlePositions
|
|
142
|
+
* only clears the top and styles the behind), so wipe any leftover inline
|
|
143
|
+
* back to the CSS defaults (layer 0%, appBar visible). The behind is left
|
|
144
|
+
* untouched, and it's a no-op if already clean.
|
|
145
|
+
*/
|
|
146
|
+
export function clearTopActivityStyles(stackEl: HTMLElement) {
|
|
147
|
+
const t = findTransitionTargets(stackEl);
|
|
148
|
+
const topParts = [
|
|
149
|
+
t.topLayer,
|
|
150
|
+
t.topDim,
|
|
151
|
+
t.topTitle,
|
|
152
|
+
t.topAppBarRoot,
|
|
153
|
+
t.topAppBarBackground,
|
|
154
|
+
...t.topIcons,
|
|
155
|
+
];
|
|
156
|
+
for (const el of topParts) clearStyles(el);
|
|
157
|
+
}
|
|
158
|
+
|
|
130
159
|
/**
|
|
131
160
|
* Set idle positions after push completes.
|
|
132
161
|
* Pattern: clear everything first, then set only non-default positions.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useStack } from "@stackflow/react";
|
|
1
2
|
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from "react";
|
|
2
3
|
import { useTopActivity } from "../private/useTopActivity";
|
|
3
4
|
import {
|
|
@@ -7,6 +8,7 @@ import {
|
|
|
7
8
|
readTransitionStyle,
|
|
8
9
|
applySwipeStyles,
|
|
9
10
|
clearAllStyles,
|
|
11
|
+
clearTopActivityStyles,
|
|
10
12
|
setIdlePositions,
|
|
11
13
|
setPostExitPositions,
|
|
12
14
|
} from "./dom";
|
|
@@ -257,6 +259,7 @@ export function useGlobalInteraction() {
|
|
|
257
259
|
}, [stopRunningAnims, stopAppBarBgScrub, setSwipeBackState]);
|
|
258
260
|
|
|
259
261
|
const topActivity = useTopActivity();
|
|
262
|
+
const stack = useStack();
|
|
260
263
|
|
|
261
264
|
// ── WAAPI push/pop transitions triggered by stackflow state changes ──
|
|
262
265
|
const prevTransitionStateRef = useRef<string>(topActivity.transitionState);
|
|
@@ -326,6 +329,26 @@ export function useGlobalInteraction() {
|
|
|
326
329
|
}
|
|
327
330
|
}, [topActivity.transitionState, stopRunningAnims, cancelPendingPushRAF]);
|
|
328
331
|
|
|
332
|
+
// ── Settle safety-net ──
|
|
333
|
+
// The top + behind pair model only handles the single immediately adjacent
|
|
334
|
+
// behind layer. When transitions overlap (concurrent pop, swipe-back race,
|
|
335
|
+
// etc.) and end on a path the pair model didn't unwind, the landing screen
|
|
336
|
+
// can get stuck with temporary styles — the layer stays at -30% and shifts
|
|
337
|
+
// by 1/3, or the appBar root stays at opacity 0 (setPostExitPositions) and
|
|
338
|
+
// the whole app bar disappears.
|
|
339
|
+
//
|
|
340
|
+
// Guarantee: once everything settles (globalTransitionState === "idle") the
|
|
341
|
+
// top must always be in a clean default state. Wipe all leftover inline
|
|
342
|
+
// styles from the top activity — the behind is left untouched (stays at
|
|
343
|
+
// -30%), this only runs at idle, and it's a no-op if already clean.
|
|
344
|
+
useLayoutEffect(() => {
|
|
345
|
+
if (stack?.globalTransitionState !== "idle") return;
|
|
346
|
+
const stackEl = stackRef.current;
|
|
347
|
+
if (stackEl) {
|
|
348
|
+
clearTopActivityStyles(stackEl);
|
|
349
|
+
}
|
|
350
|
+
}, [stack?.globalTransitionState]);
|
|
351
|
+
|
|
329
352
|
// Cancel any pending push rAF and running animations on unmount so
|
|
330
353
|
// late-firing finished handlers can't run against a torn-down stack.
|
|
331
354
|
useEffect(() => {
|