@multiplatform.one/forms 7.73.0 → 7.75.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/Button/IconButtonGroup.cjs +32 -0
- package/dist/cjs/Button/IconButtonGroup.native.js +32 -0
- package/dist/cjs/Button/IconButtonGroup.native.js.map +1 -1
- package/dist/cjs/Button/glass.cjs +4 -1
- package/dist/cjs/Button/index.cjs +71 -18
- package/dist/cjs/Button/index.native.js +81 -20
- package/dist/cjs/Button/index.native.js.map +1 -1
- package/dist/cjs/Button/webGlass.cjs +62 -0
- package/dist/cjs/Button/webGlass.native.js +68 -0
- package/dist/cjs/Button/webGlass.native.js.map +1 -0
- package/dist/cjs/fields/FileUpload/index.cjs +8 -3
- package/dist/cjs/fields/FileUpload/index.native.js +8 -3
- package/dist/cjs/fields/FileUpload/index.native.js.map +1 -1
- package/dist/esm/AdaptivePopup/index.native.js.map +1 -1
- package/dist/esm/Button/IconButtonGroup.mjs +32 -0
- package/dist/esm/Button/IconButtonGroup.mjs.map +1 -1
- package/dist/esm/Button/IconButtonGroup.native.js +32 -0
- package/dist/esm/Button/IconButtonGroup.native.js.map +1 -1
- package/dist/esm/Button/circleGlyph.native.js.map +1 -1
- package/dist/esm/Button/glass.mjs +4 -2
- package/dist/esm/Button/glass.mjs.map +1 -1
- package/dist/esm/Button/iconButtonGroupContext.mjs.map +1 -1
- package/dist/esm/Button/iconButtonGroupContext.native.js.map +1 -1
- package/dist/esm/Button/index.mjs +72 -20
- package/dist/esm/Button/index.mjs.map +1 -1
- package/dist/esm/Button/index.native.js +82 -22
- package/dist/esm/Button/index.native.js.map +1 -1
- package/dist/esm/Button/mediaPlate.native.js.map +1 -1
- package/dist/esm/Button/webGlass.mjs +36 -0
- package/dist/esm/Button/webGlass.mjs.map +1 -0
- package/dist/esm/Button/webGlass.native.js +39 -0
- package/dist/esm/Button/webGlass.native.js.map +1 -0
- package/dist/esm/Chip/index.native.js.map +1 -1
- package/dist/esm/ContextualSaveBar/UnsavedChangesDialog.native.js.map +1 -1
- package/dist/esm/ContextualSaveBar/clientKnownIdentity.native.js.map +1 -1
- package/dist/esm/ContextualSaveBar/commitImmediateField.native.js.map +1 -1
- package/dist/esm/ContextualSaveBar/index.native.js.map +1 -1
- package/dist/esm/ContextualSaveBar/useUnsavedChangesGuard.native.js.map +1 -1
- package/dist/esm/ControlGroup/index.native.js.map +1 -1
- package/dist/esm/DescriptionList/index.native.js.map +1 -1
- package/dist/esm/ErrorSummary.native.js.map +1 -1
- package/dist/esm/FieldDisplay/editMode.native.js.map +1 -1
- package/dist/esm/FieldDisplay/formatters.native.js.map +1 -1
- package/dist/esm/FieldDisplay/index.native.js.map +1 -1
- package/dist/esm/FieldGroup.native.js.map +1 -1
- package/dist/esm/FloatingPanel/PanelHeader.native.js.map +1 -1
- package/dist/esm/FloatingPanel/RegularPanel.native.js.map +1 -1
- package/dist/esm/FloatingPanel/compactFace.native.js.map +1 -1
- package/dist/esm/FloatingPanel/containingBlock.native.js.map +1 -1
- package/dist/esm/FloatingPanel/echoContext.native.js.map +1 -1
- package/dist/esm/FloatingPanel/iconEcho.native.js.map +1 -1
- package/dist/esm/FloatingPanel/nestedAction.native.js.map +1 -1
- package/dist/esm/FloatingPanel/paintedBox.native.js.map +1 -1
- package/dist/esm/FloatingPanel/panelWidth.native.js.map +1 -1
- package/dist/esm/FloatingPanel/pointAnchor.native.js.map +1 -1
- package/dist/esm/FloatingPanel/regularPlacement.native.js.map +1 -1
- package/dist/esm/FloatingPanel/trailingAction.native.js.map +1 -1
- package/dist/esm/FloatingPanel/useFloatingPanel.native.js.map +1 -1
- package/dist/esm/Form/index.native.js.map +1 -1
- package/dist/esm/Form/submissionController.native.js.map +1 -1
- package/dist/esm/Form/validateOn.native.js.map +1 -1
- package/dist/esm/FormGrid.native.js.map +1 -1
- package/dist/esm/FormSection.native.js.map +1 -1
- package/dist/esm/InputParts/PasswordReveal.native.js.map +1 -1
- package/dist/esm/InputParts/index.native.js.map +1 -1
- package/dist/esm/Label/index.native.js.map +1 -1
- package/dist/esm/MediaPicker/base64.native.js.map +1 -1
- package/dist/esm/MediaPicker/payload.native.js.map +1 -1
- package/dist/esm/MediaPicker/types.native.js.map +1 -1
- package/dist/esm/MediaPicker/useMediaPicker.native.js.map +1 -1
- package/dist/esm/Skeleton/index.native.js.map +1 -1
- package/dist/esm/Spinner/SpinnerGlyph.native.js.map +1 -1
- package/dist/esm/Spinner/spinnerSize.native.js.map +1 -1
- package/dist/esm/Table/index.native.js.map +1 -1
- package/dist/esm/Wheel/haptic.native.js.map +1 -1
- package/dist/esm/Wheel/index.native.js.map +1 -1
- package/dist/esm/Wheel/wheelShared.native.js.map +1 -1
- package/dist/esm/actionLabel.native.js.map +1 -1
- package/dist/esm/fieldDefaults.native.js.map +1 -1
- package/dist/esm/fieldLayout.native.js.map +1 -1
- package/dist/esm/fields/Barcode/BarcodePreview.native.js.map +1 -1
- package/dist/esm/fields/Barcode/code128.native.js.map +1 -1
- package/dist/esm/fields/Barcode/index.native.js.map +1 -1
- package/dist/esm/fields/Barcode/qr.native.js.map +1 -1
- package/dist/esm/fields/Barcode/scannerTypes.native.js.map +1 -1
- package/dist/esm/fields/Checkbox/Checkbox.native.js.map +1 -1
- package/dist/esm/fields/Checkbox/CheckboxBox.native.js.map +1 -1
- package/dist/esm/fields/Checkbox/CheckboxCard.native.js.map +1 -1
- package/dist/esm/fields/Checkbox/CheckboxFieldFrame.native.js.map +1 -1
- package/dist/esm/fields/Checkbox/CheckboxGroup.native.js.map +1 -1
- package/dist/esm/fields/Checkbox/checkboxGroupContext.native.js.map +1 -1
- package/dist/esm/fields/Checkbox/index.native.js.map +1 -1
- package/dist/esm/fields/ColorPicker/SystemColorField.native.js.map +1 -1
- package/dist/esm/fields/ColorPicker/index.native.js.map +1 -1
- package/dist/esm/fields/ColorPicker/parts.native.js.map +1 -1
- package/dist/esm/fields/ColorPicker/systemColorWell.gnome.native.js.map +1 -1
- package/dist/esm/fields/Combobox/authorOptions.native.js.map +1 -1
- package/dist/esm/fields/Combobox/index.native.js.map +1 -1
- package/dist/esm/fields/DatePicker/Calendar.native.js.map +1 -1
- package/dist/esm/fields/DatePicker/DateRangePicker.native.js.map +1 -1
- package/dist/esm/fields/DatePicker/DatetimePicker.catalog.native.js.map +1 -1
- package/dist/esm/fields/DatePicker/MonthPicker.native.js.map +1 -1
- package/dist/esm/fields/DatePicker/MultiDatePicker.native.js.map +1 -1
- package/dist/esm/fields/DatePicker/catalog.native.js.map +1 -1
- package/dist/esm/fields/DatePicker/fieldValueSync.native.js.map +1 -1
- package/dist/esm/fields/DatePicker/parts.native.js.map +1 -1
- package/dist/esm/fields/DatePicker/utils.native.js.map +1 -1
- package/dist/esm/fields/Duration/durationMath.native.js.map +1 -1
- package/dist/esm/fields/Duration/index.native.js.map +1 -1
- package/dist/esm/fields/FileUpload/filePickerTypes.native.js.map +1 -1
- package/dist/esm/fields/FileUpload/index.mjs +8 -3
- package/dist/esm/fields/FileUpload/index.mjs.map +1 -1
- package/dist/esm/fields/FileUpload/index.native.js +8 -3
- package/dist/esm/fields/FileUpload/index.native.js.map +1 -1
- package/dist/esm/fields/FileUpload/nativePickerLoader.native.js.map +1 -1
- package/dist/esm/fields/Geolocation/expoMapsLoader.native.js.map +1 -1
- package/dist/esm/fields/Geolocation/geoValue.native.js.map +1 -1
- package/dist/esm/fields/Geolocation/geocoder.native.js.map +1 -1
- package/dist/esm/fields/Geolocation/geocoderContext.native.js.map +1 -1
- package/dist/esm/fields/Geolocation/index.native.js.map +1 -1
- package/dist/esm/fields/Geolocation/mapMath.native.js.map +1 -1
- package/dist/esm/fields/Geolocation/mapTypes.native.js.map +1 -1
- package/dist/esm/fields/Geolocation/nativeSearchFocus.native.js.map +1 -1
- package/dist/esm/fields/Geolocation/photonGeocoder.native.js.map +1 -1
- package/dist/esm/fields/Icon/drawnSet.native.js.map +1 -1
- package/dist/esm/fields/Icon/index.native.js.map +1 -1
- package/dist/esm/fields/ImageField/index.native.js.map +1 -1
- package/dist/esm/fields/Input/index.native.js.map +1 -1
- package/dist/esm/fields/MentionInput/index.native.js.map +1 -1
- package/dist/esm/fields/OTPInput/index.native.js.map +1 -1
- package/dist/esm/fields/OTPInput/otpCellCss.native.js.map +1 -1
- package/dist/esm/fields/PhoneInput/dialCodes.native.js.map +1 -1
- package/dist/esm/fields/PhoneInput/index.native.js.map +1 -1
- package/dist/esm/fields/PhoneInput/phone.native.js.map +1 -1
- package/dist/esm/fields/PhoneInput/regionNames.native.js.map +1 -1
- package/dist/esm/fields/Progress/index.native.js.map +1 -1
- package/dist/esm/fields/RadioGroup/index.native.js.map +1 -1
- package/dist/esm/fields/Rating/index.native.js.map +1 -1
- package/dist/esm/fields/Rating/ratingMath.native.js.map +1 -1
- package/dist/esm/fields/Rating/types.native.js.map +1 -1
- package/dist/esm/fields/SearchInput/index.native.js.map +1 -1
- package/dist/esm/fields/Select/InlineChoices.native.js.map +1 -1
- package/dist/esm/fields/Select/SheetPickerRow.native.js.map +1 -1
- package/dist/esm/fields/Select/index.native.js.map +1 -1
- package/dist/esm/fields/Select/shouldInlineChoices.native.js.map +1 -1
- package/dist/esm/fields/Signature/index.native.js.map +1 -1
- package/dist/esm/fields/Slider/index.native.js.map +1 -1
- package/dist/esm/fields/Slider/jumpMotion.native.js.map +1 -1
- package/dist/esm/fields/Stepper/index.native.js.map +1 -1
- package/dist/esm/fields/Stepper/resolveStepperPlacement.native.js.map +1 -1
- package/dist/esm/fields/Switch/footprint.native.js.map +1 -1
- package/dist/esm/fields/Switch/index.native.js.map +1 -1
- package/dist/esm/fields/TextArea/index.native.js.map +1 -1
- package/dist/esm/fields/TimePicker/catalog.native.js.map +1 -1
- package/dist/esm/fields/TimePicker/parts.native.js.map +1 -1
- package/dist/esm/fields/TimeRangeScrubber/geometry.native.js.map +1 -1
- package/dist/esm/fields/TimeRangeScrubber/index.native.js.map +1 -1
- package/dist/esm/fields/ToggleGroup/index.native.js.map +1 -1
- package/dist/esm/fields/nativeDateTime/OsDateTimeSheet.native.js.map +1 -1
- package/dist/esm/fields/nativeDateTime/dateTimePickerLoader.native.js.map +1 -1
- package/dist/esm/fields/nativeDateTime/osDateTimeIosDisplay.native.js.map +1 -1
- package/dist/esm/fields/nativeDateTime/sheetKeyboard.native.js.map +1 -1
- package/dist/esm/fields/nativeSelect.native.js.map +1 -1
- package/dist/esm/fields/selectedOrder.native.js.map +1 -1
- package/dist/esm/formSpacing.native.js.map +1 -1
- package/dist/esm/hooks/useFocusManagement.native.js.map +1 -1
- package/dist/esm/index.native.js.map +1 -1
- package/dist/esm/keyboard/chain.native.js.map +1 -1
- package/dist/esm/keyboard/index.native.js.map +1 -1
- package/dist/esm/keyboard/types.native.js.map +1 -1
- package/dist/esm/requiredMarking.native.js.map +1 -1
- package/dist/esm/shared/TimeColumn.native.js.map +1 -1
- package/dist/esm/shared/capabilityPreflight.native.js.map +1 -1
- package/dist/esm/shared/choiceRowPress.native.js.map +1 -1
- package/dist/esm/shared/colorRamps.native.js.map +1 -1
- package/dist/esm/shared/devWarn.native.js.map +1 -1
- package/dist/esm/shared/floatingList.native.js.map +1 -1
- package/dist/esm/shared/focusManagement.native.js.map +1 -1
- package/dist/esm/shared/groupContext.native.js.map +1 -1
- package/dist/esm/shared/i18n.native.js.map +1 -1
- package/dist/esm/shared/instantMotion.native.js.map +1 -1
- package/dist/esm/shared/nativeA11y.native.js.map +1 -1
- package/dist/esm/shared/nativeAutoFocus.native.js.map +1 -1
- package/dist/esm/shared/overlaySizeClass.native.js.map +1 -1
- package/dist/esm/shared/overlayStack.native.js.map +1 -1
- package/dist/esm/shared/panelPortal.native.js.map +1 -1
- package/dist/esm/shared/pickerTriggerContract.native.js.map +1 -1
- package/dist/esm/shared/pressSlopProps.native.js.map +1 -1
- package/dist/esm/shared/restoreRetries.native.js.map +1 -1
- package/dist/esm/shared/sheetNavigation.native.js.map +1 -1
- package/dist/esm/shared/systemControlModules.gnome.native.js.map +1 -1
- package/dist/esm/shared/systemControls.native.js.map +1 -1
- package/dist/esm/shared/t.native.js.map +1 -1
- package/dist/esm/shared/tableCellContext.native.js.map +1 -1
- package/dist/esm/shared/useDragScroll.native.js.map +1 -1
- package/dist/esm/shared/useKeyboardInset.native.js.map +1 -1
- package/dist/esm/shared/useSheetEscapeDismiss.native.js.map +1 -1
- package/dist/esm/shared/useWheelIncrement.native.js.map +1 -1
- package/dist/esm/shared/utils.native.js.map +1 -1
- package/dist/esm/shared/zIndex.native.js.map +1 -1
- package/dist/esm/standardActions.native.js.map +1 -1
- package/dist/esm/themedPrimitives.native.js.map +1 -1
- package/dist/esm/types.native.js.map +1 -1
- package/dist/esm/utils/LinkCell.native.js.map +1 -1
- package/dist/jsx/Button/IconButtonGroup.mjs +32 -0
- package/dist/jsx/Button/IconButtonGroup.mjs.map +1 -1
- package/dist/jsx/Button/IconButtonGroup.native.js +32 -0
- package/dist/jsx/Button/IconButtonGroup.native.js.map +1 -1
- package/dist/jsx/Button/glass.mjs +4 -2
- package/dist/jsx/Button/glass.mjs.map +1 -1
- package/dist/jsx/Button/iconButtonGroupContext.mjs.map +1 -1
- package/dist/jsx/Button/iconButtonGroupContext.native.js.map +1 -1
- package/dist/jsx/Button/index.mjs +72 -20
- package/dist/jsx/Button/index.mjs.map +1 -1
- package/dist/jsx/Button/index.native.js +81 -20
- package/dist/jsx/Button/index.native.js.map +1 -1
- package/dist/jsx/Button/webGlass.mjs +36 -0
- package/dist/jsx/Button/webGlass.mjs.map +1 -0
- package/dist/jsx/Button/webGlass.native.js +68 -0
- package/dist/jsx/Button/webGlass.native.js.map +1 -0
- package/dist/jsx/fields/FileUpload/index.mjs +8 -3
- package/dist/jsx/fields/FileUpload/index.mjs.map +1 -1
- package/dist/jsx/fields/FileUpload/index.native.js +8 -3
- package/dist/jsx/fields/FileUpload/index.native.js.map +1 -1
- package/package.json +8 -8
- package/src/Button/Button.accentInk.spec.tsx +39 -8
- package/src/Button/Button.confirmFill.spec.tsx +141 -0
- package/src/Button/Button.iconButton.spec.tsx +143 -3
- package/src/Button/Button.stories.tsx +55 -38
- package/src/Button/IconButtonGroup.spec.tsx +40 -0
- package/src/Button/IconButtonGroup.tsx +39 -0
- package/src/Button/glass.spec.ts +73 -0
- package/src/Button/glass.ts +8 -3
- package/src/Button/iconButtonGroupContext.ts +2 -1
- package/src/Button/index.tsx +111 -16
- package/src/Button/webGlass.ts +42 -0
- package/src/fields/FileUpload/FileUpload.spec.tsx +8 -0
- package/src/fields/FileUpload/index.tsx +12 -4
- package/types/Button/IconButtonGroup.d.ts.map +1 -1
- package/types/Button/glass.d.ts +5 -2
- package/types/Button/glass.d.ts.map +1 -1
- package/types/Button/iconButtonGroupContext.d.ts +2 -1
- package/types/Button/iconButtonGroupContext.d.ts.map +1 -1
- package/types/Button/index.d.ts +7 -0
- package/types/Button/index.d.ts.map +1 -1
- package/types/Button/webGlass.d.ts +32 -0
- package/types/Button/webGlass.d.ts.map +1 -0
- package/types/fields/FileUpload/index.d.ts.map +1 -1
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*
|
|
4
|
+
* FZ-8439: an overlay's one confirm is the accent ✓ (IconButton
|
|
5
|
+
* plate="primary"), never a black pill. Clay on the FZ-8116 deck
|
|
6
|
+
* (rv_0ea65e640a0ef261): "The cancel and Move button are not how we like to
|
|
7
|
+
* do buttons." His complaint was the word in a pill, not ink: a hued preset
|
|
8
|
+
* fills the ✓ circle with its accent, and the hue-less house preset fills it
|
|
9
|
+
* with its ink under an inverse check, iOS's mono style (rv_d2757722a7cb0992,
|
|
10
|
+
* "ink").
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { cleanup, render, screen } from "@testing-library/react";
|
|
14
|
+
import {
|
|
15
|
+
contrastRatio,
|
|
16
|
+
createDefaultThemeConfig,
|
|
17
|
+
normalizeToHex,
|
|
18
|
+
relativeLuminance,
|
|
19
|
+
} from "@multiplatform.one/theme";
|
|
20
|
+
import type { ReactElement } from "react";
|
|
21
|
+
import { TamaguiProvider, YStack } from "tamagui";
|
|
22
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
23
|
+
import { Button } from "./index";
|
|
24
|
+
|
|
25
|
+
afterEach(cleanup);
|
|
26
|
+
|
|
27
|
+
const houseConfig = createDefaultThemeConfig();
|
|
28
|
+
const SCHEMES = ["light", "dark"] as const;
|
|
29
|
+
|
|
30
|
+
function Check({ size, color }: { size?: number; color?: string }) {
|
|
31
|
+
return <svg data-glyph="" width={size} height={size} color={color} />;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function renderIn(ui: ReactElement, theme: string) {
|
|
35
|
+
return render(
|
|
36
|
+
<TamaguiProvider config={houseConfig.tamagui} defaultTheme={theme}>
|
|
37
|
+
<YStack backgroundColor="$background">{ui}</YStack>
|
|
38
|
+
</TamaguiProvider>,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function injectedRuleValue(className: string, property: string): string | null {
|
|
43
|
+
for (const sheet of Array.from(document.styleSheets)) {
|
|
44
|
+
for (const rule of Array.from(sheet.cssRules)) {
|
|
45
|
+
if (!(rule instanceof CSSStyleRule)) continue;
|
|
46
|
+
if (!rule.selectorText.endsWith(`.${className}`)) continue;
|
|
47
|
+
const value = rule.style.getPropertyValue(property);
|
|
48
|
+
if (value) return value.trim();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function resting(el: Element, prefix: string, property: string): string | null {
|
|
55
|
+
const cls = Array.from(el.classList).find(
|
|
56
|
+
(c) => c.startsWith(prefix) && !new RegExp(`^${prefix}0(hover|active|focus|press)`).test(c),
|
|
57
|
+
);
|
|
58
|
+
return cls ? injectedRuleValue(cls, property) : null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Tamagui paints `$token` values as CSS variables from the theme the Button
|
|
63
|
+
* wraps itself in; jsdom drops tamagui's theme stylesheet, so read the
|
|
64
|
+
* variable off that theme in the config, as the browser would.
|
|
65
|
+
*/
|
|
66
|
+
function resolvePaint(paint: string, themeName: string): string {
|
|
67
|
+
const variable = /^var\(--([^)]+)\)$/.exec(paint)?.[1];
|
|
68
|
+
if (!variable) return paint;
|
|
69
|
+
const themes = houseConfig.tamagui.themes as Record<string, Record<string, { val?: unknown }>>;
|
|
70
|
+
const theme = themes[themeName];
|
|
71
|
+
if (!theme) {
|
|
72
|
+
const near = Object.keys(themes).filter((name) => name.endsWith("_accent"));
|
|
73
|
+
throw new Error(`no theme ${themeName}; accent themes: ${near.slice(0, 12).join(", ")}`);
|
|
74
|
+
}
|
|
75
|
+
const value = theme[variable]?.val;
|
|
76
|
+
return typeof value === "string" ? resolvePaint(value, themeName) : paint;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function luminance(paint: string): number {
|
|
80
|
+
const hex = normalizeToHex(paint);
|
|
81
|
+
if (!hex) throw new Error(`unreadable paint ${paint}`);
|
|
82
|
+
return relativeLuminance(hex);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function confirmPaint(scheme: string) {
|
|
86
|
+
const confirm = screen.getByRole("button", { name: "Move" });
|
|
87
|
+
const fill = resting(confirm, "_bg-", "background-color");
|
|
88
|
+
expect(fill).toBeTruthy();
|
|
89
|
+
const glyph = confirm.querySelector("[data-glyph]")!;
|
|
90
|
+
const glyphColor = glyph.getAttribute("color");
|
|
91
|
+
const ink =
|
|
92
|
+
glyphColor && glyphColor !== "currentColor" ? glyphColor : resting(confirm, "_col-", "color");
|
|
93
|
+
expect(ink).toBeTruthy();
|
|
94
|
+
return {
|
|
95
|
+
confirm,
|
|
96
|
+
fill: luminance(resolvePaint(fill!, `${scheme}_accent`)),
|
|
97
|
+
check: luminance(resolvePaint(ink!, `${scheme}_accent`)),
|
|
98
|
+
dark: scheme.startsWith("dark"),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
describe("FZ-8439: the overlay confirm is a ✓ circle, never a word in a pill", () => {
|
|
103
|
+
for (const scheme of SCHEMES) {
|
|
104
|
+
it(`${scheme}: the confirm is an icon-only circle named by its action`, () => {
|
|
105
|
+
renderIn(<Button plate="primary" icon={Check} aria-label="Move" />, scheme);
|
|
106
|
+
const confirm = screen.getByRole("button", { name: "Move" });
|
|
107
|
+
expect(confirm.getAttribute("data-mp-icon-button")).toBe("primary");
|
|
108
|
+
expect(confirm.textContent).toBe("");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it(`${scheme}, hue-less house preset: an ink circle under an inverse check`, () => {
|
|
112
|
+
renderIn(<Button plate="primary" icon={Check} aria-label="Move" />, scheme);
|
|
113
|
+
const { fill, check, dark } = confirmPaint(scheme);
|
|
114
|
+
// Ink is the scheme's text pole: dark on a light page, light on a dark one.
|
|
115
|
+
if (dark) expect(fill).toBeGreaterThan(0.6);
|
|
116
|
+
else expect(fill).toBeLessThan(0.05);
|
|
117
|
+
expect(contrastRatio(check, fill)).toBeGreaterThanOrEqual(4.5);
|
|
118
|
+
// r3: white wherever it holds on the fill, so light mode's ✓ is white.
|
|
119
|
+
if (!dark) expect(check).toBeCloseTo(1, 5);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it(`${scheme}, hued preset: the circle takes the accent hue`, () => {
|
|
123
|
+
renderIn(<Button plate="primary" icon={Check} aria-label="Move" />, `${scheme}_blue`);
|
|
124
|
+
const { fill, check } = confirmPaint(`${scheme}_blue`);
|
|
125
|
+
expect(fill).toBeGreaterThan(0.05);
|
|
126
|
+
expect(fill).toBeLessThan(0.6);
|
|
127
|
+
expect(contrastRatio(check, fill)).toBeGreaterThanOrEqual(3);
|
|
128
|
+
// r3 (Clay): the ✓ on an accent fill is white, iOS's checkmark.
|
|
129
|
+
expect(check).toBeCloseTo(1, 5);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it(`${scheme}: the circle's rest fill is the accent as a literal, so native paints the browser's colour`, () => {
|
|
133
|
+
renderIn(<Button plate="primary" icon={Check} aria-label="Move" />, scheme);
|
|
134
|
+
const confirm = screen.getByRole("button", { name: "Move" });
|
|
135
|
+
const fill = resting(confirm, "_bg-", "background-color");
|
|
136
|
+
const accent = resolvePaint("var(--background)", `${scheme}_accent`);
|
|
137
|
+
expect(normalizeToHex(fill!)).toBe(normalizeToHex(accent));
|
|
138
|
+
expect(fill).not.toMatch(/^var\(/);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
});
|
|
@@ -14,16 +14,22 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { cleanup, render, screen } from "@testing-library/react";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
createDefaultThemeConfig,
|
|
19
|
+
GlyphTintFallbackProvider,
|
|
20
|
+
Surface,
|
|
21
|
+
} from "@multiplatform.one/theme";
|
|
18
22
|
import type { ReactElement } from "react";
|
|
19
23
|
import { TamaguiProvider, YStack } from "tamagui";
|
|
20
24
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
21
25
|
import {
|
|
22
26
|
Button,
|
|
27
|
+
GLYPH_BOLD_STROKE_UNITS,
|
|
23
28
|
ICON_BUTTON_CIRCLE_GLYPH_RATIO,
|
|
24
29
|
ICON_BUTTON_GLYPH_PX,
|
|
25
30
|
ICON_BUTTON_NEUTRAL_GLYPH_PX,
|
|
26
31
|
IconButton,
|
|
32
|
+
IconButtonGroup,
|
|
27
33
|
iconButtonGlyphPx,
|
|
28
34
|
labelLinePx,
|
|
29
35
|
} from "./index";
|
|
@@ -44,8 +50,27 @@ const houseConfig = createDefaultThemeConfig();
|
|
|
44
50
|
|
|
45
51
|
const SCHEMES = ["light", "dark"] as const;
|
|
46
52
|
|
|
47
|
-
function Glyph({
|
|
48
|
-
|
|
53
|
+
function Glyph({
|
|
54
|
+
size,
|
|
55
|
+
color,
|
|
56
|
+
weight,
|
|
57
|
+
strokeWidth,
|
|
58
|
+
}: {
|
|
59
|
+
size?: number;
|
|
60
|
+
color?: string;
|
|
61
|
+
weight?: string;
|
|
62
|
+
strokeWidth?: number;
|
|
63
|
+
}) {
|
|
64
|
+
return (
|
|
65
|
+
<svg
|
|
66
|
+
data-glyph=""
|
|
67
|
+
data-weight={weight}
|
|
68
|
+
data-stroke-width={strokeWidth}
|
|
69
|
+
width={size}
|
|
70
|
+
height={size}
|
|
71
|
+
color={color}
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
49
74
|
}
|
|
50
75
|
|
|
51
76
|
function glyphOf(label: string): SVGElement {
|
|
@@ -144,6 +169,25 @@ describe("LC-176: an icon-only Button is a bare glyph", () => {
|
|
|
144
169
|
},
|
|
145
170
|
);
|
|
146
171
|
|
|
172
|
+
it.each(SCHEMES)("%s: a bare glyph fills its box less 2pt, up to the row's glyph", (scheme) => {
|
|
173
|
+
// Round 4's critique: the SHC "+" at $3 drew a 19pt ink beside a 23pt
|
|
174
|
+
// "+" at the default size in the same app.
|
|
175
|
+
houseRender(
|
|
176
|
+
<>
|
|
177
|
+
<Button size="$3" circular chromeless accentInk icon={Glyph} aria-label="Add" />
|
|
178
|
+
<Button size="$2" circular chromeless accentInk icon={Glyph} aria-label="Copy" />
|
|
179
|
+
<Button chromeless accentInk icon={Glyph} aria-label="Default" />
|
|
180
|
+
</>,
|
|
181
|
+
scheme,
|
|
182
|
+
);
|
|
183
|
+
const glyph = (label: string) => Number(glyphOf(label).getAttribute("width"));
|
|
184
|
+
const box = (label: string) =>
|
|
185
|
+
Number(screen.getByLabelText(label).getAttribute("data-mp-button-height"));
|
|
186
|
+
for (const label of ["Add", "Copy"])
|
|
187
|
+
expect(glyph(label)).toBe(Math.min(glyph("Default"), box(label) - 4));
|
|
188
|
+
expect(glyph("Add")).toBeGreaterThanOrEqual(ICON_BUTTON_GLYPH_PX);
|
|
189
|
+
});
|
|
190
|
+
|
|
147
191
|
it.each(["$3", "$4"] as const)(
|
|
148
192
|
"%s: a circle the caller asks for carries its glyph at the Settings ratio, bold",
|
|
149
193
|
(size) => {
|
|
@@ -183,6 +227,22 @@ describe("LC-176: an icon-only Button is a bare glyph", () => {
|
|
|
183
227
|
expect(rest(media, "_bg-").join(" ")).not.toMatch(/transparent/);
|
|
184
228
|
});
|
|
185
229
|
|
|
230
|
+
it.each(SCHEMES)("%s: a lone media plate and a group's capsule are one material", (scheme) => {
|
|
231
|
+
// Round 4's critique: the lone heart painted the tinted tier's opaque
|
|
232
|
+
// ground beside the capsule's translucent material on the same tile.
|
|
233
|
+
const { container } = houseRender(
|
|
234
|
+
<>
|
|
235
|
+
<Button plate="media" icon={Glyph} aria-label="Favourite" />
|
|
236
|
+
<IconButtonGroup plate="media">
|
|
237
|
+
<Button plate="media" icon={Glyph} aria-label="Replace" />
|
|
238
|
+
</IconButtonGroup>
|
|
239
|
+
</>,
|
|
240
|
+
scheme,
|
|
241
|
+
);
|
|
242
|
+
const capsule = container.querySelector('[data-mp-icon-button-group="media"]')!;
|
|
243
|
+
expect(rest(screen.getByLabelText("Favourite"), "_bg-")).toEqual(rest(capsule, "_bg-"));
|
|
244
|
+
});
|
|
245
|
+
|
|
186
246
|
it("a toggle over media shows its state in the glyph, not a selected fill", () => {
|
|
187
247
|
houseRender(<Button plate="media" icon={Glyph} aria-label="Like" aria-pressed />, "light");
|
|
188
248
|
expect(screen.getByLabelText("Like").hasAttribute("data-mp-selected")).toBe(false);
|
|
@@ -231,6 +291,9 @@ describe("LC-176: an icon-only Button is a bare glyph", () => {
|
|
|
231
291
|
const search = screen.getByLabelText("Search");
|
|
232
292
|
expect(search.getAttribute("data-mp-icon-button")).toBe("glass");
|
|
233
293
|
expect(search.querySelector("[data-glass]")).not.toBeNull();
|
|
294
|
+
// iOS Safari: the button holds its absolute glass circle under the glyph.
|
|
295
|
+
expect(search.className).toMatch(/(?:^| )_pos-relative(?: |$)/);
|
|
296
|
+
expect(search.className).toMatch(/(?:^| )_zi-0(?: |$)/);
|
|
234
297
|
expect(glyphOf("Search").getAttribute("data-weight")).toBe("bold");
|
|
235
298
|
// Glass is the bar button; content and inline glyphs never float on it.
|
|
236
299
|
expect(screen.getByLabelText("Content").querySelector("[data-glass]")).toBeNull();
|
|
@@ -313,6 +376,25 @@ describe("LC-176: an icon-only Button is a bare glyph", () => {
|
|
|
313
376
|
}
|
|
314
377
|
});
|
|
315
378
|
|
|
379
|
+
it("a caller's small px never shrinks a bar glyph (the DataTable toolbar's 14pt icons)", () => {
|
|
380
|
+
houseRender(
|
|
381
|
+
<Surface bar>
|
|
382
|
+
<Button chromeless size="$3" icon={Glyph} aria-label="Bare" />
|
|
383
|
+
<Button chromeless size="$3" icon={<Glyph size={14} />} aria-label="Save view" />
|
|
384
|
+
<Button icon={<Glyph size={40} />} aria-label="Large" />
|
|
385
|
+
<Button size="$3" icon={<Glyph size={14} />}>
|
|
386
|
+
Sort
|
|
387
|
+
</Button>
|
|
388
|
+
</Surface>,
|
|
389
|
+
"light",
|
|
390
|
+
);
|
|
391
|
+
const bar = Number(glyphOf("Bare").getAttribute("width"));
|
|
392
|
+
expect(Number(glyphOf("Save view").getAttribute("width"))).toBe(bar);
|
|
393
|
+
expect(Number(glyphOf("Large").getAttribute("width"))).toBe(40);
|
|
394
|
+
const sort = screen.getByText("Sort").closest("[role=button]")!.querySelector("[data-glyph]")!;
|
|
395
|
+
expect(Number(sort.getAttribute("width"))).toBe(14);
|
|
396
|
+
});
|
|
397
|
+
|
|
316
398
|
it("an inline glyph is the text's size, 8pt after the value, bold and tinted in content", () => {
|
|
317
399
|
houseRender(
|
|
318
400
|
<>
|
|
@@ -338,4 +420,62 @@ describe("LC-176: an icon-only Button is a bare glyph", () => {
|
|
|
338
420
|
);
|
|
339
421
|
expect(glyphOf("Copy").getAttribute("data-weight")).toBe("bold");
|
|
340
422
|
});
|
|
423
|
+
|
|
424
|
+
it("a bold glyph is stroked up to an SF Symbols semibold-bold stem; a regular one is not", () => {
|
|
425
|
+
// Clay's critique of round 3: the SHC + read thin. Phosphor bold's stem
|
|
426
|
+
// is 24 of a 200-unit plus (0.12); SF semibold's is 0.153, SF bold's 0.18.
|
|
427
|
+
// The trash lid's 16-unit slot must stay open, so the stroke stays under 16.
|
|
428
|
+
houseRender(
|
|
429
|
+
<>
|
|
430
|
+
<Button size="$3" circular chromeless accentInk icon={Glyph} aria-label="Add" />
|
|
431
|
+
<IconButton glyphWeight="regular" icon={Glyph} aria-label="Regular" />
|
|
432
|
+
</>,
|
|
433
|
+
"light",
|
|
434
|
+
);
|
|
435
|
+
expect(Number(glyphOf("Add").getAttribute("data-stroke-width"))).toBe(GLYPH_BOLD_STROKE_UNITS);
|
|
436
|
+
expect((24 + GLYPH_BOLD_STROKE_UNITS) / (200 + GLYPH_BOLD_STROKE_UNITS)).toBeGreaterThan(0.15);
|
|
437
|
+
expect(GLYPH_BOLD_STROKE_UNITS).toBeLessThan(16);
|
|
438
|
+
expect(glyphOf("Regular").hasAttribute("data-stroke-width")).toBe(false);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it.each(SCHEMES)(
|
|
442
|
+
"%s: a glyph on a plate takes the plate's on-colour at full strength",
|
|
443
|
+
(scheme) => {
|
|
444
|
+
houseRender(
|
|
445
|
+
<>
|
|
446
|
+
<Button plate="media" icon={Glyph} aria-label="Favourite" />
|
|
447
|
+
<Button size="$3" circular outlined icon={Glyph} aria-label="Update" />
|
|
448
|
+
</>,
|
|
449
|
+
scheme,
|
|
450
|
+
);
|
|
451
|
+
expect(rest(screen.getByLabelText("Favourite"), "_col-").join(" ")).toMatch(/color12/);
|
|
452
|
+
expect(rest(screen.getByLabelText("Update"), "_col-").length).toBeGreaterThan(0);
|
|
453
|
+
},
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
it.each(SCHEMES)(
|
|
457
|
+
"%s: with no hue in the theme, the tint follows the one fallback token",
|
|
458
|
+
(scheme) => {
|
|
459
|
+
houseRender(
|
|
460
|
+
<>
|
|
461
|
+
<GlyphTintFallbackProvider value="ink">
|
|
462
|
+
<Button chromeless accentInk icon={Glyph} aria-label="Ink" />
|
|
463
|
+
</GlyphTintFallbackProvider>
|
|
464
|
+
<GlyphTintFallbackProvider value="system">
|
|
465
|
+
<Button chromeless accentInk icon={Glyph} aria-label="System" />
|
|
466
|
+
</GlyphTintFallbackProvider>
|
|
467
|
+
<GlyphTintFallbackProvider value="accent">
|
|
468
|
+
<Button chromeless accentInk icon={Glyph} aria-label="Accent" />
|
|
469
|
+
</GlyphTintFallbackProvider>
|
|
470
|
+
</>,
|
|
471
|
+
scheme,
|
|
472
|
+
);
|
|
473
|
+
const ink = rest(screen.getByLabelText("Ink"), "_col-");
|
|
474
|
+
const system = rest(screen.getByLabelText("System"), "_col-");
|
|
475
|
+
const accent = rest(screen.getByLabelText("Accent"), "_col-");
|
|
476
|
+
expect(system).not.toEqual(ink);
|
|
477
|
+
expect(accent).not.toEqual(ink);
|
|
478
|
+
expect(accent).not.toEqual(system);
|
|
479
|
+
},
|
|
480
|
+
);
|
|
341
481
|
});
|
|
@@ -5,8 +5,11 @@ import {
|
|
|
5
5
|
FOCUS_RING_CLIPPED_OFFSET,
|
|
6
6
|
FOCUS_RING_MIN_CONTRAST,
|
|
7
7
|
ensureFocusVisibleRing,
|
|
8
|
+
GlyphTintFallbackProvider,
|
|
8
9
|
Paragraph,
|
|
9
10
|
Surface,
|
|
11
|
+
glyphTintFallback,
|
|
12
|
+
type GlyphTintFallback,
|
|
10
13
|
} from "@multiplatform.one/theme";
|
|
11
14
|
import { Button as TamaguiButton, XStack, YStack, useTheme } from "tamagui";
|
|
12
15
|
import {
|
|
@@ -539,25 +542,36 @@ export const IconButtons: Story = {
|
|
|
539
542
|
// SHC's own code rather than this reproduction.
|
|
540
543
|
const shcRoundGlyph = { circular: true, chromeless: true, accentInk: true } as const;
|
|
541
544
|
|
|
545
|
+
// FZ-8020 r4: the house theme has no hue, so `tint` renders each fallback
|
|
546
|
+
// (ink, the system tint, a house hue) from one build for Clay's compare.
|
|
547
|
+
type TintStory = StoryObj<{ tint: GlyphTintFallback }>;
|
|
548
|
+
const tintArgs: Pick<TintStory, "args" | "argTypes"> = {
|
|
549
|
+
args: { tint: glyphTintFallback },
|
|
550
|
+
argTypes: { tint: { control: "inline-radio", options: ["ink", "system", "accent"] } },
|
|
551
|
+
};
|
|
552
|
+
|
|
542
553
|
/** Clay's SHC Nodes screen (IMG_1925 on FZ-8020): the add and the copy. */
|
|
543
|
-
export const ShcNodesScreen:
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
<
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
<
|
|
559
|
-
|
|
560
|
-
|
|
554
|
+
export const ShcNodesScreen: TintStory = {
|
|
555
|
+
...tintArgs,
|
|
556
|
+
render: ({ tint }) => (
|
|
557
|
+
<GlyphTintFallbackProvider value={tint}>
|
|
558
|
+
<YStack gap="$4" padding="$4" backgroundColor="$background" data-testid="shc-nodes">
|
|
559
|
+
<XStack alignItems="center" gap="$2">
|
|
560
|
+
<Paragraph flex={1} size="$9" fontWeight="700">
|
|
561
|
+
Nodes
|
|
562
|
+
</Paragraph>
|
|
563
|
+
<Button size="$3" {...shcRoundGlyph} icon={PlusIcon} aria-label="Add node" />
|
|
564
|
+
</XStack>
|
|
565
|
+
<XStack alignItems="center" gap="$2">
|
|
566
|
+
<Paragraph fontFamily="$mono">shc node join</Paragraph>
|
|
567
|
+
<Button size="$2" {...shcRoundGlyph} icon={CopyIcon} aria-label="Copy" />
|
|
568
|
+
</XStack>
|
|
569
|
+
<XStack alignItems="center">
|
|
570
|
+
<Paragraph fontFamily="$mono">shc node join</Paragraph>
|
|
571
|
+
<IconButton inline icon={CopyIcon} aria-label="Copy (inline)" />
|
|
572
|
+
</XStack>
|
|
573
|
+
</YStack>
|
|
574
|
+
</GlyphTintFallbackProvider>
|
|
561
575
|
),
|
|
562
576
|
};
|
|
563
577
|
|
|
@@ -568,30 +582,33 @@ const shcUpdatePlate = { circular: true, outlined: true } as const;
|
|
|
568
582
|
* Clay's SHC Updates rows (IMG_1926 on FZ-8020): the plate SHC asks for, its
|
|
569
583
|
* glyph now at the circle ratio, above the bare tinted glyph LC-176 wants.
|
|
570
584
|
*/
|
|
571
|
-
export const ShcUpdateRows:
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
585
|
+
export const ShcUpdateRows: TintStory = {
|
|
586
|
+
...tintArgs,
|
|
587
|
+
render: ({ tint }) => (
|
|
588
|
+
<GlyphTintFallbackProvider value={tint}>
|
|
589
|
+
<YStack gap="$2" padding="$4" backgroundColor="$background" data-testid="shc-update-rows">
|
|
590
|
+
{(["$3", "$4"] as const).map((size) => (
|
|
591
|
+
<XStack key={size} alignItems="center" gap="$3" minHeight="$5">
|
|
592
|
+
<YStack flex={1}>
|
|
593
|
+
<Paragraph fontWeight="600">pokemon</Paragraph>
|
|
594
|
+
<Paragraph color="$color11">1.2.0+34</Paragraph>
|
|
595
|
+
</YStack>
|
|
596
|
+
<Button
|
|
597
|
+
size={size}
|
|
598
|
+
{...shcUpdatePlate}
|
|
599
|
+
icon={ArrowCircleUpIcon}
|
|
600
|
+
aria-label={`Update pokemon (plate ${size})`}
|
|
601
|
+
/>
|
|
602
|
+
</XStack>
|
|
603
|
+
))}
|
|
604
|
+
<XStack alignItems="center" gap="$3" minHeight="$5">
|
|
576
605
|
<YStack flex={1}>
|
|
577
606
|
<Paragraph fontWeight="600">pokemon</Paragraph>
|
|
578
607
|
<Paragraph color="$color11">1.2.0+34</Paragraph>
|
|
579
608
|
</YStack>
|
|
580
|
-
<
|
|
581
|
-
size={size}
|
|
582
|
-
{...shcUpdatePlate}
|
|
583
|
-
icon={ArrowCircleUpIcon}
|
|
584
|
-
aria-label={`Update pokemon (plate ${size})`}
|
|
585
|
-
/>
|
|
609
|
+
<IconButton accentInk icon={ArrowCircleUpIcon} aria-label="Update pokemon" />
|
|
586
610
|
</XStack>
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
<YStack flex={1}>
|
|
590
|
-
<Paragraph fontWeight="600">pokemon</Paragraph>
|
|
591
|
-
<Paragraph color="$color11">1.2.0+34</Paragraph>
|
|
592
|
-
</YStack>
|
|
593
|
-
<IconButton accentInk icon={ArrowCircleUpIcon} aria-label="Update pokemon" />
|
|
594
|
-
</XStack>
|
|
595
|
-
</YStack>
|
|
611
|
+
</YStack>
|
|
612
|
+
</GlyphTintFallbackProvider>
|
|
596
613
|
),
|
|
597
614
|
};
|
|
@@ -29,10 +29,14 @@ vi.mock("./glass", () => ({
|
|
|
29
29
|
|
|
30
30
|
afterEach(() => {
|
|
31
31
|
cleanup();
|
|
32
|
+
vi.unstubAllGlobals();
|
|
32
33
|
glass.available = false;
|
|
33
34
|
glass.containerLoads = 0;
|
|
34
35
|
});
|
|
35
36
|
|
|
37
|
+
const IPHONE =
|
|
38
|
+
"Mozilla/5.0 (iPhone; CPU iPhone OS 26_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 Mobile/15E148 Safari/604.1";
|
|
39
|
+
|
|
36
40
|
const houseConfig = createDefaultThemeConfig();
|
|
37
41
|
|
|
38
42
|
function Glyph({ size, weight }: { size?: number; weight?: string }) {
|
|
@@ -48,6 +52,42 @@ function houseRender(ui: ReactElement) {
|
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
describe("LC-176: IconButtonGroup", () => {
|
|
55
|
+
it("on iOS Safari a bar group is one glass capsule, its buttons bare glyphs on it", () => {
|
|
56
|
+
vi.stubGlobal("navigator", { userAgent: IPHONE, platform: "iPhone", maxTouchPoints: 5 });
|
|
57
|
+
glass.available = true;
|
|
58
|
+
houseRender(
|
|
59
|
+
<Surface bar>
|
|
60
|
+
<IconButtonGroup data-testid="group">
|
|
61
|
+
<IconButton icon={Glyph} aria-label="Archive" />
|
|
62
|
+
<IconButton icon={Glyph} aria-label="Delete" />
|
|
63
|
+
</IconButtonGroup>
|
|
64
|
+
<IconButton icon={Glyph} aria-label="Back" />
|
|
65
|
+
</Surface>,
|
|
66
|
+
);
|
|
67
|
+
const group = screen.getByTestId("group");
|
|
68
|
+
expect(group.getAttribute("data-mp-icon-button-group")).toBe("glass");
|
|
69
|
+
expect(group.querySelectorAll('[data-mp-glass="web"]')).toHaveLength(1);
|
|
70
|
+
expect(group.querySelector("[data-glass]")).toBeNull();
|
|
71
|
+
for (const name of ["Archive", "Delete"])
|
|
72
|
+
expect(screen.getByLabelText(name).getAttribute("data-mp-icon-button")).not.toBe("glass");
|
|
73
|
+
// A lone bar button keeps its own circle.
|
|
74
|
+
expect(screen.getByLabelText("Back").querySelector("[data-glass]")).not.toBeNull();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("a lone glass circle fills the button's border box, as wide as the ✓ beside it (FZ-8439)", () => {
|
|
78
|
+
glass.available = true;
|
|
79
|
+
houseRender(
|
|
80
|
+
<Surface bar>
|
|
81
|
+
<IconButton icon={Glyph} aria-label="Close" />
|
|
82
|
+
</Surface>,
|
|
83
|
+
);
|
|
84
|
+
const face = screen.getByLabelText("Close").querySelector<HTMLElement>("[data-glass]")!;
|
|
85
|
+
const inset = Number.parseFloat(face.style.top);
|
|
86
|
+
expect(inset).toBeLessThan(0);
|
|
87
|
+
for (const side of ["right", "bottom", "left"] as const)
|
|
88
|
+
expect(Number.parseFloat(face.style[side])).toBe(inset);
|
|
89
|
+
});
|
|
90
|
+
|
|
51
91
|
it("over media, the group is one capsule and its buttons draw their glyphs alone", () => {
|
|
52
92
|
houseRender(
|
|
53
93
|
<>
|
|
@@ -3,6 +3,7 @@ import { useCallback, useState, type ReactNode } from "react";
|
|
|
3
3
|
import { isWeb, Theme, View, XStack, type XStackProps } from "tamagui";
|
|
4
4
|
import { loadGlassView } from "./glass";
|
|
5
5
|
import { MEDIA_PLATE_WEB_STYLE } from "./mediaPlate";
|
|
6
|
+
import { isAppleTouchWeb, WebGlassFace } from "./webGlass";
|
|
6
7
|
import { IconButtonGroupContext, IconButtonGroupRegistry } from "./iconButtonGroupContext";
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -55,6 +56,44 @@ export function IconButtonGroup({ plate, children, ...rest }: IconButtonGroupPro
|
|
|
55
56
|
</IconButtonGroupContext.Provider>
|
|
56
57
|
);
|
|
57
58
|
}
|
|
59
|
+
if (bar && isWeb && isAppleTouchWeb()) {
|
|
60
|
+
// The caller's layout props stay on the outer row; the capsule hugs the
|
|
61
|
+
// buttons inside it, so a stretched slot never draws a wide empty pill.
|
|
62
|
+
return (
|
|
63
|
+
<IconButtonGroupRegistry.Provider value={register}>
|
|
64
|
+
<IconButtonGroupContext.Provider value="glass">
|
|
65
|
+
<XStack
|
|
66
|
+
alignItems="center"
|
|
67
|
+
data-mp-icon-button-group={members > 0 ? "glass" : "bar"}
|
|
68
|
+
{...rest}
|
|
69
|
+
>
|
|
70
|
+
<XStack
|
|
71
|
+
alignItems="center"
|
|
72
|
+
position="relative"
|
|
73
|
+
zIndex={0}
|
|
74
|
+
paddingHorizontal="$2"
|
|
75
|
+
gap="$1"
|
|
76
|
+
data-mp-glass-capsule=""
|
|
77
|
+
>
|
|
78
|
+
{members > 0 ? (
|
|
79
|
+
<WebGlassFace
|
|
80
|
+
style={{
|
|
81
|
+
position: "absolute",
|
|
82
|
+
top: 0,
|
|
83
|
+
right: 0,
|
|
84
|
+
bottom: 0,
|
|
85
|
+
left: 0,
|
|
86
|
+
borderRadius: 100000,
|
|
87
|
+
}}
|
|
88
|
+
/>
|
|
89
|
+
) : null}
|
|
90
|
+
{children}
|
|
91
|
+
</XStack>
|
|
92
|
+
</XStack>
|
|
93
|
+
</IconButtonGroupContext.Provider>
|
|
94
|
+
</IconButtonGroupRegistry.Provider>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
58
97
|
const GlassView = bar ? loadGlassView() : null;
|
|
59
98
|
const row = (
|
|
60
99
|
<XStack
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*
|
|
4
|
+
* LC-176 / LC-266: on the web only iOS Safari takes the glass face (Clay on
|
|
5
|
+
* rv_3bb3f1f65d6b981f).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createDefaultThemeConfig } from "@multiplatform.one/theme";
|
|
9
|
+
import { cleanup, render } from "@testing-library/react";
|
|
10
|
+
import { createElement } from "react";
|
|
11
|
+
import { TamaguiProvider } from "tamagui";
|
|
12
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
13
|
+
import { isAppleTouchWeb, loadGlassView } from "./glass";
|
|
14
|
+
|
|
15
|
+
const IPHONE =
|
|
16
|
+
"Mozilla/5.0 (iPhone; CPU iPhone OS 26_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 Mobile/15E148 Safari/604.1";
|
|
17
|
+
const MAC_SAFARI =
|
|
18
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 Safari/605.1.15";
|
|
19
|
+
const houseConfig = createDefaultThemeConfig();
|
|
20
|
+
const PIXEL =
|
|
21
|
+
"Mozilla/5.0 (Linux; Android 15; Pixel 9) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Mobile Safari/537.36";
|
|
22
|
+
|
|
23
|
+
describe("isAppleTouchWeb", () => {
|
|
24
|
+
it("an iPhone's Safari takes the glass face", () => {
|
|
25
|
+
expect(isAppleTouchWeb({ userAgent: IPHONE, platform: "iPhone", maxTouchPoints: 5 })).toBe(
|
|
26
|
+
true,
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("iPadOS reports a Mac with touch, and is iOS", () => {
|
|
31
|
+
expect(
|
|
32
|
+
isAppleTouchWeb({ userAgent: MAC_SAFARI, platform: "MacIntel", maxTouchPoints: 5 }),
|
|
33
|
+
).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("a Mac, an Android phone and no navigator draw the bare glyph", () => {
|
|
37
|
+
expect(
|
|
38
|
+
isAppleTouchWeb({ userAgent: MAC_SAFARI, platform: "MacIntel", maxTouchPoints: 0 }),
|
|
39
|
+
).toBe(false);
|
|
40
|
+
expect(isAppleTouchWeb({ userAgent: PIXEL, platform: "Linux armv8l", maxTouchPoints: 5 })).toBe(
|
|
41
|
+
false,
|
|
42
|
+
);
|
|
43
|
+
expect(isAppleTouchWeb(undefined)).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("loadGlassView on the web", () => {
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
cleanup();
|
|
50
|
+
vi.unstubAllGlobals();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("iOS Safari's glass circle sits under the glyph it backs", () => {
|
|
54
|
+
vi.stubGlobal("navigator", { userAgent: IPHONE, platform: "iPhone", maxTouchPoints: 5 });
|
|
55
|
+
const Glass = loadGlassView();
|
|
56
|
+
expect(Glass).not.toBeNull();
|
|
57
|
+
const { container } = render(
|
|
58
|
+
createElement(
|
|
59
|
+
TamaguiProvider,
|
|
60
|
+
{ config: houseConfig.tamagui, defaultTheme: "light", disableInjectCSS: true },
|
|
61
|
+
createElement(Glass!, { style: { position: "absolute" } }),
|
|
62
|
+
),
|
|
63
|
+
);
|
|
64
|
+
const face = container.querySelector('[data-mp-glass="web"]') as HTMLElement | null;
|
|
65
|
+
expect(face).not.toBeNull();
|
|
66
|
+
expect(face!.style.zIndex).toBe("-1");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("every other browser draws the bare glyph", () => {
|
|
70
|
+
vi.stubGlobal("navigator", { userAgent: PIXEL, platform: "Linux armv8l", maxTouchPoints: 5 });
|
|
71
|
+
expect(loadGlassView()).toBeNull();
|
|
72
|
+
});
|
|
73
|
+
});
|
package/src/Button/glass.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ComponentType, ReactNode } from "react";
|
|
2
|
+
import { isAppleTouchWeb, WebGlassFace } from "./webGlass";
|
|
2
3
|
|
|
3
4
|
/** The slice of expo-glass-effect's `GlassView` an icon button draws. */
|
|
4
5
|
export type GlassViewComponent = ComponentType<{
|
|
@@ -15,13 +16,17 @@ export type GlassContainerComponent = ComponentType<{
|
|
|
15
16
|
children?: ReactNode;
|
|
16
17
|
}>;
|
|
17
18
|
|
|
19
|
+
export { isAppleTouchWeb } from "./webGlass";
|
|
20
|
+
|
|
18
21
|
/**
|
|
19
22
|
* LC-176: the iOS 26 Liquid Glass material an icon button floats on in a bar.
|
|
20
|
-
*
|
|
21
|
-
*
|
|
23
|
+
* Native iOS draws it from expo-glass-effect (`glass.native.ts`; Android
|
|
24
|
+
* probes null). On the web only iOS Safari takes the glass face (Clay on
|
|
25
|
+
* rv_3bb3f1f65d6b981f: "Is there a way to have the buttons at the bottom be
|
|
26
|
+
* liquid glass if it's an iPhone"); every other browser draws the bare glyph.
|
|
22
27
|
*/
|
|
23
28
|
export function loadGlassView(): GlassViewComponent | null {
|
|
24
|
-
return null;
|
|
29
|
+
return isAppleTouchWeb() ? (WebGlassFace as GlassViewComponent) : null;
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
/**
|