@jsenv/navi 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +51 -0
- package/package.json +38 -0
- package/src/action_private_properties.js +11 -0
- package/src/action_proxy_test.html +353 -0
- package/src/action_run_states.js +5 -0
- package/src/actions.js +1377 -0
- package/src/browser_integration/browser_integration.js +191 -0
- package/src/browser_integration/document_back_and_forward.js +17 -0
- package/src/browser_integration/document_loading_signal.js +100 -0
- package/src/browser_integration/document_state_signal.js +9 -0
- package/src/browser_integration/document_url_signal.js +9 -0
- package/src/browser_integration/use_is_visited.js +19 -0
- package/src/browser_integration/via_history.js +199 -0
- package/src/browser_integration/via_navigation.js +168 -0
- package/src/components/action_execution/form_context.js +8 -0
- package/src/components/action_execution/render_actionable_component.jsx +27 -0
- package/src/components/action_execution/use_action.js +330 -0
- package/src/components/action_execution/use_execute_action.js +161 -0
- package/src/components/action_renderer.jsx +136 -0
- package/src/components/collect_form_element_values.js +79 -0
- package/src/components/demos/0_button_demo.html +155 -0
- package/src/components/demos/1_checkbox_demo.html +257 -0
- package/src/components/demos/2_input_textual_demo.html +354 -0
- package/src/components/demos/3_radio_demo.html +222 -0
- package/src/components/demos/4_select_demo.html +104 -0
- package/src/components/demos/5_list_scrollable_demo.html +153 -0
- package/src/components/demos/action/0_button_demo.html +204 -0
- package/src/components/demos/action/10_shortcuts_demo.html +189 -0
- package/src/components/demos/action/11_nested_shortcuts_demo.html +401 -0
- package/src/components/demos/action/1_input_text_demo.html +461 -0
- package/src/components/demos/action/2_form_multiple.html +303 -0
- package/src/components/demos/action/3_details_demo.html +172 -0
- package/src/components/demos/action/4_input_checkbox_demo.html +611 -0
- package/src/components/demos/action/6_checkbox_list_demo.html +109 -0
- package/src/components/demos/action/7_radio_list_demo.html +217 -0
- package/src/components/demos/action/8_editable_text_demo.html +442 -0
- package/src/components/demos/action/9_link_demo.html +172 -0
- package/src/components/demos/demo.md +0 -0
- package/src/components/demos/route/basic/basic.html +14 -0
- package/src/components/demos/route/basic/basic_route_demo.jsx +224 -0
- package/src/components/demos/route/multi/multi.html +14 -0
- package/src/components/demos/route/multi/multi_route_demo.jsx +277 -0
- package/src/components/details/details.jsx +248 -0
- package/src/components/details/summary_marker.jsx +141 -0
- package/src/components/editable_text/editable_text.jsx +96 -0
- package/src/components/error_boundary_context.js +9 -0
- package/src/components/form.jsx +144 -0
- package/src/components/input/button.jsx +333 -0
- package/src/components/input/checkbox_list.jsx +294 -0
- package/src/components/input/field.jsx +61 -0
- package/src/components/input/field_css.js +118 -0
- package/src/components/input/input.jsx +15 -0
- package/src/components/input/input_checkbox.jsx +370 -0
- package/src/components/input/input_radio.jsx +299 -0
- package/src/components/input/input_textual.jsx +338 -0
- package/src/components/input/radio_list.jsx +283 -0
- package/src/components/input/select.jsx +273 -0
- package/src/components/input/use_form_event.js +20 -0
- package/src/components/input/use_on_change.js +12 -0
- package/src/components/link/link.jsx +291 -0
- package/src/components/loader/loader_background.jsx +324 -0
- package/src/components/loader/loading_spinner.jsx +68 -0
- package/src/components/loader/network_speed.js +83 -0
- package/src/components/loader/rectangle_loading.jsx +225 -0
- package/src/components/route.jsx +15 -0
- package/src/components/selection/selection.js +5 -0
- package/src/components/selection/selection_context.jsx +262 -0
- package/src/components/shortcut/os.js +9 -0
- package/src/components/shortcut/shortcut_context.jsx +390 -0
- package/src/components/use_action_events.js +37 -0
- package/src/components/use_auto_focus.js +43 -0
- package/src/components/use_debounce_true.js +31 -0
- package/src/components/use_focus_group.js +19 -0
- package/src/components/use_initial_value.js +104 -0
- package/src/components/use_is_visited.js +19 -0
- package/src/components/use_ref_array.js +38 -0
- package/src/components/use_signal_sync.js +50 -0
- package/src/components/use_state_array.js +40 -0
- package/src/docs/actions.md +228 -0
- package/src/docs/demos/resource/action_status.jsx +42 -0
- package/src/docs/demos/resource/demo.md +1 -0
- package/src/docs/demos/resource/resource_demo_0.html +84 -0
- package/src/docs/demos/resource/resource_demo_10_post_gc.html +364 -0
- package/src/docs/demos/resource/resource_demo_11_describe_many.html +362 -0
- package/src/docs/demos/resource/resource_demo_2.html +173 -0
- package/src/docs/demos/resource/resource_demo_3_filtered_users.html +415 -0
- package/src/docs/demos/resource/resource_demo_4_details.html +284 -0
- package/src/docs/demos/resource/resource_demo_5_renderer_lazy.html +115 -0
- package/src/docs/demos/resource/resource_demo_6_gc.html +217 -0
- package/src/docs/demos/resource/resource_demo_7_child_gc.html +240 -0
- package/src/docs/demos/resource/resource_demo_8_proxy_gc.html +319 -0
- package/src/docs/demos/resource/resource_demo_9_describe_one.html +472 -0
- package/src/docs/demos/resource/tata.jsx +3 -0
- package/src/docs/demos/resource/toto.jsx +3 -0
- package/src/docs/demos/user_nav/user_nav.html +12 -0
- package/src/docs/demos/user_nav/user_nav.jsx +330 -0
- package/src/docs/resource_dependencies.md +103 -0
- package/src/docs/resource_with_params.md +80 -0
- package/src/notes.md +13 -0
- package/src/route/route.js +518 -0
- package/src/route/route.test.html +228 -0
- package/src/store/array_signal_store.js +537 -0
- package/src/store/local_storage_signal.js +17 -0
- package/src/store/resource_graph.js +1303 -0
- package/src/store/tests/resource_graph_autoreload_demo.html +12 -0
- package/src/store/tests/resource_graph_autoreload_demo.jsx +964 -0
- package/src/store/tests/resource_graph_dependencies.test.js +95 -0
- package/src/store/value_in_local_storage.js +187 -0
- package/src/symbol_object_signal.js +1 -0
- package/src/use_action_data.js +10 -0
- package/src/use_action_status.js +47 -0
- package/src/utils/add_many_event_listeners.js +15 -0
- package/src/utils/array_add_remove.js +61 -0
- package/src/utils/array_signal.js +15 -0
- package/src/utils/compare_two_js_values.js +172 -0
- package/src/utils/execute_with_cleanup.js +21 -0
- package/src/utils/get_caller_info.js +85 -0
- package/src/utils/iterable_weak_set.js +62 -0
- package/src/utils/js_value_weak_map.js +162 -0
- package/src/utils/js_value_weak_map_demo.html +690 -0
- package/src/utils/merge_two_js_values.js +53 -0
- package/src/utils/stringify_for_display.js +150 -0
- package/src/utils/weak_effect.js +48 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { closeValidationMessage, useConstraints } from "@jsenv/validation";
|
|
2
|
+
import { forwardRef } from "preact/compat";
|
|
3
|
+
import { useImperativeHandle, useLayoutEffect, useRef } from "preact/hooks";
|
|
4
|
+
import { useIsVisited } from "../../browser_integration/use_is_visited.js";
|
|
5
|
+
import { useActionStatus } from "../../use_action_status.js";
|
|
6
|
+
import { renderActionableComponent } from "../action_execution/render_actionable_component.jsx";
|
|
7
|
+
import { LoaderBackground } from "../loader/loader_background.jsx";
|
|
8
|
+
import {
|
|
9
|
+
clickToSelect,
|
|
10
|
+
keydownToSelect,
|
|
11
|
+
useRegisterSelectionValue,
|
|
12
|
+
useSelectionContext,
|
|
13
|
+
} from "../selection/selection_context.jsx";
|
|
14
|
+
import {
|
|
15
|
+
ShortcutProvider,
|
|
16
|
+
useShortcutContext,
|
|
17
|
+
} from "../shortcut/shortcut_context.jsx";
|
|
18
|
+
import { useAutoFocus } from "../use_auto_focus.js";
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
* Apply opacity to child content, not the link element itself.
|
|
22
|
+
*
|
|
23
|
+
* Why not apply opacity directly to .navi_link?
|
|
24
|
+
* - Would make focus outlines semi-transparent too (accessibility issue)
|
|
25
|
+
* - We want dimmed text but full-opacity focus indicators for visibility
|
|
26
|
+
*
|
|
27
|
+
* This approach dims the content while preserving focus outline visibility.
|
|
28
|
+
*/
|
|
29
|
+
import.meta.css = /* css */ `
|
|
30
|
+
.navi_link {
|
|
31
|
+
border-radius: 2px;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.navi_link:focus {
|
|
35
|
+
position: relative;
|
|
36
|
+
z-index: 1; /* Ensure focus outline is above other elements */
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.navi_link[data-readonly] > *,
|
|
40
|
+
.navi_link[inert] > * {
|
|
41
|
+
opacity: 0.5;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.navi_link[inert] {
|
|
45
|
+
pointer-events: none;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.navi_link[data-with-selection] {
|
|
49
|
+
position: relative;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.navi_link[data-with-selection] input[type="checkbox"] {
|
|
53
|
+
position: absolute;
|
|
54
|
+
opacity: 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* Visual feedback for selected state */
|
|
58
|
+
.navi_link[data-selected] {
|
|
59
|
+
background-color: light-dark(#bbdefb, #2563eb);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.navi_link[data-active] {
|
|
63
|
+
font-weight: bold;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.navi_link[data-visited] {
|
|
67
|
+
color: light-dark(#6a1b9a, #ab47bc);
|
|
68
|
+
}
|
|
69
|
+
`;
|
|
70
|
+
|
|
71
|
+
export const Link = forwardRef((props, ref) => {
|
|
72
|
+
return renderActionableComponent(props, ref, {
|
|
73
|
+
Basic: LinkBasic,
|
|
74
|
+
WithAction: LinkWithAction,
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const LinkBasic = forwardRef((props, ref) => {
|
|
79
|
+
const selectionContext = useSelectionContext();
|
|
80
|
+
|
|
81
|
+
if (selectionContext) {
|
|
82
|
+
return (
|
|
83
|
+
<LinkWithSelection
|
|
84
|
+
ref={ref}
|
|
85
|
+
selectionContext={selectionContext}
|
|
86
|
+
{...props}
|
|
87
|
+
/>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return <LinkPlain ref={ref} {...props} />;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const LinkPlain = forwardRef((props, ref) => {
|
|
94
|
+
const {
|
|
95
|
+
className = "",
|
|
96
|
+
loading,
|
|
97
|
+
readOnly,
|
|
98
|
+
disabled,
|
|
99
|
+
children,
|
|
100
|
+
autoFocus,
|
|
101
|
+
active,
|
|
102
|
+
visited,
|
|
103
|
+
spaceToClick = true,
|
|
104
|
+
constraints = [],
|
|
105
|
+
onClick,
|
|
106
|
+
onKeyDown,
|
|
107
|
+
href,
|
|
108
|
+
...rest
|
|
109
|
+
} = props;
|
|
110
|
+
|
|
111
|
+
const innerRef = useRef();
|
|
112
|
+
useImperativeHandle(ref, () => innerRef.current);
|
|
113
|
+
|
|
114
|
+
useAutoFocus(innerRef, autoFocus);
|
|
115
|
+
useConstraints(innerRef, constraints);
|
|
116
|
+
|
|
117
|
+
const shouldDimColor = readOnly || disabled;
|
|
118
|
+
useDimColorWhen(innerRef, shouldDimColor);
|
|
119
|
+
|
|
120
|
+
const isVisited = useIsVisited(href);
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<LoaderBackground loading={loading} color="light-dark(#355fcc, #3b82f6)">
|
|
124
|
+
<a
|
|
125
|
+
{...rest}
|
|
126
|
+
href={href}
|
|
127
|
+
ref={innerRef}
|
|
128
|
+
className={["navi_link", ...className.split(" ")].join(" ")}
|
|
129
|
+
aria-busy={loading}
|
|
130
|
+
inert={disabled}
|
|
131
|
+
data-field=""
|
|
132
|
+
data-readonly={readOnly ? "" : undefined}
|
|
133
|
+
data-active={active ? "" : undefined}
|
|
134
|
+
data-visited={visited || isVisited ? "" : undefined}
|
|
135
|
+
onClick={(e) => {
|
|
136
|
+
closeValidationMessage(e.target, "click");
|
|
137
|
+
if (readOnly) {
|
|
138
|
+
e.preventDefault();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
onClick?.(e);
|
|
142
|
+
}}
|
|
143
|
+
onKeyDown={(e) => {
|
|
144
|
+
if (spaceToClick && e.key === " ") {
|
|
145
|
+
e.preventDefault(); // Prevent page scroll
|
|
146
|
+
if (!readOnly && !disabled) {
|
|
147
|
+
e.target.click();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
onKeyDown?.(e);
|
|
151
|
+
}}
|
|
152
|
+
>
|
|
153
|
+
{children}
|
|
154
|
+
</a>
|
|
155
|
+
</LoaderBackground>
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const LinkWithSelection = forwardRef((props, ref) => {
|
|
160
|
+
const {
|
|
161
|
+
selectionContext,
|
|
162
|
+
name,
|
|
163
|
+
value,
|
|
164
|
+
children,
|
|
165
|
+
onClick,
|
|
166
|
+
onKeyDown,
|
|
167
|
+
...rest
|
|
168
|
+
} = props;
|
|
169
|
+
const isSelected = selectionContext.isSelected(value);
|
|
170
|
+
useRegisterSelectionValue(value);
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<LinkPlain
|
|
174
|
+
ref={ref}
|
|
175
|
+
{...rest}
|
|
176
|
+
onClick={(e) => {
|
|
177
|
+
clickToSelect(e, { selectionContext, value });
|
|
178
|
+
onClick?.(e);
|
|
179
|
+
}}
|
|
180
|
+
onKeyDown={(e) => {
|
|
181
|
+
keydownToSelect(e, { selectionContext, value });
|
|
182
|
+
onKeyDown?.(e);
|
|
183
|
+
}}
|
|
184
|
+
data-with-selection=""
|
|
185
|
+
data-selected={isSelected ? "" : undefined}
|
|
186
|
+
>
|
|
187
|
+
<input
|
|
188
|
+
className="navi_link_checkbox"
|
|
189
|
+
type="checkbox"
|
|
190
|
+
name={name}
|
|
191
|
+
value={value}
|
|
192
|
+
checked={isSelected}
|
|
193
|
+
// Prevent direct checkbox interaction - only via link clicks or keyboard nav (arrows)
|
|
194
|
+
disabled
|
|
195
|
+
tabIndex={-1} // Don't interfere with link tab order (might be overkill because there is already [disabled])
|
|
196
|
+
/>
|
|
197
|
+
{children}
|
|
198
|
+
</LinkPlain>
|
|
199
|
+
);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
/*
|
|
203
|
+
* Custom hook to apply semi-transparent color when an element should be dimmed.
|
|
204
|
+
*
|
|
205
|
+
* Why we do it this way:
|
|
206
|
+
* 1. **Precise timing**: Captures the element's natural color exactly when transitioning
|
|
207
|
+
* from normal to dimmed state (not before, not after)
|
|
208
|
+
* 2. **Avoids CSS inheritance issues**: CSS `currentColor` and `color-mix()` don't work
|
|
209
|
+
* reliably for creating true transparency that matches `opacity: 0.5`
|
|
210
|
+
* 3. **Performance**: Only executes when the dimmed state actually changes, not on every render
|
|
211
|
+
* 4. **Color accuracy**: Uses `color(from ... / 0.5)` syntax to preserve the exact visual
|
|
212
|
+
* appearance of `opacity: 0.5` but applied only to color
|
|
213
|
+
* 5. **Works with any color**: Handles default blue, visited purple, inherited colors, etc.
|
|
214
|
+
* 6. **Maintains focus outline**: Since we only dim the text color, focus outlines remain
|
|
215
|
+
* fully visible for accessibility
|
|
216
|
+
*/
|
|
217
|
+
const useDimColorWhen = (elementRef, shouldDim) => {
|
|
218
|
+
const shouldDimPreviousRef = useRef();
|
|
219
|
+
useLayoutEffect(() => {
|
|
220
|
+
const element = elementRef.current;
|
|
221
|
+
const shouldDimPrevious = shouldDimPreviousRef.current;
|
|
222
|
+
|
|
223
|
+
if (shouldDim === shouldDimPrevious) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
shouldDimPreviousRef.current = shouldDim;
|
|
227
|
+
if (shouldDim) {
|
|
228
|
+
// Capture color just before applying disabled state
|
|
229
|
+
const computedStyle = getComputedStyle(element);
|
|
230
|
+
const currentColor = computedStyle.color;
|
|
231
|
+
element.style.color = `color(from ${currentColor} srgb r g b / 0.5)`;
|
|
232
|
+
} else {
|
|
233
|
+
// Clear the inline style to let CSS take over
|
|
234
|
+
element.style.color = "";
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const LinkWithAction = forwardRef((props, ref) => {
|
|
240
|
+
const {
|
|
241
|
+
shortcuts = [],
|
|
242
|
+
onActionPrevented,
|
|
243
|
+
onActionStart,
|
|
244
|
+
onActionAbort,
|
|
245
|
+
onActionError,
|
|
246
|
+
onActionEnd,
|
|
247
|
+
...rest
|
|
248
|
+
} = props;
|
|
249
|
+
const innerRef = useRef();
|
|
250
|
+
useImperativeHandle(ref, () => innerRef.current);
|
|
251
|
+
|
|
252
|
+
return (
|
|
253
|
+
<ShortcutProvider
|
|
254
|
+
shortcuts={shortcuts}
|
|
255
|
+
elementRef={innerRef}
|
|
256
|
+
onActionPrevented={onActionPrevented}
|
|
257
|
+
onActionStart={onActionStart}
|
|
258
|
+
onActionAbort={onActionAbort}
|
|
259
|
+
onActionError={onActionError}
|
|
260
|
+
onActionEnd={onActionEnd}
|
|
261
|
+
>
|
|
262
|
+
<LinkWithShortcuts ref={innerRef} {...rest} />
|
|
263
|
+
</ShortcutProvider>
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const LinkWithShortcuts = forwardRef((props, ref) => {
|
|
268
|
+
const { children, readOnly, loading, ...rest } = props;
|
|
269
|
+
const innerRef = useRef();
|
|
270
|
+
useImperativeHandle(ref, () => innerRef.current);
|
|
271
|
+
const { shortcutAction } = useShortcutContext();
|
|
272
|
+
|
|
273
|
+
const { loading: actionLoading } = useActionStatus(shortcutAction);
|
|
274
|
+
const innerLoading = Boolean(loading || actionLoading);
|
|
275
|
+
|
|
276
|
+
return (
|
|
277
|
+
<>
|
|
278
|
+
<LinkBasic
|
|
279
|
+
ref={innerRef}
|
|
280
|
+
{...rest}
|
|
281
|
+
loading={innerLoading}
|
|
282
|
+
readOnly={readOnly || actionLoading}
|
|
283
|
+
data-readonly-silent={actionLoading && !readOnly ? "" : undefined}
|
|
284
|
+
/* When we have keyboard shortcuts the link outline is visible on focus (not solely on focus-visible) */
|
|
285
|
+
data-focus-visible=""
|
|
286
|
+
>
|
|
287
|
+
{children}
|
|
288
|
+
</LinkBasic>
|
|
289
|
+
</>
|
|
290
|
+
);
|
|
291
|
+
});
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { resolveCSSSize } from "@jsenv/dom";
|
|
2
|
+
import { createPortal } from "preact/compat";
|
|
3
|
+
import { useLayoutEffect, useRef, useState } from "preact/hooks";
|
|
4
|
+
import { useDebounceTrue } from "../use_debounce_true.js";
|
|
5
|
+
import { RectangleLoading } from "./rectangle_loading.jsx";
|
|
6
|
+
|
|
7
|
+
import.meta.css = /* css */ `
|
|
8
|
+
[name="element_with_loader_wrapper"] {
|
|
9
|
+
display: inline-flex;
|
|
10
|
+
position: relative;
|
|
11
|
+
width: fit-content;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
[name="loading_rectangle_wrapper"] {
|
|
15
|
+
pointer-events: none;
|
|
16
|
+
position: absolute;
|
|
17
|
+
z-index: 1;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
[name="rectangle_loading"] {
|
|
21
|
+
position: relative;
|
|
22
|
+
width: 100%;
|
|
23
|
+
height: 100%;
|
|
24
|
+
}
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
export const LoaderBackground = ({
|
|
28
|
+
loading,
|
|
29
|
+
containerRef,
|
|
30
|
+
targetSelector,
|
|
31
|
+
color,
|
|
32
|
+
inset = 0,
|
|
33
|
+
spacingTop = 0,
|
|
34
|
+
spacingLeft = 0,
|
|
35
|
+
spacingBottom = 0,
|
|
36
|
+
spacingRight = 0,
|
|
37
|
+
children,
|
|
38
|
+
}) => {
|
|
39
|
+
if (containerRef) {
|
|
40
|
+
const container = containerRef.current;
|
|
41
|
+
if (!container) {
|
|
42
|
+
return children;
|
|
43
|
+
}
|
|
44
|
+
return createPortal(
|
|
45
|
+
<LoaderBackgroundWithPortal
|
|
46
|
+
container={container}
|
|
47
|
+
loading={loading}
|
|
48
|
+
color={color}
|
|
49
|
+
inset={inset}
|
|
50
|
+
spacingTop={spacingTop}
|
|
51
|
+
spacingLeft={spacingLeft}
|
|
52
|
+
spacingBottom={spacingBottom}
|
|
53
|
+
spacingRight={spacingRight}
|
|
54
|
+
>
|
|
55
|
+
{children}
|
|
56
|
+
</LoaderBackgroundWithPortal>,
|
|
57
|
+
container,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<LoaderBackgroundWithWrapper
|
|
63
|
+
targetSelector={targetSelector}
|
|
64
|
+
loading={loading}
|
|
65
|
+
color={color}
|
|
66
|
+
inset={inset}
|
|
67
|
+
spacingTop={spacingTop}
|
|
68
|
+
spacingLeft={spacingLeft}
|
|
69
|
+
spacingBottom={spacingBottom}
|
|
70
|
+
spacingRight={spacingRight}
|
|
71
|
+
>
|
|
72
|
+
{children}
|
|
73
|
+
</LoaderBackgroundWithWrapper>
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const LoaderBackgroundWithPortal = ({
|
|
78
|
+
container,
|
|
79
|
+
loading,
|
|
80
|
+
color,
|
|
81
|
+
inset,
|
|
82
|
+
spacingTop,
|
|
83
|
+
spacingLeft,
|
|
84
|
+
spacingBottom,
|
|
85
|
+
spacingRight,
|
|
86
|
+
children,
|
|
87
|
+
}) => {
|
|
88
|
+
const shouldShowSpinner = useDebounceTrue(loading, 300);
|
|
89
|
+
|
|
90
|
+
if (!shouldShowSpinner) {
|
|
91
|
+
return children;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
container.style.position = "relative";
|
|
95
|
+
let paddingTop = 0;
|
|
96
|
+
if (container.nodeName === "DETAILS") {
|
|
97
|
+
paddingTop = container.querySelector("summary").offsetHeight;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<>
|
|
102
|
+
<div
|
|
103
|
+
style={{
|
|
104
|
+
position: "absolute",
|
|
105
|
+
top: `${inset + paddingTop + spacingTop}px`,
|
|
106
|
+
bottom: `${inset + spacingBottom}px`,
|
|
107
|
+
left: `${inset + spacingLeft}px`,
|
|
108
|
+
right: `${inset + spacingRight}px`,
|
|
109
|
+
}}
|
|
110
|
+
>
|
|
111
|
+
{shouldShowSpinner && <RectangleLoading color={color} />}
|
|
112
|
+
</div>
|
|
113
|
+
{children}
|
|
114
|
+
</>
|
|
115
|
+
);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const LoaderBackgroundWithWrapper = ({
|
|
119
|
+
loading,
|
|
120
|
+
targetSelector,
|
|
121
|
+
color,
|
|
122
|
+
spacingTop,
|
|
123
|
+
spacingLeft,
|
|
124
|
+
spacingBottom,
|
|
125
|
+
spacingRight,
|
|
126
|
+
inset,
|
|
127
|
+
children,
|
|
128
|
+
}) => {
|
|
129
|
+
const shouldShowSpinner = useDebounceTrue(loading, 300);
|
|
130
|
+
const containerRef = useRef(null);
|
|
131
|
+
const [outlineOffset, setOutlineOffset] = useState(0);
|
|
132
|
+
const [borderRadius, setBorderRadius] = useState(0);
|
|
133
|
+
const [borderTopWidth, setBorderTopWidth] = useState(0);
|
|
134
|
+
const [borderLeftWidth, setBorderLeftWidth] = useState(0);
|
|
135
|
+
const [borderRightWidth, setBorderRightWidth] = useState(0);
|
|
136
|
+
const [borderBottomWidth, setBorderBottomWidth] = useState(0);
|
|
137
|
+
const [marginTop, setMarginTop] = useState(0);
|
|
138
|
+
const [marginBottom, setMarginBottom] = useState(0);
|
|
139
|
+
const [marginLeft, setMarginLeft] = useState(0);
|
|
140
|
+
const [marginRight, setMarginRight] = useState(0);
|
|
141
|
+
const [paddingTop, setPaddingTop] = useState(0);
|
|
142
|
+
const [paddingLeft, setPaddingLeft] = useState(0);
|
|
143
|
+
const [paddingRight, setPaddingRight] = useState(0);
|
|
144
|
+
const [paddingBottom, setPaddingBottom] = useState(0);
|
|
145
|
+
const [flexGrow, setFlexGrow] = useState(0);
|
|
146
|
+
const [flexShrink, setFlexShrink] = useState(1);
|
|
147
|
+
const [flexBasis, setFlexBasis] = useState("auto");
|
|
148
|
+
|
|
149
|
+
const [currentColor, setCurrentColor] = useState(color);
|
|
150
|
+
|
|
151
|
+
useLayoutEffect(() => {
|
|
152
|
+
let animationFrame;
|
|
153
|
+
const updateStyles = () => {
|
|
154
|
+
const container = containerRef.current;
|
|
155
|
+
const containedElement = container.lastElementChild;
|
|
156
|
+
const target = targetSelector
|
|
157
|
+
? container.querySelector(targetSelector)
|
|
158
|
+
: containedElement;
|
|
159
|
+
if (target) {
|
|
160
|
+
const { width, height } = target.getBoundingClientRect();
|
|
161
|
+
|
|
162
|
+
const containedComputedStyle =
|
|
163
|
+
window.getComputedStyle(containedElement);
|
|
164
|
+
const targetComputedStyle = window.getComputedStyle(target);
|
|
165
|
+
|
|
166
|
+
// Read flex properties from the contained element to mirror its behavior
|
|
167
|
+
const newFlexGrow = containedComputedStyle.flexGrow || "0";
|
|
168
|
+
const newFlexShrink = containedComputedStyle.flexShrink || "1";
|
|
169
|
+
const newFlexBasis = containedComputedStyle.flexBasis || "auto";
|
|
170
|
+
|
|
171
|
+
const newBorderTopWidth = resolveCSSSize(
|
|
172
|
+
targetComputedStyle.borderTopWidth,
|
|
173
|
+
);
|
|
174
|
+
const newBorderLeftWidth = resolveCSSSize(
|
|
175
|
+
targetComputedStyle.borderLeftWidth,
|
|
176
|
+
);
|
|
177
|
+
const newBorderRightWidth = resolveCSSSize(
|
|
178
|
+
targetComputedStyle.borderRightWidth,
|
|
179
|
+
);
|
|
180
|
+
const newBorderBottomWidth = resolveCSSSize(
|
|
181
|
+
targetComputedStyle.borderBottomWidth,
|
|
182
|
+
);
|
|
183
|
+
const newBorderRadius = resolveCSSSize(
|
|
184
|
+
targetComputedStyle.borderRadius,
|
|
185
|
+
{
|
|
186
|
+
availableSize: Math.min(width, height),
|
|
187
|
+
},
|
|
188
|
+
);
|
|
189
|
+
const newOutlineColor = targetComputedStyle.outlineColor;
|
|
190
|
+
const newBorderColor = targetComputedStyle.borderColor;
|
|
191
|
+
const newDetectedColor = targetComputedStyle.color;
|
|
192
|
+
const newOutlineOffset = resolveCSSSize(
|
|
193
|
+
targetComputedStyle.outlineOffset,
|
|
194
|
+
);
|
|
195
|
+
const newMarginTop = resolveCSSSize(targetComputedStyle.marginTop);
|
|
196
|
+
const newMarginBottom = resolveCSSSize(
|
|
197
|
+
targetComputedStyle.marginBottom,
|
|
198
|
+
);
|
|
199
|
+
const newMarginLeft = resolveCSSSize(targetComputedStyle.marginLeft);
|
|
200
|
+
const newMarginRight = resolveCSSSize(targetComputedStyle.marginRight);
|
|
201
|
+
|
|
202
|
+
const paddingTop = resolveCSSSize(containedComputedStyle.paddingTop);
|
|
203
|
+
const paddingLeft = resolveCSSSize(containedComputedStyle.paddingLeft);
|
|
204
|
+
const paddingRight = resolveCSSSize(
|
|
205
|
+
containedComputedStyle.paddingRight,
|
|
206
|
+
);
|
|
207
|
+
const paddingBottom = resolveCSSSize(
|
|
208
|
+
containedComputedStyle.paddingBottom,
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
setBorderTopWidth(newBorderTopWidth);
|
|
212
|
+
setBorderLeftWidth(newBorderLeftWidth);
|
|
213
|
+
setBorderRightWidth(newBorderRightWidth);
|
|
214
|
+
setBorderBottomWidth(newBorderBottomWidth);
|
|
215
|
+
setBorderRadius(newBorderRadius);
|
|
216
|
+
setOutlineOffset(newOutlineOffset);
|
|
217
|
+
setMarginTop(newMarginTop);
|
|
218
|
+
setMarginBottom(newMarginBottom);
|
|
219
|
+
setMarginLeft(newMarginLeft);
|
|
220
|
+
setMarginRight(newMarginRight);
|
|
221
|
+
setPaddingTop(paddingTop);
|
|
222
|
+
setPaddingLeft(paddingLeft);
|
|
223
|
+
setPaddingRight(paddingRight);
|
|
224
|
+
setPaddingBottom(paddingBottom);
|
|
225
|
+
setFlexGrow(newFlexGrow);
|
|
226
|
+
setFlexShrink(newFlexShrink);
|
|
227
|
+
setFlexBasis(newFlexBasis);
|
|
228
|
+
|
|
229
|
+
if (color) {
|
|
230
|
+
setCurrentColor(color);
|
|
231
|
+
} else if (
|
|
232
|
+
newOutlineColor &&
|
|
233
|
+
newOutlineColor !== "rgba(0, 0, 0, 0)" &&
|
|
234
|
+
(document.activeElement === containedElement ||
|
|
235
|
+
newBorderColor === "rgba(0, 0, 0, 0)")
|
|
236
|
+
) {
|
|
237
|
+
setCurrentColor(newOutlineColor);
|
|
238
|
+
} else if (newBorderColor && newBorderColor !== "rgba(0, 0, 0, 0)") {
|
|
239
|
+
setCurrentColor(newBorderColor);
|
|
240
|
+
} else {
|
|
241
|
+
setCurrentColor(newDetectedColor);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// updateStyles is very cheap so we run it every frame
|
|
246
|
+
animationFrame = requestAnimationFrame(updateStyles);
|
|
247
|
+
};
|
|
248
|
+
updateStyles();
|
|
249
|
+
|
|
250
|
+
return () => {
|
|
251
|
+
cancelAnimationFrame(animationFrame);
|
|
252
|
+
};
|
|
253
|
+
}, [color, targetSelector]);
|
|
254
|
+
|
|
255
|
+
spacingTop += inset;
|
|
256
|
+
spacingTop += outlineOffset;
|
|
257
|
+
spacingTop -= borderTopWidth;
|
|
258
|
+
spacingTop += marginTop;
|
|
259
|
+
spacingLeft += inset;
|
|
260
|
+
spacingLeft += outlineOffset;
|
|
261
|
+
spacingLeft -= borderLeftWidth;
|
|
262
|
+
spacingLeft += marginLeft;
|
|
263
|
+
spacingRight += inset;
|
|
264
|
+
spacingRight += outlineOffset;
|
|
265
|
+
spacingRight -= borderRightWidth;
|
|
266
|
+
spacingRight += marginRight;
|
|
267
|
+
spacingBottom += inset;
|
|
268
|
+
spacingBottom += outlineOffset;
|
|
269
|
+
spacingBottom -= borderBottomWidth;
|
|
270
|
+
spacingBottom += marginBottom;
|
|
271
|
+
if (targetSelector) {
|
|
272
|
+
// oversimplification that actually works
|
|
273
|
+
// (simplified because it assumes the targeted element is a direct child of the contained element which may have padding)
|
|
274
|
+
spacingTop += paddingTop;
|
|
275
|
+
spacingLeft += paddingLeft;
|
|
276
|
+
spacingRight += paddingRight;
|
|
277
|
+
spacingBottom += paddingBottom;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const maxBorderWidth = Math.max(
|
|
281
|
+
borderTopWidth,
|
|
282
|
+
borderLeftWidth,
|
|
283
|
+
borderRightWidth,
|
|
284
|
+
borderBottomWidth,
|
|
285
|
+
);
|
|
286
|
+
const size = Math.max(2, maxBorderWidth / 2);
|
|
287
|
+
|
|
288
|
+
spacingTop += size / 4;
|
|
289
|
+
spacingLeft += size / 4;
|
|
290
|
+
spacingRight += size / 4;
|
|
291
|
+
spacingBottom += size / 4;
|
|
292
|
+
|
|
293
|
+
return (
|
|
294
|
+
<div
|
|
295
|
+
name="element_with_loader_wrapper"
|
|
296
|
+
ref={containerRef}
|
|
297
|
+
data-loader-visible={shouldShowSpinner ? "" : undefined}
|
|
298
|
+
style={{
|
|
299
|
+
flexGrow,
|
|
300
|
+
flexShrink,
|
|
301
|
+
flexBasis,
|
|
302
|
+
}}
|
|
303
|
+
>
|
|
304
|
+
{shouldShowSpinner && (
|
|
305
|
+
<div
|
|
306
|
+
name="loading_rectangle_wrapper"
|
|
307
|
+
style={{
|
|
308
|
+
top: `${spacingTop}px`,
|
|
309
|
+
left: `${spacingLeft}px`,
|
|
310
|
+
bottom: `${spacingBottom}px`,
|
|
311
|
+
right: `${spacingRight}px`,
|
|
312
|
+
}}
|
|
313
|
+
>
|
|
314
|
+
<RectangleLoading
|
|
315
|
+
color={currentColor}
|
|
316
|
+
radius={borderRadius}
|
|
317
|
+
size={size}
|
|
318
|
+
/>
|
|
319
|
+
</div>
|
|
320
|
+
)}
|
|
321
|
+
{children}
|
|
322
|
+
</div>
|
|
323
|
+
);
|
|
324
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export const LoadingSpinner = ({ color = "FF156D" }) => {
|
|
2
|
+
return (
|
|
3
|
+
<svg
|
|
4
|
+
viewBox="0 0 200 200"
|
|
5
|
+
width="100%"
|
|
6
|
+
height="100%"
|
|
7
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
8
|
+
>
|
|
9
|
+
<rect
|
|
10
|
+
fill={color}
|
|
11
|
+
stroke={color}
|
|
12
|
+
stroke-width="15"
|
|
13
|
+
width="30"
|
|
14
|
+
height="30"
|
|
15
|
+
x="25"
|
|
16
|
+
y="85"
|
|
17
|
+
>
|
|
18
|
+
<animate
|
|
19
|
+
attributeName="opacity"
|
|
20
|
+
calcMode="spline"
|
|
21
|
+
dur="2"
|
|
22
|
+
values="1;0;1;"
|
|
23
|
+
keySplines=".5 0 .5 1;.5 0 .5 1"
|
|
24
|
+
repeatCount="indefinite"
|
|
25
|
+
begin="-.4"
|
|
26
|
+
></animate>
|
|
27
|
+
</rect>
|
|
28
|
+
<rect
|
|
29
|
+
fill={color}
|
|
30
|
+
stroke={color}
|
|
31
|
+
stroke-width="15"
|
|
32
|
+
width="30"
|
|
33
|
+
height="30"
|
|
34
|
+
x="85"
|
|
35
|
+
y="85"
|
|
36
|
+
>
|
|
37
|
+
<animate
|
|
38
|
+
attributeName="opacity"
|
|
39
|
+
calcMode="spline"
|
|
40
|
+
dur="2"
|
|
41
|
+
values="1;0;1;"
|
|
42
|
+
keySplines=".5 0 .5 1;.5 0 .5 1"
|
|
43
|
+
repeatCount="indefinite"
|
|
44
|
+
begin="-.2"
|
|
45
|
+
></animate>
|
|
46
|
+
</rect>
|
|
47
|
+
<rect
|
|
48
|
+
fill={color}
|
|
49
|
+
stroke={color}
|
|
50
|
+
stroke-width="15"
|
|
51
|
+
width="30"
|
|
52
|
+
height="30"
|
|
53
|
+
x="145"
|
|
54
|
+
y="85"
|
|
55
|
+
>
|
|
56
|
+
<animate
|
|
57
|
+
attributeName="opacity"
|
|
58
|
+
calcMode="spline"
|
|
59
|
+
dur="2"
|
|
60
|
+
values="1;0;1;"
|
|
61
|
+
keySplines=".5 0 .5 1;.5 0 .5 1"
|
|
62
|
+
repeatCount="indefinite"
|
|
63
|
+
begin="0"
|
|
64
|
+
></animate>
|
|
65
|
+
</rect>
|
|
66
|
+
</svg>
|
|
67
|
+
);
|
|
68
|
+
};
|