@oxyhq/bloom 0.12.0 → 0.13.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/lib/commonjs/fab/Fab.js +26 -4
- package/lib/commonjs/fab/Fab.js.map +1 -1
- package/lib/commonjs/fab/Fab.web.js +48 -15
- package/lib/commonjs/fab/Fab.web.js.map +1 -1
- package/lib/module/fab/Fab.js +26 -4
- package/lib/module/fab/Fab.js.map +1 -1
- package/lib/module/fab/Fab.web.js +48 -15
- package/lib/module/fab/Fab.web.js.map +1 -1
- package/lib/typescript/commonjs/fab/Fab.d.ts.map +1 -1
- package/lib/typescript/commonjs/fab/Fab.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/fab/types.d.ts +22 -8
- package/lib/typescript/commonjs/fab/types.d.ts.map +1 -1
- package/lib/typescript/module/fab/Fab.d.ts.map +1 -1
- package/lib/typescript/module/fab/Fab.web.d.ts.map +1 -1
- package/lib/typescript/module/fab/types.d.ts +22 -8
- package/lib/typescript/module/fab/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/Fab.test.tsx +17 -0
- package/src/__tests__/Fab.web.test.tsx +27 -0
- package/src/fab/Fab.stories.tsx +9 -0
- package/src/fab/Fab.tsx +34 -5
- package/src/fab/Fab.web.tsx +55 -16
- package/src/fab/types.ts +22 -8
package/lib/commonjs/fab/Fab.js
CHANGED
|
@@ -11,10 +11,13 @@ var _usePressAnimation = require("../hooks/usePressAnimation.js");
|
|
|
11
11
|
var _useInteractionState = require("../hooks/useInteractionState.js");
|
|
12
12
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
13
13
|
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|
|
14
|
+
// Material-style FAB scale. `small` (40px) keeps a full 24px icon box so the
|
|
15
|
+
// glyph reads clearly; `medium` (56px) is the canonical FAB; `large` (64px) is
|
|
16
|
+
// the high-prominence action.
|
|
14
17
|
const SIZE_CONFIG = {
|
|
15
18
|
small: {
|
|
16
19
|
diameter: 40,
|
|
17
|
-
iconBox:
|
|
20
|
+
iconBox: 24,
|
|
18
21
|
fontSize: 14
|
|
19
22
|
},
|
|
20
23
|
medium: {
|
|
@@ -28,6 +31,25 @@ const SIZE_CONFIG = {
|
|
|
28
31
|
fontSize: 16
|
|
29
32
|
}
|
|
30
33
|
};
|
|
34
|
+
const MIN_NUMERIC_ICON_BOX = 22;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolve a `size` prop (preset name or raw pixel diameter) to concrete pixel
|
|
38
|
+
* geometry. A numeric size sets the diameter directly, derives the icon box as
|
|
39
|
+
* `round(size * 0.5)` (clamped to a sensible minimum) and lets the circle radius
|
|
40
|
+
* stay `size / 2` (the container uses a full pill radius, so any square stays a
|
|
41
|
+
* perfect circle).
|
|
42
|
+
*/
|
|
43
|
+
function resolveSize(size) {
|
|
44
|
+
if (typeof size === 'number') {
|
|
45
|
+
return {
|
|
46
|
+
diameter: size,
|
|
47
|
+
iconBox: Math.max(MIN_NUMERIC_ICON_BOX, Math.round(size * 0.5)),
|
|
48
|
+
fontSize: Math.max(13, Math.round(size * 0.27))
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return SIZE_CONFIG[size];
|
|
52
|
+
}
|
|
31
53
|
const PILL_RADIUS = 999;
|
|
32
54
|
const PRESS_SCALE = 0.94;
|
|
33
55
|
const DEFAULT_OFFSET = 16;
|
|
@@ -81,7 +103,7 @@ const FabComponent = ({
|
|
|
81
103
|
zIndex = DEFAULT_Z_INDEX
|
|
82
104
|
}) => {
|
|
83
105
|
const theme = (0, _useTheme.useTheme)();
|
|
84
|
-
const sizeConfig =
|
|
106
|
+
const sizeConfig = (0, _react.useMemo)(() => resolveSize(size), [size]);
|
|
85
107
|
const isExtended = label != null && label.length > 0;
|
|
86
108
|
const content = icon ?? children;
|
|
87
109
|
const {
|
|
@@ -114,13 +136,13 @@ const FabComponent = ({
|
|
|
114
136
|
elevation: 6
|
|
115
137
|
};
|
|
116
138
|
if (isExtended) {
|
|
117
|
-
base.paddingHorizontal =
|
|
139
|
+
base.paddingHorizontal = sizeConfig.diameter <= 44 ? 14 : 20;
|
|
118
140
|
base.gap = 8;
|
|
119
141
|
} else {
|
|
120
142
|
base.width = sizeConfig.diameter;
|
|
121
143
|
}
|
|
122
144
|
return base;
|
|
123
|
-
}, [resolvedColors.background, sizeConfig.diameter, theme.colors.shadow, isExtended
|
|
145
|
+
}, [resolvedColors.background, sizeConfig.diameter, theme.colors.shadow, isExtended]);
|
|
124
146
|
const labelTextStyle = (0, _react.useMemo)(() => ({
|
|
125
147
|
fontSize: sizeConfig.fontSize,
|
|
126
148
|
fontWeight: '600',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["_react","_interopRequireWildcard","require","_reactNative","_useTheme","_usePressAnimation","_useInteractionState","_jsxRuntime","e","t","WeakMap","r","n","__esModule","o","i","f","__proto__","default","has","get","set","hasOwnProperty","call","Object","defineProperty","getOwnPropertyDescriptor","SIZE_CONFIG","small","diameter","iconBox","fontSize","medium","large","PILL_RADIUS","PRESS_SCALE","DEFAULT_OFFSET","DEFAULT_Z_INDEX","HIT_SLOP","top","bottom","left","right","placementStyle","placement","offset","style","position","FabComponent","onPress","icon","children","label","variant","
|
|
1
|
+
{"version":3,"names":["_react","_interopRequireWildcard","require","_reactNative","_useTheme","_usePressAnimation","_useInteractionState","_jsxRuntime","e","t","WeakMap","r","n","__esModule","o","i","f","__proto__","default","has","get","set","hasOwnProperty","call","Object","defineProperty","getOwnPropertyDescriptor","SIZE_CONFIG","small","diameter","iconBox","fontSize","medium","large","MIN_NUMERIC_ICON_BOX","resolveSize","size","Math","max","round","PILL_RADIUS","PRESS_SCALE","DEFAULT_OFFSET","DEFAULT_Z_INDEX","HIT_SLOP","top","bottom","left","right","placementStyle","placement","offset","style","position","FabComponent","onPress","icon","children","label","variant","disabled","accessibilityLabel","accessibilityHint","labelStyle","className","testID","zIndex","theme","useTheme","sizeConfig","useMemo","isExtended","length","content","scaleAnim","onPressIn","onPressOut","usePressAnimation","undefined","state","pressed","onIn","onPressedIn","onOut","onPressedOut","useInteractionState","resolvedColors","resolveVariant","colors","containerStyle","base","alignItems","justifyContent","flexDirection","backgroundColor","background","borderRadius","height","shadowColor","shadow","shadowOffset","width","shadowOpacity","shadowRadius","elevation","paddingHorizontal","gap","labelTextStyle","fontWeight","color","foreground","handlePressIn","handlePressOut","jsx","Animated","View","transform","scale","jsxs","Pressable","opacity","hitSlop","accessibilityRole","accessibilityState","Text","numberOfLines","card","text","primary","primaryForeground","Fab","exports","memo","displayName"],"sourceRoot":"../../../src","sources":["fab/Fab.tsx"],"mappings":"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","ignoreList":[]}
|
|
@@ -8,10 +8,13 @@ var _react = _interopRequireWildcard(require("react"));
|
|
|
8
8
|
var _useTheme = require("../theme/use-theme.js");
|
|
9
9
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
10
10
|
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|
|
11
|
+
// Material-style FAB scale. `small` (40px) keeps a full 24px icon box so the
|
|
12
|
+
// glyph reads clearly; `medium` (56px) is the canonical FAB; `large` (64px) is
|
|
13
|
+
// the high-prominence action.
|
|
11
14
|
const SIZE_CONFIG = {
|
|
12
15
|
small: {
|
|
13
16
|
diameter: 40,
|
|
14
|
-
iconBox:
|
|
17
|
+
iconBox: 24,
|
|
15
18
|
fontSize: 14
|
|
16
19
|
},
|
|
17
20
|
medium: {
|
|
@@ -25,6 +28,24 @@ const SIZE_CONFIG = {
|
|
|
25
28
|
fontSize: 16
|
|
26
29
|
}
|
|
27
30
|
};
|
|
31
|
+
const MIN_NUMERIC_ICON_BOX = 22;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Resolve a `size` prop (preset name or raw pixel diameter) to concrete pixel
|
|
35
|
+
* geometry. Mirrors the native `resolveSize` exactly so a numeric size renders
|
|
36
|
+
* identically on both platforms. A numeric size sets the diameter directly and
|
|
37
|
+
* derives the icon box as `round(size * 0.5)` (clamped to a sensible minimum).
|
|
38
|
+
*/
|
|
39
|
+
function resolveSize(size) {
|
|
40
|
+
if (typeof size === 'number') {
|
|
41
|
+
return {
|
|
42
|
+
diameter: size,
|
|
43
|
+
iconBox: Math.max(MIN_NUMERIC_ICON_BOX, Math.round(size * 0.5)),
|
|
44
|
+
fontSize: Math.max(13, Math.round(size * 0.27))
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return SIZE_CONFIG[size];
|
|
48
|
+
}
|
|
28
49
|
const PILL_RADIUS = 999;
|
|
29
50
|
const PRESS_SCALE = 0.94;
|
|
30
51
|
const DEFAULT_OFFSET = 16;
|
|
@@ -95,26 +116,37 @@ function useFabCss() {
|
|
|
95
116
|
* CENTRAL CONTENT COLUMN as it scrolls and never escapes to the viewport edge /
|
|
96
117
|
* over a side rail in a constrained multi-column app layout.
|
|
97
118
|
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
119
|
+
* Pinning a FAB to the BOTTOM of a column needs TWO mechanisms working together,
|
|
120
|
+
* because sticky alone is not enough:
|
|
121
|
+
*
|
|
122
|
+
* 1. `margin-top: auto` — in a flex COLUMN this consumes all the free vertical
|
|
123
|
+
* space, pushing the FAB to the bottom of the container EVEN WHEN THE
|
|
124
|
+
* CONTENT IS SHORT (e.g. an empty feed / loading spinner). Without it, a
|
|
125
|
+
* short column leaves the FAB at its natural flow position (mid-column) —
|
|
126
|
+
* which is exactly the "floating in the middle / broken" bug.
|
|
127
|
+
* 2. `position: sticky; bottom` — when the content is TALL and the column
|
|
128
|
+
* scrolls, sticky keeps the FAB pinned to the bottom of the viewport as the
|
|
129
|
+
* user scrolls, instead of scrolling away with the content.
|
|
130
|
+
*
|
|
131
|
+
* Horizontal placement uses `align-self` (sticky elements can't be pushed by
|
|
132
|
+
* `left`/`right` the way absolute ones are) plus an inline-edge margin for the
|
|
133
|
+
* `offset`. The consumer column MUST be a flex column that fills the available
|
|
134
|
+
* height, and the FAB MUST be its last child — documented in the usage story.
|
|
103
135
|
*/
|
|
104
136
|
function placementStyle(placement, offset) {
|
|
105
137
|
if (placement === 'static') return {};
|
|
106
138
|
const style = {
|
|
107
139
|
position: 'sticky'
|
|
108
140
|
};
|
|
109
|
-
|
|
141
|
+
const isBottom = placement === 'bottom-right' || placement === 'bottom-left';
|
|
142
|
+
if (isBottom) {
|
|
110
143
|
style.bottom = offset;
|
|
144
|
+
// Push the FAB to the bottom of a (flex-column) container even when the
|
|
145
|
+
// content is too short to scroll, so it never floats mid-column.
|
|
146
|
+
style.marginTop = 'auto';
|
|
111
147
|
} else {
|
|
112
148
|
style.top = offset;
|
|
113
149
|
}
|
|
114
|
-
// Sticky elements can't be pushed horizontally by left/right the way absolute
|
|
115
|
-
// ones are; we self-align within the (flex column) container instead, and add
|
|
116
|
-
// a horizontal margin for the offset so the FAB sits `offset` px from the
|
|
117
|
-
// column's inline edge.
|
|
118
150
|
if (placement === 'bottom-right' || placement === 'top-right') {
|
|
119
151
|
style.alignSelf = 'flex-end';
|
|
120
152
|
style.marginRight = offset;
|
|
@@ -169,7 +201,7 @@ const FabWebComponent = ({
|
|
|
169
201
|
const theme = (0, _useTheme.useTheme)();
|
|
170
202
|
const reactId = (0, _react.useId)();
|
|
171
203
|
const resolvedId = id ?? `bloom-fab-${reactId}`;
|
|
172
|
-
const sizeConfig =
|
|
204
|
+
const sizeConfig = (0, _react.useMemo)(() => resolveSize(size), [size]);
|
|
173
205
|
const isExtended = label != null && label.length > 0;
|
|
174
206
|
const content = icon ?? children;
|
|
175
207
|
const variantColors = (0, _react.useMemo)(() => resolveVariant(variant, theme.colors), [variant, theme.colors]);
|
|
@@ -194,13 +226,14 @@ const FabWebComponent = ({
|
|
|
194
226
|
...placementStyle(placement, offset)
|
|
195
227
|
};
|
|
196
228
|
if (isExtended) {
|
|
197
|
-
|
|
198
|
-
base.
|
|
229
|
+
const pad = sizeConfig.diameter <= 44 ? 14 : 20;
|
|
230
|
+
base.paddingLeft = pad;
|
|
231
|
+
base.paddingRight = pad;
|
|
199
232
|
} else {
|
|
200
233
|
base.width = sizeConfig.diameter;
|
|
201
234
|
}
|
|
202
235
|
return base;
|
|
203
|
-
}, [variantColors, sizeConfig.diameter, sizeConfig.fontSize, zIndex, restShadow, hoverShadow, placement, offset, isExtended
|
|
236
|
+
}, [variantColors, sizeConfig.diameter, sizeConfig.fontSize, zIndex, restShadow, hoverShadow, placement, offset, isExtended]);
|
|
204
237
|
const handleClick = (0, _react.useCallback)(event => {
|
|
205
238
|
if (disabled) {
|
|
206
239
|
event.preventDefault();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["_react","_interopRequireWildcard","require","_useTheme","_jsxRuntime","e","t","WeakMap","r","n","__esModule","o","i","f","__proto__","default","has","get","set","hasOwnProperty","call","Object","defineProperty","getOwnPropertyDescriptor","SIZE_CONFIG","small","diameter","iconBox","fontSize","medium","large","PILL_RADIUS","PRESS_SCALE","DEFAULT_OFFSET","DEFAULT_Z_INDEX","STYLE_ID","BLOOM_FAB_CSS","useFabCss","useEffect","document","getElementById","style","createElement","id","textContent","head","appendChild","placementStyle","placement","offset","position","bottom","top","alignSelf","marginRight","marginLeft","resolveVariant","variant","c","background","card","foreground","text","ring","primary","primaryForeground","FabWebComponent","onPress","onClick","icon","children","label","
|
|
1
|
+
{"version":3,"names":["_react","_interopRequireWildcard","require","_useTheme","_jsxRuntime","e","t","WeakMap","r","n","__esModule","o","i","f","__proto__","default","has","get","set","hasOwnProperty","call","Object","defineProperty","getOwnPropertyDescriptor","SIZE_CONFIG","small","diameter","iconBox","fontSize","medium","large","MIN_NUMERIC_ICON_BOX","resolveSize","size","Math","max","round","PILL_RADIUS","PRESS_SCALE","DEFAULT_OFFSET","DEFAULT_Z_INDEX","STYLE_ID","BLOOM_FAB_CSS","useFabCss","useEffect","document","getElementById","style","createElement","id","textContent","head","appendChild","placementStyle","placement","offset","position","isBottom","bottom","marginTop","top","alignSelf","marginRight","marginLeft","resolveVariant","variant","c","background","card","foreground","text","ring","primary","primaryForeground","FabWebComponent","onPress","onClick","icon","children","label","disabled","accessibilityLabel","ariaLabelProp","accessibilityHint","labelStyle","className","testID","zIndex","title","type","theme","useTheme","reactId","useId","resolvedId","sizeConfig","useMemo","isExtended","length","content","variantColors","colors","restShadow","shadow","hoverShadow","containerStyle","base","backgroundColor","color","borderRadius","height","boxShadow","fontWeight","pad","paddingLeft","paddingRight","width","handleClick","useCallback","event","preventDefault","ariaLabel","composedClassName","concat","join","jsxs","undefined","jsx","display","alignItems","justifyContent","Fab","exports","memo","displayName"],"sourceRoot":"../../../src","sources":["fab/Fab.web.tsx"],"mappings":"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","ignoreList":[]}
|
package/lib/module/fab/Fab.js
CHANGED
|
@@ -6,10 +6,13 @@ import { useTheme } from "../theme/use-theme.js";
|
|
|
6
6
|
import { usePressAnimation } from "../hooks/usePressAnimation.js";
|
|
7
7
|
import { useInteractionState } from "../hooks/useInteractionState.js";
|
|
8
8
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
9
|
+
// Material-style FAB scale. `small` (40px) keeps a full 24px icon box so the
|
|
10
|
+
// glyph reads clearly; `medium` (56px) is the canonical FAB; `large` (64px) is
|
|
11
|
+
// the high-prominence action.
|
|
9
12
|
const SIZE_CONFIG = {
|
|
10
13
|
small: {
|
|
11
14
|
diameter: 40,
|
|
12
|
-
iconBox:
|
|
15
|
+
iconBox: 24,
|
|
13
16
|
fontSize: 14
|
|
14
17
|
},
|
|
15
18
|
medium: {
|
|
@@ -23,6 +26,25 @@ const SIZE_CONFIG = {
|
|
|
23
26
|
fontSize: 16
|
|
24
27
|
}
|
|
25
28
|
};
|
|
29
|
+
const MIN_NUMERIC_ICON_BOX = 22;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolve a `size` prop (preset name or raw pixel diameter) to concrete pixel
|
|
33
|
+
* geometry. A numeric size sets the diameter directly, derives the icon box as
|
|
34
|
+
* `round(size * 0.5)` (clamped to a sensible minimum) and lets the circle radius
|
|
35
|
+
* stay `size / 2` (the container uses a full pill radius, so any square stays a
|
|
36
|
+
* perfect circle).
|
|
37
|
+
*/
|
|
38
|
+
function resolveSize(size) {
|
|
39
|
+
if (typeof size === 'number') {
|
|
40
|
+
return {
|
|
41
|
+
diameter: size,
|
|
42
|
+
iconBox: Math.max(MIN_NUMERIC_ICON_BOX, Math.round(size * 0.5)),
|
|
43
|
+
fontSize: Math.max(13, Math.round(size * 0.27))
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return SIZE_CONFIG[size];
|
|
47
|
+
}
|
|
26
48
|
const PILL_RADIUS = 999;
|
|
27
49
|
const PRESS_SCALE = 0.94;
|
|
28
50
|
const DEFAULT_OFFSET = 16;
|
|
@@ -76,7 +98,7 @@ const FabComponent = ({
|
|
|
76
98
|
zIndex = DEFAULT_Z_INDEX
|
|
77
99
|
}) => {
|
|
78
100
|
const theme = useTheme();
|
|
79
|
-
const sizeConfig =
|
|
101
|
+
const sizeConfig = useMemo(() => resolveSize(size), [size]);
|
|
80
102
|
const isExtended = label != null && label.length > 0;
|
|
81
103
|
const content = icon ?? children;
|
|
82
104
|
const {
|
|
@@ -109,13 +131,13 @@ const FabComponent = ({
|
|
|
109
131
|
elevation: 6
|
|
110
132
|
};
|
|
111
133
|
if (isExtended) {
|
|
112
|
-
base.paddingHorizontal =
|
|
134
|
+
base.paddingHorizontal = sizeConfig.diameter <= 44 ? 14 : 20;
|
|
113
135
|
base.gap = 8;
|
|
114
136
|
} else {
|
|
115
137
|
base.width = sizeConfig.diameter;
|
|
116
138
|
}
|
|
117
139
|
return base;
|
|
118
|
-
}, [resolvedColors.background, sizeConfig.diameter, theme.colors.shadow, isExtended
|
|
140
|
+
}, [resolvedColors.background, sizeConfig.diameter, theme.colors.shadow, isExtended]);
|
|
119
141
|
const labelTextStyle = useMemo(() => ({
|
|
120
142
|
fontSize: sizeConfig.fontSize,
|
|
121
143
|
fontWeight: '600',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["React","memo","useMemo","Animated","Pressable","Text","View","useTheme","usePressAnimation","useInteractionState","jsx","_jsx","jsxs","_jsxs","SIZE_CONFIG","small","diameter","iconBox","fontSize","medium","large","PILL_RADIUS","PRESS_SCALE","DEFAULT_OFFSET","DEFAULT_Z_INDEX","HIT_SLOP","top","bottom","left","right","placementStyle","placement","offset","style","position","FabComponent","onPress","icon","children","label","variant","
|
|
1
|
+
{"version":3,"names":["React","memo","useMemo","Animated","Pressable","Text","View","useTheme","usePressAnimation","useInteractionState","jsx","_jsx","jsxs","_jsxs","SIZE_CONFIG","small","diameter","iconBox","fontSize","medium","large","MIN_NUMERIC_ICON_BOX","resolveSize","size","Math","max","round","PILL_RADIUS","PRESS_SCALE","DEFAULT_OFFSET","DEFAULT_Z_INDEX","HIT_SLOP","top","bottom","left","right","placementStyle","placement","offset","style","position","FabComponent","onPress","icon","children","label","variant","disabled","accessibilityLabel","accessibilityHint","labelStyle","className","testID","zIndex","theme","sizeConfig","isExtended","length","content","scaleAnim","onPressIn","onPressOut","undefined","state","pressed","onIn","onPressedIn","onOut","onPressedOut","resolvedColors","resolveVariant","colors","containerStyle","base","alignItems","justifyContent","flexDirection","backgroundColor","background","borderRadius","height","shadowColor","shadow","shadowOffset","width","shadowOpacity","shadowRadius","elevation","paddingHorizontal","gap","labelTextStyle","fontWeight","color","foreground","handlePressIn","handlePressOut","transform","scale","opacity","hitSlop","accessibilityRole","accessibilityState","numberOfLines","card","text","primary","primaryForeground","Fab","displayName"],"sourceRoot":"../../../src","sources":["fab/Fab.tsx"],"mappings":"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","ignoreList":[]}
|
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
import React, { memo, useCallback, useEffect, useId, useMemo } from 'react';
|
|
4
4
|
import { useTheme } from "../theme/use-theme.js";
|
|
5
5
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
6
|
+
// Material-style FAB scale. `small` (40px) keeps a full 24px icon box so the
|
|
7
|
+
// glyph reads clearly; `medium` (56px) is the canonical FAB; `large` (64px) is
|
|
8
|
+
// the high-prominence action.
|
|
6
9
|
const SIZE_CONFIG = {
|
|
7
10
|
small: {
|
|
8
11
|
diameter: 40,
|
|
9
|
-
iconBox:
|
|
12
|
+
iconBox: 24,
|
|
10
13
|
fontSize: 14
|
|
11
14
|
},
|
|
12
15
|
medium: {
|
|
@@ -20,6 +23,24 @@ const SIZE_CONFIG = {
|
|
|
20
23
|
fontSize: 16
|
|
21
24
|
}
|
|
22
25
|
};
|
|
26
|
+
const MIN_NUMERIC_ICON_BOX = 22;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resolve a `size` prop (preset name or raw pixel diameter) to concrete pixel
|
|
30
|
+
* geometry. Mirrors the native `resolveSize` exactly so a numeric size renders
|
|
31
|
+
* identically on both platforms. A numeric size sets the diameter directly and
|
|
32
|
+
* derives the icon box as `round(size * 0.5)` (clamped to a sensible minimum).
|
|
33
|
+
*/
|
|
34
|
+
function resolveSize(size) {
|
|
35
|
+
if (typeof size === 'number') {
|
|
36
|
+
return {
|
|
37
|
+
diameter: size,
|
|
38
|
+
iconBox: Math.max(MIN_NUMERIC_ICON_BOX, Math.round(size * 0.5)),
|
|
39
|
+
fontSize: Math.max(13, Math.round(size * 0.27))
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return SIZE_CONFIG[size];
|
|
43
|
+
}
|
|
23
44
|
const PILL_RADIUS = 999;
|
|
24
45
|
const PRESS_SCALE = 0.94;
|
|
25
46
|
const DEFAULT_OFFSET = 16;
|
|
@@ -90,26 +111,37 @@ function useFabCss() {
|
|
|
90
111
|
* CENTRAL CONTENT COLUMN as it scrolls and never escapes to the viewport edge /
|
|
91
112
|
* over a side rail in a constrained multi-column app layout.
|
|
92
113
|
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
114
|
+
* Pinning a FAB to the BOTTOM of a column needs TWO mechanisms working together,
|
|
115
|
+
* because sticky alone is not enough:
|
|
116
|
+
*
|
|
117
|
+
* 1. `margin-top: auto` — in a flex COLUMN this consumes all the free vertical
|
|
118
|
+
* space, pushing the FAB to the bottom of the container EVEN WHEN THE
|
|
119
|
+
* CONTENT IS SHORT (e.g. an empty feed / loading spinner). Without it, a
|
|
120
|
+
* short column leaves the FAB at its natural flow position (mid-column) —
|
|
121
|
+
* which is exactly the "floating in the middle / broken" bug.
|
|
122
|
+
* 2. `position: sticky; bottom` — when the content is TALL and the column
|
|
123
|
+
* scrolls, sticky keeps the FAB pinned to the bottom of the viewport as the
|
|
124
|
+
* user scrolls, instead of scrolling away with the content.
|
|
125
|
+
*
|
|
126
|
+
* Horizontal placement uses `align-self` (sticky elements can't be pushed by
|
|
127
|
+
* `left`/`right` the way absolute ones are) plus an inline-edge margin for the
|
|
128
|
+
* `offset`. The consumer column MUST be a flex column that fills the available
|
|
129
|
+
* height, and the FAB MUST be its last child — documented in the usage story.
|
|
98
130
|
*/
|
|
99
131
|
function placementStyle(placement, offset) {
|
|
100
132
|
if (placement === 'static') return {};
|
|
101
133
|
const style = {
|
|
102
134
|
position: 'sticky'
|
|
103
135
|
};
|
|
104
|
-
|
|
136
|
+
const isBottom = placement === 'bottom-right' || placement === 'bottom-left';
|
|
137
|
+
if (isBottom) {
|
|
105
138
|
style.bottom = offset;
|
|
139
|
+
// Push the FAB to the bottom of a (flex-column) container even when the
|
|
140
|
+
// content is too short to scroll, so it never floats mid-column.
|
|
141
|
+
style.marginTop = 'auto';
|
|
106
142
|
} else {
|
|
107
143
|
style.top = offset;
|
|
108
144
|
}
|
|
109
|
-
// Sticky elements can't be pushed horizontally by left/right the way absolute
|
|
110
|
-
// ones are; we self-align within the (flex column) container instead, and add
|
|
111
|
-
// a horizontal margin for the offset so the FAB sits `offset` px from the
|
|
112
|
-
// column's inline edge.
|
|
113
145
|
if (placement === 'bottom-right' || placement === 'top-right') {
|
|
114
146
|
style.alignSelf = 'flex-end';
|
|
115
147
|
style.marginRight = offset;
|
|
@@ -164,7 +196,7 @@ const FabWebComponent = ({
|
|
|
164
196
|
const theme = useTheme();
|
|
165
197
|
const reactId = useId();
|
|
166
198
|
const resolvedId = id ?? `bloom-fab-${reactId}`;
|
|
167
|
-
const sizeConfig =
|
|
199
|
+
const sizeConfig = useMemo(() => resolveSize(size), [size]);
|
|
168
200
|
const isExtended = label != null && label.length > 0;
|
|
169
201
|
const content = icon ?? children;
|
|
170
202
|
const variantColors = useMemo(() => resolveVariant(variant, theme.colors), [variant, theme.colors]);
|
|
@@ -189,13 +221,14 @@ const FabWebComponent = ({
|
|
|
189
221
|
...placementStyle(placement, offset)
|
|
190
222
|
};
|
|
191
223
|
if (isExtended) {
|
|
192
|
-
|
|
193
|
-
base.
|
|
224
|
+
const pad = sizeConfig.diameter <= 44 ? 14 : 20;
|
|
225
|
+
base.paddingLeft = pad;
|
|
226
|
+
base.paddingRight = pad;
|
|
194
227
|
} else {
|
|
195
228
|
base.width = sizeConfig.diameter;
|
|
196
229
|
}
|
|
197
230
|
return base;
|
|
198
|
-
}, [variantColors, sizeConfig.diameter, sizeConfig.fontSize, zIndex, restShadow, hoverShadow, placement, offset, isExtended
|
|
231
|
+
}, [variantColors, sizeConfig.diameter, sizeConfig.fontSize, zIndex, restShadow, hoverShadow, placement, offset, isExtended]);
|
|
199
232
|
const handleClick = useCallback(event => {
|
|
200
233
|
if (disabled) {
|
|
201
234
|
event.preventDefault();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["React","memo","useCallback","useEffect","useId","useMemo","useTheme","jsx","_jsx","jsxs","_jsxs","SIZE_CONFIG","small","diameter","iconBox","fontSize","medium","large","PILL_RADIUS","PRESS_SCALE","DEFAULT_OFFSET","DEFAULT_Z_INDEX","STYLE_ID","BLOOM_FAB_CSS","useFabCss","document","getElementById","style","createElement","id","textContent","head","appendChild","placementStyle","placement","offset","position","bottom","top","alignSelf","marginRight","marginLeft","resolveVariant","variant","c","background","card","foreground","text","ring","primary","primaryForeground","FabWebComponent","onPress","onClick","icon","children","label","
|
|
1
|
+
{"version":3,"names":["React","memo","useCallback","useEffect","useId","useMemo","useTheme","jsx","_jsx","jsxs","_jsxs","SIZE_CONFIG","small","diameter","iconBox","fontSize","medium","large","MIN_NUMERIC_ICON_BOX","resolveSize","size","Math","max","round","PILL_RADIUS","PRESS_SCALE","DEFAULT_OFFSET","DEFAULT_Z_INDEX","STYLE_ID","BLOOM_FAB_CSS","useFabCss","document","getElementById","style","createElement","id","textContent","head","appendChild","placementStyle","placement","offset","position","isBottom","bottom","marginTop","top","alignSelf","marginRight","marginLeft","resolveVariant","variant","c","background","card","foreground","text","ring","primary","primaryForeground","FabWebComponent","onPress","onClick","icon","children","label","disabled","accessibilityLabel","ariaLabelProp","accessibilityHint","labelStyle","className","testID","zIndex","title","type","theme","reactId","resolvedId","sizeConfig","isExtended","length","content","variantColors","colors","restShadow","shadow","hoverShadow","containerStyle","base","backgroundColor","color","borderRadius","height","boxShadow","fontWeight","pad","paddingLeft","paddingRight","width","handleClick","event","preventDefault","ariaLabel","composedClassName","concat","join","undefined","display","alignItems","justifyContent","Fab","displayName"],"sourceRoot":"../../../src","sources":["fab/Fab.web.tsx"],"mappings":"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","ignoreList":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Fab.d.ts","sourceRoot":"","sources":["../../../../src/fab/Fab.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAwB,MAAM,OAAO,CAAC;AAa7C,OAAO,KAAK,EAAgB,QAAQ,EAAuB,MAAM,SAAS,CAAC;AAE3E,YAAY,EAAE,QAAQ,EAAE,UAAU,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"Fab.d.ts","sourceRoot":"","sources":["../../../../src/fab/Fab.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAwB,MAAM,OAAO,CAAC;AAa7C,OAAO,KAAK,EAAgB,QAAQ,EAAuB,MAAM,SAAS,CAAC;AAE3E,YAAY,EAAE,QAAQ,EAAE,UAAU,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAyM3E,eAAO,MAAM,GAAG,sCAAqB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Fab.web.d.ts","sourceRoot":"","sources":["../../../../src/fab/Fab.web.tsx"],"names":[],"mappings":"AAAA,OAAO,KAQN,MAAM,OAAO,CAAC;AAIf,OAAO,KAAK,EAAgB,QAAQ,EAAuB,MAAM,SAAS,CAAC;AAE3E,YAAY,EAAE,QAAQ,EAAE,UAAU,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"Fab.web.d.ts","sourceRoot":"","sources":["../../../../src/fab/Fab.web.tsx"],"names":[],"mappings":"AAAA,OAAO,KAQN,MAAM,OAAO,CAAC;AAIf,OAAO,KAAK,EAAgB,QAAQ,EAAuB,MAAM,SAAS,CAAC;AAE3E,YAAY,EAAE,QAAQ,EAAE,UAAU,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAyR3E,eAAO,MAAM,GAAG,sCAAwB,CAAC"}
|
|
@@ -10,24 +10,33 @@ import type { StyleProp, ViewStyle, TextStyle } from 'react-native';
|
|
|
10
10
|
*/
|
|
11
11
|
export type FabVariant = 'primary' | 'secondary' | 'surface';
|
|
12
12
|
/**
|
|
13
|
-
* FAB
|
|
13
|
+
* FAB size presets, matching Material-ish conventions but in Bloom's scale:
|
|
14
14
|
* - `small` — 40px (compact / dense layouts)
|
|
15
15
|
* - `medium` — 56px (the canonical FAB; the default)
|
|
16
16
|
* - `large` — 64px (high-prominence primary action)
|
|
17
17
|
*
|
|
18
18
|
* In the `extended` variant the height tracks the size but the FAB grows
|
|
19
19
|
* horizontally to fit its icon + label as a pill.
|
|
20
|
+
*
|
|
21
|
+
* The `size` prop ALSO accepts a raw pixel `number` for an exact diameter
|
|
22
|
+
* between (or outside) the presets — e.g. `size={48}` for a FAB halfway between
|
|
23
|
+
* `small` and `medium`. A numeric size sets the diameter directly, derives the
|
|
24
|
+
* icon box as `round(size * 0.5)` (min 22px) and keeps the circle radius at
|
|
25
|
+
* `size / 2`.
|
|
20
26
|
*/
|
|
21
27
|
export type FabSize = 'small' | 'medium' | 'large';
|
|
22
28
|
/**
|
|
23
29
|
* Where the FAB anchors itself.
|
|
24
30
|
*
|
|
25
31
|
* - `bottom-right` (default) / `bottom-left` / `top-right` / `top-left` —
|
|
26
|
-
* the FAB pins itself to that corner of its CONTAINING block
|
|
27
|
-
* uses `position:
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
32
|
+
* the FAB pins itself to that corner of its CONTAINING block, never the
|
|
33
|
+
* viewport. On native it uses `position: absolute` within the nearest
|
|
34
|
+
* positioned ancestor. On web a `bottom-*` placement combines
|
|
35
|
+
* `margin-top: auto` (pins to the bottom of a flex COLUMN even when the
|
|
36
|
+
* content is short / not scrolling) with `position: sticky; bottom` (keeps
|
|
37
|
+
* it visible while a tall column scrolls). The consumer column MUST be a
|
|
38
|
+
* flex column that fills the available height and the FAB MUST be its last
|
|
39
|
+
* child.
|
|
31
40
|
* - `static` — no positioning at all. The FAB is laid out inline and the
|
|
32
41
|
* consumer is fully responsible for placement (e.g. wrapping it in their
|
|
33
42
|
* own absolutely/sticky-positioned container). Use this when you need
|
|
@@ -35,7 +44,8 @@ export type FabSize = 'small' | 'medium' | 'large';
|
|
|
35
44
|
*
|
|
36
45
|
* CRITICAL: the positioned placements DO NOT use web `position: fixed` and are
|
|
37
46
|
* never anchored to the viewport. They stay inside the containing column so the
|
|
38
|
-
* FAB never escapes a constrained 3-column app layout and sits over a side rail
|
|
47
|
+
* FAB never escapes a constrained 3-column app layout and sits over a side rail,
|
|
48
|
+
* and never float mid-column when the content is short.
|
|
39
49
|
*/
|
|
40
50
|
export type FabPlacement = 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left' | 'static';
|
|
41
51
|
export interface FabProps {
|
|
@@ -57,7 +67,11 @@ export interface FabProps {
|
|
|
57
67
|
*/
|
|
58
68
|
label?: string;
|
|
59
69
|
variant?: FabVariant;
|
|
60
|
-
|
|
70
|
+
/**
|
|
71
|
+
* A preset (`'small' | 'medium' | 'large'`) or a raw pixel diameter
|
|
72
|
+
* (`number`). Defaults to `'medium'`. See {@link FabSize}.
|
|
73
|
+
*/
|
|
74
|
+
size?: FabSize | number;
|
|
61
75
|
/**
|
|
62
76
|
* Corner of the containing block to anchor to. Defaults to `'bottom-right'`.
|
|
63
77
|
* See {@link FabPlacement}. The DEFAULT never escapes the containing column
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/fab/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEpE;;;;;;;;GAQG;AACH,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,CAAC;AAE7D
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/fab/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEpE;;;;;;;;GAQG;AACH,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,CAAC;AAE7D;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,OAAO,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEnD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,MAAM,YAAY,GACpB,cAAc,GACd,aAAa,GACb,WAAW,GACX,UAAU,GACV,QAAQ,CAAC;AAEb,MAAM,WAAW,QAAQ;IACvB,qBAAqB;IACrB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IAErB;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAE3B;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf,OAAO,CAAC,EAAE,UAAU,CAAC;IACrB;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAExB;;;;OAIG;IACH,SAAS,CAAC,EAAE,YAAY,CAAC;IAEzB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB,yEAAyE;IACzE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAE3B,8CAA8C;IAC9C,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,8CAA8C;IAC9C,UAAU,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAClC,8CAA8C;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAUhB,wDAAwD;IACxD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,UAAU,CAAC,iBAAiB,CAAC,KAAK,IAAI,CAAC;IAC/D,mEAAmE;IACnE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4CAA4C;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kCAAkC;IAClC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,6DAA6D;IAC7D,IAAI,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;CACtC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Fab.d.ts","sourceRoot":"","sources":["../../../../src/fab/Fab.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAwB,MAAM,OAAO,CAAC;AAa7C,OAAO,KAAK,EAAgB,QAAQ,EAAuB,MAAM,SAAS,CAAC;AAE3E,YAAY,EAAE,QAAQ,EAAE,UAAU,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"Fab.d.ts","sourceRoot":"","sources":["../../../../src/fab/Fab.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAwB,MAAM,OAAO,CAAC;AAa7C,OAAO,KAAK,EAAgB,QAAQ,EAAuB,MAAM,SAAS,CAAC;AAE3E,YAAY,EAAE,QAAQ,EAAE,UAAU,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAyM3E,eAAO,MAAM,GAAG,sCAAqB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Fab.web.d.ts","sourceRoot":"","sources":["../../../../src/fab/Fab.web.tsx"],"names":[],"mappings":"AAAA,OAAO,KAQN,MAAM,OAAO,CAAC;AAIf,OAAO,KAAK,EAAgB,QAAQ,EAAuB,MAAM,SAAS,CAAC;AAE3E,YAAY,EAAE,QAAQ,EAAE,UAAU,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"Fab.web.d.ts","sourceRoot":"","sources":["../../../../src/fab/Fab.web.tsx"],"names":[],"mappings":"AAAA,OAAO,KAQN,MAAM,OAAO,CAAC;AAIf,OAAO,KAAK,EAAgB,QAAQ,EAAuB,MAAM,SAAS,CAAC;AAE3E,YAAY,EAAE,QAAQ,EAAE,UAAU,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAyR3E,eAAO,MAAM,GAAG,sCAAwB,CAAC"}
|
|
@@ -10,24 +10,33 @@ import type { StyleProp, ViewStyle, TextStyle } from 'react-native';
|
|
|
10
10
|
*/
|
|
11
11
|
export type FabVariant = 'primary' | 'secondary' | 'surface';
|
|
12
12
|
/**
|
|
13
|
-
* FAB
|
|
13
|
+
* FAB size presets, matching Material-ish conventions but in Bloom's scale:
|
|
14
14
|
* - `small` — 40px (compact / dense layouts)
|
|
15
15
|
* - `medium` — 56px (the canonical FAB; the default)
|
|
16
16
|
* - `large` — 64px (high-prominence primary action)
|
|
17
17
|
*
|
|
18
18
|
* In the `extended` variant the height tracks the size but the FAB grows
|
|
19
19
|
* horizontally to fit its icon + label as a pill.
|
|
20
|
+
*
|
|
21
|
+
* The `size` prop ALSO accepts a raw pixel `number` for an exact diameter
|
|
22
|
+
* between (or outside) the presets — e.g. `size={48}` for a FAB halfway between
|
|
23
|
+
* `small` and `medium`. A numeric size sets the diameter directly, derives the
|
|
24
|
+
* icon box as `round(size * 0.5)` (min 22px) and keeps the circle radius at
|
|
25
|
+
* `size / 2`.
|
|
20
26
|
*/
|
|
21
27
|
export type FabSize = 'small' | 'medium' | 'large';
|
|
22
28
|
/**
|
|
23
29
|
* Where the FAB anchors itself.
|
|
24
30
|
*
|
|
25
31
|
* - `bottom-right` (default) / `bottom-left` / `top-right` / `top-left` —
|
|
26
|
-
* the FAB pins itself to that corner of its CONTAINING block
|
|
27
|
-
* uses `position:
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
32
|
+
* the FAB pins itself to that corner of its CONTAINING block, never the
|
|
33
|
+
* viewport. On native it uses `position: absolute` within the nearest
|
|
34
|
+
* positioned ancestor. On web a `bottom-*` placement combines
|
|
35
|
+
* `margin-top: auto` (pins to the bottom of a flex COLUMN even when the
|
|
36
|
+
* content is short / not scrolling) with `position: sticky; bottom` (keeps
|
|
37
|
+
* it visible while a tall column scrolls). The consumer column MUST be a
|
|
38
|
+
* flex column that fills the available height and the FAB MUST be its last
|
|
39
|
+
* child.
|
|
31
40
|
* - `static` — no positioning at all. The FAB is laid out inline and the
|
|
32
41
|
* consumer is fully responsible for placement (e.g. wrapping it in their
|
|
33
42
|
* own absolutely/sticky-positioned container). Use this when you need
|
|
@@ -35,7 +44,8 @@ export type FabSize = 'small' | 'medium' | 'large';
|
|
|
35
44
|
*
|
|
36
45
|
* CRITICAL: the positioned placements DO NOT use web `position: fixed` and are
|
|
37
46
|
* never anchored to the viewport. They stay inside the containing column so the
|
|
38
|
-
* FAB never escapes a constrained 3-column app layout and sits over a side rail
|
|
47
|
+
* FAB never escapes a constrained 3-column app layout and sits over a side rail,
|
|
48
|
+
* and never float mid-column when the content is short.
|
|
39
49
|
*/
|
|
40
50
|
export type FabPlacement = 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left' | 'static';
|
|
41
51
|
export interface FabProps {
|
|
@@ -57,7 +67,11 @@ export interface FabProps {
|
|
|
57
67
|
*/
|
|
58
68
|
label?: string;
|
|
59
69
|
variant?: FabVariant;
|
|
60
|
-
|
|
70
|
+
/**
|
|
71
|
+
* A preset (`'small' | 'medium' | 'large'`) or a raw pixel diameter
|
|
72
|
+
* (`number`). Defaults to `'medium'`. See {@link FabSize}.
|
|
73
|
+
*/
|
|
74
|
+
size?: FabSize | number;
|
|
61
75
|
/**
|
|
62
76
|
* Corner of the containing block to anchor to. Defaults to `'bottom-right'`.
|
|
63
77
|
* See {@link FabPlacement}. The DEFAULT never escapes the containing column
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/fab/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEpE;;;;;;;;GAQG;AACH,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,CAAC;AAE7D
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/fab/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEpE;;;;;;;;GAQG;AACH,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,CAAC;AAE7D;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,OAAO,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEnD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,MAAM,YAAY,GACpB,cAAc,GACd,aAAa,GACb,WAAW,GACX,UAAU,GACV,QAAQ,CAAC;AAEb,MAAM,WAAW,QAAQ;IACvB,qBAAqB;IACrB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IAErB;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAE3B;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf,OAAO,CAAC,EAAE,UAAU,CAAC;IACrB;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAExB;;;;OAIG;IACH,SAAS,CAAC,EAAE,YAAY,CAAC;IAEzB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB,yEAAyE;IACzE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAE3B,8CAA8C;IAC9C,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,8CAA8C;IAC9C,UAAU,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAClC,8CAA8C;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAUhB,wDAAwD;IACxD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,UAAU,CAAC,iBAAiB,CAAC,KAAK,IAAI,CAAC;IAC/D,mEAAmE;IACnE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4CAA4C;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kCAAkC;IAClC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,6DAA6D;IAC7D,IAAI,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;CACtC"}
|
package/package.json
CHANGED
|
@@ -73,4 +73,21 @@ describe('Fab (native)', () => {
|
|
|
73
73
|
);
|
|
74
74
|
expect(getByText('★')).toBeTruthy();
|
|
75
75
|
});
|
|
76
|
+
|
|
77
|
+
it('accepts a numeric size as a raw pixel diameter', () => {
|
|
78
|
+
const { getByTestId } = renderWithTheme(
|
|
79
|
+
<Fab testID="fab" size={48} accessibilityLabel="Add" icon={<Text>+</Text>} />,
|
|
80
|
+
);
|
|
81
|
+
// The Pressable's style is a flat-able array; the container object (first
|
|
82
|
+
// entry) carries the resolved geometry.
|
|
83
|
+
const style = getByTestId('fab').props.style as Array<
|
|
84
|
+
{ width?: number; height?: number } | false
|
|
85
|
+
>;
|
|
86
|
+
const container = style.find(
|
|
87
|
+
(s): s is { width?: number; height?: number } =>
|
|
88
|
+
typeof s === 'object' && s !== null && 'width' in s,
|
|
89
|
+
);
|
|
90
|
+
expect(container?.width).toBe(48);
|
|
91
|
+
expect(container?.height).toBe(48);
|
|
92
|
+
});
|
|
76
93
|
});
|
|
@@ -84,10 +84,23 @@ describe('Fab.web', () => {
|
|
|
84
84
|
expect(fab.style.alignSelf).toBe('flex-end');
|
|
85
85
|
});
|
|
86
86
|
|
|
87
|
+
it('sets margin-top: auto for bottom placements so a short column pins the FAB to the bottom', () => {
|
|
88
|
+
const c = mount(<Fab accessibilityLabel="Add" icon={<span>+</span>} />);
|
|
89
|
+
expect(getByRole(c, 'button').style.marginTop).toBe('auto');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('does NOT set margin-top: auto for top placements', () => {
|
|
93
|
+
const c = mount(<Fab accessibilityLabel="Add" placement="top-right" icon={<span>+</span>} />);
|
|
94
|
+
const fab = getByRole(c, 'button');
|
|
95
|
+
expect(fab.style.marginTop).toBe('');
|
|
96
|
+
expect(fab.style.top).toBe('16px');
|
|
97
|
+
});
|
|
98
|
+
|
|
87
99
|
it('applies no positioning for placement="static"', () => {
|
|
88
100
|
const c = mount(<Fab accessibilityLabel="Add" placement="static" icon={<span>+</span>} />);
|
|
89
101
|
const fab = getByRole(c, 'button');
|
|
90
102
|
expect(fab.style.position).toBe('');
|
|
103
|
+
expect(fab.style.marginTop).toBe('');
|
|
91
104
|
});
|
|
92
105
|
|
|
93
106
|
it('renders an extended label', () => {
|
|
@@ -111,4 +124,18 @@ describe('Fab.web', () => {
|
|
|
111
124
|
const c = mount(<Fab accessibilityLabel="Add" testID="my-fab" icon={<span>+</span>} />);
|
|
112
125
|
expect(c.querySelector('[data-testid="my-fab"]')?.tagName).toBe('BUTTON');
|
|
113
126
|
});
|
|
127
|
+
|
|
128
|
+
it('accepts a numeric size as a raw pixel diameter', () => {
|
|
129
|
+
const c = mount(<Fab accessibilityLabel="Add" size={48} icon={<span>+</span>} />);
|
|
130
|
+
const fab = getByRole(c, 'button');
|
|
131
|
+
expect(fab.style.width).toBe('48px');
|
|
132
|
+
expect(fab.style.height).toBe('48px');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('keeps preset sizes working (medium = 56px diameter)', () => {
|
|
136
|
+
const c = mount(<Fab accessibilityLabel="Add" size="medium" icon={<span>+</span>} />);
|
|
137
|
+
const fab = getByRole(c, 'button');
|
|
138
|
+
expect(fab.style.width).toBe('56px');
|
|
139
|
+
expect(fab.style.height).toBe('56px');
|
|
140
|
+
});
|
|
114
141
|
});
|
package/src/fab/Fab.stories.tsx
CHANGED
|
@@ -47,6 +47,15 @@ export const Extended: Story = {
|
|
|
47
47
|
args: { label: 'Compose' },
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* A raw pixel diameter via `size={number}` — here 48px, halfway between the
|
|
52
|
+
* `small` (40) and `medium` (56) presets. The icon box and font scale with the
|
|
53
|
+
* diameter automatically.
|
|
54
|
+
*/
|
|
55
|
+
export const NumericSize: Story = {
|
|
56
|
+
args: { size: 48 },
|
|
57
|
+
};
|
|
58
|
+
|
|
50
59
|
/**
|
|
51
60
|
* The FAB anchored to the bottom-right of a CONSTRAINED column. On web the FAB
|
|
52
61
|
* uses `position: sticky`, so it tracks the bottom of THIS column (not the
|
package/src/fab/Fab.tsx
CHANGED
|
@@ -15,12 +15,41 @@ import type { FabPlacement, FabProps, FabSize, FabVariant } from './types';
|
|
|
15
15
|
|
|
16
16
|
export type { FabProps, FabVariant, FabSize, FabPlacement } from './types';
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
interface ResolvedSize {
|
|
19
|
+
diameter: number;
|
|
20
|
+
iconBox: number;
|
|
21
|
+
fontSize: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Material-style FAB scale. `small` (40px) keeps a full 24px icon box so the
|
|
25
|
+
// glyph reads clearly; `medium` (56px) is the canonical FAB; `large` (64px) is
|
|
26
|
+
// the high-prominence action.
|
|
27
|
+
const SIZE_CONFIG: Record<FabSize, ResolvedSize> = {
|
|
28
|
+
small: { diameter: 40, iconBox: 24, fontSize: 14 },
|
|
20
29
|
medium: { diameter: 56, iconBox: 24, fontSize: 15 },
|
|
21
30
|
large: { diameter: 64, iconBox: 28, fontSize: 16 },
|
|
22
31
|
};
|
|
23
32
|
|
|
33
|
+
const MIN_NUMERIC_ICON_BOX = 22;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Resolve a `size` prop (preset name or raw pixel diameter) to concrete pixel
|
|
37
|
+
* geometry. A numeric size sets the diameter directly, derives the icon box as
|
|
38
|
+
* `round(size * 0.5)` (clamped to a sensible minimum) and lets the circle radius
|
|
39
|
+
* stay `size / 2` (the container uses a full pill radius, so any square stays a
|
|
40
|
+
* perfect circle).
|
|
41
|
+
*/
|
|
42
|
+
function resolveSize(size: FabSize | number): ResolvedSize {
|
|
43
|
+
if (typeof size === 'number') {
|
|
44
|
+
return {
|
|
45
|
+
diameter: size,
|
|
46
|
+
iconBox: Math.max(MIN_NUMERIC_ICON_BOX, Math.round(size * 0.5)),
|
|
47
|
+
fontSize: Math.max(13, Math.round(size * 0.27)),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return SIZE_CONFIG[size];
|
|
51
|
+
}
|
|
52
|
+
|
|
24
53
|
const PILL_RADIUS = 999;
|
|
25
54
|
const PRESS_SCALE = 0.94;
|
|
26
55
|
const DEFAULT_OFFSET = 16;
|
|
@@ -68,7 +97,7 @@ const FabComponent: React.FC<FabProps> = ({
|
|
|
68
97
|
zIndex = DEFAULT_Z_INDEX,
|
|
69
98
|
}) => {
|
|
70
99
|
const theme = useTheme();
|
|
71
|
-
const sizeConfig =
|
|
100
|
+
const sizeConfig = useMemo(() => resolveSize(size), [size]);
|
|
72
101
|
const isExtended = label != null && label.length > 0;
|
|
73
102
|
const content = icon ?? children;
|
|
74
103
|
|
|
@@ -96,13 +125,13 @@ const FabComponent: React.FC<FabProps> = ({
|
|
|
96
125
|
elevation: 6,
|
|
97
126
|
};
|
|
98
127
|
if (isExtended) {
|
|
99
|
-
base.paddingHorizontal =
|
|
128
|
+
base.paddingHorizontal = sizeConfig.diameter <= 44 ? 14 : 20;
|
|
100
129
|
base.gap = 8;
|
|
101
130
|
} else {
|
|
102
131
|
base.width = sizeConfig.diameter;
|
|
103
132
|
}
|
|
104
133
|
return base;
|
|
105
|
-
}, [resolvedColors.background, sizeConfig.diameter, theme.colors.shadow, isExtended
|
|
134
|
+
}, [resolvedColors.background, sizeConfig.diameter, theme.colors.shadow, isExtended]);
|
|
106
135
|
|
|
107
136
|
const labelTextStyle = useMemo((): TextStyle => ({
|
|
108
137
|
fontSize: sizeConfig.fontSize,
|
package/src/fab/Fab.web.tsx
CHANGED
|
@@ -14,12 +14,40 @@ import type { FabPlacement, FabProps, FabSize, FabVariant } from './types';
|
|
|
14
14
|
|
|
15
15
|
export type { FabProps, FabVariant, FabSize, FabPlacement } from './types';
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
interface ResolvedSize {
|
|
18
|
+
diameter: number;
|
|
19
|
+
iconBox: number;
|
|
20
|
+
fontSize: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Material-style FAB scale. `small` (40px) keeps a full 24px icon box so the
|
|
24
|
+
// glyph reads clearly; `medium` (56px) is the canonical FAB; `large` (64px) is
|
|
25
|
+
// the high-prominence action.
|
|
26
|
+
const SIZE_CONFIG: Record<FabSize, ResolvedSize> = {
|
|
27
|
+
small: { diameter: 40, iconBox: 24, fontSize: 14 },
|
|
19
28
|
medium: { diameter: 56, iconBox: 24, fontSize: 15 },
|
|
20
29
|
large: { diameter: 64, iconBox: 28, fontSize: 16 },
|
|
21
30
|
};
|
|
22
31
|
|
|
32
|
+
const MIN_NUMERIC_ICON_BOX = 22;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Resolve a `size` prop (preset name or raw pixel diameter) to concrete pixel
|
|
36
|
+
* geometry. Mirrors the native `resolveSize` exactly so a numeric size renders
|
|
37
|
+
* identically on both platforms. A numeric size sets the diameter directly and
|
|
38
|
+
* derives the icon box as `round(size * 0.5)` (clamped to a sensible minimum).
|
|
39
|
+
*/
|
|
40
|
+
function resolveSize(size: FabSize | number): ResolvedSize {
|
|
41
|
+
if (typeof size === 'number') {
|
|
42
|
+
return {
|
|
43
|
+
diameter: size,
|
|
44
|
+
iconBox: Math.max(MIN_NUMERIC_ICON_BOX, Math.round(size * 0.5)),
|
|
45
|
+
fontSize: Math.max(13, Math.round(size * 0.27)),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return SIZE_CONFIG[size];
|
|
49
|
+
}
|
|
50
|
+
|
|
23
51
|
const PILL_RADIUS = 999;
|
|
24
52
|
const PRESS_SCALE = 0.94;
|
|
25
53
|
const DEFAULT_OFFSET = 16;
|
|
@@ -92,24 +120,35 @@ function useFabCss(): void {
|
|
|
92
120
|
* CENTRAL CONTENT COLUMN as it scrolls and never escapes to the viewport edge /
|
|
93
121
|
* over a side rail in a constrained multi-column app layout.
|
|
94
122
|
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
123
|
+
* Pinning a FAB to the BOTTOM of a column needs TWO mechanisms working together,
|
|
124
|
+
* because sticky alone is not enough:
|
|
125
|
+
*
|
|
126
|
+
* 1. `margin-top: auto` — in a flex COLUMN this consumes all the free vertical
|
|
127
|
+
* space, pushing the FAB to the bottom of the container EVEN WHEN THE
|
|
128
|
+
* CONTENT IS SHORT (e.g. an empty feed / loading spinner). Without it, a
|
|
129
|
+
* short column leaves the FAB at its natural flow position (mid-column) —
|
|
130
|
+
* which is exactly the "floating in the middle / broken" bug.
|
|
131
|
+
* 2. `position: sticky; bottom` — when the content is TALL and the column
|
|
132
|
+
* scrolls, sticky keeps the FAB pinned to the bottom of the viewport as the
|
|
133
|
+
* user scrolls, instead of scrolling away with the content.
|
|
134
|
+
*
|
|
135
|
+
* Horizontal placement uses `align-self` (sticky elements can't be pushed by
|
|
136
|
+
* `left`/`right` the way absolute ones are) plus an inline-edge margin for the
|
|
137
|
+
* `offset`. The consumer column MUST be a flex column that fills the available
|
|
138
|
+
* height, and the FAB MUST be its last child — documented in the usage story.
|
|
100
139
|
*/
|
|
101
140
|
function placementStyle(placement: FabPlacement, offset: number): CSSProperties {
|
|
102
141
|
if (placement === 'static') return {};
|
|
103
142
|
const style: CSSProperties = { position: 'sticky' };
|
|
104
|
-
|
|
143
|
+
const isBottom = placement === 'bottom-right' || placement === 'bottom-left';
|
|
144
|
+
if (isBottom) {
|
|
105
145
|
style.bottom = offset;
|
|
146
|
+
// Push the FAB to the bottom of a (flex-column) container even when the
|
|
147
|
+
// content is too short to scroll, so it never floats mid-column.
|
|
148
|
+
style.marginTop = 'auto';
|
|
106
149
|
} else {
|
|
107
150
|
style.top = offset;
|
|
108
151
|
}
|
|
109
|
-
// Sticky elements can't be pushed horizontally by left/right the way absolute
|
|
110
|
-
// ones are; we self-align within the (flex column) container instead, and add
|
|
111
|
-
// a horizontal margin for the offset so the FAB sits `offset` px from the
|
|
112
|
-
// column's inline edge.
|
|
113
152
|
if (placement === 'bottom-right' || placement === 'top-right') {
|
|
114
153
|
style.alignSelf = 'flex-end';
|
|
115
154
|
style.marginRight = offset;
|
|
@@ -162,7 +201,7 @@ const FabWebComponent: React.FC<FabProps> = ({
|
|
|
162
201
|
const reactId = useId();
|
|
163
202
|
const resolvedId = id ?? `bloom-fab-${reactId}`;
|
|
164
203
|
|
|
165
|
-
const sizeConfig =
|
|
204
|
+
const sizeConfig = useMemo(() => resolveSize(size), [size]);
|
|
166
205
|
const isExtended = label != null && label.length > 0;
|
|
167
206
|
const content = icon ?? children;
|
|
168
207
|
const variantColors = useMemo(() => resolveVariant(variant, theme.colors), [variant, theme.colors]);
|
|
@@ -188,8 +227,9 @@ const FabWebComponent: React.FC<FabProps> = ({
|
|
|
188
227
|
...placementStyle(placement, offset),
|
|
189
228
|
};
|
|
190
229
|
if (isExtended) {
|
|
191
|
-
|
|
192
|
-
base.
|
|
230
|
+
const pad = sizeConfig.diameter <= 44 ? 14 : 20;
|
|
231
|
+
base.paddingLeft = pad;
|
|
232
|
+
base.paddingRight = pad;
|
|
193
233
|
} else {
|
|
194
234
|
base.width = sizeConfig.diameter;
|
|
195
235
|
}
|
|
@@ -204,7 +244,6 @@ const FabWebComponent: React.FC<FabProps> = ({
|
|
|
204
244
|
placement,
|
|
205
245
|
offset,
|
|
206
246
|
isExtended,
|
|
207
|
-
size,
|
|
208
247
|
]);
|
|
209
248
|
|
|
210
249
|
const handleClick = useCallback(
|
package/src/fab/types.ts
CHANGED
|
@@ -12,13 +12,19 @@ import type { StyleProp, ViewStyle, TextStyle } from 'react-native';
|
|
|
12
12
|
export type FabVariant = 'primary' | 'secondary' | 'surface';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* FAB
|
|
15
|
+
* FAB size presets, matching Material-ish conventions but in Bloom's scale:
|
|
16
16
|
* - `small` — 40px (compact / dense layouts)
|
|
17
17
|
* - `medium` — 56px (the canonical FAB; the default)
|
|
18
18
|
* - `large` — 64px (high-prominence primary action)
|
|
19
19
|
*
|
|
20
20
|
* In the `extended` variant the height tracks the size but the FAB grows
|
|
21
21
|
* horizontally to fit its icon + label as a pill.
|
|
22
|
+
*
|
|
23
|
+
* The `size` prop ALSO accepts a raw pixel `number` for an exact diameter
|
|
24
|
+
* between (or outside) the presets — e.g. `size={48}` for a FAB halfway between
|
|
25
|
+
* `small` and `medium`. A numeric size sets the diameter directly, derives the
|
|
26
|
+
* icon box as `round(size * 0.5)` (min 22px) and keeps the circle radius at
|
|
27
|
+
* `size / 2`.
|
|
22
28
|
*/
|
|
23
29
|
export type FabSize = 'small' | 'medium' | 'large';
|
|
24
30
|
|
|
@@ -26,11 +32,14 @@ export type FabSize = 'small' | 'medium' | 'large';
|
|
|
26
32
|
* Where the FAB anchors itself.
|
|
27
33
|
*
|
|
28
34
|
* - `bottom-right` (default) / `bottom-left` / `top-right` / `top-left` —
|
|
29
|
-
* the FAB pins itself to that corner of its CONTAINING block
|
|
30
|
-
* uses `position:
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
35
|
+
* the FAB pins itself to that corner of its CONTAINING block, never the
|
|
36
|
+
* viewport. On native it uses `position: absolute` within the nearest
|
|
37
|
+
* positioned ancestor. On web a `bottom-*` placement combines
|
|
38
|
+
* `margin-top: auto` (pins to the bottom of a flex COLUMN even when the
|
|
39
|
+
* content is short / not scrolling) with `position: sticky; bottom` (keeps
|
|
40
|
+
* it visible while a tall column scrolls). The consumer column MUST be a
|
|
41
|
+
* flex column that fills the available height and the FAB MUST be its last
|
|
42
|
+
* child.
|
|
34
43
|
* - `static` — no positioning at all. The FAB is laid out inline and the
|
|
35
44
|
* consumer is fully responsible for placement (e.g. wrapping it in their
|
|
36
45
|
* own absolutely/sticky-positioned container). Use this when you need
|
|
@@ -38,7 +47,8 @@ export type FabSize = 'small' | 'medium' | 'large';
|
|
|
38
47
|
*
|
|
39
48
|
* CRITICAL: the positioned placements DO NOT use web `position: fixed` and are
|
|
40
49
|
* never anchored to the viewport. They stay inside the containing column so the
|
|
41
|
-
* FAB never escapes a constrained 3-column app layout and sits over a side rail
|
|
50
|
+
* FAB never escapes a constrained 3-column app layout and sits over a side rail,
|
|
51
|
+
* and never float mid-column when the content is short.
|
|
42
52
|
*/
|
|
43
53
|
export type FabPlacement =
|
|
44
54
|
| 'bottom-right'
|
|
@@ -69,7 +79,11 @@ export interface FabProps {
|
|
|
69
79
|
label?: string;
|
|
70
80
|
|
|
71
81
|
variant?: FabVariant;
|
|
72
|
-
|
|
82
|
+
/**
|
|
83
|
+
* A preset (`'small' | 'medium' | 'large'`) or a raw pixel diameter
|
|
84
|
+
* (`number`). Defaults to `'medium'`. See {@link FabSize}.
|
|
85
|
+
*/
|
|
86
|
+
size?: FabSize | number;
|
|
73
87
|
|
|
74
88
|
/**
|
|
75
89
|
* Corner of the containing block to anchor to. Defaults to `'bottom-right'`.
|