@typed/ui 1.0.0-beta.1 → 1.0.0-beta.11
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 +54 -10
- 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 +198 -55
- 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 +48 -20
- package/src/HttpRouter.test.ts +0 -294
- package/src/HttpRouter.ts +0 -168
- package/src/Link.test.ts +0 -84
- package/src/Link.ts +0 -107
- package/src/index.ts +0 -2
package/dist/Toolbar.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Toolbar is a roving-focus composite with toolbar and button roles. Orientation and RTL control
|
|
3
|
+
* Arrow-key order; Home and End select the document-order endpoints.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* The module keeps policy, state transitions, and DOM rendering separable so applications can use
|
|
7
|
+
* the state and pure operations without mounting UI, or supply custom hosts without replacing native
|
|
8
|
+
* events and browser-owned focus.
|
|
9
|
+
*
|
|
10
|
+
* Learn the interaction in the [Toolbar guide](/explore/ui-toolbar).
|
|
11
|
+
*
|
|
12
|
+
* @since 1.0.0
|
|
13
|
+
* @category modules
|
|
14
|
+
* @packageDocumentation
|
|
15
|
+
*/
|
|
16
|
+
import * as Effect from "effect/Effect";
|
|
17
|
+
import { RefSubject } from "@typed/fx";
|
|
18
|
+
import { EventHandler, html, } from "@typed/template";
|
|
19
|
+
import * as Collection from "./Collection.js";
|
|
20
|
+
import * as Composite from "./Composite.js";
|
|
21
|
+
import * as Dom from "./Dom.js";
|
|
22
|
+
/**
|
|
23
|
+
* Effect Schema used by makeState to encode, decode, and hydrate Toolbar state.
|
|
24
|
+
*
|
|
25
|
+
* @remarks
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* import * as Schema from "effect/Schema";
|
|
29
|
+
* import * as Toolbar from "@typed/ui/Toolbar";
|
|
30
|
+
*
|
|
31
|
+
* const decodeState = Schema.decodeUnknownEffect(Toolbar.StateSchema);
|
|
32
|
+
* ```
|
|
33
|
+
* @since 1.0.0
|
|
34
|
+
* @category Command focus
|
|
35
|
+
*/
|
|
36
|
+
export const StateSchema = Composite.StateSchema;
|
|
37
|
+
/**
|
|
38
|
+
* Creates hydrated Toolbar state. Uses Composite defaults unless overridden.
|
|
39
|
+
*
|
|
40
|
+
* @remarks
|
|
41
|
+
* The returned Effect creates the RefSubject when run. That state is renderer-independent;
|
|
42
|
+
* collection registrations belong to the separate Scope that runs register or ref, not to state
|
|
43
|
+
* creation.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* import * as Effect from "effect/Effect";
|
|
48
|
+
* import * as Toolbar from "@typed/ui/Toolbar";
|
|
49
|
+
*
|
|
50
|
+
* const program = Effect.scoped(
|
|
51
|
+
* Effect.gen(function* () {
|
|
52
|
+
* const state = yield* Toolbar.makeState({});
|
|
53
|
+
* const collection = yield* Toolbar.makeCollection();
|
|
54
|
+
* return { state: yield* state, collection: yield* collection };
|
|
55
|
+
* }),
|
|
56
|
+
* );
|
|
57
|
+
* ```
|
|
58
|
+
* @since 1.0.0
|
|
59
|
+
* @category Command focus
|
|
60
|
+
*/
|
|
61
|
+
export function makeState(initial = {}) {
|
|
62
|
+
return Composite.makeState(initial);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Creates a scoped Collection for Toolbar items.
|
|
66
|
+
*
|
|
67
|
+
* @remarks
|
|
68
|
+
* The returned Effect allocates the RefSubject in the caller's Scope. Each later registration is
|
|
69
|
+
* owned by the Scope that runs register, independently of this construction Effect.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* import * as Effect from "effect/Effect";
|
|
74
|
+
* import * as Toolbar from "@typed/ui/Toolbar";
|
|
75
|
+
*
|
|
76
|
+
* const program = Effect.scoped(
|
|
77
|
+
* Effect.gen(function* () {
|
|
78
|
+
* const collection = yield* Toolbar.makeCollection();
|
|
79
|
+
* return yield* collection;
|
|
80
|
+
* }),
|
|
81
|
+
* );
|
|
82
|
+
* ```
|
|
83
|
+
* @since 1.0.0
|
|
84
|
+
* @category Command registration
|
|
85
|
+
*/
|
|
86
|
+
export const makeCollection = Collection.makeState;
|
|
87
|
+
function rootProps(options) {
|
|
88
|
+
const onfocus = options.collection === undefined
|
|
89
|
+
? undefined
|
|
90
|
+
: Effect.gen(function* () {
|
|
91
|
+
if ((yield* options.state).activeId !== null)
|
|
92
|
+
return;
|
|
93
|
+
yield* Composite.moveAndFocus({ state: options.state, collection: options.collection }, "first");
|
|
94
|
+
});
|
|
95
|
+
const onkeydown = options.collection === undefined
|
|
96
|
+
? undefined
|
|
97
|
+
: EventHandler.make(Effect.fn(function* (event) {
|
|
98
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
99
|
+
const activeId = (yield* options.state).activeId;
|
|
100
|
+
const item = activeId === null
|
|
101
|
+
? undefined
|
|
102
|
+
: (yield* options.collection).find((candidate) => candidate.id === activeId);
|
|
103
|
+
const click = item?.disabled === true ? undefined : Reflect.get(item?.element ?? {}, "click");
|
|
104
|
+
if (typeof click === "function") {
|
|
105
|
+
event.preventDefault();
|
|
106
|
+
yield* Effect.sync(() => click.call(item.element));
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const direction = Composite.keyMove(event, yield* options.state);
|
|
111
|
+
if (direction === undefined)
|
|
112
|
+
return;
|
|
113
|
+
event.preventDefault();
|
|
114
|
+
yield* Composite.moveAndFocus({ state: options.state, collection: options.collection }, direction);
|
|
115
|
+
}));
|
|
116
|
+
return ({ property }) => ({
|
|
117
|
+
role: "toolbar",
|
|
118
|
+
"aria-label": property("label", undefined),
|
|
119
|
+
"aria-orientation": RefSubject.map(options.state, (state) => state.orientation),
|
|
120
|
+
tabindex: Composite.rootTabIndex(options.state),
|
|
121
|
+
onfocus,
|
|
122
|
+
onkeydown,
|
|
123
|
+
ref: options.state,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Renders the toolbar root and moves roving focus through registered items in DOM order.
|
|
128
|
+
*
|
|
129
|
+
* @remarks
|
|
130
|
+
* The returned Fx installs DOM refs, native listeners, state subscriptions, and optional
|
|
131
|
+
* collection registrations only when rendered. The rendering Scope removes those resources;
|
|
132
|
+
* unrelated nodes and attributes remain caller-owned.
|
|
133
|
+
*
|
|
134
|
+
* @since 1.0.0
|
|
135
|
+
* @category Toolbar surface
|
|
136
|
+
*/
|
|
137
|
+
export function Root(options, host) {
|
|
138
|
+
return Dom.renderHost()(options, host, rootProps(options), options.content, (props, content) => {
|
|
139
|
+
return html `<div ...${props}>${content}</div>`;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Consumer-facing alias of the canonical Toolbar component with identical behavior and lifetime.
|
|
144
|
+
*
|
|
145
|
+
* @remarks
|
|
146
|
+
* The alias acquires nothing. Rendering it has exactly the canonical component's Scope and DOM
|
|
147
|
+
* ownership contract.
|
|
148
|
+
*
|
|
149
|
+
* @since 1.0.0
|
|
150
|
+
* @category Toolbar surface
|
|
151
|
+
*/
|
|
152
|
+
export const Toolbar = Root;
|
|
153
|
+
function itemProps(options) {
|
|
154
|
+
const activate = options.disabled === true
|
|
155
|
+
? Effect.void
|
|
156
|
+
: RefSubject.update(options.state, (state) => ({ ...state, activeId: options.id }));
|
|
157
|
+
const register = options.collection === undefined
|
|
158
|
+
? undefined
|
|
159
|
+
: Collection.ref(options.collection, {
|
|
160
|
+
id: options.id,
|
|
161
|
+
value: options.id,
|
|
162
|
+
textValue: options.textValue ?? options.id,
|
|
163
|
+
disabled: options.disabled,
|
|
164
|
+
});
|
|
165
|
+
return () => ({
|
|
166
|
+
id: options.id,
|
|
167
|
+
role: "button",
|
|
168
|
+
"aria-disabled": options.disabled ?? false,
|
|
169
|
+
tabindex: Composite.tabIndex(options.state, options.id),
|
|
170
|
+
onfocus: activate,
|
|
171
|
+
ref: Dom.composeRefs(register, options.ref),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Renders and optionally registers a toolbar button; focus activates it unless disabled.
|
|
176
|
+
*
|
|
177
|
+
* @remarks
|
|
178
|
+
* The returned Fx installs DOM refs, native listeners, state subscriptions, and optional
|
|
179
|
+
* collection registrations only when rendered. The rendering Scope removes those resources;
|
|
180
|
+
* unrelated nodes and attributes remain caller-owned.
|
|
181
|
+
*
|
|
182
|
+
* @since 1.0.0
|
|
183
|
+
* @category Toolbar commands
|
|
184
|
+
*/
|
|
185
|
+
export function Item(options, host) {
|
|
186
|
+
return Dom.renderHost()(options, host, itemProps(options), options.content, (props, content) => html `<div ...${props}>${content}</div>`);
|
|
187
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import type * as Scope from "effect/Scope";
|
|
3
|
+
import * as Schema from "effect/Schema";
|
|
4
|
+
import type { Fx } from "@typed/fx/Fx";
|
|
5
|
+
import { RefSubject } from "@typed/fx";
|
|
6
|
+
import { EventHandler, type Renderable, type RenderEvent, type RenderTemplate } from "@typed/template";
|
|
7
|
+
import * as Dom from "./Dom.js";
|
|
8
|
+
import type { HostResult } from "./Dom/Types.js";
|
|
9
|
+
/** Current tooltip identity and visibility.
|
|
10
|
+
* @remarks
|
|
11
|
+
* ## Why
|
|
12
|
+
* A stable id connects the anchor's description relationship to native popover
|
|
13
|
+
* content while open state stays renderer-independent.
|
|
14
|
+
* ## Ownership and lifetime
|
|
15
|
+
* Plain state retains no resources; RefSubject observation is Scope-owned.
|
|
16
|
+
* @since 1.0.0
|
|
17
|
+
* @category Open state
|
|
18
|
+
*/
|
|
19
|
+
export interface State {
|
|
20
|
+
/** Stable id used by `aria-describedby` and tooltip content.
|
|
21
|
+
* @remarks
|
|
22
|
+
* ## Why
|
|
23
|
+
* Server and client must agree on the relationship target during hydration.
|
|
24
|
+
* ## Ownership and lifetime
|
|
25
|
+
* Plain data acquires no resources.
|
|
26
|
+
* @since 1.0.0
|
|
27
|
+
* @category Identity and relationships
|
|
28
|
+
*/
|
|
29
|
+
readonly id: string;
|
|
30
|
+
/** Whether tooltip content is open.
|
|
31
|
+
* @remarks
|
|
32
|
+
* ## Why
|
|
33
|
+
* The value coordinates delayed anchor interactions and native popover state.
|
|
34
|
+
* ## Ownership and lifetime
|
|
35
|
+
* Plain data acquires no resources.
|
|
36
|
+
* @since 1.0.0
|
|
37
|
+
* @category Open state
|
|
38
|
+
*/
|
|
39
|
+
readonly open: boolean;
|
|
40
|
+
}
|
|
41
|
+
/** Initial tooltip identity and visibility.
|
|
42
|
+
* @remarks
|
|
43
|
+
* ## Why
|
|
44
|
+
* Requiring an id makes the accessibility relationship explicit.
|
|
45
|
+
* ## Ownership and lifetime
|
|
46
|
+
* Configuration is inert.
|
|
47
|
+
* @since 1.0.0
|
|
48
|
+
* @category Open state
|
|
49
|
+
*/
|
|
50
|
+
export interface InitialState {
|
|
51
|
+
/** Stable tooltip id.
|
|
52
|
+
* @remarks
|
|
53
|
+
* ## Why
|
|
54
|
+
* Deterministic ids prevent SSR/client relationship drift.
|
|
55
|
+
* ## Ownership and lifetime
|
|
56
|
+
* Plain data retains no resources.
|
|
57
|
+
* @since 1.0.0
|
|
58
|
+
* @category Identity and relationships
|
|
59
|
+
*/
|
|
60
|
+
readonly id: string;
|
|
61
|
+
/** Initial visibility, defaulting to false.
|
|
62
|
+
* @remarks
|
|
63
|
+
* ## Why
|
|
64
|
+
* Closed is the deterministic default before user focus or hover.
|
|
65
|
+
* ## Ownership and lifetime
|
|
66
|
+
* Plain data retains no resources.
|
|
67
|
+
* @since 1.0.0
|
|
68
|
+
* @category Open state
|
|
69
|
+
*/
|
|
70
|
+
readonly open?: boolean;
|
|
71
|
+
}
|
|
72
|
+
/** Schema for tooltip hydration state.
|
|
73
|
+
* @remarks
|
|
74
|
+
* ## Why
|
|
75
|
+
* Shared identity/open encoding keeps SSR and browser relationships compatible.
|
|
76
|
+
* ## Ownership and lifetime
|
|
77
|
+
* The immutable schema acquires no resources.
|
|
78
|
+
* @since 1.0.0
|
|
79
|
+
* @category Open state
|
|
80
|
+
*/
|
|
81
|
+
export declare const StateSchema: Schema.Struct<{
|
|
82
|
+
readonly id: Schema.String;
|
|
83
|
+
readonly open: Schema.Boolean;
|
|
84
|
+
}>;
|
|
85
|
+
/** Creates hydrated tooltip state.
|
|
86
|
+
* @remarks
|
|
87
|
+
* ## Why
|
|
88
|
+
* Visibility logic can be tested without mounting an anchor or popover.
|
|
89
|
+
* ## Ownership and lifetime
|
|
90
|
+
* The calling Effect Scope owns the returned RefSubject.
|
|
91
|
+
* @example
|
|
92
|
+
* ```ts
|
|
93
|
+
* import { Effect } from "effect"
|
|
94
|
+
* import * as Tooltip from "@typed/ui/Tooltip"
|
|
95
|
+
*
|
|
96
|
+
* const program = Effect.gen(function* () {
|
|
97
|
+
* return yield* Tooltip.makeState({ id: "save-help" })
|
|
98
|
+
* })
|
|
99
|
+
* ```
|
|
100
|
+
* @since 1.0.0
|
|
101
|
+
* @category Open state
|
|
102
|
+
*/
|
|
103
|
+
export declare function makeState(initial: InitialState): Effect.Effect<RefSubject.HydratedRefSubject<{
|
|
104
|
+
readonly id: string;
|
|
105
|
+
readonly open: boolean;
|
|
106
|
+
}, Schema.SchemaError, never, never>, never, Scope.Scope>;
|
|
107
|
+
/** Sets tooltip visibility.
|
|
108
|
+
* @remarks
|
|
109
|
+
* ## Why
|
|
110
|
+
* Delayed focus/hover logic converges on one explicit state transition.
|
|
111
|
+
* ## Ownership and lifetime
|
|
112
|
+
* The Effect uses the existing state Scope and acquires no resource.
|
|
113
|
+
* @example
|
|
114
|
+
* ```ts
|
|
115
|
+
* import { Effect } from "effect"
|
|
116
|
+
* import * as Tooltip from "@typed/ui/Tooltip"
|
|
117
|
+
*
|
|
118
|
+
* const program = Effect.gen(function* () {
|
|
119
|
+
* const state = yield* Tooltip.makeState({ id: "save-help" })
|
|
120
|
+
* yield* Tooltip.setOpen(state, true)
|
|
121
|
+
* })
|
|
122
|
+
* ```
|
|
123
|
+
* @since 1.0.0
|
|
124
|
+
* @category Open state
|
|
125
|
+
*/
|
|
126
|
+
export declare function setOpen<E, R>(state: RefSubject.RefSubject<State, E, R>, open: boolean): Effect.Effect<State, E, R>;
|
|
127
|
+
/** Options for a tooltip anchor host.
|
|
128
|
+
* @remarks
|
|
129
|
+
* ## Why
|
|
130
|
+
* Pointer entry works with the default span. Keyboard focus behavior requires
|
|
131
|
+
* the host itself to be focusable, for example `props: { tabindex: 0 }` or a
|
|
132
|
+
* focusable custom host; focus and blur on descendants do not bubble to it.
|
|
133
|
+
* ## Ownership and lifetime
|
|
134
|
+
* Options are inert; rendering owns listeners/timers by Scope.
|
|
135
|
+
* @since 1.0.0
|
|
136
|
+
* @category Pointer and focus anchors
|
|
137
|
+
*/
|
|
138
|
+
export interface AnchorOptions extends Dom.HostOptions<HTMLSpanElement> {
|
|
139
|
+
/** Hydrated state shared with tooltip content.
|
|
140
|
+
* @remarks
|
|
141
|
+
* ## Why
|
|
142
|
+
* Its id drives `aria-describedby` and its open value drives the popover.
|
|
143
|
+
* ## Ownership and lifetime
|
|
144
|
+
* The anchor borrows state; its original Scope owns it.
|
|
145
|
+
* @since 1.0.0
|
|
146
|
+
* @category State connection
|
|
147
|
+
*/
|
|
148
|
+
readonly state: RefSubject.HydratedRefSubject<State, Schema.SchemaError>;
|
|
149
|
+
/** Anchor content.
|
|
150
|
+
* @remarks
|
|
151
|
+
* ## Why
|
|
152
|
+
* The anchor wraps descriptive content. A focusable descendant does not make
|
|
153
|
+
* the default span receive its non-bubbling focus and blur handlers.
|
|
154
|
+
* ## Ownership and lifetime
|
|
155
|
+
* Dynamic content follows the anchor Scope.
|
|
156
|
+
* @since 1.0.0
|
|
157
|
+
* @category Rendered content
|
|
158
|
+
*/
|
|
159
|
+
readonly content: Renderable.Any;
|
|
160
|
+
/** Delay in milliseconds before opening.
|
|
161
|
+
* @remarks
|
|
162
|
+
* ## Why
|
|
163
|
+
* A delay avoids accidental tooltip activation during pointer transit.
|
|
164
|
+
* ## Ownership and lifetime
|
|
165
|
+
* Delayed Effects run inside the component interaction lifetime; newer
|
|
166
|
+
* schedules invalidate older versions.
|
|
167
|
+
* @since 1.0.0
|
|
168
|
+
* @category Interaction delays
|
|
169
|
+
*/
|
|
170
|
+
readonly showDelay?: number;
|
|
171
|
+
/** Delay in milliseconds before closing.
|
|
172
|
+
* @remarks
|
|
173
|
+
* ## Why
|
|
174
|
+
* A delay prevents flicker during brief pointer movement.
|
|
175
|
+
* ## Ownership and lifetime
|
|
176
|
+
* Newer schedules invalidate older delayed updates.
|
|
177
|
+
* @since 1.0.0
|
|
178
|
+
* @category Interaction delays
|
|
179
|
+
*/
|
|
180
|
+
readonly hideDelay?: number;
|
|
181
|
+
}
|
|
182
|
+
declare function anchorInternalProps<const Options extends AnchorOptions>(options: Options): () => {
|
|
183
|
+
readonly "aria-describedby": RefSubject.Computed<string, Schema.SchemaError, never>;
|
|
184
|
+
readonly onfocus: Effect.Effect<void, Schema.SchemaError, never>;
|
|
185
|
+
readonly onblur: Effect.Effect<void, Schema.SchemaError, never>;
|
|
186
|
+
readonly onkeydown: EventHandler.EventHandler<KeyboardEvent, Schema.SchemaError, never>;
|
|
187
|
+
readonly onmouseenter: Effect.Effect<void, Schema.SchemaError, never>;
|
|
188
|
+
readonly onmouseleave: Effect.Effect<void, Schema.SchemaError, never>;
|
|
189
|
+
};
|
|
190
|
+
type AnchorInternalProps<Options extends AnchorOptions> = ReturnType<ReturnType<typeof anchorInternalProps<Options>>>;
|
|
191
|
+
/** Renders a tooltip anchor with pointer and optional host-focus behavior.
|
|
192
|
+
* @remarks
|
|
193
|
+
* ## Why
|
|
194
|
+
* The default host is a non-focusable `<span>`, so it provides pointer behavior
|
|
195
|
+
* only. Give that host `tabindex`, or supply a natively focusable custom host,
|
|
196
|
+
* to activate its direct focus, blur, and keydown handlers. Those events are
|
|
197
|
+
* attached to the host itself and do not observe focus on nested content.
|
|
198
|
+
* Tooltip content remains descriptive rather than an interactive dialog.
|
|
199
|
+
* ## Ownership and lifetime
|
|
200
|
+
* Running the Fx owns event listeners and dynamic relationship state in its
|
|
201
|
+
* Scope. Custom hosts must preserve `aria-describedby` and all handlers.
|
|
202
|
+
* @example
|
|
203
|
+
* ```ts
|
|
204
|
+
* import { Effect } from "effect"
|
|
205
|
+
* import * as Tooltip from "@typed/ui/Tooltip"
|
|
206
|
+
*
|
|
207
|
+
* const program = Effect.gen(function* () {
|
|
208
|
+
* const state = yield* Tooltip.makeState({ id: "save-help" })
|
|
209
|
+
* return Tooltip.Anchor({
|
|
210
|
+
* state,
|
|
211
|
+
* content: "Save",
|
|
212
|
+
* props: { tabindex: 0 }
|
|
213
|
+
* })
|
|
214
|
+
* })
|
|
215
|
+
* ```
|
|
216
|
+
* @since 1.0.0
|
|
217
|
+
* @category Pointer and focus anchors
|
|
218
|
+
*/
|
|
219
|
+
export declare function Anchor<const Options extends AnchorOptions, const Host extends HostResult = never>(options: Options, host?: Dom.HostOverride<Dom.RenderHostProps<Options, AnchorInternalProps<Options>>, Options["content"], Host>): Fx<RenderEvent, Renderable.Error<Options | Host>, Renderable.Services<Options | Host> | Scope.Scope | RenderTemplate>;
|
|
220
|
+
/** Options for native tooltip popover content.
|
|
221
|
+
* @remarks
|
|
222
|
+
* ## Why
|
|
223
|
+
* The role and native top-layer lifecycle remain explicit.
|
|
224
|
+
* ## Ownership and lifetime
|
|
225
|
+
* Options are inert; rendering owns listeners/ref observation by Scope.
|
|
226
|
+
* @since 1.0.0
|
|
227
|
+
* @category Native content host
|
|
228
|
+
*/
|
|
229
|
+
export interface ContentOptions extends Dom.HostOptions<HTMLDivElement> {
|
|
230
|
+
/** Hydrated state supplying content id and visibility.
|
|
231
|
+
* @remarks
|
|
232
|
+
* ## Why
|
|
233
|
+
* One source keeps ARIA relationship and native popover state aligned.
|
|
234
|
+
* ## Ownership and lifetime
|
|
235
|
+
* Content borrows state; its original Scope owns it.
|
|
236
|
+
* @since 1.0.0
|
|
237
|
+
* @category State connection
|
|
238
|
+
*/
|
|
239
|
+
readonly state: RefSubject.HydratedRefSubject<State, Schema.SchemaError>;
|
|
240
|
+
/** Non-interactive explanatory tooltip content.
|
|
241
|
+
* @remarks
|
|
242
|
+
* ## Why
|
|
243
|
+
* Tooltip content describes its anchor and must not contain required controls.
|
|
244
|
+
* ## Ownership and lifetime
|
|
245
|
+
* Dynamic content follows the content Scope.
|
|
246
|
+
* @since 1.0.0
|
|
247
|
+
* @category Rendered content
|
|
248
|
+
*/
|
|
249
|
+
readonly content: Renderable.Any;
|
|
250
|
+
}
|
|
251
|
+
declare function contentInternalProps<const Options extends ContentOptions>(options: Options): () => {
|
|
252
|
+
readonly id: RefSubject.Computed<string, Schema.SchemaError, never>;
|
|
253
|
+
readonly role: "tooltip";
|
|
254
|
+
readonly popover: "manual";
|
|
255
|
+
readonly onmouseenter: Effect.Effect<void, Schema.SchemaError, never>;
|
|
256
|
+
readonly onmouseleave: EventHandler.EventHandler<MouseEvent, Schema.SchemaError, never>;
|
|
257
|
+
readonly onkeydown: EventHandler.EventHandler<KeyboardEvent, Schema.SchemaError, never>;
|
|
258
|
+
readonly ontoggle: EventHandler.EventHandler<Event, Schema.SchemaError, never>;
|
|
259
|
+
readonly ref: import("./Dom/Refs.js").ComposedRef<RefSubject.HydratedRefSubject<State, Schema.SchemaError, never, never>, (element: HTMLElement) => Effect.Effect<void, Schema.SchemaError, Scope.Scope>> | undefined;
|
|
260
|
+
};
|
|
261
|
+
type ContentInternalProps<Options extends ContentOptions> = ReturnType<ReturnType<typeof contentInternalProps<Options>>>;
|
|
262
|
+
/** Renders `role="tooltip"` content through the native Popover API.
|
|
263
|
+
* @remarks
|
|
264
|
+
* ## Why
|
|
265
|
+
* Native top-layer output avoids reparenting into a private portal while real
|
|
266
|
+
* toggle/mouse/keyboard events synchronize renderer-independent state.
|
|
267
|
+
* ## Ownership and lifetime
|
|
268
|
+
* Running the Fx owns listeners and NativePopover observation in its Scope. A
|
|
269
|
+
* custom host must preserve id, role, manual popover, handlers, and one
|
|
270
|
+
* hydration ref owner.
|
|
271
|
+
* @example
|
|
272
|
+
* ```ts
|
|
273
|
+
* import { Effect } from "effect"
|
|
274
|
+
* import * as Tooltip from "@typed/ui/Tooltip"
|
|
275
|
+
*
|
|
276
|
+
* const program = Effect.gen(function* () {
|
|
277
|
+
* const state = yield* Tooltip.makeState({ id: "save-help" })
|
|
278
|
+
* return Tooltip.Content({ state, content: "Stores changes" })
|
|
279
|
+
* })
|
|
280
|
+
* ```
|
|
281
|
+
* @since 1.0.0
|
|
282
|
+
* @category Native content host
|
|
283
|
+
*/
|
|
284
|
+
export declare function Content<const Options extends ContentOptions, const Host extends HostResult = never>(options: Options, host?: Dom.HostOverride<Dom.RenderHostProps<Options, ContentInternalProps<Options>>, Options["content"], Host>): Fx<RenderEvent, Renderable.Error<Options | Host>, Renderable.Services<Options | Host> | Scope.Scope | RenderTemplate>;
|
|
285
|
+
/** Canonical widget alias for `Content`.
|
|
286
|
+
* @remarks
|
|
287
|
+
* ## Why
|
|
288
|
+
* The alias provides the widget name while `Content` names compound use.
|
|
289
|
+
* ## Ownership and lifetime
|
|
290
|
+
* It has exactly the same Scope and popover ownership as `Content`.
|
|
291
|
+
* @since 1.0.0
|
|
292
|
+
* @category Native content host
|
|
293
|
+
*/
|
|
294
|
+
export declare const Tooltip: typeof Content;
|
|
295
|
+
export {};
|
|
296
|
+
//# sourceMappingURL=Tooltip.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Tooltip.d.ts","sourceRoot":"","sources":["../src/Tooltip.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,KAAK,KAAK,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EACL,YAAY,EAEZ,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,cAAc,EACpB,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,GAAG,MAAM,UAAU,CAAC;AAChC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAGjD;;;;;;;;;GASG;AACH,MAAM,WAAW,KAAK;IACpB;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB;;;;;;;;OAQG;IACH,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAY;IAC3B;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB;;;;;;;;OAQG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,WAAW;;;EAA6D,CAAC;AAEtF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,YAAY;;;0DAE9C;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,CAAC,EAC1B,KAAK,EAAE,UAAU,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,EACzC,IAAI,EAAE,OAAO,GACZ,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,CAE5B;AAeD;;;;;;;;;;GAUG;AACH,MAAM,WAAW,aAAc,SAAQ,GAAG,CAAC,WAAW,CAAC,eAAe,CAAC;IACrE;;;;;;;;OAQG;IACH,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,kBAAkB,CAAC,KAAK,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC;IACzE;;;;;;;;;OASG;IACH,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,CAAC;IACjC;;;;;;;;;OASG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;;;;;OAQG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,iBAAS,mBAAmB,CAAC,KAAK,CAAC,OAAO,SAAS,aAAa,EAAE,OAAO,EAAE,OAAO;aAK5E,kBAAkB;aAClB,OAAO;aACP,MAAM;aACN,SAAS;aAKT,YAAY;aACZ,YAAY;EAEjB;AAED,KAAK,mBAAmB,CAAC,OAAO,SAAS,aAAa,IAAI,UAAU,CAClE,UAAU,CAAC,OAAO,mBAAmB,CAAC,OAAO,CAAC,CAAC,CAChD,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,MAAM,CAAC,KAAK,CAAC,OAAO,SAAS,aAAa,EAAE,KAAK,CAAC,IAAI,SAAS,UAAU,GAAG,KAAK,EAC/F,OAAO,EAAE,OAAO,EAChB,IAAI,CAAC,EAAE,GAAG,CAAC,YAAY,CACrB,GAAG,CAAC,eAAe,CAAC,OAAO,EAAE,mBAAmB,CAAC,OAAO,CAAC,CAAC,EAC1D,OAAO,CAAC,SAAS,CAAC,EAClB,IAAI,CACL,GACA,EAAE,CACH,WAAW,EACX,UAAU,CAAC,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,EAChC,UAAU,CAAC,QAAQ,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,KAAK,CAAC,KAAK,GAAG,cAAc,CACnE,CAcA;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,cAAe,SAAQ,GAAG,CAAC,WAAW,CAAC,cAAc,CAAC;IACrE;;;;;;;;OAQG;IACH,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,kBAAkB,CAAC,KAAK,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC;IACzE;;;;;;;;OAQG;IACH,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,CAAC;CAClC;AAED,iBAAS,oBAAoB,CAAC,KAAK,CAAC,OAAO,SAAS,cAAc,EAAE,OAAO,EAAE,OAAO;;mBAKxE,SAAS;sBACN,QAAQ;;;;;;EAuBtB;AAED,KAAK,oBAAoB,CAAC,OAAO,SAAS,cAAc,IAAI,UAAU,CACpE,UAAU,CAAC,OAAO,oBAAoB,CAAC,OAAO,CAAC,CAAC,CACjD,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,OAAO,CACrB,KAAK,CAAC,OAAO,SAAS,cAAc,EACpC,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,oBAAoB,CAAC,OAAO,CAAC,CAAC,EAC3D,OAAO,CAAC,SAAS,CAAC,EAClB,IAAI,CACL,GACA,EAAE,CACH,WAAW,EACX,UAAU,CAAC,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,EAChC,UAAU,CAAC,QAAQ,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,KAAK,CAAC,KAAK,GAAG,cAAc,CACnE,CAUA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,OAAO,gBAAU,CAAC"}
|
package/dist/Tooltip.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Schema from "effect/Schema";
|
|
3
|
+
import { RefSubject } from "@typed/fx";
|
|
4
|
+
import { EventHandler, html, } from "@typed/template";
|
|
5
|
+
import * as Dom from "./Dom.js";
|
|
6
|
+
import * as NativePopover from "./NativePopover.js";
|
|
7
|
+
/** Schema for tooltip hydration state.
|
|
8
|
+
* @remarks
|
|
9
|
+
* ## Why
|
|
10
|
+
* Shared identity/open encoding keeps SSR and browser relationships compatible.
|
|
11
|
+
* ## Ownership and lifetime
|
|
12
|
+
* The immutable schema acquires no resources.
|
|
13
|
+
* @since 1.0.0
|
|
14
|
+
* @category Open state
|
|
15
|
+
*/
|
|
16
|
+
export const StateSchema = Schema.Struct({ id: Schema.String, open: Schema.Boolean });
|
|
17
|
+
/** Creates hydrated tooltip state.
|
|
18
|
+
* @remarks
|
|
19
|
+
* ## Why
|
|
20
|
+
* Visibility logic can be tested without mounting an anchor or popover.
|
|
21
|
+
* ## Ownership and lifetime
|
|
22
|
+
* The calling Effect Scope owns the returned RefSubject.
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* import { Effect } from "effect"
|
|
26
|
+
* import * as Tooltip from "@typed/ui/Tooltip"
|
|
27
|
+
*
|
|
28
|
+
* const program = Effect.gen(function* () {
|
|
29
|
+
* return yield* Tooltip.makeState({ id: "save-help" })
|
|
30
|
+
* })
|
|
31
|
+
* ```
|
|
32
|
+
* @since 1.0.0
|
|
33
|
+
* @category Open state
|
|
34
|
+
*/
|
|
35
|
+
export function makeState(initial) {
|
|
36
|
+
return RefSubject.hydrate(StateSchema, { id: initial.id, open: initial.open ?? false });
|
|
37
|
+
}
|
|
38
|
+
/** Sets tooltip visibility.
|
|
39
|
+
* @remarks
|
|
40
|
+
* ## Why
|
|
41
|
+
* Delayed focus/hover logic converges on one explicit state transition.
|
|
42
|
+
* ## Ownership and lifetime
|
|
43
|
+
* The Effect uses the existing state Scope and acquires no resource.
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* import { Effect } from "effect"
|
|
47
|
+
* import * as Tooltip from "@typed/ui/Tooltip"
|
|
48
|
+
*
|
|
49
|
+
* const program = Effect.gen(function* () {
|
|
50
|
+
* const state = yield* Tooltip.makeState({ id: "save-help" })
|
|
51
|
+
* yield* Tooltip.setOpen(state, true)
|
|
52
|
+
* })
|
|
53
|
+
* ```
|
|
54
|
+
* @since 1.0.0
|
|
55
|
+
* @category Open state
|
|
56
|
+
*/
|
|
57
|
+
export function setOpen(state, open) {
|
|
58
|
+
return RefSubject.update(state, (current) => ({ ...current, open }));
|
|
59
|
+
}
|
|
60
|
+
const scheduleVersions = new WeakMap();
|
|
61
|
+
const scheduleOpen = Effect.fn(function* (state, open, delay) {
|
|
62
|
+
const version = (scheduleVersions.get(state) ?? 0) + 1;
|
|
63
|
+
scheduleVersions.set(state, version);
|
|
64
|
+
if (delay > 0)
|
|
65
|
+
yield* Effect.sleep(delay);
|
|
66
|
+
if (scheduleVersions.get(state) === version)
|
|
67
|
+
yield* setOpen(state, open);
|
|
68
|
+
});
|
|
69
|
+
function anchorInternalProps(options) {
|
|
70
|
+
const id = RefSubject.map(options.state, (state) => state.id);
|
|
71
|
+
return () => ({
|
|
72
|
+
"aria-describedby": id,
|
|
73
|
+
onfocus: scheduleOpen(options.state, true, options.showDelay ?? 0),
|
|
74
|
+
onblur: scheduleOpen(options.state, false, options.hideDelay ?? 0),
|
|
75
|
+
onkeydown: EventHandler.make(Effect.fn(function* (event) {
|
|
76
|
+
if (event.key === "Escape")
|
|
77
|
+
yield* scheduleOpen(options.state, false, 0);
|
|
78
|
+
})),
|
|
79
|
+
onmouseenter: scheduleOpen(options.state, true, options.showDelay ?? 0),
|
|
80
|
+
onmouseleave: scheduleOpen(options.state, false, options.hideDelay ?? 0),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/** Renders a tooltip anchor with pointer and optional host-focus behavior.
|
|
84
|
+
* @remarks
|
|
85
|
+
* ## Why
|
|
86
|
+
* The default host is a non-focusable `<span>`, so it provides pointer behavior
|
|
87
|
+
* only. Give that host `tabindex`, or supply a natively focusable custom host,
|
|
88
|
+
* to activate its direct focus, blur, and keydown handlers. Those events are
|
|
89
|
+
* attached to the host itself and do not observe focus on nested content.
|
|
90
|
+
* Tooltip content remains descriptive rather than an interactive dialog.
|
|
91
|
+
* ## Ownership and lifetime
|
|
92
|
+
* Running the Fx owns event listeners and dynamic relationship state in its
|
|
93
|
+
* Scope. Custom hosts must preserve `aria-describedby` and all handlers.
|
|
94
|
+
* @example
|
|
95
|
+
* ```ts
|
|
96
|
+
* import { Effect } from "effect"
|
|
97
|
+
* import * as Tooltip from "@typed/ui/Tooltip"
|
|
98
|
+
*
|
|
99
|
+
* const program = Effect.gen(function* () {
|
|
100
|
+
* const state = yield* Tooltip.makeState({ id: "save-help" })
|
|
101
|
+
* return Tooltip.Anchor({
|
|
102
|
+
* state,
|
|
103
|
+
* content: "Save",
|
|
104
|
+
* props: { tabindex: 0 }
|
|
105
|
+
* })
|
|
106
|
+
* })
|
|
107
|
+
* ```
|
|
108
|
+
* @since 1.0.0
|
|
109
|
+
* @category Pointer and focus anchors
|
|
110
|
+
*/
|
|
111
|
+
export function Anchor(options, host) {
|
|
112
|
+
return Dom.renderHost()(options, host, anchorInternalProps(options), options.content, (props, content) => html `<span ...${props}>${content}</span>`);
|
|
113
|
+
}
|
|
114
|
+
function contentInternalProps(options) {
|
|
115
|
+
const id = RefSubject.map(options.state, (state) => state.id);
|
|
116
|
+
return () => ({
|
|
117
|
+
id,
|
|
118
|
+
role: "tooltip",
|
|
119
|
+
popover: "manual",
|
|
120
|
+
onmouseenter: scheduleOpen(options.state, true, 0),
|
|
121
|
+
onmouseleave: EventHandler.make(Effect.fn(function* (event) {
|
|
122
|
+
const contentId = (yield* options.state).id;
|
|
123
|
+
if (event.relatedTarget instanceof Element &&
|
|
124
|
+
event.relatedTarget.getAttribute("aria-describedby") === contentId)
|
|
125
|
+
return;
|
|
126
|
+
yield* scheduleOpen(options.state, false, 0);
|
|
127
|
+
})),
|
|
128
|
+
onkeydown: EventHandler.make(Effect.fn(function* (event) {
|
|
129
|
+
if (event.key === "Escape")
|
|
130
|
+
yield* scheduleOpen(options.state, false, 0);
|
|
131
|
+
})),
|
|
132
|
+
ontoggle: EventHandler.make(Effect.fn((event) => setOpen(options.state, Dom.toggleState(event) === "open"))),
|
|
133
|
+
ref: Dom.composeRefs(options.state, NativePopover.ref(options.state)),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
/** Renders `role="tooltip"` content through the native Popover API.
|
|
137
|
+
* @remarks
|
|
138
|
+
* ## Why
|
|
139
|
+
* Native top-layer output avoids reparenting into a private portal while real
|
|
140
|
+
* toggle/mouse/keyboard events synchronize renderer-independent state.
|
|
141
|
+
* ## Ownership and lifetime
|
|
142
|
+
* Running the Fx owns listeners and NativePopover observation in its Scope. A
|
|
143
|
+
* custom host must preserve id, role, manual popover, handlers, and one
|
|
144
|
+
* hydration ref owner.
|
|
145
|
+
* @example
|
|
146
|
+
* ```ts
|
|
147
|
+
* import { Effect } from "effect"
|
|
148
|
+
* import * as Tooltip from "@typed/ui/Tooltip"
|
|
149
|
+
*
|
|
150
|
+
* const program = Effect.gen(function* () {
|
|
151
|
+
* const state = yield* Tooltip.makeState({ id: "save-help" })
|
|
152
|
+
* return Tooltip.Content({ state, content: "Stores changes" })
|
|
153
|
+
* })
|
|
154
|
+
* ```
|
|
155
|
+
* @since 1.0.0
|
|
156
|
+
* @category Native content host
|
|
157
|
+
*/
|
|
158
|
+
export function Content(options, host) {
|
|
159
|
+
return Dom.renderHost()(options, host, contentInternalProps(options), options.content, (props, content) => {
|
|
160
|
+
return html `<div ...${props}>${content}</div>`;
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
/** Canonical widget alias for `Content`.
|
|
164
|
+
* @remarks
|
|
165
|
+
* ## Why
|
|
166
|
+
* The alias provides the widget name while `Content` names compound use.
|
|
167
|
+
* ## Ownership and lifetime
|
|
168
|
+
* It has exactly the same Scope and popover ownership as `Content`.
|
|
169
|
+
* @since 1.0.0
|
|
170
|
+
* @category Native content host
|
|
171
|
+
*/
|
|
172
|
+
export const Tooltip = Content;
|