pythonnative 0.23.0__py3-none-any.whl → 0.24.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.
1639
+
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.
1975
1648
 
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.
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).
2003
1690
 
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.
1691
+ ``fallback`` may be:
1692
+
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,20 @@ 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)
2036
1733
 
2037
1734
 
2038
1735
  # ======================================================================
@@ -2082,7 +1779,7 @@ class _RowSpec:
2082
1779
 
2083
1780
  def _dispatch_scroll_command(scroll_ref: Any, name: str, args: Dict[str, Any]) -> Any:
2084
1781
  """Send an imperative command to the ScrollView under ``scroll_ref``."""
2085
- tag = scroll_ref.get("_pn_tag") if isinstance(scroll_ref, dict) else None
1782
+ tag = getattr(scroll_ref, "_pn_tag", None)
2086
1783
  if tag is None:
2087
1784
  return None
2088
1785
  from .native_views import get_registry
@@ -2093,6 +1790,55 @@ def _dispatch_scroll_command(scroll_ref: Any, name: str, args: Dict[str, Any]) -
2093
1790
  return None
2094
1791
 
2095
1792
 
1793
+ class ListController:
1794
+ """Imperative scroll handle published on a list's ``ref``.
1795
+
1796
+ [`FlatList`][pythonnative.FlatList] and
1797
+ [`SectionList`][pythonnative.SectionList] install a
1798
+ ``ListController`` on ``ref.current`` (via
1799
+ [`use_imperative_handle`][pythonnative.use_imperative_handle])
1800
+ after mount and clear it back to ``None`` on unmount.
1801
+
1802
+ Example:
1803
+ ```python
1804
+ import pythonnative as pn
1805
+
1806
+ @pn.component
1807
+ def Chat(messages):
1808
+ list_ref = pn.use_ref()
1809
+ pn.use_layout_effect(
1810
+ lambda: list_ref.current and list_ref.current.scroll_to_end(animated=False),
1811
+ [len(messages)],
1812
+ )
1813
+ return pn.FlatList(data=messages, render_item=Bubble, ref=list_ref)
1814
+ ```
1815
+ """
1816
+
1817
+ __slots__ = ("_scroll_to_offset", "_scroll_to_index", "_scroll_to_end")
1818
+
1819
+ def __init__(
1820
+ self,
1821
+ scroll_to_offset: Callable[[float, bool], None],
1822
+ scroll_to_index: Callable[[int, bool], None],
1823
+ scroll_to_end: Callable[[bool], None],
1824
+ ) -> None:
1825
+ self._scroll_to_offset = scroll_to_offset
1826
+ self._scroll_to_index = scroll_to_index
1827
+ self._scroll_to_end = scroll_to_end
1828
+
1829
+ def scroll_to_offset(self, offset: float, animated: bool = True) -> None:
1830
+ """Scroll to an absolute content offset in points."""
1831
+ self._scroll_to_offset(offset, animated)
1832
+
1833
+ def scroll_to_index(self, index: int, animated: bool = True) -> None:
1834
+ """Scroll so the row at ``index`` sits at the top of the viewport."""
1835
+ self._scroll_to_index(index, animated)
1836
+
1837
+ def scroll_to_end(self, animated: bool = True) -> None:
1838
+ """Scroll to the end of the content."""
1839
+ self._scroll_to_end(animated)
1840
+
1841
+
2096
1842
  @component
2097
1843
  def _VirtualizedList(**p: Any) -> Element:
2098
1844
  """Shared windowing engine behind FlatList and SectionList."""
@@ -2104,18 +1850,18 @@ def _VirtualizedList(**p: Any) -> Element:
2104
1850
  initial_extent: float = float(p.get("initial_window_extent") or 800.0)
2105
1851
 
2106
1852
  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
1853
+ measured: Ref[Dict[str, float]] = use_ref({}) # row key -> measured extent (points)
1854
+ row_refs: Ref[Dict[str, Ref]] = use_ref({}) # row key -> Ref for live rows
2109
1855
  end_latch = use_ref({"fired_for": -1})
