pythonnative 0.23.0__py3-none-any.whl → 0.25.0__py3-none-any.whl

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.
@@ -1,16 +1,12 @@
1
- """Built-in element factories and the typed prop schemas they share.
1
+ """Built-in element factories.
2
2
 
3
- Each ``@dataclass(frozen=True)`` class in this module (``TextProps``,
4
- ``ButtonProps``, etc.) is the canonical schema for one built-in
5
- component. Each factory function (``Text``, ``Button``, …) is a thin
6
- ergonomic wrapper that builds an [`Element`][pythonnative.Element]
7
- through the shared :func:`_make_element` helper, so style resolution,
8
- ``ref`` attachment, ``None``-default dropping, and forced overrides
9
- (e.g. ``Column``'s fixed ``flex_direction``) live in exactly one place.
10
-
11
- The same Props dataclasses are used by the `pythonnative.sdk` surface
12
- for third-party components, so the built-in API and the extension API
13
- speak the same shape.
3
+ Each factory function (``Text``, ``Button``, …) is a fully-typed thin
4
+ wrapper that builds an [`Element`][pythonnative.Element] through the
5
+ shared :func:`_make_element` helper, so style resolution, ``ref``
6
+ attachment, ``None``-default dropping, and forced overrides (e.g.
7
+ ``Column``'s fixed ``flex_direction``) live in exactly one place. The
8
+ factory signatures themselves are the canonical prop schemas: editors
9
+ and type checkers validate calls directly against them.
14
10
 
15
11
  Example:
16
12
  ```python
@@ -18,27 +14,27 @@ Example:
18
14
 
19
15
  pn.Column(
20
16
  pn.Text("Hello", style=pn.style(font_size=18)),
21
- pn.Button("Tap", on_click=lambda: print("tapped")),
17
+ pn.Button("Tap", on_press=lambda: print("tapped")),
22
18
  style=pn.style(spacing=12, padding=16),
23
19
  )
24
20
  ```
25
21
  """
26
22
 
27
23
  import bisect
28
- from dataclasses import dataclass, field
29
24
  from typing import Any, Callable, Dict, List, Literal, Optional, Tuple, Union
30
25
 
31
26
  from .element import Element
32
27
  from .hooks import (
28
+ Ref,
33
29
  component,
34
- use_effect,
30
+ use_imperative_handle,
35
31
  use_keyboard_height,
36
32
  use_ref,
37
33
  use_safe_area_insets,
38
34
  use_state,
39
35
  )
40
- from .sdk import Props
41
36
  from .style import (
37
+ AccessibilityState,
42
38
  AutoCapitalize,
43
39
  Color,
44
40
  KeyboardType,
@@ -46,6 +42,7 @@ from .style import (
46
42
  ScaleType,
47
43
  StyleProp,
48
44
  resolve_style,
45
+ validate_style_keys,
49
46
  )
50
47
 
51
48
  # ======================================================================
@@ -57,7 +54,7 @@ def _make_element(
57
54
  name: str,
58
55
  *children: Element,
59
56
  style: StyleProp = None,
60
- ref: Optional[Dict[str, Any]] = None,
57
+ ref: Optional[Ref] = None,
61
58
  key: Optional[str] = None,
62
59
  _defaults: Optional[Dict[str, Any]] = None,
63
60
  _forced: Optional[Dict[str, Any]] = None,
@@ -85,8 +82,9 @@ def _make_element(
85
82
  name: Element type name (e.g. ``"Text"``).
86
83
  *children: Child elements.
87
84
  style: Style dict, list of dicts, or ``None``.
88
- ref: Optional ``use_ref()`` dict; the reconciler populates
89
- ``ref["current"]`` with the underlying native view.
85
+ ref: Optional [`Ref`][pythonnative.Ref] from ``use_ref()``; the
86
+ reconciler populates ``ref.current`` with the underlying
87
+ native view.
90
88
  key: Stable identity for keyed reconciliation.
91
89
  _defaults: Internal: fill-only-if-missing prop defaults.
92
90
  _forced: Internal: prop overrides applied last.
@@ -96,6 +94,8 @@ def _make_element(
96
94
  A fresh [`Element`][pythonnative.Element].
97
95
  """
98
96
  out: Dict[str, Any] = dict(resolve_style(style))
97
+ if out:
98
+ validate_style_keys(out, owner=name)
99
99
  if _defaults:
100
100
  for k, v in _defaults.items():
101
101
  out.setdefault(k, v)
@@ -109,352 +109,6 @@ def _make_element(
109
109
  return Element(name, out, list(children), key=key)
110
110
 
111
111
 
112
- # ======================================================================
113
- # Props dataclasses
114
- # ======================================================================
115
- #
116
- # These are the canonical schemas for every built-in component. They
117
- # subclass the SDK's ``Props`` base, so the same shape works for both
118
- # the built-in factory functions and the third-party
119
- # [`element_factory`][pythonnative.element_factory] API.
120
-
121
-
122
- @dataclass(frozen=True)
123
- class TextProps(Props):
124
- """Props for [`Text`][pythonnative.Text]."""
125
-
126
- text: str = ""
127
- spans: Optional[List[Dict[str, Any]]] = None
128
- accessibility_label: Optional[str] = None
129
- accessibility_hint: Optional[str] = None
130
- accessibility_role: Optional[str] = None
131
- accessible: Optional[bool] = None
132
- accessibility_state: Optional[Dict[str, Any]] = None
133
- accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None
134
- test_id: Optional[str] = None
135
-
136
-
137
- @dataclass(frozen=True)
138
- class ButtonProps(Props):
139
- """Props for [`Button`][pythonnative.Button]."""
140
-
141
- title: str = ""
142
- on_click: Optional[Callable[[], None]] = None
143
- enabled: bool = True
144
- accessibility_label: Optional[str] = None
145
- accessibility_hint: Optional[str] = None
146
- accessibility_role: Optional[str] = None
147
- accessible: Optional[bool] = None
148
- accessibility_state: Optional[Dict[str, Any]] = None
149
- accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None
150
- test_id: Optional[str] = None
151
-
152
-
153
- @dataclass(frozen=True)
154
- class TextInputProps(Props):
155
- """Props for [`TextInput`][pythonnative.TextInput]."""
156
-
157
- value: str = ""
158
- placeholder: Optional[str] = None
159
- on_change: Optional[Callable[[str], None]] = None
160
- on_submit: Optional[Callable[[str], None]] = None
161
- secure: bool = False
162
- multiline: bool = False
163
- keyboard_type: Optional[KeyboardType] = None
164
- auto_capitalize: Optional[AutoCapitalize] = None
165
- auto_correct: Optional[bool] = None
166
- auto_focus: bool = False
167
- return_key_type: Optional[ReturnKeyType] = None
168
- max_length: Optional[int] = None
169
- placeholder_color: Optional[Color] = None
170
- editable: bool = True
171
- clear_button: bool = False
172
- on_focus: Optional[Callable[[], None]] = None
173
- on_blur: Optional[Callable[[], None]] = None
174
- selection_color: Optional[Color] = None
175
- text_content_type: Optional[str] = None
176
- accessibility_label: Optional[str] = None
177
- accessibility_hint: Optional[str] = None
178
- accessible: Optional[bool] = None
179
- accessibility_state: Optional[Dict[str, Any]] = None
180
- accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None
181
- test_id: Optional[str] = None
182
-
183
-
184
- @dataclass(frozen=True)
185
- class ImageProps(Props):
186
- """Props for [`Image`][pythonnative.Image]."""
187
-
188
- source: Optional[str] = None
189
- scale_type: Optional[ScaleType] = None
190
- tint_color: Optional[Color] = None
191
- placeholder_color: Optional[Color] = None
192
- on_load: Optional[Callable[[], None]] = None
193
- on_error: Optional[Callable[[str], None]] = None
194
- accessibility_label: Optional[str] = None
195
- accessibility_role: Optional[str] = None
196
- accessible: Optional[bool] = None
197
- accessibility_state: Optional[Dict[str, Any]] = None
198
- accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None
199
- test_id: Optional[str] = None
200
-
201
-
202
- @dataclass(frozen=True)
203
- class SwitchProps(Props):
204
- """Props for [`Switch`][pythonnative.Switch]."""
205
-
206
- value: bool = False
207
- on_change: Optional[Callable[[bool], None]] = None
208
- accessibility_label: Optional[str] = None
209
-
210
-
211
- @dataclass(frozen=True)
212
- class ProgressBarProps(Props):
213
- """Props for [`ProgressBar`][pythonnative.ProgressBar]."""
214
-
215
- value: float = 0.0
216
- color: Optional[Color] = None
217
- track_color: Optional[Color] = None
218
- indeterminate: bool = False
219
-
220
-
221
- @dataclass(frozen=True)
222
- class ActivityIndicatorProps(Props):
223
- """Props for [`ActivityIndicator`][pythonnative.ActivityIndicator]."""
224
-
225
- animating: bool = True
226
- color: Optional[Color] = None
227
- size: Literal["small", "large"] = "small"
228
-
229
-
230
- @dataclass(frozen=True)
231
- class WebViewProps(Props):
232
- """Props for [`WebView`][pythonnative.WebView]."""
233
-
234
- url: Optional[str] = None
235
- html: Optional[str] = None
236
- on_load: Optional[Callable[[str], None]] = None
237
- on_message: Optional[Callable[[str], None]] = None
238
- on_navigation_state_change: Optional[Callable[[str], None]] = None
239
- inject_javascript: Optional[str] = None
240
- scroll_enabled: bool = True
241
-
242
-
243
- @dataclass(frozen=True)
244
- class SpacerProps(Props):
245
- """Props for [`Spacer`][pythonnative.Spacer]."""
246
-
247
- size: Optional[float] = None
248
- flex: Optional[float] = None
249
-
250
-
251
- @dataclass(frozen=True)
252
- class SliderProps(Props):
253
- """Props for [`Slider`][pythonnative.Slider]."""
254
-
255
- value: float = 0.0
256
- min_value: float = 0.0
257
- max_value: float = 1.0
258
- on_change: Optional[Callable[[float], None]] = None
259
- accessibility_label: Optional[str] = None
260
-
261
-
262
- @dataclass(frozen=True)
263
- class ViewProps(Props):
264
- """Props for [`View`][pythonnative.View], [`Column`][pythonnative.Column], and [`Row`][pythonnative.Row]."""
265
-
266
- gestures: Optional[List[Any]] = None
267
- accessibility_label: Optional[str] = None
268
- accessibility_hint: Optional[str] = None
269
- accessibility_role: Optional[str] = None
270
- accessible: Optional[bool] = None
271
- accessibility_state: Optional[Dict[str, Any]] = None
272
- accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None
273
- test_id: Optional[str] = None
274
-
275
-
276
- @dataclass(frozen=True)
277
- class ScrollViewProps(Props):
278
- """Props for [`ScrollView`][pythonnative.ScrollView].
279
-
280
- ``on_scroll`` receives a single payload dict with ``"x"`` and
281
- ``"y"`` content offsets in points.
282
- """
283
-
284
- refresh_control: Optional[Dict[str, Any]] = None
285
- scroll_axis: Optional[Literal["vertical", "horizontal"]] = None
286
- on_scroll: Optional[Callable[[Dict[str, float]], None]] = None
287
- shows_scroll_indicator: bool = True
288
- paging_enabled: bool = False
289
- bounces: bool = True
290
- content_container_style: StyleProp = None
291
- keyboard_dismiss_mode: Optional[Literal["none", "on_drag", "interactive"]] = None
292
-
293
-
294
- @dataclass(frozen=True)
295
- class SafeAreaViewProps(Props):
296
- """Props for [`SafeAreaView`][pythonnative.SafeAreaView]."""
297
-
298
- edges: Optional[Tuple[Literal["top", "left", "bottom", "right"], ...]] = None
299
-
300
-
301
- @dataclass(frozen=True)
302
- class ModalProps(Props):
303
- """Props for [`Modal`][pythonnative.Modal]."""
304
-
305
- visible: bool = False
306
- on_dismiss: Optional[Callable[[], None]] = None
307
- on_show: Optional[Callable[[], None]] = None
308
- title: Optional[str] = None
309
- animation_type: Literal["slide", "fade", "none"] = "slide"
310
- transparent: bool = False
311
- presentation_style: Literal["page_sheet", "form_sheet", "full_screen", "overlay"] = "page_sheet"
312
- dismiss_on_backdrop: bool = True
313
-
314
-
315
- @dataclass(frozen=True)
316
- class PressableProps(Props):
317
- """Props for [`Pressable`][pythonnative.Pressable]."""
318
-
319
- on_press: Optional[Callable[[], None]] = None
320
- on_long_press: Optional[Callable[[], None]] = None
321
- on_press_in: Optional[Callable[[], None]] = None
322
- on_press_out: Optional[Callable[[], None]] = None
323
- pressed_opacity: float = 0.6
324
- gestures: Optional[List[Any]] = None
325
- accessibility_label: Optional[str] = None
326
- accessibility_hint: Optional[str] = None
327
- accessibility_role: Optional[str] = None
328
- accessible: Optional[bool] = None
329
- accessibility_state: Optional[Dict[str, Any]] = None
330
- accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None
331
- test_id: Optional[str] = None
332
-
333
-
334
- @dataclass(frozen=True)
335
- class StatusBarProps(Props):
336
- """Props for [`StatusBar`][pythonnative.StatusBar]."""
337
-
338
- bar_style: Optional[Literal["light", "dark", "default"]] = None
339
- background_color: Optional[Color] = None
340
- hidden: Optional[bool] = None
341
-
342
-
343
- @dataclass(frozen=True)
344
- class KeyboardAvoidingViewProps(Props):
345
- """Props for [`KeyboardAvoidingView`][pythonnative.KeyboardAvoidingView]."""
346
-
347
- behavior: Literal["padding", "position"] = "padding"
348
- keyboard_vertical_offset: float = 0.0
349
-
350
-
351
- @dataclass(frozen=True)
352
- class PickerProps(Props):
353
- """Props for [`Picker`][pythonnative.Picker].
354
-
355
- ``items`` is an ordered list of ``{"value": Any, "label": str}``
356
- entries. ``value`` is matched against ``items[i]["value"]`` to
357
- determine the currently selected row.
358
- """
359
-
360
- value: Any = None
361
- items: List[Dict[str, Any]] = field(default_factory=list)
362
- on_change: Optional[Callable[[Any], None]] = None
363
- placeholder: str = "Select…"
364
- accessibility_label: Optional[str] = None
365
- accessibility_hint: Optional[str] = None
366
- accessible: Optional[bool] = None
367
- accessibility_state: Optional[Dict[str, Any]] = None
368
- accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None
369
- test_id: Optional[str] = None
370
-
371
-
372
- @dataclass(frozen=True)
373
- class TouchableOpacityProps(Props):
374
- """Props for [`TouchableOpacity`][pythonnative.TouchableOpacity]."""
375
-
376
- on_press: Optional[Callable[[], None]] = None
377
- on_long_press: Optional[Callable[[], None]] = None
378
- active_opacity: float = 0.2
379
- disabled: bool = False
380
- accessibility_label: Optional[str] = None
381
- accessibility_hint: Optional[str] = None
382
- accessibility_role: Optional[str] = None
383
- accessible: Optional[bool] = None
384
- accessibility_state: Optional[Dict[str, Any]] = None
385
- accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None
386
- test_id: Optional[str] = None
387
-
388
-
389
- @dataclass(frozen=True)
390
- class ImageBackgroundProps(Props):
391
- """Props for [`ImageBackground`][pythonnative.ImageBackground]."""
392
-
393
- source: Optional[str] = None
394
- scale_type: Optional[ScaleType] = None
395
- accessibility_label: Optional[str] = None
396
- accessible: Optional[bool] = None
397
- accessibility_state: Optional[Dict[str, Any]] = None
398
- accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None
399
- test_id: Optional[str] = None
400
-
401
-
402
- @dataclass(frozen=True)
403
- class CheckboxProps(Props):
404
- """Props for [`Checkbox`][pythonnative.Checkbox]."""
405
-
406
- value: bool = False
407
- on_change: Optional[Callable[[bool], None]] = None
408
- label: Optional[str] = None
409
- disabled: bool = False
410
- color: Optional[Color] = None
411
- accessibility_label: Optional[str] = None
412
- accessibility_hint: Optional[str] = None
413
- accessible: Optional[bool] = None
414
- accessibility_state: Optional[Dict[str, Any]] = None
415
- accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None
416
- test_id: Optional[str] = None
417
-
418
-
419
- @dataclass(frozen=True)
420
- class SegmentedControlProps(Props):
421
- """Props for [`SegmentedControl`][pythonnative.SegmentedControl]."""
422
-
423
- segments: List[str] = field(default_factory=list)
424
- selected_index: int = 0
425
- on_change: Optional[Callable[[int], None]] = None
426
- enabled: bool = True
427
- tint_color: Optional[Color] = None
428
- accessibility_label: Optional[str] = None
429
- accessible: Optional[bool] = None
430
- accessibility_state: Optional[Dict[str, Any]] = None
431
- accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None
432
- test_id: Optional[str] = None
433
-
434
-
435
- @dataclass(frozen=True)
436
- class DatePickerProps(Props):
437
- """Props for [`DatePicker`][pythonnative.DatePicker].
438
-
439
- ``value`` and the value passed to ``on_change`` are ISO-8601
440
- strings (``"2026-05-31"`` for ``mode="date"``, ``"14:30"`` for
441
- ``mode="time"``, ``"2026-05-31T14:30"`` for ``mode="datetime"``),
442
- so the schema stays JSON-serializable and platform-agnostic.
443
- """
444
-
445
- value: Optional[str] = None
446
- mode: Literal["date", "time", "datetime"] = "date"
447
- on_change: Optional[Callable[[str], None]] = None
448
- minimum: Optional[str] = None
449
- maximum: Optional[str] = None
450
- enabled: bool = True
451
- accessibility_label: Optional[str] = None
452
- accessible: Optional[bool] = None
453
- accessibility_state: Optional[Dict[str, Any]] = None
454
- accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None
455
- test_id: Optional[str] = None
456
-
457
-
458
112
  # ======================================================================
459
113
  # Leaf factories
460
114
  # ======================================================================
@@ -518,10 +172,10 @@ def Text(
518
172
  accessibility_hint: Optional[str] = None,
519
173
  accessibility_role: Optional[str] = None,
520
174
  accessible: Optional[bool] = None,
521
- accessibility_state: Optional[Dict[str, Any]] = None,
175
+ accessibility_state: Optional[AccessibilityState] = None,
522
176
  accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
523
177
  test_id: Optional[str] = None,
524
- ref: Optional[Dict[str, Any]] = None,
178
+ ref: Optional[Ref] = None,
525
179
  key: Optional[str] = None,
526
180
  ) -> Element:
527
181
  """Display a string of text, optionally with styled nested spans.
@@ -571,7 +225,7 @@ def Text(
571
225
  test_id: Stable identifier for UI tests; exposed as
572
226
  ``resource-id`` on Android and ``accessibilityIdentifier``
573
227
  on iOS.
574
- ref: Optional ``use_ref()`` dict.
228
+ ref: Optional [`Ref`][pythonnative.Ref] from ``use_ref()``.
575
229
  key: Stable identity for keyed reconciliation.
576
230
 
577
231
  Returns:
@@ -604,17 +258,17 @@ def Text(
604
258
  def Button(
605
259
  title: str = "",
606
260
  *,
607
- on_click: Optional[Callable[[], None]] = None,
261
+ on_press: Optional[Callable[[], None]] = None,
608
262
  enabled: bool = True,
609
263
  style: StyleProp = None,
610
264
  accessibility_label: Optional[str] = None,
611
265
  accessibility_hint: Optional[str] = None,
612
266
  accessibility_role: Optional[str] = None,
613
267
  accessible: Optional[bool] = None,
614
- accessibility_state: Optional[Dict[str, Any]] = None,
268
+ accessibility_state: Optional[AccessibilityState] = None,
615
269
  accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
616
270
  test_id: Optional[str] = None,
617
- ref: Optional[Dict[str, Any]] = None,
271
+ ref: Optional[Ref] = None,
618
272
  key: Optional[str] = None,
619
273
  ) -> Element:
620
274
  """Display a tappable button.
@@ -627,7 +281,7 @@ def Button(
627
281
 
628
282
  Args:
629
283
  title: Button label.
630
- on_click: Callback invoked when the user taps the button.
284
+ on_press: Callback invoked when the user taps the button.
631
285
  enabled: When ``False``, the button is disabled and cannot be
632
286
  tapped.
633
287
  style: Style dict (or list of dicts).
@@ -645,7 +299,7 @@ def Button(
645
299
  test_id: Stable identifier for UI tests; exposed as
646
300
  ``resource-id`` on Android and ``accessibilityIdentifier``
647
301
  on iOS.
648
- ref: Optional ``use_ref()`` dict.
302
+ ref: Optional [`Ref`][pythonnative.Ref] from ``use_ref()``.
649
303
  key: Stable identity for keyed reconciliation.
650
304
 
651
305
  Returns:
@@ -657,7 +311,7 @@ def Button(
657
311
  ref=ref,
658
312
  key=key,
659
313
  title=title,
660
- on_click=on_click,
314
+ on_press=on_press,
661
315
  enabled=enabled,
662
316
  accessibility_label=accessibility_label,
663
317
  accessibility_hint=accessibility_hint,
@@ -695,10 +349,10 @@ def TextInput(
695
349
  accessibility_label: Optional[str] = None,
696
350
  accessibility_hint: Optional[str] = None,
697
351
  accessible: Optional[bool] = None,
698
- accessibility_state: Optional[Dict[str, Any]] = None,
352
+ accessibility_state: Optional[AccessibilityState] = None,
699
353
  accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
700
354
  test_id: Optional[str] = None,
701
- ref: Optional[Dict[str, Any]] = None,
355
+ ref: Optional[Ref] = None,
702
356
  key: Optional[str] = None,
703
357
  ) -> Element:
704
358
  """Display a text-entry field (single-line by default, or ``multiline``).
@@ -748,7 +402,7 @@ def TextInput(
748
402
  test_id: Stable identifier for UI tests; exposed as
749
403
  ``resource-id`` on Android and ``accessibilityIdentifier``
750
404
  on iOS.
751
- ref: Optional ``use_ref()`` dict.
405
+ ref: Optional [`Ref`][pythonnative.Ref] from ``use_ref()``.
752
406
  key: Stable identity for keyed reconciliation.
753
407
 
754
408
  Returns:
@@ -799,10 +453,10 @@ def Image(
799
453
  accessibility_label: Optional[str] = None,
800
454
  accessibility_role: Optional[str] = None,
801
455
  accessible: Optional[bool] = None,
802
- accessibility_state: Optional[Dict[str, Any]] = None,
456
+ accessibility_state: Optional[AccessibilityState] = None,
803
457
  accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
804
458
  test_id: Optional[str] = None,
805
- ref: Optional[Dict[str, Any]] = None,
459
+ ref: Optional[Ref] = None,
806
460
  key: Optional[str] = None,
807
461
  ) -> Element:
808
462
  """Display an image from a resource path or URL.
@@ -842,7 +496,7 @@ def Image(
842
496
  test_id: Stable identifier for UI tests; exposed as
843
497
  ``resource-id`` on Android and ``accessibilityIdentifier``
844
498
  on iOS.
845
- ref: Optional ``use_ref()`` dict.
499
+ ref: Optional [`Ref`][pythonnative.Ref] from ``use_ref()``.
846
500
  key: Stable identity for keyed reconciliation.
847
501
 
848
502
  Returns:
@@ -1105,10 +759,10 @@ def View(
1105
759
  accessibility_hint: Optional[str] = None,
1106
760
  accessibility_role: Optional[str] = None,
1107
761
  accessible: Optional[bool] = None,
1108
- accessibility_state: Optional[Dict[str, Any]] = None,
762
+ accessibility_state: Optional[AccessibilityState] = None,
1109
763
  accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
1110
764
  test_id: Optional[str] = None,
1111
- ref: Optional[Dict[str, Any]] = None,
765
+ ref: Optional[Ref] = None,
1112
766
  key: Optional[str] = None,
1113
767
  ) -> Element:
1114
768
  """Universal flex container (like React Native's ``View``).
@@ -1157,7 +811,7 @@ def View(
1157
811
  test_id: Stable identifier for UI tests; exposed as
1158
812
  ``resource-id`` on Android and ``accessibilityIdentifier``
1159
813
  on iOS.
1160
- ref: Optional ``use_ref()`` dict.
814
+ ref: Optional [`Ref`][pythonnative.Ref] from ``use_ref()``.
1161
815
  key: Stable identity for keyed reconciliation.
1162
816
 
1163
817
  Returns:
@@ -1184,7 +838,7 @@ def View(
1184
838
  def Column(
1185
839
  *children: Element,
1186
840
  style: StyleProp = None,
1187
- ref: Optional[Dict[str, Any]] = None,
841
+ ref: Optional[Ref] = None,
1188
842
  key: Optional[str] = None,
1189
843
  ) -> Element:
1190
844
  """Arrange children vertically.
@@ -1196,7 +850,7 @@ def Column(
1196
850
  Args:
1197
851
  *children: Child elements stacked top to bottom.
1198
852
  style: Style dict (or list of dicts).
1199
- ref: Optional ``use_ref()`` dict for native-view access.
853
+ ref: Optional [`Ref`][pythonnative.Ref] for native-view access.
1200
854
  key: Stable identity for keyed reconciliation.
1201
855
 
1202
856
  Returns:
@@ -1215,7 +869,7 @@ def Column(
1215
869
  def Row(
1216
870
  *children: Element,
1217
871
  style: StyleProp = None,
1218
- ref: Optional[Dict[str, Any]] = None,
872
+ ref: Optional[Ref] = None,
1219
873
  key: Optional[str] = None,
1220
874
  ) -> Element:
1221
875
  """Arrange children horizontally.
@@ -1227,7 +881,7 @@ def Row(
1227
881
  Args:
1228
882
  *children: Child elements arranged left to right.
1229
883
  style: Style dict (or list of dicts).
1230
- ref: Optional ``use_ref()`` dict for native-view access.
884
+ ref: Optional [`Ref`][pythonnative.Ref] for native-view access.
1231
885
  key: Stable identity for keyed reconciliation.
1232
886
 
1233
887
  Returns:
@@ -1254,7 +908,7 @@ def ScrollView(
1254
908
  content_container_style: StyleProp = None,
1255
909
  keyboard_dismiss_mode: Optional[Literal["none", "on_drag", "interactive"]] = None,
1256
910
  style: StyleProp = None,
1257
- ref: Optional[Dict[str, Any]] = None,
911
+ ref: Optional[Ref] = None,
1258
912
  key: Optional[str] = None,
1259
913
  ) -> Element:
1260
914
  """Wrap children in a scrollable container.
@@ -1285,7 +939,7 @@ def ScrollView(
1285
939
  ``"interactive"``. Controls whether scrolling dismisses
1286
940
  the keyboard.
1287
941
  style: Style dict (or list of dicts).
1288
- ref: Optional ``use_ref()`` dict.
942
+ ref: Optional [`Ref`][pythonnative.Ref] from ``use_ref()``.
1289
943
  key: Stable identity for keyed reconciliation.
1290
944
 
1291
945
  Returns:
@@ -1495,10 +1149,10 @@ def Pressable(
1495
1149
  accessibility_hint: Optional[str] = None,
1496
1150
  accessibility_role: Optional[str] = None,
1497
1151
  accessible: Optional[bool] = None,
1498
- accessibility_state: Optional[Dict[str, Any]] = None,
1152
+ accessibility_state: Optional[AccessibilityState] = None,
1499
1153
  accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
1500
1154
  test_id: Optional[str] = None,
1501
- ref: Optional[Dict[str, Any]] = None,
1155
+ ref: Optional[Ref] = None,
1502
1156
  key: Optional[str] = None,
1503
1157
  ) -> Element:
1504
1158
  """Wrap children with tap / long-press / gesture handlers.
@@ -1547,7 +1201,7 @@ def Pressable(
1547
1201
  test_id: Stable identifier for UI tests; exposed as
1548
1202
  ``resource-id`` on Android and ``accessibilityIdentifier``
1549
1203
  on iOS.
1550
- ref: Optional ``use_ref()`` dict.
1204
+ ref: Optional [`Ref`][pythonnative.Ref] from ``use_ref()``.
1551
1205
  key: Stable identity for keyed reconciliation.
1552
1206
 
1553
1207
  Returns:
@@ -1613,7 +1267,7 @@ def TouchableOpacity(
1613
1267
  accessibility_hint: Optional[str] = None,
1614
1268
  accessibility_role: Optional[str] = None,
1615
1269
  accessible: Optional[bool] = None,
1616
- accessibility_state: Optional[Dict[str, Any]] = None,
1270
+ accessibility_state: Optional[AccessibilityState] = None,
1617
1271
  accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
1618
1272
  test_id: Optional[str] = None,
1619
1273
  key: Optional[str] = None,
@@ -1683,7 +1337,7 @@ def ImageBackground(
1683
1337
  style: StyleProp = None,
1684
1338
  accessibility_label: Optional[str] = None,
1685
1339
  accessible: Optional[bool] = None,
1686
- accessibility_state: Optional[Dict[str, Any]] = None,
1340
+ accessibility_state: Optional[AccessibilityState] = None,
1687
1341
  accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
1688
1342
  test_id: Optional[str] = None,
1689
1343
  key: Optional[str] = None,
@@ -1751,7 +1405,7 @@ def Checkbox(
1751
1405
  accessibility_label: Optional[str] = None,
1752
1406
  accessibility_hint: Optional[str] = None,
1753
1407
  accessible: Optional[bool] = None,
1754
- accessibility_state: Optional[Dict[str, Any]] = None,
1408
+ accessibility_state: Optional[AccessibilityState] = None,
1755
1409
  accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
1756
1410
  test_id: Optional[str] = None,
1757
1411
  key: Optional[str] = None,
@@ -1816,7 +1470,7 @@ def SegmentedControl(
1816
1470
  style: StyleProp = None,
1817
1471
  accessibility_label: Optional[str] = None,
1818
1472
  accessible: Optional[bool] = None,
1819
- accessibility_state: Optional[Dict[str, Any]] = None,
1473
+ accessibility_state: Optional[AccessibilityState] = None,
1820
1474
  accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
1821
1475
  test_id: Optional[str] = None,
1822
1476
  key: Optional[str] = None,
@@ -1879,7 +1533,7 @@ def DatePicker(
1879
1533
  style: StyleProp = None,
1880
1534
  accessibility_label: Optional[str] = None,
1881
1535
  accessible: Optional[bool] = None,
1882
- accessibility_state: Optional[Dict[str, Any]] = None,
1536
+ accessibility_state: Optional[AccessibilityState] = None,
1883
1537
  accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
1884
1538
  test_id: Optional[str] = None,
1885
1539
  key: Optional[str] = None,
@@ -1889,7 +1543,10 @@ def DatePicker(
1889
1543
  Backed by ``UIDatePicker`` on iOS and a trigger button that opens
1890
1544
  the platform ``DatePickerDialog`` / ``TimePickerDialog`` on
1891
1545
  Android. ``value`` and the value reported to ``on_change`` are
1892
- ISO-8601 strings (see [`DatePickerProps`][pythonnative.DatePickerProps]).
1546
+ ISO-8601 strings (``"2026-05-31"`` for ``mode="date"``, ``"14:30"``
1547
+ for ``mode="time"``, ``"2026-05-31T14:30"`` for
1548
+ ``mode="datetime"``), so values stay JSON-serializable and
1549
+ platform-agnostic.
1893
1550
 
1894
1551
  Args:
1895
1552
  value: Currently selected value as an ISO-8601 string.
@@ -1943,14 +1600,12 @@ def DatePicker(
1943
1600
  def Fragment(*children: Optional[Element], key: Optional[str] = None) -> Element:
1944
1601
  """Group children without adding a wrapping native view.
1945
1602
 
1946
- Like React's ``<></>``: returns multiple elements from a component
1947
- without introducing an extra container. The reconciler flattens
1948
- Fragment elements at the children-list level, so each child appears
1949
- as a direct sibling of the Fragment's parent in the native tree.
1950
-
1951
- Useful inside [`Provider`][pythonnative.Provider] /
1952
- [`memo`][pythonnative.memo] / conditional logic when grouping
1953
- siblings inside another component's child list:
1603
+ Like React's ``<></>``: groups elements without introducing an
1604
+ extra container. Each child mounts as a direct sibling of the
1605
+ Fragment's position in the parent's child list. Components may
1606
+ also simply return a plain ``list`` of elements; ``Fragment``
1607
+ exists for when the group needs a ``key`` (e.g. rendering a list
1608
+ of pairs) or when a single expression reads better.
1954
1609
 
1955
1610
  ```python
1956
1611
  pn.Column(
@@ -1964,24 +1619,54 @@ def Fragment(*children: Optional[Element], key: Optional[str] = None) -> Element
1964
1619
  ```
1965
1620
 
1966
1621
  Args:
1967
- *children: Child elements to expose at the parent level. ``None``
1968
- children are dropped, which makes conditional rendering with
1969
- ``cond and pn.Text(...)`` ergonomic.
1970
- key: Optional key for the Fragment itself (rarely useful since
1971
- Fragment doesn't appear in the native tree).
1622
+ *children: Child elements to expose at the parent level.
1623
+ ``None`` and ``False`` children are dropped, which makes
1624
+ conditional rendering with ``cond and pn.Text(...)``
1625
+ ergonomic.
1626
+ key: Stable identity for keyed reconciliation. A keyed
1627
+ Fragment moves all of its children as one unit and
1628
+ preserves their state across reorders.
1972
1629
 
1973
1630
  Returns:
1974
1631
  An [`Element`][pythonnative.Element] of type ``"__Fragment__"``.
1632
+ """
1633
+ kept = [c for c in children if c is not None and c is not False]
1634
+ return Element("__Fragment__", {}, kept, key=key)
1635
+
1636
+
1637
+ def Portal(*children: Element, key: Optional[str] = None) -> Element:
1638
+ """Render ``children`` into a full-screen overlay above everything else.
1975
1639
 
1976
- Note:
1977
- Today, returning a Fragment from a ``@pn.component`` function
1978
- only mounts its first child as the component's root. To return
1979
- multiple top-level elements from a function component, use a
1980
- container such as [`Column`][pythonnative.Column] or
1981
- [`Row`][pythonnative.Row] instead.
1640
+ Like React DOM's ``createPortal``: the children stay part of this
1641
+ component's tree for state, context, and events, but their native
1642
+ views mount in a transparent overlay attached to the window (above
1643
+ the screen's content) instead of inside the surrounding parent.
1644
+ Use it for toasts, dropdowns, tooltips, and lightweight custom
1645
+ overlays that must escape ``overflow: "hidden"`` ancestors. For a
1646
+ system-styled dialog with its own presentation and dismissal
1647
+ gestures, use [`Modal`][pythonnative.Modal] instead.
1648
+
1649
+ The overlay itself does not intercept touches; only the children
1650
+ themselves are hit-testable. Children are laid out against the
1651
+ full viewport, so position them with absolute insets:
1652
+
1653
+ ```python
1654
+ pn.Portal(
1655
+ pn.View(
1656
+ pn.Text("Saved!"),
1657
+ style=pn.style(position="absolute", bottom=40, left=40, right=40),
1658
+ ),
1659
+ )
1660
+ ```
1661
+
1662
+ Args:
1663
+ *children: Overlay content.
1664
+ key: Stable identity for keyed reconciliation.
1665
+
1666
+ Returns:
1667
+ An [`Element`][pythonnative.Element] of type ``"Portal"``.
1982
1668
  """
1983
- filtered = [c for c in children if c is not None]
1984
- return Element("__Fragment__", {}, filtered, key=key)
1669
+ return Element("Portal", {}, list(children), key=key)
1985
1670
 
1986
1671
 
1987
1672
  # ======================================================================
@@ -1992,23 +1677,33 @@ def Fragment(*children: Optional[Element], key: Optional[str] = None) -> Element
1992
1677
  def ErrorBoundary(
1993
1678
  *children: Element,
1994
1679
  fallback: Optional[Any] = None,
1680
+ on_error: Optional[Callable[[BaseException], None]] = None,
1995
1681
  key: Optional[str] = None,
1996
1682
  ) -> Element:
1997
1683
  """Catch render errors in the wrapped subtree and display ``fallback`` instead.
1998
1684
 
1999
- ``fallback`` may be an [`Element`][pythonnative.Element] or a
2000
- callable that receives the exception and returns an ``Element``.
2001
- Useful for isolating risky subtrees so a single failure doesn't
2002
- crash the page.
1685
+ When any descendant raises during render (initial mount, a parent
1686
+ re-render, or a local state-driven update), the failed subtree is
1687
+ torn down and ``fallback`` is mounted in its place. Without a
1688
+ boundary the error propagates to the screen host (which shows the
1689
+ dev error overlay in dev mode).
1690
+
1691
+ ``fallback`` may be:
2003
1692
 
2004
- When multiple children are passed they're grouped under a
2005
- [`Fragment`][pythonnative.Fragment] so the boundary still wraps a
2006
- single logical subtree.
1693
+ - An [`Element`][pythonnative.Element], shown as-is.
1694
+ - ``fallback(error) -> Element``.
1695
+ - ``fallback(error, reset) -> Element``, where ``reset`` is a
1696
+ zero-arg callable that clears the error and remounts the
1697
+ original children (fresh state), for retry buttons.
2007
1698
 
2008
1699
  Args:
2009
1700
  *children: Subtree to wrap.
2010
- fallback: Element rendered when the subtree raises during
2011
- render, or a callable ``fallback(err) -> Element``.
1701
+ fallback: Fallback content (see above). Required for the
1702
+ boundary to actually catch; without it errors propagate
1703
+ to the next boundary up.
1704
+ on_error: Callback invoked with the exception when the
1705
+ boundary catches, before the fallback mounts. Use it for
1706
+ error reporting.
2012
1707
  key: Stable identity for keyed reconciliation.
2013
1708
 
2014
1709
  Returns:
@@ -2021,18 +1716,79 @@ def ErrorBoundary(
2021
1716
 
2022
1717
  pn.ErrorBoundary(
2023
1718
  MyRiskyComponent(),
2024
- fallback=lambda err: pn.Text(f"Error: {err}"),
1719
+ fallback=lambda err, reset: pn.Column(
1720
+ pn.Text(f"Error: {err}"),
1721
+ pn.Button("Retry", on_press=reset),
1722
+ ),
1723
+ on_error=lambda err: log.exception(err),
2025
1724
  )
2026
1725
  ```
2027
1726
  """
2028
1727
  props: Dict[str, Any] = {}
2029
1728
  if fallback is not None:
2030
1729
  props["__fallback__"] = fallback
2031
- if len(children) <= 1:
2032
- kids = list(children)
2033
- else:
2034
- kids = [Fragment(*children)]
2035
- return Element("__ErrorBoundary__", props, kids, key=key)
1730
+ if on_error is not None:
1731
+ props["__on_error__"] = on_error
1732
+ return Element("__ErrorBoundary__", props, list(children), key=key)
1733
+
1734
+
1735
+ def Suspense(
1736
+ *children: Element,
1737
+ fallback: Optional[Any] = None,
1738
+ key: Optional[str] = None,
1739
+ ) -> Element:
1740
+ """Show ``fallback`` while descendants wait on async work, then swap in the content.
1741
+
1742
+ A Suspense boundary catches **suspensions** from the subtree it
1743
+ wraps: an ``async def`` component body blocking on a pending
1744
+ await, or a regular component calling
1745
+ [`Resource.read`][pythonnative.suspense.Resource.read] on data that hasn't
1746
+ arrived (see [`use_resource`][pythonnative.use_resource] and
1747
+ [`lazy`][pythonnative.lazy]). While anything is pending the
1748
+ boundary renders ``fallback``; when the awaited work completes it
1749
+ retries the content and swaps it in. Suspended components keep
1750
+ their hook state across retries, so cached resources aren't
1751
+ refetched.
1752
+
1753
+ Two timing behaviors, matching React:
1754
+
1755
+ - **Initial mount**: the fallback shows until the content is ready.
1756
+ - **Updates**: a component that's already on screen and suspends
1757
+ again (its dependencies changed) keeps its previous content
1758
+ visible and re-renders when ready; there's no fallback flash.
1759
+
1760
+ Args:
1761
+ *children: Subtree to wrap (the async content).
1762
+ fallback: Content shown while suspended: an
1763
+ [`Element`][pythonnative.Element] or a zero-arg callable
1764
+ returning one. Without it, suspensions propagate to the
1765
+ next Suspense boundary up.
1766
+ key: Stable identity for keyed reconciliation.
1767
+
1768
+ Returns:
1769
+ An [`Element`][pythonnative.Element] of type ``"__Suspense__"``.
1770
+
1771
+ Example:
1772
+ ```python
1773
+ import pythonnative as pn
1774
+
1775
+ @pn.component
1776
+ async def Profile(user_id: str):
1777
+ user = await api.fetch_user(user_id)
1778
+ return pn.Text(user.name)
1779
+
1780
+ @pn.component
1781
+ def Screen():
1782
+ return pn.Suspense(
1783
+ Profile(user_id="42"),
1784
+ fallback=pn.ActivityIndicator(),
1785
+ )
1786
+ ```
1787
+ """
1788
+ props: Dict[str, Any] = {}
1789
+ if fallback is not None:
1790
+ props["__fallback__"] = fallback
1791
+ return Element("__Suspense__", props, list(children), key=key)
2036
1792
 
2037
1793
 
2038
1794
  # ======================================================================
@@ -2082,7 +1838,7 @@ class _RowSpec:
2082
1838
 
2083
1839
  def _dispatch_scroll_command(scroll_ref: Any, name: str, args: Dict[str, Any]) -> Any:
2084
1840
  """Send an imperative command to the ScrollView under ``scroll_ref``."""
2085
- tag = scroll_ref.get("_pn_tag") if isinstance(scroll_ref, dict) else None
1841
+ tag = getattr(scroll_ref, "_pn_tag", None)
2086
1842
  if tag is None:
2087
1843
  return None
2088
1844
  from .native_views import get_registry
@@ -2093,6 +1849,55 @@ def _dispatch_scroll_command(scroll_ref: Any, name: str, args: Dict[str, Any]) -
2093
1849
  return None
2094
1850
 
2095
1851
 
1852
+ class ListController:
1853
+ """Imperative scroll handle published on a list's ``ref``.
1854
+
1855
+ [`FlatList`][pythonnative.FlatList] and
1856
+ [`SectionList`][pythonnative.SectionList] install a
1857
+ ``ListController`` on ``ref.current`` (via
1858
+ [`use_imperative_handle`][pythonnative.use_imperative_handle])
1859
+ after mount and clear it back to ``None`` on unmount.
1860
+
1861
+ Example:
1862
+ ```python
1863
+ import pythonnative as pn
1864
+
1865
+ @pn.component
1866
+ def Chat(messages):
1867
+ list_ref = pn.use_ref()
1868
+ pn.use_layout_effect(
1869
+ lambda: list_ref.current and list_ref.current.scroll_to_end(animated=False),
1870
+ [len(messages)],
1871
+ )
1872
+ return pn.FlatList(data=messages, render_item=Bubble, ref=list_ref)
1873
+ ```
1874
+ """
1875
+
1876
+ __slots__ = ("_scroll_to_offset", "_scroll_to_index", "_scroll_to_end")
1877
+
1878
+ def __init__(
1879
+ self,
1880
+ scroll_to_offset: Callable[[float, bool], None],
1881
+ scroll_to_index: Callable[[int, bool], None],
1882
+ scroll_to_end: Callable[[bool], None],
1883
+ ) -> None:
1884
+ self._scroll_to_offset = scroll_to_offset
1885
+ self._scroll_to_index = scroll_to_index
1886
+ self._scroll_to_end = scroll_to_end
1887
+
1888
+ def scroll_to_offset(self, offset: float, animated: bool = True) -> None:
1889
+ """Scroll to an absolute content offset in points."""
1890
+ self._scroll_to_offset(offset, animated)
1891
+
1892
+ def scroll_to_index(self, index: int, animated: bool = True) -> None:
1893
+ """Scroll so the row at ``index`` sits at the top of the viewport."""
1894
+ self._scroll_to_index(index, animated)
1895
+
1896
+ def scroll_to_end(self, animated: bool = True) -> None:
1897
+ """Scroll to the end of the content."""
1898
+ self._scroll_to_end(animated)
1899
+
1900
+
2096
1901
  @component
2097
1902
  def _VirtualizedList(**p: Any) -> Element:
2098
1903
  """Shared windowing engine behind FlatList and SectionList."""
@@ -2104,18 +1909,18 @@ def _VirtualizedList(**p: Any) -> Element:
2104
1909
  initial_extent: float = float(p.get("initial_window_extent") or 800.0)
2105
1910
 
2106
1911
  window, set_window = use_state((0, -1))
2107
- measured = use_ref({}) # row key -> measured extent (points)
2108
- row_refs = use_ref({}) # row key -> ref dict for live rows
1912
+ measured: Ref[Dict[str, float]] = use_ref({}) # row key -> measured extent (points)
1913
+ row_refs: Ref[Dict[str, Ref]] = use_ref({}) # row key -> Ref for live rows
2109
1914
  end_latch = use_ref({"fired_for": -1})
2110
- viewable_ref = use_ref({"keys": ()})
1915
+ viewable_ref: Ref[Dict[str, Tuple[str, ...]]] = use_ref({"keys": ()})
2111
1916
  scroll_pos = use_ref({"offset": 0.0})
2112
- sv_ref = use_ref(None)
1917
+ sv_ref: Ref = use_ref(None)
2113
1918
 
2114
1919
  # ------------------------------------------------------------------
2115
1920
  # Extent model: measured > per-row hint > estimate. ``starts`` are
2116
1921
  # prefix sums; ``starts[n]`` is the total content extent.
2117
1922
  # ------------------------------------------------------------------
2118
- measured_map: Dict[str, float] = measured["current"]
1923
+ measured_map: Dict[str, float] = measured.current
2119
1924
  starts: List[float] = [0.0] * (n + 1)
2120
1925
  acc = 0.0
2121
1926
  for i, spec in enumerate(rows):
@@ -2128,7 +1933,7 @@ def _VirtualizedList(**p: Any) -> Element:
2128
1933
  total_extent = acc
2129
1934
 
2130
1935
  def _viewport_extent() -> float:
2131
- frame = sv_ref.get("_pn_frame") if isinstance(sv_ref, dict) else None
1936
+ frame = sv_ref._pn_frame
2132
1937
  if frame:
2133
1938
  extent = frame[2] if horizontal else frame[3]
2134
1939
  if extent and extent > 0:
@@ -2147,7 +1952,7 @@ def _VirtualizedList(**p: Any) -> Element:
2147
1952
 
2148
1953
  first, last = window
2149
1954
  if last < 0 or first >= n:
2150
- first, last = _window_for(scroll_pos["current"]["offset"], _viewport_extent())
1955
+ first, last = _window_for(scroll_pos.current["offset"], _viewport_extent())
2151
1956
  last = min(last, n - 1)
2152
1957
  first = max(0, min(first, max(0, n - 1)))
2153
1958
 
@@ -2163,8 +1968,8 @@ def _VirtualizedList(**p: Any) -> Element:
2163
1968
  user_on_scroll = p.get("on_scroll")
2164
1969
 
2165
1970
  def _sweep_measured() -> None:
2166
- for row_key, ref in row_refs["current"].items():
2167
- frame = ref.get("_pn_frame") if isinstance(ref, dict) else None
1971
+ for row_key, row_ref in row_refs.current.items():
1972
+ frame = getattr(row_ref, "_pn_frame", None)
2168
1973
  if frame:
2169
1974
  extent = frame[2] if horizontal else frame[3]
2170
1975
  if extent and extent > 0:
@@ -2175,7 +1980,7 @@ def _VirtualizedList(**p: Any) -> Element:
2175
1980
  offset = float(payload.get("x" if horizontal else "y", 0.0) or 0.0)
2176
1981
  else:
2177
1982
  offset = float(payload or 0.0)
2178
- scroll_pos["current"]["offset"] = offset
1983
+ scroll_pos.current["offset"] = offset
2179
1984
  _sweep_measured()
2180
1985
  viewport = _viewport_extent()
2181
1986
 
@@ -2186,18 +1991,18 @@ def _VirtualizedList(**p: Any) -> Element:
2186
1991
  if on_end_reached is not None and total_extent > 0:
2187
1992
  remaining = total_extent - (offset + viewport)
2188
1993
  if remaining <= end_threshold * viewport:
2189
- if end_latch["current"]["fired_for"] != n:
2190
- end_latch["current"]["fired_for"] = n
1994
+ if end_latch.current["fired_for"] != n:
1995
+ end_latch.current["fired_for"] = n
2191
1996
  on_end_reached()
2192
1997
  elif remaining > end_threshold * viewport + viewport:
2193
- end_latch["current"]["fired_for"] = -1
1998
+ end_latch.current["fired_for"] = -1
2194
1999
 
2195
2000
  if on_viewable is not None and n > 0:
2196
2001
  v_first = max(0, bisect.bisect_right(starts, offset, 0, n) - 1)
2197
2002
  v_last = min(n - 1, bisect.bisect_left(starts, offset + viewport, 0, n))
2198
2003
  keys = tuple(rows[i].key for i in range(v_first, v_last + 1))
2199
- if keys != viewable_ref["current"]["keys"]:
2200
- viewable_ref["current"]["keys"] = keys
2004
+ if keys != viewable_ref.current["keys"]:
2005
+ viewable_ref.current["keys"] = keys
2201
2006
  on_viewable(
2202
2007
  [
2203
2008
  {"index": rows[i].index, "key": rows[i].key, "item": rows[i].item}
@@ -2209,33 +2014,26 @@ def _VirtualizedList(**p: Any) -> Element:
2209
2014
  user_on_scroll(payload)
2210
2015
 
2211
2016
  # ------------------------------------------------------------------
2212
- # Imperative controller (scroll_to_index / offset / end) exposed on
2213
- # the user's ref dict. Re-attached every render so the closures see
2214
- # fresh extents; the effect itself must run unconditionally to keep
2215
- # hook order stable.
2017
+ # Imperative controller (scroll_to_index / offset / end) published
2018
+ # on the user's ref. Rebuilt every render (deps=None) so the
2019
+ # closures see fresh extents.
2216
2020
  # ------------------------------------------------------------------
2217
- controller = p.get("controller_ref")
2218
-
2219
- def _attach_controller() -> None:
2220
- if not isinstance(controller, dict):
2221
- return
2222
-
2223
- def scroll_to_offset(offset: float, animated: bool = True) -> None:
2224
- axis = "x" if horizontal else "y"
2225
- _dispatch_scroll_command(sv_ref, "scroll_to_offset", {axis: float(offset), "animated": animated})
2021
+ def _scroll_to_offset(offset: float, animated: bool = True) -> None:
2022
+ axis = "x" if horizontal else "y"
2023
+ _dispatch_scroll_command(sv_ref, "scroll_to_offset", {axis: float(offset), "animated": animated})
2226
2024
 
2227
- def scroll_to_index(index: int, animated: bool = True) -> None:
2228
- idx = max(0, min(int(index), n - 1)) if n else 0
2229
- scroll_to_offset(starts[idx], animated)
2025
+ def _scroll_to_index(index: int, animated: bool = True) -> None:
2026
+ idx = max(0, min(int(index), n - 1)) if n else 0
2027
+ _scroll_to_offset(starts[idx], animated)
2230
2028
 
2231
- def scroll_to_end(animated: bool = True) -> None:
2232
- scroll_to_offset(max(0.0, total_extent - _viewport_extent()), animated)
2029
+ def _scroll_to_end(animated: bool = True) -> None:
2030
+ _scroll_to_offset(max(0.0, total_extent - _viewport_extent()), animated)
2233
2031
 
2234
- controller["scroll_to_offset"] = scroll_to_offset
2235
- controller["scroll_to_index"] = scroll_to_index
2236
- controller["scroll_to_end"] = scroll_to_end
2237
-
2238
- use_effect(_attach_controller, None)
2032
+ use_imperative_handle(
2033
+ p.get("controller_ref"),
2034
+ lambda: ListController(_scroll_to_offset, _scroll_to_index, _scroll_to_end),
2035
+ None,
2036
+ )
2239
2037
 
2240
2038
  # ------------------------------------------------------------------
2241
2039
  # Children: header, leading spacer, windowed rows, trailing spacer,
@@ -2254,17 +2052,17 @@ def _VirtualizedList(**p: Any) -> Element:
2254
2052
  if empty is not None:
2255
2053
  children.append(View(empty, key="__pn_empty__"))
2256
2054
  else:
2257
- live_refs: Dict[str, Any] = {}
2055
+ live_refs: Dict[str, Ref] = {}
2258
2056
  lead = starts[first]
2259
2057
  if lead > 0:
2260
2058
  lead_style: Dict[str, Any] = {spacer_key: lead}
2261
2059
  children.append(View(style=lead_style, key="__pn_lead__"))
2262
2060
  for i in range(first, last + 1):
2263
2061
  spec = rows[i]
2264
- row_ref = row_refs["current"].get(spec.key) or {"current": None}
2062
+ row_ref = row_refs.current.get(spec.key) or Ref()
2265
2063
  live_refs[spec.key] = row_ref
2266
2064
  children.append(View(spec.make(), ref=row_ref, key=spec.key))
2267
- row_refs["current"] = live_refs
2065
+ row_refs.current = live_refs
2268
2066
  trail = total_extent - starts[last + 1]
2269
2067
  if trail > 0:
2270
2068
  trail_style: Dict[str, Any] = {spacer_key: trail}
@@ -2318,9 +2116,9 @@ def _NativeList(**p: Any) -> Element:
2318
2116
  heights: List[float] = [float(spec.extent or 0.0) for spec in rows]
2319
2117
  uniform = len(set(heights)) <= 1
2320
2118
 
2321
- internal_ref = use_ref(None)
2119
+ internal_ref: Ref = use_ref(None)
2322
2120
  end_latch = use_ref({"fired_for": -1})
2323
- viewable_ref = use_ref({"keys": ()})
2121
+ viewable_ref: Ref[Dict[str, Tuple[str, ...]]] = use_ref({"keys": ()})
2324
2122
 
2325
2123
  starts: List[float] = [0.0] * (n + 1)
2326
2124
  acc = 0.0
@@ -2349,18 +2147,18 @@ def _NativeList(**p: Any) -> Element:
2349
2147
  if on_end_reached is not None and total_extent > 0:
2350
2148
  remaining = total_extent - (offset + viewport)
2351
2149
  if remaining <= end_threshold * viewport:
2352
- if end_latch["current"]["fired_for"] != n:
2353
- end_latch["current"]["fired_for"] = n
2150
+ if end_latch.current["fired_for"] != n:
2151
+ end_latch.current["fired_for"] = n
2354
2152
  on_end_reached()
2355
2153
  elif remaining > end_threshold * viewport + viewport:
2356
- end_latch["current"]["fired_for"] = -1
2154
+ end_latch.current["fired_for"] = -1
2357
2155
 
2358
2156
  if on_viewable is not None and n > 0:
2359
2157
  v_first = max(0, bisect.bisect_right(starts, offset, 0, n) - 1)
2360
2158
  v_last = min(n - 1, bisect.bisect_left(starts, offset + viewport, 0, n))
2361
2159
  keys = tuple(rows[i].key for i in range(v_first, v_last + 1))
2362
- if keys != viewable_ref["current"]["keys"]:
2363
- viewable_ref["current"]["keys"] = keys
2160
+ if keys != viewable_ref.current["keys"]:
2161
+ viewable_ref.current["keys"] = keys
2364
2162
  on_viewable(
2365
2163
  [
2366
2164
  {"index": rows[i].index, "key": rows[i].key, "item": rows[i].item}
@@ -2371,26 +2169,20 @@ def _NativeList(**p: Any) -> Element:
2371
2169
  if user_on_scroll is not None:
2372
2170
  user_on_scroll({"x": 0.0, "y": offset})
2373
2171
 
2374
- controller = p.get("controller_ref")
2375
-
2376
- def _attach_controller() -> None:
2377
- if not isinstance(controller, dict):
2378
- return
2172
+ def _scroll_to_offset(offset: float, animated: bool = True) -> None:
2173
+ _dispatch_scroll_command(internal_ref, "scroll_to_offset", {"y": float(offset), "animated": animated})
2379
2174
 
2380
- def scroll_to_offset(offset: float, animated: bool = True) -> None:
2381
- _dispatch_scroll_command(internal_ref, "scroll_to_offset", {"y": float(offset), "animated": animated})
2175
+ def _scroll_to_index(index: int, animated: bool = True) -> None:
2176
+ _dispatch_scroll_command(internal_ref, "scroll_to_index", {"index": int(index), "animated": animated})
2382
2177
 
2383
- def scroll_to_index(index: int, animated: bool = True) -> None:
2384
- _dispatch_scroll_command(internal_ref, "scroll_to_index", {"index": int(index), "animated": animated})
2178
+ def _scroll_to_end(animated: bool = True) -> None:
2179
+ _dispatch_scroll_command(internal_ref, "scroll_to_end", {"animated": animated})
2385
2180
 
2386
- def scroll_to_end(animated: bool = True) -> None:
2387
- _dispatch_scroll_command(internal_ref, "scroll_to_end", {"animated": animated})
2388
-
2389
- controller["scroll_to_offset"] = scroll_to_offset
2390
- controller["scroll_to_index"] = scroll_to_index
2391
- controller["scroll_to_end"] = scroll_to_end
2392
-
2393
- use_effect(_attach_controller, None)
2181
+ use_imperative_handle(
2182
+ p.get("controller_ref"),
2183
+ lambda: ListController(_scroll_to_offset, _scroll_to_index, _scroll_to_end),
2184
+ None,
2185
+ )
2394
2186
 
2395
2187
  props: Dict[str, Any] = dict(p.get("list_style") or {})
2396
2188
  props["count"] = n
@@ -2434,7 +2226,7 @@ def FlatList(
2434
2226
  shows_scroll_indicator: bool = True,
2435
2227
  content_container_style: StyleProp = None,
2436
2228
  style: StyleProp = None,
2437
- ref: Optional[Dict[str, Any]] = None,
2229
+ ref: Optional[Ref] = None,
2438
2230
  key: Optional[str] = None,
2439
2231
  ) -> Element:
2440
2232
  """Virtualized scrollable list that renders items from ``data`` lazily.
@@ -2447,10 +2239,12 @@ def FlatList(
2447
2239
  unknown rows start at ``estimated_item_height`` and are corrected
2448
2240
  with their measured extent once they've been on screen.
2449
2241
 
2450
- The ``ref`` dict (from [`use_ref`][pythonnative.use_ref]) is
2451
- populated with an imperative controller:
2452
- ``ref["scroll_to_index"](i)``, ``ref["scroll_to_offset"](pts)``,
2453
- and ``ref["scroll_to_end"]()``.
2242
+ Pass a [`Ref`][pythonnative.Ref] (from
2243
+ [`use_ref`][pythonnative.use_ref]) to receive a
2244
+ [`ListController`][pythonnative.ListController] on ``ref.current``:
2245
+ ``ref.current.scroll_to_index(i)``,
2246
+ ``ref.current.scroll_to_offset(pts)``, and
2247
+ ``ref.current.scroll_to_end()``.
2454
2248
 
2455
2249
  Args:
2456
2250
  data: List of arbitrary item values.
@@ -2485,8 +2279,9 @@ def FlatList(
2485
2279
  content_container_style: Style applied to the inner content
2486
2280
  wrapper.
2487
2281
  style: Style for the outer scroll container.
2488
- ref: Optional ``use_ref()`` dict; receives the scroll
2489
- controller functions.
2282
+ ref: Optional [`Ref`][pythonnative.Ref]; receives a
2283
+ [`ListController`][pythonnative.ListController] on
2284
+ ``ref.current`` after mount.
2490
2285
  key: Stable identity for keyed reconciliation of the list.
2491
2286
 
2492
2287
  Returns:
@@ -2630,7 +2425,7 @@ def SectionList(
2630
2425
  on_end_reached_threshold: float = 0.5,
2631
2426
  on_scroll: Optional[Callable[[Dict[str, float]], None]] = None,
2632
2427
  style: StyleProp = None,
2633
- ref: Optional[Dict[str, Any]] = None,
2428
+ ref: Optional[Ref] = None,
2634
2429
  key: Optional[str] = None,
2635
2430
  ) -> Element:
2636
2431
  """Virtualized list with section headers interleaved between row groups.
@@ -2664,8 +2459,9 @@ def SectionList(
2664
2459
  multiples, at which ``on_end_reached`` fires.
2665
2460
  on_scroll: Called with the raw scroll payload.
2666
2461
  style: Style for the outer scroll container.
2667
- ref: Optional ``use_ref()`` dict; receives the scroll
2668
- controller functions.
2462
+ ref: Optional [`Ref`][pythonnative.Ref]; receives a
2463
+ [`ListController`][pythonnative.ListController] on
2464
+ ``ref.current`` after mount.
2669
2465
  key: Stable identity for keyed reconciliation of the list.
2670
2466
 
2671
2467
  Returns:
@@ -2945,10 +2741,10 @@ def Picker(
2945
2741
  accessibility_label: Optional[str] = None,
2946
2742
  accessibility_hint: Optional[str] = None,
2947
2743
  accessible: Optional[bool] = None,
2948
- accessibility_state: Optional[Dict[str, Any]] = None,
2744
+ accessibility_state: Optional[AccessibilityState] = None,
2949
2745
  accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
2950
2746
  test_id: Optional[str] = None,
2951
- ref: Optional[Dict[str, Any]] = None,
2747
+ ref: Optional[Ref] = None,
2952
2748
  key: Optional[str] = None,
2953
2749
  ) -> Element:
2954
2750
  """A real native dropdown / select widget.
@@ -2981,7 +2777,7 @@ def Picker(
2981
2777
  test_id: Stable identifier for UI tests; exposed as
2982
2778
  ``resource-id`` on Android and ``accessibilityIdentifier``
2983
2779
  on iOS.
2984
- ref: Optional ``use_ref()`` dict.
2780
+ ref: Optional [`Ref`][pythonnative.Ref] from ``use_ref()``.
2985
2781
  key: Stable identity for keyed reconciliation.
2986
2782
 
2987
2783
  Returns: