@typed/ui 1.0.0-beta.0 → 1.0.0-beta.10
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/LICENSE +21 -0
- package/README.md +78 -26
- package/dist/Alert.d.ts +72 -0
- package/dist/Alert.d.ts.map +1 -0
- package/dist/Alert.js +40 -0
- package/dist/Button.d.ts +100 -0
- package/dist/Button.d.ts.map +1 -0
- package/dist/Button.js +42 -0
- package/dist/Carousel.d.ts +346 -0
- package/dist/Carousel.d.ts.map +1 -0
- package/dist/Carousel.js +264 -0
- package/dist/Checkbox.d.ts +168 -0
- package/dist/Checkbox.d.ts.map +1 -0
- package/dist/Checkbox.js +146 -0
- package/dist/Collection.d.ts +254 -0
- package/dist/Collection.d.ts.map +1 -0
- package/dist/Collection.js +218 -0
- package/dist/Combobox.d.ts +367 -0
- package/dist/Combobox.d.ts.map +1 -0
- package/dist/Combobox.js +300 -0
- package/dist/Composite.d.ts +823 -0
- package/dist/Composite.d.ts.map +1 -0
- package/dist/Composite.js +615 -0
- package/dist/Dialog.d.ts +544 -0
- package/dist/Dialog.d.ts.map +1 -0
- package/dist/Dialog.js +357 -0
- package/dist/Disclosure.d.ts +219 -0
- package/dist/Disclosure.d.ts.map +1 -0
- package/dist/Disclosure.js +128 -0
- package/dist/Dom/Events.d.ts +122 -0
- package/dist/Dom/Events.d.ts.map +1 -0
- package/dist/Dom/Events.js +192 -0
- package/dist/Dom/Props.d.ts +161 -0
- package/dist/Dom/Props.d.ts.map +1 -0
- package/dist/Dom/Props.js +110 -0
- package/dist/Dom/Refs.d.ts +58 -0
- package/dist/Dom/Refs.d.ts.map +1 -0
- package/dist/Dom/Refs.js +61 -0
- package/dist/Dom/Render.d.ts +59 -0
- package/dist/Dom/Render.d.ts.map +1 -0
- package/dist/Dom/Render.js +71 -0
- package/dist/Dom/Types.d.ts +570 -0
- package/dist/Dom/Types.d.ts.map +1 -0
- package/dist/Dom/Types.js +1 -0
- package/dist/Dom/index.d.ts +20 -0
- package/dist/Dom/index.d.ts.map +1 -0
- package/dist/Dom/index.js +8 -0
- package/dist/Dom.d.ts +14 -0
- package/dist/Dom.d.ts.map +1 -0
- package/dist/Dom.js +13 -0
- package/dist/Focusable.d.ts +85 -0
- package/dist/Focusable.d.ts.map +1 -0
- package/dist/Focusable.js +35 -0
- package/dist/Form.d.ts +1727 -0
- package/dist/Form.d.ts.map +1 -0
- package/dist/Form.js +1142 -0
- package/dist/Grid.d.ts +388 -0
- package/dist/Grid.d.ts.map +1 -0
- package/dist/Grid.js +284 -0
- package/dist/Group.d.ts +128 -0
- package/dist/Group.d.ts.map +1 -0
- package/dist/Group.js +71 -0
- package/dist/Heading.d.ts +87 -0
- package/dist/Heading.d.ts.map +1 -0
- package/dist/Heading.js +58 -0
- package/dist/Hovercard.d.ts +297 -0
- package/dist/Hovercard.d.ts.map +1 -0
- package/dist/Hovercard.js +188 -0
- package/dist/HttpRouter.d.ts +129 -6
- package/dist/HttpRouter.d.ts.map +1 -1
- package/dist/HttpRouter.js +199 -56
- package/dist/Link.d.ts +63 -28
- package/dist/Link.d.ts.map +1 -1
- package/dist/Link.js +84 -37
- package/dist/Listbox.d.ts +305 -0
- package/dist/Listbox.d.ts.map +1 -0
- package/dist/Listbox.js +245 -0
- package/dist/Menu.d.ts +663 -0
- package/dist/Menu.d.ts.map +1 -0
- package/dist/Menu.js +569 -0
- package/dist/Menubar.d.ts +249 -0
- package/dist/Menubar.d.ts.map +1 -0
- package/dist/Menubar.js +207 -0
- package/dist/Meter.d.ts +157 -0
- package/dist/Meter.d.ts.map +1 -0
- package/dist/Meter.js +87 -0
- package/dist/NativeDetails.d.ts +41 -0
- package/dist/NativeDetails.d.ts.map +1 -0
- package/dist/NativeDetails.js +40 -0
- package/dist/NativeDialog.d.ts +66 -0
- package/dist/NativeDialog.d.ts.map +1 -0
- package/dist/NativeDialog.js +88 -0
- package/dist/NativePopover.d.ts +43 -0
- package/dist/NativePopover.d.ts.map +1 -0
- package/dist/NativePopover.js +84 -0
- package/dist/Popover.d.ts +240 -0
- package/dist/Popover.d.ts.map +1 -0
- package/dist/Popover.js +140 -0
- package/dist/RadioGroup.d.ts +330 -0
- package/dist/RadioGroup.d.ts.map +1 -0
- package/dist/RadioGroup.js +241 -0
- package/dist/Role.d.ts +64 -0
- package/dist/Role.d.ts.map +1 -0
- package/dist/Role.js +27 -0
- package/dist/Select.d.ts +418 -0
- package/dist/Select.d.ts.map +1 -0
- package/dist/Select.js +357 -0
- package/dist/Separator.d.ts +58 -0
- package/dist/Separator.d.ts.map +1 -0
- package/dist/Separator.js +32 -0
- package/dist/Slider.d.ts +141 -0
- package/dist/Slider.d.ts.map +1 -0
- package/dist/Slider.js +101 -0
- package/dist/SpinButton.d.ts +141 -0
- package/dist/SpinButton.d.ts.map +1 -0
- package/dist/SpinButton.js +101 -0
- package/dist/Storybook.d.ts +76 -0
- package/dist/Storybook.d.ts.map +1 -0
- package/dist/Storybook.js +102 -0
- package/dist/Switch.d.ts +148 -0
- package/dist/Switch.d.ts.map +1 -0
- package/dist/Switch.js +110 -0
- package/dist/Tab.d.ts +26 -0
- package/dist/Tab.d.ts.map +1 -0
- package/dist/Tab.js +25 -0
- package/dist/Tabs.d.ts +411 -0
- package/dist/Tabs.d.ts.map +1 -0
- package/dist/Tabs.js +262 -0
- package/dist/Toolbar.d.ts +248 -0
- package/dist/Toolbar.d.ts.map +1 -0
- package/dist/Toolbar.js +187 -0
- package/dist/Tooltip.d.ts +296 -0
- package/dist/Tooltip.d.ts.map +1 -0
- package/dist/Tooltip.js +172 -0
- package/dist/Tree.d.ts +405 -0
- package/dist/Tree.d.ts.map +1 -0
- package/dist/Tree.js +333 -0
- package/dist/TreeGrid.d.ts +426 -0
- package/dist/TreeGrid.d.ts.map +1 -0
- package/dist/TreeGrid.js +308 -0
- package/dist/VisuallyHidden.d.ts +68 -0
- package/dist/VisuallyHidden.d.ts.map +1 -0
- package/dist/VisuallyHidden.js +44 -0
- package/dist/WindowSplitter.d.ts +336 -0
- package/dist/WindowSplitter.d.ts.map +1 -0
- package/dist/WindowSplitter.js +305 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +48 -0
- package/package.json +47 -14
- package/src/HttpRouter.test.ts +0 -294
- package/src/HttpRouter.ts +0 -168
- package/src/Link.test.ts +0 -85
- package/src/Link.ts +0 -107
- package/src/index.ts +0 -2
- package/tsconfig.json +0 -6
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Scope from "effect/Scope";
|
|
3
|
+
import * as Schema from "effect/Schema";
|
|
4
|
+
import { Fx, RefSubject } from "@typed/fx";
|
|
5
|
+
import { EventHandler, type Renderable } from "@typed/template";
|
|
6
|
+
import * as Dom from "./Dom.js";
|
|
7
|
+
import type { ComposedRef } from "./Dom/Refs.js";
|
|
8
|
+
import type { HostResult } from "./Dom/Types.js";
|
|
9
|
+
/** Visual orientation of the separator between panes.
|
|
10
|
+
* @remarks
|
|
11
|
+
* A vertical separator adjusts with Left/Right; a horizontal separator adjusts
|
|
12
|
+
* with Up/Down, matching the axis perpendicular to the dividing line.
|
|
13
|
+
* @since 1.0.0
|
|
14
|
+
* @category Resize axis
|
|
15
|
+
*/
|
|
16
|
+
export type Orientation = "horizontal" | "vertical";
|
|
17
|
+
/** Current value and keyboard policy for a window splitter.
|
|
18
|
+
* @remarks
|
|
19
|
+
* All range and collapse information remains renderer-independent and directly
|
|
20
|
+
* testable.
|
|
21
|
+
* Plain state retains no resources; its hydrated RefSubject is Scope-owned.
|
|
22
|
+
* @since 1.0.0
|
|
23
|
+
* @category Pane sizing state
|
|
24
|
+
*/
|
|
25
|
+
export interface State {
|
|
26
|
+
/** Current pane size value.
|
|
27
|
+
* @remarks
|
|
28
|
+
* It drives `aria-valuenow` and keyboard adjustments.
|
|
29
|
+
* @since 1.0.0
|
|
30
|
+
* @category Current value
|
|
31
|
+
*/
|
|
32
|
+
readonly value: number;
|
|
33
|
+
/** Restore value used by `toggleCollapsed`.
|
|
34
|
+
* @remarks
|
|
35
|
+
* It starts at the initial clamped value. `toggleCollapsed` overwrites it only
|
|
36
|
+
* when collapsing from a non-minimum value, then reads it when restoring.
|
|
37
|
+
* Ordinary `setValue` and `adjust` calls do not update it.
|
|
38
|
+
* @since 1.0.0
|
|
39
|
+
* @category Collapse and restore
|
|
40
|
+
*/
|
|
41
|
+
readonly previousValue: number;
|
|
42
|
+
/** Inclusive minimum pane value.
|
|
43
|
+
* @remarks
|
|
44
|
+
* Updates clamp to this bound and Home jumps to it.
|
|
45
|
+
* @since 1.0.0
|
|
46
|
+
* @category Range constraints
|
|
47
|
+
*/
|
|
48
|
+
readonly min: number;
|
|
49
|
+
/** Inclusive maximum pane value.
|
|
50
|
+
* @remarks
|
|
51
|
+
* Updates clamp to this bound and End jumps to it.
|
|
52
|
+
* @since 1.0.0
|
|
53
|
+
* @category Range constraints
|
|
54
|
+
*/
|
|
55
|
+
readonly max: number;
|
|
56
|
+
/** Keyboard adjustment increment.
|
|
57
|
+
* @remarks
|
|
58
|
+
* Arrow keys require a deterministic application-defined change size. The
|
|
59
|
+
* state constructor does not enforce positivity; callers must provide a
|
|
60
|
+
* positive value for conventional keyboard direction.
|
|
61
|
+
* @since 1.0.0
|
|
62
|
+
* @category Keyboard adjustment
|
|
63
|
+
*/
|
|
64
|
+
readonly step: number;
|
|
65
|
+
/** Separator orientation and keyboard axis.
|
|
66
|
+
* @remarks
|
|
67
|
+
* The value drives both `aria-orientation` and valid arrow keys.
|
|
68
|
+
* @since 1.0.0
|
|
69
|
+
* @category Keyboard navigation
|
|
70
|
+
*/
|
|
71
|
+
readonly orientation: Orientation;
|
|
72
|
+
}
|
|
73
|
+
/** Initial splitter range and keyboard configuration.
|
|
74
|
+
* @remarks
|
|
75
|
+
* Defaults give the widget a complete deterministic state before rendering.
|
|
76
|
+
* Callers remain responsible for `min <= max` and `step > 0`.
|
|
77
|
+
* @since 1.0.0
|
|
78
|
+
* @category Pane sizing state
|
|
79
|
+
*/
|
|
80
|
+
export interface InitialState {
|
|
81
|
+
/** Initial pane size, clamped to min/max.
|
|
82
|
+
* @remarks
|
|
83
|
+
* Invalid out-of-range starts become a valid accessible value.
|
|
84
|
+
* @since 1.0.0
|
|
85
|
+
* @category Current value
|
|
86
|
+
*/
|
|
87
|
+
readonly value: number;
|
|
88
|
+
/** Minimum value, defaulting to zero.
|
|
89
|
+
* @remarks
|
|
90
|
+
* It defines the collapsed boundary and Home result. Callers must ensure it is
|
|
91
|
+
* less than or equal to `max`; neither the schema nor constructor compares them.
|
|
92
|
+
* @since 1.0.0
|
|
93
|
+
* @category Range constraints
|
|
94
|
+
*/
|
|
95
|
+
readonly min?: number;
|
|
96
|
+
/** Maximum value, defaulting to one hundred.
|
|
97
|
+
* @remarks
|
|
98
|
+
* It defines the upper keyboard and ARIA bound. Callers must ensure it is
|
|
99
|
+
* greater than or equal to `min`; neither the schema nor constructor compares them.
|
|
100
|
+
* @since 1.0.0
|
|
101
|
+
* @category Range constraints
|
|
102
|
+
*/
|
|
103
|
+
readonly max?: number;
|
|
104
|
+
/** Arrow-key increment, defaulting to ten.
|
|
105
|
+
* @remarks
|
|
106
|
+
* The widget needs an explicit adjustment granularity. Callers must provide a
|
|
107
|
+
* positive value; `Schema.Finite` rejects infinities and NaN but accepts zero
|
|
108
|
+
* and negative numbers.
|
|
109
|
+
* @since 1.0.0
|
|
110
|
+
* @category Keyboard adjustment
|
|
111
|
+
*/
|
|
112
|
+
readonly step?: number;
|
|
113
|
+
/** Separator orientation, defaulting to vertical.
|
|
114
|
+
* @remarks
|
|
115
|
+
* Orientation determines ARIA output and arrow-key mapping.
|
|
116
|
+
* @since 1.0.0
|
|
117
|
+
* @category Keyboard navigation
|
|
118
|
+
*/
|
|
119
|
+
readonly orientation?: Orientation;
|
|
120
|
+
}
|
|
121
|
+
/** Schema for the structural splitter hydration state.
|
|
122
|
+
* @remarks
|
|
123
|
+
* It validates finite numeric fields and the orientation union. It does not
|
|
124
|
+
* enforce `min <= max`, a positive `step`, or any relationship among fields;
|
|
125
|
+
* callers must satisfy those semantic preconditions.
|
|
126
|
+
* @since 1.0.0
|
|
127
|
+
* @category Pane sizing state
|
|
128
|
+
*/
|
|
129
|
+
export declare const StateSchema: Schema.Struct<{
|
|
130
|
+
readonly value: Schema.Finite;
|
|
131
|
+
readonly previousValue: Schema.Finite;
|
|
132
|
+
readonly min: Schema.Finite;
|
|
133
|
+
readonly max: Schema.Finite;
|
|
134
|
+
readonly step: Schema.Finite;
|
|
135
|
+
readonly orientation: Schema.Literals<readonly ["horizontal", "vertical"]>;
|
|
136
|
+
}>;
|
|
137
|
+
/** Creates hydrated splitter state and clamps the initial value.
|
|
138
|
+
* @remarks
|
|
139
|
+
* Pane sizing behavior can be composed and tested without mounting a separator.
|
|
140
|
+
* Callers must supply `min <= max` and `step > 0`; the constructor defaults
|
|
141
|
+
* omitted bounds/step but does not validate those relationships.
|
|
142
|
+
* The calling Effect Scope owns the returned RefSubject.
|
|
143
|
+
* @example
|
|
144
|
+
* ```ts
|
|
145
|
+
* import { Effect } from "effect"
|
|
146
|
+
* import * as WindowSplitter from "@typed/ui/WindowSplitter"
|
|
147
|
+
*
|
|
148
|
+
* const program = Effect.gen(function* () {
|
|
149
|
+
* return yield* WindowSplitter.makeState({ value: 320, min: 180, max: 640 })
|
|
150
|
+
* })
|
|
151
|
+
* ```
|
|
152
|
+
* @since 1.0.0
|
|
153
|
+
* @category Pane sizing state
|
|
154
|
+
*/
|
|
155
|
+
export declare function makeState(initial: InitialState): Effect.Effect<RefSubject.HydratedRefSubject<{
|
|
156
|
+
readonly value: number;
|
|
157
|
+
readonly previousValue: number;
|
|
158
|
+
readonly min: number;
|
|
159
|
+
readonly max: number;
|
|
160
|
+
readonly step: number;
|
|
161
|
+
readonly orientation: "horizontal" | "vertical";
|
|
162
|
+
}, Schema.SchemaError, never, never>, never, Scope.Scope>;
|
|
163
|
+
/** Sets and clamps the current splitter value.
|
|
164
|
+
* @remarks
|
|
165
|
+
* All callers share one transition bounded by the state's `min` and `max`.
|
|
166
|
+
* Correct range ordering remains a caller precondition. This operation does not
|
|
167
|
+
* change `previousValue`.
|
|
168
|
+
* The Effect uses the existing RefSubject lifetime and acquires no resource.
|
|
169
|
+
* @example
|
|
170
|
+
* ```ts
|
|
171
|
+
* import { Effect } from "effect"
|
|
172
|
+
* import * as WindowSplitter from "@typed/ui/WindowSplitter"
|
|
173
|
+
*
|
|
174
|
+
* const program = Effect.gen(function* () {
|
|
175
|
+
* const state = yield* WindowSplitter.makeState({ value: 320 })
|
|
176
|
+
* yield* WindowSplitter.setValue(state, 400)
|
|
177
|
+
* })
|
|
178
|
+
* ```
|
|
179
|
+
* @since 1.0.0
|
|
180
|
+
* @category Bounded resizing
|
|
181
|
+
*/
|
|
182
|
+
export declare function setValue<E, R>(state: RefSubject.RefSubject<State, E, R>, value: number): Effect.Effect<State, E, R>;
|
|
183
|
+
/** Adds a delta and clamps the resulting splitter value.
|
|
184
|
+
* @remarks
|
|
185
|
+
* Arrow-key behavior can be tested independently of DOM events. Correct range
|
|
186
|
+
* ordering remains a caller precondition, and this operation does not change
|
|
187
|
+
* `previousValue`.
|
|
188
|
+
* The Effect uses the existing RefSubject lifetime and acquires no resource.
|
|
189
|
+
* @example
|
|
190
|
+
* ```ts
|
|
191
|
+
* import { Effect } from "effect"
|
|
192
|
+
* import * as WindowSplitter from "@typed/ui/WindowSplitter"
|
|
193
|
+
*
|
|
194
|
+
* const program = Effect.gen(function* () {
|
|
195
|
+
* const state = yield* WindowSplitter.makeState({ value: 320, step: 24 })
|
|
196
|
+
* yield* WindowSplitter.adjust(state, 24)
|
|
197
|
+
* })
|
|
198
|
+
* ```
|
|
199
|
+
* @since 1.0.0
|
|
200
|
+
* @category Bounded resizing
|
|
201
|
+
*/
|
|
202
|
+
export declare function adjust<E, R>(state: RefSubject.RefSubject<State, E, R>, delta: number): Effect.Effect<State, E, R>;
|
|
203
|
+
/** Collapses to min or restores the recorded collapse value.
|
|
204
|
+
* @remarks
|
|
205
|
+
* When the current value is not `min`, the operation records that exact value in
|
|
206
|
+
* `previousValue` and collapses. At `min`, it restores the stored value after
|
|
207
|
+
* clamping it to the current range. Other updates do not maintain a history.
|
|
208
|
+
* The Effect updates existing state atomically and acquires no resource.
|
|
209
|
+
* @example
|
|
210
|
+
* ```ts
|
|
211
|
+
* import { Effect } from "effect"
|
|
212
|
+
* import * as WindowSplitter from "@typed/ui/WindowSplitter"
|
|
213
|
+
*
|
|
214
|
+
* const program = Effect.gen(function* () {
|
|
215
|
+
* const state = yield* WindowSplitter.makeState({ value: 320 })
|
|
216
|
+
* yield* WindowSplitter.toggleCollapsed(state)
|
|
217
|
+
* })
|
|
218
|
+
* ```
|
|
219
|
+
* @since 1.0.0
|
|
220
|
+
* @category Collapse and restore
|
|
221
|
+
*/
|
|
222
|
+
export declare function toggleCollapsed<E, R>(state: RefSubject.RefSubject<State, E, R>): Effect.Effect<State, E, R>;
|
|
223
|
+
/** Options for the focusable ARIA window splitter.
|
|
224
|
+
* @remarks
|
|
225
|
+
* The model connects renderer-independent sizing to its controlled pane and
|
|
226
|
+
* accessible range metadata. Its state must already satisfy `min <= max` and a
|
|
227
|
+
* positive `step`; the component does not repair invalid state.
|
|
228
|
+
* Options are inert; rendering owns subscriptions/listeners by Scope.
|
|
229
|
+
* @since 1.0.0
|
|
230
|
+
* @category Resize separator
|
|
231
|
+
*/
|
|
232
|
+
export interface WindowSplitterOptions extends Dom.HostOptions<HTMLDivElement> {
|
|
233
|
+
/** Hydrated sizing state.
|
|
234
|
+
* @remarks
|
|
235
|
+
* One source drives keyboard updates and every ARIA range attribute.
|
|
236
|
+
* The splitter borrows state; its original Scope owns it.
|
|
237
|
+
* @since 1.0.0
|
|
238
|
+
* @category State connection
|
|
239
|
+
*/
|
|
240
|
+
readonly state: RefSubject.HydratedRefSubject<State, Schema.SchemaError>;
|
|
241
|
+
/** Id of the pane controlled by the splitter.
|
|
242
|
+
* @remarks
|
|
243
|
+
* `aria-controls` makes the resizing relationship explicit.
|
|
244
|
+
* Dynamic values follow the component Scope.
|
|
245
|
+
* @since 1.0.0
|
|
246
|
+
* @category Identity and relationships
|
|
247
|
+
*/
|
|
248
|
+
readonly primaryPaneId: Renderable.Any<string | null | undefined>;
|
|
249
|
+
/** Accessible name when no external label is supplied.
|
|
250
|
+
* @remarks
|
|
251
|
+
* A focusable separator needs a discoverable purpose.
|
|
252
|
+
* Dynamic values follow the component Scope.
|
|
253
|
+
* @since 1.0.0
|
|
254
|
+
* @category Accessible naming
|
|
255
|
+
*/
|
|
256
|
+
readonly label?: Renderable.Any<string | null | undefined>;
|
|
257
|
+
/** Human-readable value text supplementing the number.
|
|
258
|
+
* @remarks
|
|
259
|
+
* Pane sizes such as percentages or labels may be clearer than raw values.
|
|
260
|
+
* Dynamic values follow the component Scope.
|
|
261
|
+
* @since 1.0.0
|
|
262
|
+
* @category Accessible value text
|
|
263
|
+
*/
|
|
264
|
+
readonly valueText?: Renderable.Any<string | null | undefined>;
|
|
265
|
+
/** State-value units represented by one CSS pixel of pointer movement.
|
|
266
|
+
* @remarks
|
|
267
|
+
* By default, the usable parent width (vertical separator) or height
|
|
268
|
+
* (horizontal separator), minus the separator thickness, represents 100
|
|
269
|
+
* units. This matches percentage pane sizes. Set `1` for CSS-pixel values.
|
|
270
|
+
* Positive axis-aligned CSS scaling is accounted for; pointer coordinates
|
|
271
|
+
* are converted from viewport pixels to the parent's CSS-pixel scale.
|
|
272
|
+
* Supply a positive finite number when using another unit system.
|
|
273
|
+
* Keyboard step and min/max remain independent of pointer resolution.
|
|
274
|
+
* @since 1.0.0
|
|
275
|
+
* @category Pointer resizing
|
|
276
|
+
*/
|
|
277
|
+
readonly valuePerPixel?: number;
|
|
278
|
+
/** Whether keyboard and pointer adjustments are disabled.
|
|
279
|
+
* @remarks
|
|
280
|
+
* ARIA disabled state and handler behavior must agree.
|
|
281
|
+
* Dynamic values follow the component Scope.
|
|
282
|
+
* @since 1.0.0
|
|
283
|
+
* @category Availability
|
|
284
|
+
*/
|
|
285
|
+
readonly disabled?: Renderable.Any<boolean | null | undefined>;
|
|
286
|
+
}
|
|
287
|
+
type WindowSplitterInternalProps<Options extends WindowSplitterOptions> = {
|
|
288
|
+
readonly role: "separator";
|
|
289
|
+
readonly style: Fx.Fx<string | Partial<CSSStyleDeclaration>, Renderable.Error<Options>, Renderable.Services<Options>>;
|
|
290
|
+
readonly tabindex: 0;
|
|
291
|
+
readonly "aria-orientation": RefSubject.Computed<Orientation, Schema.SchemaError>;
|
|
292
|
+
readonly "aria-valuenow": RefSubject.Computed<number, Schema.SchemaError>;
|
|
293
|
+
readonly "aria-valuemin": RefSubject.Computed<number, Schema.SchemaError>;
|
|
294
|
+
readonly "aria-valuemax": RefSubject.Computed<number, Schema.SchemaError>;
|
|
295
|
+
readonly "aria-controls": Dom.NonNullish<Dom.Property<Options, "primaryPaneId">> | undefined;
|
|
296
|
+
readonly "aria-label": Dom.NonNullish<Dom.Property<Options, "label">> | undefined;
|
|
297
|
+
readonly "aria-valuetext": Dom.NonNullish<Dom.Property<Options, "valueText">> | undefined;
|
|
298
|
+
readonly "aria-disabled": Dom.NonNullish<Dom.Property<Options, "disabled">> | false;
|
|
299
|
+
readonly onkeydown: EventHandler.EventHandler<KeyboardEvent, Schema.SchemaError>;
|
|
300
|
+
readonly onpointerdown: EventHandler.EventHandler<PointerEvent>;
|
|
301
|
+
readonly onpointermove: EventHandler.EventHandler<PointerEvent, Schema.SchemaError>;
|
|
302
|
+
readonly onpointerup: EventHandler.EventHandler<PointerEvent>;
|
|
303
|
+
readonly onpointercancel: EventHandler.EventHandler<PointerEvent>;
|
|
304
|
+
readonly onlostpointercapture: EventHandler.EventHandler<PointerEvent>;
|
|
305
|
+
readonly ref: ComposedRef<RefSubject.HydratedRefSubject<State, Schema.SchemaError>, (element: HTMLDivElement) => Effect.Effect<void, never, Scope.Scope>> | undefined;
|
|
306
|
+
};
|
|
307
|
+
/** Renders a focusable ARIA separator for resizing a pane.
|
|
308
|
+
* @remarks
|
|
309
|
+
* The component exposes native DOM focus/events and the ARIA window-splitter
|
|
310
|
+
* range contract: arrows adjust by step, Home/End reach bounds, and Enter
|
|
311
|
+
* collapses/restores. Pointer dragging adjusts the same clamped state, using
|
|
312
|
+
* percentage-of-parent movement by default or the supplied `valuePerPixel`.
|
|
313
|
+
* Native pointer capture keeps movement attached outside the separator;
|
|
314
|
+
* release, cancellation, lost capture, and Scope teardown end the drag.
|
|
315
|
+
* The host composes caller styles with `touch-action: none` so touch gestures
|
|
316
|
+
* remain resize interactions. The caller binds state to actual pane layout. Disabled state
|
|
317
|
+
* leaves keyboard and pointer activation inert.
|
|
318
|
+
* Running the returned Fx owns real key listeners, reactive ARIA attributes,
|
|
319
|
+
* and the hydration ref in its Effect Scope. A custom host must preserve role,
|
|
320
|
+
* tab index, range/relationship attributes, handler, and one hydration owner.
|
|
321
|
+
* @example
|
|
322
|
+
* ```ts
|
|
323
|
+
* import { Effect } from "effect"
|
|
324
|
+
* import * as WindowSplitter from "@typed/ui/WindowSplitter"
|
|
325
|
+
*
|
|
326
|
+
* const program = Effect.gen(function* () {
|
|
327
|
+
* const state = yield* WindowSplitter.makeState({ value: 320, min: 180, max: 640 })
|
|
328
|
+
* return WindowSplitter.WindowSplitter({ state, primaryPaneId: "navigation" })
|
|
329
|
+
* })
|
|
330
|
+
* ```
|
|
331
|
+
* @since 1.0.0
|
|
332
|
+
* @category Resize separator
|
|
333
|
+
*/
|
|
334
|
+
export declare function WindowSplitter<const Options extends WindowSplitterOptions, const Host extends HostResult = never>(options: Options, host?: Dom.HostOverride<Dom.RenderHostProps<Options, WindowSplitterInternalProps<Options>>, "", Host>): import("./Dom/Types.js").HostComponent<Host | Options>;
|
|
335
|
+
export {};
|
|
336
|
+
//# sourceMappingURL=WindowSplitter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WindowSplitter.d.ts","sourceRoot":"","sources":["../src/WindowSplitter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AACtC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,EAAE,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,YAAY,EAA4B,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC1F,OAAO,KAAK,GAAG,MAAM,UAAU,CAAC;AAChC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEjD;;;;;;GAMG;AACH,MAAM,MAAM,WAAW,GAAG,YAAY,GAAG,UAAU,CAAC;AAEpD;;;;;;;GAOG;AACH,MAAM,WAAW,KAAK;IACpB;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB;;;;;;;OAOG;IACH,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B;;;;;OAKG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;CACnC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB;;;;;;OAMG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;OAMG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;;OAOG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,WAAW,CAAC;CACpC;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW;;;;;;;EAOtB,CAAC;AAEH;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,YAAY;;;;;;;0DAY9C;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,CAAC,EAC3B,KAAK,EAAE,UAAU,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,EACzC,KAAK,EAAE,MAAM,GACZ,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,CAK5B;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,MAAM,CAAC,CAAC,EAAE,CAAC,EACzB,KAAK,EAAE,UAAU,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,EACzC,KAAK,EAAE,MAAM,GACZ,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,CAK5B;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,CAAC,EAClC,KAAK,EAAE,UAAU,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,GACxC,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,CAM5B;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,qBAAsB,SAAQ,GAAG,CAAC,WAAW,CAAC,cAAc,CAAC;IAC5E;;;;;;OAMG;IACH,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,kBAAkB,CAAC,KAAK,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC;IACzE;;;;;;OAMG;IACH,QAAQ,CAAC,aAAa,EAAE,UAAU,CAAC,GAAG,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IAClE;;;;;;OAMG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,UAAU,CAAC,GAAG,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IAC3D;;;;;;OAMG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,UAAU,CAAC,GAAG,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IAC/D;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,UAAU,CAAC,GAAG,CAAC,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;CAChE;AA0ID,KAAK,2BAA2B,CAAC,OAAO,SAAS,qBAAqB,IAAI;IACxE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE,CAAC,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,EAAE,UAAU,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,UAAU,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;IACtH,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC;IACrB,QAAQ,CAAC,kBAAkB,EAAE,UAAU,CAAC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC;IAClF,QAAQ,CAAC,eAAe,EAAE,UAAU,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC;IAC1E,QAAQ,CAAC,eAAe,EAAE,UAAU,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC;IAC1E,QAAQ,CAAC,eAAe,EAAE,UAAU,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC;IAC1E,QAAQ,CAAC,eAAe,EAAE,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC,CAAC,GAAG,SAAS,CAAC;IAC7F,QAAQ,CAAC,YAAY,EAAE,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC;IAClF,QAAQ,CAAC,gBAAgB,EAAE,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,GAAG,SAAS,CAAC;IAC1F,QAAQ,CAAC,eAAe,EAAE,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC,GAAG,KAAK,CAAC;IACpF,QAAQ,CAAC,SAAS,EAAE,YAAY,CAAC,YAAY,CAAC,aAAa,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC;IACjF,QAAQ,CAAC,aAAa,EAAE,YAAY,CAAC,YAAY,CAAC,YAAY,CAAC,CAAC;IAChE,QAAQ,CAAC,aAAa,EAAE,YAAY,CAAC,YAAY,CAAC,YAAY,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC;IACpF,QAAQ,CAAC,WAAW,EAAE,YAAY,CAAC,YAAY,CAAC,YAAY,CAAC,CAAC;IAC9D,QAAQ,CAAC,eAAe,EAAE,YAAY,CAAC,YAAY,CAAC,YAAY,CAAC,CAAC;IAClE,QAAQ,CAAC,oBAAoB,EAAE,YAAY,CAAC,YAAY,CAAC,YAAY,CAAC,CAAC;IACvE,QAAQ,CAAC,GAAG,EAAE,WAAW,CACvB,UAAU,CAAC,kBAAkB,CAAC,KAAK,EAAE,MAAM,CAAC,WAAW,CAAC,EACxD,CAAC,OAAO,EAAE,cAAc,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,CACrE,GAAG,SAAS,CAAC;CACf,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,cAAc,CAC5B,KAAK,CAAC,OAAO,SAAS,qBAAqB,EAC3C,KAAK,CAAC,IAAI,SAAS,UAAU,GAAG,KAAK,EAErC,OAAO,EAAE,OAAO,EAChB,IAAI,CAAC,EAAE,GAAG,CAAC,YAAY,CACrB,GAAG,CAAC,eAAe,CAAC,OAAO,EAAE,2BAA2B,CAAC,OAAO,CAAC,CAAC,EAClE,EAAE,EACF,IAAI,CACL,0DAWF"}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Scope from "effect/Scope";
|
|
3
|
+
import * as Schema from "effect/Schema";
|
|
4
|
+
import { Fx, RefSubject } from "@typed/fx";
|
|
5
|
+
import { EventHandler, html, liftRenderableToFx } from "@typed/template";
|
|
6
|
+
import * as Dom from "./Dom.js";
|
|
7
|
+
/** Schema for the structural splitter hydration state.
|
|
8
|
+
* @remarks
|
|
9
|
+
* It validates finite numeric fields and the orientation union. It does not
|
|
10
|
+
* enforce `min <= max`, a positive `step`, or any relationship among fields;
|
|
11
|
+
* callers must satisfy those semantic preconditions.
|
|
12
|
+
* @since 1.0.0
|
|
13
|
+
* @category Pane sizing state
|
|
14
|
+
*/
|
|
15
|
+
export const StateSchema = Schema.Struct({
|
|
16
|
+
value: Schema.Finite,
|
|
17
|
+
previousValue: Schema.Finite,
|
|
18
|
+
min: Schema.Finite,
|
|
19
|
+
max: Schema.Finite,
|
|
20
|
+
step: Schema.Finite,
|
|
21
|
+
orientation: Schema.Literals(["horizontal", "vertical"]),
|
|
22
|
+
});
|
|
23
|
+
/** Creates hydrated splitter state and clamps the initial value.
|
|
24
|
+
* @remarks
|
|
25
|
+
* Pane sizing behavior can be composed and tested without mounting a separator.
|
|
26
|
+
* Callers must supply `min <= max` and `step > 0`; the constructor defaults
|
|
27
|
+
* omitted bounds/step but does not validate those relationships.
|
|
28
|
+
* The calling Effect Scope owns the returned RefSubject.
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* import { Effect } from "effect"
|
|
32
|
+
* import * as WindowSplitter from "@typed/ui/WindowSplitter"
|
|
33
|
+
*
|
|
34
|
+
* const program = Effect.gen(function* () {
|
|
35
|
+
* return yield* WindowSplitter.makeState({ value: 320, min: 180, max: 640 })
|
|
36
|
+
* })
|
|
37
|
+
* ```
|
|
38
|
+
* @since 1.0.0
|
|
39
|
+
* @category Pane sizing state
|
|
40
|
+
*/
|
|
41
|
+
export function makeState(initial) {
|
|
42
|
+
const min = initial.min ?? 0;
|
|
43
|
+
const max = initial.max ?? 100;
|
|
44
|
+
const value = clamp(initial.value, min, max);
|
|
45
|
+
return RefSubject.hydrate(StateSchema, {
|
|
46
|
+
value,
|
|
47
|
+
previousValue: value,
|
|
48
|
+
min,
|
|
49
|
+
max,
|
|
50
|
+
step: initial.step ?? 10,
|
|
51
|
+
orientation: initial.orientation ?? "vertical",
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/** Sets and clamps the current splitter value.
|
|
55
|
+
* @remarks
|
|
56
|
+
* All callers share one transition bounded by the state's `min` and `max`.
|
|
57
|
+
* Correct range ordering remains a caller precondition. This operation does not
|
|
58
|
+
* change `previousValue`.
|
|
59
|
+
* The Effect uses the existing RefSubject lifetime and acquires no resource.
|
|
60
|
+
* @example
|
|
61
|
+
* ```ts
|
|
62
|
+
* import { Effect } from "effect"
|
|
63
|
+
* import * as WindowSplitter from "@typed/ui/WindowSplitter"
|
|
64
|
+
*
|
|
65
|
+
* const program = Effect.gen(function* () {
|
|
66
|
+
* const state = yield* WindowSplitter.makeState({ value: 320 })
|
|
67
|
+
* yield* WindowSplitter.setValue(state, 400)
|
|
68
|
+
* })
|
|
69
|
+
* ```
|
|
70
|
+
* @since 1.0.0
|
|
71
|
+
* @category Bounded resizing
|
|
72
|
+
*/
|
|
73
|
+
export function setValue(state, value) {
|
|
74
|
+
return RefSubject.update(state, (current) => ({
|
|
75
|
+
...current,
|
|
76
|
+
value: clamp(value, current.min, current.max),
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
/** Adds a delta and clamps the resulting splitter value.
|
|
80
|
+
* @remarks
|
|
81
|
+
* Arrow-key behavior can be tested independently of DOM events. Correct range
|
|
82
|
+
* ordering remains a caller precondition, and this operation does not change
|
|
83
|
+
* `previousValue`.
|
|
84
|
+
* The Effect uses the existing RefSubject lifetime and acquires no resource.
|
|
85
|
+
* @example
|
|
86
|
+
* ```ts
|
|
87
|
+
* import { Effect } from "effect"
|
|
88
|
+
* import * as WindowSplitter from "@typed/ui/WindowSplitter"
|
|
89
|
+
*
|
|
90
|
+
* const program = Effect.gen(function* () {
|
|
91
|
+
* const state = yield* WindowSplitter.makeState({ value: 320, step: 24 })
|
|
92
|
+
* yield* WindowSplitter.adjust(state, 24)
|
|
93
|
+
* })
|
|
94
|
+
* ```
|
|
95
|
+
* @since 1.0.0
|
|
96
|
+
* @category Bounded resizing
|
|
97
|
+
*/
|
|
98
|
+
export function adjust(state, delta) {
|
|
99
|
+
return RefSubject.update(state, (current) => ({
|
|
100
|
+
...current,
|
|
101
|
+
value: clamp(current.value + delta, current.min, current.max),
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
/** Collapses to min or restores the recorded collapse value.
|
|
105
|
+
* @remarks
|
|
106
|
+
* When the current value is not `min`, the operation records that exact value in
|
|
107
|
+
* `previousValue` and collapses. At `min`, it restores the stored value after
|
|
108
|
+
* clamping it to the current range. Other updates do not maintain a history.
|
|
109
|
+
* The Effect updates existing state atomically and acquires no resource.
|
|
110
|
+
* @example
|
|
111
|
+
* ```ts
|
|
112
|
+
* import { Effect } from "effect"
|
|
113
|
+
* import * as WindowSplitter from "@typed/ui/WindowSplitter"
|
|
114
|
+
*
|
|
115
|
+
* const program = Effect.gen(function* () {
|
|
116
|
+
* const state = yield* WindowSplitter.makeState({ value: 320 })
|
|
117
|
+
* yield* WindowSplitter.toggleCollapsed(state)
|
|
118
|
+
* })
|
|
119
|
+
* ```
|
|
120
|
+
* @since 1.0.0
|
|
121
|
+
* @category Collapse and restore
|
|
122
|
+
*/
|
|
123
|
+
export function toggleCollapsed(state) {
|
|
124
|
+
return RefSubject.update(state, (current) => current.value === current.min
|
|
125
|
+
? { ...current, value: clamp(current.previousValue, current.min, current.max) }
|
|
126
|
+
: { ...current, previousValue: current.value, value: current.min });
|
|
127
|
+
}
|
|
128
|
+
function internalProps(options) {
|
|
129
|
+
const drags = new WeakMap();
|
|
130
|
+
const release = (element) => {
|
|
131
|
+
const drag = drags.get(element);
|
|
132
|
+
drags.delete(element);
|
|
133
|
+
if (drag !== undefined && element.hasPointerCapture(drag.pointerId)) {
|
|
134
|
+
element.releasePointerCapture(drag.pointerId);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
const pointerRef = Effect.fn(function* (element) {
|
|
138
|
+
yield* Scope.addFinalizer(yield* Effect.scope, Effect.sync(() => {
|
|
139
|
+
release(element);
|
|
140
|
+
}));
|
|
141
|
+
});
|
|
142
|
+
const pointerStyle = liftRenderableToFx(options.props?.style).pipe(Fx.map((style) => {
|
|
143
|
+
if (typeof style === "string")
|
|
144
|
+
return `${style};touch-action:none;`;
|
|
145
|
+
if (typeof style?.cssText === "string")
|
|
146
|
+
return `${style.cssText};touch-action:none;`;
|
|
147
|
+
return { ...style, touchAction: "none" };
|
|
148
|
+
}));
|
|
149
|
+
const onpointerdown = EventHandler.make((event) => Effect.sync(() => {
|
|
150
|
+
const element = Dom.currentTarget(event);
|
|
151
|
+
if (event.button !== 0 || !event.isPrimary || element.getAttribute("aria-disabled") === "true")
|
|
152
|
+
return;
|
|
153
|
+
if (drags.has(element))
|
|
154
|
+
return;
|
|
155
|
+
// The visible ARIA snapshot starts the native gesture without yielding to
|
|
156
|
+
// state acquisition before preventDefault and pointer capture.
|
|
157
|
+
const valueAttribute = element.getAttribute("aria-valuenow");
|
|
158
|
+
const value = Number(valueAttribute);
|
|
159
|
+
const orientation = element.getAttribute("aria-orientation");
|
|
160
|
+
if (valueAttribute === null || !Number.isFinite(value))
|
|
161
|
+
return;
|
|
162
|
+
const parent = element.parentElement;
|
|
163
|
+
if (parent === null)
|
|
164
|
+
return;
|
|
165
|
+
const vertical = orientation === "vertical";
|
|
166
|
+
const parentRect = parent.getBoundingClientRect();
|
|
167
|
+
const separatorRect = element.getBoundingClientRect();
|
|
168
|
+
// Pointer coordinates and DOMRects use viewport pixels. Convert the
|
|
169
|
+
// parent's client extent to that same unit, excluding its border/scrollbar.
|
|
170
|
+
const axisScale = vertical
|
|
171
|
+
? parentRect.width / parent.offsetWidth
|
|
172
|
+
: parentRect.height / parent.offsetHeight;
|
|
173
|
+
if (!Number.isFinite(axisScale) || axisScale <= 0)
|
|
174
|
+
return;
|
|
175
|
+
const extent = vertical
|
|
176
|
+
? parent.clientWidth * axisScale - separatorRect.width
|
|
177
|
+
: parent.clientHeight * axisScale - separatorRect.height;
|
|
178
|
+
const scale = options.valuePerPixel === undefined
|
|
179
|
+
? 100 / extent
|
|
180
|
+
: options.valuePerPixel / axisScale;
|
|
181
|
+
if (!Number.isFinite(scale) || scale <= 0)
|
|
182
|
+
return;
|
|
183
|
+
event.preventDefault();
|
|
184
|
+
element.focus();
|
|
185
|
+
element.setPointerCapture(event.pointerId);
|
|
186
|
+
drags.set(element, {
|
|
187
|
+
pointerId: event.pointerId,
|
|
188
|
+
coordinate: vertical ? event.clientX : event.clientY,
|
|
189
|
+
value,
|
|
190
|
+
orientation: vertical ? "vertical" : "horizontal",
|
|
191
|
+
scale,
|
|
192
|
+
});
|
|
193
|
+
}));
|
|
194
|
+
const onpointermove = EventHandler.make(Effect.fn(function* (event) {
|
|
195
|
+
const element = Dom.currentTarget(event);
|
|
196
|
+
const drag = drags.get(element);
|
|
197
|
+
if (drag === undefined || drag.pointerId !== event.pointerId)
|
|
198
|
+
return;
|
|
199
|
+
if (element.getAttribute("aria-disabled") === "true" || (event.buttons & 1) === 0) {
|
|
200
|
+
release(element);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
event.preventDefault();
|
|
204
|
+
const coordinate = drag.orientation === "vertical" ? event.clientX : event.clientY;
|
|
205
|
+
yield* setValue(options.state, drag.value + (coordinate - drag.coordinate) * drag.scale);
|
|
206
|
+
}));
|
|
207
|
+
const endDrag = EventHandler.make((event) => Effect.sync(() => {
|
|
208
|
+
const element = Dom.currentTarget(event);
|
|
209
|
+
if (drags.get(element)?.pointerId === event.pointerId)
|
|
210
|
+
release(element);
|
|
211
|
+
}));
|
|
212
|
+
const onkeydown = EventHandler.make(Effect.fn(function* (event) {
|
|
213
|
+
const current = yield* options.state;
|
|
214
|
+
if (Dom.currentTarget(event).getAttribute("aria-disabled") === "true") {
|
|
215
|
+
return current;
|
|
216
|
+
}
|
|
217
|
+
const delta = keyDelta(event.key, current);
|
|
218
|
+
if (delta !== undefined) {
|
|
219
|
+
event.preventDefault();
|
|
220
|
+
return yield* adjust(options.state, delta);
|
|
221
|
+
}
|
|
222
|
+
if (event.key === "Home") {
|
|
223
|
+
event.preventDefault();
|
|
224
|
+
return yield* setValue(options.state, current.min);
|
|
225
|
+
}
|
|
226
|
+
if (event.key === "End") {
|
|
227
|
+
event.preventDefault();
|
|
228
|
+
return yield* setValue(options.state, current.max);
|
|
229
|
+
}
|
|
230
|
+
if (event.key === "Enter") {
|
|
231
|
+
event.preventDefault();
|
|
232
|
+
return yield* toggleCollapsed(options.state);
|
|
233
|
+
}
|
|
234
|
+
return current;
|
|
235
|
+
}));
|
|
236
|
+
return ({ property }) => ({
|
|
237
|
+
role: "separator",
|
|
238
|
+
style: pointerStyle,
|
|
239
|
+
tabindex: 0,
|
|
240
|
+
"aria-orientation": RefSubject.map(options.state, (state) => state.orientation),
|
|
241
|
+
"aria-valuenow": RefSubject.map(options.state, (state) => state.value),
|
|
242
|
+
"aria-valuemin": RefSubject.map(options.state, (state) => state.min),
|
|
243
|
+
"aria-valuemax": RefSubject.map(options.state, (state) => state.max),
|
|
244
|
+
"aria-controls": property("primaryPaneId", undefined),
|
|
245
|
+
"aria-label": property("label", undefined),
|
|
246
|
+
"aria-valuetext": property("valueText", undefined),
|
|
247
|
+
"aria-disabled": property("disabled", false),
|
|
248
|
+
onkeydown,
|
|
249
|
+
onpointerdown,
|
|
250
|
+
onpointermove,
|
|
251
|
+
onpointerup: endDrag,
|
|
252
|
+
onpointercancel: endDrag,
|
|
253
|
+
onlostpointercapture: endDrag,
|
|
254
|
+
ref: Dom.composeRefs(options.state, pointerRef),
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
/** Renders a focusable ARIA separator for resizing a pane.
|
|
258
|
+
* @remarks
|
|
259
|
+
* The component exposes native DOM focus/events and the ARIA window-splitter
|
|
260
|
+
* range contract: arrows adjust by step, Home/End reach bounds, and Enter
|
|
261
|
+
* collapses/restores. Pointer dragging adjusts the same clamped state, using
|
|
262
|
+
* percentage-of-parent movement by default or the supplied `valuePerPixel`.
|
|
263
|
+
* Native pointer capture keeps movement attached outside the separator;
|
|
264
|
+
* release, cancellation, lost capture, and Scope teardown end the drag.
|
|
265
|
+
* The host composes caller styles with `touch-action: none` so touch gestures
|
|
266
|
+
* remain resize interactions. The caller binds state to actual pane layout. Disabled state
|
|
267
|
+
* leaves keyboard and pointer activation inert.
|
|
268
|
+
* Running the returned Fx owns real key listeners, reactive ARIA attributes,
|
|
269
|
+
* and the hydration ref in its Effect Scope. A custom host must preserve role,
|
|
270
|
+
* tab index, range/relationship attributes, handler, and one hydration owner.
|
|
271
|
+
* @example
|
|
272
|
+
* ```ts
|
|
273
|
+
* import { Effect } from "effect"
|
|
274
|
+
* import * as WindowSplitter from "@typed/ui/WindowSplitter"
|
|
275
|
+
*
|
|
276
|
+
* const program = Effect.gen(function* () {
|
|
277
|
+
* const state = yield* WindowSplitter.makeState({ value: 320, min: 180, max: 640 })
|
|
278
|
+
* return WindowSplitter.WindowSplitter({ state, primaryPaneId: "navigation" })
|
|
279
|
+
* })
|
|
280
|
+
* ```
|
|
281
|
+
* @since 1.0.0
|
|
282
|
+
* @category Resize separator
|
|
283
|
+
*/
|
|
284
|
+
export function WindowSplitter(options, host) {
|
|
285
|
+
return Dom.renderHost()(options, host, internalProps(options), "", (props) => {
|
|
286
|
+
return html `<div ...${props}></div>`;
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
function keyDelta(key, state) {
|
|
290
|
+
if (state.orientation === "vertical") {
|
|
291
|
+
if (key === "ArrowLeft")
|
|
292
|
+
return -state.step;
|
|
293
|
+
if (key === "ArrowRight")
|
|
294
|
+
return state.step;
|
|
295
|
+
return undefined;
|
|
296
|
+
}
|
|
297
|
+
if (key === "ArrowUp")
|
|
298
|
+
return -state.step;
|
|
299
|
+
if (key === "ArrowDown")
|
|
300
|
+
return state.step;
|
|
301
|
+
return undefined;
|
|
302
|
+
}
|
|
303
|
+
function clamp(value, min, max) {
|
|
304
|
+
return Math.min(Math.max(value, min), max);
|
|
305
|
+
}
|