2110
- viewable_ref = use_ref({"keys": ()})
1856
+ viewable_ref: Ref[Dict[str, Tuple[str, ...]]] = use_ref({"keys": ()})
2111
1857
  scroll_pos = use_ref({"offset": 0.0})
2112
- sv_ref = use_ref(None)
1858
+ sv_ref: Ref = use_ref(None)
2113
1859
 
2114
1860
  # ------------------------------------------------------------------
2115
1861
  # Extent model: measured > per-row hint > estimate. ``starts`` are
2116
1862
  # prefix sums; ``starts[n]`` is the total content extent.
2117
1863
  # ------------------------------------------------------------------
2118
- measured_map: Dict[str, float] = measured["current"]
1864
+ measured_map: Dict[str, float] = measured.current
2119
1865
  starts: List[float] = [0.0] * (n + 1)
2120
1866
  acc = 0.0
2121
1867
  for i, spec in enumerate(rows):
@@ -2128,7 +1874,7 @@ def _VirtualizedList(**p: Any) -> Element:
2128
1874
  total_extent = acc
2129
1875
 
2130
1876
  def _viewport_extent() -> float:
2131
- frame = sv_ref.get("_pn_frame") if isinstance(sv_ref, dict) else None
1877
+ frame = sv_ref._pn_frame
2132
1878
  if frame:
2133
1879
  extent = frame[2] if horizontal else frame[3]
2134
1880
  if extent and extent > 0:
@@ -2147,7 +1893,7 @@ def _VirtualizedList(**p: Any) -> Element:
2147
1893
 
2148
1894
  first, last = window
2149
1895
  if last < 0 or first >= n:
2150
- first, last = _window_for(scroll_pos["current"]["offset"], _viewport_extent())
1896
+ first, last = _window_for(scroll_pos.current["offset"], _viewport_extent())
2151
1897
  last = min(last, n - 1)
2152
1898
  first = max(0, min(first, max(0, n - 1)))
2153
1899
 
@@ -2163,8 +1909,8 @@ def _VirtualizedList(**p: Any) -> Element:
2163
1909
  user_on_scroll = p.get("on_scroll")
2164
1910
 
2165
1911
  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
1912
+ for row_key, row_ref in row_refs.current.items():
1913
+ frame = getattr(row_ref, "_pn_frame", None)
2168
1914
  if frame:
2169
1915
  extent = frame[2] if horizontal else frame[3]
2170
1916
  if extent and extent > 0:
@@ -2175,7 +1921,7 @@ def _VirtualizedList(**p: Any) -> Element:
2175
1921
  offset = float(payload.get("x" if horizontal else "y", 0.0) or 0.0)
2176
1922
  else:
2177
1923
  offset = float(payload or 0.0)
2178
- scroll_pos["current"]["offset"] = offset
1924
+ scroll_pos.current["offset"] = offset
2179
1925
  _sweep_measured()
2180
1926
  viewport = _viewport_extent()
2181
1927
 
@@ -2186,18 +1932,18 @@ def _VirtualizedList(**p: Any) -> Element:
2186
1932
  if on_end_reached is not None and total_extent > 0:
2187
1933
  remaining = total_extent - (offset + viewport)
2188
1934
  if remaining <= end_threshold * viewport:
2189
- if end_latch["current"]["fired_for"] != n:
2190
- end_latch["current"]["fired_for"] = n
1935
+ if end_latch.current["fired_for"] != n:
1936
+ end_latch.current["fired_for"] = n
2191
1937
  on_end_reached()
2192
1938
  elif remaining > end_threshold * viewport + viewport:
2193
- end_latch["current"]["fired_for"] = -1
1939
+ end_latch.current["fired_for"] = -1
2194
1940
 
2195
1941
  if on_viewable is not None and n > 0:
