@multiplatform.one/forms 7.43.0 → 7.44.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/InputParts/index.cjs +20 -116
- package/dist/cjs/InputParts/index.native.js +23 -121
- package/dist/cjs/InputParts/index.native.js.map +1 -1
- package/dist/cjs/fields/Input/index.cjs +1 -18
- package/dist/cjs/fields/Input/index.native.js +1 -18
- package/dist/cjs/fields/Input/index.native.js.map +1 -1
- package/dist/cjs/fields/OTPInput/index.cjs +65 -82
- package/dist/cjs/fields/OTPInput/index.native.js +67 -84
- package/dist/cjs/fields/OTPInput/index.native.js.map +1 -1
- package/dist/cjs/fields/OTPInput/otpCellCss.cjs +1 -3
- package/dist/cjs/fields/OTPInput/otpCellCss.native.js +1 -3
- package/dist/cjs/fields/OTPInput/otpCellCss.native.js.map +1 -1
- package/dist/cjs/fields/PhoneInput/index.cjs +1 -1
- package/dist/cjs/fields/PhoneInput/index.native.js +1 -1
- package/dist/cjs/fields/Signature/index.cjs +3 -11
- package/dist/cjs/fields/Signature/index.native.js +3 -11
- package/dist/cjs/fields/Signature/index.native.js.map +1 -1
- package/dist/esm/InputParts/index.mjs +22 -116
- package/dist/esm/InputParts/index.mjs.map +1 -1
- package/dist/esm/InputParts/index.native.js +25 -121
- package/dist/esm/InputParts/index.native.js.map +1 -1
- package/dist/esm/fields/Input/index.mjs +2 -18
- package/dist/esm/fields/Input/index.mjs.map +1 -1
- package/dist/esm/fields/Input/index.native.js +2 -18
- package/dist/esm/fields/Input/index.native.js.map +1 -1
- package/dist/esm/fields/OTPInput/index.mjs +67 -84
- package/dist/esm/fields/OTPInput/index.mjs.map +1 -1
- package/dist/esm/fields/OTPInput/index.native.js +69 -86
- package/dist/esm/fields/OTPInput/index.native.js.map +1 -1
- package/dist/esm/fields/OTPInput/otpCellCss.mjs +1 -3
- package/dist/esm/fields/OTPInput/otpCellCss.mjs.map +1 -1
- package/dist/esm/fields/OTPInput/otpCellCss.native.js +1 -3
- package/dist/esm/fields/OTPInput/otpCellCss.native.js.map +1 -1
- package/dist/esm/fields/PhoneInput/index.mjs +1 -1
- package/dist/esm/fields/PhoneInput/index.native.js +1 -1
- package/dist/esm/fields/Signature/index.mjs +4 -12
- package/dist/esm/fields/Signature/index.mjs.map +1 -1
- package/dist/esm/fields/Signature/index.native.js +4 -12
- package/dist/esm/fields/Signature/index.native.js.map +1 -1
- package/dist/jsx/InputParts/index.mjs +22 -116
- package/dist/jsx/InputParts/index.mjs.map +1 -1
- package/dist/jsx/InputParts/index.native.js +23 -121
- package/dist/jsx/InputParts/index.native.js.map +1 -1
- package/dist/jsx/fields/Input/index.mjs +2 -18
- package/dist/jsx/fields/Input/index.mjs.map +1 -1
- package/dist/jsx/fields/Input/index.native.js +1 -18
- package/dist/jsx/fields/Input/index.native.js.map +1 -1
- package/dist/jsx/fields/OTPInput/index.mjs +67 -84
- package/dist/jsx/fields/OTPInput/index.mjs.map +1 -1
- package/dist/jsx/fields/OTPInput/index.native.js +67 -84
- package/dist/jsx/fields/OTPInput/index.native.js.map +1 -1
- package/dist/jsx/fields/OTPInput/otpCellCss.mjs +1 -3
- package/dist/jsx/fields/OTPInput/otpCellCss.mjs.map +1 -1
- package/dist/jsx/fields/OTPInput/otpCellCss.native.js +1 -3
- package/dist/jsx/fields/OTPInput/otpCellCss.native.js.map +1 -1
- package/dist/jsx/fields/PhoneInput/index.mjs +1 -1
- package/dist/jsx/fields/PhoneInput/index.native.js +1 -1
- package/dist/jsx/fields/Signature/index.mjs +4 -12
- package/dist/jsx/fields/Signature/index.mjs.map +1 -1
- package/dist/jsx/fields/Signature/index.native.js +3 -11
- package/dist/jsx/fields/Signature/index.native.js.map +1 -1
- package/package.json +8 -8
- package/src/InputParts/index.spec.tsx +11 -17
- package/src/InputParts/index.tsx +25 -147
- package/src/fieldFocus.spec.ts +44 -0
- package/src/fields/DatePicker/DatePicker.spec.tsx +1 -1
- package/src/fields/DatePicker/DateRangePicker.spec.tsx +1 -1
- package/src/fields/DatePicker/MultiDatePicker.spec.tsx +1 -1
- package/src/fields/Duration/Duration.spec.tsx +2 -2
- package/src/fields/Input/Input.copy.spec.tsx +1 -1
- package/src/fields/Input/Input.spec.tsx +7 -8
- package/src/fields/Input/index.tsx +1 -20
- package/src/fields/OTPInput/index.tsx +79 -98
- package/src/fields/OTPInput/otpCellCss.ts +4 -8
- package/src/fields/PhoneInput/PhoneInput.spec.tsx +3 -3
- package/src/fields/PhoneInput/index.tsx +1 -1
- package/src/fields/SearchInput/SearchInput.spec.tsx +1 -1
- package/src/fields/Signature/index.tsx +4 -10
- package/src/fields/TextArea/TextArea.spec.tsx +3 -3
- package/src/fields/TimePicker/TimePicker.spec.tsx +1 -1
- package/types/InputParts/index.d.ts +29 -32
- package/types/InputParts/index.d.ts.map +1 -1
- package/types/fields/Input/index.d.ts +0 -1
- package/types/fields/Input/index.d.ts.map +1 -1
- package/types/fields/OTPInput/index.d.ts.map +1 -1
- package/types/fields/OTPInput/otpCellCss.d.ts.map +1 -1
- package/types/fields/Signature/index.d.ts.map +1 -1
package/src/InputParts/index.tsx
CHANGED
|
@@ -28,9 +28,9 @@ import {
|
|
|
28
28
|
} from "tamagui";
|
|
29
29
|
import {
|
|
30
30
|
ensureCompositeFocusRing,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
FIELD_FOCUS_REST,
|
|
32
|
+
FIELD_FOCUS_STYLE,
|
|
33
|
+
fieldFocusCss,
|
|
34
34
|
useTouchSurface,
|
|
35
35
|
isTouchSurface,
|
|
36
36
|
recipeFamilies,
|
|
@@ -160,22 +160,6 @@ export const useForwardFocus = (target: RefObject<TamaguiElement | null>) => {
|
|
|
160
160
|
// Default group styles
|
|
161
161
|
// ---------------------------------------------------------------------------
|
|
162
162
|
|
|
163
|
-
/** Resting outline: transparent + 0 width so blur never lands on theme `$outlineColor`. */
|
|
164
|
-
const inputOutlineRest = {
|
|
165
|
-
outlineWidth: 0,
|
|
166
|
-
outlineColor: "transparent",
|
|
167
|
-
outlineStyle: "solid" as any,
|
|
168
|
-
} as const;
|
|
169
|
-
|
|
170
|
-
const inputOutlineFocus = {
|
|
171
|
-
outlineColor: "$outlineColor",
|
|
172
|
-
outlineWidth: 2,
|
|
173
|
-
outlineStyle: "solid" as any,
|
|
174
|
-
// Inset: Input.Box clips children with overflow:hidden; an outer +2px
|
|
175
|
-
// ring is severed (LC-71 clipping carve-out).
|
|
176
|
-
outlineOffset: -2,
|
|
177
|
-
} as const;
|
|
178
|
-
|
|
179
163
|
/**
|
|
180
164
|
* LC-71 field anatomy (Polaris Connected / Primer TextInputWrapper / Spectrum):
|
|
181
165
|
* one painted Box owns border + radius + ring; Icon/prefix/suffix live inside
|
|
@@ -192,37 +176,9 @@ const AREA_NO_OUTLINE = {
|
|
|
192
176
|
|
|
193
177
|
type BoxOverflow = "hidden" | "visible";
|
|
194
178
|
|
|
195
|
-
export
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
): "inset" | "outset" | undefined {
|
|
199
|
-
if (!paintsRing) return undefined;
|
|
200
|
-
return overflow === "hidden" ? "inset" : "outset";
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
export function boxRingOffset(mode: "inset" | "outset" | undefined): number {
|
|
204
|
-
// Both branches are LC-71 §4 sanctioned offsets: the clipping inset when the
|
|
205
|
-
// Box clips its adornments, the shape halo when it does not (MPO-92).
|
|
206
|
-
return mode === "outset" ? FOCUS_RING_HALO_OFFSET : FOCUS_RING_CLIPPED_OFFSET;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
export const inputPartsRingCss = `/* LC-71: ring on Box (adornments inside); Area never outlines.
|
|
210
|
-
Inset iff the Box clips (overflow:hidden). Do not :has(:focus-visible) on
|
|
211
|
-
every descendant — SearchInput clear ✕ is a second focusable. */
|
|
212
|
-
[data-mp-input-box][data-mp-ring="inset"]:has(input:focus),
|
|
213
|
-
[data-mp-input-box][data-mp-ring="inset"]:has(textarea:focus),
|
|
214
|
-
[data-mp-input-box][data-mp-ring="inset"]:has([data-mp-input-area]:focus),
|
|
215
|
-
[data-mp-input-box][data-mp-ring="inset"]:focus-visible {
|
|
216
|
-
outline: 2px solid var(--outlineColor, var(--c-outlineColor, CanvasText)) !important;
|
|
217
|
-
outline-offset: -2px !important;
|
|
218
|
-
}
|
|
219
|
-
[data-mp-input-box][data-mp-ring="outset"]:has(input:focus),
|
|
220
|
-
[data-mp-input-box][data-mp-ring="outset"]:has(textarea:focus),
|
|
221
|
-
[data-mp-input-box][data-mp-ring="outset"]:has([data-mp-input-area]:focus),
|
|
222
|
-
[data-mp-input-box][data-mp-ring="outset"]:focus-visible {
|
|
223
|
-
outline: 2px solid var(--outlineColor, var(--c-outlineColor, CanvasText)) !important;
|
|
224
|
-
outline-offset: 2px !important;
|
|
225
|
-
}
|
|
179
|
+
export const inputPartsRingCss = `/* FZ-7717: a focused Box takes the shared field focus look; Area never outlines. */
|
|
180
|
+
${fieldFocusCss("[data-mp-input-box][data-mp-field-focus]")}
|
|
181
|
+
${fieldFocusCss("[data-mp-input-box][data-mp-field-focus].mp-field-focus-deep", { deep: true })}
|
|
226
182
|
[data-mp-input-area],
|
|
227
183
|
[data-mp-input-area]:focus,
|
|
228
184
|
[data-mp-input-area]:focus-visible,
|
|
@@ -250,19 +206,14 @@ export function ensureInputPartsFocusRing(): void {
|
|
|
250
206
|
}
|
|
251
207
|
|
|
252
208
|
function webBoxRingAttrs(
|
|
253
|
-
|
|
254
|
-
paintsRing: boolean,
|
|
209
|
+
paintsFocus: boolean,
|
|
255
210
|
extraClass?: string,
|
|
256
211
|
): Record<string, unknown> | undefined {
|
|
257
212
|
if (!isWeb) return undefined;
|
|
258
|
-
const mode = boxRingMode(overflow, paintsRing);
|
|
259
|
-
const className = [paintsRing ? "mp-composite-ring" : undefined, extraClass]
|
|
260
|
-
.filter(Boolean)
|
|
261
|
-
.join(" ");
|
|
262
213
|
return {
|
|
263
|
-
...(
|
|
214
|
+
...(extraClass ? { className: extraClass } : undefined),
|
|
264
215
|
"data-mp-input-box": "true",
|
|
265
|
-
...(
|
|
216
|
+
...(paintsFocus ? { "data-mp-field-focus": "true" } : undefined),
|
|
266
217
|
};
|
|
267
218
|
}
|
|
268
219
|
|
|
@@ -293,23 +244,14 @@ export const defaultInputGroupStyles = {
|
|
|
293
244
|
backgroundColor: formInputColors.background.base,
|
|
294
245
|
|
|
295
246
|
minWidth: 0,
|
|
296
|
-
...
|
|
247
|
+
...FIELD_FOCUS_REST,
|
|
297
248
|
|
|
298
249
|
hoverStyle: {
|
|
299
250
|
borderColor: formInputColors.border.hover,
|
|
300
251
|
},
|
|
301
252
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
// button-like Boxes (Select, pagination) do not ring on mouse click.
|
|
305
|
-
focusStyle: {
|
|
306
|
-
borderColor: formInputColors.border.focus,
|
|
307
|
-
},
|
|
308
|
-
|
|
309
|
-
focusVisibleStyle: {
|
|
310
|
-
borderColor: formInputColors.border.focus,
|
|
311
|
-
...inputOutlineFocus,
|
|
312
|
-
},
|
|
253
|
+
focusStyle: FIELD_FOCUS_STYLE,
|
|
254
|
+
focusVisibleStyle: FIELD_FOCUS_STYLE,
|
|
313
255
|
} as const;
|
|
314
256
|
|
|
315
257
|
// ---------------------------------------------------------------------------
|
|
@@ -325,9 +267,6 @@ const InputGroupFrame = styled(XGroup, {
|
|
|
325
267
|
alignItems: "center",
|
|
326
268
|
context: InputContext,
|
|
327
269
|
overflow: "hidden",
|
|
328
|
-
// LC-71: className on XGroup is best-effort (often dropped). The styleable
|
|
329
|
-
// spread stamps data-mp-input-box / data-mp-ring; CSS keys off those.
|
|
330
|
-
...(isWeb ? { className: "mp-composite-ring" } : {}),
|
|
331
270
|
|
|
332
271
|
variants: {
|
|
333
272
|
touch: { true: {}, false: {} },
|
|
@@ -376,11 +315,9 @@ function wrapNativePressable(
|
|
|
376
315
|
|
|
377
316
|
const InputGroupImpl = InputGroupFrame.styleable<{
|
|
378
317
|
/**
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
* sits outside the border box (outline + offset), so a covering panel can
|
|
383
|
-
* never hide it — it ghosts around the panel's corners as a doubled edge.
|
|
318
|
+
* Kept for callers. Since FZ-7717 the field focus look is the Box's own
|
|
319
|
+
* accent border, which a covering panel hides, so there is no ring left to
|
|
320
|
+
* suppress.
|
|
384
321
|
*/
|
|
385
322
|
suppressFocusRing?: boolean;
|
|
386
323
|
/**
|
|
@@ -399,7 +336,7 @@ const InputGroupImpl = InputGroupFrame.styleable<{
|
|
|
399
336
|
children,
|
|
400
337
|
onPress,
|
|
401
338
|
disabled,
|
|
402
|
-
suppressFocusRing,
|
|
339
|
+
suppressFocusRing: _suppressFocusRing,
|
|
403
340
|
recipeFamily = "control",
|
|
404
341
|
sizeRecipeEscape,
|
|
405
342
|
height: heightProp,
|
|
@@ -418,27 +355,21 @@ const InputGroupImpl = InputGroupFrame.styleable<{
|
|
|
418
355
|
const inTableCell = useIsInTableCell();
|
|
419
356
|
// Native Pressable owns onPress; do not also pass it to XGroup (double-fire).
|
|
420
357
|
const framePressProps = isWeb ? { onPress, disabled } : { disabled };
|
|
421
|
-
// Standalone Box clips children to radius
|
|
422
|
-
//
|
|
358
|
+
// Standalone Box clips children to radius. Grouped / in-cell frames leave
|
|
359
|
+
// overflow visible and leave focus to their container.
|
|
423
360
|
// LC-85: on native, an elevation token must not share a clipsToBounds layer
|
|
424
|
-
// with the shadow (iOS masks the layer's own drop shadow).
|
|
425
|
-
|
|
426
|
-
const paintsRing = !grouped && !inTableCell;
|
|
361
|
+
// with the shadow (iOS masks the layer's own drop shadow).
|
|
362
|
+
const paintsFocus = !grouped && !inTableCell;
|
|
427
363
|
const hasElevation = knobProps.elevation != null;
|
|
428
364
|
const hasWebPressSlop =
|
|
429
365
|
isWeb && (props as { "data-mp-press-slop"?: string })["data-mp-press-slop"] != null;
|
|
430
366
|
const boxOverflow: BoxOverflow =
|
|
431
|
-
|
|
432
|
-
const ringMode = boxRingMode(boxOverflow, paintsRing);
|
|
367
|
+
paintsFocus && !(hasElevation && !isWeb) && !hasWebPressSlop ? "hidden" : "visible";
|
|
433
368
|
if (isWeb) {
|
|
434
369
|
ensureCompositeFocusRing();
|
|
435
370
|
ensureInputPartsFocusRing();
|
|
436
371
|
}
|
|
437
|
-
const boxRingAttrs = webBoxRingAttrs(
|
|
438
|
-
boxOverflow,
|
|
439
|
-
paintsRing,
|
|
440
|
-
(props as { className?: string }).className,
|
|
441
|
-
);
|
|
372
|
+
const boxRingAttrs = webBoxRingAttrs(paintsFocus, (props as { className?: string }).className);
|
|
442
373
|
|
|
443
374
|
// DG-A11Y-01 / DG-A11Y-03 — DEV guardrails (tree-shaken in production).
|
|
444
375
|
warnPositiveTabIndex({
|
|
@@ -578,29 +509,6 @@ const InputGroupImpl = InputGroupFrame.styleable<{
|
|
|
578
509
|
);
|
|
579
510
|
}
|
|
580
511
|
|
|
581
|
-
// Covered by an overlay panel (see prop doc): keep the focus border color
|
|
582
|
-
// but draw no ring on either focus path. This is an intentional a11y
|
|
583
|
-
// exception — the open panel is the focus affordance while it covers this
|
|
584
|
-
// box. Hoisted (with `undefined` in the union) so the JSX spread does not
|
|
585
|
-
// re-specify focusStyle/focusVisibleStyle literally (TS2783 under the
|
|
586
|
-
// build tsconfig).
|
|
587
|
-
const suppressedRingProps = suppressFocusRing
|
|
588
|
-
? {
|
|
589
|
-
focusStyle: { borderColor: formInputColors.border.focus, ...inputOutlineRest },
|
|
590
|
-
focusVisibleStyle: {
|
|
591
|
-
borderColor: formInputColors.border.focus,
|
|
592
|
-
...inputOutlineRest,
|
|
593
|
-
},
|
|
594
|
-
}
|
|
595
|
-
: undefined;
|
|
596
|
-
|
|
597
|
-
const focusRing = ensureFocusVisibleRing({
|
|
598
|
-
...inputOutlineFocus,
|
|
599
|
-
...control.focusKnobProps,
|
|
600
|
-
...control.focusVisibleKnobProps,
|
|
601
|
-
outlineOffset: boxRingOffset(ringMode),
|
|
602
|
-
});
|
|
603
|
-
|
|
604
512
|
return (
|
|
605
513
|
<FocusContext.Provider focused={focused} setFocused={setFocused}>
|
|
606
514
|
{wrapNativePressable(
|
|
@@ -624,45 +532,15 @@ const InputGroupImpl = InputGroupFrame.styleable<{
|
|
|
624
532
|
pressStyle={{
|
|
625
533
|
...control.pressKnobProps,
|
|
626
534
|
}}
|
|
627
|
-
focusStyle={
|
|
628
|
-
|
|
629
|
-
...control.focusKnobProps,
|
|
630
|
-
}}
|
|
631
|
-
focusVisibleStyle={ensureFocusVisibleRing({
|
|
632
|
-
borderColor: formInputColors.border.focus,
|
|
633
|
-
...inputOutlineFocus,
|
|
634
|
-
...control.focusVisibleKnobProps,
|
|
635
|
-
outlineOffset: boxRingOffset(ringMode),
|
|
636
|
-
})}
|
|
535
|
+
focusStyle={FIELD_FOCUS_STYLE}
|
|
536
|
+
focusVisibleStyle={FIELD_FOCUS_STYLE}
|
|
637
537
|
{...props}
|
|
638
538
|
{...passthroughSizeProps}
|
|
639
539
|
{...insetPadProps}
|
|
640
540
|
{...sizeRecipeEscapeOverride}
|
|
641
541
|
{...framePressProps}
|
|
642
542
|
{...boxRingAttrs}
|
|
643
|
-
{...(focused
|
|
644
|
-
? {
|
|
645
|
-
// Text-entry carve-out (LC-71): inner Area focus means typing
|
|
646
|
-
// is next, so the Box rings on any focus — including click.
|
|
647
|
-
// CSS `[data-mp-ring]` is the sync path; this is the token-
|
|
648
|
-
// resolved backup (and the native path). Button-like Boxes
|
|
649
|
-
// (Select) do not set `focused` (no Area onFocus).
|
|
650
|
-
borderColor: formInputColors.border.focus,
|
|
651
|
-
...focusRing,
|
|
652
|
-
}
|
|
653
|
-
: isWeb
|
|
654
|
-
? {
|
|
655
|
-
// Explicit rest — avoid leaving theme `$outlineColor` (near-white in
|
|
656
|
-
// dark) as the computed outline-color while width settles.
|
|
657
|
-
// Keep outline-transparent rest (border-flicker fix); do not warn.
|
|
658
|
-
...inputOutlineRest,
|
|
659
|
-
}
|
|
660
|
-
: // FZ-7718: on iOS the outline's width and offset changing as
|
|
661
|
-
// the field takes focus makes it resign first responder, so a
|
|
662
|
-
// programmatic focus (Return to the next field) never sticks.
|
|
663
|
-
// Native keeps the ring's geometry at rest and fades its color.
|
|
664
|
-
{ ...focusRing, outlineColor: "transparent" })}
|
|
665
|
-
{...suppressedRingProps}
|
|
543
|
+
{...(focused ? FIELD_FOCUS_STYLE : FIELD_FOCUS_REST)}
|
|
666
544
|
// LC-40 DISABLED-VISIBLE: the input frame is text-bearing chrome —
|
|
667
545
|
// keepLabel washes fill/border (value text keeps >= AA) and
|
|
668
546
|
// dimWhole leaves the dim to the FieldLayout assembly. Read-only
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { inspectDesign } from "@multiplatform.one/config/design-lint";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
|
|
6
|
+
const src = join(process.cwd(), "src");
|
|
7
|
+
|
|
8
|
+
function sources(dir: string): string[] {
|
|
9
|
+
return readdirSync(dir).flatMap((entry) => {
|
|
10
|
+
const path = join(dir, entry);
|
|
11
|
+
if (statSync(path).isDirectory()) return sources(path);
|
|
12
|
+
return /\.tsx?$/.test(entry) && !/\.(spec|stories|test)\./.test(entry) ? [path] : [];
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("FZ-7717: every MPO field takes its focus look from FIELD_FOCUS_STYLE", () => {
|
|
17
|
+
const files = [...sources(join(src, "fields")), ...sources(join(src, "InputParts"))];
|
|
18
|
+
|
|
19
|
+
it("no field paints its own focus ring or shadow", () => {
|
|
20
|
+
const hits = files.flatMap((file) =>
|
|
21
|
+
(inspectDesign(readFileSync(file, "utf8"), file) as string[]).filter((error) =>
|
|
22
|
+
error.includes("[field-focus]"),
|
|
23
|
+
),
|
|
24
|
+
);
|
|
25
|
+
expect(hits).toEqual([]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("Input.Box, the Box every text field renders, uses the shared look", () => {
|
|
29
|
+
const box = readFileSync(join(src, "InputParts", "index.tsx"), "utf8");
|
|
30
|
+
expect(box).toContain("focusStyle={FIELD_FOCUS_STYLE}");
|
|
31
|
+
expect(box).toContain("focusVisibleStyle={FIELD_FOCUS_STYLE}");
|
|
32
|
+
expect(box).not.toMatch(
|
|
33
|
+
/outlineOffset|FOCUS_RING_(?:HALO|CLIPPED)_OFFSET|ensureFocusVisibleRing/,
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("the detector catches a field that forks the look", () => {
|
|
38
|
+
const forked = inspectDesign(
|
|
39
|
+
"const x = <Input.Box focusStyle={{ outlineWidth: 2, outlineOffset: 2 }} />;",
|
|
40
|
+
"Forked.tsx",
|
|
41
|
+
) as string[];
|
|
42
|
+
expect(forked.join("\n")).toMatch(/\[field-focus\]/);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -51,7 +51,7 @@ describe("DatePicker", () => {
|
|
|
51
51
|
const label = result.container.querySelector("label") as HTMLLabelElement;
|
|
52
52
|
expect(trigger).toBeTruthy();
|
|
53
53
|
expect(trigger.contains(input)).toBe(true);
|
|
54
|
-
expect(trigger.
|
|
54
|
+
expect(trigger.getAttribute("data-mp-field-focus")).toBe("true");
|
|
55
55
|
expect(trigger.getAttribute("tabindex")).toBe("0");
|
|
56
56
|
expect(trigger.getAttribute("role")).toBe("combobox");
|
|
57
57
|
expect(input.tabIndex).toBe(-1);
|
|
@@ -197,7 +197,7 @@ describe("DateRangePicker", () => {
|
|
|
197
197
|
const result = renderWithProviders(<DateRangePicker label="Range" name="range" />);
|
|
198
198
|
const input = result.container.querySelector("input");
|
|
199
199
|
expect(input?.className).toMatch(/mp-input-area/);
|
|
200
|
-
const box = result.container.querySelector("
|
|
200
|
+
const box = result.container.querySelector("[data-mp-field-focus]");
|
|
201
201
|
expect(box).toBeTruthy();
|
|
202
202
|
expect(box?.contains(input)).toBe(true);
|
|
203
203
|
});
|
|
@@ -162,7 +162,7 @@ describe("MultiDatePicker", () => {
|
|
|
162
162
|
const result = renderWithProviders(<MultiDatePicker label="Dates" name="dates" />);
|
|
163
163
|
const input = result.container.querySelector("input");
|
|
164
164
|
expect(input?.className).toMatch(/mp-input-area/);
|
|
165
|
-
const box = result.container.querySelector("
|
|
165
|
+
const box = result.container.querySelector("[data-mp-field-focus]");
|
|
166
166
|
expect(box).toBeTruthy();
|
|
167
167
|
expect(box?.contains(input)).toBe(true);
|
|
168
168
|
});
|
|
@@ -207,12 +207,12 @@ describe("Duration", () => {
|
|
|
207
207
|
expect(sSize).not.toBe(lSize);
|
|
208
208
|
});
|
|
209
209
|
|
|
210
|
-
it("
|
|
210
|
+
it("focuses the outer box, never the inner segments (FZ-7717)", () => {
|
|
211
211
|
const result = renderWithProviders(<Duration label="Ring" value={3600} />);
|
|
212
212
|
const box = result.container.querySelector("[data-ring-target='box']") as HTMLElement;
|
|
213
213
|
const hours = result.container.querySelector('input[aria-label="Hours"]') as HTMLInputElement;
|
|
214
214
|
expect(box).toBeTruthy();
|
|
215
|
-
expect(box.
|
|
215
|
+
expect(box.getAttribute("data-mp-field-focus")).toBe("true");
|
|
216
216
|
expect(box.contains(hours)).toBe(true);
|
|
217
217
|
expect(hours.className).toMatch(/mp-input-area/);
|
|
218
218
|
});
|
|
@@ -30,7 +30,7 @@ describe("Input copy (CopyField slot)", () => {
|
|
|
30
30
|
(el) => el !== input && el !== copy,
|
|
31
31
|
);
|
|
32
32
|
expect(extraStops).toHaveLength(0);
|
|
33
|
-
expect(result.container.querySelector("
|
|
33
|
+
expect(result.container.querySelector("[data-mp-field-focus]")).toBeTruthy();
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
it("empty omits the cap so there is one tab stop", () => {
|
|
@@ -250,7 +250,7 @@ describe("Input", () => {
|
|
|
250
250
|
});
|
|
251
251
|
|
|
252
252
|
describe("LC-71 RING-ANATOMY", () => {
|
|
253
|
-
it("
|
|
253
|
+
it("focuses the Box (icons included), never the inner input", () => {
|
|
254
254
|
const result = renderWithProviders(
|
|
255
255
|
<Input
|
|
256
256
|
label="Search"
|
|
@@ -262,7 +262,7 @@ describe("Input", () => {
|
|
|
262
262
|
const box = result.container.querySelector("[data-ring-target='box']") as HTMLElement;
|
|
263
263
|
const input = result.container.querySelector("input") as HTMLInputElement;
|
|
264
264
|
expect(box).toBeTruthy();
|
|
265
|
-
expect(box.
|
|
265
|
+
expect(box.getAttribute("data-mp-field-focus")).toBe("true");
|
|
266
266
|
expect(box.contains(result.getByTestId("leading"))).toBe(true);
|
|
267
267
|
expect(box.contains(result.getByTestId("trailing"))).toBe(true);
|
|
268
268
|
expect(box.contains(input)).toBe(true);
|
|
@@ -282,7 +282,7 @@ describe("Input", () => {
|
|
|
282
282
|
);
|
|
283
283
|
const box = result.container.querySelector("[data-ring-target='box']") as HTMLElement;
|
|
284
284
|
const input = result.container.querySelector("input") as HTMLInputElement;
|
|
285
|
-
expect(box.
|
|
285
|
+
expect(box.getAttribute("data-mp-field-focus")).toBe("true");
|
|
286
286
|
expect(box.contains(result.getByTestId("leading"))).toBe(true);
|
|
287
287
|
expect(input.className).toMatch(/mp-input-area/);
|
|
288
288
|
});
|
|
@@ -451,13 +451,12 @@ describe("Input", () => {
|
|
|
451
451
|
expect(box.className).toContain("_h-48px");
|
|
452
452
|
});
|
|
453
453
|
|
|
454
|
-
it("focus
|
|
454
|
+
it("focus is the shared accent border, not an offset ring (FZ-7717)", () => {
|
|
455
455
|
const result = renderWithProviders(<Input label="Name" name="n" />);
|
|
456
456
|
const box = result.container.querySelector("[data-ring-target='box']") as HTMLElement;
|
|
457
|
-
expect(box.getAttribute("data-
|
|
458
|
-
expect(
|
|
459
|
-
|
|
460
|
-
);
|
|
457
|
+
expect(box.getAttribute("data-mp-field-focus")).toBe("true");
|
|
458
|
+
expect(box.getAttribute("data-ring-offset")).toBeNull();
|
|
459
|
+
expect(document.getElementById("mp-input-field-ring-offset")).toBeNull();
|
|
461
460
|
});
|
|
462
461
|
|
|
463
462
|
it("paints the field label at weight 400 (R7 / §6.1)", () => {
|
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
SizeTokens,
|
|
12
12
|
TamaguiElement,
|
|
13
13
|
} from "tamagui";
|
|
14
|
-
import {
|
|
14
|
+
import { Text } from "tamagui";
|
|
15
15
|
import { Skeleton } from "../../Skeleton";
|
|
16
16
|
import { resolveFieldInputProps } from "../../fieldDefaults";
|
|
17
17
|
import { Field, FieldLayout } from "../../fieldLayout";
|
|
@@ -31,23 +31,6 @@ import { useKeyboardChain } from "../../keyboard";
|
|
|
31
31
|
|
|
32
32
|
type InputAreaProps = ComponentProps<typeof InputParts.Area>;
|
|
33
33
|
|
|
34
|
-
/** F1 / reference §10: standalone Input rings at offset 0, not the inset −2. */
|
|
35
|
-
const INPUT_FIELD_RING_STYLE_ID = "mp-input-field-ring-offset";
|
|
36
|
-
|
|
37
|
-
export function ensureInputFieldRingOffset(): void {
|
|
38
|
-
if (typeof document === "undefined") return;
|
|
39
|
-
if (document.getElementById(INPUT_FIELD_RING_STYLE_ID)) return;
|
|
40
|
-
const tag = document.createElement("style");
|
|
41
|
-
tag.id = INPUT_FIELD_RING_STYLE_ID;
|
|
42
|
-
tag.textContent = `[data-mp-input-field][data-mp-ring="inset"]:has(input:focus),
|
|
43
|
-
[data-mp-input-field][data-mp-ring="inset"]:has(textarea:focus),
|
|
44
|
-
[data-mp-input-field][data-mp-ring="inset"]:has([data-mp-input-area]:focus),
|
|
45
|
-
[data-mp-input-field][data-mp-ring="inset"]:focus-visible {
|
|
46
|
-
outline-offset: 0px !important;
|
|
47
|
-
}`;
|
|
48
|
-
document.head.appendChild(tag);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
34
|
export interface InputProps {
|
|
52
35
|
name?: string;
|
|
53
36
|
form?: AnyFormApi;
|
|
@@ -165,7 +148,6 @@ export function Input({
|
|
|
165
148
|
});
|
|
166
149
|
const inputRef = useRef<TamaguiElement>(null);
|
|
167
150
|
const focusTrigger = useForwardFocus(inputRef);
|
|
168
|
-
if (isWeb) ensureInputFieldRingOffset();
|
|
169
151
|
|
|
170
152
|
const resolvedValidators = useResolvedValidators(required, validators, label, name);
|
|
171
153
|
|
|
@@ -199,7 +181,6 @@ export function Input({
|
|
|
199
181
|
"data-ring-target": "box",
|
|
200
182
|
"data-radius-class": "DEFAULT",
|
|
201
183
|
"data-radius-part": "Input",
|
|
202
|
-
"data-ring-offset": "0",
|
|
203
184
|
"data-mp-input-field": "true",
|
|
204
185
|
...(belowFloor ? { hitSlop: pressTargetHitSlop(visualHeight) } : undefined),
|
|
205
186
|
} as const;
|