Skip to content

Commit 5643c83

Browse files
committed
refactor: radio-group
1 parent 342f2bf commit 5643c83

File tree

10 files changed

+551
-322
lines changed

10 files changed

+551
-322
lines changed

.xstate/hover-card.js

Lines changed: 219 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,65 @@ const {
1010
choose
1111
} = actions;
1212
const fetchMachine = createMachine({
13-
id: "hover-card",
14-
initial: ctx.open ? "open" : "closed",
15-
context: {
16-
"isOpenControlled": false,
17-
"isOpenControlled": false,
18-
"isOpenControlled && !isPointer": false,
19-
"!isPointer": false,
20-
"isOpenControlled": false,
21-
"isOpenControlled": false,
22-
"isOpenControlled && !isPointer": false,
23-
"!isPointer": false,
24-
"isOpenControlled": false
13+
props({
14+
props
15+
}) {
16+
return {
17+
openDelay: 700,
18+
closeDelay: 300,
19+
...compact(props),
20+
positioning: {
21+
placement: "bottom",
22+
...props.positioning
23+
}
24+
};
25+
},
26+
initialState({
27+
prop
28+
}) {
29+
const open = prop("open") || prop("defaultOpen");
30+
return open ? "open" : "closed";
31+
},
32+
context({
33+
prop,
34+
bindable
35+
}) {
36+
return {
37+
open: bindable < boolean > (() => ({
38+
defaultValue: prop("defaultOpen"),
39+
value: prop("open"),
40+
onChange(value) {
41+
prop("onOpenChange")?.({
42+
open: value
43+
});
44+
}
45+
})),
46+
currentPlacement: bindable < Placement | undefined > (() => ({
47+
defaultValue: undefined
48+
})),
49+
isPointer: bindable < boolean > (() => ({
50+
defaultValue: false
51+
}))
52+
};
53+
},
54+
watch({
55+
track,
56+
context: {
57+
"isOpenControlled": false,
58+
"isOpenControlled": false,
59+
"isOpenControlled && !isPointer": false,
60+
"!isPointer": false,
61+
"isOpenControlled": false,
62+
"isOpenControlled": false,
63+
"isOpenControlled && !isPointer": false,
64+
"!isPointer": false,
65+
"isOpenControlled": false
66+
},
67+
action
68+
}) {
69+
track([() => context.get("open")], () => {
70+
action(["toggleVisibility"]);
71+
});
2572
},
2673
on: {
2774
UPDATE_CONTEXT: {
@@ -33,29 +80,38 @@ const fetchMachine = createMachine({
3380
tags: ["closed"],
3481
entry: ["clearIsPointer"],
3582
on: {
36-
"CONTROLLED.OPEN": "open",
83+
"CONTROLLED.OPEN": {
84+
target: "open"
85+
},
3786
POINTER_ENTER: {
3887
target: "opening",
3988
actions: ["setIsPointer"]
4089
},
41-
TRIGGER_FOCUS: "opening",
42-
OPEN: "opening"
90+
TRIGGER_FOCUS: {
91+
target: "opening"
92+
},
93+
OPEN: {
94+
target: "opening"
95+
}
4396
}
4497
},
4598
opening: {
4699
tags: ["closed"],
47-
after: {
100+
effects: ["waitForOpenDelay"],
101+
on: {
48102
OPEN_DELAY: [{
49103
cond: "isOpenControlled",
50104
actions: ["invokeOnOpen"]
51105
}, {
52106
target: "open",
53107
actions: ["invokeOnOpen"]
54-
}]
55-
},
56-
on: {
57-
"CONTROLLED.OPEN": "open",
58-
"CONTROLLED.CLOSE": "closed",
108+
}],
109+
"CONTROLLED.OPEN": {
110+
target: "open"
111+
},
112+
"CONTROLLED.CLOSE": {
113+
target: "closed"
114+
},
59115
POINTER_LEAVE: [{
60116
cond: "isOpenControlled",
61117
// We trigger toggleVisibility manually since the `ctx.open` has not changed yet (at this point)
@@ -85,13 +141,17 @@ const fetchMachine = createMachine({
85141
},
86142
open: {
87143
tags: ["open"],
88-
activities: ["trackDismissableElement", "trackPositioning"],
144+
effects: ["trackDismissableElement", "trackPositioning"],
89145
on: {
90-
"CONTROLLED.CLOSE": "closed",
146+
"CONTROLLED.CLOSE": {
147+
target: "closed"
148+
},
91149
POINTER_ENTER: {
92150
actions: ["setIsPointer"]
93151
},
94-
POINTER_LEAVE: "closing",
152+
POINTER_LEAVE: {
153+
target: "closing"
154+
},
95155
CLOSE: [{
96156
cond: "isOpenControlled",
97157
actions: ["invokeOnClose"]
@@ -108,32 +168,159 @@ const fetchMachine = createMachine({
108168
actions: ["invokeOnClose"]
109169
}],
110170
"POSITIONING.SET": {
111-
actions: "reposition"
171+
actions: ["reposition"]
112172
}
113173
}
114174
},
115175
closing: {
116176
tags: ["open"],
117-
activities: ["trackPositioning"],
118-
after: {
177+
effects: ["trackPositioning", "waitForCloseDelay"],
178+
on: {
119179
CLOSE_DELAY: [{
120180
cond: "isOpenControlled",
121181
actions: ["invokeOnClose"]
122182
}, {
123183
target: "closed",
124184
actions: ["invokeOnClose"]
125-
}]
126-
},
127-
on: {
128-
"CONTROLLED.CLOSE": "closed",
129-
"CONTROLLED.OPEN": "open",
185+
}],
186+
"CONTROLLED.CLOSE": {
187+
target: "closed"
188+
},
189+
"CONTROLLED.OPEN": {
190+
target: "open"
191+
},
130192
POINTER_ENTER: {
131193
target: "open",
132194
// no need to invokeOnOpen here because it's still open (but about to close)
133195
actions: ["setIsPointer"]
134196
}
135197
}
136198
}
199+
},
200+
implementations: {
201+
guards: {
202+
isPointer: ({
203+
context
204+
}) => !!context.get("isPointer"),
205+
isOpenControlled: ({
206+
prop
207+
}) => prop("open") != null
208+
},
209+
effects: {
210+
waitForOpenDelay({
211+
send,
212+
prop
213+
}) {
214+
const id = setTimeout(() => {
215+
send({
216+
type: "OPEN_DELAY"
217+
});
218+
}, prop("openDelay"));
219+
return () => clearTimeout(id);
220+
},
221+
waitForCloseDelay({
222+
send,
223+
prop
224+
}) {
225+
const id = setTimeout(() => {
226+
send({
227+
type: "CLOSE_DELAY"
228+
});
229+
}, prop("closeDelay"));
230+
return () => clearTimeout(id);
231+
},
232+
trackPositioning({
233+
context,
234+
prop,
235+
scope
236+
}) {
237+
if (!context.get("currentPlacement")) {
238+
context.set("currentPlacement", prop("positioning").placement);
239+
}
240+
const getPositionerEl = () => dom.getPositionerEl(scope);
241+
return getPlacement(dom.getTriggerEl(scope), getPositionerEl, {
242+
...prop("positioning"),
243+
defer: true,
244+
onComplete(data) {
245+
context.set("currentPlacement", data.placement);
246+
}
247+
});
248+
},
249+
trackDismissableElement({
250+
send,
251+
scope
252+
}) {
253+
const getContentEl = () => dom.getContentEl(scope);
254+
return trackDismissableElement(getContentEl, {
255+
defer: true,
256+
exclude: [dom.getTriggerEl(scope)],
257+
onDismiss() {
258+
send({
259+
type: "CLOSE",
260+
src: "interact-outside"
261+
});
262+
},
263+
onFocusOutside(event) {
264+
event.preventDefault();
265+
}
266+
});
267+
}
268+
},
269+
actions: {
270+
invokeOnClose({
271+
prop
272+
}) {
273+
prop("onOpenChange")?.({
274+
open: false
275+
});
276+
},
277+
invokeOnOpen({
278+
prop
279+
}) {
280+
prop("onOpenChange")?.({
281+
open: true
282+
});
283+
},
284+
setIsPointer({
285+
context
286+
}) {
287+
context.set("isPointer", true);
288+
},
289+
clearIsPointer({
290+
context
291+
}) {
292+
context.set("isPointer", false);
293+
},
294+
reposition({
295+
context,
296+
prop,
297+
scope,
298+
event
299+
}) {
300+
const getPositionerEl = () => dom.getPositionerEl(scope);
301+
getPlacement(dom.getTriggerEl(scope), getPositionerEl, {
302+
...prop("positioning"),
303+
...event.options,
304+
defer: true,
305+
listeners: false,
306+
onComplete(data) {
307+
context.set("currentPlacement", data.placement);
308+
}
309+
});
310+
},
311+
toggleVisibility({
312+
prop,
313+
event,
314+
send
315+
}) {
316+
queueMicrotask(() => {
317+
send({
318+
type: prop("open") ? "CONTROLLED.OPEN" : "CONTROLLED.CLOSE",
319+
previousEvent: event
320+
});
321+
});
322+
}
323+
}
137324
}
138325
}, {
139326
actions: {

MIGRATION.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- `api.setCount` is removed in favor of explicitly setting the `count` prop.
66

77
- Machine
8+
89
- `activities` is now renamed to `effects`
910
- prop, context and refs are now explicitly passed to the machine. Prior to this everything was pass to the `context`
1011
object.
@@ -15,6 +16,8 @@
1516
- `useMachine` now returns a `service` object with `send`, `prop`, `context`, `computed` and `scope` properties. It no
1617
longer returns a tuple of `[state, send]`.
1718

19+
- Removed `useActor` hook in favor of `useMachine` everywhere.
20+
1821
## Avatar
1922

2023
### Before

examples/next-ts/pages/radio-group.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export default function Page() {
5757
</main>
5858

5959
<Toolbar controls={controls.ui}>
60-
<StateVisualizer state={state} />
60+
<StateVisualizer state={service} />
6161
</Toolbar>
6262
</>
6363
)

examples/next-ts/pages/time-picker.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,8 @@ import { useControls } from "../hooks/use-controls"
99
export default function Page() {
1010
const controls = useControls(timePickerControls)
1111

12-
const [state, send] = useMachine(timePicker.machine({ id: useId() }), {
13-
context: controls.context,
14-
})
15-
16-
const api = timePicker.connect(state, send, normalizeProps)
12+
const service = useMachine(timePicker.machine, { id: useId() })
13+
const api = timePicker.connect(service, normalizeProps)
1714

1815
return (
1916
<>
@@ -67,7 +64,7 @@ export default function Page() {
6764
</main>
6865

6966
<Toolbar controls={controls.ui} viz>
70-
<StateVisualizer state={state} />
67+
<StateVisualizer state={service} />
7168
</Toolbar>
7269
</>
7370
)

0 commit comments

Comments
 (0)