2196
1942
  v_first = max(0, bisect.bisect_right(starts, offset, 0, n) - 1)
2197
1943
  v_last = min(n - 1, bisect.bisect_left(starts, offset + viewport, 0, n))
2198
1944
  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
1945
+ if keys != viewable_ref.current["keys"]:
1946
+ viewable_ref.current["keys"] = keys
2201
1947
  on_viewable(
2202
1948
  [
2203
1949
  {"index": rows[i].index, "key": rows[i].key, "item": rows[i].item}
@@ -2209,33 +1955,26 @@ def _VirtualizedList(**p: Any) -> Element:
2209
1955
  user_on_scroll(payload)
2210
1956
 
2211
1957
  # ------------------------------------------------------------------
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.
1958
+ # Imperative controller (scroll_to_index / offset / end) published
1959
+ # on the user's ref. Rebuilt every render (deps=None) so the
1960
+ # closures see fresh extents.
2216
1961
  # ------------------------------------------------------------------
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})
2226
-
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)
1962
+ def _scroll_to_offset(offset: float, animated: bool = True) -> None:
1963
+ axis = "x" if horizontal else "y"
1964
+ _dispatch_scroll_command(sv_ref, "scroll_to_offset", {axis: float(offset), "animated": animated})
2230
1965
 
2231
- def scroll_to_end(animated: bool = True) -> None:
2232
- scroll_to_offset(max(0.0, total_extent - _viewport_extent()), animated)
1966
+ def _scroll_to_index(index: int, animated: bool = True) -> None:
1967
+ idx = max(0, min(int(index), n - 1)) if n else 0
1968
+ _scroll_to_offset(starts[idx], animated)
2233
1969
 
2234
- controller["scroll_to_offset"] = scroll_to_offset
2235
- controller["scroll_to_index"] = scroll_to_index
2236
- controller["scroll_to_end"] = scroll_to_end
1970
+ def _scroll_to_end(animated: bool = True) -> None:
1971
+ _scroll_to_offset(max(0.0, total_extent - _viewport_extent()), animated)
2237
1972
 
2238
- use_effect(_attach_controller, None)
1973
+ use_imperative_handle(
1974
+ p.get("controller_ref"),
1975
+ lambda: ListController(_scroll_to_offset, _scroll_to_index, _scroll_to_end),
1976
+ None,
1977
+ )
2239
1978
 
2240
1979
  # ------------------------------------------------------------------
2241
1980
  # Children: header, leading spacer, windowed rows, trailing spacer,
@@ -2254,17 +1993,17 @@ def _VirtualizedList(**p: Any) -> Element:
2254
1993
  if empty is not None:
2255
1994
  children.append(View(empty, key="__pn_empty__"))
2256
1995
  else:
2257
- live_refs: Dict[str, Any] = {}
1996
+ live_refs: Dict[str, Ref] = {}
2258
1997
  lead = starts[first]
2259
1998
  if lead > 0:
2260
1999
  lead_style: Dict[str, Any] = {spacer_key: lead}
2261
2000
  children.append(View(style=lead_style, key="__pn_lead__"))
2262
2001
  for i in range(first, last + 1):
2263
2002
  spec = rows[i]
2264
- row_ref = row_refs["current"].get(spec.key) or {"current": None}
2003
+ row_ref = row_refs.current.get(spec.key) or Ref()
2265
2004
  live_refs[spec.key] = row_ref
2266
2005
  children.append(View(spec.make(), ref=row_ref, key=spec.key))
2267
- row_refs["current"] = live_refs
2006
+ row_refs.current = live_refs
2268
2007
  trail = total_extent - starts[last + 1]
2269
2008
  if trail > 0:
2270
2009
  trail_style: Dict[str, Any] = {spacer_key: trail}
@@ -2318,9 +2057,9 @@ def _NativeList(**p: Any) -> Element:
2318
2057
  heights: List[float] = [float(spec.extent or 0.0) for spec in rows]
