@oxy.so/bloom 2.12.4 → 3.0.1
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/README.md +4 -4
- package/docs/button-group.mdx +46 -4
- package/docs/composition.mdx +159 -0
- package/docs/field.mdx +39 -1
- package/docs/glass.mdx +61 -0
- package/docs/layout.mdx +72 -4
- package/docs/migrating-to-3.0.mdx +169 -0
- package/docs/page-header.mdx +237 -36
- package/lib/commonjs/button/Button.js +2 -0
- package/lib/commonjs/button/Button.js.map +1 -1
- package/lib/commonjs/button-group/ButtonGroup.js +124 -19
- package/lib/commonjs/button-group/ButtonGroup.js.map +1 -1
- package/lib/commonjs/control-surface/ControlSurface.js +46 -0
- package/lib/commonjs/control-surface/ControlSurface.js.map +1 -0
- package/lib/commonjs/control-surface/context.js +84 -0
- package/lib/commonjs/control-surface/context.js.map +1 -0
- package/lib/commonjs/control-surface/index.js +26 -0
- package/lib/commonjs/control-surface/index.js.map +1 -0
- package/lib/commonjs/control-surface/types.js +6 -0
- package/lib/commonjs/control-surface/types.js.map +1 -0
- package/lib/commonjs/field/Field.js +71 -26
- package/lib/commonjs/field/Field.js.map +1 -1
- package/lib/commonjs/field/context.js +80 -0
- package/lib/commonjs/field/context.js.map +1 -0
- package/lib/commonjs/field/index.js +7 -0
- package/lib/commonjs/field/index.js.map +1 -1
- package/lib/commonjs/floating/TriggerSlot.js +32 -0
- package/lib/commonjs/floating/TriggerSlot.js.map +1 -1
- package/lib/commonjs/glass/GlassIsland.js +107 -0
- package/lib/commonjs/glass/GlassIsland.js.map +1 -0
- package/lib/commonjs/glass/GlassSurface.js +4 -1
- package/lib/commonjs/glass/GlassSurface.js.map +1 -1
- package/lib/commonjs/glass/index.js +7 -0
- package/lib/commonjs/glass/index.js.map +1 -1
- package/lib/commonjs/index.js +564 -517
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +564 -517
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/label/Label.js +4 -1
- package/lib/commonjs/label/Label.js.map +1 -1
- package/lib/commonjs/layout/bottom-edge.js +3 -42
- package/lib/commonjs/layout/bottom-edge.js.map +1 -1
- package/lib/commonjs/layout/edge-store.js +65 -0
- package/lib/commonjs/layout/edge-store.js.map +1 -0
- package/lib/commonjs/layout/index.js +32 -0
- package/lib/commonjs/layout/index.js.map +1 -1
- package/lib/commonjs/layout/scroll-offset.js +83 -0
- package/lib/commonjs/layout/scroll-offset.js.map +1 -0
- package/lib/commonjs/layout/top-edge.js +109 -0
- package/lib/commonjs/layout/top-edge.js.map +1 -0
- package/lib/commonjs/page-header/EdgeScrim.js +136 -0
- package/lib/commonjs/page-header/EdgeScrim.js.map +1 -0
- package/lib/commonjs/page-header/PageHeader.js +236 -61
- package/lib/commonjs/page-header/PageHeader.js.map +1 -1
- package/lib/commonjs/provider/BloomProvider.js +5 -2
- package/lib/commonjs/provider/BloomProvider.js.map +1 -1
- package/lib/commonjs/social-button/OxyMark.js +2 -2
- package/lib/commonjs/social-button/OxyMark.js.map +1 -1
- package/lib/commonjs/social-button/SocialButton.js +2 -2
- package/lib/commonjs/social-button/SocialButton.js.map +1 -1
- package/lib/commonjs/styles/svg-id.js +29 -0
- package/lib/commonjs/styles/svg-id.js.map +1 -0
- package/lib/commonjs/text-field/TextField.js +39 -6
- package/lib/commonjs/text-field/TextField.js.map +1 -1
- package/lib/commonjs/theme/glass-colors.js +81 -1
- package/lib/commonjs/theme/glass-colors.js.map +1 -1
- package/lib/module/button/Button.js +2 -0
- package/lib/module/button/Button.js.map +1 -1
- package/lib/module/button-group/ButtonGroup.js +124 -19
- package/lib/module/button-group/ButtonGroup.js.map +1 -1
- package/lib/module/control-surface/ControlSurface.js +41 -0
- package/lib/module/control-surface/ControlSurface.js.map +1 -0
- package/lib/module/control-surface/context.js +77 -0
- package/lib/module/control-surface/context.js.map +1 -0
- package/lib/module/control-surface/index.js +5 -0
- package/lib/module/control-surface/index.js.map +1 -0
- package/lib/module/control-surface/types.js +4 -0
- package/lib/module/control-surface/types.js.map +1 -0
- package/lib/module/field/Field.js +72 -27
- package/lib/module/field/Field.js.map +1 -1
- package/lib/module/field/context.js +74 -0
- package/lib/module/field/context.js.map +1 -0
- package/lib/module/field/index.js +1 -0
- package/lib/module/field/index.js.map +1 -1
- package/lib/module/floating/TriggerSlot.js +33 -1
- package/lib/module/floating/TriggerSlot.js.map +1 -1
- package/lib/module/glass/GlassIsland.js +102 -0
- package/lib/module/glass/GlassIsland.js.map +1 -0
- package/lib/module/glass/GlassSurface.js +5 -2
- package/lib/module/glass/GlassSurface.js.map +1 -1
- package/lib/module/glass/index.js +1 -0
- package/lib/module/glass/index.js.map +1 -1
- package/lib/module/index.js +2 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +2 -1
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/label/Label.js +4 -1
- package/lib/module/label/Label.js.map +1 -1
- package/lib/module/layout/bottom-edge.js +2 -41
- package/lib/module/layout/bottom-edge.js.map +1 -1
- package/lib/module/layout/edge-store.js +58 -0
- package/lib/module/layout/edge-store.js.map +1 -0
- package/lib/module/layout/index.js +8 -3
- package/lib/module/layout/index.js.map +1 -1
- package/lib/module/layout/scroll-offset.js +77 -0
- package/lib/module/layout/scroll-offset.js.map +1 -0
- package/lib/module/layout/top-edge.js +102 -0
- package/lib/module/layout/top-edge.js.map +1 -0
- package/lib/module/page-header/EdgeScrim.js +132 -0
- package/lib/module/page-header/EdgeScrim.js.map +1 -0
- package/lib/module/page-header/PageHeader.js +233 -58
- package/lib/module/page-header/PageHeader.js.map +1 -1
- package/lib/module/provider/BloomProvider.js +5 -2
- package/lib/module/provider/BloomProvider.js.map +1 -1
- package/lib/module/social-button/OxyMark.js +1 -1
- package/lib/module/social-button/OxyMark.js.map +1 -1
- package/lib/module/social-button/SocialButton.js +1 -1
- package/lib/module/social-button/SocialButton.js.map +1 -1
- package/lib/module/styles/svg-id.js +26 -0
- package/lib/module/styles/svg-id.js.map +1 -0
- package/lib/module/text-field/TextField.js +39 -6
- package/lib/module/text-field/TextField.js.map +1 -1
- package/lib/module/theme/glass-colors.js +79 -0
- package/lib/module/theme/glass-colors.js.map +1 -1
- package/lib/typescript/commonjs/button/Button.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 +61 -5
- package/lib/typescript/commonjs/button-group/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/control-surface/ControlSurface.d.ts +4 -0
- package/lib/typescript/commonjs/control-surface/ControlSurface.d.ts.map +1 -0
- package/lib/typescript/commonjs/control-surface/context.d.ts +21 -0
- package/lib/typescript/commonjs/control-surface/context.d.ts.map +1 -0
- package/lib/typescript/commonjs/control-surface/index.d.ts +4 -0
- package/lib/typescript/commonjs/control-surface/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/control-surface/types.d.ts +37 -0
- package/lib/typescript/commonjs/control-surface/types.d.ts.map +1 -0
- package/lib/typescript/commonjs/field/Field.d.ts +1 -1
- package/lib/typescript/commonjs/field/Field.d.ts.map +1 -1
- package/lib/typescript/commonjs/field/context.d.ts +55 -0
- package/lib/typescript/commonjs/field/context.d.ts.map +1 -0
- package/lib/typescript/commonjs/field/index.d.ts +2 -0
- package/lib/typescript/commonjs/field/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/field/types.d.ts +22 -3
- package/lib/typescript/commonjs/field/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/floating/TriggerSlot.d.ts.map +1 -1
- package/lib/typescript/commonjs/glass/GlassIsland.d.ts +4 -0
- package/lib/typescript/commonjs/glass/GlassIsland.d.ts.map +1 -0
- package/lib/typescript/commonjs/glass/GlassSurface.d.ts.map +1 -1
- package/lib/typescript/commonjs/glass/index.d.ts +2 -1
- package/lib/typescript/commonjs/glass/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/glass/types.d.ts +43 -0
- package/lib/typescript/commonjs/glass/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +2 -1
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.web.d.ts +2 -1
- package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/label/Label.d.ts.map +1 -1
- package/lib/typescript/commonjs/label/types.d.ts +7 -1
- package/lib/typescript/commonjs/label/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/layout/bottom-edge.d.ts.map +1 -1
- package/lib/typescript/commonjs/layout/edge-store.d.ts +31 -0
- package/lib/typescript/commonjs/layout/edge-store.d.ts.map +1 -0
- package/lib/typescript/commonjs/layout/index.d.ts +8 -3
- package/lib/typescript/commonjs/layout/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/layout/scroll-offset.d.ts +16 -0
- package/lib/typescript/commonjs/layout/scroll-offset.d.ts.map +1 -0
- package/lib/typescript/commonjs/layout/top-edge.d.ts +77 -0
- package/lib/typescript/commonjs/layout/top-edge.d.ts.map +1 -0
- package/lib/typescript/commonjs/page-header/EdgeScrim.d.ts +95 -0
- package/lib/typescript/commonjs/page-header/EdgeScrim.d.ts.map +1 -0
- package/lib/typescript/commonjs/page-header/PageHeader.d.ts +1 -1
- package/lib/typescript/commonjs/page-header/PageHeader.d.ts.map +1 -1
- package/lib/typescript/commonjs/page-header/index.d.ts +1 -1
- package/lib/typescript/commonjs/page-header/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/page-header/types.d.ts +97 -10
- package/lib/typescript/commonjs/page-header/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/provider/BloomProvider.d.ts.map +1 -1
- package/lib/typescript/commonjs/styles/svg-id.d.ts +20 -0
- package/lib/typescript/commonjs/styles/svg-id.d.ts.map +1 -0
- package/lib/typescript/commonjs/text-field/TextField.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/glass-colors.d.ts +67 -0
- package/lib/typescript/commonjs/theme/glass-colors.d.ts.map +1 -1
- package/lib/typescript/module/button/Button.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 +61 -5
- package/lib/typescript/module/button-group/types.d.ts.map +1 -1
- package/lib/typescript/module/control-surface/ControlSurface.d.ts +4 -0
- package/lib/typescript/module/control-surface/ControlSurface.d.ts.map +1 -0
- package/lib/typescript/module/control-surface/context.d.ts +21 -0
- package/lib/typescript/module/control-surface/context.d.ts.map +1 -0
- package/lib/typescript/module/control-surface/index.d.ts +4 -0
- package/lib/typescript/module/control-surface/index.d.ts.map +1 -0
- package/lib/typescript/module/control-surface/types.d.ts +37 -0
- package/lib/typescript/module/control-surface/types.d.ts.map +1 -0
- package/lib/typescript/module/field/Field.d.ts +1 -1
- package/lib/typescript/module/field/Field.d.ts.map +1 -1
- package/lib/typescript/module/field/context.d.ts +55 -0
- package/lib/typescript/module/field/context.d.ts.map +1 -0
- package/lib/typescript/module/field/index.d.ts +2 -0
- package/lib/typescript/module/field/index.d.ts.map +1 -1
- package/lib/typescript/module/field/types.d.ts +22 -3
- package/lib/typescript/module/field/types.d.ts.map +1 -1
- package/lib/typescript/module/floating/TriggerSlot.d.ts.map +1 -1
- package/lib/typescript/module/glass/GlassIsland.d.ts +4 -0
- package/lib/typescript/module/glass/GlassIsland.d.ts.map +1 -0
- package/lib/typescript/module/glass/GlassSurface.d.ts.map +1 -1
- package/lib/typescript/module/glass/index.d.ts +2 -1
- package/lib/typescript/module/glass/index.d.ts.map +1 -1
- package/lib/typescript/module/glass/types.d.ts +43 -0
- package/lib/typescript/module/glass/types.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +2 -1
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/index.web.d.ts +2 -1
- package/lib/typescript/module/index.web.d.ts.map +1 -1
- package/lib/typescript/module/label/Label.d.ts.map +1 -1
- package/lib/typescript/module/label/types.d.ts +7 -1
- package/lib/typescript/module/label/types.d.ts.map +1 -1
- package/lib/typescript/module/layout/bottom-edge.d.ts.map +1 -1
- package/lib/typescript/module/layout/edge-store.d.ts +31 -0
- package/lib/typescript/module/layout/edge-store.d.ts.map +1 -0
- package/lib/typescript/module/layout/index.d.ts +8 -3
- package/lib/typescript/module/layout/index.d.ts.map +1 -1
- package/lib/typescript/module/layout/scroll-offset.d.ts +16 -0
- package/lib/typescript/module/layout/scroll-offset.d.ts.map +1 -0
- package/lib/typescript/module/layout/top-edge.d.ts +77 -0
- package/lib/typescript/module/layout/top-edge.d.ts.map +1 -0
- package/lib/typescript/module/page-header/EdgeScrim.d.ts +95 -0
- package/lib/typescript/module/page-header/EdgeScrim.d.ts.map +1 -0
- package/lib/typescript/module/page-header/PageHeader.d.ts +1 -1
- package/lib/typescript/module/page-header/PageHeader.d.ts.map +1 -1
- package/lib/typescript/module/page-header/index.d.ts +1 -1
- package/lib/typescript/module/page-header/index.d.ts.map +1 -1
- package/lib/typescript/module/page-header/types.d.ts +97 -10
- package/lib/typescript/module/page-header/types.d.ts.map +1 -1
- package/lib/typescript/module/provider/BloomProvider.d.ts.map +1 -1
- package/lib/typescript/module/styles/svg-id.d.ts +20 -0
- package/lib/typescript/module/styles/svg-id.d.ts.map +1 -0
- package/lib/typescript/module/text-field/TextField.d.ts.map +1 -1
- package/lib/typescript/module/theme/glass-colors.d.ts +67 -0
- package/lib/typescript/module/theme/glass-colors.d.ts.map +1 -1
- package/package.json +15 -1
- package/src/button/Button.tsx +10 -1
- package/src/button-group/ButtonGroup.tsx +146 -19
- package/src/button-group/types.ts +68 -5
- package/src/control-surface/ControlSurface.tsx +41 -0
- package/src/control-surface/context.ts +81 -0
- package/src/control-surface/index.ts +4 -0
- package/src/control-surface/types.ts +40 -0
- package/src/field/Field.tsx +84 -27
- package/src/field/context.ts +110 -0
- package/src/field/index.ts +3 -0
- package/src/field/types.ts +22 -3
- package/src/floating/TriggerSlot.tsx +35 -1
- package/src/glass/GlassIsland.tsx +109 -0
- package/src/glass/GlassSurface.tsx +26 -2
- package/src/glass/index.ts +3 -0
- package/src/glass/types.ts +45 -0
- package/src/index.ts +6 -0
- package/src/index.web.ts +6 -0
- package/src/label/Label.tsx +3 -1
- package/src/label/types.ts +7 -1
- package/src/layout/bottom-edge.tsx +3 -56
- package/src/layout/edge-store.ts +69 -0
- package/src/layout/index.ts +8 -3
- package/src/layout/scroll-offset.ts +76 -0
- package/src/layout/top-edge.tsx +109 -0
- package/src/page-header/EdgeScrim.tsx +118 -0
- package/src/page-header/PageHeader.tsx +253 -61
- package/src/page-header/index.ts +9 -1
- package/src/page-header/types.ts +103 -10
- package/src/provider/BloomProvider.tsx +12 -1
- package/src/social-button/OxyMark.tsx +1 -1
- package/src/social-button/SocialButton.tsx +1 -1
- package/src/styles/svg-id.ts +23 -0
- package/src/text-field/TextField.tsx +44 -5
- package/src/theme/glass-colors.ts +80 -0
- package/lib/commonjs/social-button/use-svg-id.js +0 -16
- package/lib/commonjs/social-button/use-svg-id.js.map +0 -1
- package/lib/module/social-button/use-svg-id.js +0 -13
- package/lib/module/social-button/use-svg-id.js.map +0 -1
- package/lib/typescript/commonjs/social-button/use-svg-id.d.ts +0 -7
- package/lib/typescript/commonjs/social-button/use-svg-id.d.ts.map +0 -1
- package/lib/typescript/module/social-button/use-svg-id.d.ts +0 -7
- package/lib/typescript/module/social-button/use-svg-id.d.ts.map +0 -1
- package/src/social-button/use-svg-id.ts +0 -10
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FIELD MEMBERSHIP — what a `Field` tells the control inside it, so the label,
|
|
3
|
+
* the description and the error are wired to it rather than merely stacked
|
|
4
|
+
* above and below it.
|
|
5
|
+
*
|
|
6
|
+
* ## What was missing
|
|
7
|
+
*
|
|
8
|
+
* `Field` drew the three pieces and rendered `children` untouched. Every
|
|
9
|
+
* association — the id the label points at, the `aria-describedby` that makes a
|
|
10
|
+
* screen reader read the error, the invalid state, the disabled state — was the
|
|
11
|
+
* app's to wire by hand, on every field, and the failure when it was not wired
|
|
12
|
+
* is silent: the field LOOKS associated, because the parts are in the right
|
|
13
|
+
* places and the error is red.
|
|
14
|
+
*
|
|
15
|
+
* ## The contract
|
|
16
|
+
*
|
|
17
|
+
* `Field` publishes ids and state; a control that can accept them calls
|
|
18
|
+
* {@link useFieldControl} and applies them. Opt-IN, not magic: an arbitrary
|
|
19
|
+
* child cannot be given behaviour by being wrapped, and a container that
|
|
20
|
+
* inspected its children's types to decide what to do with them would be the
|
|
21
|
+
* thing `docs/composition.mdx` argues against. A control that does not read the
|
|
22
|
+
* context renders exactly as it did before.
|
|
23
|
+
*
|
|
24
|
+
* ## The ids are stable across SSR
|
|
25
|
+
*
|
|
26
|
+
* From `useId`, seeded by the caller's `nativeID` when it gave one. A
|
|
27
|
+
* module-scope counter would number the server's render and the client's
|
|
28
|
+
* hydration differently, so the `for` attribute React sent and the `id` it
|
|
29
|
+
* re-rendered would stop matching — which is invisible until someone clicks a
|
|
30
|
+
* label.
|
|
31
|
+
*
|
|
32
|
+
* ## One control or several
|
|
33
|
+
*
|
|
34
|
+
* A field with ONE control gives it the field's id, and the error is described
|
|
35
|
+
* BY that control. A field with several (a radio set, a date's three inputs)
|
|
36
|
+
* must not: three inputs sharing one id is invalid HTML, and an error attached
|
|
37
|
+
* to each of them is read three times. `multiple` switches the field to a
|
|
38
|
+
* labelled `group` and publishes no control id at all — the group carries the
|
|
39
|
+
* name and the description, and each control keeps its own.
|
|
40
|
+
*
|
|
41
|
+
* ## What does NOT travel here
|
|
42
|
+
*
|
|
43
|
+
* The VALUE, the validation and the submission. Bloom owns presentation and
|
|
44
|
+
* association; an app's form library owns the data. A context that also carried
|
|
45
|
+
* the value would make `Field` a form framework, which is the one thing this is
|
|
46
|
+
* not.
|
|
47
|
+
*/
|
|
48
|
+
import { createContext, useContext } from 'react';
|
|
49
|
+
|
|
50
|
+
export interface FieldControlValue {
|
|
51
|
+
/**
|
|
52
|
+
* The id the single control should carry, so the label's `for` points at it.
|
|
53
|
+
* `undefined` in a `multiple` field, where each control keeps its own.
|
|
54
|
+
*/
|
|
55
|
+
controlId?: string;
|
|
56
|
+
/** The label's own id — for a control that names itself by reference. */
|
|
57
|
+
labelId?: string;
|
|
58
|
+
/**
|
|
59
|
+
* The id list for `aria-describedby`: the description, or the error when
|
|
60
|
+
* there is one. `undefined` when the field has neither.
|
|
61
|
+
*/
|
|
62
|
+
describedBy?: string;
|
|
63
|
+
/**
|
|
64
|
+
* The label as TEXT, when the field's label is a string.
|
|
65
|
+
*
|
|
66
|
+
* This is the cross-platform half of naming. On web a `<label for>` is a real
|
|
67
|
+
* association and the control needs no name of its own; React Native has no
|
|
68
|
+
* equivalent, so the control has to carry the name, and the name it should
|
|
69
|
+
* carry is the one the user can see. A control that has both its own label
|
|
70
|
+
* prop and this should prefer this — otherwise the visible label and the
|
|
71
|
+
* announced name can disagree, which is worse than either alone.
|
|
72
|
+
*/
|
|
73
|
+
labelText?: string;
|
|
74
|
+
/** Whether the field is currently in its error state. */
|
|
75
|
+
invalid: boolean;
|
|
76
|
+
/** Whether the field is marked required. */
|
|
77
|
+
required: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* The field's disabled state.
|
|
80
|
+
*
|
|
81
|
+
* A CONSTRAINT, not a default: a control inside a disabled field must not be
|
|
82
|
+
* able to re-enable itself, so a consumer applies this with `||` against its
|
|
83
|
+
* own prop rather than with `??`.
|
|
84
|
+
*/
|
|
85
|
+
disabled: boolean;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const FieldControlContext = createContext<FieldControlValue | null>(null);
|
|
89
|
+
FieldControlContext.displayName = 'BloomFieldControlContext';
|
|
90
|
+
|
|
91
|
+
/** @internal The provider `Field` renders. */
|
|
92
|
+
export const FieldControlProvider = FieldControlContext.Provider;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The enclosing `Field`'s association, or `null` outside one.
|
|
96
|
+
*
|
|
97
|
+
* ```tsx
|
|
98
|
+
* const field = useFieldControl();
|
|
99
|
+
* <TextInput
|
|
100
|
+
* nativeID={field?.controlId ?? nativeID}
|
|
101
|
+
* aria-describedby={field?.describedBy}
|
|
102
|
+
* aria-invalid={field?.invalid || isInvalid || undefined}
|
|
103
|
+
* accessibilityLabel={field?.labelText ?? label}
|
|
104
|
+
* editable={!(field?.disabled || disabled)}
|
|
105
|
+
* />
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
export function useFieldControl(): FieldControlValue | null {
|
|
109
|
+
return useContext(FieldControlContext);
|
|
110
|
+
}
|
package/src/field/index.ts
CHANGED
package/src/field/types.ts
CHANGED
|
@@ -14,11 +14,30 @@ export interface FieldProps {
|
|
|
14
14
|
error?: string | null;
|
|
15
15
|
/** Mark the field's label with a required asterisk. */
|
|
16
16
|
required?: boolean;
|
|
17
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* Disables the label/description styling AND, for a control that reads the
|
|
19
|
+
* field context, the control itself.
|
|
20
|
+
*
|
|
21
|
+
* It is a CONSTRAINT rather than a default: a control inside a disabled field
|
|
22
|
+
* combines it with `||`, so nothing inside can re-enable itself.
|
|
23
|
+
*/
|
|
18
24
|
disabled?: boolean;
|
|
19
25
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
26
|
+
* The field holds SEVERAL controls — a radio set, a date's three boxes.
|
|
27
|
+
*
|
|
28
|
+
* It becomes a labelled `group` and publishes no control id, because one id
|
|
29
|
+
* shared by three inputs is invalid HTML and one error described by three
|
|
30
|
+
* inputs is announced three times. Each control keeps its own id and name.
|
|
31
|
+
*/
|
|
32
|
+
multiple?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* The control's id, wired to the label's `htmlFor` on web so a label click
|
|
35
|
+
* focuses it.
|
|
36
|
+
*
|
|
37
|
+
* Optional now: omitted, the field generates one from `useId` and hands it to
|
|
38
|
+
* a control that reads `useFieldControl()`. Pass it when the id has to be a
|
|
39
|
+
* particular string — an app's form library keyed by field name, or a control
|
|
40
|
+
* that does not read the context and needs the id written on it by hand.
|
|
22
41
|
*/
|
|
23
42
|
nativeID?: string;
|
|
24
43
|
style?: StyleProp<ViewStyle>;
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* `alignSelf: 'flex-start'` keeps that wrapper from stretching across its
|
|
23
23
|
* parent, which would anchor the surface to the row rather than to the control.
|
|
24
24
|
*/
|
|
25
|
-
import React, { Children, cloneElement, isValidElement, useRef } from 'react';
|
|
25
|
+
import React, { Children, cloneElement, Fragment, isValidElement, useRef } from 'react';
|
|
26
26
|
import { StyleSheet, View, type StyleProp, type ViewStyle } from 'react-native';
|
|
27
27
|
|
|
28
28
|
import { useRestoreAccessibilityFocus } from '../hooks/use-accessibility-focus';
|
|
@@ -50,6 +50,16 @@ export interface TriggerSlotProps {
|
|
|
50
50
|
* several children, which is the right failure: `asChild` has no meaning for a
|
|
51
51
|
* fragment, and silently picking the first child is how a trigger ends up
|
|
52
52
|
* un-pressable with nothing logged.
|
|
53
|
+
*
|
|
54
|
+
* ── A FRAGMENT PASSES `isValidElement` AND DROPS EVERY PROP ─────────────────
|
|
55
|
+
*
|
|
56
|
+
* `Children.only` accepts `<>{child}</>` — a fragment IS one child, and it IS a
|
|
57
|
+
* valid element — and `cloneElement` on it succeeds, returning a fragment
|
|
58
|
+
* carrying an `onPress` and an `aria-expanded` that React then discards,
|
|
59
|
+
* because a fragment renders no host node to put them on. Nothing throws. The
|
|
60
|
+
* trigger is simply dead, and the caller's most likely next move is to wrap it
|
|
61
|
+
* in another fragment. So it is rejected here, by name, with the fix in the
|
|
62
|
+
* message.
|
|
53
63
|
*/
|
|
54
64
|
function cloneTrigger(
|
|
55
65
|
children: React.ReactNode,
|
|
@@ -61,6 +71,11 @@ function cloneTrigger(
|
|
|
61
71
|
'A trigger with `asChild` needs a single React element child. Drop `asChild` to let Bloom render the pressable.',
|
|
62
72
|
);
|
|
63
73
|
}
|
|
74
|
+
if (child.type === Fragment) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
'A trigger with `asChild` cannot clone a fragment — it renders no element, so the press handler and the a11y state would be dropped silently. Pass the control itself as the single child.',
|
|
77
|
+
);
|
|
78
|
+
}
|
|
64
79
|
const childProps = child.props;
|
|
65
80
|
// A disabled trigger must not OPEN, and the child's own `disabled` is where a
|
|
66
81
|
// caller most naturally writes that — a disabled `Pressable` child rather than
|
|
@@ -73,10 +88,28 @@ function cloneTrigger(
|
|
|
73
88
|
...handle,
|
|
74
89
|
// Compose rather than overwrite: the child's own handler runs first, then
|
|
75
90
|
// ours opens the surface.
|
|
91
|
+
//
|
|
92
|
+
// ── AND THE CHILD'S HANDLER CAN CANCEL THE OPEN ─────────────────────────
|
|
93
|
+
//
|
|
94
|
+
// `event.preventDefault()` in the child's own handler stops the surface
|
|
95
|
+
// from opening. Composition needs an answer to "what if the first handler
|
|
96
|
+
// decides this press should not do the second thing" — a trigger that has
|
|
97
|
+
// just discovered the form is dirty, or that the row it belongs to was
|
|
98
|
+
// removed — and without one the only way to express it is to stop using
|
|
99
|
+
// `asChild` and rebuild the control.
|
|
100
|
+
//
|
|
101
|
+
// It is `preventDefault` rather than a Bloom-specific return value because
|
|
102
|
+
// the event already carries it on BOTH platforms and both mean the same
|
|
103
|
+
// thing by it: on web `onPress` is handed React's synthetic `click` event,
|
|
104
|
+
// and on native the responder event is a React synthetic event too. One
|
|
105
|
+
// consequence worth knowing on web: the same call also suppresses the
|
|
106
|
+
// browser's own default for that click, so cancelling the open on an
|
|
107
|
+
// anchor child cancels its navigation as well.
|
|
76
108
|
onPress: isDisabled
|
|
77
109
|
? childProps.onPress
|
|
78
110
|
: (event) => {
|
|
79
111
|
childProps.onPress?.(event);
|
|
112
|
+
if (event?.defaultPrevented) return;
|
|
80
113
|
handle.onPress(event);
|
|
81
114
|
},
|
|
82
115
|
onLongPress:
|
|
@@ -84,6 +117,7 @@ function cloneTrigger(
|
|
|
84
117
|
? childProps.onLongPress
|
|
85
118
|
: (event) => {
|
|
86
119
|
childProps.onLongPress?.(event);
|
|
120
|
+
if (event?.defaultPrevented) return;
|
|
87
121
|
handle.onLongPress?.(event);
|
|
88
122
|
},
|
|
89
123
|
// A label the caller already wrote is more specific than the family's.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import React, { memo, useMemo } from 'react';
|
|
2
|
+
import { StyleSheet, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { ControlSurface } from '../control-surface';
|
|
5
|
+
import { bloomShadowStyle } from '../design-tokens/shadows';
|
|
6
|
+
import { resolveSurfaceLevel } from '../styles/surface-levels';
|
|
7
|
+
import { borderRadius } from '../styles/tokens';
|
|
8
|
+
import { resolveChromeGlassColors } from '../theme/glass-colors';
|
|
9
|
+
import { useTheme } from '../theme/use-theme';
|
|
10
|
+
import { GlassSurface } from './GlassSurface';
|
|
11
|
+
import type { GlassIslandProps } from './types';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* An ISLAND: one translucent, rounded container holding one or more controls
|
|
15
|
+
* that float over content the container does not own.
|
|
16
|
+
*
|
|
17
|
+
* ── WHY THIS EXISTS RATHER THAN A SECOND MATERIAL ───────────────────────────
|
|
18
|
+
*
|
|
19
|
+
* `GlassSurface` is the material — blur, tint, sheen, lit rim — and by its own
|
|
20
|
+
* contract it does NOT own the geometry, the hairline or the shadow: it clips
|
|
21
|
+
* itself, and a shadow drawn on a clipping node is clipped away on iOS. So
|
|
22
|
+
* every caller that wants a floating pane has to assemble the same four things
|
|
23
|
+
* in the same order. Two callers now want exactly that assembly — a
|
|
24
|
+
* `ButtonGroup` in its glass variant, and a page header's back capsule — which
|
|
25
|
+
* is the second real need that justifies writing it once.
|
|
26
|
+
*
|
|
27
|
+
* ── THE FILL IS THE LADDER'S, NOT A LOCAL PALETTE ───────────────────────────
|
|
28
|
+
*
|
|
29
|
+
* The island paints rung 1 of `styles/surface-levels` — byte-for-byte the fill
|
|
30
|
+
* a card and a menu panel already paint — at the CHROME alpha. That is the
|
|
31
|
+
* whole reason it reads as Bloom rather than as a fourth material invented for
|
|
32
|
+
* headers, and it is why a preset change moves islands with everything else.
|
|
33
|
+
*
|
|
34
|
+
* ── WHAT IT TELLS ITS CHILDREN ──────────────────────────────────────────────
|
|
35
|
+
*
|
|
36
|
+
* It mounts a `ControlSurface` with `material: 'glass'`, so a control inside it
|
|
37
|
+
* paints flush — no second fill, no second blur — without the caller writing
|
|
38
|
+
* `variant="glass"` on every one. The nesting rule is the material's, not a
|
|
39
|
+
* preference: a translucent pane inside a translucent pane composites two
|
|
40
|
+
* alphas and two blurs for one pane's worth of depth, and on Android the second
|
|
41
|
+
* blur is not available at all.
|
|
42
|
+
*
|
|
43
|
+
* It does NOT publish a `SurfaceLevelProvider`. A published surface fill is a
|
|
44
|
+
* promise that a descendant can read the pixel it lands on, and the pixel here
|
|
45
|
+
* depends on content Bloom cannot see.
|
|
46
|
+
*
|
|
47
|
+
* ── NO `overflow: hidden` ───────────────────────────────────────────────────
|
|
48
|
+
*
|
|
49
|
+
* Deliberately absent, twice over: `GlassSurface` clips its own layers, and
|
|
50
|
+
* `clipsToBounds` on iOS would clip this box's drop shadow away. Controls
|
|
51
|
+
* inside round their own press highlight instead of relying on the island to
|
|
52
|
+
* cut it.
|
|
53
|
+
*/
|
|
54
|
+
const GlassIslandComponent: React.FC<GlassIslandProps> = ({
|
|
55
|
+
children,
|
|
56
|
+
radius = borderRadius.full,
|
|
57
|
+
role,
|
|
58
|
+
accessibilityLabel,
|
|
59
|
+
sheen = true,
|
|
60
|
+
style,
|
|
61
|
+
testID,
|
|
62
|
+
}) => {
|
|
63
|
+
const theme = useTheme();
|
|
64
|
+
const paint = useMemo(() => {
|
|
65
|
+
const level = resolveSurfaceLevel(theme, 1);
|
|
66
|
+
return { fill: level.background, glass: resolveChromeGlassColors(level.background, level.border, theme.isDark) };
|
|
67
|
+
}, [theme]);
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<ControlSurface material="glass">
|
|
71
|
+
<View
|
|
72
|
+
role={role}
|
|
73
|
+
accessibilityLabel={accessibilityLabel}
|
|
74
|
+
testID={testID}
|
|
75
|
+
style={[
|
|
76
|
+
styles.island,
|
|
77
|
+
{
|
|
78
|
+
borderRadius: radius,
|
|
79
|
+
borderWidth: paint.glass.hairlineWidth,
|
|
80
|
+
borderColor: paint.glass.hairline,
|
|
81
|
+
},
|
|
82
|
+
bloomShadowStyle('glass'),
|
|
83
|
+
style,
|
|
84
|
+
]}
|
|
85
|
+
>
|
|
86
|
+
<GlassSurface
|
|
87
|
+
fill={paint.fill}
|
|
88
|
+
material="chrome"
|
|
89
|
+
radius={radius}
|
|
90
|
+
sheen={sheen}
|
|
91
|
+
testID={testID ? `${testID}-material` : undefined}
|
|
92
|
+
/>
|
|
93
|
+
{children}
|
|
94
|
+
</View>
|
|
95
|
+
</ControlSurface>
|
|
96
|
+
);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const styles = StyleSheet.create({
|
|
100
|
+
island: {
|
|
101
|
+
flexDirection: 'row',
|
|
102
|
+
alignItems: 'stretch',
|
|
103
|
+
alignSelf: 'flex-start',
|
|
104
|
+
position: 'relative',
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
export const GlassIsland = memo(GlassIslandComponent);
|
|
109
|
+
GlassIsland.displayName = 'GlassIsland';
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
GLASS_BLUR_INTENSITY,
|
|
9
9
|
GLASS_RIM_HIGHLIGHT,
|
|
10
10
|
GLASS_SHEEN,
|
|
11
|
+
resolveChromeGlassColors,
|
|
11
12
|
resolveGlassColors,
|
|
12
13
|
} from '../theme/glass-colors';
|
|
13
14
|
import type { GlassSurfaceProps } from './types';
|
|
@@ -54,12 +55,19 @@ let glassSheenIdCounter = 0;
|
|
|
54
55
|
const GlassSurfaceComponent: React.FC<GlassSurfaceProps> = ({
|
|
55
56
|
fill,
|
|
56
57
|
radius,
|
|
58
|
+
material = 'accent',
|
|
57
59
|
sheen = true,
|
|
58
60
|
style,
|
|
59
61
|
testID,
|
|
60
62
|
}) => {
|
|
61
63
|
const theme = useTheme();
|
|
62
|
-
const glass = useMemo(
|
|
64
|
+
const glass = useMemo(
|
|
65
|
+
() =>
|
|
66
|
+
material === 'chrome'
|
|
67
|
+
? resolveChromeGlassColors(fill, fill, theme.isDark)
|
|
68
|
+
: resolveGlassColors(fill),
|
|
69
|
+
[fill, material, theme.isDark],
|
|
70
|
+
);
|
|
63
71
|
// Per instance, because two panes in one document would otherwise share an id
|
|
64
72
|
// and the survivor of an unmount would reference a gradient that is gone. Same
|
|
65
73
|
// counter shape as `AvatarRing`.
|
|
@@ -104,8 +112,24 @@ const GlassSurfaceComponent: React.FC<GlassSurfaceProps> = ({
|
|
|
104
112
|
content Bloom does not own.
|
|
105
113
|
*/}
|
|
106
114
|
<View style={[StyleSheet.absoluteFill, { backgroundColor: glass.fill }]} />
|
|
115
|
+
{/*
|
|
116
|
+
The sheen's `width`/`height` are load-bearing beside `absoluteFill`: an
|
|
117
|
+
`<svg>` is a REPLACED element, so `width: auto` resolves to its intrinsic
|
|
118
|
+
size and an SVG without one falls back to CSS's 300 x 150. `left: 0;
|
|
119
|
+
right: 0` does not stretch it — the `right` is over-constrained and
|
|
120
|
+
dropped. On a surface narrower than 300 the sheen still covers, but its
|
|
121
|
+
gradient is scaled over 150px instead of the surface's own height, so
|
|
122
|
+
only the top of the ramp is ever visible; on a wider one it stops dead
|
|
123
|
+
at 300px. Native lays the host view out with flexbox and is unaffected.
|
|
124
|
+
Gate: `src/__tests__/svg-absolute-fill-size.test.ts`.
|
|
125
|
+
*/}
|
|
107
126
|
{sheen ? (
|
|
108
|
-
<Svg
|
|
127
|
+
<Svg
|
|
128
|
+
width="100%"
|
|
129
|
+
height="100%"
|
|
130
|
+
style={StyleSheet.absoluteFill}
|
|
131
|
+
testID={testID ? `${testID}-sheen` : undefined}
|
|
132
|
+
>
|
|
109
133
|
<Defs>
|
|
110
134
|
{/* Top to bottom: simulated light from above. */}
|
|
111
135
|
{/*
|
package/src/glass/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { GlassIsland } from './GlassIsland';
|
|
1
2
|
export { GlassSurface } from './GlassSurface';
|
|
2
3
|
export {
|
|
3
4
|
GlassBlurTargetProvider,
|
|
@@ -5,6 +6,8 @@ export {
|
|
|
5
6
|
useWindowedBlurTarget,
|
|
6
7
|
} from './blur-target';
|
|
7
8
|
export type {
|
|
9
|
+
GlassIslandProps,
|
|
10
|
+
GlassMaterial,
|
|
8
11
|
GlassSurfaceProps,
|
|
9
12
|
GlassBlurTargetProviderProps,
|
|
10
13
|
GlassBlurWindowProps,
|
package/src/glass/types.ts
CHANGED
|
@@ -14,6 +14,20 @@ export interface GlassBlurWindowProps {
|
|
|
14
14
|
children?: ReactNode;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Which of the two glass ROLES a pane paints.
|
|
19
|
+
*
|
|
20
|
+
* `accent` is a brand fill over one of Bloom's own neutral surfaces — a
|
|
21
|
+
* `primary` control, labelled with that fill's own on-colour. `chrome` is a
|
|
22
|
+
* neutral surface off the ladder (`styles/surface-levels`) over content Bloom
|
|
23
|
+
* does NOT own — a page header's island — labelled with the theme's own `text`.
|
|
24
|
+
*
|
|
25
|
+
* They carry different alphas because they are priced against different
|
|
26
|
+
* backdrops, and in opposite directions; the measurement is on
|
|
27
|
+
* {@link ../theme/glass-colors#GLASS_CHROME_ALPHA}.
|
|
28
|
+
*/
|
|
29
|
+
export type GlassMaterial = 'accent' | 'chrome';
|
|
30
|
+
|
|
17
31
|
export interface GlassSurfaceProps {
|
|
18
32
|
/**
|
|
19
33
|
* The OPAQUE brand fill the pane is tinted with — `theme.colors.primary`,
|
|
@@ -35,6 +49,14 @@ export interface GlassSurfaceProps {
|
|
|
35
49
|
* hides until someone uses a different shape.
|
|
36
50
|
*/
|
|
37
51
|
radius: number;
|
|
52
|
+
/**
|
|
53
|
+
* Which role's alpha to apply to `fill`. Default `accent`.
|
|
54
|
+
*
|
|
55
|
+
* A pane cannot infer this from the colour it is given — `chrome`'s fill is a
|
|
56
|
+
* neutral that in light mode IS `colors.card`, which an accent pane could
|
|
57
|
+
* legitimately be asked to paint too.
|
|
58
|
+
*/
|
|
59
|
+
material?: GlassMaterial;
|
|
38
60
|
/**
|
|
39
61
|
* Whether to paint the sheen — the top-lit gradient that separates glass from
|
|
40
62
|
* flat tinted plastic.
|
|
@@ -49,3 +71,26 @@ export interface GlassSurfaceProps {
|
|
|
49
71
|
style?: StyleProp<ViewStyle>;
|
|
50
72
|
testID?: string;
|
|
51
73
|
}
|
|
74
|
+
|
|
75
|
+
export interface GlassIslandProps {
|
|
76
|
+
children?: ReactNode;
|
|
77
|
+
/**
|
|
78
|
+
* Corner radius. A full pill by default — an island is a capsule, and a
|
|
79
|
+
* capsule of icon-only controls is what makes the row read as separated
|
|
80
|
+
* groups rather than as one bar.
|
|
81
|
+
*/
|
|
82
|
+
radius?: number;
|
|
83
|
+
/**
|
|
84
|
+
* `'group'` when the island holds several RELATED actions the consumer has
|
|
85
|
+
* declared as one group. Omitted for a single control in its own capsule: a
|
|
86
|
+
* group of one is noise for a screen reader, and the control already names
|
|
87
|
+
* itself.
|
|
88
|
+
*/
|
|
89
|
+
role?: 'group';
|
|
90
|
+
/** Names the group. Required by `role="group"` to be worth announcing. */
|
|
91
|
+
accessibilityLabel?: string;
|
|
92
|
+
/** Forwarded to the material. See {@link GlassSurfaceProps.sheen}. */
|
|
93
|
+
sheen?: boolean;
|
|
94
|
+
style?: StyleProp<ViewStyle>;
|
|
95
|
+
testID?: string;
|
|
96
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -155,12 +155,18 @@ export type {
|
|
|
155
155
|
} from './surfaces';
|
|
156
156
|
export * from './button';
|
|
157
157
|
export * from './button-group';
|
|
158
|
+
export * from './control-surface';
|
|
158
159
|
export {
|
|
159
160
|
EDGE_GAP,
|
|
160
161
|
windowEdgeGap,
|
|
161
162
|
BottomEdgeProvider,
|
|
162
163
|
useBottomEdgeInset,
|
|
163
164
|
useClaimBottomEdge,
|
|
165
|
+
TopEdgeProvider,
|
|
166
|
+
useClaimTopEdge,
|
|
167
|
+
useTopEdgeInset,
|
|
168
|
+
ScrollOffsetProvider,
|
|
169
|
+
useScrollOffset,
|
|
164
170
|
} from './layout';
|
|
165
171
|
export { Fab } from './fab';
|
|
166
172
|
export type { FabProps, FabVariant, FabSize, FabPlacement } from './fab';
|
package/src/index.web.ts
CHANGED
|
@@ -160,12 +160,18 @@ export type {
|
|
|
160
160
|
} from './surfaces/index.web';
|
|
161
161
|
export * from './button/index.web';
|
|
162
162
|
export * from './button-group';
|
|
163
|
+
export * from './control-surface';
|
|
163
164
|
export {
|
|
164
165
|
EDGE_GAP,
|
|
165
166
|
windowEdgeGap,
|
|
166
167
|
BottomEdgeProvider,
|
|
167
168
|
useBottomEdgeInset,
|
|
168
169
|
useClaimBottomEdge,
|
|
170
|
+
TopEdgeProvider,
|
|
171
|
+
useClaimTopEdge,
|
|
172
|
+
useTopEdgeInset,
|
|
173
|
+
ScrollOffsetProvider,
|
|
174
|
+
useScrollOffset,
|
|
169
175
|
} from './layout';
|
|
170
176
|
export { Fab } from './fab/index.web';
|
|
171
177
|
export type { FabProps, FabVariant, FabSize, FabPlacement } from './fab/index.web';
|
package/src/label/Label.tsx
CHANGED
|
@@ -40,8 +40,10 @@ const LabelComponent = function Label({
|
|
|
40
40
|
}: LabelProps) {
|
|
41
41
|
const palette = useTextFieldPalette();
|
|
42
42
|
|
|
43
|
+
// `null` is DISTINCT from omitted: omitted falls back to the control's id,
|
|
44
|
+
// `null` says the label names a group and points at nothing.
|
|
43
45
|
const webProps: Record<string, unknown> =
|
|
44
|
-
IS_WEB ? { htmlFor: htmlFor ?? nativeID } : {};
|
|
46
|
+
IS_WEB ? { htmlFor: htmlFor === null ? undefined : (htmlFor ?? nativeID) } : {};
|
|
45
47
|
|
|
46
48
|
return (
|
|
47
49
|
<Text
|
package/src/label/types.ts
CHANGED
|
@@ -11,8 +11,14 @@ export interface LabelProps {
|
|
|
11
11
|
/**
|
|
12
12
|
* Web-only convenience: the `id` of the form control this label is for.
|
|
13
13
|
* Defaults to `nativeID` when omitted. Ignored on native.
|
|
14
|
+
*
|
|
15
|
+
* `null` means there is NO single control to point at — a label naming a
|
|
16
|
+
* GROUP of them, which is `Field`'s `multiple` case. Without a way to say
|
|
17
|
+
* that, the `nativeID` fallback makes the label its own target: a
|
|
18
|
+
* `<label for>` pointing at the label, which focuses nothing and which
|
|
19
|
+
* nothing reports.
|
|
14
20
|
*/
|
|
15
|
-
htmlFor?: string;
|
|
21
|
+
htmlFor?: string | null;
|
|
16
22
|
/**
|
|
17
23
|
* Render a required marker (an asterisk in the theme's negative color)
|
|
18
24
|
* after the label text. Defaults to `false`.
|
|
@@ -67,57 +67,9 @@ import {
|
|
|
67
67
|
type PropsWithChildren,
|
|
68
68
|
} from 'react';
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
subscribe: (onChange: () => void) => () => void;
|
|
72
|
-
/**
|
|
73
|
-
* The cached total. Returns the SAME number until a claim actually changes it
|
|
74
|
-
* — `useSyncExternalStore` re-renders forever if the snapshot is recomputed
|
|
75
|
-
* per call.
|
|
76
|
-
*/
|
|
77
|
-
getInset: () => number;
|
|
78
|
-
claim: (id: string, height: number) => void;
|
|
79
|
-
release: (id: string) => void;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function createBottomEdgeStore(): BottomEdgeStore {
|
|
83
|
-
const claims = new Map<string, number>();
|
|
84
|
-
const listeners = new Set<() => void>();
|
|
85
|
-
let inset = 0;
|
|
86
|
-
|
|
87
|
-
const recompute = () => {
|
|
88
|
-
let next = 0;
|
|
89
|
-
for (const height of claims.values()) {
|
|
90
|
-
if (height > next) next = height;
|
|
91
|
-
}
|
|
92
|
-
// Bail before notifying: a re-registration at an unchanged height (every
|
|
93
|
-
// render of a claimant whose footprint did not move) must not re-render
|
|
94
|
-
// every reader.
|
|
95
|
-
if (next === inset) return;
|
|
96
|
-
inset = next;
|
|
97
|
-
for (const listener of listeners) listener();
|
|
98
|
-
};
|
|
70
|
+
import { createEdgeStore, NO_INSET, NO_SUBSCRIPTION, type EdgeStore } from './edge-store';
|
|
99
71
|
|
|
100
|
-
|
|
101
|
-
subscribe(onChange) {
|
|
102
|
-
listeners.add(onChange);
|
|
103
|
-
return () => {
|
|
104
|
-
listeners.delete(onChange);
|
|
105
|
-
};
|
|
106
|
-
},
|
|
107
|
-
getInset: () => inset,
|
|
108
|
-
claim(id, height) {
|
|
109
|
-
if (claims.get(id) === height) return;
|
|
110
|
-
claims.set(id, height);
|
|
111
|
-
recompute();
|
|
112
|
-
},
|
|
113
|
-
release(id) {
|
|
114
|
-
if (!claims.delete(id)) return;
|
|
115
|
-
recompute();
|
|
116
|
-
},
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const BottomEdgeContext = createContext<BottomEdgeStore | null>(null);
|
|
72
|
+
const BottomEdgeContext = createContext<EdgeStore | null>(null);
|
|
121
73
|
|
|
122
74
|
/**
|
|
123
75
|
* Wrap the app once — `BloomProvider` already does. Rendering a second one
|
|
@@ -126,17 +78,12 @@ const BottomEdgeContext = createContext<BottomEdgeStore | null>(null);
|
|
|
126
78
|
* the tab bar the modal is covering.
|
|
127
79
|
*/
|
|
128
80
|
export function BottomEdgeProvider({ children }: PropsWithChildren) {
|
|
129
|
-
const [store] = useState(
|
|
81
|
+
const [store] = useState(createEdgeStore);
|
|
130
82
|
return <BottomEdgeContext.Provider value={store}>{children}</BottomEdgeContext.Provider>;
|
|
131
83
|
}
|
|
132
84
|
|
|
133
85
|
BottomEdgeProvider.displayName = 'BottomEdgeProvider';
|
|
134
86
|
|
|
135
|
-
// Stable module-scope identities for the no-provider path. Fresh closures here
|
|
136
|
-
// would resubscribe `useSyncExternalStore` on every render.
|
|
137
|
-
const NO_SUBSCRIPTION = () => () => {};
|
|
138
|
-
const NO_INSET = () => 0;
|
|
139
|
-
|
|
140
87
|
/**
|
|
141
88
|
* How much of the bottom edge is already occupied, in px.
|
|
142
89
|
*
|