@reykjavik/hanna-react 0.10.168 → 0.10.170
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/Alert.d.ts +2 -2
- package/Alert.js +29 -12
- package/CHANGELOG.md +17 -0
- package/Multiselect.js +1 -0
- package/_abstract/_AbstractModal.js +7 -11
- package/esm/Alert.d.ts +2 -2
- package/esm/Alert.js +29 -12
- package/esm/Multiselect.js +1 -0
- package/esm/_abstract/_AbstractModal.js +7 -11
- package/package.json +1 -1
package/Alert.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ type AlertType = 'info' | 'success' | 'warning' | 'error' | 'critical';
|
|
|
11
11
|
export declare const alertTypes: Record<string, 1>;
|
|
12
12
|
export type AlertProps = {
|
|
13
13
|
type: AlertType;
|
|
14
|
-
/** Defaults to `true` if an `onClose`
|
|
14
|
+
/** Defaults to `true` if an `onClose` or `closeUrl` handlers are passaed */
|
|
15
15
|
closable?: boolean;
|
|
16
16
|
/** The alert content */
|
|
17
17
|
children?: ReactNode;
|
|
@@ -25,7 +25,7 @@ export type AlertProps = {
|
|
|
25
25
|
instantShow?: boolean;
|
|
26
26
|
} & SSRSupportProps & EitherObj<{
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
28
|
+
* Number of **seconds** until the Alert auto-closes.
|
|
29
29
|
* Mosueover and keyboard focus resets the timer. \
|
|
30
30
|
* NOTE: An `onClosed` handler is required when using this option.
|
|
31
31
|
*/
|
package/Alert.js
CHANGED
|
@@ -10,25 +10,42 @@ const env_js_1 = require("./utils/env.js");
|
|
|
10
10
|
const utils_js_1 = require("./utils.js");
|
|
11
11
|
// FIXME: Eventually import from @reykjavik/hanna-css
|
|
12
12
|
const AlertCloseTransitionDuration = 400;
|
|
13
|
+
/** How much to extend the remaining time when the auto-closing is interrupted (frozen) by mouseover or keyboard focus */
|
|
14
|
+
const FREEZE_EXTENSION_MS = 750;
|
|
13
15
|
const useAutoClosing = (autoClose, props) => {
|
|
14
|
-
const
|
|
16
|
+
const startTime = (0, react_1.useRef)(Date.now());
|
|
17
|
+
const remainingMs = (0, react_1.useRef)(autoClose);
|
|
18
|
+
const [frost, setFrost] = (0, react_1.useState)(0);
|
|
15
19
|
if (!autoClose) {
|
|
16
|
-
return {
|
|
20
|
+
return { autoCloseIn: undefined };
|
|
17
21
|
}
|
|
18
22
|
const thaw = (e) => {
|
|
19
|
-
|
|
23
|
+
// When it's about to thaw, make set a new start time to base future
|
|
24
|
+
// "remaining time" calculations on.
|
|
25
|
+
if (frost === 1) {
|
|
26
|
+
startTime.current = Date.now();
|
|
27
|
+
}
|
|
28
|
+
setFrost((frost) => frost - 1);
|
|
20
29
|
const handler = props[e.type.startsWith('blur') ? 'onBlur' : 'onMouseLeave'];
|
|
21
|
-
// @ts-expect-error (Proper fix ends up as too much code for this extreme edge case)
|
|
22
30
|
handler && handler(e);
|
|
23
31
|
};
|
|
24
32
|
const freeze = (e) => {
|
|
25
|
-
|
|
33
|
+
// When there's no frost and it's about to freeze, make note
|
|
34
|
+
// of the remaining time. Furhter freezes should not update this.
|
|
35
|
+
if (!frost) {
|
|
36
|
+
console.log(remainingMs.current);
|
|
37
|
+
// Calculate the currently remaining time based on the last start time.
|
|
38
|
+
const newRemainingMs = remainingMs.current - (Date.now() - startTime.current);
|
|
39
|
+
// Extend the remainint time a bit to give usees some leeway,
|
|
40
|
+
// but never extend it beyond the original autoClose value.
|
|
41
|
+
remainingMs.current = Math.min(newRemainingMs + FREEZE_EXTENSION_MS, autoClose);
|
|
42
|
+
}
|
|
43
|
+
setFrost((frost) => frost + 1);
|
|
26
44
|
const handler = props[e.type.startsWith('focus') ? 'onFocus' : 'onMouseEnter'];
|
|
27
|
-
// @ts-expect-error (Resolving ends up as too much code for this extreme edge case)
|
|
28
45
|
handler && handler(e);
|
|
29
46
|
};
|
|
30
47
|
return {
|
|
31
|
-
|
|
48
|
+
autoCloseIn: frost === 0 ? remainingMs.current : undefined,
|
|
32
49
|
autoClosingProps: Object.assign({ onMouseEnter: freeze, onMouseLeave: thaw, onFocus: freeze, onBlur: thaw }, (env_js_1.isPreact && {
|
|
33
50
|
onfocusin: (e) => e.currentTarget !== e.target && freeze(e),
|
|
34
51
|
onfocusout: (e) => e.currentTarget !== e.target && thaw(e),
|
|
@@ -51,7 +68,7 @@ exports.alertTypes = {
|
|
|
51
68
|
const Alert = (props) => {
|
|
52
69
|
var _a;
|
|
53
70
|
const { type, childrenHTML, children, onClose, closeUrl, ssr, onClosed, instantShow, wrapperProps, } = props;
|
|
54
|
-
const autoClose = Math.max(props.autoClose || 0, 0);
|
|
71
|
+
const autoClose = Math.max(props.autoClose || 0, 0) * 1000;
|
|
55
72
|
const closable = (_a = props.closable) !== null && _a !== void 0 ? _a : !!(onClose || (onClosed && !autoClose) || closeUrl != null);
|
|
56
73
|
const closing = (0, react_1.useRef)();
|
|
57
74
|
const isBrowser = (0, utils_js_1.useIsBrowserSide)(ssr);
|
|
@@ -76,13 +93,13 @@ const Alert = (props) => {
|
|
|
76
93
|
}, AlertCloseTransitionDuration);
|
|
77
94
|
}
|
|
78
95
|
}, [onClose, onClosed]);
|
|
79
|
-
const {
|
|
96
|
+
const { autoCloseIn, autoClosingProps } = useAutoClosing(autoClose, props);
|
|
80
97
|
(0, react_1.useEffect)(() => {
|
|
81
|
-
if (
|
|
98
|
+
if (autoCloseIn) {
|
|
82
99
|
let autoCloseTimeout;
|
|
83
100
|
autoCloseTimeout = setTimeout(() => {
|
|
84
101
|
closeAlert();
|
|
85
|
-
},
|
|
102
|
+
}, autoCloseIn);
|
|
86
103
|
return () => {
|
|
87
104
|
if (autoCloseTimeout) {
|
|
88
105
|
clearTimeout(autoCloseTimeout);
|
|
@@ -94,7 +111,7 @@ const Alert = (props) => {
|
|
|
94
111
|
}
|
|
95
112
|
};
|
|
96
113
|
}
|
|
97
|
-
}, [closeAlert,
|
|
114
|
+
}, [closeAlert, autoCloseIn]);
|
|
98
115
|
return (react_1.default.createElement("div", Object.assign({}, wrapperProps, { className: (0, hanna_utils_1.modifiedClass)('Alert', [!!exports.alertTypes[type] && type, closable && 'closable'], (wrapperProps || {}).className), role: "alert", hidden: !open || undefined }, autoClosingProps),
|
|
99
116
|
childrenHTML ? (react_1.default.createElement("div", { dangerouslySetInnerHTML: { __html: childrenHTML } })) : (children),
|
|
100
117
|
' ',
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@
|
|
|
4
4
|
|
|
5
5
|
- ... <!-- Add new lines here. -->
|
|
6
6
|
|
|
7
|
+
## 0.10.170
|
|
8
|
+
|
|
9
|
+
_2026-03-10_
|
|
10
|
+
|
|
11
|
+
- `Alert`:
|
|
12
|
+
- fix: On freeze only extend auto-closing alerts' remaining time by a bit
|
|
13
|
+
|
|
14
|
+
## 0.10.169
|
|
15
|
+
|
|
16
|
+
_2026-02-24_
|
|
17
|
+
|
|
18
|
+
- `Modal`:
|
|
19
|
+
- fix: Togling `open` prop works unreliably in modern React versions
|
|
20
|
+
- `Multiselect`:
|
|
21
|
+
- fix: Stop `'Escape'` key events bubbling and causing side-effects (e.g.
|
|
22
|
+
closing a parent `Modal`)
|
|
23
|
+
|
|
7
24
|
## 0.10.168
|
|
8
25
|
|
|
9
26
|
_2026-02-23_
|
package/Multiselect.js
CHANGED
|
@@ -82,17 +82,6 @@ const AbstractModal = (props) => {
|
|
|
82
82
|
}, closeDelay);
|
|
83
83
|
}
|
|
84
84
|
};
|
|
85
|
-
// ---
|
|
86
|
-
// Update open state when props.open changes. Icky but simple.
|
|
87
|
-
const lastPropsOpen = (0, react_1.useRef)(openProp);
|
|
88
|
-
if (openProp !== lastPropsOpen.current && openProp !== open) {
|
|
89
|
-
lastPropsOpen.current = openProp;
|
|
90
|
-
// these update state during render, which aborts the current render
|
|
91
|
-
// and triggers an immediate rerender.
|
|
92
|
-
openProp ? openModal() : closeModal();
|
|
93
|
-
}
|
|
94
|
-
lastPropsOpen.current = openProp;
|
|
95
|
-
// ---
|
|
96
85
|
const closeOnCurtainClick = isFickle &&
|
|
97
86
|
((e) => {
|
|
98
87
|
if (e.target === e.currentTarget) {
|
|
@@ -118,6 +107,13 @@ const AbstractModal = (props) => {
|
|
|
118
107
|
return () => removeFromModalStack(privateDomId);
|
|
119
108
|
}, [] // eslint-disable-line react-hooks/exhaustive-deps
|
|
120
109
|
);
|
|
110
|
+
(0, react_1.useEffect)(() => {
|
|
111
|
+
if (openProp === open) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
openProp ? openModal() : closeModal();
|
|
115
|
+
}, [openProp] // eslint-disable-line react-hooks/exhaustive-deps
|
|
116
|
+
);
|
|
121
117
|
const PortalOrFragment = props.portal !== false ? _Portal_js_1.Portal : react_1.Fragment;
|
|
122
118
|
const closeButtonLabel = txt.closeButtonLabel || txt.closeButton;
|
|
123
119
|
const { onClick, className } = wrapperProps;
|
package/esm/Alert.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ type AlertType = 'info' | 'success' | 'warning' | 'error' | 'critical';
|
|
|
11
11
|
export declare const alertTypes: Record<string, 1>;
|
|
12
12
|
export type AlertProps = {
|
|
13
13
|
type: AlertType;
|
|
14
|
-
/** Defaults to `true` if an `onClose`
|
|
14
|
+
/** Defaults to `true` if an `onClose` or `closeUrl` handlers are passaed */
|
|
15
15
|
closable?: boolean;
|
|
16
16
|
/** The alert content */
|
|
17
17
|
children?: ReactNode;
|
|
@@ -25,7 +25,7 @@ export type AlertProps = {
|
|
|
25
25
|
instantShow?: boolean;
|
|
26
26
|
} & SSRSupportProps & EitherObj<{
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
28
|
+
* Number of **seconds** until the Alert auto-closes.
|
|
29
29
|
* Mosueover and keyboard focus resets the timer. \
|
|
30
30
|
* NOTE: An `onClosed` handler is required when using this option.
|
|
31
31
|
*/
|
package/esm/Alert.js
CHANGED
|
@@ -6,25 +6,42 @@ import { isPreact } from './utils/env.js';
|
|
|
6
6
|
import { useIsBrowserSide, } from './utils.js';
|
|
7
7
|
// FIXME: Eventually import from @reykjavik/hanna-css
|
|
8
8
|
const AlertCloseTransitionDuration = 400;
|
|
9
|
+
/** How much to extend the remaining time when the auto-closing is interrupted (frozen) by mouseover or keyboard focus */
|
|
10
|
+
const FREEZE_EXTENSION_MS = 750;
|
|
9
11
|
const useAutoClosing = (autoClose, props) => {
|
|
10
|
-
const
|
|
12
|
+
const startTime = useRef(Date.now());
|
|
13
|
+
const remainingMs = useRef(autoClose);
|
|
14
|
+
const [frost, setFrost] = useState(0);
|
|
11
15
|
if (!autoClose) {
|
|
12
|
-
return {
|
|
16
|
+
return { autoCloseIn: undefined };
|
|
13
17
|
}
|
|
14
18
|
const thaw = (e) => {
|
|
15
|
-
|
|
19
|
+
// When it's about to thaw, make set a new start time to base future
|
|
20
|
+
// "remaining time" calculations on.
|
|
21
|
+
if (frost === 1) {
|
|
22
|
+
startTime.current = Date.now();
|
|
23
|
+
}
|
|
24
|
+
setFrost((frost) => frost - 1);
|
|
16
25
|
const handler = props[e.type.startsWith('blur') ? 'onBlur' : 'onMouseLeave'];
|
|
17
|
-
// @ts-expect-error (Proper fix ends up as too much code for this extreme edge case)
|
|
18
26
|
handler && handler(e);
|
|
19
27
|
};
|
|
20
28
|
const freeze = (e) => {
|
|
21
|
-
|
|
29
|
+
// When there's no frost and it's about to freeze, make note
|
|
30
|
+
// of the remaining time. Furhter freezes should not update this.
|
|
31
|
+
if (!frost) {
|
|
32
|
+
console.log(remainingMs.current);
|
|
33
|
+
// Calculate the currently remaining time based on the last start time.
|
|
34
|
+
const newRemainingMs = remainingMs.current - (Date.now() - startTime.current);
|
|
35
|
+
// Extend the remainint time a bit to give usees some leeway,
|
|
36
|
+
// but never extend it beyond the original autoClose value.
|
|
37
|
+
remainingMs.current = Math.min(newRemainingMs + FREEZE_EXTENSION_MS, autoClose);
|
|
38
|
+
}
|
|
39
|
+
setFrost((frost) => frost + 1);
|
|
22
40
|
const handler = props[e.type.startsWith('focus') ? 'onFocus' : 'onMouseEnter'];
|
|
23
|
-
// @ts-expect-error (Resolving ends up as too much code for this extreme edge case)
|
|
24
41
|
handler && handler(e);
|
|
25
42
|
};
|
|
26
43
|
return {
|
|
27
|
-
|
|
44
|
+
autoCloseIn: frost === 0 ? remainingMs.current : undefined,
|
|
28
45
|
autoClosingProps: Object.assign({ onMouseEnter: freeze, onMouseLeave: thaw, onFocus: freeze, onBlur: thaw }, (isPreact && {
|
|
29
46
|
onfocusin: (e) => e.currentTarget !== e.target && freeze(e),
|
|
30
47
|
onfocusout: (e) => e.currentTarget !== e.target && thaw(e),
|
|
@@ -47,7 +64,7 @@ export const alertTypes = {
|
|
|
47
64
|
export const Alert = (props) => {
|
|
48
65
|
var _a;
|
|
49
66
|
const { type, childrenHTML, children, onClose, closeUrl, ssr, onClosed, instantShow, wrapperProps, } = props;
|
|
50
|
-
const autoClose = Math.max(props.autoClose || 0, 0);
|
|
67
|
+
const autoClose = Math.max(props.autoClose || 0, 0) * 1000;
|
|
51
68
|
const closable = (_a = props.closable) !== null && _a !== void 0 ? _a : !!(onClose || (onClosed && !autoClose) || closeUrl != null);
|
|
52
69
|
const closing = useRef();
|
|
53
70
|
const isBrowser = useIsBrowserSide(ssr);
|
|
@@ -72,13 +89,13 @@ export const Alert = (props) => {
|
|
|
72
89
|
}, AlertCloseTransitionDuration);
|
|
73
90
|
}
|
|
74
91
|
}, [onClose, onClosed]);
|
|
75
|
-
const {
|
|
92
|
+
const { autoCloseIn, autoClosingProps } = useAutoClosing(autoClose, props);
|
|
76
93
|
useEffect(() => {
|
|
77
|
-
if (
|
|
94
|
+
if (autoCloseIn) {
|
|
78
95
|
let autoCloseTimeout;
|
|
79
96
|
autoCloseTimeout = setTimeout(() => {
|
|
80
97
|
closeAlert();
|
|
81
|
-
},
|
|
98
|
+
}, autoCloseIn);
|
|
82
99
|
return () => {
|
|
83
100
|
if (autoCloseTimeout) {
|
|
84
101
|
clearTimeout(autoCloseTimeout);
|
|
@@ -90,7 +107,7 @@ export const Alert = (props) => {
|
|
|
90
107
|
}
|
|
91
108
|
};
|
|
92
109
|
}
|
|
93
|
-
}, [closeAlert,
|
|
110
|
+
}, [closeAlert, autoCloseIn]);
|
|
94
111
|
return (React.createElement("div", Object.assign({}, wrapperProps, { className: modifiedClass('Alert', [!!alertTypes[type] && type, closable && 'closable'], (wrapperProps || {}).className), role: "alert", hidden: !open || undefined }, autoClosingProps),
|
|
95
112
|
childrenHTML ? (React.createElement("div", { dangerouslySetInnerHTML: { __html: childrenHTML } })) : (children),
|
|
96
113
|
' ',
|
package/esm/Multiselect.js
CHANGED
|
@@ -78,17 +78,6 @@ export const AbstractModal = (props) => {
|
|
|
78
78
|
}, closeDelay);
|
|
79
79
|
}
|
|
80
80
|
};
|
|
81
|
-
// ---
|
|
82
|
-
// Update open state when props.open changes. Icky but simple.
|
|
83
|
-
const lastPropsOpen = useRef(openProp);
|
|
84
|
-
if (openProp !== lastPropsOpen.current && openProp !== open) {
|
|
85
|
-
lastPropsOpen.current = openProp;
|
|
86
|
-
// these update state during render, which aborts the current render
|
|
87
|
-
// and triggers an immediate rerender.
|
|
88
|
-
openProp ? openModal() : closeModal();
|
|
89
|
-
}
|
|
90
|
-
lastPropsOpen.current = openProp;
|
|
91
|
-
// ---
|
|
92
81
|
const closeOnCurtainClick = isFickle &&
|
|
93
82
|
((e) => {
|
|
94
83
|
if (e.target === e.currentTarget) {
|
|
@@ -114,6 +103,13 @@ export const AbstractModal = (props) => {
|
|
|
114
103
|
return () => removeFromModalStack(privateDomId);
|
|
115
104
|
}, [] // eslint-disable-line react-hooks/exhaustive-deps
|
|
116
105
|
);
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
if (openProp === open) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
openProp ? openModal() : closeModal();
|
|
111
|
+
}, [openProp] // eslint-disable-line react-hooks/exhaustive-deps
|
|
112
|
+
);
|
|
117
113
|
const PortalOrFragment = props.portal !== false ? Portal : Fragment;
|
|
118
114
|
const closeButtonLabel = txt.closeButtonLabel || txt.closeButton;
|
|
119
115
|
const { onClick, className } = wrapperProps;
|