2319
2058
  uniform = len(set(heights)) <= 1
2320
2059
 
2321
- internal_ref = use_ref(None)
2060
+ internal_ref: Ref = use_ref(None)
2322
2061
  end_latch = use_ref({"fired_for": -1})
2323
- viewable_ref = use_ref({"keys": ()})
2062
+ viewable_ref: Ref[Dict[str, Tuple[str, ...]]] = use_ref({"keys": ()})
2324
2063
 
2325
2064
  starts: List[float] = [0.0] * (n + 1)
2326
2065
  acc = 0.0
@@ -2349,18 +2088,18 @@ def _NativeList(**p: Any) -> Element:
2349
2088
  if on_end_reached is not None and total_extent > 0:
2350
2089
  remaining = total_extent - (offset + viewport)
2351
2090
  if remaining <= end_threshold * viewport:
2352
- if end_latch["current"]["fired_for"] != n:
2353
- end_latch["current"]["fired_for"] = n
2091
+ if end_latch.current["fired_for"] != n:
2092
+ end_latch.current["fired_for"] = n
2354
2093
  on_end_reached()
2355
2094
  elif remaining > end_threshold * viewport + viewport:
2356
- end_latch["current"]["fired_for"] = -1
2095
+ end_latch.current["fired_for"] = -1
2357
2096
 
2358
2097
  if on_viewable is not None and n > 0:
2359
2098
  v_first = max(0, bisect.bisect_right(starts, offset, 0, n) - 1)
2360
2099
  v_last = min(n - 1, bisect.bisect_left(starts, offset + viewport, 0, n))
2361
2100
  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
2101
+ if keys != viewable_ref.current["keys"]:
2102
+ viewable_ref.current["keys"] = keys
2364
2103
  on_viewable(
2365
2104
  [
2366
2105
  {"index": rows[i].index, "key": rows[i].key, "item": rows[i].item}
@@ -2371,26 +2110,20 @@ def _NativeList(**p: Any) -> Element:
2371
2110
  if user_on_scroll is not None:
2372
2111
  user_on_scroll({"x": 0.0, "y": offset})
2373
2112
 
2374
- controller = p.get("controller_ref")
2375
-
2376
- def _attach_controller() -> None:
2377
- if not isinstance(controller, dict):
2378
- return
2379
-
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})
2113
+ def _scroll_to_offset(offset: float, animated: bool = True) -> None:
2114
+ _dispatch_scroll_command(internal_ref, "scroll_to_offset", {"y": float(offset), "animated": animated})
2382
2115
 
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})
2116
+ def _scroll_to_index(index: int, animated: bool = True) -> None:
2117
+ _dispatch_scroll_command(internal_ref, "scroll_to_index", {"index": int(index), "animated": animated})
2385
2118
 
2386
- def scroll_to_end(animated: bool = True) -> None:
2387
- _dispatch_scroll_command(internal_ref, "scroll_to_end", {"animated": animated})
2119
+ def _scroll_to_end(animated: bool = True) -> None:
2120
+ _dispatch_scroll_command(internal_ref, "scroll_to_end", {"animated": animated})
2388
2121
 
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)
2122
+ use_imperative_handle(
2123
+ p.get("controller_ref"),
2124
+ lambda: ListController(_scroll_to_offset, _scroll_to_index, _scroll_to_end),
2125
+ None,
2126
+ )
2394
2127
 
2395
2128
  props: Dict[str, Any] = dict(p.get("list_style") or {})
2396
2129
  props["count"] = n
@@ -2434,7 +2167,7 @@ def FlatList(
2434
2167
  shows_scroll_indicator: bool = True,
2435
2168
  content_container_style: StyleProp = None,
2436
2169
  style: StyleProp = None,
2437
- ref: Optional[Dict[str, Any]] = None,
2170
+ ref: Optional[Ref] = None,
2438
2171
  key: Optional[str] = None,
2439
2172
  ) -> Element:
