Skip to content

Commit c1015e5

Browse files
feat(components): migrate 8 more components to useReactiveProp (#1704)
Drawer, Radio, Select, Textarea, Tooltip, Progress, Pagination, and SidebarSection now use useReactiveProp for their canonical prop instead of the legacy state({{ prop }}) capture-once pattern. Brings the total to 14 migrated components, covering all the common reactive primitives (open, checked, value, show, current-page, expanded). Each migration is a single-line swap. The default useReactiveProp parser handles booleans (open/checked/show/expanded), numbers (current-page, progress value), and strings (input/textarea/select value via explicit parse opt to coerce null→''). Updated test/edge-cases/issue-fixes.test.ts to assert all 14 migrated components actually invoke useReactiveProp for their canonical prop. Components left on capture-once: Notification (intentional — uses defineExpose's open(opts) for imperative control), Tabs / Accordion (active state is owned by the container, not driven from outside), CommandPalette (similar — open state is imperative).
1 parent c0e80b3 commit c1015e5

9 files changed

Lines changed: 27 additions & 8 deletions

File tree

packages/components/src/ui/drawer/Drawer.stx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@ export const closeButtonClasses = `absolute top-0 ${closeButtonPosition[position
3232

3333
<script client>
3434
const emit = defineEmits()
35-
const isOpen = state({{ open }})
35+
// useReactiveProp lets the parent drive `open` via signals
36+
// (`:open="drawerOpen()"`) instead of the prop being captured once.
37+
// See stacksjs/stx#1704.
38+
const isOpen = useReactiveProp('open', {{ open }})
3639

3740
function close() {
3841
isOpen.set(false)

packages/components/src/ui/pagination/Pagination.stx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ export const iconClasses = `w-5 h-5`.trim()
1717
const emit = defineEmits()
1818
const totalPages = {{ totalPages }}
1919
const siblingCount = {{ siblingCount }}
20-
const page = state({{ currentPage }})
20+
// Reactive prop binding — parent can drive `current-page` via signals
21+
// (useful for URL-driven pagination via useSearchParams). See #1704.
22+
const page = useReactiveProp('current-page', {{ currentPage }})
2123

2224
function goToPage(target) {
2325
if (target >= 1 && target <= totalPages && target !== page()) {

packages/components/src/ui/progress/Progress.stx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ export const circularBarColor = color === 'primary' ? 'stroke-blue-600 dark:stro
4545
const max = {{ max }}
4646
const indeterminate = {{ indeterminate }}
4747
const circumference = {{ circumference }}
48-
const value = state({{ value }})
48+
// Reactive prop binding — parent can drive `value` via signals. See #1704.
49+
const value = useReactiveProp('value', {{ value }})
4950

5051
const percentage = derived(() => Math.min(Math.max((value() / max) * 100, 0), 100))
5152
const percentLabel = derived(() => `${Math.round(percentage())}%`)

packages/components/src/ui/radio/Radio.stx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ export const inputId = `radio-${value || label}`
2525
const emit = defineEmits()
2626
const isDisabled = {{ disabled }}
2727
const radioValue = {{ value }}
28-
const isChecked = state({{ checked }})
28+
// Reactive prop binding — parent can drive `checked` via signals. See #1704.
29+
const isChecked = useReactiveProp('checked', {{ checked }})
2930

3031
function onInputChange(event) {
3132
if (isDisabled) {

packages/components/src/ui/select/Select.stx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ export const selectId = `select-${value}`
2727

2828
<script client>
2929
const emit = defineEmits()
30-
const selectedValue = state({{ value }})
30+
// Reactive prop binding — parent can drive `value` via signals. See #1704.
31+
const selectedValue = useReactiveProp('value', {{ value }}, { parse: (v) => v == null ? '' : String(v) })
3132

3233
function onChange(event) {
3334
selectedValue.set(event.target.value)

packages/components/src/ui/sidebar/SidebarSection.stx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ export const labelClasses = variant === 'workspace' || variant === 'desktop'
3737
const emit = defineEmits()
3838
const sectionId = {{ id }}
3939
const collapsibleProp = {{ collapsible }}
40-
const isExpanded = state({{ expanded }})
40+
// Reactive prop binding — parent can drive `expanded` via signals. See #1704.
41+
const isExpanded = useReactiveProp('expanded', {{ expanded }})
4142

4243
function onHeaderClick() {
4344
if (!collapsibleProp) return

packages/components/src/ui/textarea/Textarea.stx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ export const helperClasses = `mt-2 text-sm ${error ? 'text-red-600 dark:text-red
3535
const emit = defineEmits()
3636
const autoResize = {{ autoResize }}
3737
const maxRows = {{ maxRows }}
38-
const inputValue = state({{ value }})
38+
// Reactive prop binding — parent can drive `value` via signals. See #1704.
39+
const inputValue = useReactiveProp('value', {{ value }}, { parse: (v) => v == null ? '' : String(v) })
3940

4041
function resizeTextarea(el) {
4142
if (!autoResize) return

packages/components/src/ui/tooltip/Tooltip.stx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ export const arrowClass = `absolute w-0 h-0 border-4 border-transparent ${arrowC
2727
<script client>
2828
const delay = {{ delay }}
2929
const isDisabled = {{ disabled }}
30-
const isVisible = state({{ show }})
30+
// Reactive prop binding — parent can drive `show` via signals. See #1704.
31+
const isVisible = useReactiveProp('show', {{ show }})
3132
let timeoutId = null
3233

3334
function showTooltip() {

packages/stx/test/edge-cases/issue-fixes.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,11 +364,19 @@ describe('#1704 — useReactiveProp bridges parent clientReactive props into chi
364364
const componentsDir = path.resolve(__dirname, '../../../components/src/ui')
365365
const checks: Array<[string, string]> = [
366366
['dialog/Dialog.stx', "useReactiveProp('open',"],
367+
['drawer/Drawer.stx', "useReactiveProp('open',"],
367368
['switch/Switch.stx', "useReactiveProp('checked',"],
368369
['checkbox/Checkbox.stx', "useReactiveProp('checked',"],
370+
['radio/Radio.stx', "useReactiveProp('checked',"],
369371
['input/TextInput.stx', "useReactiveProp('value',"],
370372
['input/PasswordInput.stx', "useReactiveProp('value',"],
371373
['input/NumberInput.stx', "useReactiveProp('value',"],
374+
['select/Select.stx', "useReactiveProp('value',"],
375+
['textarea/Textarea.stx', "useReactiveProp('value',"],
376+
['progress/Progress.stx', "useReactiveProp('value',"],
377+
['tooltip/Tooltip.stx', "useReactiveProp('show',"],
378+
['pagination/Pagination.stx', "useReactiveProp('current-page',"],
379+
['sidebar/SidebarSection.stx', "useReactiveProp('expanded',"],
372380
]
373381
for (const [file, marker] of checks) {
374382
const src = fs.readFileSync(path.join(componentsDir, file), 'utf8')

0 commit comments

Comments
 (0)