@hjmds/react-native 1.18.0 → 1.20.0
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.
- package/README.md +8 -8
- package/dist/actions.d.ts +3 -23
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +7 -10
- package/dist/actions.js.map +1 -1
- package/dist/agreement.d.ts +1 -7
- package/dist/agreement.d.ts.map +1 -1
- package/dist/agreement.js +21 -15
- package/dist/agreement.js.map +1 -1
- package/dist/asset.d.ts +2 -13
- package/dist/asset.d.ts.map +1 -1
- package/dist/asset.js +4 -7
- package/dist/asset.js.map +1 -1
- package/dist/bottom-info.d.ts +1 -7
- package/dist/bottom-info.d.ts.map +1 -1
- package/dist/bottom-info.js +2 -4
- package/dist/bottom-info.js.map +1 -1
- package/dist/carousel-motion.d.ts +1 -0
- package/dist/carousel-motion.d.ts.map +1 -1
- package/dist/carousel-motion.js +5 -3
- package/dist/carousel-motion.js.map +1 -1
- package/dist/carousel.d.ts +1 -7
- package/dist/carousel.d.ts.map +1 -1
- package/dist/carousel.js +14 -6
- package/dist/carousel.js.map +1 -1
- package/dist/collapsible.d.ts +1 -7
- package/dist/collapsible.d.ts.map +1 -1
- package/dist/collapsible.js +4 -4
- package/dist/collapsible.js.map +1 -1
- package/dist/data-display.d.ts +19 -153
- package/dist/data-display.d.ts.map +1 -1
- package/dist/data-display.js +73 -90
- package/dist/data-display.js.map +1 -1
- package/dist/date-entry.d.ts +1 -0
- package/dist/date-entry.d.ts.map +1 -1
- package/dist/date-entry.js +22 -20
- package/dist/date-entry.js.map +1 -1
- package/dist/date-picker.d.ts +1 -7
- package/dist/date-picker.d.ts.map +1 -1
- package/dist/date-picker.js +19 -11
- package/dist/date-picker.js.map +1 -1
- package/dist/duration-field.d.ts +1 -1
- package/dist/duration-field.d.ts.map +1 -1
- package/dist/duration-field.js +11 -9
- package/dist/duration-field.js.map +1 -1
- package/dist/evidence.d.ts +1 -1
- package/dist/evidence.js +1 -1
- package/dist/feedback.d.ts +8 -88
- package/dist/feedback.d.ts.map +1 -1
- package/dist/feedback.js +72 -62
- package/dist/feedback.js.map +1 -1
- package/dist/field-group.d.ts +3 -2
- package/dist/field-group.d.ts.map +1 -1
- package/dist/field-group.js +7 -4
- package/dist/field-group.js.map +1 -1
- package/dist/file-picker.d.ts +1 -7
- package/dist/file-picker.d.ts.map +1 -1
- package/dist/file-picker.js +21 -11
- package/dist/file-picker.js.map +1 -1
- package/dist/forms.d.ts +6 -21
- package/dist/forms.d.ts.map +1 -1
- package/dist/forms.js +29 -35
- package/dist/forms.js.map +1 -1
- package/dist/gravity-letters.d.ts.map +1 -1
- package/dist/gravity-letters.js +13 -5
- package/dist/gravity-letters.js.map +1 -1
- package/dist/grid-reveal.d.ts.map +1 -1
- package/dist/grid-reveal.js +10 -2
- package/dist/grid-reveal.js.map +1 -1
- package/dist/heading.d.ts +1 -7
- package/dist/heading.d.ts.map +1 -1
- package/dist/heading.js +1 -4
- package/dist/heading.js.map +1 -1
- package/dist/image-viewer.d.ts.map +1 -1
- package/dist/image-viewer.js +2 -1
- package/dist/image-viewer.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/inline-confirm.d.ts.map +1 -1
- package/dist/inline-confirm.js +5 -1
- package/dist/inline-confirm.js.map +1 -1
- package/dist/inputs.d.ts +4 -71
- package/dist/inputs.d.ts.map +1 -1
- package/dist/inputs.js +25 -50
- package/dist/inputs.js.map +1 -1
- package/dist/internal/accessible-focus.d.ts +3 -0
- package/dist/internal/accessible-focus.d.ts.map +1 -0
- package/dist/internal/accessible-focus.js +15 -0
- package/dist/internal/accessible-focus.js.map +1 -0
- package/dist/internal/diagnostics.d.ts +7 -0
- package/dist/internal/diagnostics.d.ts.map +1 -0
- package/dist/internal/diagnostics.js +20 -0
- package/dist/internal/diagnostics.js.map +1 -0
- package/dist/internal/document-resource.d.ts.map +1 -1
- package/dist/internal/document-resource.js +2 -2
- package/dist/internal/document-resource.js.map +1 -1
- package/dist/internal/spinner.d.ts +1 -7
- package/dist/internal/spinner.d.ts.map +1 -1
- package/dist/internal/spinner.js +2 -4
- package/dist/internal/spinner.js.map +1 -1
- package/dist/masonry.d.ts.map +1 -1
- package/dist/masonry.js +2 -1
- package/dist/masonry.js.map +1 -1
- package/dist/mentions.d.ts +1 -7
- package/dist/mentions.d.ts.map +1 -1
- package/dist/mentions.js +11 -7
- package/dist/mentions.js.map +1 -1
- package/dist/navigation.d.ts +4 -74
- package/dist/navigation.d.ts.map +1 -1
- package/dist/navigation.js +97 -45
- package/dist/navigation.js.map +1 -1
- package/dist/number-field.d.ts +1 -21
- package/dist/number-field.d.ts.map +1 -1
- package/dist/number-field.js +6 -5
- package/dist/number-field.js.map +1 -1
- package/dist/overlays.d.ts +7 -16
- package/dist/overlays.d.ts.map +1 -1
- package/dist/overlays.js +220 -213
- package/dist/overlays.js.map +1 -1
- package/dist/primitives.d.ts +3 -18
- package/dist/primitives.d.ts.map +1 -1
- package/dist/primitives.js +8 -10
- package/dist/primitives.js.map +1 -1
- package/dist/provider-button.d.ts +1 -7
- package/dist/provider-button.d.ts.map +1 -1
- package/dist/provider-button.js +1 -4
- package/dist/provider-button.js.map +1 -1
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +9 -1
- package/dist/provider.js.map +1 -1
- package/dist/saved-items.d.ts.map +1 -1
- package/dist/saved-items.js +6 -1
- package/dist/saved-items.js.map +1 -1
- package/dist/screen-flows.d.ts +1 -2
- package/dist/screen-flows.d.ts.map +1 -1
- package/dist/screen-flows.js +60 -16
- package/dist/screen-flows.js.map +1 -1
- package/dist/screens.d.ts.map +1 -1
- package/dist/screens.js +21 -10
- package/dist/screens.js.map +1 -1
- package/dist/sheet-gesture.d.ts.map +1 -1
- package/dist/sheet-gesture.js +6 -3
- package/dist/sheet-gesture.js.map +1 -1
- package/dist/slider.d.ts +1 -21
- package/dist/slider.d.ts.map +1 -1
- package/dist/slider.js +3 -5
- package/dist/slider.js.map +1 -1
- package/dist/sortable.d.ts.map +1 -1
- package/dist/sortable.js +2 -1
- package/dist/sortable.js.map +1 -1
- package/dist/status-text.d.ts +17 -0
- package/dist/status-text.d.ts.map +1 -0
- package/dist/status-text.js +18 -0
- package/dist/status-text.js.map +1 -0
- package/dist/steps.d.ts +1 -7
- package/dist/steps.d.ts.map +1 -1
- package/dist/steps.js +2 -4
- package/dist/steps.js.map +1 -1
- package/dist/swipe-actions.d.ts.map +1 -1
- package/dist/swipe-actions.js +13 -2
- package/dist/swipe-actions.js.map +1 -1
- package/dist/tags-input.d.ts +1 -7
- package/dist/tags-input.d.ts.map +1 -1
- package/dist/tags-input.js +2 -4
- package/dist/tags-input.js.map +1 -1
- package/dist/toggle-group.d.ts +1 -7
- package/dist/toggle-group.d.ts.map +1 -1
- package/dist/toggle-group.js +10 -5
- package/dist/toggle-group.js.map +1 -1
- package/dist/top.d.ts +1 -7
- package/dist/top.d.ts.map +1 -1
- package/dist/top.js +1 -4
- package/dist/top.js.map +1 -1
- package/dist/transfer-list.d.ts +2 -8
- package/dist/transfer-list.d.ts.map +1 -1
- package/dist/transfer-list.js +30 -11
- package/dist/transfer-list.js.map +1 -1
- package/dist/upload-item.d.ts +1 -7
- package/dist/upload-item.d.ts.map +1 -1
- package/dist/upload-item.js +2 -4
- package/dist/upload-item.js.map +1 -1
- package/dist/virtual-list.d.ts.map +1 -1
- package/dist/virtual-list.js +2 -1
- package/dist/virtual-list.js.map +1 -1
- package/docs/liquid-toast.md +9 -0
- package/package.json +9 -3
- package/dist/internal/deprecated-style.d.ts +0 -8
- package/dist/internal/deprecated-style.d.ts.map +0 -1
- package/dist/internal/deprecated-style.js +0 -34
- package/dist/internal/deprecated-style.js.map +0 -1
package/dist/navigation.js
CHANGED
|
@@ -9,7 +9,7 @@ import { resolveTabIndicator, resolveTabsAppearance, sampleTabIndicator, } from
|
|
|
9
9
|
import { bottomNavigationRecipe, counterBadgeRecipe, loadMoreRecipe, menuRecipe, spinnerRecipe, tabsRecipe, topBarRecipe, } from "@hjmds/design-contracts/recipes";
|
|
10
10
|
import { Fragment, forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState, } from "react";
|
|
11
11
|
import { AccessibilityInfo, ActivityIndicator, Animated, AppState, Easing, Keyboard, Modal, Platform, Pressable, ScrollView, View, findNodeHandle, } from "react-native";
|
|
12
|
-
import {
|
|
12
|
+
import { assertNoRemovedStyleProps } from "./internal/diagnostics.js";
|
|
13
13
|
import { RecipeButton } from "./internal/recipe-button.js";
|
|
14
14
|
import { isWebRenderer, webOnly, webTabProps } from "./internal/web-a11y.js";
|
|
15
15
|
import { Button } from "./actions.js";
|
|
@@ -35,8 +35,8 @@ export function getDynamicTabPanelId(tabsId) {
|
|
|
35
35
|
}
|
|
36
36
|
/** External panel host for products that keep routing, query, or list state outside Tabs. */
|
|
37
37
|
export function TabPanel(props) {
|
|
38
|
-
const { tabsId, activeValue, label, children, layoutStyle
|
|
39
|
-
|
|
38
|
+
const { tabsId, activeValue, label, children, layoutStyle } = props;
|
|
39
|
+
assertNoRemovedStyleProps("TabPanel", props, ["style"]);
|
|
40
40
|
const dynamic = props.mode === "dynamic";
|
|
41
41
|
const value = dynamic ? activeValue : props.value;
|
|
42
42
|
const selected = value === activeValue;
|
|
@@ -52,12 +52,12 @@ export function TabPanel(props) {
|
|
|
52
52
|
(mountPolicy === "visited" && visited);
|
|
53
53
|
if (!mounted)
|
|
54
54
|
return null;
|
|
55
|
-
return (_jsx(View, { nativeID: getTabPanelId(tabsId, value, dynamic ? "dynamic" : "keyed"), accessibilityLabel: label, accessibilityLabelledBy: getTabId(tabsId, dynamic ? activeValue : value), accessibilityElementsHidden: !selected, importantForAccessibility: selected ? "auto" : "no-hide-descendants", pointerEvents: selected ? "auto" : "none", role: "tabpanel", style: [
|
|
55
|
+
return (_jsx(View, { nativeID: getTabPanelId(tabsId, value, dynamic ? "dynamic" : "keyed"), accessibilityLabel: label, accessibilityLabelledBy: getTabId(tabsId, dynamic ? activeValue : value), accessibilityElementsHidden: !selected, importantForAccessibility: selected ? "auto" : "no-hide-descendants", pointerEvents: selected ? "auto" : "none", role: "tabpanel", style: [layoutStyle, selected ? null : { display: "none" }], children: children }));
|
|
56
56
|
}
|
|
57
57
|
// UI control labels keep the ui font even when their metric variant is body; content still uses reading.
|
|
58
58
|
export function Tabs(props) {
|
|
59
|
-
const { id, label, items, value: valueProp, defaultValue, onValueChange, activationMode = tabsBehaviorDefaults.activationMode, mountPolicy = tabsBehaviorDefaults.mountPolicy, panelMode = tabsBehaviorDefaults.panelMode, appearance: suppliedAppearance, orientation = tabsBehaviorDefaults.orientation, direction: directionProp, loop = tabsBehaviorDefaults.loop, size = tabsRecipe.defaults.size, layout = tabsRecipe.defaults.layout, overflow = tabsRecipe.defaults.overflow, renderPanels = true, children, layoutStyle,
|
|
60
|
-
|
|
59
|
+
const { id, label, items, value: valueProp, defaultValue, onValueChange, activationMode = tabsBehaviorDefaults.activationMode, mountPolicy = tabsBehaviorDefaults.mountPolicy, panelMode = tabsBehaviorDefaults.panelMode, appearance: suppliedAppearance, orientation = tabsBehaviorDefaults.orientation, direction: directionProp, loop = tabsBehaviorDefaults.loop, size = tabsRecipe.defaults.size, layout = tabsRecipe.defaults.layout, overflow = tabsRecipe.defaults.overflow, renderPanels = true, children, layoutStyle, } = props;
|
|
60
|
+
assertNoRemovedStyleProps("Tabs", props, ["style", "tabListStyle"]);
|
|
61
61
|
if (!label.trim())
|
|
62
62
|
throw new TypeError("Tabs label must not be empty");
|
|
63
63
|
if (panelMode === "dynamic" && mountPolicy !== "active") {
|
|
@@ -215,9 +215,8 @@ export function Tabs(props) {
|
|
|
215
215
|
flexDirection: orientation === "vertical" ? "row" : "column",
|
|
216
216
|
gap: spacing.md,
|
|
217
217
|
},
|
|
218
|
-
style,
|
|
219
218
|
layoutStyle,
|
|
220
|
-
], children: [_jsxs(ScrollView, { ref: tabScroll, onLayout: event => setTabViewport(event.nativeEvent.layout.width), onContentSizeChange: width => setTabContentWidth(width), nativeID: id, accessibilityLabel: label, accessibilityRole: "tablist", horizontal: orientation === "horizontal", scrollEnabled: scrollable && !fitted, showsHorizontalScrollIndicator: false, showsVerticalScrollIndicator: false,
|
|
219
|
+
], children: [_jsxs(ScrollView, { ref: tabScroll, onLayout: event => setTabViewport(event.nativeEvent.layout.width), onContentSizeChange: width => setTabContentWidth(width), nativeID: id, accessibilityLabel: label, accessibilityRole: "tablist", horizontal: orientation === "horizontal", scrollEnabled: scrollable && !fitted, showsHorizontalScrollIndicator: false, showsVerticalScrollIndicator: false, contentContainerStyle: [
|
|
221
220
|
{
|
|
222
221
|
borderBottomColor: orientation === "horizontal"
|
|
223
222
|
? resolveColorReference(tabsRecipe.colors.divider, theme.palette)
|
|
@@ -273,6 +272,8 @@ export function Tabs(props) {
|
|
|
273
272
|
},
|
|
274
273
|
onMoveFocus: (intent) => moveFocus(item.id, intent),
|
|
275
274
|
})), disabled: item.disabled, onAccessibilityAction: (event) => {
|
|
275
|
+
if (item.disabled)
|
|
276
|
+
return;
|
|
276
277
|
const action = event.nativeEvent.actionName;
|
|
277
278
|
if (action === "activate")
|
|
278
279
|
setSelected(item.id);
|
|
@@ -281,10 +282,14 @@ export function Tabs(props) {
|
|
|
281
282
|
else if (action === "decrement")
|
|
282
283
|
moveFocus(item.id, "previous");
|
|
283
284
|
}, onFocus: () => {
|
|
285
|
+
if (item.disabled)
|
|
286
|
+
return;
|
|
284
287
|
setFocusValue(item.id);
|
|
285
288
|
if (activationMode === "automatic")
|
|
286
289
|
setSelected(item.id);
|
|
287
290
|
}, onPress: () => {
|
|
291
|
+
if (item.disabled)
|
|
292
|
+
return;
|
|
288
293
|
setFocusValue(item.id);
|
|
289
294
|
setSelected(item.id);
|
|
290
295
|
}, style: ({ pressed }) => [
|
|
@@ -358,8 +363,8 @@ function useBottomNavigationKeyboardVisible() {
|
|
|
358
363
|
return visible;
|
|
359
364
|
}
|
|
360
365
|
/** Router-owned persistent destinations; activation emits intent without mutating selection. */
|
|
361
|
-
export function BottomNavigation({ descriptor, onActivate, onLongActivate, renderIcon, renderBadge, getItemTestID, primaryAction, configuration = {}, safeAreaBottom = 0, layoutStyle,
|
|
362
|
-
|
|
366
|
+
export function BottomNavigation({ descriptor, onActivate, onLongActivate, renderIcon, renderBadge, getItemTestID, primaryAction, configuration = {}, safeAreaBottom = 0, layoutStyle, ...removedProps }) {
|
|
367
|
+
assertNoRemovedStyleProps("BottomNavigation", removedProps, ["style", "surfaceStyle", "listStyle", "primaryActionStyle"]);
|
|
363
368
|
const resolved = resolveBottomNavigationDescriptor(descriptor);
|
|
364
369
|
if (!Number.isFinite(safeAreaBottom) || safeAreaBottom < 0) {
|
|
365
370
|
throw new RangeError("BottomNavigation safeAreaBottom must be non-negative");
|
|
@@ -370,6 +375,10 @@ export function BottomNavigation({ descriptor, onActivate, onLongActivate, rende
|
|
|
370
375
|
direction: configuration.direction ?? theme.environment.direction,
|
|
371
376
|
}, resolved.items.length);
|
|
372
377
|
const capsule = presentation.presentation === "capsule";
|
|
378
|
+
const tile = presentation.selection === "tile";
|
|
379
|
+
const tileRecipe = bottomNavigationRecipe.selectionTile;
|
|
380
|
+
const raised = tile && presentation.surface === "raised";
|
|
381
|
+
const raisedRecipe = bottomNavigationRecipe.raisedSurface;
|
|
373
382
|
// Hidden labels keep the item's accessibilityLabel; only the visible Text row is dropped.
|
|
374
383
|
const labelsVisible = presentation.labels === "visible";
|
|
375
384
|
const expandedLabels = theme.environment.textScale >= 1.5 || resolved.items.length > 4;
|
|
@@ -381,17 +390,21 @@ export function BottomNavigation({ descriptor, onActivate, onLongActivate, rende
|
|
|
381
390
|
const presentationRecipe = bottomNavigationRecipe.presentations[presentation.presentation];
|
|
382
391
|
// Floating/capsule presentations own elevation; a flat bar must never gain
|
|
383
392
|
// one just because a profile has a floating token. Keep router state outside.
|
|
384
|
-
|
|
393
|
+
// The raised plank's lower edge is its elevation; a floating shadow under it reads as two objects.
|
|
394
|
+
const surfaceShadow = presentationRecipe.shadow && !raised
|
|
385
395
|
? theme.designProfile?.tokens.shadow.floating ?? presentationRecipe.shadow
|
|
386
396
|
: null;
|
|
387
397
|
const centerGap = bottomNavigationRecipe.distributions[presentation.distribution].centerGap;
|
|
388
398
|
const middleIndex = resolved.items.length / 2 - 1;
|
|
389
399
|
const idleColor = resolveColorReference(bottomNavigationRecipe.colors.idle, theme.palette);
|
|
390
|
-
const selectedIconColor = resolveColorReference(bottomNavigationRecipe.colors.selectedIcon, theme.palette);
|
|
391
|
-
const selectedLabelColor = resolveColorReference(bottomNavigationRecipe.colors.selectedLabel, theme.palette);
|
|
400
|
+
const selectedIconColor = resolveColorReference(tile ? tileRecipe.selectedContent : bottomNavigationRecipe.colors.selectedIcon, theme.palette);
|
|
401
|
+
const selectedLabelColor = resolveColorReference(tile ? tileRecipe.selectedContent : bottomNavigationRecipe.colors.selectedLabel, theme.palette);
|
|
392
402
|
const pressedBackground = resolveColorReference(bottomNavigationRecipe.states.pressedBackground, theme.palette);
|
|
393
|
-
const surfaceBackground = resolveColorReference(presentationRecipe.background, theme.palette);
|
|
394
|
-
const surfaceBorder = resolveColorReference(presentationRecipe.border, theme.palette);
|
|
403
|
+
const surfaceBackground = resolveColorReference(raised ? raisedRecipe.background : tile ? tileRecipe.background : presentationRecipe.background, theme.palette);
|
|
404
|
+
const surfaceBorder = resolveColorReference(raised ? raisedRecipe.border : presentationRecipe.border, theme.palette);
|
|
405
|
+
const selectedDepthColor = raised
|
|
406
|
+
? resolveColorReference(raisedRecipe.selectedDepthColor, theme.palette)
|
|
407
|
+
: undefined;
|
|
395
408
|
const badgeMetrics = counterBadgeRecipe.sizes[bottomNavigationRecipe.badge.size];
|
|
396
409
|
const badgeTone = counterBadgeRecipe.tones.danger;
|
|
397
410
|
const badgeVariant = counterBadgeRecipe.variants[bottomNavigationRecipe.badge.variant];
|
|
@@ -407,7 +420,6 @@ export function BottomNavigation({ descriptor, onActivate, onLongActivate, rende
|
|
|
407
420
|
paddingTop: presentationRecipe.outerPaddingTop,
|
|
408
421
|
width: "100%",
|
|
409
422
|
},
|
|
410
|
-
style,
|
|
411
423
|
layoutStyle,
|
|
412
424
|
], children: _jsxs(View, { style: [
|
|
413
425
|
{
|
|
@@ -424,6 +436,8 @@ export function BottomNavigation({ descriptor, onActivate, onLongActivate, rende
|
|
|
424
436
|
// Capsule's own list paints its border; bar/floating keep the recipe top edge here.
|
|
425
437
|
borderTopWidth: capsule ? 0 : presentationRecipe.borderWidth,
|
|
426
438
|
borderWidth: presentation.presentation === "floating" ? presentationRecipe.borderWidth : 0,
|
|
439
|
+
// Per-edge width after borderWidth: only the lower edge thickens into the plank side.
|
|
440
|
+
...(raised ? { borderBottomWidth: raisedRecipe.depth } : {}),
|
|
427
441
|
...(surfaceShadow ? resolveNativeShadowElevation(surfaceShadow, theme.designProfile !== undefined, 8) : { elevation: 0 }),
|
|
428
442
|
maxWidth: presentationRecipe.maxWidth ?? undefined,
|
|
429
443
|
position: "relative",
|
|
@@ -435,7 +449,6 @@ export function BottomNavigation({ descriptor, onActivate, onLongActivate, rende
|
|
|
435
449
|
shadowRadius: surfaceShadow?.radius,
|
|
436
450
|
width: "100%",
|
|
437
451
|
},
|
|
438
|
-
surfaceStyle,
|
|
439
452
|
], children: [_jsx(View, { accessibilityLabel: resolved.accessibilityLabel, accessibilityRole: "tablist", style: [
|
|
440
453
|
{
|
|
441
454
|
alignItems: capsule ? "stretch" : "flex-start",
|
|
@@ -444,13 +457,13 @@ export function BottomNavigation({ descriptor, onActivate, onLongActivate, rende
|
|
|
444
457
|
width: capsule ? undefined : "100%",
|
|
445
458
|
flex: capsule ? 1 : undefined,
|
|
446
459
|
minWidth: 0,
|
|
447
|
-
padding: capsule ? spacing.xxs : 0,
|
|
460
|
+
padding: tile ? tileRecipe.padding : capsule ? spacing.xxs : 0,
|
|
461
|
+
gap: tile ? tileRecipe.gap : 0,
|
|
448
462
|
backgroundColor: capsule ? surfaceBackground : undefined,
|
|
449
463
|
borderRadius: capsule ? radius.full : undefined,
|
|
450
464
|
borderWidth: capsule ? presentationRecipe.borderWidth : 0,
|
|
451
465
|
borderColor: surfaceBorder,
|
|
452
466
|
},
|
|
453
|
-
listStyle,
|
|
454
467
|
], children: resolved.items.map((item, index) => {
|
|
455
468
|
const selected = item.id === resolved.selectedKey;
|
|
456
469
|
const focused = focusedKey === item.id;
|
|
@@ -476,12 +489,20 @@ export function BottomNavigation({ descriptor, onActivate, onLongActivate, rende
|
|
|
476
489
|
minimumTargetStyle,
|
|
477
490
|
{
|
|
478
491
|
alignItems: "center",
|
|
479
|
-
backgroundColor:
|
|
480
|
-
|
|
492
|
+
backgroundColor: tile && selected
|
|
493
|
+
? resolveColorReference(tileRecipe.selectedBackground, theme.palette)
|
|
494
|
+
: pressed ? pressedBackground : capsule && selected ? theme.colors.surfaceAccent : "transparent",
|
|
495
|
+
borderRadius: tile ? theme.tokens.radius[tileRecipe.radius] : capsule ? radius.full : theme.tokens.radius.lg,
|
|
496
|
+
// Every raised tile reserves the edge so selection never shifts icon/label rows.
|
|
497
|
+
// The translucent edge composites over the brand fill, darkening it.
|
|
498
|
+
...(raised ? {
|
|
499
|
+
borderBottomWidth: raisedRecipe.selectedDepth,
|
|
500
|
+
borderBottomColor: selected ? selectedDepthColor : "transparent",
|
|
501
|
+
} : {}),
|
|
481
502
|
// Floating edge items touch the frame. Their pressed fill must follow its
|
|
482
503
|
// inner curve; the smaller generic radius painted over the border in Spint.
|
|
483
504
|
// Match corners instead of clipping the whole surface (which clips shadows/focus).
|
|
484
|
-
...(presentation.presentation === "floating" ? {
|
|
505
|
+
...(presentation.presentation === "floating" && !tile ? {
|
|
485
506
|
...(index === 0 ? {
|
|
486
507
|
borderTopStartRadius: Math.max(0, theme.tokens.radius[presentationRecipe.radius] - presentationRecipe.borderWidth),
|
|
487
508
|
borderBottomStartRadius: Math.max(0, theme.tokens.radius[presentationRecipe.radius] - presentationRecipe.borderWidth),
|
|
@@ -582,13 +603,12 @@ export function BottomNavigation({ descriptor, onActivate, onLongActivate, rende
|
|
|
582
603
|
},
|
|
583
604
|
// Keep creation outside route destinations; absolute center placement overlaps capsule labels.
|
|
584
605
|
capsule ? { position: "relative", left: undefined, right: undefined, top: undefined, bottom: undefined, flexShrink: 0 } : undefined,
|
|
585
|
-
primaryActionStyle,
|
|
586
606
|
], children: primaryAction })) : null] }) }));
|
|
587
607
|
}
|
|
588
608
|
/** Recipe-owned icon-over-micro-label action for Native screen chrome. */
|
|
589
609
|
export function TopBarAction(props) {
|
|
590
|
-
const { label, accessibilityLabel, accessibilityState, children, disabled = false, labelVisibility = "visible",
|
|
591
|
-
|
|
610
|
+
const { label, accessibilityLabel, accessibilityState, children, disabled = false, labelVisibility = "visible", layoutStyle, ...intentAndHostProps } = props;
|
|
611
|
+
assertNoRemovedStyleProps("TopBarAction", props, ["style", "labelStyle"]);
|
|
592
612
|
if (!label.trim())
|
|
593
613
|
throw new TypeError("TopBarAction label must not be empty");
|
|
594
614
|
const resolvedAccessibilityLabel = accessibilityLabel ?? label;
|
|
@@ -630,7 +650,6 @@ export function TopBarAction(props) {
|
|
|
630
650
|
color: resolveColorReference(topBarRecipe.actionLabel.color, theme.palette),
|
|
631
651
|
fontWeight: topBarRecipe.actionLabel.fontWeight,
|
|
632
652
|
},
|
|
633
|
-
labelStyle,
|
|
634
653
|
], variant: topBarRecipe.actionLabel.textVariant, children: label })) : null] })),
|
|
635
654
|
disabled,
|
|
636
655
|
onPress: handlePress,
|
|
@@ -650,7 +669,6 @@ export function TopBarAction(props) {
|
|
|
650
669
|
: 1,
|
|
651
670
|
paddingHorizontal: topBarRecipe.action.paddingHorizontal,
|
|
652
671
|
},
|
|
653
|
-
style,
|
|
654
672
|
layoutStyle,
|
|
655
673
|
],
|
|
656
674
|
};
|
|
@@ -664,8 +682,8 @@ export function TopBarAction(props) {
|
|
|
664
682
|
return props.renderAction?.(controlProps) ?? _jsx(Pressable, { ...controlProps });
|
|
665
683
|
}
|
|
666
684
|
/** Native screen top bar with logical action slots and large-text reflow. */
|
|
667
|
-
export function TopBar({ title, titleLeading, onTitlePress, titleAccessibilityLabel, titleAccessibilityHint, leading, trailing, actions, centered = topBarRecipe.defaults.centered, safeAreaTop = 0, layoutStyle,
|
|
668
|
-
|
|
685
|
+
export function TopBar({ title, titleLeading, onTitlePress, titleAccessibilityLabel, titleAccessibilityHint, leading, trailing, actions, centered = topBarRecipe.defaults.centered, safeAreaTop = 0, layoutStyle, ...removedProps }) {
|
|
686
|
+
assertNoRemovedStyleProps("TopBar", removedProps, ["style", "leadingStyle", "titleStyle", "trailingStyle"]);
|
|
669
687
|
if (!Number.isFinite(safeAreaTop) || safeAreaTop < 0) {
|
|
670
688
|
throw new RangeError("TopBar safeAreaTop must be non-negative");
|
|
671
689
|
}
|
|
@@ -703,7 +721,6 @@ export function TopBar({ title, titleLeading, onTitlePress, titleAccessibilityLa
|
|
|
703
721
|
color: resolveColorReference(topBarRecipe.title.color, theme.palette),
|
|
704
722
|
fontWeight: topBarRecipe.title.fontWeight,
|
|
705
723
|
},
|
|
706
|
-
titleStyle,
|
|
707
724
|
];
|
|
708
725
|
const renderTitle = (containerStyle, numberOfLines) => {
|
|
709
726
|
// Match Web: a clickable title is a UI button; a static heading keeps display.
|
|
@@ -744,7 +761,6 @@ export function TopBar({ title, titleLeading, onTitlePress, titleAccessibilityLa
|
|
|
744
761
|
paddingHorizontal: topBarRecipe.paddingHorizontal,
|
|
745
762
|
paddingTop: safeAreaTop,
|
|
746
763
|
},
|
|
747
|
-
style,
|
|
748
764
|
layoutStyle,
|
|
749
765
|
], children: largeText ? (
|
|
750
766
|
// These branches have different host hierarchies. Reusing the full-width
|
|
@@ -763,7 +779,6 @@ export function TopBar({ title, titleLeading, onTitlePress, titleAccessibilityLa
|
|
|
763
779
|
flexDirection: "row",
|
|
764
780
|
minWidth: topBarRecipe.sideMinWidth,
|
|
765
781
|
},
|
|
766
|
-
leadingStyle,
|
|
767
782
|
], children: leading })) : null, hasTitle ? (renderTitle({ flex: 1, flexShrink: 1, minWidth: 0 })) : null, hasTitle && centered && hasLeading ? (_jsx(View, { accessibilityElementsHidden: true, accessible: false, importantForAccessibility: "no-hide-descendants", pointerEvents: "none", style: { minWidth: topBarRecipe.sideMinWidth } })) : null] })) : null, hasTrailingContent ? (_jsx(View, { style: [
|
|
768
783
|
{
|
|
769
784
|
direction: theme.environment.direction,
|
|
@@ -774,7 +789,6 @@ export function TopBar({ title, titleLeading, onTitlePress, titleAccessibilityLa
|
|
|
774
789
|
minWidth: topBarRecipe.sideMinWidth,
|
|
775
790
|
width: "100%",
|
|
776
791
|
},
|
|
777
|
-
trailingStyle,
|
|
778
792
|
], children: trailingContent })) : null] }, "large-text")) : (_jsxs(Fragment, { children: [renderCompactLeadingSlot ? (_jsx(View, { style: [
|
|
779
793
|
{
|
|
780
794
|
alignItems: "center",
|
|
@@ -784,7 +798,6 @@ export function TopBar({ title, titleLeading, onTitlePress, titleAccessibilityLa
|
|
|
784
798
|
justifyContent: "flex-start",
|
|
785
799
|
minWidth: topBarRecipe.sideMinWidth,
|
|
786
800
|
},
|
|
787
|
-
leadingStyle,
|
|
788
801
|
], children: hasLeading ? leading : null })) : null, hasTitle ? (
|
|
789
802
|
// Keep long route titles readable; the screen-chrome contract promises wrapping, while a one-line cap silently hid the rest.
|
|
790
803
|
renderTitle({ flex: 2, flexShrink: 1, minWidth: 0 })) : null, renderCompactTrailingSlot ? (_jsx(View, { style: [
|
|
@@ -799,15 +812,20 @@ export function TopBar({ title, titleLeading, onTitlePress, titleAccessibilityLa
|
|
|
799
812
|
marginStart: hasTitle ? undefined : "auto",
|
|
800
813
|
minWidth: topBarRecipe.sideMinWidth,
|
|
801
814
|
},
|
|
802
|
-
trailingStyle,
|
|
803
815
|
], children: hasTrailingContent ? trailingContent : null })) : null] }, "compact")) }));
|
|
804
816
|
}
|
|
805
817
|
function useMenuAfterDismiss(visible) {
|
|
818
|
+
const visibleRef = useRef(visible);
|
|
819
|
+
visibleRef.current = visible;
|
|
820
|
+
const mountedRef = useRef(true);
|
|
821
|
+
const generationRef = useRef(0);
|
|
806
822
|
const shownRef = useRef(false);
|
|
807
823
|
const previousVisibleRef = useRef(visible);
|
|
808
824
|
const pendingRef = useRef(null);
|
|
809
825
|
const taskRef = useRef(null);
|
|
810
826
|
const complete = useCallback(() => {
|
|
827
|
+
if (!mountedRef.current || visibleRef.current)
|
|
828
|
+
return;
|
|
811
829
|
taskRef.current?.cancel();
|
|
812
830
|
taskRef.current = null;
|
|
813
831
|
shownRef.current = false;
|
|
@@ -819,12 +837,32 @@ function useMenuAfterDismiss(visible) {
|
|
|
819
837
|
useLayoutEffect(() => {
|
|
820
838
|
const wasVisible = previousVisibleRef.current;
|
|
821
839
|
previousVisibleRef.current = visible;
|
|
840
|
+
if (!wasVisible && visible) {
|
|
841
|
+
// Reopening invalidates the previous selection; its late teardown must not run under the new menu.
|
|
842
|
+
generationRef.current += 1;
|
|
843
|
+
taskRef.current?.cancel();
|
|
844
|
+
taskRef.current = null;
|
|
845
|
+
pendingRef.current = null;
|
|
846
|
+
shownRef.current = false;
|
|
847
|
+
}
|
|
822
848
|
if (!wasVisible || visible || shouldAwaitNativeModalDismiss(shownRef.current))
|
|
823
849
|
return;
|
|
824
850
|
taskRef.current?.cancel();
|
|
825
|
-
|
|
851
|
+
const generation = generationRef.current;
|
|
852
|
+
taskRef.current = scheduleAfterNativeModalTeardown(() => {
|
|
853
|
+
if (generation === generationRef.current)
|
|
854
|
+
complete();
|
|
855
|
+
});
|
|
826
856
|
}, [complete, visible]);
|
|
827
|
-
useEffect(() =>
|
|
857
|
+
useEffect(() => {
|
|
858
|
+
mountedRef.current = true;
|
|
859
|
+
return () => {
|
|
860
|
+
mountedRef.current = false;
|
|
861
|
+
generationRef.current += 1;
|
|
862
|
+
pendingRef.current = null;
|
|
863
|
+
taskRef.current?.cancel();
|
|
864
|
+
};
|
|
865
|
+
}, []);
|
|
828
866
|
return {
|
|
829
867
|
queue(callback) {
|
|
830
868
|
pendingRef.current = callback;
|
|
@@ -836,8 +874,8 @@ function useMenuAfterDismiss(visible) {
|
|
|
836
874
|
};
|
|
837
875
|
}
|
|
838
876
|
/** Sectioned Native action/selection menu with teardown-safe action callbacks. */
|
|
839
|
-
export function Menu({ triggerLabel, title = triggerLabel, items, sections, source: sourceProp, selection = { mode: "none" }, onAction, onActionAfterDismiss, onSelectionAfterDismiss, open, defaultOpen = false, onOpenChange, onDismiss, disabled = false, readOnly = false, busy = false, readOnlyLabel, asyncState = { status: "idle" }, onRetry, retryLabel, density = menuRecipe.defaults.density, renderLeading, renderTrailing, dismissLabel, trigger, renderTrigger, layoutStyle,
|
|
840
|
-
|
|
877
|
+
export function Menu({ triggerLabel, title = triggerLabel, items, sections, source: sourceProp, selection = { mode: "none" }, onAction, onActionAfterDismiss, onSelectionAfterDismiss, open, defaultOpen = false, onOpenChange, onDismiss, disabled = false, readOnly = false, busy = false, readOnlyLabel, asyncState = { status: "idle" }, onRetry, retryLabel, density = menuRecipe.defaults.density, renderLeading, renderTrailing, dismissLabel, trigger, renderTrigger, layoutStyle, ...modalProps }) {
|
|
878
|
+
assertNoRemovedStyleProps("Menu", modalProps, ["style"]);
|
|
841
879
|
const providedSources = [sourceProp, items, sections].filter((candidate) => candidate !== undefined).length;
|
|
842
880
|
if (providedSources !== 1) {
|
|
843
881
|
throw new TypeError("Menu requires exactly one of source, items, or sections");
|
|
@@ -1000,7 +1038,7 @@ export function Menu({ triggerLabel, title = triggerLabel, items, sections, sour
|
|
|
1000
1038
|
requestOpen(!visible, "trigger");
|
|
1001
1039
|
},
|
|
1002
1040
|
};
|
|
1003
|
-
return (_jsxs(View, { style: [
|
|
1041
|
+
return (_jsxs(View, { style: [layoutStyle], children: [_jsx(View, { ref: triggerRef, children: renderTrigger ? renderTrigger(triggerProps) : (_jsx(Pressable, { accessibilityHint: readOnly ? readOnlyLabel : undefined, accessibilityLabel: triggerLabel, accessibilityRole: "button", accessibilityState: triggerProps.accessibilityState, disabled: disabled || readOnly || busy, onPress: triggerProps.onPress, style: ({ pressed }) => [
|
|
1004
1042
|
minimumTargetStyle,
|
|
1005
1043
|
{
|
|
1006
1044
|
alignItems: "center",
|
|
@@ -1053,6 +1091,8 @@ export const LoadMore = forwardRef(function LoadMore({ descriptor, onLoadMore, m
|
|
|
1053
1091
|
stateRef.current = descriptor.state;
|
|
1054
1092
|
handlerRef.current = onLoadMore;
|
|
1055
1093
|
const [controllerFacade] = useState(createNativeLoadMoreControllerFacade);
|
|
1094
|
+
const requestGeneration = useRef(0);
|
|
1095
|
+
const requestActive = useRef(false);
|
|
1056
1096
|
const lastViewportRequest = useRef(null);
|
|
1057
1097
|
useLayoutEffect(() => {
|
|
1058
1098
|
const controller = createLoadMoreController({
|
|
@@ -1060,13 +1100,21 @@ export const LoadMore = forwardRef(function LoadMore({ descriptor, onLoadMore, m
|
|
|
1060
1100
|
onLoadMore: (request) => handlerRef.current(request),
|
|
1061
1101
|
});
|
|
1062
1102
|
const detach = controllerFacade.attach(controller);
|
|
1103
|
+
requestGeneration.current += 1;
|
|
1104
|
+
requestActive.current = true;
|
|
1063
1105
|
lastViewportRequest.current = null;
|
|
1064
1106
|
return () => {
|
|
1107
|
+
requestGeneration.current += 1;
|
|
1108
|
+
requestActive.current = false;
|
|
1065
1109
|
detach();
|
|
1066
1110
|
controller.dispose();
|
|
1067
1111
|
};
|
|
1068
1112
|
}, [controllerFacade, mode]);
|
|
1069
1113
|
const request = useCallback(async (reason) => {
|
|
1114
|
+
// FlatList may have queued onEndReached before the footer was removed.
|
|
1115
|
+
if (!requestActive.current)
|
|
1116
|
+
return "blocked-by-state";
|
|
1117
|
+
const generation = requestGeneration.current;
|
|
1070
1118
|
const state = stateRef.current;
|
|
1071
1119
|
if (reason === "viewport" && mode === "automatic" && state.status === "ready") {
|
|
1072
1120
|
const previous = lastViewportRequest.current;
|
|
@@ -1076,18 +1124,22 @@ export const LoadMore = forwardRef(function LoadMore({ descriptor, onLoadMore, m
|
|
|
1076
1124
|
onRequestOutcome?.(outcome, reason);
|
|
1077
1125
|
return outcome;
|
|
1078
1126
|
}
|
|
1079
|
-
lastViewportRequest.current = {
|
|
1080
|
-
mode,
|
|
1081
|
-
requestKey: state.requestKey,
|
|
1082
|
-
};
|
|
1083
1127
|
}
|
|
1084
1128
|
try {
|
|
1085
1129
|
const outcome = await controllerFacade.request(state, reason);
|
|
1130
|
+
if (generation !== requestGeneration.current)
|
|
1131
|
+
return outcome;
|
|
1132
|
+
// Only a successfully completed viewport request retires its cursor.
|
|
1133
|
+
// A blocked next page or a rejection must remain eligible for retry.
|
|
1134
|
+
if (outcome === "started" && reason === "viewport" && state.status === "ready") {
|
|
1135
|
+
lastViewportRequest.current = { mode, requestKey: state.requestKey };
|
|
1136
|
+
}
|
|
1086
1137
|
onRequestOutcome?.(outcome, reason);
|
|
1087
1138
|
return outcome;
|
|
1088
1139
|
}
|
|
1089
1140
|
catch (error) {
|
|
1090
|
-
|
|
1141
|
+
if (generation === requestGeneration.current)
|
|
1142
|
+
onRequestError?.(error, reason);
|
|
1091
1143
|
// The request was accepted and started; product error state arrives
|
|
1092
1144
|
// through the controlled descriptor and optional callback. Keep the
|
|
1093
1145
|
// FlatList imperative bridge rejection-free like the Web observer.
|