@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
package/dist/Meter.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import { RefSubject } from "@typed/fx";
|
|
3
|
+
import { html } from "@typed/template";
|
|
4
|
+
import * as Dom from "./Dom.js";
|
|
5
|
+
/**
|
|
6
|
+
*/
|
|
7
|
+
export const StateSchema = Schema.Struct({ value: Schema.Finite });
|
|
8
|
+
/**
|
|
9
|
+
* Creates hydrated meter state.
|
|
10
|
+
* @remarks
|
|
11
|
+
* The state remains testable and reusable without rendering a UI component.
|
|
12
|
+
* The caller's Effect Scope owns the hydrated RefSubject.
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { Effect } from "effect"
|
|
16
|
+
* import * as Meter from "@typed/ui/Meter"
|
|
17
|
+
*
|
|
18
|
+
* const program = Effect.gen(function* () {
|
|
19
|
+
* return yield* Meter.makeState({ value: 0.72 })
|
|
20
|
+
* })
|
|
21
|
+
* ```
|
|
22
|
+
* @since 1.0.0
|
|
23
|
+
* @category State construction
|
|
24
|
+
*/
|
|
25
|
+
export function makeState(initial) {
|
|
26
|
+
return RefSubject.hydrate(StateSchema, initial);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Assigns the displayed measurement without clamping it to the meter range.
|
|
30
|
+
*
|
|
31
|
+
* @remarks
|
|
32
|
+
* Keep measurement units consistent with min/max and threshold options. This operation does not
|
|
33
|
+
* execute threshold actions or validate the preferred range.
|
|
34
|
+
* @since 1.0.0
|
|
35
|
+
* @category State transitions
|
|
36
|
+
*/
|
|
37
|
+
export function setValue(state, value) {
|
|
38
|
+
return RefSubject.update(state, (current) => ({ ...current, value }));
|
|
39
|
+
}
|
|
40
|
+
function internalProps(options) {
|
|
41
|
+
return ({ property }) => ({
|
|
42
|
+
value: RefSubject.map(options.state, (state) => state.value),
|
|
43
|
+
min: property("min", undefined),
|
|
44
|
+
max: property("max", undefined),
|
|
45
|
+
low: property("low", undefined),
|
|
46
|
+
high: property("high", undefined),
|
|
47
|
+
optimum: property("optimum", undefined),
|
|
48
|
+
ref: options.state,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Renders a native meter for a measurement within a known range.
|
|
53
|
+
*
|
|
54
|
+
* @remarks
|
|
55
|
+
* Supply a label and coherent min/max/low/high/optimum values. The browser presents thresholds;
|
|
56
|
+
* the component does not trigger alerts when they are crossed. Use a slider for editing or a
|
|
57
|
+
* progress element for operation completion. The meter has no keyboard action.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* import { RefSubject } from "@typed/fx";
|
|
62
|
+
* import { html } from "@typed/template";
|
|
63
|
+
* import { component } from "@typed/template";
|
|
64
|
+
* import * as Meter from "@typed/ui/Meter";
|
|
65
|
+
*
|
|
66
|
+
* export const StorageUsage = component(function* () {
|
|
67
|
+
* const state = yield* Meter.makeState({ value: 64 });
|
|
68
|
+
* const description = RefSubject.map(state, ({ value }) => `${value} GB of 100 GB used`);
|
|
69
|
+
* return html`<section>
|
|
70
|
+
* <label for="storage-meter">Storage used</label>
|
|
71
|
+
* ${Meter.Meter({
|
|
72
|
+
* state, min: 0, max: 100, low: 60, high: 85, optimum: 0,
|
|
73
|
+
* content: description,
|
|
74
|
+
* props: { id: "storage-meter", "aria-valuetext": description },
|
|
75
|
+
* })}
|
|
76
|
+
* <p>${description}</p>
|
|
77
|
+
* </section>`;
|
|
78
|
+
* });
|
|
79
|
+
* ```
|
|
80
|
+
* @since 1.0.0
|
|
81
|
+
* @category Controls
|
|
82
|
+
*/
|
|
83
|
+
export function Meter(options, host) {
|
|
84
|
+
return Dom.renderHost()(options, host, internalProps(options), options.content ?? "", (props, content) => {
|
|
85
|
+
return html `<meter ...${props}>${content}</meter>`;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import type * as Scope from "effect/Scope";
|
|
3
|
+
import { RefSubject } from "@typed/fx";
|
|
4
|
+
interface State {
|
|
5
|
+
readonly open: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Creates a scoped ref that synchronizes state to a native `<details>` element.
|
|
9
|
+
*
|
|
10
|
+
* @remarks
|
|
11
|
+
* ## Why
|
|
12
|
+
*
|
|
13
|
+
* `<details>` already owns disclosure rendering and keyboard behavior. This ref
|
|
14
|
+
* only applies pushed `open` state to that real element; the public Disclosure
|
|
15
|
+
* component listens to native `toggle` events for the reverse direction.
|
|
16
|
+
*
|
|
17
|
+
* ## Ownership and lifetime
|
|
18
|
+
*
|
|
19
|
+
* Applying the ref forks one observer in the current Effect Scope. Scope
|
|
20
|
+
* finalization interrupts it; the state remains owned by its original Scope and
|
|
21
|
+
* the DOM element remains owned by its renderer. Use only one hydration ref
|
|
22
|
+
* owner for an element.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { Effect } from "effect"
|
|
27
|
+
* import * as Disclosure from "@typed/ui/Disclosure"
|
|
28
|
+
* import * as NativeDetails from "@typed/ui/NativeDetails"
|
|
29
|
+
*
|
|
30
|
+
* const program = Effect.gen(function* () {
|
|
31
|
+
* const state = yield* Disclosure.makeState()
|
|
32
|
+
* return NativeDetails.ref(state)
|
|
33
|
+
* })
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* @since 1.0.0
|
|
37
|
+
* @category Native synchronization
|
|
38
|
+
*/
|
|
39
|
+
export declare function ref<S extends State, E, R>(state: RefSubject.RefSubject<S, E, R>): (element: HTMLDetailsElement) => Effect.Effect<void, E, R | Scope.Scope>;
|
|
40
|
+
export {};
|
|
41
|
+
//# sourceMappingURL=NativeDetails.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NativeDetails.d.ts","sourceRoot":"","sources":["../src/NativeDetails.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,KAAK,KAAK,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAM,UAAU,EAAE,MAAM,WAAW,CAAC;AAE3C,UAAU,KAAK;IACb,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,GAAG,CAAC,CAAC,SAAS,KAAK,EAAE,CAAC,EAAE,CAAC,EACvC,KAAK,EAAE,UAAU,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GACpC,CAAC,OAAO,EAAE,kBAAkB,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAe1E"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import { Fx } from "@typed/fx";
|
|
3
|
+
/**
|
|
4
|
+
* Creates a scoped ref that synchronizes state to a native `<details>` element.
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* ## Why
|
|
8
|
+
*
|
|
9
|
+
* `<details>` already owns disclosure rendering and keyboard behavior. This ref
|
|
10
|
+
* only applies pushed `open` state to that real element; the public Disclosure
|
|
11
|
+
* component listens to native `toggle` events for the reverse direction.
|
|
12
|
+
*
|
|
13
|
+
* ## Ownership and lifetime
|
|
14
|
+
*
|
|
15
|
+
* Applying the ref forks one observer in the current Effect Scope. Scope
|
|
16
|
+
* finalization interrupts it; the state remains owned by its original Scope and
|
|
17
|
+
* the DOM element remains owned by its renderer. Use only one hydration ref
|
|
18
|
+
* owner for an element.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* import { Effect } from "effect"
|
|
23
|
+
* import * as Disclosure from "@typed/ui/Disclosure"
|
|
24
|
+
* import * as NativeDetails from "@typed/ui/NativeDetails"
|
|
25
|
+
*
|
|
26
|
+
* const program = Effect.gen(function* () {
|
|
27
|
+
* const state = yield* Disclosure.makeState()
|
|
28
|
+
* return NativeDetails.ref(state)
|
|
29
|
+
* })
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* @since 1.0.0
|
|
33
|
+
* @category Native synchronization
|
|
34
|
+
*/
|
|
35
|
+
export function ref(state) {
|
|
36
|
+
return Effect.fn((element) => Effect.asVoid(Effect.forkScoped(Fx.observe(state, Effect.fn((value) => Effect.sync(() => {
|
|
37
|
+
if (element.open !== value.open)
|
|
38
|
+
element.open = value.open;
|
|
39
|
+
}))))));
|
|
40
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import type * as Scope from "effect/Scope";
|
|
3
|
+
import { RefSubject } from "@typed/fx";
|
|
4
|
+
interface State {
|
|
5
|
+
readonly open: boolean;
|
|
6
|
+
}
|
|
7
|
+
/** Native dialog synchronization mode.
|
|
8
|
+
* @remarks
|
|
9
|
+
* ## Why
|
|
10
|
+
* The choice maps directly to `showModal()` or `show()` rather than simulating
|
|
11
|
+
* modality in application code.
|
|
12
|
+
* ## Ownership and lifetime
|
|
13
|
+
* Options are inert and retain no resources.
|
|
14
|
+
* @since 1.0.0
|
|
15
|
+
* @category Native display options
|
|
16
|
+
*/
|
|
17
|
+
export interface Options {
|
|
18
|
+
/** Whether opening uses the modal top layer; defaults to true.
|
|
19
|
+
* @remarks
|
|
20
|
+
* ## Why
|
|
21
|
+
* Modal and non-modal dialogs have different focus, inertness, and dismissal
|
|
22
|
+
* behavior owned by the browser.
|
|
23
|
+
* ## Ownership and lifetime
|
|
24
|
+
* The flag is read during synchronization and retains no resources.
|
|
25
|
+
* @since 1.0.0
|
|
26
|
+
* @category Native display options
|
|
27
|
+
*/
|
|
28
|
+
readonly modal?: boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Creates a scoped ref that drives a real `HTMLDialogElement` from state.
|
|
32
|
+
*
|
|
33
|
+
* @remarks
|
|
34
|
+
* ## Why
|
|
35
|
+
*
|
|
36
|
+
* The ref delegates open/close, top-layer placement, focus handling, and modal
|
|
37
|
+
* inertness to `showModal()`, `show()`, and `close()`. It does not reproduce a
|
|
38
|
+
* dialog with ordinary divs or synthetic events.
|
|
39
|
+
*
|
|
40
|
+
* ## Ownership and lifetime
|
|
41
|
+
*
|
|
42
|
+
* Applying the ref forks an observer in the current Effect Scope. Modal opening
|
|
43
|
+
* waits for a detached host to connect; a newer state or Scope finalization cancels
|
|
44
|
+
* that pending connection check. Hidden documents may defer the check until animation
|
|
45
|
+
* frames resume. Finalization does not close state owned elsewhere or remove
|
|
46
|
+
* the element. `Dialog.Content` handles native `cancel`, `close`, and `toggle`
|
|
47
|
+
* events and composes exactly one hydration owner.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```ts
|
|
51
|
+
* import { Effect } from "effect"
|
|
52
|
+
* import * as Dialog from "@typed/ui/Dialog"
|
|
53
|
+
* import * as NativeDialog from "@typed/ui/NativeDialog"
|
|
54
|
+
*
|
|
55
|
+
* const program = Effect.gen(function* () {
|
|
56
|
+
* const state = yield* Dialog.makeState()
|
|
57
|
+
* return NativeDialog.ref(state, { modal: true })
|
|
58
|
+
* })
|
|
59
|
+
* ```
|
|
60
|
+
*
|
|
61
|
+
* @since 1.0.0
|
|
62
|
+
* @category Native synchronization
|
|
63
|
+
*/
|
|
64
|
+
export declare function ref<S extends State, E, R>(state: RefSubject.RefSubject<S, E, R>, options?: Options): (element: HTMLDialogElement) => Effect.Effect<void, E, R | Scope.Scope>;
|
|
65
|
+
export {};
|
|
66
|
+
//# sourceMappingURL=NativeDialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NativeDialog.d.ts","sourceRoot":"","sources":["../src/NativeDialog.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,KAAK,KAAK,KAAK,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAM,UAAU,EAAE,MAAM,WAAW,CAAC;AAE3C,UAAU,KAAK;IACb,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,OAAO;IACtB;;;;;;;;;OASG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,GAAG,CAAC,CAAC,SAAS,KAAK,EAAE,CAAC,EAAE,CAAC,EACvC,KAAK,EAAE,UAAU,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EACrC,OAAO,GAAE,OAAY,GACpB,CAAC,OAAO,EAAE,iBAAiB,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAqBzE"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Fiber from "effect/Fiber";
|
|
3
|
+
import { Fx } from "@typed/fx";
|
|
4
|
+
/**
|
|
5
|
+
* Creates a scoped ref that drives a real `HTMLDialogElement` from state.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* ## Why
|
|
9
|
+
*
|
|
10
|
+
* The ref delegates open/close, top-layer placement, focus handling, and modal
|
|
11
|
+
* inertness to `showModal()`, `show()`, and `close()`. It does not reproduce a
|
|
12
|
+
* dialog with ordinary divs or synthetic events.
|
|
13
|
+
*
|
|
14
|
+
* ## Ownership and lifetime
|
|
15
|
+
*
|
|
16
|
+
* Applying the ref forks an observer in the current Effect Scope. Modal opening
|
|
17
|
+
* waits for a detached host to connect; a newer state or Scope finalization cancels
|
|
18
|
+
* that pending connection check. Hidden documents may defer the check until animation
|
|
19
|
+
* frames resume. Finalization does not close state owned elsewhere or remove
|
|
20
|
+
* the element. `Dialog.Content` handles native `cancel`, `close`, and `toggle`
|
|
21
|
+
* events and composes exactly one hydration owner.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* import { Effect } from "effect"
|
|
26
|
+
* import * as Dialog from "@typed/ui/Dialog"
|
|
27
|
+
* import * as NativeDialog from "@typed/ui/NativeDialog"
|
|
28
|
+
*
|
|
29
|
+
* const program = Effect.gen(function* () {
|
|
30
|
+
* const state = yield* Dialog.makeState()
|
|
31
|
+
* return NativeDialog.ref(state, { modal: true })
|
|
32
|
+
* })
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* @since 1.0.0
|
|
36
|
+
* @category Native synchronization
|
|
37
|
+
*/
|
|
38
|
+
export function ref(state, options = {}) {
|
|
39
|
+
return Effect.fn(function* (element) {
|
|
40
|
+
let pending;
|
|
41
|
+
yield* Effect.forkScoped(Fx.observe(state, Effect.fn(function* (value) {
|
|
42
|
+
if (pending !== undefined) {
|
|
43
|
+
const previous = pending;
|
|
44
|
+
pending = undefined;
|
|
45
|
+
yield* Fiber.interrupt(previous);
|
|
46
|
+
}
|
|
47
|
+
const update = Effect.sync(() => synchronize(element, value.open, options));
|
|
48
|
+
if (value.open && options.modal !== false && !element.isConnected) {
|
|
49
|
+
pending = yield* Effect.forkScoped(Effect.andThen(whenConnected(element), update));
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
// Complete connected transitions before queued native lifecycle events
|
|
53
|
+
// can reflect an older open state back into the application.
|
|
54
|
+
yield* update;
|
|
55
|
+
}
|
|
56
|
+
})));
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
// Template refs run before their host is inserted. Native showModal requires a connected element.
|
|
60
|
+
function whenConnected(element) {
|
|
61
|
+
return Effect.callback((resume) => {
|
|
62
|
+
if (element.isConnected) {
|
|
63
|
+
resume(Effect.void);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const check = () => {
|
|
67
|
+
if (element.isConnected)
|
|
68
|
+
resume(Effect.void);
|
|
69
|
+
else
|
|
70
|
+
frame = requestAnimationFrame(check);
|
|
71
|
+
};
|
|
72
|
+
let frame = requestAnimationFrame(check);
|
|
73
|
+
return Effect.sync(() => cancelAnimationFrame(frame));
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function synchronize(element, open, options) {
|
|
77
|
+
if (open) {
|
|
78
|
+
if (element.open)
|
|
79
|
+
return;
|
|
80
|
+
if (options.modal === false)
|
|
81
|
+
element.show();
|
|
82
|
+
else
|
|
83
|
+
element.showModal();
|
|
84
|
+
}
|
|
85
|
+
else if (element.open) {
|
|
86
|
+
element.close();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import type * as Scope from "effect/Scope";
|
|
3
|
+
import { RefSubject } from "@typed/fx";
|
|
4
|
+
interface State {
|
|
5
|
+
readonly open: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Creates a scoped ref that synchronizes state with the native Popover API.
|
|
9
|
+
*
|
|
10
|
+
* @remarks
|
|
11
|
+
* ## Why
|
|
12
|
+
*
|
|
13
|
+
* The browser owns top-layer placement and popover lifecycle. The ref checks
|
|
14
|
+
* `:popover-open` before calling `showPopover()` or `hidePopover()`, avoiding
|
|
15
|
+
* invalid duplicate transitions while preserving native toggle events.
|
|
16
|
+
*
|
|
17
|
+
* ## Ownership and lifetime
|
|
18
|
+
*
|
|
19
|
+
* Applying the ref forks one observer in the current Effect Scope. Opening
|
|
20
|
+
* waits for a detached host to connect. A newer state or Scope finalization
|
|
21
|
+
* cancels that pending connection check; hidden documents may defer it until
|
|
22
|
+
* animation frames resume. Closing the Scope interrupts observation without
|
|
23
|
+
* removing the host or closing caller-owned state. The host must support the Popover API and retain its `popover`
|
|
24
|
+
* attribute. Only one hydration owner may be composed for the element.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* import { Effect } from "effect"
|
|
29
|
+
* import * as NativePopover from "@typed/ui/NativePopover"
|
|
30
|
+
* import * as Popover from "@typed/ui/Popover"
|
|
31
|
+
*
|
|
32
|
+
* const program = Effect.gen(function* () {
|
|
33
|
+
* const state = yield* Popover.makeState()
|
|
34
|
+
* return NativePopover.ref(state)
|
|
35
|
+
* })
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* @since 1.0.0
|
|
39
|
+
* @category Native synchronization
|
|
40
|
+
*/
|
|
41
|
+
export declare function ref<S extends State, E, R>(state: RefSubject.RefSubject<S, E, R>): (element: HTMLElement) => Effect.Effect<void, E, R | Scope.Scope>;
|
|
42
|
+
export {};
|
|
43
|
+
//# sourceMappingURL=NativePopover.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NativePopover.d.ts","sourceRoot":"","sources":["../src/NativePopover.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,KAAK,KAAK,KAAK,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAM,UAAU,EAAE,MAAM,WAAW,CAAC;AAE3C,UAAU,KAAK;IACb,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,GAAG,CAAC,CAAC,SAAS,KAAK,EAAE,CAAC,EAAE,CAAC,EACvC,KAAK,EAAE,UAAU,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GACpC,CAAC,OAAO,EAAE,WAAW,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAqBnE"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Fiber from "effect/Fiber";
|
|
3
|
+
import { Fx } from "@typed/fx";
|
|
4
|
+
/**
|
|
5
|
+
* Creates a scoped ref that synchronizes state with the native Popover API.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* ## Why
|
|
9
|
+
*
|
|
10
|
+
* The browser owns top-layer placement and popover lifecycle. The ref checks
|
|
11
|
+
* `:popover-open` before calling `showPopover()` or `hidePopover()`, avoiding
|
|
12
|
+
* invalid duplicate transitions while preserving native toggle events.
|
|
13
|
+
*
|
|
14
|
+
* ## Ownership and lifetime
|
|
15
|
+
*
|
|
16
|
+
* Applying the ref forks one observer in the current Effect Scope. Opening
|
|
17
|
+
* waits for a detached host to connect. A newer state or Scope finalization
|
|
18
|
+
* cancels that pending connection check; hidden documents may defer it until
|
|
19
|
+
* animation frames resume. Closing the Scope interrupts observation without
|
|
20
|
+
* removing the host or closing caller-owned state. The host must support the Popover API and retain its `popover`
|
|
21
|
+
* attribute. Only one hydration owner may be composed for the element.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* import { Effect } from "effect"
|
|
26
|
+
* import * as NativePopover from "@typed/ui/NativePopover"
|
|
27
|
+
* import * as Popover from "@typed/ui/Popover"
|
|
28
|
+
*
|
|
29
|
+
* const program = Effect.gen(function* () {
|
|
30
|
+
* const state = yield* Popover.makeState()
|
|
31
|
+
* return NativePopover.ref(state)
|
|
32
|
+
* })
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* @since 1.0.0
|
|
36
|
+
* @category Native synchronization
|
|
37
|
+
*/
|
|
38
|
+
export function ref(state) {
|
|
39
|
+
return Effect.fn(function* (element) {
|
|
40
|
+
let pending;
|
|
41
|
+
yield* Effect.forkScoped(Fx.observe(state, Effect.fn(function* (value) {
|
|
42
|
+
if (pending !== undefined) {
|
|
43
|
+
const previous = pending;
|
|
44
|
+
pending = undefined;
|
|
45
|
+
yield* Fiber.interrupt(previous);
|
|
46
|
+
}
|
|
47
|
+
const update = Effect.sync(() => synchronize(element, value.open));
|
|
48
|
+
if (value.open && !element.isConnected) {
|
|
49
|
+
pending = yield* Effect.forkScoped(Effect.andThen(whenConnected(element), update));
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
// Connected transitions stay in the observer: Menu/Select can focus
|
|
53
|
+
// their now-visible items immediately after updating open state.
|
|
54
|
+
yield* update;
|
|
55
|
+
}
|
|
56
|
+
})));
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
// Template refs run before insertion; native showPopover requires a connected host.
|
|
60
|
+
function whenConnected(element) {
|
|
61
|
+
return Effect.callback((resume) => {
|
|
62
|
+
if (element.isConnected) {
|
|
63
|
+
resume(Effect.void);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const check = () => {
|
|
67
|
+
if (element.isConnected)
|
|
68
|
+
resume(Effect.void);
|
|
69
|
+
else
|
|
70
|
+
frame = requestAnimationFrame(check);
|
|
71
|
+
};
|
|
72
|
+
let frame = requestAnimationFrame(check);
|
|
73
|
+
return Effect.sync(() => cancelAnimationFrame(frame));
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function synchronize(element, open) {
|
|
77
|
+
if (open) {
|
|
78
|
+
if (!element.matches(":popover-open"))
|
|
79
|
+
element.showPopover();
|
|
80
|
+
}
|
|
81
|
+
else if (element.matches(":popover-open")) {
|
|
82
|
+
element.hidePopover();
|
|
83
|
+
}
|
|
84
|
+
}
|