@charcuterie/ui 0.2.0 → 1.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/dist/Field/Field.d.ts +41 -6
- package/dist/Field/Field.d.ts.map +1 -1
- package/dist/Field/Field.js +50 -8
- package/dist/Field/Field.js.map +1 -1
- package/dist/Field/FieldGroup.d.ts +58 -0
- package/dist/Field/FieldGroup.d.ts.map +1 -0
- package/dist/Field/FieldGroup.js +65 -0
- package/dist/Field/FieldGroup.js.map +1 -0
- package/dist/LogViewer/LogViewer.d.ts +3 -1
- package/dist/LogViewer/LogViewer.d.ts.map +1 -1
- package/dist/LogViewer/LogViewer.js +62 -1
- package/dist/LogViewer/LogViewer.js.map +1 -1
- package/dist/Select/Select.d.ts +10 -2
- package/dist/Select/Select.d.ts.map +1 -1
- package/dist/Select/Select.js.map +1 -1
- package/dist/Tooltip/Tooltip.d.ts +16 -2
- package/dist/Tooltip/Tooltip.d.ts.map +1 -1
- package/dist/Tooltip/Tooltip.js +23 -3
- package/dist/Tooltip/Tooltip.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/slotProps.d.ts +121 -0
- package/dist/slotProps.d.ts.map +1 -0
- package/dist/slotProps.js +55 -0
- package/dist/slotProps.js.map +1 -0
- package/dist/slotWiring.d.ts +32 -0
- package/dist/slotWiring.d.ts.map +1 -0
- package/dist/slotWiring.js +115 -0
- package/dist/slotWiring.js.map +1 -0
- package/package.json +3 -3
- package/src/Field/Field.mdx +73 -3
- package/src/Field/Field.stories.tsx +136 -0
- package/src/Field/Field.test.tsx +207 -5
- package/src/Field/Field.tsx +70 -21
- package/src/Field/FieldGroup.tsx +138 -0
- package/src/LogViewer/LogViewer.mdx +36 -1
- package/src/LogViewer/LogViewer.stories.tsx +34 -0
- package/src/LogViewer/LogViewer.test.tsx +65 -2
- package/src/LogViewer/LogViewer.tsx +67 -1
- package/src/Menu/Menu.mdx +23 -0
- package/src/Menu/Menu.stories.tsx +67 -0
- package/src/Menu/Menu.test.tsx +113 -2
- package/src/Popover/Popover.mdx +5 -0
- package/src/Select/Select.stories.tsx +10 -2
- package/src/Select/Select.tsx +10 -2
- package/src/Tooltip/Tooltip.tsx +29 -5
- package/src/index.ts +4 -0
- package/src/slotProps.ts +163 -0
- package/src/slotWiring.ts +173 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two props a slot may not overwrite, and how it merges them.
|
|
3
|
+
*
|
|
4
|
+
* `mergeSlotProps` next door settles the five *attributes* two
|
|
5
|
+
* nested slots both write, and last-write-wins is right for four of
|
|
6
|
+
* them. It is not right for the wiring, because neither of these is
|
|
7
|
+
* a value:
|
|
8
|
+
*
|
|
9
|
+
* - **`ref`** is a subscription. `Menu` and `Tooltip` on one trigger
|
|
10
|
+
* both hand it a floating-ui `refs.setReference` — the anchor each
|
|
11
|
+
* of them positions its panel against — and the inner one used to
|
|
12
|
+
* replace the outer one's. Nothing threw. The outer panel simply
|
|
13
|
+
* had no reference element, so floating-ui left it at
|
|
14
|
+
* `left: 0; top: 0` in the corner of the viewport, which reads as
|
|
15
|
+
* a CSS bug and is not one.
|
|
16
|
+
* - **`on*`** is a listener, and listeners are plural.
|
|
17
|
+
*
|
|
18
|
+
* ### Why this is not imported from `@charcuterie/logic`
|
|
19
|
+
*
|
|
20
|
+
* `useClonedChild`'s `mergeClonedProps` applies the same rule one
|
|
21
|
+
* level down — between a slot and the element a caller wrote — and
|
|
22
|
+
* the two are near-identical. Sharing them means a new public export
|
|
23
|
+
* on `@charcuterie/logic`, which is a `minor`; this is a patch, and
|
|
24
|
+
* the duplication is thirty lines against a wrong version number.
|
|
25
|
+
* Queued in `docs/2026-08-02-reported-by-consumers-queued-for-1-1.md`.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const isEventHandlerName = (name: string) =>
|
|
29
|
+
/^on[A-Z]/.test(name)
|
|
30
|
+
|
|
31
|
+
type MergeableRef =
|
|
32
|
+
| ((node: unknown) => unknown)
|
|
33
|
+
| { current: unknown }
|
|
34
|
+
|
|
35
|
+
const isMergeableRef = (
|
|
36
|
+
value: unknown,
|
|
37
|
+
): value is MergeableRef =>
|
|
38
|
+
typeof value === "function" ||
|
|
39
|
+
(typeof value === "object" &&
|
|
40
|
+
value !== null &&
|
|
41
|
+
"current" in value)
|
|
42
|
+
|
|
43
|
+
const setRef = (ref: MergeableRef, node: unknown) => {
|
|
44
|
+
if (typeof ref === "function") {
|
|
45
|
+
return ref(node)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
ref.current = node
|
|
49
|
+
|
|
50
|
+
return undefined
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Every pair of refs ever merged, so the same two always come back
|
|
55
|
+
* as the **same function** — which is load-bearing rather than
|
|
56
|
+
* frugal.
|
|
57
|
+
*
|
|
58
|
+
* React re-runs a callback ref whenever its identity changes between
|
|
59
|
+
* renders, detaching with the old one and attaching with the new.
|
|
60
|
+
* Both refs here are floating-ui `setReference`s, and calling one
|
|
61
|
+
* sets state; a merged ref rebuilt every render would call both
|
|
62
|
+
* twice per render, each call scheduling the render that rebuilds
|
|
63
|
+
* it. `mergeSlotProps` is a plain function called during render, so
|
|
64
|
+
* it cannot reach for `useMemo` — the memo has to be in the identity
|
|
65
|
+
* of the inputs, which is what a `WeakMap` of `WeakMap`s is.
|
|
66
|
+
*/
|
|
67
|
+
const mergedRefs = new WeakMap<
|
|
68
|
+
MergeableRef,
|
|
69
|
+
WeakMap<MergeableRef, (node: unknown) => () => void>
|
|
70
|
+
>()
|
|
71
|
+
|
|
72
|
+
const mergeRefs = (
|
|
73
|
+
first: MergeableRef,
|
|
74
|
+
second: MergeableRef,
|
|
75
|
+
) => {
|
|
76
|
+
const bySecond =
|
|
77
|
+
mergedRefs.get(first) ??
|
|
78
|
+
new WeakMap<
|
|
79
|
+
MergeableRef,
|
|
80
|
+
(node: unknown) => () => void
|
|
81
|
+
>()
|
|
82
|
+
|
|
83
|
+
mergedRefs.set(first, bySecond)
|
|
84
|
+
|
|
85
|
+
const cached = bySecond.get(second)
|
|
86
|
+
|
|
87
|
+
if (cached) {
|
|
88
|
+
return cached
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const merged = (node: unknown) => {
|
|
92
|
+
const cleanups = [
|
|
93
|
+
setRef(first, node),
|
|
94
|
+
setRef(second, node),
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
// Per ref: one that returned a cleanup has opted out of being
|
|
98
|
+
// called with `null`, and one that did not still expects it.
|
|
99
|
+
return () => {
|
|
100
|
+
for (const [index, cleanup] of cleanups.entries()) {
|
|
101
|
+
if (typeof cleanup === "function") {
|
|
102
|
+
cleanup()
|
|
103
|
+
} else {
|
|
104
|
+
setRef(index === 0 ? first : second, null)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
bySecond.set(second, merged)
|
|
111
|
+
|
|
112
|
+
return merged
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const chainHandlers =
|
|
116
|
+
(
|
|
117
|
+
first: (...args: unknown[]) => unknown,
|
|
118
|
+
second: (...args: unknown[]) => unknown,
|
|
119
|
+
) =>
|
|
120
|
+
(...args: unknown[]) => {
|
|
121
|
+
// Outer first, the same order `aria-describedby` is joined in:
|
|
122
|
+
// the merge order is structural — whoever wrapped whom — and the
|
|
123
|
+
// author of the nesting is the one who chose it.
|
|
124
|
+
first(...args)
|
|
125
|
+
|
|
126
|
+
second(...args)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The keys of `ownProps` that must not simply replace the ones
|
|
131
|
+
* `receivedProps` arrived with. Spread **last**, over both.
|
|
132
|
+
*/
|
|
133
|
+
export const mergeSlotWiring = (
|
|
134
|
+
receivedProps: Record<string, unknown>,
|
|
135
|
+
ownProps: Record<string, unknown>,
|
|
136
|
+
): Record<string, unknown> =>
|
|
137
|
+
Object.fromEntries(
|
|
138
|
+
Object.entries(ownProps).flatMap(
|
|
139
|
+
([name, ownValue]): [string, unknown][] => {
|
|
140
|
+
const receivedValue = receivedProps[name]
|
|
141
|
+
|
|
142
|
+
if (
|
|
143
|
+
name === "ref" &&
|
|
144
|
+
isMergeableRef(receivedValue) &&
|
|
145
|
+
isMergeableRef(ownValue)
|
|
146
|
+
) {
|
|
147
|
+
return [
|
|
148
|
+
[name, mergeRefs(receivedValue, ownValue)],
|
|
149
|
+
]
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (
|
|
153
|
+
isEventHandlerName(name) &&
|
|
154
|
+
typeof receivedValue === "function" &&
|
|
155
|
+
typeof ownValue === "function"
|
|
156
|
+
) {
|
|
157
|
+
return [
|
|
158
|
+
[
|
|
159
|
+
name,
|
|
160
|
+
chainHandlers(
|
|
161
|
+
receivedValue as (
|
|
162
|
+
...args: unknown[]
|
|
163
|
+
) => unknown,
|
|
164
|
+
ownValue as (...args: unknown[]) => unknown,
|
|
165
|
+
),
|
|
166
|
+
],
|
|
167
|
+
]
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return []
|
|
171
|
+
},
|
|
172
|
+
),
|
|
173
|
+
)
|