@colixsystems/widget-sdk 0.245.0 → 0.247.0
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/README.md +60 -2
- package/dist/button-tokens.js +154 -0
- package/dist/button-view.js +251 -93
- package/dist/button.js +5 -3
- package/dist/button.native.js +6 -4
- package/dist/chart-logic.js +575 -0
- package/dist/chart-tokens.js +131 -0
- package/dist/chart-view.js +262 -0
- package/dist/chart.js +21 -0
- package/dist/chart.native.js +10 -0
- package/dist/contract.cjs +132 -35
- package/dist/contract.js +132 -35
- package/dist/detail-parts-view.js +31 -2
- package/dist/file-image-logic.js +162 -0
- package/dist/file-image-source.js +71 -0
- package/dist/file-image-view.js +119 -0
- package/dist/file-image.js +8 -0
- package/dist/file-image.native.js +8 -0
- package/dist/form-control-logic.js +23 -0
- package/dist/form-control-tokens.js +25 -10
- package/dist/grid-logic.js +222 -0
- package/dist/grid-view.js +109 -0
- package/dist/grid.js +7 -0
- package/dist/grid.native.js +7 -0
- package/dist/host.d.ts +6 -0
- package/dist/host.js +2 -0
- package/dist/index.d.ts +481 -20
- package/dist/index.js +14 -1
- package/dist/index.native.js +14 -1
- package/dist/menu-view.js +168 -0
- package/dist/overlay-tokens.js +50 -0
- package/dist/overlay-view.js +67 -24
- package/dist/overlay.native.js +3 -1
- package/dist/people-picker-logic.js +203 -0
- package/dist/people-picker-view.js +247 -0
- package/dist/people-picker.js +11 -0
- package/dist/people-picker.native.js +9 -0
- package/dist/popover-tokens.js +145 -0
- package/dist/popover-view.js +252 -0
- package/dist/popover.js +23 -0
- package/dist/popover.native.js +22 -0
- package/dist/radio-group-logic.js +61 -0
- package/dist/radio-group-tokens.js +62 -0
- package/dist/radio-group-view.js +262 -0
- package/dist/radio-group.js +12 -0
- package/dist/radio-group.native.js +7 -0
- package/dist/select-search-logic.js +150 -0
- package/dist/select-view.js +255 -43
- package/dist/select.js +6 -2
- package/dist/select.native.js +5 -3
- package/dist/tabs-tokens.js +59 -0
- package/dist/tabs-view.js +9 -43
- package/dist/tooltip-view.js +137 -0
- package/dist/web-modal.js +95 -23
- package/package.json +1 -1
- package/src/contract.js +132 -35
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// The ONE implementation behind `<Tooltip>`: a short label for the control it
|
|
2
|
+
// wraps, usually an icon-only button. On the web it shows on hover and on
|
|
3
|
+
// keyboard focus; on a touch screen, which has no hover, a long press shows
|
|
4
|
+
// it instead (a sheet on a device, as `<Menu>` is).
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { useHostTheme } from "./hooks.js";
|
|
8
|
+
import { DEFAULT_THEME_TOKENS } from "./_theme-tokens.js";
|
|
9
|
+
import { isText } from "./form-control-logic.js";
|
|
10
|
+
import { POPOVER_DEFAULTS, resolvePopoverTokens } from "./popover-tokens.js";
|
|
11
|
+
import { elementRef, mergeRefs } from "./popover-view.js";
|
|
12
|
+
|
|
13
|
+
const h = React.createElement;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Bind the shared tooltip to one platform's primitives and anchored surface.
|
|
17
|
+
*
|
|
18
|
+
* @param {{View: any, Text: any}} rn
|
|
19
|
+
* @param {any} AnchoredSurface — from `makePopover()` for the same host.
|
|
20
|
+
*/
|
|
21
|
+
export function makeTooltip(rn, AnchoredSurface) {
|
|
22
|
+
const { View, Text } = rn;
|
|
23
|
+
|
|
24
|
+
// "hover" (pointer or keyboard focus) or "press" (a long press), else null.
|
|
25
|
+
function useTooltipMode() {
|
|
26
|
+
const [mode, setMode] = React.useState(null);
|
|
27
|
+
const timer = React.useRef(null);
|
|
28
|
+
const cancel = () => clearTimeout(timer.current);
|
|
29
|
+
React.useEffect(() => cancel, []);
|
|
30
|
+
const after = (ms, next) => {
|
|
31
|
+
cancel();
|
|
32
|
+
timer.current = setTimeout(() => setMode(next), ms);
|
|
33
|
+
};
|
|
34
|
+
const ifHover = (next) => (own) => (own === "press" ? own : next);
|
|
35
|
+
return {
|
|
36
|
+
mode,
|
|
37
|
+
hoverIn: () => after(POPOVER_DEFAULTS.tooltipShowDelayMs, ifHover("hover")),
|
|
38
|
+
hoverOut: () => after(POPOVER_DEFAULTS.tooltipHideDelayMs, ifHover(null)),
|
|
39
|
+
stay: cancel,
|
|
40
|
+
focus: () => {
|
|
41
|
+
cancel();
|
|
42
|
+
setMode(ifHover("hover"));
|
|
43
|
+
},
|
|
44
|
+
blur: () => {
|
|
45
|
+
cancel();
|
|
46
|
+
setMode(ifHover(null));
|
|
47
|
+
},
|
|
48
|
+
press: () => {
|
|
49
|
+
cancel();
|
|
50
|
+
setMode("press");
|
|
51
|
+
},
|
|
52
|
+
close: () => {
|
|
53
|
+
cancel();
|
|
54
|
+
setMode(null);
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A short label for the control it wraps. Wrap one pressable element (a
|
|
61
|
+
* `Pressable`, an icon button) that already carries its own
|
|
62
|
+
* `accessibilityLabel`: the tooltip shows that name to sighted users.
|
|
63
|
+
* A child with its own `onLongPress` keeps it, and gets no long-press
|
|
64
|
+
* tooltip.
|
|
65
|
+
*
|
|
66
|
+
* @param {object} props
|
|
67
|
+
* @param {string} props.label — one short phrase.
|
|
68
|
+
* @param {"top"|"bottom"} [props.placement] — default "top"; flips.
|
|
69
|
+
* @param {string} [props.testID] — on the label.
|
|
70
|
+
* @param {any} props.children — the one element it labels.
|
|
71
|
+
*/
|
|
72
|
+
const Tooltip = React.forwardRef(function Tooltip(
|
|
73
|
+
{ label, placement = "top", testID, children, ...passThrough },
|
|
74
|
+
forwardedRef,
|
|
75
|
+
) {
|
|
76
|
+
const theme = useHostTheme() || DEFAULT_THEME_TOKENS;
|
|
77
|
+
const anchorRef = React.useRef(null);
|
|
78
|
+
const labelId = React.useId();
|
|
79
|
+
const tip = useTooltipMode();
|
|
80
|
+
if (!React.isValidElement(children)) return children ?? null;
|
|
81
|
+
if (!isText(label) || label === "") return children;
|
|
82
|
+
|
|
83
|
+
const popover = resolvePopoverTokens(theme);
|
|
84
|
+
const spacing = theme.spacing || DEFAULT_THEME_TOKENS.spacing;
|
|
85
|
+
const typography = theme.typography || DEFAULT_THEME_TOKENS.typography;
|
|
86
|
+
const ownLongPress = typeof children.props.onLongPress === "function";
|
|
87
|
+
const child = React.cloneElement(children, {
|
|
88
|
+
...passThrough,
|
|
89
|
+
ref: mergeRefs(elementRef(children), anchorRef, forwardedRef),
|
|
90
|
+
...(ownLongPress ? null : { onLongPress: tip.press }),
|
|
91
|
+
...(tip.mode ? { "aria-describedby": labelId } : null),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return h(
|
|
95
|
+
View,
|
|
96
|
+
{ onMouseEnter: tip.hoverIn, onMouseLeave: tip.hoverOut, onFocus: tip.focus, onBlur: tip.blur },
|
|
97
|
+
child,
|
|
98
|
+
h(
|
|
99
|
+
AnchoredSurface,
|
|
100
|
+
{
|
|
101
|
+
visible: tip.mode !== null,
|
|
102
|
+
onRequestClose: tip.close,
|
|
103
|
+
anchorRef,
|
|
104
|
+
placement,
|
|
105
|
+
passive: tip.mode === "hover",
|
|
106
|
+
maxWidth: POPOVER_DEFAULTS.tooltipMaxWidth,
|
|
107
|
+
accessibilityLabel: String(label),
|
|
108
|
+
},
|
|
109
|
+
h(
|
|
110
|
+
View,
|
|
111
|
+
{
|
|
112
|
+
nativeID: labelId,
|
|
113
|
+
role: "tooltip",
|
|
114
|
+
onMouseEnter: tip.stay,
|
|
115
|
+
onMouseLeave: tip.hoverOut,
|
|
116
|
+
testID,
|
|
117
|
+
style: { paddingHorizontal: spacing.sm, paddingVertical: spacing.xs },
|
|
118
|
+
},
|
|
119
|
+
h(
|
|
120
|
+
Text,
|
|
121
|
+
{
|
|
122
|
+
style: {
|
|
123
|
+
fontFamily: typography.fontFamily,
|
|
124
|
+
fontSize: typography.sizes.sm,
|
|
125
|
+
color: popover.textColor,
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
String(label),
|
|
129
|
+
),
|
|
130
|
+
),
|
|
131
|
+
),
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
Tooltip.displayName = "Tooltip";
|
|
136
|
+
return Tooltip;
|
|
137
|
+
}
|
package/dist/web-modal.js
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
// re-render rebuilt it, and its Escape and focus handling never re-armed.
|
|
8
8
|
//
|
|
9
9
|
// Implements only the `Modal` props `<Overlay>` passes: `visible`,
|
|
10
|
-
// `onRequestClose`, the dialog's role and name, `initialFocusRef`
|
|
11
|
-
// `children`; it is never animated.
|
|
10
|
+
// `onRequestClose`, the dialog's role and name, `initialFocusRef`, `passive`,
|
|
11
|
+
// `closesOnOutsideScroll` and `children`; it is never animated.
|
|
12
12
|
|
|
13
13
|
import React from "react";
|
|
14
14
|
import { createPortal } from "react-dom";
|
|
@@ -28,30 +28,111 @@ const layerStyle = {
|
|
|
28
28
|
outline: "none",
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
-
// Open layers, innermost last
|
|
31
|
+
// Open layers, innermost last. The top one handles Escape; the top modal
|
|
32
|
+
// one traps Tab and focus, as a tooltip above it takes neither.
|
|
32
33
|
const openLayers = [];
|
|
33
34
|
|
|
35
|
+
function openLayer(passive) {
|
|
36
|
+
const layer = { passive };
|
|
37
|
+
openLayers.push(layer);
|
|
38
|
+
return {
|
|
39
|
+
isTop: () => openLayers[openLayers.length - 1] === layer,
|
|
40
|
+
isTopModal: () => openLayers.filter((open) => !open.passive).pop() === layer,
|
|
41
|
+
close: () => {
|
|
42
|
+
const index = openLayers.indexOf(layer);
|
|
43
|
+
if (index !== -1) openLayers.splice(index, 1);
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// An anchored panel would drift from its trigger, so a scroll beneath closes it.
|
|
49
|
+
function useCloseOnOutsideScroll(container, enabled, onCloseRef) {
|
|
50
|
+
React.useEffect(() => {
|
|
51
|
+
if (!container || !enabled) return undefined;
|
|
52
|
+
const onScroll = (event) => {
|
|
53
|
+
const target = event.target;
|
|
54
|
+
if (target instanceof Node && container.contains(target)) return;
|
|
55
|
+
if (typeof onCloseRef.current === "function") onCloseRef.current();
|
|
56
|
+
};
|
|
57
|
+
document.addEventListener("scroll", onScroll, { capture: true, passive: true });
|
|
58
|
+
return () => document.removeEventListener("scroll", onScroll, { capture: true });
|
|
59
|
+
}, [container, enabled, onCloseRef]);
|
|
60
|
+
}
|
|
61
|
+
|
|
34
62
|
function tabStops(dialog) {
|
|
35
63
|
return Array.from(dialog.querySelectorAll(FOCUSABLE)).filter(
|
|
36
64
|
(node) => node.getAttribute("aria-hidden") !== "true",
|
|
37
65
|
);
|
|
38
66
|
}
|
|
39
67
|
|
|
68
|
+
const passiveLayerStyle = { ...layerStyle, pointerEvents: "none" };
|
|
69
|
+
|
|
70
|
+
// Owns a body-level container while mounted, as the modal layer does.
|
|
71
|
+
function useBodyContainer(visible) {
|
|
72
|
+
const [container, setContainer] = React.useState(null);
|
|
73
|
+
React.useLayoutEffect(() => {
|
|
74
|
+
if (!visible) return undefined;
|
|
75
|
+
const node = document.createElement("div");
|
|
76
|
+
document.body.appendChild(node);
|
|
77
|
+
setContainer(node);
|
|
78
|
+
return () => {
|
|
79
|
+
node.remove();
|
|
80
|
+
setContainer(null);
|
|
81
|
+
};
|
|
82
|
+
}, [visible]);
|
|
83
|
+
return container;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// A tooltip's layer: no focus trap, no modal semantics, the pointer passes
|
|
87
|
+
// through. Escape dismisses it while it is the top layer, ahead of any
|
|
88
|
+
// dialog beneath; a dialog opened above it takes Escape first.
|
|
89
|
+
function PassiveLayer({ visible = true, onRequestClose, closesOnOutsideScroll, children }) {
|
|
90
|
+
const container = useBodyContainer(visible);
|
|
91
|
+
const onCloseRef = React.useRef(onRequestClose);
|
|
92
|
+
onCloseRef.current = onRequestClose;
|
|
93
|
+
useCloseOnOutsideScroll(container, closesOnOutsideScroll, onCloseRef);
|
|
94
|
+
|
|
95
|
+
React.useEffect(() => {
|
|
96
|
+
if (!container) return undefined;
|
|
97
|
+
const layer = openLayer(true);
|
|
98
|
+
const onKeyUp = (event) => {
|
|
99
|
+
if (event.key !== "Escape" || !layer.isTop()) return;
|
|
100
|
+
event.stopPropagation();
|
|
101
|
+
if (typeof onCloseRef.current === "function") onCloseRef.current();
|
|
102
|
+
};
|
|
103
|
+
document.addEventListener("keyup", onKeyUp);
|
|
104
|
+
return () => {
|
|
105
|
+
document.removeEventListener("keyup", onKeyUp);
|
|
106
|
+
layer.close();
|
|
107
|
+
};
|
|
108
|
+
}, [container]);
|
|
109
|
+
|
|
110
|
+
if (!visible || !container) return null;
|
|
111
|
+
return createPortal(
|
|
112
|
+
React.createElement("div", { style: passiveLayerStyle }, children),
|
|
113
|
+
container,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
40
117
|
/**
|
|
41
118
|
* @param {{visible?: boolean, onRequestClose?: () => void, role?: string,
|
|
42
119
|
* "aria-label"?: string, "aria-labelledby"?: string,
|
|
43
|
-
* initialFocusRef?: {current: any}, children?: any}} props
|
|
120
|
+
* initialFocusRef?: {current: any}, passive?: boolean, children?: any}} props
|
|
44
121
|
*/
|
|
45
|
-
export function WebModal({
|
|
122
|
+
export function WebModal(props) {
|
|
123
|
+
return React.createElement(props.passive ? PassiveLayer : ModalLayer, props);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function ModalLayer({
|
|
46
127
|
visible = true,
|
|
47
128
|
onRequestClose,
|
|
48
129
|
role = "dialog",
|
|
49
130
|
"aria-label": ariaLabel,
|
|
50
131
|
"aria-labelledby": ariaLabelledBy,
|
|
51
132
|
initialFocusRef,
|
|
133
|
+
closesOnOutsideScroll,
|
|
52
134
|
children,
|
|
53
135
|
}) {
|
|
54
|
-
const [container, setContainer] = React.useState(null);
|
|
55
136
|
const dialogRef = React.useRef(null);
|
|
56
137
|
const onCloseRef = React.useRef(onRequestClose);
|
|
57
138
|
onCloseRef.current = onRequestClose;
|
|
@@ -62,23 +143,15 @@ export function WebModal({
|
|
|
62
143
|
const returnFocusRef = React.useRef(null);
|
|
63
144
|
|
|
64
145
|
React.useLayoutEffect(() => {
|
|
65
|
-
if (
|
|
66
|
-
returnFocusRef.current = document.activeElement;
|
|
67
|
-
const node = document.createElement("div");
|
|
68
|
-
document.body.appendChild(node);
|
|
69
|
-
setContainer(node);
|
|
70
|
-
return () => {
|
|
71
|
-
node.remove();
|
|
72
|
-
setContainer(null);
|
|
73
|
-
};
|
|
146
|
+
if (visible) returnFocusRef.current = document.activeElement;
|
|
74
147
|
}, [visible]);
|
|
148
|
+
const container = useBodyContainer(visible);
|
|
149
|
+
useCloseOnOutsideScroll(container, closesOnOutsideScroll, onCloseRef);
|
|
75
150
|
|
|
76
151
|
React.useEffect(() => {
|
|
77
152
|
const dialog = dialogRef.current;
|
|
78
153
|
if (!container || !dialog) return undefined;
|
|
79
|
-
const layer =
|
|
80
|
-
openLayers.push(layer);
|
|
81
|
-
const isTop = () => openLayers[openLayers.length - 1] === layer;
|
|
154
|
+
const layer = openLayer(false);
|
|
82
155
|
const returnFocusTo = returnFocusRef.current;
|
|
83
156
|
const wanted = initialFocusRefRef.current;
|
|
84
157
|
const first = wanted && wanted.current;
|
|
@@ -91,12 +164,12 @@ export function WebModal({
|
|
|
91
164
|
// keyup, as react-native-web's Modal did, so the keydown that opened a
|
|
92
165
|
// nested overlay cannot also close it.
|
|
93
166
|
const onKeyUp = (event) => {
|
|
94
|
-
if (event.key !== "Escape" || !isTop()) return;
|
|
167
|
+
if (event.key !== "Escape" || !layer.isTop()) return;
|
|
95
168
|
event.stopPropagation();
|
|
96
169
|
if (typeof onCloseRef.current === "function") onCloseRef.current();
|
|
97
170
|
};
|
|
98
171
|
const onKeyDown = (event) => {
|
|
99
|
-
if (event.key !== "Tab" || !
|
|
172
|
+
if (event.key !== "Tab" || !layer.isTopModal()) return;
|
|
100
173
|
const stops = tabStops(dialog);
|
|
101
174
|
if (!stops.length) {
|
|
102
175
|
event.preventDefault();
|
|
@@ -115,7 +188,7 @@ export function WebModal({
|
|
|
115
188
|
}
|
|
116
189
|
};
|
|
117
190
|
const onFocusIn = (event) => {
|
|
118
|
-
if (
|
|
191
|
+
if (layer.isTopModal() && !dialog.contains(event.target)) {
|
|
119
192
|
dialog.focus({ preventScroll: true });
|
|
120
193
|
}
|
|
121
194
|
};
|
|
@@ -126,8 +199,7 @@ export function WebModal({
|
|
|
126
199
|
document.removeEventListener("keyup", onKeyUp);
|
|
127
200
|
document.removeEventListener("keydown", onKeyDown);
|
|
128
201
|
document.removeEventListener("focusin", onFocusIn);
|
|
129
|
-
|
|
130
|
-
if (index !== -1) openLayers.splice(index, 1);
|
|
202
|
+
layer.close();
|
|
131
203
|
if (
|
|
132
204
|
returnFocusTo &&
|
|
133
205
|
typeof returnFocusTo.focus === "function" &&
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.247.0",
|
|
4
4
|
"description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
|
|
5
5
|
"homepage": "https://github.com/Colix-AB/AppStudio",
|
|
6
6
|
"type": "module",
|