@multiplatform.one/forms 7.28.0 → 7.29.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/dist/cjs/AdaptivePopup/index.cjs +19 -22
- package/dist/cjs/AdaptivePopup/index.native.js +19 -22
- package/dist/cjs/AdaptivePopup/index.native.js.map +1 -1
- package/dist/cjs/Button/index.cjs +32 -22
- package/dist/cjs/Button/index.native.js +40 -23
- package/dist/cjs/Button/index.native.js.map +1 -1
- package/dist/cjs/Chip/index.cjs +3 -2
- package/dist/cjs/Chip/index.native.js +4 -3
- package/dist/cjs/Chip/index.native.js.map +1 -1
- package/dist/cjs/ContextualSaveBar/UnsavedChangesDialog.cjs +14 -0
- package/dist/cjs/ContextualSaveBar/UnsavedChangesDialog.native.js +14 -0
- package/dist/cjs/ContextualSaveBar/UnsavedChangesDialog.native.js.map +1 -1
- package/dist/cjs/fields/Geolocation/index.cjs +1 -0
- package/dist/cjs/fields/Geolocation/index.native.js +1 -0
- package/dist/cjs/fields/Geolocation/index.native.js.map +1 -1
- package/dist/cjs/shared/focusManagement.cjs +28 -0
- package/dist/cjs/shared/focusManagement.native.js +38 -0
- package/dist/cjs/shared/focusManagement.native.js.map +1 -1
- package/dist/esm/AdaptivePopup/index.mjs +20 -23
- package/dist/esm/AdaptivePopup/index.mjs.map +1 -1
- package/dist/esm/AdaptivePopup/index.native.js +20 -23
- package/dist/esm/AdaptivePopup/index.native.js.map +1 -1
- package/dist/esm/Button/index.mjs +33 -23
- package/dist/esm/Button/index.mjs.map +1 -1
- package/dist/esm/Button/index.native.js +41 -24
- package/dist/esm/Button/index.native.js.map +1 -1
- package/dist/esm/Chip/index.mjs +4 -3
- package/dist/esm/Chip/index.mjs.map +1 -1
- package/dist/esm/Chip/index.native.js +5 -4
- package/dist/esm/Chip/index.native.js.map +1 -1
- package/dist/esm/ContextualSaveBar/UnsavedChangesDialog.mjs +16 -2
- package/dist/esm/ContextualSaveBar/UnsavedChangesDialog.mjs.map +1 -1
- package/dist/esm/ContextualSaveBar/UnsavedChangesDialog.native.js +16 -2
- package/dist/esm/ContextualSaveBar/UnsavedChangesDialog.native.js.map +1 -1
- package/dist/esm/fields/Geolocation/index.mjs +1 -0
- package/dist/esm/fields/Geolocation/index.mjs.map +1 -1
- package/dist/esm/fields/Geolocation/index.native.js +1 -0
- package/dist/esm/fields/Geolocation/index.native.js.map +1 -1
- package/dist/esm/shared/focusManagement.mjs +28 -2
- package/dist/esm/shared/focusManagement.mjs.map +1 -1
- package/dist/esm/shared/focusManagement.native.js +38 -2
- package/dist/esm/shared/focusManagement.native.js.map +1 -1
- package/dist/jsx/AdaptivePopup/index.mjs +20 -23
- package/dist/jsx/AdaptivePopup/index.mjs.map +1 -1
- package/dist/jsx/AdaptivePopup/index.native.js +19 -22
- package/dist/jsx/AdaptivePopup/index.native.js.map +1 -1
- package/dist/jsx/Button/index.mjs +33 -23
- package/dist/jsx/Button/index.mjs.map +1 -1
- package/dist/jsx/Button/index.native.js +40 -23
- package/dist/jsx/Button/index.native.js.map +1 -1
- package/dist/jsx/Chip/index.mjs +4 -3
- package/dist/jsx/Chip/index.mjs.map +1 -1
- package/dist/jsx/Chip/index.native.js +4 -3
- package/dist/jsx/Chip/index.native.js.map +1 -1
- package/dist/jsx/ContextualSaveBar/UnsavedChangesDialog.mjs +16 -2
- package/dist/jsx/ContextualSaveBar/UnsavedChangesDialog.mjs.map +1 -1
- package/dist/jsx/ContextualSaveBar/UnsavedChangesDialog.native.js +14 -0
- package/dist/jsx/ContextualSaveBar/UnsavedChangesDialog.native.js.map +1 -1
- package/dist/jsx/fields/Geolocation/index.mjs +1 -0
- package/dist/jsx/fields/Geolocation/index.mjs.map +1 -1
- package/dist/jsx/fields/Geolocation/index.native.js +1 -0
- package/dist/jsx/fields/Geolocation/index.native.js.map +1 -1
- package/dist/jsx/shared/focusManagement.mjs +28 -2
- package/dist/jsx/shared/focusManagement.mjs.map +1 -1
- package/dist/jsx/shared/focusManagement.native.js +38 -0
- package/dist/jsx/shared/focusManagement.native.js.map +1 -1
- package/package.json +8 -8
- package/src/AdaptivePopup/AdaptivePopup.spec.tsx +44 -1
- package/src/AdaptivePopup/index.tsx +19 -19
- package/src/Button/Button.disabledReasonInk.spec.tsx +134 -0
- package/src/Button/Button.disabledWash.spec.tsx +206 -0
- package/src/Button/Button.outlinedAccentInk.spec.tsx +152 -0
- package/src/Button/Button.stories.tsx +22 -6
- package/src/Button/index.tsx +70 -31
- package/src/Chip/index.tsx +9 -4
- package/src/ContextualSaveBar/UnsavedChangesDialog.spec.tsx +50 -3
- package/src/ContextualSaveBar/UnsavedChangesDialog.tsx +26 -2
- package/src/fields/Geolocation/index.tsx +3 -1
- package/src/shared/focusManagement.ts +40 -1
- package/types/AdaptivePopup/index.d.ts.map +1 -1
- package/types/Button/index.d.ts.map +1 -1
- package/types/Chip/index.d.ts +1 -1
- package/types/Chip/index.d.ts.map +1 -1
- package/types/ContextualSaveBar/UnsavedChangesDialog.d.ts.map +1 -1
- package/types/fields/Checkbox/index.d.ts +3 -3
- package/types/fields/Geolocation/index.d.ts.map +1 -1
- package/types/fields/RadioGroup/index.d.ts +2 -2
- package/types/fields/ToggleGroup/index.d.ts +1 -1
- package/types/shared/focusManagement.d.ts +19 -0
- package/types/shared/focusManagement.d.ts.map +1 -1
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
ensureFocusVisibleRing,
|
|
8
8
|
} from "@multiplatform.one/theme";
|
|
9
9
|
import { Button as TamaguiButton, Paragraph, XStack, YStack, useTheme } from "tamagui";
|
|
10
|
-
import { Button } from "./index";
|
|
10
|
+
import { Button, type ButtonProps } from "./index";
|
|
11
11
|
import { useState } from "react";
|
|
12
12
|
|
|
13
13
|
const pressed = (name: string) => () => action(name)();
|
|
@@ -45,23 +45,39 @@ const meta: Meta<typeof Button> = {
|
|
|
45
45
|
export default meta;
|
|
46
46
|
type Story = StoryObj<typeof Button>;
|
|
47
47
|
|
|
48
|
-
function
|
|
48
|
+
function ContrastExample({ children = "Save", ...props }: ButtonProps) {
|
|
49
49
|
const [presses, setPresses] = useState(0);
|
|
50
50
|
return (
|
|
51
51
|
<YStack data-lc-clicks={presses}>
|
|
52
|
-
<Button
|
|
53
|
-
|
|
52
|
+
<Button {...props} onPress={() => setPresses((value) => value + 1)}>
|
|
53
|
+
{children}
|
|
54
54
|
</Button>
|
|
55
55
|
</YStack>
|
|
56
56
|
);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
export const DestructiveEnabled: Story = {
|
|
60
|
-
render: () => <
|
|
60
|
+
render: () => <ContrastExample error>Delete</ContrastExample>,
|
|
61
61
|
};
|
|
62
62
|
|
|
63
63
|
export const DestructiveDisabled: Story = {
|
|
64
|
-
render: () =>
|
|
64
|
+
render: () => (
|
|
65
|
+
<ContrastExample error disabled>
|
|
66
|
+
Delete
|
|
67
|
+
</ContrastExample>
|
|
68
|
+
),
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** LC-40 sweeps each intent through args, enabled and disabled (MPO-433). */
|
|
72
|
+
export const IntentContrast: Story = {
|
|
73
|
+
argTypes: {
|
|
74
|
+
accent: { control: "boolean" },
|
|
75
|
+
warning: { control: "boolean" },
|
|
76
|
+
success: { control: "boolean" },
|
|
77
|
+
theme: { control: "text" },
|
|
78
|
+
disabled: { control: "boolean" },
|
|
79
|
+
},
|
|
80
|
+
render: (args) => <ContrastExample {...args} />,
|
|
65
81
|
};
|
|
66
82
|
|
|
67
83
|
export const Basic: Story = {
|
package/src/Button/index.tsx
CHANGED
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
sizeRecipeForToken,
|
|
21
21
|
sizeRecipeFromHeight,
|
|
22
22
|
solidButtonIntents,
|
|
23
|
+
solidPaletteThemes,
|
|
24
|
+
useAccentOnSurface,
|
|
23
25
|
useResolvedKnobs,
|
|
24
26
|
type RecipeFamilyName,
|
|
25
27
|
type SizeRecipe,
|
|
@@ -426,6 +428,7 @@ export function Button({
|
|
|
426
428
|
// page-scope `$outlineColor` literal (DG-A11Y-01; see focusVisibleStyle).
|
|
427
429
|
const parentScopeTheme = useTheme();
|
|
428
430
|
const pageOutlineColor = parentScopeTheme.outlineColor?.val as string | undefined;
|
|
431
|
+
const hostAccentInk = useAccentOnSurface();
|
|
429
432
|
const grouped = useControlGrouped();
|
|
430
433
|
const sizeProp = (props as { size?: SizeTokens | number }).size;
|
|
431
434
|
const resolvedSize = sizeProp ?? knobProps.sizeToken;
|
|
@@ -484,6 +487,32 @@ export function Button({
|
|
|
484
487
|
};
|
|
485
488
|
|
|
486
489
|
const activeThemeName = theme ?? intent;
|
|
490
|
+
const hostScoped = <T extends object>(fragment: T): T =>
|
|
491
|
+
Object.fromEntries(
|
|
492
|
+
Object.entries(fragment).map(([key, value]) => [
|
|
493
|
+
key,
|
|
494
|
+
typeof value === "string" && value.startsWith("$")
|
|
495
|
+
? (parentScopeTheme[value.slice(1) as keyof typeof parentScopeTheme]?.val ?? value)
|
|
496
|
+
: value,
|
|
497
|
+
]),
|
|
498
|
+
) as T;
|
|
499
|
+
// LC-40 / MPO-433: a solid-palette theme has no wash tier, so the wash and
|
|
500
|
+
// its ink come from the host scope. Solid-intent ramps keep their own wash,
|
|
501
|
+
// where `$color11` misses 4.5:1 (light warning 4.48, success 4.27) and the
|
|
502
|
+
// hue's `$color12` text step clears it.
|
|
503
|
+
const washOnHost = Boolean(
|
|
504
|
+
isDisabled && activeThemeName && solidPaletteThemes.has(activeThemeName),
|
|
505
|
+
);
|
|
506
|
+
const disabledSurface = washOnHost
|
|
507
|
+
? hostScoped(disabledState.surfaceKnobProps)
|
|
508
|
+
: disabledState.surfaceKnobProps;
|
|
509
|
+
const disabledText = washOnHost
|
|
510
|
+
? hostScoped(disabledState.textKnobProps)
|
|
511
|
+
: activeThemeName &&
|
|
512
|
+
solidButtonIntents.has(activeThemeName) &&
|
|
513
|
+
disabledState.textKnobProps.color
|
|
514
|
+
? { ...disabledState.textKnobProps, color: "$color12" }
|
|
515
|
+
: disabledState.textKnobProps;
|
|
487
516
|
outlined = outlined ?? knobProps.outlined;
|
|
488
517
|
const isLow = knobProps.textAccent === "low";
|
|
489
518
|
const isMedium = knobProps.textAccent === "medium";
|
|
@@ -513,7 +542,16 @@ export function Button({
|
|
|
513
542
|
// (measured 1.02–1.06:1 on the page in both schemes). At the default
|
|
514
543
|
// textAccent (`high`, textColor undefined) the label must re-anchor to the
|
|
515
544
|
// same readable text tier the frame uses.
|
|
516
|
-
const outlinedIntentInk = Boolean(
|
|
545
|
+
const outlinedIntentInk = Boolean(
|
|
546
|
+
outlined && activeThemeName && solidButtonIntents.has(activeThemeName),
|
|
547
|
+
);
|
|
548
|
+
// MPO-447: a solid-palette theme (accent) has no readable-on-page step at
|
|
549
|
+
// all, `$color11` included, so an outlined one takes its ink from the host:
|
|
550
|
+
// the accent that clears AA there, or host-scoped text under a textAccent.
|
|
551
|
+
const outlinedOnHost = Boolean(
|
|
552
|
+
outlined && activeThemeName && solidPaletteThemes.has(activeThemeName),
|
|
553
|
+
);
|
|
554
|
+
const outlinedInk = outlinedOnHost ? hostAccentInk : outlinedIntentInk ? "$color11" : undefined;
|
|
517
555
|
|
|
518
556
|
const resolvedRole: ButtonActionRole | undefined = actionRole ?? (accent ? "primary" : undefined);
|
|
519
557
|
const labelFromParts =
|
|
@@ -541,13 +579,15 @@ export function Button({
|
|
|
541
579
|
{...knobProps.body}
|
|
542
580
|
{...(nested ? knobProps.label : { fontSize: recipe.fontSize })}
|
|
543
581
|
{...(textColor
|
|
544
|
-
?
|
|
545
|
-
|
|
546
|
-
|
|
582
|
+
? outlinedOnHost
|
|
583
|
+
? hostScoped({ color: textColor })
|
|
584
|
+
: { color: textColor }
|
|
585
|
+
: outlinedInk
|
|
586
|
+
? { color: outlinedInk }
|
|
547
587
|
: undefined)}
|
|
548
588
|
// LC-40: on the washed disabled chrome the label leaves the on-fill
|
|
549
|
-
// foreground
|
|
550
|
-
{...(isDisabled ?
|
|
589
|
+
// foreground for the wash's AA ink (keepLabel keeps >=4.5:1).
|
|
590
|
+
{...(isDisabled ? disabledText : undefined)}
|
|
551
591
|
// Boards measure ink.weight on the TEXT NODE, never the frame. The
|
|
552
592
|
// prop is restated after the spreads because SizableText's size
|
|
553
593
|
// variant otherwise leaks 500/600 over it (MPO-125: a literal 400
|
|
@@ -692,9 +732,7 @@ export function Button({
|
|
|
692
732
|
// Solid-intent sub-themes carry the on-fill foreground as `$color`
|
|
693
733
|
// (LC-05); outlined frames sit on the page, so re-anchor inherited
|
|
694
734
|
// text/icon color (currentColor) to the hue's readable text tier.
|
|
695
|
-
{...(
|
|
696
|
-
? { color: "$color11" }
|
|
697
|
-
: undefined)}
|
|
735
|
+
{...(outlinedInk ? { color: outlinedInk } : undefined)}
|
|
698
736
|
// LC-40 DISABLED-VISIBLE: keepLabel washes the chrome (muted fill/
|
|
699
737
|
// border) while label/glyphs re-anchor to the lowest AA text tier
|
|
700
738
|
// (`color` inherits into currentColor icons); dimWhole dims the
|
|
@@ -702,8 +740,8 @@ export function Button({
|
|
|
702
740
|
// whole-dim that dropped label contrast under AA.
|
|
703
741
|
{...(isDisabled
|
|
704
742
|
? {
|
|
705
|
-
...
|
|
706
|
-
...
|
|
743
|
+
...disabledSurface,
|
|
744
|
+
...disabledText,
|
|
707
745
|
...disabledState.assemblyKnobProps,
|
|
708
746
|
}
|
|
709
747
|
: undefined)}
|
|
@@ -784,11 +822,11 @@ export function Button({
|
|
|
784
822
|
// control must not light up like a pressable one.
|
|
785
823
|
hoverStyle: {
|
|
786
824
|
backgroundColor: outlined ? "transparent" : "$background",
|
|
787
|
-
...
|
|
825
|
+
...disabledSurface,
|
|
788
826
|
},
|
|
789
827
|
pressStyle: {
|
|
790
828
|
backgroundColor: outlined ? "transparent" : "$background",
|
|
791
|
-
...
|
|
829
|
+
...disabledSurface,
|
|
792
830
|
},
|
|
793
831
|
"data-mp-disabled-explained": "true",
|
|
794
832
|
}
|
|
@@ -814,11 +852,20 @@ export function Button({
|
|
|
814
852
|
</ButtonFrame>
|
|
815
853
|
);
|
|
816
854
|
|
|
855
|
+
const themedButton = activeThemeName ? (
|
|
856
|
+
<Theme name={activeThemeName as ThemeName}>{buttonElement}</Theme>
|
|
857
|
+
) : (
|
|
858
|
+
buttonElement
|
|
859
|
+
);
|
|
860
|
+
|
|
817
861
|
// DG-ST-03 explain affordance: visible reason adjacent to the control —
|
|
818
862
|
// discoverable by pointer, touch, keyboard, and SR (aria-describedby)
|
|
819
863
|
// without any hover interaction (Axiom 12). "below" stacks (forms);
|
|
820
864
|
// "inline" keeps toolbar rows on one shared control line, where the
|
|
821
865
|
// stacked reason wrapped into cramped multi-line type under the button.
|
|
866
|
+
// The reason stays OUTSIDE the intent Theme: it is T-HELPER copy on the
|
|
867
|
+
// host surface, and inside it `$color11` is the intent's on-fill ramp
|
|
868
|
+
// (MPO-431: 1.17:1 on the light page under accent).
|
|
822
869
|
const reasonText = explainedDisable ? (
|
|
823
870
|
<SizableText
|
|
824
871
|
id={reasonId}
|
|
@@ -830,26 +877,18 @@ export function Button({
|
|
|
830
877
|
{disabledReason}
|
|
831
878
|
</SizableText>
|
|
832
879
|
) : null;
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
) : (
|
|
840
|
-
<YStack gap="$1" flexShrink={1} data-mp-disabled-reason-wrapper="true">
|
|
841
|
-
{buttonElement}
|
|
842
|
-
{reasonText}
|
|
843
|
-
</YStack>
|
|
844
|
-
)
|
|
880
|
+
if (!explainedDisable) return themedButton;
|
|
881
|
+
return disabledReasonPlacement === "inline" ? (
|
|
882
|
+
<XStack gap="$2" alignItems="center" flexShrink={1} data-mp-disabled-reason-wrapper="true">
|
|
883
|
+
{themedButton}
|
|
884
|
+
{reasonText}
|
|
885
|
+
</XStack>
|
|
845
886
|
) : (
|
|
846
|
-
|
|
887
|
+
<YStack gap="$1" flexShrink={1} data-mp-disabled-reason-wrapper="true">
|
|
888
|
+
{themedButton}
|
|
889
|
+
{reasonText}
|
|
890
|
+
</YStack>
|
|
847
891
|
);
|
|
848
|
-
|
|
849
|
-
if (activeThemeName) {
|
|
850
|
-
return <Theme name={activeThemeName as ThemeName}>{explainedElement}</Theme>;
|
|
851
|
-
}
|
|
852
|
-
return explainedElement;
|
|
853
892
|
}
|
|
854
893
|
|
|
855
894
|
Button.Icon = TamaguiButton.Icon;
|
package/src/Chip/index.tsx
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
resolveRadiusClass,
|
|
11
11
|
sizeRecipeForToken,
|
|
12
12
|
stackRadiusProps,
|
|
13
|
+
useLegibleInkOn,
|
|
13
14
|
useResolvedKnobs,
|
|
14
15
|
type GroupOrientation,
|
|
15
16
|
type GroupPosition,
|
|
@@ -416,7 +417,10 @@ export function Chip({
|
|
|
416
417
|
}
|
|
417
418
|
};
|
|
418
419
|
|
|
419
|
-
|
|
420
|
+
// Selected paints the family's step 8, where step-12 ink misses AA in dark
|
|
421
|
+
// (#c2e6ff on #2870bd, 3.88:1); the fill keeps its ink only while it reads.
|
|
422
|
+
const selectedInk = useLegibleInkOn(colors.fill, colors.text);
|
|
423
|
+
const textColor = (selected ? selectedInk : colors.text) as ColorTokens;
|
|
420
424
|
const borderColor = selected
|
|
421
425
|
? "transparent"
|
|
422
426
|
: variant === "outline"
|
|
@@ -434,9 +438,10 @@ export function Chip({
|
|
|
434
438
|
// 1.32:1 on the dark tint — the "invisible ✕" regression). useTheme gives
|
|
435
439
|
// the scheme-aware value for the same step-12 ink the label uses.
|
|
436
440
|
const theme = useTheme();
|
|
437
|
-
const glyphInk =
|
|
438
|
-
(theme as Record<string, { val?: string } | undefined>)[(textColor as string).
|
|
439
|
-
|
|
441
|
+
const glyphInk = (textColor as string).startsWith("$")
|
|
442
|
+
? ((theme as Record<string, { val?: string } | undefined>)[(textColor as string).slice(1)]
|
|
443
|
+
?.val ?? "currentColor")
|
|
444
|
+
: (textColor as string);
|
|
440
445
|
// Undifferentiated "Remove" fails when a row holds many chips (UX-019);
|
|
441
446
|
// interpolate the label whenever it is a plain string.
|
|
442
447
|
const dismissLabel =
|
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
* outside click from dismissing a destructive confirm. Those are the parts a
|
|
8
8
|
* refactor of the save bar would carry away without noticing.
|
|
9
9
|
*/
|
|
10
|
-
import { cleanup, fireEvent } from "@testing-library/react";
|
|
11
|
-
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
12
|
-
import {
|
|
10
|
+
import { cleanup, fireEvent, screen, waitFor } from "@testing-library/react";
|
|
11
|
+
import { modelFirstFocusableShow, renderWithProviders } from "@multiplatform.one/test-utils";
|
|
12
|
+
import { useState } from "react";
|
|
13
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
13
14
|
import { UnsavedChangesDialog } from "./UnsavedChangesDialog";
|
|
14
15
|
|
|
15
16
|
afterEach(cleanup);
|
|
@@ -165,3 +166,49 @@ describe("UnsavedChangesDialog", () => {
|
|
|
165
166
|
expect(cssOf(dialog() as HTMLElement, "max-width")).toBe("440px");
|
|
166
167
|
});
|
|
167
168
|
});
|
|
169
|
+
|
|
170
|
+
// MPO-472: Chromium and WebKit's <dialog>.show() focuses the first focusable
|
|
171
|
+
// descendant, and the panel is tabindex=-1, so those two opened on the panel
|
|
172
|
+
// while Firefox and the HTML spec open on Keep editing, the first tab stop.
|
|
173
|
+
// Keep editing is the alertdialog's least destructive action, the one tamagui's
|
|
174
|
+
// AlertDialog.Cancel takes on open, so every browser opens there.
|
|
175
|
+
describe("UnsavedChangesDialog open focus", () => {
|
|
176
|
+
function LeavePage() {
|
|
177
|
+
const [open, setOpen] = useState(false);
|
|
178
|
+
return (
|
|
179
|
+
<>
|
|
180
|
+
<button type="button" onClick={() => setOpen(true)}>
|
|
181
|
+
Leave the page
|
|
182
|
+
</button>
|
|
183
|
+
<UnsavedChangesDialog
|
|
184
|
+
open={open}
|
|
185
|
+
fieldCount={2}
|
|
186
|
+
onKeepEditing={() => setOpen(false)}
|
|
187
|
+
onDiscard={() => setOpen(false)}
|
|
188
|
+
/>
|
|
189
|
+
</>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
it("opens on Keep editing", async () => {
|
|
194
|
+
renderWithProviders(<LeavePage />);
|
|
195
|
+
fireEvent.click(screen.getByRole("button", { name: "Leave the page" }));
|
|
196
|
+
await waitFor(() => expect(dialog()).not.toBeNull());
|
|
197
|
+
await waitFor(() => expect(document.activeElement).toBe(button("data-mp-savebar-keep")));
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe("behind <dialog>.show()", () => {
|
|
201
|
+
let restoreShow = () => {};
|
|
202
|
+
beforeEach(() => {
|
|
203
|
+
restoreShow = modelFirstFocusableShow();
|
|
204
|
+
});
|
|
205
|
+
afterEach(() => restoreShow());
|
|
206
|
+
|
|
207
|
+
it("moves focus off the panel onto Keep editing", async () => {
|
|
208
|
+
renderWithProviders(<LeavePage />);
|
|
209
|
+
fireEvent.click(screen.getByRole("button", { name: "Leave the page" }));
|
|
210
|
+
await waitFor(() => expect(dialog()).not.toBeNull());
|
|
211
|
+
await waitFor(() => expect(document.activeElement).toBe(button("data-mp-savebar-keep")));
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
});
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
ensureKeyboardModalityTracking,
|
|
3
|
+
useResolvedKnobs,
|
|
4
|
+
warnCancelDisabled,
|
|
5
|
+
wasKeyboardFocus,
|
|
6
|
+
} from "@multiplatform.one/theme";
|
|
2
7
|
import { useEffect } from "react";
|
|
3
8
|
import { Dialog, Paragraph, XStack } from "tamagui";
|
|
4
9
|
import { Button } from "../Button";
|
|
5
10
|
import { formCommonColors } from "../shared/colorRamps";
|
|
6
|
-
import { useReturnFocusOnClose } from "../shared/focusManagement";
|
|
11
|
+
import { DialogShowFocus, useReturnFocusOnClose } from "../shared/focusManagement";
|
|
7
12
|
import { t } from "../shared/t";
|
|
8
13
|
|
|
9
14
|
export interface UnsavedChangesDialogProps {
|
|
@@ -13,6 +18,22 @@ export interface UnsavedChangesDialogProps {
|
|
|
13
18
|
onDiscard: () => void;
|
|
14
19
|
}
|
|
15
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Opens on Keep editing, the least destructive action, as tamagui's
|
|
23
|
+
* AlertDialog.Cancel does. Without it Chromium and WebKit's `<dialog>.show()`
|
|
24
|
+
* parks focus on the tabIndex -1 panel and Firefox on Keep editing (MPO-472).
|
|
25
|
+
* WebKit's show() also marks the panel :focus-visible, which a plain focus()
|
|
26
|
+
* would carry onto the button after a pointer open; the ring is keyboard-only
|
|
27
|
+
* (LC-71).
|
|
28
|
+
*/
|
|
29
|
+
function focusKeepEditing(event: Event) {
|
|
30
|
+
const root = event.currentTarget as HTMLElement | null;
|
|
31
|
+
const keep = root?.querySelector<HTMLElement>("[data-mp-savebar-keep]");
|
|
32
|
+
if (!keep) return;
|
|
33
|
+
event.preventDefault();
|
|
34
|
+
keep.focus({ preventScroll: true, focusVisible: wasKeyboardFocus() } as FocusOptions);
|
|
35
|
+
}
|
|
36
|
+
|
|
16
37
|
/**
|
|
17
38
|
* Route-leave / Discard confirm (DG-OVL-02). Lives in forms so ContextualSaveBar
|
|
18
39
|
* does not import `@multiplatform.one/components` (ConfirmDialog → forms Button
|
|
@@ -25,6 +46,7 @@ export function UnsavedChangesDialog({
|
|
|
25
46
|
onDiscard,
|
|
26
47
|
}: UnsavedChangesDialogProps) {
|
|
27
48
|
const { knobProps } = useResolvedKnobs();
|
|
49
|
+
ensureKeyboardModalityTracking();
|
|
28
50
|
useReturnFocusOnClose(open);
|
|
29
51
|
|
|
30
52
|
useEffect(() => {
|
|
@@ -73,6 +95,7 @@ export function UnsavedChangesDialog({
|
|
|
73
95
|
data-mp-savebar-guard=""
|
|
74
96
|
onPointerDownOutside={(e) => e.preventDefault()}
|
|
75
97
|
onInteractOutside={(e) => e.preventDefault()}
|
|
98
|
+
onOpenAutoFocus={focusKeepEditing}
|
|
76
99
|
>
|
|
77
100
|
<Dialog.Title {...knobProps.body} fontSize="$4">
|
|
78
101
|
{t("Discard unsaved changes?")}
|
|
@@ -103,6 +126,7 @@ export function UnsavedChangesDialog({
|
|
|
103
126
|
{t("Discard changes")}
|
|
104
127
|
</Button>
|
|
105
128
|
</XStack>
|
|
129
|
+
<DialogShowFocus onOpenAutoFocus={focusKeepEditing} />
|
|
106
130
|
</Dialog.Content>
|
|
107
131
|
</Dialog.Portal>
|
|
108
132
|
</Dialog>
|
|
@@ -1420,7 +1420,9 @@ export function Geolocation({
|
|
|
1420
1420
|
overflow="hidden"
|
|
1421
1421
|
style={{ boxShadow: RESULTS_PANEL_SHADOW }}
|
|
1422
1422
|
>
|
|
1423
|
-
<ScrollView maxHeight={mapHeight * 1.5}>
|
|
1423
|
+
<ScrollView keyboardShouldPersistTaps="handled" maxHeight={mapHeight * 1.5}>
|
|
1424
|
+
{renderResults(handlers)}
|
|
1425
|
+
</ScrollView>
|
|
1424
1426
|
</YStack>
|
|
1425
1427
|
)
|
|
1426
1428
|
) : null;
|
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
* focus INTO the sheet frame on open, keeps Tab cycling within it while
|
|
16
16
|
* open (Tamagui Sheet has no focus scope), and treats focus left inside
|
|
17
17
|
* the closed (offscreen-parked, still-mounted) frame as dropped.
|
|
18
|
+
* - `useDialogShowFocus(onOpenAutoFocus)` — runs a dialog's open focus when
|
|
19
|
+
* the browser's `<dialog>.show()` left focus on an element that is not a
|
|
20
|
+
* tab stop (MPO-470).
|
|
18
21
|
*
|
|
19
22
|
* Capture happens during RENDER, not in an effect: child effects (the Dialog
|
|
20
23
|
* focus scope) run before parent effects, so an effect-time capture would
|
|
@@ -32,7 +35,7 @@
|
|
|
32
35
|
*/
|
|
33
36
|
|
|
34
37
|
import { useEffect, useRef, type RefObject } from "react";
|
|
35
|
-
import { isWeb } from "tamagui";
|
|
38
|
+
import { isWeb, useDialogContext } from "tamagui";
|
|
36
39
|
import { wasKeyboardFocus } from "@multiplatform.one/theme";
|
|
37
40
|
|
|
38
41
|
const FOCUSABLE =
|
|
@@ -164,3 +167,39 @@ export function useSheetModalFocus(
|
|
|
164
167
|
};
|
|
165
168
|
}, [open, frameRef, prevRef, generationRef, skipReturnFocusRef]);
|
|
166
169
|
}
|
|
170
|
+
|
|
171
|
+
const DIALOG_SHOW_FOCUS_EVENT = "mp.dialogShowFocus";
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Chromium and WebKit's `<dialog>.show()` focuses the first focusable
|
|
175
|
+
* descendant, so a tabIndex -1 surface or field frame takes focus; Firefox and
|
|
176
|
+
* the HTML spec take the first tab stop. Focus is then inside before tamagui's
|
|
177
|
+
* FocusScope checks, and its open auto-focus never fires. When focus sits on
|
|
178
|
+
* something that is not a tab stop, run `onOpenAutoFocus` against the content
|
|
179
|
+
* the way FocusScope would. Call it inside the Dialog. The content is found by
|
|
180
|
+
* its id because FocusScope clones it with its own ref, so a ref passed to
|
|
181
|
+
* `Dialog.Content` never attaches on web. Passive effects run after the
|
|
182
|
+
* portal's layout-effect `show()`, which is why this is not a layout effect.
|
|
183
|
+
*/
|
|
184
|
+
export function useDialogShowFocus(onOpenAutoFocus: (event: Event) => void) {
|
|
185
|
+
const { open, contentId } = useDialogContext();
|
|
186
|
+
useEffect(() => {
|
|
187
|
+
if (!open || !contentId || !canUseDom()) return;
|
|
188
|
+
const content = document.getElementById(contentId);
|
|
189
|
+
const focused = document.activeElement;
|
|
190
|
+
if (!content || !(focused instanceof HTMLElement)) return;
|
|
191
|
+
if (!content.contains(focused) || focused.tabIndex >= 0) return;
|
|
192
|
+
content.addEventListener(DIALOG_SHOW_FOCUS_EVENT, onOpenAutoFocus, { once: true });
|
|
193
|
+
content.dispatchEvent(new CustomEvent(DIALOG_SHOW_FOCUS_EVENT, { cancelable: true }));
|
|
194
|
+
}, [open, contentId]);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** `useDialogShowFocus` for a caller that renders the Dialog itself. */
|
|
198
|
+
export function DialogShowFocus({
|
|
199
|
+
onOpenAutoFocus,
|
|
200
|
+
}: {
|
|
201
|
+
onOpenAutoFocus: (event: Event) => void;
|
|
202
|
+
}): null {
|
|
203
|
+
useDialogShowFocus(onOpenAutoFocus);
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/AdaptivePopup/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/AdaptivePopup/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AA4B5D,MAAM,WAAW,kBAAkB;IACjC,4BAA4B;IAC5B,IAAI,EAAE,OAAO,CAAC;IACd,uCAAuC;IACvC,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACtC,gFAAgF;IAChF,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,gCAAgC;IAChC,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,wCAAwC;IACxC,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB,mBAAmB;IACnB,QAAQ,EAAE,SAAS,CAAC;IACpB;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,YAAY,CAAC;IAChD;;;;;OAKG;IACH,IAAI,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC3B,sDAAsD;IACtD,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,kDAAkD;IAClD,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,qCAAqC;IACrC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAkBD,MAAM,MAAM,yBAAyB,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEtE;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE;IAC7C,IAAI,CAAC,EAAE,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAClC,IAAI,CAAC,EAAE,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAClC,aAAa,EAAE,MAAM,CAAC;CACvB,GAAG,yBAAyB,CAK5B;AA6ED,wBAAgB,aAAa,CAAC,EAC5B,IAAI,EACJ,YAAY,EACZ,OAAO,EACP,KAAK,EACL,WAAW,EACX,QAAQ,EACR,IAAW,EACX,IAAe,EACf,eAAsB,EACtB,MAAM,EACN,QAAQ,GACT,EAAE,kBAAkB,2CAwSpB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/Button/index.tsx"],"names":[],"mappings":"AACA,OAAO,EAOL,KAAK,aAAa,EAClB,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/Button/index.tsx"],"names":[],"mappings":"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"}
|
package/types/Chip/index.d.ts
CHANGED
|
@@ -29,7 +29,7 @@ export interface ChipProps {
|
|
|
29
29
|
*/
|
|
30
30
|
export declare const ChipFrame: import("tamagui").TamaguiComponent<import("@tamagui/web").TamaDefer, import("tamagui").TamaguiElement, import("@tamagui/core").RNTamaguiViewNonStyleProps, import("@tamagui/web").StackStyleBase, {
|
|
31
31
|
size?: import("tamagui").SizeTokens | undefined;
|
|
32
|
-
variant?: "
|
|
32
|
+
variant?: "solid" | "outline" | "subtle" | undefined;
|
|
33
33
|
}, import("@tamagui/web").StaticConfigPublic>;
|
|
34
34
|
export declare const ChipText: import("tamagui").TamaguiComponent<import("@tamagui/web").TamaDefer, import("tamagui").TamaguiTextElement, import("@tamagui/core").RNTamaguiTextNonStyleProps, import("@tamagui/web").TextStylePropsBase, {
|
|
35
35
|
size?: import("tamagui").SizeTokens | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/Chip/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,SAAS,EAAE,MAAM,OAAO,CAAC;AAGtD,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/Chip/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,SAAS,EAAE,MAAM,OAAO,CAAC;AAGtD,OAAO,EAWL,KAAK,gBAAgB,EACrB,KAAK,aAAa,EACnB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAe,QAAQ,EAAE,MAAM,SAAS,CAAC;AAMrD,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,QAAQ,GAAG,SAAS,CAAC;AACzD,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAC3F,MAAM,MAAM,QAAQ,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAEjD,gFAAgF;AAChF,MAAM,MAAM,eAAe,GAAG,WAAW,GAAG,QAAQ,GAAG,UAAU,GAAG,OAAO,CAAC;AAE5E,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,SAAS,CAAC;IACpB,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,OAAO,CAAC,EAAE,eAAe,CAAC;IAC1B,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,yEAAyE;IACzE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,+EAA+E;IAC/E,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,oEAAoE;IACpE,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;CACrC;AAqID;;;;GAIG;AACH,eAAO,MAAM,SAAS;;;6CA+BpB,CAAC;AAEH,eAAO,MAAM,QAAQ;;;6CAenB,CAAC;AAiBH,sFAAsF;AACtF,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAEnD;AAED;;;;;GAKG;AACH,eAAO,MAAM,4BAA4B,KAAK,CAAC;AAE/C;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE3D;AA2GD,wBAAgB,IAAI,CAAC,EACnB,QAAQ,EACR,IAAI,EACJ,OAAO,EAAE,WAAsB,EAC/B,KAAc,EACd,SAAS,EACT,QAAQ,EACR,QAAgB,EAChB,OAAO,EACP,IAAI,EACJ,aAAa,EACb,gBAA+B,GAChC,EAAE,SAAS,2CA0NX;AAED,MAAM,MAAM,cAAc,GAAG,QAAQ,CAAC,OAAO,SAAS,CAAC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UnsavedChangesDialog.d.ts","sourceRoot":"","sources":["../../src/ContextualSaveBar/UnsavedChangesDialog.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"UnsavedChangesDialog.d.ts","sourceRoot":"","sources":["../../src/ContextualSaveBar/UnsavedChangesDialog.tsx"],"names":[],"mappings":"AAaA,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,OAAO,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B,SAAS,EAAE,MAAM,IAAI,CAAC;CACvB;AAkBD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,IAAI,EACJ,UAAU,EACV,aAAa,EACb,SAAS,GACV,EAAE,yBAAyB,2CAuF3B"}
|