@multiplatform.one/components 7.7.3 → 7.7.6
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/dist/cjs/Accordion/index.cjs +4 -0
- package/dist/cjs/Accordion/index.native.js +4 -0
- package/dist/cjs/Accordion/index.native.js.map +1 -1
- package/dist/cjs/AnimateHeight/index.cjs +29 -2
- package/dist/cjs/AnimateHeight/index.native.js +30 -2
- package/dist/cjs/AnimateHeight/index.native.js.map +1 -1
- package/dist/cjs/Collapsible.cjs +48 -0
- package/dist/cjs/Collapsible.native.js +56 -0
- package/dist/cjs/Collapsible.native.js.map +1 -0
- package/dist/cjs/Pagination.cjs +28 -9
- package/dist/cjs/Pagination.native.js +28 -9
- package/dist/cjs/Pagination.native.js.map +1 -1
- package/dist/cjs/SheetModal/index.native.js +49 -25
- package/dist/cjs/SheetModal/index.native.js.map +1 -1
- package/dist/cjs/Tabs/index.cjs +144 -91
- package/dist/cjs/Tabs/index.native.js +186 -123
- package/dist/cjs/Tabs/index.native.js.map +1 -1
- package/dist/cjs/knobImmuneExports.cjs +0 -1
- package/dist/cjs/knobImmuneExports.native.js +0 -1
- package/dist/cjs/knobImmuneExports.native.js.map +1 -1
- package/dist/cjs/tamagui.cjs +3 -2
- package/dist/cjs/tamagui.native.js +2 -1
- package/dist/cjs/tamagui.native.js.map +1 -1
- package/dist/esm/Accordion/index.mjs +4 -0
- package/dist/esm/Accordion/index.mjs.map +1 -1
- package/dist/esm/Accordion/index.native.js +4 -0
- package/dist/esm/Accordion/index.native.js.map +1 -1
- package/dist/esm/AnimateHeight/index.mjs +29 -2
- package/dist/esm/AnimateHeight/index.mjs.map +1 -1
- package/dist/esm/AnimateHeight/index.native.js +30 -2
- package/dist/esm/AnimateHeight/index.native.js.map +1 -1
- package/dist/esm/Collapsible.mjs +23 -0
- package/dist/esm/Collapsible.mjs.map +1 -0
- package/dist/esm/Collapsible.native.js +28 -0
- package/dist/esm/Collapsible.native.js.map +1 -0
- package/dist/esm/Pagination.mjs +28 -9
- package/dist/esm/Pagination.mjs.map +1 -1
- package/dist/esm/Pagination.native.js +28 -9
- package/dist/esm/Pagination.native.js.map +1 -1
- package/dist/esm/SheetModal/index.native.js +49 -25
- package/dist/esm/SheetModal/index.native.js.map +1 -1
- package/dist/esm/Tabs/index.mjs +144 -91
- package/dist/esm/Tabs/index.mjs.map +1 -1
- package/dist/esm/Tabs/index.native.js +186 -123
- package/dist/esm/Tabs/index.native.js.map +1 -1
- package/dist/esm/knobImmuneExports.mjs +0 -1
- package/dist/esm/knobImmuneExports.mjs.map +1 -1
- package/dist/esm/knobImmuneExports.native.js +0 -1
- package/dist/esm/knobImmuneExports.native.js.map +1 -1
- package/dist/esm/tamagui.mjs +2 -1
- package/dist/esm/tamagui.mjs.map +1 -1
- package/dist/esm/tamagui.native.js +2 -1
- package/dist/esm/tamagui.native.js.map +1 -1
- package/dist/jsx/Accordion/index.mjs +4 -0
- package/dist/jsx/Accordion/index.mjs.map +1 -1
- package/dist/jsx/Accordion/index.native.js +4 -0
- package/dist/jsx/Accordion/index.native.js.map +1 -1
- package/dist/jsx/AnimateHeight/index.mjs +29 -2
- package/dist/jsx/AnimateHeight/index.mjs.map +1 -1
- package/dist/jsx/AnimateHeight/index.native.js +30 -2
- package/dist/jsx/AnimateHeight/index.native.js.map +1 -1
- package/dist/jsx/Collapsible.mjs +23 -0
- package/dist/jsx/Collapsible.mjs.map +1 -0
- package/dist/jsx/Collapsible.native.js +56 -0
- package/dist/jsx/Collapsible.native.js.map +1 -0
- package/dist/jsx/Pagination.mjs +28 -9
- package/dist/jsx/Pagination.mjs.map +1 -1
- package/dist/jsx/Pagination.native.js +28 -9
- package/dist/jsx/Pagination.native.js.map +1 -1
- package/dist/jsx/SheetModal/index.native.js +49 -25
- package/dist/jsx/SheetModal/index.native.js.map +1 -1
- package/dist/jsx/Tabs/index.mjs +144 -91
- package/dist/jsx/Tabs/index.mjs.map +1 -1
- package/dist/jsx/Tabs/index.native.js +186 -123
- package/dist/jsx/Tabs/index.native.js.map +1 -1
- package/dist/jsx/knobImmuneExports.mjs +0 -1
- package/dist/jsx/knobImmuneExports.mjs.map +1 -1
- package/dist/jsx/knobImmuneExports.native.js +0 -1
- package/dist/jsx/knobImmuneExports.native.js.map +1 -1
- package/dist/jsx/tamagui.mjs +2 -1
- package/dist/jsx/tamagui.mjs.map +1 -1
- package/dist/jsx/tamagui.native.js +2 -1
- package/dist/jsx/tamagui.native.js.map +1 -1
- package/package.json +9 -9
- package/src/Accordion/index.tsx +7 -1
- package/src/AnimateHeight/AnimateHeight.native.spec.tsx +51 -0
- package/src/AnimateHeight/index.tsx +30 -5
- package/src/Collapsible.tsx +23 -0
- package/src/Pagination.simple.spec.tsx +23 -0
- package/src/Pagination.stories.tsx +8 -0
- package/src/Pagination.tsx +27 -10
- package/src/SheetModal/SheetModal.native.spec.tsx +138 -0
- package/src/SheetModal/index.native.tsx +59 -26
- package/src/Tabs/Tabs.bandColor.spec.tsx +23 -0
- package/src/Tabs/Tabs.native.spec.tsx +35 -0
- package/src/Tabs/Tabs.spec.tsx +51 -1
- package/src/Tabs/Tabs.stories.tsx +27 -1
- package/src/Tabs/index.tsx +266 -187
- package/src/disclosureForm.spec.tsx +57 -0
- package/src/disclosureNativeProps.spec.tsx +35 -0
- package/src/knobImmuneExports.ts +0 -2
- package/src/primitives.spec.tsx +19 -4
- package/src/tamagui.ts +3 -1
- package/types/Accordion/index.d.ts +2 -0
- package/types/Accordion/index.d.ts.map +1 -1
- package/types/AnimateHeight/index.d.ts +3 -4
- package/types/AnimateHeight/index.d.ts.map +1 -1
- package/types/Collapsible.d.ts +19 -0
- package/types/Collapsible.d.ts.map +1 -0
- package/types/Pagination.d.ts +1 -1
- package/types/Pagination.d.ts.map +1 -1
- package/types/SheetModal/index.native.d.ts.map +1 -1
- package/types/Tabs/index.d.ts +7 -3
- package/types/Tabs/index.d.ts.map +1 -1
- package/types/knobImmuneExports.d.ts.map +1 -1
- package/types/tamagui.d.ts +2 -1
- package/types/tamagui.d.ts.map +1 -1
package/src/Tabs/index.tsx
CHANGED
|
@@ -59,7 +59,7 @@ import { componentColors } from "../componentColors";
|
|
|
59
59
|
|
|
60
60
|
// ── Types ─────────────────────────────────────────────────────
|
|
61
61
|
|
|
62
|
-
export type TabsVariant = "underline" | "contained";
|
|
62
|
+
export type TabsVariant = "underline" | "contained" | "band";
|
|
63
63
|
export type TabsSize = "$2" | "$3" | "$4" | "$5";
|
|
64
64
|
export type TabsDirection = "ltr" | "rtl";
|
|
65
65
|
|
|
@@ -68,6 +68,8 @@ export interface TabsItem {
|
|
|
68
68
|
value: string;
|
|
69
69
|
/** Tab label */
|
|
70
70
|
label: ReactNode;
|
|
71
|
+
/** Spoken name when the label contains more than plain text. */
|
|
72
|
+
accessibilityLabel?: string;
|
|
71
73
|
/** Optional leading icon */
|
|
72
74
|
icon?: ReactNode;
|
|
73
75
|
/** Optional trailing adornment (e.g. a Badge/count pill) */
|
|
@@ -89,6 +91,8 @@ export interface TabsProps {
|
|
|
89
91
|
onChange?: (value: string) => void;
|
|
90
92
|
/** underline (desk-style, default) or contained (segmented track) */
|
|
91
93
|
variant?: TabsVariant;
|
|
94
|
+
/** Equal tabs share a bounded row. Band always uses equal distribution. */
|
|
95
|
+
distribution?: "content" | "equal";
|
|
92
96
|
/** Accent intent for the active indicator (default) */
|
|
93
97
|
accent?: boolean;
|
|
94
98
|
/** Error intent */
|
|
@@ -218,7 +222,8 @@ const fillProps = { flex: 1, minHeight: 0 } as const;
|
|
|
218
222
|
|
|
219
223
|
const StripWrapper = styled(View, {
|
|
220
224
|
name: "MpTabsStrip",
|
|
221
|
-
|
|
225
|
+
alignSelf: "stretch",
|
|
226
|
+
minWidth: 0,
|
|
222
227
|
position: "relative",
|
|
223
228
|
});
|
|
224
229
|
|
|
@@ -297,6 +302,7 @@ function TabsRoot({
|
|
|
297
302
|
defaultValue,
|
|
298
303
|
onChange,
|
|
299
304
|
variant = "underline",
|
|
305
|
+
distribution = "content",
|
|
300
306
|
accent,
|
|
301
307
|
error,
|
|
302
308
|
warning,
|
|
@@ -395,6 +401,27 @@ function TabsRoot({
|
|
|
395
401
|
}, [activeLayout, direction]);
|
|
396
402
|
|
|
397
403
|
const isUnderline = variant === "underline";
|
|
404
|
+
const isBand = variant === "band";
|
|
405
|
+
const equalDistribution = isBand || distribution === "equal";
|
|
406
|
+
const tabRefs = React.useRef(new Map<string, HTMLElement>());
|
|
407
|
+
const onTabKeyDown = (event: React.KeyboardEvent, value: string) => {
|
|
408
|
+
if (!["Home", "End", "ArrowLeft", "ArrowRight"].includes(event.key)) return;
|
|
409
|
+
const enabled = items.filter((item) => !disabled && !item.disabled);
|
|
410
|
+
if (!enabled.length) return;
|
|
411
|
+
const current = enabled.findIndex((item) => item.value === value);
|
|
412
|
+
const step = (event.key === "ArrowRight" ? 1 : -1) * (direction === "rtl" ? -1 : 1);
|
|
413
|
+
const next =
|
|
414
|
+
event.key === "Home"
|
|
415
|
+
? 0
|
|
416
|
+
: event.key === "End"
|
|
417
|
+
? enabled.length - 1
|
|
418
|
+
: loop
|
|
419
|
+
? (current + step + enabled.length) % enabled.length
|
|
420
|
+
: Math.max(0, Math.min(enabled.length - 1, current + step));
|
|
421
|
+
event.preventDefault();
|
|
422
|
+
event.stopPropagation();
|
|
423
|
+
tabRefs.current.get(enabled[next]!.value)?.focus();
|
|
424
|
+
};
|
|
398
425
|
const indicatorTheme = (intent ?? "accent") as ThemeName;
|
|
399
426
|
// Size recipe owns chrome (LC-75). Explicit `size` already flowed into
|
|
400
427
|
// knobProps via useResolvedKnobs. Compact tightens gaps only (LC-76).
|
|
@@ -426,36 +453,209 @@ function TabsRoot({
|
|
|
426
453
|
})()
|
|
427
454
|
: undefined;
|
|
428
455
|
|
|
429
|
-
const indicator =
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
<
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
<
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
456
|
+
const indicator =
|
|
457
|
+
activeLayout && !isBand ? (
|
|
458
|
+
isUnderline ? (
|
|
459
|
+
<Theme name={indicatorTheme}>
|
|
460
|
+
<UnderlineIndicator
|
|
461
|
+
x={activeLayout.x}
|
|
462
|
+
width={activeLayout.width}
|
|
463
|
+
data-tabs-indicator="underline"
|
|
464
|
+
{...(transition ? { transition } : undefined)}
|
|
465
|
+
/>
|
|
466
|
+
</Theme>
|
|
467
|
+
) : (
|
|
468
|
+
<Theme name={indicatorTheme}>
|
|
469
|
+
<ThumbIndicator
|
|
470
|
+
x={activeLayout.x}
|
|
471
|
+
y={activeLayout.y}
|
|
472
|
+
width={activeLayout.width}
|
|
473
|
+
height={activeLayout.height}
|
|
474
|
+
backgroundColor={selectedTint ?? "$color4"}
|
|
475
|
+
data-tabs-indicator="thumb"
|
|
476
|
+
{...thumbRadius}
|
|
477
|
+
{...(knobProps.elevation !== undefined
|
|
478
|
+
? { elevation: knobProps.elevation }
|
|
479
|
+
: undefined)}
|
|
480
|
+
{...(transition ? { transition } : undefined)}
|
|
481
|
+
/>
|
|
482
|
+
</Theme>
|
|
483
|
+
)
|
|
484
|
+
) : null;
|
|
455
485
|
|
|
456
486
|
if (items.length === 0) return null;
|
|
457
487
|
|
|
458
488
|
const tabListLabel = ariaLabel ?? t("Tabs");
|
|
489
|
+
const tabList = (
|
|
490
|
+
<TabsList
|
|
491
|
+
loop={loop}
|
|
492
|
+
aria-label={tabListLabel}
|
|
493
|
+
data-tabs-list={variant}
|
|
494
|
+
data-size-token={visualSizeToken}
|
|
495
|
+
data-density={knobProps.density}
|
|
496
|
+
data-visual-px={sizePx}
|
|
497
|
+
{...(!isWeb ? { role: "tablist" as const } : undefined)}
|
|
498
|
+
alignSelf={equalDistribution ? "stretch" : "flex-start"}
|
|
499
|
+
minWidth={0}
|
|
500
|
+
contained={!isUnderline && !isBand}
|
|
501
|
+
{...(isUnderline || isBand
|
|
502
|
+
? { gap: 0 }
|
|
503
|
+
: {
|
|
504
|
+
backgroundColor: componentColors.interactive.background,
|
|
505
|
+
gap: 0,
|
|
506
|
+
// Full fragment spread (AGENTS.md knob rule) so the
|
|
507
|
+
// cornerSmoothing class rides along with the radius.
|
|
508
|
+
...knobProps.borderRadiusNested,
|
|
509
|
+
})}
|
|
510
|
+
>
|
|
511
|
+
{indicator}
|
|
512
|
+
{items.map((item, itemIndex) => {
|
|
513
|
+
const isActive = item.value === currentValue;
|
|
514
|
+
const isDisabled = disabled || item.disabled;
|
|
515
|
+
const position = getGroupPosition(itemIndex, enabledCount);
|
|
516
|
+
const cornerProps = isUnderline
|
|
517
|
+
? underlineStackRadius(position, radiusToken)
|
|
518
|
+
: stackRadiusProps(position, outerRadiusToken, "horizontal");
|
|
519
|
+
// Roving tabindex (single tab stop): tamagui's vendored
|
|
520
|
+
// roving-focus item hardcodes tabIndex=0 on every tab, making
|
|
521
|
+
// each tab its own Tab stop. Exactly one tab stays in the page
|
|
522
|
+
// tab order — the focused one while focus is in the tablist,
|
|
523
|
+
// else the active one (or the first, when value matches
|
|
524
|
+
// nothing); ArrowLeft/Right move within the tablist.
|
|
525
|
+
const anchorValue =
|
|
526
|
+
(focusValue !== null &&
|
|
527
|
+
items.some((i) => i.value === focusValue && !(disabled || i.disabled))
|
|
528
|
+
? focusValue
|
|
529
|
+
: undefined) ??
|
|
530
|
+
(items.some((i) => i.value === currentValue && !(disabled || i.disabled))
|
|
531
|
+
? currentValue
|
|
532
|
+
: items.find((i) => !(disabled || i.disabled))?.value);
|
|
533
|
+
const isTabStop =
|
|
534
|
+
item.value === anchorValue &&
|
|
535
|
+
itemIndex === items.findIndex((i) => i.value === anchorValue);
|
|
536
|
+
const kbRing = kbFocusValue === item.value;
|
|
537
|
+
return (
|
|
538
|
+
<Trigger
|
|
539
|
+
key={item.value}
|
|
540
|
+
value={item.value}
|
|
541
|
+
ref={(node) => {
|
|
542
|
+
if (!isWeb) return;
|
|
543
|
+
if (node) tabRefs.current.set(item.value, node as unknown as HTMLElement);
|
|
544
|
+
else tabRefs.current.delete(item.value);
|
|
545
|
+
}}
|
|
546
|
+
flex={equalDistribution ? 1 : undefined}
|
|
547
|
+
minWidth={0}
|
|
548
|
+
minHeight={isBand ? MIN_PRESS_TARGET : undefined}
|
|
549
|
+
borderEndWidth={isBand && itemIndex < items.length - 1 ? 1 : 0}
|
|
550
|
+
borderColor={isBand ? componentColors.surface.border : undefined}
|
|
551
|
+
disabled={isDisabled}
|
|
552
|
+
{...(item.content === undefined ? { "aria-controls": undefined } : undefined)}
|
|
553
|
+
data-tab-position={position}
|
|
554
|
+
data-kb-ring={kbRing ? "true" : undefined}
|
|
555
|
+
{...(isWeb
|
|
556
|
+
? {
|
|
557
|
+
tabIndex: isDisabled ? -1 : isTabStop ? 0 : -1,
|
|
558
|
+
"aria-label": item.accessibilityLabel,
|
|
559
|
+
onKeyDownCapture: (event: React.KeyboardEvent) => onTabKeyDown(event, item.value),
|
|
560
|
+
}
|
|
561
|
+
: {
|
|
562
|
+
accessible: true,
|
|
563
|
+
role: "button" as const,
|
|
564
|
+
accessibilityRole: "button" as const,
|
|
565
|
+
accessibilityLabel:
|
|
566
|
+
item.accessibilityLabel ??
|
|
567
|
+
(typeof item.label === "string" || typeof item.label === "number"
|
|
568
|
+
? String(item.label)
|
|
569
|
+
: undefined),
|
|
570
|
+
accessibilityState: { selected: isActive, disabled: Boolean(isDisabled) },
|
|
571
|
+
})}
|
|
572
|
+
hitSlop={tabHitSlop}
|
|
573
|
+
onInteraction={handleInteraction}
|
|
574
|
+
cursor={isDisabled ? "not-allowed" : "pointer"}
|
|
575
|
+
backgroundColor={
|
|
576
|
+
isBand && isActive ? componentColors.surface.background : "transparent"
|
|
577
|
+
}
|
|
578
|
+
borderRadius={0}
|
|
579
|
+
{...(isDisabled
|
|
580
|
+
? isActive
|
|
581
|
+
? disabledState.chromeKnobProps
|
|
582
|
+
: { ...disabledState.chromeKnobProps, ...disabledState.textKnobProps }
|
|
583
|
+
: undefined)}
|
|
584
|
+
{...(isWeb
|
|
585
|
+
? {
|
|
586
|
+
onFocusCapture: () => {
|
|
587
|
+
setFocusValue(item.value);
|
|
588
|
+
if (wasKeyboardFocus()) setKbFocusValue(item.value);
|
|
589
|
+
},
|
|
590
|
+
onBlurCapture: () => {
|
|
591
|
+
setFocusValue((prev) => (prev === item.value ? null : prev));
|
|
592
|
+
setKbFocusValue((prev) => (prev === item.value ? null : prev));
|
|
593
|
+
},
|
|
594
|
+
onPointerEnter: () => setHoverValue(item.value),
|
|
595
|
+
onPointerLeave: () =>
|
|
596
|
+
setHoverValue((prev) => (prev === item.value ? null : prev)),
|
|
597
|
+
onPointerDown: () => setPressValue(item.value),
|
|
598
|
+
onPointerUp: () => setPressValue((prev) => (prev === item.value ? null : prev)),
|
|
599
|
+
}
|
|
600
|
+
: undefined)}
|
|
601
|
+
>
|
|
602
|
+
<View
|
|
603
|
+
{...(isBand ? undefined : cornerProps)}
|
|
604
|
+
// ONE chrome owner (LC-75/LC-79): the full control recipe
|
|
605
|
+
// (height/paddingHorizontal/gap) lives on this inner row —
|
|
606
|
+
// it also paints hover/press fill, so the pill covers the
|
|
607
|
+
// padded box. The Trigger stays an unpainted hit target
|
|
608
|
+
// (spreading the recipe there too doubled the horizontal
|
|
609
|
+
// padding).
|
|
610
|
+
{...knobProps.control}
|
|
611
|
+
// Equal columns own band spacing; extra inset can exceed a narrow column.
|
|
612
|
+
paddingHorizontal={isBand ? 0 : knobProps.control.paddingHorizontal}
|
|
613
|
+
alignSelf="stretch"
|
|
614
|
+
flexGrow={equalDistribution ? 1 : undefined}
|
|
615
|
+
flexShrink={equalDistribution ? 1 : undefined}
|
|
616
|
+
flexBasis={equalDistribution ? 0 : undefined}
|
|
617
|
+
minWidth={0}
|
|
618
|
+
flexDirection="row"
|
|
619
|
+
alignItems="center"
|
|
620
|
+
justifyContent="center"
|
|
621
|
+
backgroundColor={
|
|
622
|
+
isActive || isDisabled
|
|
623
|
+
? "transparent"
|
|
624
|
+
: pressValue === item.value
|
|
625
|
+
? componentColors.interactive.hover
|
|
626
|
+
: hoverValue === item.value
|
|
627
|
+
? componentColors.interactive.background
|
|
628
|
+
: "transparent"
|
|
629
|
+
}
|
|
630
|
+
{...(kbRing ? keyboardFocusRingProps : undefined)}
|
|
631
|
+
>
|
|
632
|
+
{item.icon}
|
|
633
|
+
<TabLabel
|
|
634
|
+
minWidth={equalDistribution ? 0 : undefined}
|
|
635
|
+
flexShrink={equalDistribution ? 1 : undefined}
|
|
636
|
+
active={isActive}
|
|
637
|
+
{...knobProps.controlType}
|
|
638
|
+
fontFamily={knobProps.label.fontFamily}
|
|
639
|
+
fontWeight={!isBand && isActive ? "700" : knobProps.label.fontWeight}
|
|
640
|
+
color={
|
|
641
|
+
isDisabled
|
|
642
|
+
? componentColors.text.muted
|
|
643
|
+
: isActive
|
|
644
|
+
? variant === "contained" && onSelectedTint
|
|
645
|
+
? onSelectedTint
|
|
646
|
+
: componentColors.text.primary
|
|
647
|
+
: componentColors.text.secondary
|
|
648
|
+
}
|
|
649
|
+
>
|
|
650
|
+
{wrapBareTextChildren(item.label, (label) => label)}
|
|
651
|
+
</TabLabel>
|
|
652
|
+
{item.badge}
|
|
653
|
+
</View>
|
|
654
|
+
</Trigger>
|
|
655
|
+
);
|
|
656
|
+
})}
|
|
657
|
+
</TabsList>
|
|
658
|
+
);
|
|
459
659
|
|
|
460
660
|
return (
|
|
461
661
|
<TamaguiTabs
|
|
@@ -463,13 +663,16 @@ function TabsRoot({
|
|
|
463
663
|
onValueChange={handleChange}
|
|
464
664
|
orientation="horizontal"
|
|
465
665
|
activationMode={activationMode}
|
|
466
|
-
width="100%"
|
|
666
|
+
width={equalDistribution ? "100%" : undefined}
|
|
667
|
+
alignSelf="stretch"
|
|
668
|
+
minWidth={0}
|
|
467
669
|
flexDirection="column"
|
|
468
670
|
// The root is the first link in the chain a filling panel needs.
|
|
469
671
|
// `flex: 1` alone is not enough — without `minHeight: 0` the column
|
|
470
672
|
// refuses to shrink below its content and the panel never gets a box.
|
|
471
673
|
{...(fill ? fillProps : undefined)}
|
|
472
674
|
data-tabs-variant={variant}
|
|
675
|
+
data-tabs-distribution={equalDistribution ? "equal" : "content"}
|
|
473
676
|
data-tabs-fill={fill ? "true" : undefined}
|
|
474
677
|
data-tabs-dir={direction}
|
|
475
678
|
data-size-token={visualSizeToken}
|
|
@@ -477,171 +680,47 @@ function TabsRoot({
|
|
|
477
680
|
data-visual-px={sizePx}
|
|
478
681
|
>
|
|
479
682
|
<StripWrapper
|
|
683
|
+
backgroundColor={isBand ? "$color2" : undefined}
|
|
684
|
+
borderBottomWidth={isBand ? 1 : undefined}
|
|
685
|
+
borderBottomColor={isBand ? componentColors.surface.border : undefined}
|
|
480
686
|
{...(isUnderline
|
|
481
687
|
? { ...hairline.bottom, borderColor: componentColors.surface.border }
|
|
482
688
|
: undefined)}
|
|
483
689
|
data-fade-left={fades.left ? "true" : undefined}
|
|
484
690
|
data-fade-right={fades.right ? "true" : undefined}
|
|
485
691
|
>
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
data-density={knobProps.density}
|
|
515
|
-
data-visual-px={sizePx}
|
|
516
|
-
{...(!isWeb ? { role: "tablist" as const } : undefined)}
|
|
517
|
-
contained={!isUnderline}
|
|
518
|
-
{...(isUnderline
|
|
519
|
-
? { gap: 0 }
|
|
520
|
-
: {
|
|
521
|
-
backgroundColor: componentColors.interactive.background,
|
|
522
|
-
gap: 0,
|
|
523
|
-
// Full fragment spread (AGENTS.md knob rule) so the
|
|
524
|
-
// cornerSmoothing class rides along with the radius.
|
|
525
|
-
...knobProps.borderRadiusNested,
|
|
526
|
-
})}
|
|
692
|
+
{equalDistribution ? (
|
|
693
|
+
<View alignSelf="stretch" minWidth={0}>
|
|
694
|
+
{tabList}
|
|
695
|
+
</View>
|
|
696
|
+
) : (
|
|
697
|
+
<ScrollView
|
|
698
|
+
ref={scrollRef}
|
|
699
|
+
data-tabs-scroll="true"
|
|
700
|
+
horizontal
|
|
701
|
+
showsHorizontalScrollIndicator={false}
|
|
702
|
+
scrollEventThrottle={16}
|
|
703
|
+
style={maskStyle as never}
|
|
704
|
+
// Slop padding keeps the 44px press floor from being clipped when
|
|
705
|
+
// the painted tab is smaller than the floor (LC-54 / nested scale).
|
|
706
|
+
contentContainerStyle={{ paddingVertical: slop }}
|
|
707
|
+
marginVertical={-slop}
|
|
708
|
+
onScroll={(e) => {
|
|
709
|
+
scrollMetrics.current.offset = e.nativeEvent.contentOffset.x;
|
|
710
|
+
updateFades();
|
|
711
|
+
}}
|
|
712
|
+
onLayout={(e) => {
|
|
713
|
+
scrollMetrics.current.viewport = e.nativeEvent.layout.width;
|
|
714
|
+
updateFades();
|
|
715
|
+
}}
|
|
716
|
+
onContentSizeChange={(w) => {
|
|
717
|
+
scrollMetrics.current.content = w;
|
|
718
|
+
updateFades();
|
|
719
|
+
}}
|
|
527
720
|
>
|
|
528
|
-
{
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
const isDisabled = disabled || item.disabled;
|
|
532
|
-
const position = getGroupPosition(itemIndex, enabledCount);
|
|
533
|
-
const cornerProps = isUnderline
|
|
534
|
-
? underlineStackRadius(position, radiusToken)
|
|
535
|
-
: stackRadiusProps(position, outerRadiusToken, "horizontal");
|
|
536
|
-
// Roving tabindex (single tab stop): tamagui's vendored
|
|
537
|
-
// roving-focus item hardcodes tabIndex=0 on every tab, making
|
|
538
|
-
// each tab its own Tab stop. Exactly one tab stays in the page
|
|
539
|
-
// tab order — the focused one while focus is in the tablist,
|
|
540
|
-
// else the active one (or the first, when value matches
|
|
541
|
-
// nothing); ArrowLeft/Right move within the tablist.
|
|
542
|
-
const anchorValue =
|
|
543
|
-
(focusValue !== null &&
|
|
544
|
-
items.some((i) => i.value === focusValue && !(disabled || i.disabled))
|
|
545
|
-
? focusValue
|
|
546
|
-
: undefined) ??
|
|
547
|
-
(items.some((i) => i.value === currentValue) ? currentValue : items[0]?.value);
|
|
548
|
-
const isTabStop =
|
|
549
|
-
item.value === anchorValue &&
|
|
550
|
-
itemIndex === items.findIndex((i) => i.value === anchorValue);
|
|
551
|
-
const kbRing = kbFocusValue === item.value;
|
|
552
|
-
return (
|
|
553
|
-
<Trigger
|
|
554
|
-
key={item.value}
|
|
555
|
-
value={item.value}
|
|
556
|
-
disabled={isDisabled}
|
|
557
|
-
data-tab-position={position}
|
|
558
|
-
data-kb-ring={kbRing ? "true" : undefined}
|
|
559
|
-
{...(isWeb
|
|
560
|
-
? { tabIndex: isDisabled ? -1 : isTabStop ? 0 : -1 }
|
|
561
|
-
: {
|
|
562
|
-
accessible: true,
|
|
563
|
-
role: "tab" as const,
|
|
564
|
-
"aria-selected": isActive,
|
|
565
|
-
...(isDisabled ? { "aria-disabled": true } : undefined),
|
|
566
|
-
})}
|
|
567
|
-
hitSlop={tabHitSlop}
|
|
568
|
-
onInteraction={handleInteraction}
|
|
569
|
-
cursor={isDisabled ? "not-allowed" : "pointer"}
|
|
570
|
-
backgroundColor="transparent"
|
|
571
|
-
borderRadius={0}
|
|
572
|
-
{...(isDisabled
|
|
573
|
-
? isActive
|
|
574
|
-
? disabledState.chromeKnobProps
|
|
575
|
-
: { ...disabledState.chromeKnobProps, ...disabledState.textKnobProps }
|
|
576
|
-
: undefined)}
|
|
577
|
-
{...(isWeb
|
|
578
|
-
? {
|
|
579
|
-
onFocusCapture: () => {
|
|
580
|
-
setFocusValue(item.value);
|
|
581
|
-
if (wasKeyboardFocus()) setKbFocusValue(item.value);
|
|
582
|
-
},
|
|
583
|
-
onBlurCapture: () => {
|
|
584
|
-
setFocusValue((prev) => (prev === item.value ? null : prev));
|
|
585
|
-
setKbFocusValue((prev) => (prev === item.value ? null : prev));
|
|
586
|
-
},
|
|
587
|
-
onPointerEnter: () => setHoverValue(item.value),
|
|
588
|
-
onPointerLeave: () =>
|
|
589
|
-
setHoverValue((prev) => (prev === item.value ? null : prev)),
|
|
590
|
-
onPointerDown: () => setPressValue(item.value),
|
|
591
|
-
onPointerUp: () =>
|
|
592
|
-
setPressValue((prev) => (prev === item.value ? null : prev)),
|
|
593
|
-
}
|
|
594
|
-
: undefined)}
|
|
595
|
-
>
|
|
596
|
-
<View
|
|
597
|
-
{...cornerProps}
|
|
598
|
-
// ONE chrome owner (LC-75/LC-79): the full control recipe
|
|
599
|
-
// (height/paddingHorizontal/gap) lives on this inner row —
|
|
600
|
-
// it also paints hover/press fill, so the pill covers the
|
|
601
|
-
// padded box. The Trigger stays an unpainted hit target
|
|
602
|
-
// (spreading the recipe there too doubled the horizontal
|
|
603
|
-
// padding).
|
|
604
|
-
{...knobProps.control}
|
|
605
|
-
width="100%"
|
|
606
|
-
flexDirection="row"
|
|
607
|
-
alignItems="center"
|
|
608
|
-
justifyContent="center"
|
|
609
|
-
backgroundColor={
|
|
610
|
-
isActive || isDisabled
|
|
611
|
-
? "transparent"
|
|
612
|
-
: pressValue === item.value
|
|
613
|
-
? componentColors.interactive.hover
|
|
614
|
-
: hoverValue === item.value
|
|
615
|
-
? componentColors.interactive.background
|
|
616
|
-
: "transparent"
|
|
617
|
-
}
|
|
618
|
-
{...(kbRing ? keyboardFocusRingProps : undefined)}
|
|
619
|
-
>
|
|
620
|
-
{item.icon}
|
|
621
|
-
<TabLabel
|
|
622
|
-
active={isActive}
|
|
623
|
-
{...knobProps.controlType}
|
|
624
|
-
fontFamily={knobProps.label.fontFamily}
|
|
625
|
-
fontWeight={isActive ? "700" : knobProps.label.fontWeight}
|
|
626
|
-
color={
|
|
627
|
-
isDisabled
|
|
628
|
-
? componentColors.text.muted
|
|
629
|
-
: isActive
|
|
630
|
-
? !isUnderline && onSelectedTint
|
|
631
|
-
? onSelectedTint
|
|
632
|
-
: componentColors.text.primary
|
|
633
|
-
: componentColors.text.secondary
|
|
634
|
-
}
|
|
635
|
-
>
|
|
636
|
-
{wrapBareTextChildren(item.label, (label) => label)}
|
|
637
|
-
</TabLabel>
|
|
638
|
-
{item.badge}
|
|
639
|
-
</View>
|
|
640
|
-
</Trigger>
|
|
641
|
-
);
|
|
642
|
-
})}
|
|
643
|
-
</TabsList>
|
|
644
|
-
</ScrollView>
|
|
721
|
+
{tabList}
|
|
722
|
+
</ScrollView>
|
|
723
|
+
)}
|
|
645
724
|
</StripWrapper>
|
|
646
725
|
|
|
647
726
|
{/*
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** @vitest-environment jsdom */
|
|
2
|
+
import { fireEvent } from "@testing-library/react";
|
|
3
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
4
|
+
import { describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { Accordion } from "./Accordion";
|
|
6
|
+
import { Collapsible } from "./tamagui";
|
|
7
|
+
|
|
8
|
+
function Disclosure({
|
|
9
|
+
kind,
|
|
10
|
+
type,
|
|
11
|
+
}: {
|
|
12
|
+
kind: "accordion" | "collapsible";
|
|
13
|
+
type?: "button" | "submit";
|
|
14
|
+
}) {
|
|
15
|
+
const props = type ? { type } : {};
|
|
16
|
+
return kind === "accordion" ? (
|
|
17
|
+
<Accordion type="single" collapsible>
|
|
18
|
+
<Accordion.Item value="preview">
|
|
19
|
+
<Accordion.Trigger {...props}>Preview</Accordion.Trigger>
|
|
20
|
+
<Accordion.Content>Contents</Accordion.Content>
|
|
21
|
+
</Accordion.Item>
|
|
22
|
+
</Accordion>
|
|
23
|
+
) : (
|
|
24
|
+
<Collapsible>
|
|
25
|
+
<Collapsible.Trigger {...props}>Preview</Collapsible.Trigger>
|
|
26
|
+
<Collapsible.Content>Contents</Collapsible.Content>
|
|
27
|
+
</Collapsible>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("disclosure triggers inside forms", () => {
|
|
32
|
+
for (const kind of ["accordion", "collapsible"] as const) {
|
|
33
|
+
it(`${kind} opens without submitting by default`, () => {
|
|
34
|
+
const submit = vi.fn((event) => event.preventDefault());
|
|
35
|
+
const result = renderWithProviders(
|
|
36
|
+
<form onSubmit={submit}>
|
|
37
|
+
<Disclosure kind={kind} />
|
|
38
|
+
</form>,
|
|
39
|
+
);
|
|
40
|
+
const trigger = result.getByRole("button", { name: "Preview" });
|
|
41
|
+
fireEvent.click(trigger);
|
|
42
|
+
expect(trigger.getAttribute("aria-expanded")).toBe("true");
|
|
43
|
+
expect(submit).not.toHaveBeenCalled();
|
|
44
|
+
expect(trigger.getAttribute("type")).toBe("button");
|
|
45
|
+
});
|
|
46
|
+
it(`${kind} preserves an explicit submit request`, () => {
|
|
47
|
+
const submit = vi.fn((event) => event.preventDefault());
|
|
48
|
+
const result = renderWithProviders(
|
|
49
|
+
<form onSubmit={submit}>
|
|
50
|
+
<Disclosure kind={kind} type="submit" />
|
|
51
|
+
</form>,
|
|
52
|
+
);
|
|
53
|
+
fireEvent.click(result.getByRole("button", { name: "Preview" }));
|
|
54
|
+
expect(submit).toHaveBeenCalledTimes(1);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createRef } from "react";
|
|
2
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
3
|
+
import { expect, it, vi } from "vitest";
|
|
4
|
+
import { Accordion } from "./Accordion";
|
|
5
|
+
import { Collapsible } from "./Collapsible";
|
|
6
|
+
|
|
7
|
+
vi.mock("@multiplatform.one/platform", async (importOriginal) => ({
|
|
8
|
+
...(await importOriginal<typeof import("@multiplatform.one/platform")>()),
|
|
9
|
+
isWeb: false,
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
it("the native prop branch omits HTML button intent and retains compound refs", () => {
|
|
13
|
+
const root = createRef<any>();
|
|
14
|
+
const trigger = createRef<any>();
|
|
15
|
+
const result = renderWithProviders(
|
|
16
|
+
<>
|
|
17
|
+
<Collapsible ref={root}>
|
|
18
|
+
<Collapsible.Trigger ref={trigger} type="submit">
|
|
19
|
+
Disclosure
|
|
20
|
+
</Collapsible.Trigger>
|
|
21
|
+
<Collapsible.Content>Contents</Collapsible.Content>
|
|
22
|
+
</Collapsible>
|
|
23
|
+
<Accordion>
|
|
24
|
+
<Accordion.Item value="one">
|
|
25
|
+
<Accordion.Trigger type="submit">Section</Accordion.Trigger>
|
|
26
|
+
<Accordion.Content>Section contents</Accordion.Content>
|
|
27
|
+
</Accordion.Item>
|
|
28
|
+
</Accordion>
|
|
29
|
+
</>,
|
|
30
|
+
);
|
|
31
|
+
expect(root.current).toBeTruthy();
|
|
32
|
+
expect(trigger.current).toBe(result.getByRole("button", { name: "Disclosure" }));
|
|
33
|
+
expect(trigger.current.getAttribute("type")).toBeNull();
|
|
34
|
+
expect(result.getByRole("button", { name: "Section" }).getAttribute("type")).toBeNull();
|
|
35
|
+
});
|
package/src/knobImmuneExports.ts
CHANGED
|
@@ -42,8 +42,6 @@ export const knobImmuneExports: Readonly<Record<string, string>> = {
|
|
|
42
42
|
// own the painted chrome. Restored with the three below after MPO-21 R9 G8
|
|
43
43
|
// (the Adapt drop) rewrote this file from a pre-MPO-21 copy and took them
|
|
44
44
|
// out as collateral; Adapt itself stays dropped.
|
|
45
|
-
Collapsible:
|
|
46
|
-
"composition root (open/close shell); knobs land on composing surfaces (Accordion, Fold)",
|
|
47
45
|
Unspaced:
|
|
48
46
|
"spacing opt-out marker; paints nothing, tells the parent stack to skip gap on its children",
|
|
49
47
|
VisuallyHidden: "a11y hide utility; paints nothing visible, no chrome for knobs to reach",
|