@oxy.so/bloom 4.16.0 → 4.17.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/docs/adoption-matrix.mdx +13 -13
- package/docs/app-shell.mdx +3 -2
- package/docs/button-group.mdx +2 -1
- package/docs/button.mdx +24 -0
- package/docs/icons.mdx +53 -0
- package/docs/page-header.mdx +7 -0
- package/docs/tab-bar.mdx +1 -1
- package/docs/tabs.mdx +6 -0
- package/lib/commonjs/app-shell/AppShellEngine.js +9 -5
- package/lib/commonjs/app-shell/AppShellEngine.js.map +1 -1
- package/lib/commonjs/button/Button.js +7 -4
- package/lib/commonjs/button/Button.js.map +1 -1
- package/lib/commonjs/button/Button.web.js +21 -2
- package/lib/commonjs/button/Button.web.js.map +1 -1
- package/lib/commonjs/button-group/ButtonGroup.js +7 -4
- package/lib/commonjs/button-group/ButtonGroup.js.map +1 -1
- package/lib/commonjs/icons/index.js +8 -0
- package/lib/commonjs/icons/index.js.map +1 -1
- package/lib/commonjs/icons/render-icon.js +63 -0
- package/lib/commonjs/icons/render-icon.js.map +1 -0
- package/lib/commonjs/page-header/PageHeader.js +6 -1
- package/lib/commonjs/page-header/PageHeader.js.map +1 -1
- package/lib/commonjs/tab-bar/TabBarBase.js +38 -16
- package/lib/commonjs/tab-bar/TabBarBase.js.map +1 -1
- package/lib/commonjs/tabs/Tabs.js +8 -1
- package/lib/commonjs/tabs/Tabs.js.map +1 -1
- package/lib/module/app-shell/AppShellEngine.js +9 -5
- package/lib/module/app-shell/AppShellEngine.js.map +1 -1
- package/lib/module/button/Button.js +7 -4
- package/lib/module/button/Button.js.map +1 -1
- package/lib/module/button/Button.web.js +21 -2
- package/lib/module/button/Button.web.js.map +1 -1
- package/lib/module/button-group/ButtonGroup.js +7 -4
- package/lib/module/button-group/ButtonGroup.js.map +1 -1
- package/lib/module/icons/index.js +1 -0
- package/lib/module/icons/index.js.map +1 -1
- package/lib/module/icons/render-icon.js +59 -0
- package/lib/module/icons/render-icon.js.map +1 -0
- package/lib/module/page-header/PageHeader.js +6 -1
- package/lib/module/page-header/PageHeader.js.map +1 -1
- package/lib/module/tab-bar/TabBarBase.js +38 -16
- package/lib/module/tab-bar/TabBarBase.js.map +1 -1
- package/lib/module/tabs/Tabs.js +8 -1
- package/lib/module/tabs/Tabs.js.map +1 -1
- package/lib/typescript/commonjs/app-shell/AppShellEngine.d.ts.map +1 -1
- package/lib/typescript/commonjs/button/Button.d.ts.map +1 -1
- package/lib/typescript/commonjs/button/Button.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/button/types.d.ts +10 -0
- package/lib/typescript/commonjs/button/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/button-group/ButtonGroup.d.ts.map +1 -1
- package/lib/typescript/commonjs/button-group/types.d.ts +9 -0
- package/lib/typescript/commonjs/button-group/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/icons/index.d.ts +2 -0
- package/lib/typescript/commonjs/icons/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/icons/render-icon.d.ts +55 -0
- package/lib/typescript/commonjs/icons/render-icon.d.ts.map +1 -0
- package/lib/typescript/commonjs/page-header/PageHeader.d.ts.map +1 -1
- package/lib/typescript/commonjs/tab-bar/TabBarBase.d.ts.map +1 -1
- package/lib/typescript/commonjs/tabs/Tabs.d.ts.map +1 -1
- package/lib/typescript/module/app-shell/AppShellEngine.d.ts.map +1 -1
- package/lib/typescript/module/button/Button.d.ts.map +1 -1
- package/lib/typescript/module/button/Button.web.d.ts.map +1 -1
- package/lib/typescript/module/button/types.d.ts +10 -0
- package/lib/typescript/module/button/types.d.ts.map +1 -1
- package/lib/typescript/module/button-group/ButtonGroup.d.ts.map +1 -1
- package/lib/typescript/module/button-group/types.d.ts +9 -0
- package/lib/typescript/module/button-group/types.d.ts.map +1 -1
- package/lib/typescript/module/icons/index.d.ts +2 -0
- package/lib/typescript/module/icons/index.d.ts.map +1 -1
- package/lib/typescript/module/icons/render-icon.d.ts +55 -0
- package/lib/typescript/module/icons/render-icon.d.ts.map +1 -0
- package/lib/typescript/module/page-header/PageHeader.d.ts.map +1 -1
- package/lib/typescript/module/tab-bar/TabBarBase.d.ts.map +1 -1
- package/lib/typescript/module/tabs/Tabs.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/app-shell/AppShellEngine.tsx +9 -5
- package/src/button/Button.tsx +15 -6
- package/src/button/Button.web.tsx +30 -3
- package/src/button/types.ts +10 -0
- package/src/button-group/ButtonGroup.tsx +15 -6
- package/src/button-group/types.ts +9 -0
- package/src/icons/index.ts +2 -0
- package/src/icons/render-icon.tsx +65 -0
- package/src/page-header/PageHeader.tsx +6 -1
- package/src/tab-bar/TabBarBase.tsx +39 -16
- package/src/tabs/Tabs.tsx +3 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/icons/index.ts"],"names":[],"mappings":"AAIA,YAAY,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AACjD,YAAY,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AACpD,cAAc,SAAS,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/icons/index.ts"],"names":[],"mappings":"AAIA,YAAY,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AACjD,YAAY,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAC3D,YAAY,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AACvE,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AACpD,cAAc,SAAS,CAAC"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* What a Bloom component tells an icon slot to draw: the box, and the colour of
|
|
4
|
+
* the glyph in the CURRENT state (rest, hover, pressed, disabled, checked).
|
|
5
|
+
*
|
|
6
|
+
* `color` carries the same value the component would have passed as `fill`. It
|
|
7
|
+
* is spelled `color` because this object is handed to CALLER code, and an app's
|
|
8
|
+
* own icon set almost always names it that.
|
|
9
|
+
*/
|
|
10
|
+
export interface BloomIconPaint {
|
|
11
|
+
size: number;
|
|
12
|
+
color: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A caller-owned glyph that still follows the control's state.
|
|
16
|
+
*
|
|
17
|
+
* Bloom's own icon slots take a {@link BloomIconComponent} and tint it by
|
|
18
|
+
* passing `fill`. An app whose icons paint from a `color` prop — which most app
|
|
19
|
+
* icon sets do — cannot be handed to one of those slots directly: the props
|
|
20
|
+
* have nothing in common, so it does not typecheck, and were it cast it would
|
|
21
|
+
* render at its default size in its default colour, silently.
|
|
22
|
+
*
|
|
23
|
+
* Passing `color` alongside `fill` is not the fix, and deliberately so:
|
|
24
|
+
* `frosted-icon-button/shared.ts` records why — "the prop is not ours to
|
|
25
|
+
* claim", because components use `color` for semantic variants (`color="danger"`).
|
|
26
|
+
*
|
|
27
|
+
* Nor can the function form share the existing prop. A component is a function
|
|
28
|
+
* too, so a slot accepting both could not tell an app's ADAPTER component —
|
|
29
|
+
* `({ width, fill }) => <Glyph size={width} color={fill} />`, which works today
|
|
30
|
+
* and is the documented way to do this — from a paint callback. It would break
|
|
31
|
+
* exactly the callers who already did the right thing. Hence its own prop.
|
|
32
|
+
*
|
|
33
|
+
* ```tsx
|
|
34
|
+
* <ButtonGroupItem
|
|
35
|
+
* iconOnly
|
|
36
|
+
* renderLeadingIcon={({ size, color }) => <AppSearchIcon size={size} color={color} />}
|
|
37
|
+
* accessibilityLabel="Search"
|
|
38
|
+
* />
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* This is `GlyphButton`'s `children?: ReactNode | ((foreground: string) => ReactNode)`
|
|
42
|
+
* generalised to the slots that take an icon rather than a child — including
|
|
43
|
+
* its precedence rule: where a family offers both, the renderer wins over the
|
|
44
|
+
* icon component.
|
|
45
|
+
*/
|
|
46
|
+
export type BloomIconRenderer = (paint: BloomIconPaint) => ReactNode;
|
|
47
|
+
/**
|
|
48
|
+
* Resolve an icon slot that offers both forms.
|
|
49
|
+
*
|
|
50
|
+
* `render` wins when present, matching `GlyphButton`, where the caller-painted
|
|
51
|
+
* glyph wins over `icon`. Returns `null` when neither is set, so a call site
|
|
52
|
+
* stays a single expression.
|
|
53
|
+
*/
|
|
54
|
+
export declare function resolveIconSlot(render: BloomIconRenderer | undefined, size: number, color: string, fallback: () => ReactNode): ReactNode;
|
|
55
|
+
//# sourceMappingURL=render-icon.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render-icon.d.ts","sourceRoot":"","sources":["../../../../src/icons/render-icon.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC;;;;;;;GAOG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,KAAK,EAAE,cAAc,KAAK,SAAS,CAAC;AAErE;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,iBAAiB,GAAG,SAAS,EACrC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,SAAS,GACxB,SAAS,CAGX"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PageHeader.d.ts","sourceRoot":"","sources":["../../../../src/page-header/PageHeader.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAyD,MAAM,OAAO,CAAC;AAkC9E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AA8E/C,iBAAS,mBAAmB,CAAC,EAC3B,KAAK,EACL,QAAQ,EACR,UAAoB,EACpB,YAAgB,EAChB,WAAsB,EACtB,iBAAqB,EACrB,YAAyB,EACzB,SAAoB,EACpB,MAAM,EACN,SAAkB,EAClB,OAAO,EACP,OAAO,EACP,MAAe,EACf,WAAmB,EACnB,KAAc,EACd,UAAU,EACV,OAAO,EAAE,eAAe,EACxB,eAAmC,EACnC,MAAa,EACb,QAAQ,EACR,KAAK,EACL,MAAM,GACP,EAAE,eAAe,
|
|
1
|
+
{"version":3,"file":"PageHeader.d.ts","sourceRoot":"","sources":["../../../../src/page-header/PageHeader.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAyD,MAAM,OAAO,CAAC;AAkC9E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AA8E/C,iBAAS,mBAAmB,CAAC,EAC3B,KAAK,EACL,QAAQ,EACR,UAAoB,EACpB,YAAgB,EAChB,WAAsB,EACtB,iBAAqB,EACrB,YAAyB,EACzB,SAAoB,EACpB,MAAM,EACN,SAAkB,EAClB,OAAO,EACP,OAAO,EACP,MAAe,EACf,WAAmB,EACnB,KAAc,EACd,UAAU,EACV,OAAO,EAAE,eAAe,EACxB,eAAmC,EACnC,MAAa,EACb,QAAQ,EACR,KAAK,EACL,MAAM,GACP,EAAE,eAAe,2CA6UjB;AAuED,eAAO,MAAM,UAAU,uDAA4B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TabBarBase.d.ts","sourceRoot":"","sources":["../../../../src/tab-bar/TabBarBase.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EASL,KAAK,aAAa,EAEnB,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"TabBarBase.d.ts","sourceRoot":"","sources":["../../../../src/tab-bar/TabBarBase.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EASL,KAAK,aAAa,EAEnB,MAAM,OAAO,CAAC;AA4Bf,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAItE,OAAO,EAkBL,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACxB,MAAM,UAAU,CAAC;AAClB,OAAO,KAAK,EAAE,iBAAiB,EAAE,WAAW,EAAe,MAAM,SAAS,CAAC;AA8rB3E;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,aAAa,CAAC,kBAAkB,CAAC,EAC1C,IAAI,EAAE,aAAa,CAAC,oBAAoB,CAAC;YAElB,WAAW;;EAGnC;AAED,+EAA+E;AAC/E,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,aAAa,CAAC,gBAAgB,CAAC;YAC1C,iBAAiB;;EAG/C"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Tabs.d.ts","sourceRoot":"","sources":["../../../../src/tabs/Tabs.tsx"],"names":[],"mappings":"AACA,OAAO,KAUN,MAAM,OAAO,CAAC;AAoBf,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAS5C,OAAO,KAAK,EACV,gBAAgB,EAEhB,SAAS,EACT,gBAAgB,EAChB,WAAW,EACZ,MAAM,SAAS,CAAC;AAuGjB,MAAM,WAAW,SAAS;IACxB,8CAA8C;IAC9C,SAAS,EAAE,MAAM,CAAC;IAClB,yBAAyB;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,8BAA8B;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,mCAAmC;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,uBAAuB,EAAE,MAAM,CAAC;IAChC,uBAAuB,EAAE,MAAM,CAAC;IAChC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,6DAA6D;IAC7D,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,OAAO,SAA0B,GAAG,SAAS,CAyBjH;AAqGD;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;;;;;;;;;;OAYG;IACH,IAAI,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC1C;;;;;;;;;OASG;IACH,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAC;CACzC;
|
|
1
|
+
{"version":3,"file":"Tabs.d.ts","sourceRoot":"","sources":["../../../../src/tabs/Tabs.tsx"],"names":[],"mappings":"AACA,OAAO,KAUN,MAAM,OAAO,CAAC;AAoBf,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAS5C,OAAO,KAAK,EACV,gBAAgB,EAEhB,SAAS,EACT,gBAAgB,EAChB,WAAW,EACZ,MAAM,SAAS,CAAC;AAuGjB,MAAM,WAAW,SAAS;IACxB,8CAA8C;IAC9C,SAAS,EAAE,MAAM,CAAC;IAClB,yBAAyB;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,8BAA8B;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,mCAAmC;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,uBAAuB,EAAE,MAAM,CAAC;IAChC,uBAAuB,EAAE,MAAM,CAAC;IAChC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,6DAA6D;IAC7D,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,OAAO,SAA0B,GAAG,SAAS,CAyBjH;AAqGD;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;;;;;;;;;;OAYG;IACH,IAAI,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC1C;;;;;;;;;OASG;IACH,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAC;CACzC;AAyrBD,eAAO,MAAM,IAAI,iFAAyB,CAAC;AAG3C,eAAO,MAAM,WAAW,8CAAqB,CAAC;AAG9C,eAAO,MAAM,WAAW,8CAA0B,CAAC"}
|
package/package.json
CHANGED
|
@@ -538,7 +538,11 @@ const AppShellComponent: React.FC<AppShellEngineProps> = ({
|
|
|
538
538
|
// Plain navigation belongs to the viewport; only the reading surfaces are inset.
|
|
539
539
|
const documentPanelInset = feedRevealed ? 12 : compactPanel ? 0 : gutter;
|
|
540
540
|
const externalTopHeight = showTopBar ? topBarHeight : 0;
|
|
541
|
-
|
|
541
|
+
// A property of the NAVIGATION, not of who scrolls: native resolves every
|
|
542
|
+
// shell to container scroll, and gating this on `document` left native
|
|
543
|
+
// phones with an 8px frame around the reading panel that the same layout on
|
|
544
|
+
// the web does not draw.
|
|
545
|
+
const plainFrame = contentAligned && flowSidebar?.surface === 'plain' && !topBarNode;
|
|
542
546
|
const readingGap = Math.max(0, asideGap ?? columnGap);
|
|
543
547
|
const readingGroupWidth = contentWidth + (asideBeside ? asideWidth + readingGap : 0);
|
|
544
548
|
const centredBody = (
|
|
@@ -556,7 +560,7 @@ const AppShellComponent: React.FC<AppShellEngineProps> = ({
|
|
|
556
560
|
// grows the page, the aside is sticky). Bounded: they stretch.
|
|
557
561
|
alignItems: doc ? 'flex-start' : 'stretch',
|
|
558
562
|
...(contentAligned ? { flexGrow: compactPanel ? 1 : 0, flexBasis: compactPanel ? 'auto' : readingGroupWidth, maxWidth: compactPanel ? undefined : readingGroupWidth } : {}),
|
|
559
|
-
...(
|
|
563
|
+
...(plainFrame ? { paddingTop: compactPanel ? 0 : gutter, paddingBottom: compactPanel ? 0 : gutter } : {}),
|
|
560
564
|
},
|
|
561
565
|
fill,
|
|
562
566
|
]}
|
|
@@ -747,7 +751,7 @@ const AppShellComponent: React.FC<AppShellEngineProps> = ({
|
|
|
747
751
|
const navRegion =
|
|
748
752
|
navInFlow && flowSidebar ? (
|
|
749
753
|
doc ? (
|
|
750
|
-
<View testID={testID ? `${testID}-navigation` : undefined} style={stickyRail(dockedNav ||
|
|
754
|
+
<View testID={testID ? `${testID}-navigation` : undefined} style={stickyRail(dockedNav || plainFrame ? 0 : gutter)}>
|
|
751
755
|
<Sidebar {...flowSidebar} />
|
|
752
756
|
</View>
|
|
753
757
|
) : canvas && !dockedNav ? (
|
|
@@ -794,8 +798,8 @@ const AppShellComponent: React.FC<AppShellEngineProps> = ({
|
|
|
794
798
|
// shorthand to physical sides, and which of two declarations for one edge
|
|
795
799
|
// wins is then a matter of declaration order rather than intent.
|
|
796
800
|
...(dockedNav
|
|
797
|
-
? { paddingTop: 0, paddingBottom: 0, paddingInlineStart: 0, paddingInlineEnd: canvas ||
|
|
798
|
-
: { padding: canvas ||
|
|
801
|
+
? { paddingTop: 0, paddingBottom: 0, paddingInlineStart: 0, paddingInlineEnd: canvas || plainFrame ? 0 : gutter }
|
|
802
|
+
: { padding: canvas || plainFrame ? 0 : gutter }),
|
|
799
803
|
backgroundColor: background,
|
|
800
804
|
};
|
|
801
805
|
|
package/src/button/Button.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { LinkButtonProps } from './types';
|
|
2
2
|
import React, { useMemo, memo, type ComponentType } from 'react';
|
|
3
|
+
import { resolveIconSlot } from '../icons/render-icon';
|
|
3
4
|
import {
|
|
4
5
|
ActivityIndicator,
|
|
5
6
|
Linking,
|
|
@@ -175,6 +176,8 @@ const ButtonComponent: React.FC<ButtonProps> = ({
|
|
|
175
176
|
trailing,
|
|
176
177
|
leadingIcon: LeadingIcon,
|
|
177
178
|
trailingIcon: TrailingIcon,
|
|
179
|
+
renderLeadingIcon,
|
|
180
|
+
renderTrailingIcon,
|
|
178
181
|
iconOnly = false,
|
|
179
182
|
linkTone = 'primary',
|
|
180
183
|
underline,
|
|
@@ -298,9 +301,11 @@ const ButtonComponent: React.FC<ButtonProps> = ({
|
|
|
298
301
|
|
|
299
302
|
const content = (
|
|
300
303
|
<>
|
|
301
|
-
{
|
|
302
|
-
|
|
303
|
-
|
|
304
|
+
{resolveIconSlot(renderLeadingIcon, iconSize, paint.foreground, () =>
|
|
305
|
+
LeadingIcon ? (
|
|
306
|
+
<LeadingIcon width={iconSize} height={iconSize} fill={paint.foreground} />
|
|
307
|
+
) : null,
|
|
308
|
+
)}
|
|
304
309
|
{leading}
|
|
305
310
|
{iconNode}
|
|
306
311
|
{!isSquare && children != null && (
|
|
@@ -313,9 +318,13 @@ const ButtonComponent: React.FC<ButtonProps> = ({
|
|
|
313
318
|
</Text>
|
|
314
319
|
)}
|
|
315
320
|
{trailing}
|
|
316
|
-
{!isSquare
|
|
317
|
-
|
|
318
|
-
|
|
321
|
+
{!isSquare
|
|
322
|
+
? resolveIconSlot(renderTrailingIcon, iconSize, paint.foreground, () =>
|
|
323
|
+
TrailingIcon ? (
|
|
324
|
+
<TrailingIcon width={iconSize} height={iconSize} fill={paint.foreground} />
|
|
325
|
+
) : null,
|
|
326
|
+
)
|
|
327
|
+
: null}
|
|
319
328
|
</>
|
|
320
329
|
);
|
|
321
330
|
|
|
@@ -9,6 +9,7 @@ import React, {
|
|
|
9
9
|
type ReactElement,
|
|
10
10
|
} from 'react';
|
|
11
11
|
|
|
12
|
+
import { resolveIconSlot } from '../icons/render-icon';
|
|
12
13
|
import { useBloomAppearance } from '../appearance/context';
|
|
13
14
|
import { useTheme } from '../theme/use-theme';
|
|
14
15
|
import { SpinnerIcon } from '../loading/SpinnerIcon.web';
|
|
@@ -182,6 +183,8 @@ const ButtonWebComponent: React.FC<ButtonProps> = ({
|
|
|
182
183
|
trailing,
|
|
183
184
|
leadingIcon: LeadingIcon,
|
|
184
185
|
trailingIcon: TrailingIcon,
|
|
186
|
+
renderLeadingIcon,
|
|
187
|
+
renderTrailingIcon,
|
|
185
188
|
iconOnly = false,
|
|
186
189
|
linkTone = 'primary',
|
|
187
190
|
underline,
|
|
@@ -339,11 +342,29 @@ const ButtonWebComponent: React.FC<ButtonProps> = ({
|
|
|
339
342
|
const iconNode =
|
|
340
343
|
icon == null ? null : isIconComponent(icon) ? renderIcon(icon) : (icon as React.ReactNode);
|
|
341
344
|
|
|
345
|
+
// A caller-drawn glyph is handed `currentColor` rather than the resolved
|
|
346
|
+
// foreground, for the same reason the icon components above are: on web the
|
|
347
|
+
// state colours come from the stylesheet, so a glyph that inherits them needs
|
|
348
|
+
// no re-render when hover or press changes them. Native has no cascade and
|
|
349
|
+
// passes the resolved value.
|
|
350
|
+
const renderSlot = (render: typeof renderLeadingIcon, fallback: () => React.ReactNode) => (
|
|
351
|
+
<span
|
|
352
|
+
aria-hidden="true"
|
|
353
|
+
style={{ display: 'inline-flex', flexShrink: 0, width: iconSize, height: iconSize }}
|
|
354
|
+
>
|
|
355
|
+
{resolveIconSlot(render, iconSize, 'currentColor', fallback)}
|
|
356
|
+
</span>
|
|
357
|
+
);
|
|
358
|
+
|
|
342
359
|
const labelPadding = isLink ? 0 : geometry.labelPaddingHorizontal;
|
|
343
360
|
const hasLabel = !isSquare && children != null && children !== false;
|
|
344
361
|
const content = (
|
|
345
362
|
<>
|
|
346
|
-
{
|
|
363
|
+
{renderLeadingIcon
|
|
364
|
+
? renderSlot(renderLeadingIcon, () => null)
|
|
365
|
+
: LeadingIcon
|
|
366
|
+
? renderIcon(LeadingIcon)
|
|
367
|
+
: null}
|
|
347
368
|
{leading}
|
|
348
369
|
{iconNode}
|
|
349
370
|
{hasLabel &&
|
|
@@ -367,9 +388,15 @@ const ButtonWebComponent: React.FC<ButtonProps> = ({
|
|
|
367
388
|
) : (
|
|
368
389
|
children
|
|
369
390
|
))}
|
|
370
|
-
{isSquare && !LeadingIcon && !iconNode && children != null
|
|
391
|
+
{isSquare && !renderLeadingIcon && !LeadingIcon && !iconNode && children != null
|
|
392
|
+
? children
|
|
393
|
+
: null}
|
|
371
394
|
{trailing}
|
|
372
|
-
{!isSquare &&
|
|
395
|
+
{!isSquare && renderTrailingIcon
|
|
396
|
+
? renderSlot(renderTrailingIcon, () => null)
|
|
397
|
+
: !isSquare && TrailingIcon
|
|
398
|
+
? renderIcon(TrailingIcon)
|
|
399
|
+
: null}
|
|
373
400
|
</>
|
|
374
401
|
);
|
|
375
402
|
|
package/src/button/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { BloomAppearance, BloomTone, BloomSize } from '../appearance/types';
|
|
2
|
+
import type { BloomIconRenderer } from '../icons/render-icon';
|
|
2
3
|
import type { ComponentType, ReactNode } from 'react';
|
|
3
4
|
import type { GestureResponderEvent, StyleProp, ViewStyle, TextStyle } from 'react-native';
|
|
4
5
|
import type { WebAriaProps } from '../styles/styled-primitives';
|
|
@@ -173,6 +174,15 @@ export interface ButtonProps {
|
|
|
173
174
|
leadingIcon?: ButtonIconComponent;
|
|
174
175
|
/** Same as {@link ButtonProps.leadingIcon}, after the label. Ignored when `iconOnly`. */
|
|
175
176
|
trailingIcon?: ButtonIconComponent;
|
|
177
|
+
/**
|
|
178
|
+
* Draw the leading glyph yourself, handed the button's icon size and the
|
|
179
|
+
* foreground for the CURRENT state — for an app's own icon set, whose props
|
|
180
|
+
* Bloom's icon slot cannot name. Wins over `leadingIcon`, and is used for the
|
|
181
|
+
* glyph when `iconOnly`. See `icons/render-icon.tsx`.
|
|
182
|
+
*/
|
|
183
|
+
renderLeadingIcon?: BloomIconRenderer;
|
|
184
|
+
/** Same as {@link ButtonProps.renderLeadingIcon}, after the label. Ignored when `iconOnly`. */
|
|
185
|
+
renderTrailingIcon?: BloomIconRenderer;
|
|
176
186
|
/**
|
|
177
187
|
* Render a square icon-only button (24 / 32 / 36 / 44) from `leadingIcon` or
|
|
178
188
|
* `icon`. Name it with `accessibilityLabel` — there is no text to read.
|
|
@@ -3,6 +3,7 @@ import React, { Children, createContext, Fragment, isValidElement, memo, useCont
|
|
|
3
3
|
import { Platform, Pressable, View, type TextStyle } from 'react-native';
|
|
4
4
|
|
|
5
5
|
import { useInheritedControl } from '../control-surface';
|
|
6
|
+
import { resolveIconSlot } from '../icons/render-icon';
|
|
6
7
|
import { GlassIsland } from '../glass';
|
|
7
8
|
import { useTheme } from '../theme/use-theme';
|
|
8
9
|
import { Text } from '../typography/Typography';
|
|
@@ -271,6 +272,8 @@ const ButtonGroupItemComponent: React.FC<ButtonGroupItemProps> = ({
|
|
|
271
272
|
iconOnly = false,
|
|
272
273
|
leadingIcon: LeadingIcon,
|
|
273
274
|
trailingIcon: TrailingIcon,
|
|
275
|
+
renderLeadingIcon,
|
|
276
|
+
renderTrailingIcon,
|
|
274
277
|
accessibilityLabel,
|
|
275
278
|
accessibilityRole = 'button',
|
|
276
279
|
hitSlop,
|
|
@@ -355,9 +358,11 @@ const ButtonGroupItemComponent: React.FC<ButtonGroupItemProps> = ({
|
|
|
355
358
|
aria-haspopup={ariaHasPopup}
|
|
356
359
|
testID={testID}
|
|
357
360
|
>
|
|
358
|
-
{
|
|
359
|
-
|
|
360
|
-
|
|
361
|
+
{resolveIconSlot(renderLeadingIcon, geometry.iconSize, foreground, () =>
|
|
362
|
+
LeadingIcon ? (
|
|
363
|
+
<LeadingIcon width={geometry.iconSize} height={geometry.iconSize} fill={foreground} />
|
|
364
|
+
) : null,
|
|
365
|
+
)}
|
|
361
366
|
{!iconOnly && children != null ? (
|
|
362
367
|
typeof children === 'string' || typeof children === 'number' ? (
|
|
363
368
|
<Text variant="body-medium" numberOfLines={1} style={labelStyle}>
|
|
@@ -367,9 +372,13 @@ const ButtonGroupItemComponent: React.FC<ButtonGroupItemProps> = ({
|
|
|
367
372
|
children
|
|
368
373
|
)
|
|
369
374
|
) : null}
|
|
370
|
-
{!iconOnly
|
|
371
|
-
|
|
372
|
-
|
|
375
|
+
{!iconOnly
|
|
376
|
+
? resolveIconSlot(renderTrailingIcon, geometry.iconSize, foreground, () =>
|
|
377
|
+
TrailingIcon ? (
|
|
378
|
+
<TrailingIcon width={geometry.iconSize} height={geometry.iconSize} fill={foreground} />
|
|
379
|
+
) : null,
|
|
380
|
+
)
|
|
381
|
+
: null}
|
|
373
382
|
</Pressable>
|
|
374
383
|
);
|
|
375
384
|
};
|
|
@@ -2,6 +2,7 @@ import type { GestureResponderEvent, StyleProp, ViewStyle } from 'react-native';
|
|
|
2
2
|
|
|
3
3
|
import type { ControlMaterial } from '../control-surface/types';
|
|
4
4
|
import type { ButtonIconComponent } from '../button/types';
|
|
5
|
+
import type { BloomIconRenderer } from '../icons/render-icon';
|
|
5
6
|
import type { WebAriaProps } from '../styles/styled-primitives';
|
|
6
7
|
|
|
7
8
|
/** The two group sizes: 34px (`md`) and 30px (`sm`) items. */
|
|
@@ -67,6 +68,14 @@ export interface ButtonGroupItemProps {
|
|
|
67
68
|
leadingIcon?: ButtonIconComponent;
|
|
68
69
|
/** Icon component after the label. */
|
|
69
70
|
trailingIcon?: ButtonIconComponent;
|
|
71
|
+
/**
|
|
72
|
+
* Draw the leading glyph yourself, handed the item's size and the foreground
|
|
73
|
+
* for the CURRENT state — for an app's own icon set, whose props Bloom's icon
|
|
74
|
+
* slot cannot name. Wins over `leadingIcon`. See `icons/render-icon.tsx`.
|
|
75
|
+
*/
|
|
76
|
+
renderLeadingIcon?: BloomIconRenderer;
|
|
77
|
+
/** Same as {@link ButtonGroupItemProps.renderLeadingIcon}, after the label. */
|
|
78
|
+
renderTrailingIcon?: BloomIconRenderer;
|
|
70
79
|
accessibilityLabel?: string;
|
|
71
80
|
|
|
72
81
|
// ── The trigger contract ────────────────────────────────────────────────
|
package/src/icons/index.ts
CHANGED
|
@@ -4,5 +4,7 @@
|
|
|
4
4
|
// rewrites an app's imports.
|
|
5
5
|
export type { IconStyle, Props } from './shared';
|
|
6
6
|
export type { BloomIconComponent } from './icon-component';
|
|
7
|
+
export type { BloomIconPaint, BloomIconRenderer } from './render-icon';
|
|
8
|
+
export { resolveIconSlot } from './render-icon';
|
|
7
9
|
export { sizes, useCommonSVGProps } from './shared';
|
|
8
10
|
export * from './remix';
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What a Bloom component tells an icon slot to draw: the box, and the colour of
|
|
5
|
+
* the glyph in the CURRENT state (rest, hover, pressed, disabled, checked).
|
|
6
|
+
*
|
|
7
|
+
* `color` carries the same value the component would have passed as `fill`. It
|
|
8
|
+
* is spelled `color` because this object is handed to CALLER code, and an app's
|
|
9
|
+
* own icon set almost always names it that.
|
|
10
|
+
*/
|
|
11
|
+
export interface BloomIconPaint {
|
|
12
|
+
size: number;
|
|
13
|
+
color: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A caller-owned glyph that still follows the control's state.
|
|
18
|
+
*
|
|
19
|
+
* Bloom's own icon slots take a {@link BloomIconComponent} and tint it by
|
|
20
|
+
* passing `fill`. An app whose icons paint from a `color` prop — which most app
|
|
21
|
+
* icon sets do — cannot be handed to one of those slots directly: the props
|
|
22
|
+
* have nothing in common, so it does not typecheck, and were it cast it would
|
|
23
|
+
* render at its default size in its default colour, silently.
|
|
24
|
+
*
|
|
25
|
+
* Passing `color` alongside `fill` is not the fix, and deliberately so:
|
|
26
|
+
* `frosted-icon-button/shared.ts` records why — "the prop is not ours to
|
|
27
|
+
* claim", because components use `color` for semantic variants (`color="danger"`).
|
|
28
|
+
*
|
|
29
|
+
* Nor can the function form share the existing prop. A component is a function
|
|
30
|
+
* too, so a slot accepting both could not tell an app's ADAPTER component —
|
|
31
|
+
* `({ width, fill }) => <Glyph size={width} color={fill} />`, which works today
|
|
32
|
+
* and is the documented way to do this — from a paint callback. It would break
|
|
33
|
+
* exactly the callers who already did the right thing. Hence its own prop.
|
|
34
|
+
*
|
|
35
|
+
* ```tsx
|
|
36
|
+
* <ButtonGroupItem
|
|
37
|
+
* iconOnly
|
|
38
|
+
* renderLeadingIcon={({ size, color }) => <AppSearchIcon size={size} color={color} />}
|
|
39
|
+
* accessibilityLabel="Search"
|
|
40
|
+
* />
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* This is `GlyphButton`'s `children?: ReactNode | ((foreground: string) => ReactNode)`
|
|
44
|
+
* generalised to the slots that take an icon rather than a child — including
|
|
45
|
+
* its precedence rule: where a family offers both, the renderer wins over the
|
|
46
|
+
* icon component.
|
|
47
|
+
*/
|
|
48
|
+
export type BloomIconRenderer = (paint: BloomIconPaint) => ReactNode;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resolve an icon slot that offers both forms.
|
|
52
|
+
*
|
|
53
|
+
* `render` wins when present, matching `GlyphButton`, where the caller-painted
|
|
54
|
+
* glyph wins over `icon`. Returns `null` when neither is set, so a call site
|
|
55
|
+
* stays a single expression.
|
|
56
|
+
*/
|
|
57
|
+
export function resolveIconSlot(
|
|
58
|
+
render: BloomIconRenderer | undefined,
|
|
59
|
+
size: number,
|
|
60
|
+
color: string,
|
|
61
|
+
fallback: () => ReactNode,
|
|
62
|
+
): ReactNode {
|
|
63
|
+
if (render) return render({ size, color });
|
|
64
|
+
return fallback();
|
|
65
|
+
}
|
|
@@ -416,7 +416,12 @@ function PageHeaderComponent({
|
|
|
416
416
|
style={[
|
|
417
417
|
styles.container,
|
|
418
418
|
{ paddingTop: padTop },
|
|
419
|
-
overlay ? [styles.overlay, {
|
|
419
|
+
overlay ? [styles.overlay, {
|
|
420
|
+
zIndex: Z_INDEX.floating,
|
|
421
|
+
// Android's native scroll surfaces also participate in elevation.
|
|
422
|
+
// Keep this local chrome above its sibling list without a portal.
|
|
423
|
+
...(!isWeb ? { elevation: Z_INDEX.floating } : {}),
|
|
424
|
+
}] : null,
|
|
420
425
|
containerWeb,
|
|
421
426
|
placement === 'overlap' ? { marginBottom: -height, zIndex: Z_INDEX.floating } : null,
|
|
422
427
|
style,
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
Pressable,
|
|
31
31
|
StyleSheet,
|
|
32
32
|
View,
|
|
33
|
+
type GestureResponderEvent,
|
|
33
34
|
type ViewStyle,
|
|
34
35
|
} from 'react-native';
|
|
35
36
|
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
|
@@ -112,6 +113,28 @@ type BarContextValue = {
|
|
|
112
113
|
|
|
113
114
|
const BarContext = createContext<BarContextValue | null>(null);
|
|
114
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Whether the bar's `Tap` gesture has already reported the touch a button's
|
|
118
|
+
* press came from. The gesture activates on RELEASE, after the `Pressable` has
|
|
119
|
+
* fired, and it cancels that press on iOS alone:
|
|
120
|
+
*
|
|
121
|
+
* - iOS: RNGH sets `cancelsTouchesInView`, so a press that still arrives came
|
|
122
|
+
* from something the gesture never saw (VoiceOver, a hardware keyboard).
|
|
123
|
+
* - Android: nothing is cancelled. Measured on a Pixel 10 Pro, one tap produced
|
|
124
|
+
* `PRESSABLE selectIndex(1)` AND `GESTURE tap selectIndex(1)` ~600ms apart.
|
|
125
|
+
* A press there cannot say whether it was a touch, so every press is taken
|
|
126
|
+
* for one.
|
|
127
|
+
* - Web: nothing is cancelled either. Measured in Mention's web build, one
|
|
128
|
+
* mouse click reached `onIndexChange` twice. react-native-web says which
|
|
129
|
+
* input it was: a pointer press arrives as the DOM `click`, and a keyboard
|
|
130
|
+
* Enter or Space as `keyup`, which the gesture never sees.
|
|
131
|
+
*/
|
|
132
|
+
function gestureReportedPress(event: GestureResponderEvent): boolean {
|
|
133
|
+
if (Platform.OS === 'android') return true;
|
|
134
|
+
if (Platform.OS === 'web') return (event.nativeEvent as { type?: string }).type === 'click';
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
115
138
|
function OptionalGesture({ enabled, children, ...props }: ComponentProps<typeof GestureDetector> & { enabled: boolean }) {
|
|
116
139
|
return enabled ? <GestureDetector {...props}>{children}</GestureDetector> : <Fragment>{children}</Fragment>;
|
|
117
140
|
}
|
|
@@ -699,8 +722,8 @@ function TabBarButtonBody({
|
|
|
699
722
|
if (isFocused === undefined) bar?.selectIndex(index);
|
|
700
723
|
}}
|
|
701
724
|
onPress={(event) => {
|
|
702
|
-
//
|
|
703
|
-
// for
|
|
725
|
+
// Fires for keyboard and assistive-technology activation, and on
|
|
726
|
+
// Android and web also for the pointer taps the gesture reports.
|
|
704
727
|
if (bar) {
|
|
705
728
|
// Appear at the tab when hidden, slide to it when visible — the same
|
|
706
729
|
// rule the tap gesture and the controlled path follow. Skipped on the
|
|
@@ -713,20 +736,20 @@ function TabBarButtonBody({
|
|
|
713
736
|
bar.highlightOpacity.value = withTiming(1, HIGHLIGHT_FADE);
|
|
714
737
|
}
|
|
715
738
|
setMinimized(minimized, 0);
|
|
716
|
-
//
|
|
717
|
-
//
|
|
718
|
-
//
|
|
719
|
-
|
|
720
|
-
//
|
|
721
|
-
// `
|
|
722
|
-
//
|
|
723
|
-
|
|
724
|
-
//
|
|
725
|
-
//
|
|
726
|
-
//
|
|
727
|
-
// there
|
|
728
|
-
|
|
729
|
-
onPress?.(event);
|
|
739
|
+
// A tap the bar's gesture already reported is not reported again
|
|
740
|
+
// (`gestureReportedPress`). A scrollable bar has no gesture, so its
|
|
741
|
+
// presses are the only reporter.
|
|
742
|
+
const reported = bar != null && !bar.scrollable && gestureReportedPress(event);
|
|
743
|
+
// Controlled path only. On the focus-driven path the trigger's own
|
|
744
|
+
// `onPress` below performs the navigation, so reporting the selection
|
|
745
|
+
// here as well would navigate twice.
|
|
746
|
+
if (isFocused === undefined && !reported) bar?.selectIndex(index);
|
|
747
|
+
// The trigger's `onPress` is for what the gesture cannot see (keyboard,
|
|
748
|
+
// assistive technology). On web a pointer click has already navigated
|
|
749
|
+
// through `onIndexChange`, so it stands down. Android keeps calling it:
|
|
750
|
+
// a press there cannot tell a touch from a keyboard or D-pad, and
|
|
751
|
+
// dropping it would strand keyboard navigation.
|
|
752
|
+
if (!(reported && Platform.OS === 'web')) onPress?.(event);
|
|
730
753
|
}}
|
|
731
754
|
// `Pressable`'s `style` also accepts a function of the press state; both
|
|
732
755
|
// forms are composed on top of the flex-share base so a caller can tint
|
package/src/tabs/Tabs.tsx
CHANGED
|
@@ -780,6 +780,9 @@ const TabsBarComponent = forwardRef<TabsDragController, TabsProps>(function Tabs
|
|
|
780
780
|
ref={scrollRef}
|
|
781
781
|
{...stripA11y}
|
|
782
782
|
horizontal
|
|
783
|
+
// A tab strip owns its intrinsic cross-axis height. RN ScrollView's
|
|
784
|
+
// default flexGrow: 1 otherwise consumes the list's remaining space.
|
|
785
|
+
style={{ flexGrow: 0, flexShrink: 0 }}
|
|
783
786
|
showsHorizontalScrollIndicator={false}
|
|
784
787
|
contentContainerStyle={[containerStyle, style]}
|
|
785
788
|
onLayout={(e) => {
|