ReviewOS
@if (!pullRequest)

No such pull request

stacks/stx has no pull request numbered 2007.

@else

also looking at this

stacks/stx

fix(components): Button reads disabled and loading as live props

#2007
Merged glennmichael123 wants to merge fix/button-reactive-disabled-loading into main
3 files +255 -25
@if (showWholeFiles)
packages/components/test/visual/__snapshots__/button-template.snap.txtmodified+56-11
Changes to packages/components/test/visual/__snapshots__/button-template.snap.txt
@@ -42,10 +42,42 @@ export const spinnerClasses = `animate-spin ${iconClasses}`.trim()
4242</script>
4343<script client>
4444 const emit = defineEmits()
45const disabledNow =
45
46/*
47 * `disabled` and `loading` are read as live props, not as a server snapshot.
48 *
49 * This used to be `const disabledNow = ` — a value computed
50 * before the page was sent and never looked at again. So the most common piece
51 * of button state in an application could not be expressed:
52 *
53 *
54 <Button :disabled="saving()" @click="save">
55 Save
56 </Button>
57 *
58 * `saving()` is a signal. On the server it is not yet meaningful, so the button
59 * shipped enabled and STAYED enabled for the life of the page — through the
60 * exact async action it was there to guard. Double submits, destructive
61 * deletes and rotate-key confirmations all land on this.
62 *
63 * useReactiveProp wires the parent's binding into a local signal: the parent's
64 * bindAttr writes the attribute on our root, a MutationObserver picks it up,
65 * and the signal flips. Dialog already does this for `open` (#1704) and Switch
66 * for `checked`, so this is the package's existing answer rather than a new one.
67 *
68 * See stacksjs/stx#1997.
69 */
70const isLoading = useReactiveProp('loading', )
71const disabledProp = useReactiveProp('disabled', )
72
73// Loading implies disabled, the same combination the server computes for the
74// initial render, so the two agree before and after hydration.
75const isDisabled = derived(() => disabledProp() || isLoading())
4676
4777function onClick(event) {
48 if (disabledNow) {
78 // Read through the signal: a button disabled after render must stop guarding
79 // on the live value, not on what was true when the page was built.
80 if (isDisabled()) {
4981 event.preventDefault()
5082 return
5183 }
@@ -56,26 +88,37 @@ function onClick(event) {
5688 type=""
5789 class=""
5890
59 @click="onClick($event)"
91 :disabled="isDisabled()"
6092 aria-busy="false"
93 :aria-busy="isLoading() ? 'true' : 'false'"
94 @click="onClick($event)"
6195>
62
70
76119 <slot />
77