@uniflowed/ui 0.0.0-alpha.18 → 0.0.0-alpha.37

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.
@@ -20,6 +20,16 @@
20
20
  // a caller legitimately wants *both* — event handlers and refs — they are
21
21
  // composed rather than one replacing the other.
22
22
  //
23
+ // # The other half of the rule: which element the props land on
24
+ //
25
+ // Everything above decides what goes onto the element. `RenderProp` is what
26
+ // decides *which element*, and it is here rather than in a module of its own
27
+ // because it is the same policy read from the other end: a part computes one
28
+ // props object, and either puts it on the element it would have chosen or hands
29
+ // it to the caller to put on theirs. Two ways of composing a caller's props
30
+ // would be two chances to get the order wrong; one shape, stated once, is what
31
+ // keeps a part that renders somebody else's element from being a weaker part.
32
+ //
23
33
  // # Why this is `internal/` and not a subpath
24
34
  //
25
35
  // It is not a "props utils" module and there is nothing else in it. It is the
@@ -28,6 +38,8 @@
28
38
  // consumer to build a part that spreads `rest` last, which is the failure this
29
39
  // exists to prevent — so it stays unreachable from outside the package.
30
40
 
41
+ import type { Node } from "@uniflowed/react";
42
+
31
43
  /**
32
44
  * Props on their way onto an element: what a caller hands a part, and what
33
45
  * `Field.Control` hands back for a caller to spread.
@@ -75,6 +87,78 @@
75
87
  */
76
88
  export type Rest = { readonly key?: empty, readonly [string]: mixed };
77
89
 
90
+ /**
91
+ * The escape hatch: a caller's element in place of the part's own.
92
+ *
93
+ * `@uniflowed/ui` has no copy step — `packages/ui/index.js`'s header argues
94
+ * that at length — and the thing a copy step is *for* is changing the markup. A
95
+ * part that always renders a `<button>` cannot be the link a menu of links
96
+ * needs; a heading fixed at `<h2>` is wrong inside an accordion. This is what
97
+ * replaces owning the source: the part still computes every attribute, every
98
+ * composed handler and every id, and hands them to the caller to put on
99
+ * whatever element they wanted.
100
+ *
101
+ * One name and one signature everywhere, which is the point. `asChild` clones a
102
+ * child and hopes its props survive; this hands the props over explicitly, so a
103
+ * caller can see what they are getting, decide the order themselves, and drop
104
+ * one deliberately. The part is still the part — `Menu.Body`'s `renders*` still
105
+ * rejects a `<div>` where a `Menu.Item` belongs, because the escape hatch
106
+ * changes the element the item renders and not what the item *is*. That
107
+ * constraint is exactly what a copied source loses.
108
+ *
109
+ * # What is in the props, and what is not
110
+ *
111
+ * Everything the part would have put on its own element, in the order
112
+ * `withProps` fixes: the caller's `rest` underneath, the part's own semantics on
113
+ * top, handlers and refs composed rather than replaced. `children` is in there
114
+ * too, so `render={(props) => <a href={to} {...props} />}` renders what was
115
+ * written between the tags — a part whose children were silently dropped
116
+ * because the caller spread the props and forgot them is the kind of quiet
117
+ * wrongness this package exists to not have. JSX children win over a spread, so
118
+ * `<a {...props}>Other</a>` still says what it says.
119
+ *
120
+ * What is *not* in there is anything true of the element rather than of the
121
+ * part: `type="button"` is the only one in practice, and it stays on the
122
+ * `<button>` branch. Handing it to a caller rendering an `<a>` would put an
123
+ * attribute the HTML has no meaning for on their link.
124
+ */
125
+ export type RenderProp = (props: Rest) => Node;
126
+
127
+ /**
128
+ * What a part's own handler reads of the event it is handed.
129
+ *
130
+ * Inexact, and named rather than inferred, for the same reason `menu.js`'s
131
+ * `MenuSelect` is: what arrives is React's synthetic event, uf does not merge
132
+ * Flow's `jsx.js` environment so `lib/react.js` models no such thing, and these
133
+ * are the members the handlers in this package actually read.
134
+ *
135
+ * It exists because of `RenderProp`. A handler written inside a JSX attribute
136
+ * gets its parameter's type from the attribute, which for an intrinsic is
137
+ * `any`; the escape hatch has to build the props *before* there is an element
138
+ * to put them on, so the same handler in an object literal has an indexer's
139
+ * `mixed` for context and Flow asks for an annotation. This is that annotation,
140
+ * written once rather than at every handler in the package.
141
+ *
142
+ * One shape for keys and for presses, which is the one thing it is not honest
143
+ * about: `key` and the modifiers belong to a keyboard event and a click has no
144
+ * `key`. It is a parameter annotation for handlers this package writes rather
145
+ * than a description of an event, nothing widens `mixed` into it, and the day
146
+ * `$JSXIntrinsics` is real — the day `Rest` becomes `React.PropsOf`, which its
147
+ * own comment is waiting for — is the day this is React's event types instead.
148
+ */
149
+ export type PartEvent = {
150
+ readonly defaultPrevented: boolean,
151
+ readonly key: string,
152
+ readonly altKey: boolean,
153
+ readonly ctrlKey: boolean,
154
+ readonly metaKey: boolean,
155
+ readonly shiftKey: boolean,
156
+ readonly currentTarget: mixed,
157
+ readonly preventDefault: () => mixed,
158
+ readonly stopPropagation: () => mixed,
159
+ ...
160
+ };
161
+
78
162
  /**
79
163
  * A caller's props on their way to another *part of this package*, rather than
80
164
  * onto an intrinsic element.
@@ -116,7 +200,7 @@ export function forwarded(rest: Rest): $FlowFixMe {
116
200
  * `defaultPrevented` is the caller's way of saying "I handled this", which is
117
201
  * the same contract the DOM uses.
118
202
  */
119
- export function composeHandlers<TEvent extends { readonly defaultPrevented?: boolean }>(
203
+ export function composeHandlers<TEvent extends { readonly defaultPrevented?: boolean, ... }>(
120
204
  theirs: mixed,
121
205
  ours: (event: TEvent) => mixed,
122
206
  ): (event: TEvent) => mixed {