2440
2173
  """Virtualized scrollable list that renders items from ``data`` lazily.
@@ -2447,10 +2180,12 @@ def FlatList(
2447
2180
  unknown rows start at ``estimated_item_height`` and are corrected
2448
2181
  with their measured extent once they've been on screen.
2449
2182
 
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"]()``.
2183
+ Pass a [`Ref`][pythonnative.Ref] (from
2184
+ [`use_ref`][pythonnative.use_ref]) to receive a
2185
+ [`ListController`][pythonnative.ListController] on ``ref.current``:
2186
+ ``ref.current.scroll_to_index(i)``,
2187
+ ``ref.current.scroll_to_offset(pts)``, and
2188
+ ``ref.current.scroll_to_end()``.
2454
2189
 
2455
2190
  Args:
2456
2191
  data: List of arbitrary item values.
@@ -2485,8 +2220,9 @@ def FlatList(
2485
2220
  content_container_style: Style applied to the inner content
2486
2221
  wrapper.
2487
2222
  style: Style for the outer scroll container.
2488
- ref: Optional ``use_ref()`` dict; receives the scroll
2489
- controller functions.
2223
+ ref: Optional [`Ref`][pythonnative.Ref]; receives a
2224
+ [`ListController`][pythonnative.ListController] on
2225
+ ``ref.current`` after mount.
2490
2226
  key: Stable identity for keyed reconciliation of the list.
2491
2227
 
2492
2228
  Returns:
@@ -2630,7 +2366,7 @@ def SectionList(
2630
2366
  on_end_reached_threshold: float = 0.5,
2631
2367
  on_scroll: Optional[Callable[[Dict[str, float]], None]] = None,
2632
2368
  style: StyleProp = None,
2633
- ref: Optional[Dict[str, Any]] = None,
2369
+ ref: Optional[Ref] = None,
2634
2370
  key: Optional[str] = None,
2635
2371
  ) -> Element:
2636
2372
  """Virtualized list with section headers interleaved between row groups.
@@ -2664,8 +2400,9 @@ def SectionList(
2664
2400
  multiples, at which ``on_end_reached`` fires.
2665
2401
  on_scroll: Called with the raw scroll payload.
2666
2402
  style: Style for the outer scroll container.
2667
- ref: Optional ``use_ref()`` dict; receives the scroll
2668
- controller functions.
2403
+ ref: Optional [`Ref`][pythonnative.Ref]; receives a
2404
+ [`ListController`][pythonnative.ListController] on
2405
+ ``ref.current`` after mount.
2669
2406
  key: Stable identity for keyed reconciliation of the list.
2670
2407
 
2671
2408
  Returns:
@@ -2945,10 +2682,10 @@ def Picker(
2945
2682
  accessibility_label: Optional[str] = None,
2946
2683
  accessibility_hint: Optional[str] = None,
2947
2684
  accessible: Optional[bool] = None,
2948
- accessibility_state: Optional[Dict[str, Any]] = None,
2685
+ accessibility_state: Optional[AccessibilityState] = None,
2949
2686
  accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
2950
2687
  test_id: Optional[str] = None,
2951
- ref: Optional[Dict[str, Any]] = None,
2688
+ ref: Optional[Ref] = None,
2952
2689
  key: Optional[str] = None,
2953
2690
  ) -> Element:
2954
2691
  """A real native dropdown / select widget.
@@ -2981,7 +2718,7 @@ def Picker(
2981
2718
  test_id: Stable identifier for UI tests; exposed as
2982
2719
  ``resource-id`` on Android and ``accessibilityIdentifier``
2983
2720
  on iOS.
2984
- ref: Optional ``use_ref()`` dict.
2721
+ ref: Optional [`Ref`][pythonnative.Ref] from ``use_ref()``.
2985
2722
  key: Stable identity for keyed reconciliation.
2986
2723
 
2987
2724
  Returns: