@reykjavik/hanna-react 0.10.138 → 0.10.139
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/CHANGELOG.md +17 -0
- package/Datepicker.js +1 -1
- package/DropdownButton.d.ts +23 -0
- package/DropdownButton.js +77 -0
- package/FocusTrap.d.ts +4 -1
- package/FocusTrap.js +1 -1
- package/FormField.d.ts +3 -1
- package/MainMenu2.d.ts +1 -1
- package/MainMenu2.js +1 -1
- package/Multiselect.js +2 -3
- package/SearchResults.js +11 -4
- package/_abstract/_AbstractCarousel.js +1 -2
- package/_abstract/_Button.d.ts +1 -0
- package/_abstract/_Button.js +5 -2
- package/esm/Datepicker.js +1 -1
- package/esm/DropdownButton.d.ts +23 -0
- package/esm/DropdownButton.js +72 -0
- package/esm/FocusTrap.d.ts +4 -1
- package/esm/FocusTrap.js +1 -1
- package/esm/FormField.d.ts +3 -1
- package/esm/MainMenu2.d.ts +1 -1
- package/esm/MainMenu2.js +1 -1
- package/esm/Multiselect.js +2 -3
- package/esm/SearchResults.js +11 -4
- package/esm/_abstract/_AbstractCarousel.js +1 -2
- package/esm/_abstract/_Button.d.ts +1 -0
- package/esm/_abstract/_Button.js +5 -2
- package/esm/index.d.ts +1 -0
- package/esm/utils/useCallbackOnEsc.d.ts +2 -2
- package/esm/utils/useCallbackOnEsc.js +1 -1
- package/esm/utils/useDomid.d.ts +4 -0
- package/esm/utils/useDomid.js +6 -1
- package/esm/utils/useOnClickOutside.d.ts +5 -4
- package/esm/utils/useOnClickOutside.js +17 -17
- package/index.d.ts +1 -0
- package/package.json +7 -3
- package/utils/useCallbackOnEsc.d.ts +2 -2
- package/utils/useCallbackOnEsc.js +1 -1
- package/utils/useDomid.d.ts +4 -0
- package/utils/useDomid.js +9 -3
- package/utils/useOnClickOutside.d.ts +5 -4
- package/utils/useOnClickOutside.js +16 -16
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@
|
|
|
4
4
|
|
|
5
5
|
- ... <!-- Add new lines here. -->
|
|
6
6
|
|
|
7
|
+
## 0.10.139
|
|
8
|
+
|
|
9
|
+
_2024-11-20_
|
|
10
|
+
|
|
11
|
+
- feat: Add component `DropdownButton`
|
|
12
|
+
- `Multiselect`:
|
|
13
|
+
- fix: Incorrect `aria-controls` attribute value on input/toggler elements
|
|
14
|
+
- `MainMenu2`:
|
|
15
|
+
- fix: Skip rendering pointless `<button/>`s when `onItemClick` is undefined
|
|
16
|
+
- `FocusTrap:`
|
|
17
|
+
- fix: Add `iframe` and `summary` to the list of focusable elements
|
|
18
|
+
- docs: Add short JSDoc on why/how the `depth` prop is useful
|
|
19
|
+
- `FormField`:
|
|
20
|
+
- fix: Indicate that `renderInput`'s `inputProps.id` value is always set
|
|
21
|
+
- `utils`:
|
|
22
|
+
- feat: Add low-level `domid` helper function
|
|
23
|
+
|
|
7
24
|
## 0.10.138
|
|
8
25
|
|
|
9
26
|
_2024-10-31_
|
package/Datepicker.js
CHANGED
|
@@ -153,7 +153,7 @@ const Datepicker = (props) => {
|
|
|
153
153
|
return elm;
|
|
154
154
|
}) }, addFocusProps()),
|
|
155
155
|
isoMode && (react_1.default.createElement("input", { type: "hidden", name: name, value: value === null || value === void 0 ? void 0 : value.toISOString().slice(0, 10) })),
|
|
156
|
-
react_1.default.createElement(ReactDatepicker_js_1.ReactDatePicker, Object.assign({
|
|
156
|
+
react_1.default.createElement(ReactDatepicker_js_1.ReactDatePicker, Object.assign({ required: inputProps.required, disabled: inputProps.disabled, readOnly: inputProps.readOnly, selected: value, name: isoMode ? undefined : name, locale: lang || i18n_1.DEFAULT_LANG, dateFormat: normalizedDateFormats, onChange: (date) => {
|
|
157
157
|
date = date || undefined;
|
|
158
158
|
setValue(date);
|
|
159
159
|
onChange && onChange(date);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { IconName } from '@reykjavik/hanna-css';
|
|
3
|
+
import { ButtonVariantProps } from './_abstract/_Button.js';
|
|
4
|
+
import { MainMenu2Item } from './MainMenu2.js';
|
|
5
|
+
import { SSRSupportProps, WrapperElmProps } from './utils.js';
|
|
6
|
+
type Prefix<record extends Record<string, unknown>, prefix extends string> = {
|
|
7
|
+
[K in keyof record as `${prefix}${Capitalize<string & K>}`]: record[K];
|
|
8
|
+
};
|
|
9
|
+
export type DropdownButtonItem = MainMenu2Item & {
|
|
10
|
+
icon?: IconName;
|
|
11
|
+
};
|
|
12
|
+
export type DropdownButtonCustomItem = (props: {
|
|
13
|
+
closeMenu: () => void;
|
|
14
|
+
}) => React.ReactElement;
|
|
15
|
+
export type DropdownButtonProps = Prefix<Omit<ButtonVariantProps, 'small'>, 'button'> & {
|
|
16
|
+
label: string | NonNullable<React.ReactElement>;
|
|
17
|
+
labelLong?: string;
|
|
18
|
+
/** Default: `"seconcary"` */
|
|
19
|
+
buttonType?: 'primary' | 'secondary';
|
|
20
|
+
items: Array<DropdownButtonItem | DropdownButtonCustomItem>;
|
|
21
|
+
} & WrapperElmProps<'details', 'open' | 'name'> & SSRSupportProps;
|
|
22
|
+
export declare const DropdownButton: (props: DropdownButtonProps) => JSX.Element;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DropdownButton = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const react_1 = tslib_1.__importStar(require("react"));
|
|
6
|
+
const react_2 = require("@floating-ui/react");
|
|
7
|
+
const hanna_utils_1 = require("@reykjavik/hanna-utils");
|
|
8
|
+
const _Button_js_1 = require("./_abstract/_Button.js");
|
|
9
|
+
const useCallbackOnEsc_js_1 = require("./utils/useCallbackOnEsc.js");
|
|
10
|
+
const useLaggedState_js_1 = require("./utils/useLaggedState.js");
|
|
11
|
+
const useOnClickOutside_js_1 = require("./utils/useOnClickOutside.js");
|
|
12
|
+
const FocusTrap_js_1 = require("./FocusTrap.js");
|
|
13
|
+
const utils_js_1 = require("./utils.js");
|
|
14
|
+
const DropdownButton = (props) => {
|
|
15
|
+
const [isOpen, setIsOpen] = (0, useLaggedState_js_1.useLaggedState)(false, 10);
|
|
16
|
+
const isBrowser = (0, utils_js_1.useIsBrowserSide)(props.ssr);
|
|
17
|
+
const [isHovering, setIsHovering] = (0, react_1.useState)(false);
|
|
18
|
+
const wrapperRef = react_1.default.useRef(null);
|
|
19
|
+
const closeMenuStat = () => setIsOpen(false, 0);
|
|
20
|
+
(0, useOnClickOutside_js_1.useOnClickOutside)(wrapperRef, isOpen && closeMenuStat);
|
|
21
|
+
(0, useCallbackOnEsc_js_1.useCallbackOnEsc)(isOpen && closeMenuStat);
|
|
22
|
+
const { x, y, refs } = (0, react_2.useFloating)({
|
|
23
|
+
placement: 'bottom-start',
|
|
24
|
+
middleware: [(0, react_2.flip)(), (0, react_2.shift)()],
|
|
25
|
+
whileElementsMounted: react_2.autoUpdate,
|
|
26
|
+
});
|
|
27
|
+
const { wrapperProps = {} } = props;
|
|
28
|
+
return (react_1.default.createElement("details", Object.assign({}, wrapperProps, { className: (0, hanna_utils_1.modifiedClass)('DropdownButton', isOpen && 'open', wrapperProps.className), open: isOpen, onBlur: (e) => {
|
|
29
|
+
var _a;
|
|
30
|
+
if (!isHovering) {
|
|
31
|
+
setIsOpen(false, 300);
|
|
32
|
+
}
|
|
33
|
+
(_a = wrapperProps.onBlur) === null || _a === void 0 ? void 0 : _a.call(wrapperProps, e);
|
|
34
|
+
}, ref: (elm) => {
|
|
35
|
+
if (!elm) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
wrapperRef.current = elm;
|
|
39
|
+
refs.setReference(elm.querySelector('.DropdownButton__toggler'));
|
|
40
|
+
refs.setFloating(elm.querySelector('.DropdownButton__menu'));
|
|
41
|
+
} }),
|
|
42
|
+
react_1.default.createElement(_Button_js_1.Button, { as: "summary", className: "DropdownButton__toggler", bem: props.buttonType === 'primary' ? 'ButtonPrimary' : 'ButtonSecondary', icon: props.buttonIcon, size: props.buttonSize, variant: props.buttonVariant, "aria-label": props.labelLong, onClick: (e) => {
|
|
43
|
+
e.preventDefault();
|
|
44
|
+
setIsOpen(!isOpen, 0);
|
|
45
|
+
} }, props.label),
|
|
46
|
+
react_1.default.createElement("ul", { className: "DropdownButton__menu", onMouseEnter: () => {
|
|
47
|
+
setIsHovering(true);
|
|
48
|
+
}, onMouseLeave: () => {
|
|
49
|
+
setIsHovering(false);
|
|
50
|
+
}, onFocus: () => {
|
|
51
|
+
setIsOpen(true, 0);
|
|
52
|
+
}, style: x != null
|
|
53
|
+
? {
|
|
54
|
+
'--DropdownButton-pos-y': `${y}px`,
|
|
55
|
+
'--DropdownButton-pos-x': `${x}px`,
|
|
56
|
+
}
|
|
57
|
+
: undefined },
|
|
58
|
+
props.items.map((item, i) => {
|
|
59
|
+
if (typeof item === 'function') {
|
|
60
|
+
const Item = item;
|
|
61
|
+
return (react_1.default.createElement("li", { key: i, className: "DropdownButton__item" },
|
|
62
|
+
react_1.default.createElement(Item, { closeMenu: closeMenuStat })));
|
|
63
|
+
}
|
|
64
|
+
const { label, onClick, href } = item;
|
|
65
|
+
const commonProps = {
|
|
66
|
+
className: 'DropdownButton__itembutton',
|
|
67
|
+
lang: item.lang,
|
|
68
|
+
'data-icon': item.icon,
|
|
69
|
+
'arial-label': item.labelLong,
|
|
70
|
+
};
|
|
71
|
+
return (react_1.default.createElement("li", { key: i, className: "DropdownButton__item" }, isBrowser && onClick ? (react_1.default.createElement("button", Object.assign({}, commonProps, { type: "button", "aria-controls": item.controlsId, onClick: (e) => {
|
|
72
|
+
onClick(item) !== false && closeMenuStat();
|
|
73
|
+
} }), label)) : href != null ? (react_1.default.createElement("a", Object.assign({}, commonProps, { href: href, hrefLang: item.hrefLang, target: item.target }), label)) : null));
|
|
74
|
+
}),
|
|
75
|
+
react_1.default.createElement(FocusTrap_js_1.FocusTrap, { Tag: "li", depth: 2 }))));
|
|
76
|
+
};
|
|
77
|
+
exports.DropdownButton = DropdownButton;
|
package/FocusTrap.d.ts
CHANGED
|
@@ -6,7 +6,10 @@ export type FocusTrapProps = {
|
|
|
6
6
|
/**
|
|
7
7
|
* How deep the trap is placed in the DOM tree beneath its container element.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* This is useful when the trap needs to be placed inside an element that's
|
|
10
|
+
* not a direct child of the desired trapping container.
|
|
11
|
+
*
|
|
12
|
+
* Default: `1` (i.e. use the parent element.)
|
|
10
13
|
*/
|
|
11
14
|
depth?: number;
|
|
12
15
|
};
|
package/FocusTrap.js
CHANGED
|
@@ -21,7 +21,7 @@ const FocusTrap = (props) => {
|
|
|
21
21
|
if (!container) {
|
|
22
22
|
return;
|
|
23
23
|
}
|
|
24
|
-
const focusables = container.querySelectorAll('a[href], input, select, textarea, button, [tabindex]:not(.FocusTrap):not([tabindex="-1"])');
|
|
24
|
+
const focusables = container.querySelectorAll('a[href], input, select, textarea, button, summary, iframe, [tabindex]:not(.FocusTrap):not([tabindex="-1"])');
|
|
25
25
|
const delta = props.atTop ? -1 : 1;
|
|
26
26
|
let i = delta < 0 ? focusables.length - 1 : 0;
|
|
27
27
|
let newTarget;
|
package/FormField.d.ts
CHANGED
|
@@ -77,7 +77,9 @@ type FormFieldProps = FormFieldGroupWrappingProps & {
|
|
|
77
77
|
group?: boolean | 'inputlike';
|
|
78
78
|
empty?: boolean;
|
|
79
79
|
filled?: boolean;
|
|
80
|
-
renderInput(className: InputClassNames, inputProps: FormFieldInputProps
|
|
80
|
+
renderInput(className: InputClassNames, inputProps: FormFieldInputProps & {
|
|
81
|
+
id: string;
|
|
82
|
+
}, addFocusProps: FocusPropMaker, isBrowser?: boolean): JSX.Element;
|
|
81
83
|
};
|
|
82
84
|
export declare const FormField: (props: FormFieldProps) => JSX.Element;
|
|
83
85
|
export default FormField;
|
package/MainMenu2.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export type MainMenu2I18n = {
|
|
|
15
15
|
export declare const defaultMainMenu2Texts: DefaultTexts<MainMenu2I18n>;
|
|
16
16
|
export type MainMenu2Item = {
|
|
17
17
|
/** Visible label text */
|
|
18
|
-
label: string |
|
|
18
|
+
label: string | ReactElement;
|
|
19
19
|
/** Un-abbreviated label set as `title=""` and `aria-label=""` */
|
|
20
20
|
labelLong?: string;
|
|
21
21
|
/** Language of the link label */
|
package/MainMenu2.js
CHANGED
|
@@ -89,7 +89,7 @@ const getRenderers = (props) => {
|
|
|
89
89
|
size: 'small',
|
|
90
90
|
}
|
|
91
91
|
: undefined;
|
|
92
|
-
return (react_1.default.createElement(Tag, { key: key, className: (0, hanna_utils_1.modifiedClass)(`${classPrefix}item`, item.modifier), "aria-current": item.current || undefined }, isBrowser && (onClick || href == null) ? (react_1.default.createElement(ButtonTag, Object.assign({}, commonProps, { type: "button", "aria-controls": controlsId, onClick: () => {
|
|
92
|
+
return (react_1.default.createElement(Tag, { key: key, className: (0, hanna_utils_1.modifiedClass)(`${classPrefix}item`, item.modifier), "aria-current": item.current || undefined }, isBrowser && (onClick || (href == null && onItemClick)) ? (react_1.default.createElement(ButtonTag, Object.assign({}, commonProps, { type: "button", "aria-controls": controlsId, onClick: () => {
|
|
93
93
|
const keepOpen1 = onClick && onClick(item) === false;
|
|
94
94
|
const keepOpen2 = onItemClick && onItemClick(item) === false;
|
|
95
95
|
!(keepOpen1 || keepOpen2) && closeMenu();
|
package/Multiselect.js
CHANGED
|
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.Multiselect = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const react_1 = tslib_1.__importStar(require("react"));
|
|
6
|
-
const domid_1 = tslib_1.__importDefault(require("@hugsmidjan/qj/domid"));
|
|
7
6
|
const hanna_utils_1 = require("@reykjavik/hanna-utils");
|
|
8
7
|
const i18n_1 = require("@reykjavik/hanna-utils/i18n");
|
|
9
8
|
const _Multiselect_search_js_1 = require("./Multiselect/_Multiselect.search.js");
|
|
@@ -202,9 +201,9 @@ const Multiselect = (props) => {
|
|
|
202
201
|
return (react_1.default.createElement(FormField_js_1.default, Object.assign({ extraClassName: (0, hanna_utils_1.modifiedClass)('Multiselect', props.nowrap && 'nowrap'), group: "inputlike", filled: filled, empty: empty }, (0, FormField_js_1.getFormFieldWrapperProps)(props), { renderInput: (className, inputProps, addFocusProps, isBrowser) => {
|
|
203
202
|
const { id } = inputProps;
|
|
204
203
|
return (react_1.default.createElement("div", Object.assign({ className: (0, hanna_utils_1.modifiedClass)('Multiselect__input', [isOpen && 'open'], className.input) }, addFocusProps(), { "data-sprinkled": isBrowser, ref: inputWrapperRef }),
|
|
205
|
-
!isBrowser ? null : isSearchable ? (react_1.default.createElement("input", { className: "Multiselect__search", id: `toggler:${id}`, "aria-label": texts.search, "aria-controls":
|
|
204
|
+
!isBrowser ? null : isSearchable ? (react_1.default.createElement("input", { className: "Multiselect__search", id: `toggler:${id}`, "aria-label": texts.search, "aria-controls": id, "data-expanded": isOpen || undefined, onChange: handleInputChange, onKeyDown: handleInputKeyDown, onClick: () => toggleOpen(), value: searchQuery,
|
|
206
205
|
// onFocus={handleInputFocus}
|
|
207
|
-
placeholder: placeholderText, disabled: disabled, ref: inputRef })) : (react_1.default.createElement("button", { className: "Multiselect__toggler", id: `toggler:${id}`, type: "button", "aria-label": texts.buttonShow, "aria-controls":
|
|
206
|
+
placeholder: placeholderText, disabled: disabled, ref: inputRef })) : (react_1.default.createElement("button", { className: "Multiselect__toggler", id: `toggler:${id}`, type: "button", "aria-label": texts.buttonShow, "aria-controls": id, "aria-expanded": isOpen, onClick: () => toggleOpen(), disabled: disabled,
|
|
208
207
|
// Seems like an innocent hack for visible "placeholder" value.
|
|
209
208
|
// For scren-readers aria-label should take precedence.
|
|
210
209
|
ref: inputRef }, placeholderText || ' ')),
|
package/SearchResults.js
CHANGED
|
@@ -4,7 +4,6 @@ exports.SearchResults = void 0;
|
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const react_1 = tslib_1.__importStar(require("react"));
|
|
6
6
|
const react_intersection_observer_1 = require("react-intersection-observer");
|
|
7
|
-
const prettyNum_1 = require("@hugsmidjan/qj/prettyNum");
|
|
8
7
|
const range_1 = tslib_1.__importDefault(require("@hugsmidjan/qj/range"));
|
|
9
8
|
const hanna_utils_1 = require("@reykjavik/hanna-utils");
|
|
10
9
|
const i18n_1 = require("@reykjavik/hanna-utils/i18n");
|
|
@@ -12,6 +11,14 @@ const _SearchResultsItem_js_1 = require("./SearchResults/_SearchResultsItem.js")
|
|
|
12
11
|
const useDomid_js_1 = require("./utils/useDomid.js");
|
|
13
12
|
const Alert_js_1 = tslib_1.__importDefault(require("./Alert.js"));
|
|
14
13
|
const Tabs_js_1 = tslib_1.__importDefault(require("./Tabs.js"));
|
|
14
|
+
const _fmts = {};
|
|
15
|
+
const formatNumber = (num, lang) => {
|
|
16
|
+
let fmt = _fmts[lang];
|
|
17
|
+
if (!fmt) {
|
|
18
|
+
fmt = _fmts[lang] = new Intl.NumberFormat(lang);
|
|
19
|
+
}
|
|
20
|
+
return fmt.format(num);
|
|
21
|
+
};
|
|
15
22
|
const renderDefaultErrorText = () => (react_1.default.createElement(react_1.default.Fragment, null,
|
|
16
23
|
"\u00DAps, \u00FEa\u00F0 hefur komi\u00F0 upp villa. M\u00E1 bj\u00F3\u00F0a \u00FE\u00E9r a\u00F0 pr\u00F3fa aftur a\u00F0 leita?",
|
|
17
24
|
react_1.default.createElement("br", null),
|
|
@@ -46,7 +53,7 @@ const SearchResults_Tabs = (props) => {
|
|
|
46
53
|
const { domid, filters, activeIdx, setFilter, lang } = props;
|
|
47
54
|
const tabs = (0, react_1.useMemo)(() => (filters || []).map(({ label, count }) => ({
|
|
48
55
|
label,
|
|
49
|
-
badge:
|
|
56
|
+
badge: count && formatNumber(count, lang),
|
|
50
57
|
})), [filters, lang]);
|
|
51
58
|
return tabs.length ? (react_1.default.createElement(Tabs_js_1.default, { role: "tablist", "aria-controls": domid, tabs: tabs, activeIdx: activeIdx || 0, onSetActive: (i) => setFilter && setFilter(i) })) : null;
|
|
52
59
|
};
|
|
@@ -75,7 +82,7 @@ const SearchResults__loadmore = (props) => {
|
|
|
75
82
|
' ',
|
|
76
83
|
react_1.default.createElement("span", { className: "SearchResults__loadmore__count" },
|
|
77
84
|
"(",
|
|
78
|
-
(
|
|
85
|
+
formatNumber(moreCount, lang),
|
|
79
86
|
")"))) : null;
|
|
80
87
|
};
|
|
81
88
|
// ===========================================================================
|
|
@@ -85,7 +92,7 @@ const renderTitle = (props, texts) => {
|
|
|
85
92
|
status === 'loadingquery'
|
|
86
93
|
? texts.loadQueryTitle
|
|
87
94
|
: totalHits
|
|
88
|
-
? `${(
|
|
95
|
+
? `${formatNumber(totalHits, texts.lang)} ${texts.resultsTitle}`
|
|
89
96
|
: texts.noResultsTitle,
|
|
90
97
|
react_1.default.createElement("span", { className: "SearchResults__query" }, query)));
|
|
91
98
|
};
|
|
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.AbstractCarousel = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const react_1 = tslib_1.__importStar(require("react"));
|
|
6
|
-
const A_1 = tslib_1.__importDefault(require("@hugsmidjan/qj/A"));
|
|
7
6
|
const debounce_1 = tslib_1.__importDefault(require("@hugsmidjan/qj/debounce"));
|
|
8
7
|
const focusElm_1 = require("@hugsmidjan/qj/focusElm");
|
|
9
8
|
const throttle_1 = tslib_1.__importDefault(require("@hugsmidjan/qj/throttle"));
|
|
@@ -67,7 +66,7 @@ const AbstractCarousel = (props) => {
|
|
|
67
66
|
const calcActiveItem = (0, throttle_1.default)(() => {
|
|
68
67
|
const { scrollLeft, children } = listElm;
|
|
69
68
|
// using Array#find as forEachUntil
|
|
70
|
-
|
|
69
|
+
Array.from(children).find((item, i) => {
|
|
71
70
|
if (scrollLeft <= item.offsetLeft + item.offsetWidth / 2) {
|
|
72
71
|
setActiveItem(i);
|
|
73
72
|
return true;
|
package/_abstract/_Button.d.ts
CHANGED
package/_abstract/_Button.js
CHANGED
|
@@ -28,11 +28,14 @@ const icons = {
|
|
|
28
28
|
// NOTE: As this component already accepts all `<button/>` and `<a/>` props
|
|
29
29
|
// directly, it makes little sense to add support for `wrapperProps` on top.
|
|
30
30
|
const Button = (props) => {
|
|
31
|
-
const { bem, small, // eslint-disable-line deprecation/deprecation
|
|
32
|
-
size = small ? 'small' : 'normal', modifier, children, variant = 'normal', icon = 'none', label = children } = props, buttonProps = tslib_1.__rest(props, ["bem", "small", "size", "modifier", "children", "variant", "icon", "label"]);
|
|
31
|
+
const { as: CustomTag, bem, small, // eslint-disable-line deprecation/deprecation
|
|
32
|
+
size = small ? 'small' : 'normal', modifier, children, variant = 'normal', icon = 'none', label = children } = props, buttonProps = tslib_1.__rest(props, ["as", "bem", "small", "size", "modifier", "children", "variant", "icon", "label"]);
|
|
33
33
|
const className = bem &&
|
|
34
34
|
(0, hanna_utils_1.modifiedClass)(bem, [modifier, variants[variant], sizes[size], navigationFlags[icon]], props.className);
|
|
35
35
|
const iconProp = icons[icon] && { 'data-icon': icons[icon] };
|
|
36
|
+
if (CustomTag) {
|
|
37
|
+
return (react_1.default.createElement(CustomTag, Object.assign({}, buttonProps, { className: className }, iconProp), label));
|
|
38
|
+
}
|
|
36
39
|
if (buttonProps.href != null) {
|
|
37
40
|
return (react_1.default.createElement(_Link_js_1.Link, Object.assign({}, buttonProps, { className: className }, iconProp), label));
|
|
38
41
|
}
|
package/esm/Datepicker.js
CHANGED
|
@@ -148,7 +148,7 @@ export const Datepicker = (props) => {
|
|
|
148
148
|
return elm;
|
|
149
149
|
}) }, addFocusProps()),
|
|
150
150
|
isoMode && (React.createElement("input", { type: "hidden", name: name, value: value === null || value === void 0 ? void 0 : value.toISOString().slice(0, 10) })),
|
|
151
|
-
React.createElement(ReactDatePicker, Object.assign({
|
|
151
|
+
React.createElement(ReactDatePicker, Object.assign({ required: inputProps.required, disabled: inputProps.disabled, readOnly: inputProps.readOnly, selected: value, name: isoMode ? undefined : name, locale: lang || DEFAULT_LANG, dateFormat: normalizedDateFormats, onChange: (date) => {
|
|
152
152
|
date = date || undefined;
|
|
153
153
|
setValue(date);
|
|
154
154
|
onChange && onChange(date);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { IconName } from '@reykjavik/hanna-css';
|
|
3
|
+
import { ButtonVariantProps } from './_abstract/_Button.js';
|
|
4
|
+
import { MainMenu2Item } from './MainMenu2.js';
|
|
5
|
+
import { SSRSupportProps, WrapperElmProps } from './utils.js';
|
|
6
|
+
type Prefix<record extends Record<string, unknown>, prefix extends string> = {
|
|
7
|
+
[K in keyof record as `${prefix}${Capitalize<string & K>}`]: record[K];
|
|
8
|
+
};
|
|
9
|
+
export type DropdownButtonItem = MainMenu2Item & {
|
|
10
|
+
icon?: IconName;
|
|
11
|
+
};
|
|
12
|
+
export type DropdownButtonCustomItem = (props: {
|
|
13
|
+
closeMenu: () => void;
|
|
14
|
+
}) => React.ReactElement;
|
|
15
|
+
export type DropdownButtonProps = Prefix<Omit<ButtonVariantProps, 'small'>, 'button'> & {
|
|
16
|
+
label: string | NonNullable<React.ReactElement>;
|
|
17
|
+
labelLong?: string;
|
|
18
|
+
/** Default: `"seconcary"` */
|
|
19
|
+
buttonType?: 'primary' | 'secondary';
|
|
20
|
+
items: Array<DropdownButtonItem | DropdownButtonCustomItem>;
|
|
21
|
+
} & WrapperElmProps<'details', 'open' | 'name'> & SSRSupportProps;
|
|
22
|
+
export declare const DropdownButton: (props: DropdownButtonProps) => JSX.Element;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { autoUpdate, flip, shift, useFloating } from '@floating-ui/react';
|
|
3
|
+
import { modifiedClass } from '@reykjavik/hanna-utils';
|
|
4
|
+
import { Button } from './_abstract/_Button.js';
|
|
5
|
+
import { useCallbackOnEsc } from './utils/useCallbackOnEsc.js';
|
|
6
|
+
import { useLaggedState } from './utils/useLaggedState.js';
|
|
7
|
+
import { useOnClickOutside } from './utils/useOnClickOutside.js';
|
|
8
|
+
import { FocusTrap } from './FocusTrap.js';
|
|
9
|
+
import { useIsBrowserSide } from './utils.js';
|
|
10
|
+
export const DropdownButton = (props) => {
|
|
11
|
+
const [isOpen, setIsOpen] = useLaggedState(false, 10);
|
|
12
|
+
const isBrowser = useIsBrowserSide(props.ssr);
|
|
13
|
+
const [isHovering, setIsHovering] = useState(false);
|
|
14
|
+
const wrapperRef = React.useRef(null);
|
|
15
|
+
const closeMenuStat = () => setIsOpen(false, 0);
|
|
16
|
+
useOnClickOutside(wrapperRef, isOpen && closeMenuStat);
|
|
17
|
+
useCallbackOnEsc(isOpen && closeMenuStat);
|
|
18
|
+
const { x, y, refs } = useFloating({
|
|
19
|
+
placement: 'bottom-start',
|
|
20
|
+
middleware: [flip(), shift()],
|
|
21
|
+
whileElementsMounted: autoUpdate,
|
|
22
|
+
});
|
|
23
|
+
const { wrapperProps = {} } = props;
|
|
24
|
+
return (React.createElement("details", Object.assign({}, wrapperProps, { className: modifiedClass('DropdownButton', isOpen && 'open', wrapperProps.className), open: isOpen, onBlur: (e) => {
|
|
25
|
+
var _a;
|
|
26
|
+
if (!isHovering) {
|
|
27
|
+
setIsOpen(false, 300);
|
|
28
|
+
}
|
|
29
|
+
(_a = wrapperProps.onBlur) === null || _a === void 0 ? void 0 : _a.call(wrapperProps, e);
|
|
30
|
+
}, ref: (elm) => {
|
|
31
|
+
if (!elm) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
wrapperRef.current = elm;
|
|
35
|
+
refs.setReference(elm.querySelector('.DropdownButton__toggler'));
|
|
36
|
+
refs.setFloating(elm.querySelector('.DropdownButton__menu'));
|
|
37
|
+
} }),
|
|
38
|
+
React.createElement(Button, { as: "summary", className: "DropdownButton__toggler", bem: props.buttonType === 'primary' ? 'ButtonPrimary' : 'ButtonSecondary', icon: props.buttonIcon, size: props.buttonSize, variant: props.buttonVariant, "aria-label": props.labelLong, onClick: (e) => {
|
|
39
|
+
e.preventDefault();
|
|
40
|
+
setIsOpen(!isOpen, 0);
|
|
41
|
+
} }, props.label),
|
|
42
|
+
React.createElement("ul", { className: "DropdownButton__menu", onMouseEnter: () => {
|
|
43
|
+
setIsHovering(true);
|
|
44
|
+
}, onMouseLeave: () => {
|
|
45
|
+
setIsHovering(false);
|
|
46
|
+
}, onFocus: () => {
|
|
47
|
+
setIsOpen(true, 0);
|
|
48
|
+
}, style: x != null
|
|
49
|
+
? {
|
|
50
|
+
'--DropdownButton-pos-y': `${y}px`,
|
|
51
|
+
'--DropdownButton-pos-x': `${x}px`,
|
|
52
|
+
}
|
|
53
|
+
: undefined },
|
|
54
|
+
props.items.map((item, i) => {
|
|
55
|
+
if (typeof item === 'function') {
|
|
56
|
+
const Item = item;
|
|
57
|
+
return (React.createElement("li", { key: i, className: "DropdownButton__item" },
|
|
58
|
+
React.createElement(Item, { closeMenu: closeMenuStat })));
|
|
59
|
+
}
|
|
60
|
+
const { label, onClick, href } = item;
|
|
61
|
+
const commonProps = {
|
|
62
|
+
className: 'DropdownButton__itembutton',
|
|
63
|
+
lang: item.lang,
|
|
64
|
+
'data-icon': item.icon,
|
|
65
|
+
'arial-label': item.labelLong,
|
|
66
|
+
};
|
|
67
|
+
return (React.createElement("li", { key: i, className: "DropdownButton__item" }, isBrowser && onClick ? (React.createElement("button", Object.assign({}, commonProps, { type: "button", "aria-controls": item.controlsId, onClick: (e) => {
|
|
68
|
+
onClick(item) !== false && closeMenuStat();
|
|
69
|
+
} }), label)) : href != null ? (React.createElement("a", Object.assign({}, commonProps, { href: href, hrefLang: item.hrefLang, target: item.target }), label)) : null));
|
|
70
|
+
}),
|
|
71
|
+
React.createElement(FocusTrap, { Tag: "li", depth: 2 }))));
|
|
72
|
+
};
|
package/esm/FocusTrap.d.ts
CHANGED
|
@@ -6,7 +6,10 @@ export type FocusTrapProps = {
|
|
|
6
6
|
/**
|
|
7
7
|
* How deep the trap is placed in the DOM tree beneath its container element.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* This is useful when the trap needs to be placed inside an element that's
|
|
10
|
+
* not a direct child of the desired trapping container.
|
|
11
|
+
*
|
|
12
|
+
* Default: `1` (i.e. use the parent element.)
|
|
10
13
|
*/
|
|
11
14
|
depth?: number;
|
|
12
15
|
};
|
package/esm/FocusTrap.js
CHANGED
|
@@ -17,7 +17,7 @@ export const FocusTrap = (props) => {
|
|
|
17
17
|
if (!container) {
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
|
-
const focusables = container.querySelectorAll('a[href], input, select, textarea, button, [tabindex]:not(.FocusTrap):not([tabindex="-1"])');
|
|
20
|
+
const focusables = container.querySelectorAll('a[href], input, select, textarea, button, summary, iframe, [tabindex]:not(.FocusTrap):not([tabindex="-1"])');
|
|
21
21
|
const delta = props.atTop ? -1 : 1;
|
|
22
22
|
let i = delta < 0 ? focusables.length - 1 : 0;
|
|
23
23
|
let newTarget;
|
package/esm/FormField.d.ts
CHANGED
|
@@ -77,7 +77,9 @@ type FormFieldProps = FormFieldGroupWrappingProps & {
|
|
|
77
77
|
group?: boolean | 'inputlike';
|
|
78
78
|
empty?: boolean;
|
|
79
79
|
filled?: boolean;
|
|
80
|
-
renderInput(className: InputClassNames, inputProps: FormFieldInputProps
|
|
80
|
+
renderInput(className: InputClassNames, inputProps: FormFieldInputProps & {
|
|
81
|
+
id: string;
|
|
82
|
+
}, addFocusProps: FocusPropMaker, isBrowser?: boolean): JSX.Element;
|
|
81
83
|
};
|
|
82
84
|
export declare const FormField: (props: FormFieldProps) => JSX.Element;
|
|
83
85
|
export default FormField;
|
package/esm/MainMenu2.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export type MainMenu2I18n = {
|
|
|
15
15
|
export declare const defaultMainMenu2Texts: DefaultTexts<MainMenu2I18n>;
|
|
16
16
|
export type MainMenu2Item = {
|
|
17
17
|
/** Visible label text */
|
|
18
|
-
label: string |
|
|
18
|
+
label: string | ReactElement;
|
|
19
19
|
/** Un-abbreviated label set as `title=""` and `aria-label=""` */
|
|
20
20
|
labelLong?: string;
|
|
21
21
|
/** Language of the link label */
|
package/esm/MainMenu2.js
CHANGED
|
@@ -85,7 +85,7 @@ const getRenderers = (props) => {
|
|
|
85
85
|
size: 'small',
|
|
86
86
|
}
|
|
87
87
|
: undefined;
|
|
88
|
-
return (React.createElement(Tag, { key: key, className: modifiedClass(`${classPrefix}item`, item.modifier), "aria-current": item.current || undefined }, isBrowser && (onClick || href == null) ? (React.createElement(ButtonTag, Object.assign({}, commonProps, { type: "button", "aria-controls": controlsId, onClick: () => {
|
|
88
|
+
return (React.createElement(Tag, { key: key, className: modifiedClass(`${classPrefix}item`, item.modifier), "aria-current": item.current || undefined }, isBrowser && (onClick || (href == null && onItemClick)) ? (React.createElement(ButtonTag, Object.assign({}, commonProps, { type: "button", "aria-controls": controlsId, onClick: () => {
|
|
89
89
|
const keepOpen1 = onClick && onClick(item) === false;
|
|
90
90
|
const keepOpen2 = onItemClick && onItemClick(item) === false;
|
|
91
91
|
!(keepOpen1 || keepOpen2) && closeMenu();
|
package/esm/Multiselect.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import React, { Fragment, useCallback, useEffect, useMemo, useRef, useState, } from 'react';
|
|
2
|
-
import domId from '@hugsmidjan/qj/domid';
|
|
3
2
|
import { modifiedClass, notNully } from '@reykjavik/hanna-utils';
|
|
4
3
|
import { getTexts } from '@reykjavik/hanna-utils/i18n';
|
|
5
4
|
import { filterItems } from './Multiselect/_Multiselect.search.js';
|
|
@@ -198,9 +197,9 @@ export const Multiselect = (props) => {
|
|
|
198
197
|
return (React.createElement(FormField, Object.assign({ extraClassName: modifiedClass('Multiselect', props.nowrap && 'nowrap'), group: "inputlike", filled: filled, empty: empty }, getFormFieldWrapperProps(props), { renderInput: (className, inputProps, addFocusProps, isBrowser) => {
|
|
199
198
|
const { id } = inputProps;
|
|
200
199
|
return (React.createElement("div", Object.assign({ className: modifiedClass('Multiselect__input', [isOpen && 'open'], className.input) }, addFocusProps(), { "data-sprinkled": isBrowser, ref: inputWrapperRef }),
|
|
201
|
-
!isBrowser ? null : isSearchable ? (React.createElement("input", { className: "Multiselect__search", id: `toggler:${id}`, "aria-label": texts.search, "aria-controls":
|
|
200
|
+
!isBrowser ? null : isSearchable ? (React.createElement("input", { className: "Multiselect__search", id: `toggler:${id}`, "aria-label": texts.search, "aria-controls": id, "data-expanded": isOpen || undefined, onChange: handleInputChange, onKeyDown: handleInputKeyDown, onClick: () => toggleOpen(), value: searchQuery,
|
|
202
201
|
// onFocus={handleInputFocus}
|
|
203
|
-
placeholder: placeholderText, disabled: disabled, ref: inputRef })) : (React.createElement("button", { className: "Multiselect__toggler", id: `toggler:${id}`, type: "button", "aria-label": texts.buttonShow, "aria-controls":
|
|
202
|
+
placeholder: placeholderText, disabled: disabled, ref: inputRef })) : (React.createElement("button", { className: "Multiselect__toggler", id: `toggler:${id}`, type: "button", "aria-label": texts.buttonShow, "aria-controls": id, "aria-expanded": isOpen, onClick: () => toggleOpen(), disabled: disabled,
|
|
204
203
|
// Seems like an innocent hack for visible "placeholder" value.
|
|
205
204
|
// For scren-readers aria-label should take precedence.
|
|
206
205
|
ref: inputRef }, placeholderText || ' ')),
|
package/esm/SearchResults.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import React, { useEffect, useMemo, useRef } from 'react';
|
|
2
2
|
import { useInView } from 'react-intersection-observer';
|
|
3
|
-
import { prettyNum } from '@hugsmidjan/qj/prettyNum';
|
|
4
3
|
import range from '@hugsmidjan/qj/range';
|
|
5
4
|
import { modifiedClass } from '@reykjavik/hanna-utils';
|
|
6
5
|
import { getTexts } from '@reykjavik/hanna-utils/i18n';
|
|
@@ -8,6 +7,14 @@ import { SearchResultsItem, } from './SearchResults/_SearchResultsItem.js';
|
|
|
8
7
|
import { useDomid } from './utils/useDomid.js';
|
|
9
8
|
import Alert from './Alert.js';
|
|
10
9
|
import Tabs from './Tabs.js';
|
|
10
|
+
const _fmts = {};
|
|
11
|
+
const formatNumber = (num, lang) => {
|
|
12
|
+
let fmt = _fmts[lang];
|
|
13
|
+
if (!fmt) {
|
|
14
|
+
fmt = _fmts[lang] = new Intl.NumberFormat(lang);
|
|
15
|
+
}
|
|
16
|
+
return fmt.format(num);
|
|
17
|
+
};
|
|
11
18
|
const renderDefaultErrorText = () => (React.createElement(React.Fragment, null,
|
|
12
19
|
"\u00DAps, \u00FEa\u00F0 hefur komi\u00F0 upp villa. M\u00E1 bj\u00F3\u00F0a \u00FE\u00E9r a\u00F0 pr\u00F3fa aftur a\u00F0 leita?",
|
|
13
20
|
React.createElement("br", null),
|
|
@@ -42,7 +49,7 @@ const SearchResults_Tabs = (props) => {
|
|
|
42
49
|
const { domid, filters, activeIdx, setFilter, lang } = props;
|
|
43
50
|
const tabs = useMemo(() => (filters || []).map(({ label, count }) => ({
|
|
44
51
|
label,
|
|
45
|
-
badge:
|
|
52
|
+
badge: count && formatNumber(count, lang),
|
|
46
53
|
})), [filters, lang]);
|
|
47
54
|
return tabs.length ? (React.createElement(Tabs, { role: "tablist", "aria-controls": domid, tabs: tabs, activeIdx: activeIdx || 0, onSetActive: (i) => setFilter && setFilter(i) })) : null;
|
|
48
55
|
};
|
|
@@ -71,7 +78,7 @@ const SearchResults__loadmore = (props) => {
|
|
|
71
78
|
' ',
|
|
72
79
|
React.createElement("span", { className: "SearchResults__loadmore__count" },
|
|
73
80
|
"(",
|
|
74
|
-
|
|
81
|
+
formatNumber(moreCount, lang),
|
|
75
82
|
")"))) : null;
|
|
76
83
|
};
|
|
77
84
|
// ===========================================================================
|
|
@@ -81,7 +88,7 @@ const renderTitle = (props, texts) => {
|
|
|
81
88
|
status === 'loadingquery'
|
|
82
89
|
? texts.loadQueryTitle
|
|
83
90
|
: totalHits
|
|
84
|
-
? `${
|
|
91
|
+
? `${formatNumber(totalHits, texts.lang)} ${texts.resultsTitle}`
|
|
85
92
|
: texts.noResultsTitle,
|
|
86
93
|
React.createElement("span", { className: "SearchResults__query" }, query)));
|
|
87
94
|
};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import React, { useEffect, useMemo, useRef, useState, } from 'react';
|
|
2
|
-
import A from '@hugsmidjan/qj/A';
|
|
3
2
|
import debounce from '@hugsmidjan/qj/debounce';
|
|
4
3
|
import { focusElm } from '@hugsmidjan/qj/focusElm';
|
|
5
4
|
import throttle from '@hugsmidjan/qj/throttle';
|
|
@@ -63,7 +62,7 @@ export const AbstractCarousel = (props) => {
|
|
|
63
62
|
const calcActiveItem = throttle(() => {
|
|
64
63
|
const { scrollLeft, children } = listElm;
|
|
65
64
|
// using Array#find as forEachUntil
|
|
66
|
-
|
|
65
|
+
Array.from(children).find((item, i) => {
|
|
67
66
|
if (scrollLeft <= item.offsetLeft + item.offsetWidth / 2) {
|
|
68
67
|
setActiveItem(i);
|
|
69
68
|
return true;
|
package/esm/_abstract/_Button.js
CHANGED
|
@@ -25,11 +25,14 @@ const icons = {
|
|
|
25
25
|
// NOTE: As this component already accepts all `<button/>` and `<a/>` props
|
|
26
26
|
// directly, it makes little sense to add support for `wrapperProps` on top.
|
|
27
27
|
export const Button = (props) => {
|
|
28
|
-
const { bem, small, // eslint-disable-line deprecation/deprecation
|
|
29
|
-
size = small ? 'small' : 'normal', modifier, children, variant = 'normal', icon = 'none', label = children } = props, buttonProps = __rest(props, ["bem", "small", "size", "modifier", "children", "variant", "icon", "label"]);
|
|
28
|
+
const { as: CustomTag, bem, small, // eslint-disable-line deprecation/deprecation
|
|
29
|
+
size = small ? 'small' : 'normal', modifier, children, variant = 'normal', icon = 'none', label = children } = props, buttonProps = __rest(props, ["as", "bem", "small", "size", "modifier", "children", "variant", "icon", "label"]);
|
|
30
30
|
const className = bem &&
|
|
31
31
|
modifiedClass(bem, [modifier, variants[variant], sizes[size], navigationFlags[icon]], props.className);
|
|
32
32
|
const iconProp = icons[icon] && { 'data-icon': icons[icon] };
|
|
33
|
+
if (CustomTag) {
|
|
34
|
+
return (React.createElement(CustomTag, Object.assign({}, buttonProps, { className: className }, iconProp), label));
|
|
35
|
+
}
|
|
33
36
|
if (buttonProps.href != null) {
|
|
34
37
|
return (React.createElement(Link, Object.assign({}, buttonProps, { className: className }, iconProp), label));
|
|
35
38
|
}
|
package/esm/index.d.ts
CHANGED
|
@@ -73,6 +73,7 @@
|
|
|
73
73
|
/// <reference path="./FieldGroup.d.tsx" />
|
|
74
74
|
/// <reference path="./FeatureList.d.tsx" />
|
|
75
75
|
/// <reference path="./ExtraLinks.d.tsx" />
|
|
76
|
+
/// <reference path="./DropdownButton.d.tsx" />
|
|
76
77
|
/// <reference path="./Datepicker.d.tsx" />
|
|
77
78
|
/// <reference path="./ContentImage.d.tsx" />
|
|
78
79
|
/// <reference path="./ContentArticle.d.tsx" />
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Performs a callback whenever the user hits the ESC key.
|
|
3
3
|
*
|
|
4
|
-
* Pass `undefined` to remove the event listener
|
|
4
|
+
* Pass `undefined` or `false` to remove the event listener.
|
|
5
5
|
*/
|
|
6
|
-
export declare const useCallbackOnEsc: (callback: (() => void) | undefined) => void;
|
|
6
|
+
export declare const useCallbackOnEsc: (callback: (() => void) | undefined | false) => void;
|
|
@@ -2,7 +2,7 @@ import { useEffect, useRef } from 'react';
|
|
|
2
2
|
/**
|
|
3
3
|
* Performs a callback whenever the user hits the ESC key.
|
|
4
4
|
*
|
|
5
|
-
* Pass `undefined` to remove the event listener
|
|
5
|
+
* Pass `undefined` or `false` to remove the event listener.
|
|
6
6
|
*/
|
|
7
7
|
export const useCallbackOnEsc = (callback) => {
|
|
8
8
|
const cb = useRef(callback);
|
package/esm/utils/useDomid.d.ts
CHANGED
package/esm/utils/useDomid.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import domid from '@hugsmidjan/qj/domid';
|
|
3
2
|
// @ts-expect-error (transparently feature-detect useId hook, which is introduced in React@18)
|
|
4
3
|
const useId = React.useId;
|
|
4
|
+
const domid_prefix = `_${ /*@__PURE__*/`${Date.now()}-`.slice(6)}`;
|
|
5
|
+
let domid_incr = 0;
|
|
6
|
+
/**
|
|
7
|
+
* Returns a short locally-unique ID string.
|
|
8
|
+
*/
|
|
9
|
+
export const domid = () => domid_prefix + domid_incr++;
|
|
5
10
|
/**
|
|
6
11
|
* Returns a stable, unique ID string.
|
|
7
12
|
*
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { MutableRefObject, RefObject } from 'react';
|
|
2
|
-
type
|
|
2
|
+
type ClickHandler = (event: globalThis.MouseEvent | globalThis.TouchEvent) => void;
|
|
3
3
|
/**
|
|
4
|
+
* A hook that calls a `handler` function when a click event occurs outside of
|
|
5
|
+
* a given `containerRef`.
|
|
4
6
|
*
|
|
5
|
-
*
|
|
6
|
-
* @param handler callback to run when clicked outside of the ref
|
|
7
|
+
* Pass `undefined` or `false` to remove the event listener.
|
|
7
8
|
*/
|
|
8
|
-
export declare const useOnClickOutside:
|
|
9
|
+
export declare const useOnClickOutside: (containerRef: MutableRefObject<Element> | RefObject<Element>, handler: ClickHandler | undefined | false) => void;
|
|
9
10
|
export {};
|
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
import { useEffect,
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
2
|
/**
|
|
3
|
+
* A hook that calls a `handler` function when a click event occurs outside of
|
|
4
|
+
* a given `containerRef`.
|
|
3
5
|
*
|
|
4
|
-
*
|
|
5
|
-
* @param handler callback to run when clicked outside of the ref
|
|
6
|
+
* Pass `undefined` or `false` to remove the event listener.
|
|
6
7
|
*/
|
|
7
|
-
export const useOnClickOutside = (
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
export const useOnClickOutside = (containerRef, handler) => {
|
|
9
|
+
const h = useRef(handler);
|
|
10
|
+
const active = !!handler;
|
|
11
|
+
h.current = handler;
|
|
11
12
|
useEffect(() => {
|
|
13
|
+
if (!active || !containerRef.current) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
12
16
|
const listener = (event) => {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return node.contains(event.target);
|
|
19
|
-
});
|
|
20
|
-
if (shouldTrigger) {
|
|
21
|
-
handler(event);
|
|
17
|
+
if (!containerRef.current) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
if (!containerRef.current.contains(event.target)) {
|
|
21
|
+
h.current && h.current(event);
|
|
22
22
|
}
|
|
23
23
|
};
|
|
24
24
|
document.addEventListener('click', listener);
|
|
25
25
|
return () => {
|
|
26
26
|
document.removeEventListener('click', listener);
|
|
27
27
|
};
|
|
28
|
-
}, [
|
|
28
|
+
}, [active, containerRef]);
|
|
29
29
|
};
|
package/index.d.ts
CHANGED
|
@@ -73,6 +73,7 @@
|
|
|
73
73
|
/// <reference path="./FieldGroup.d.tsx" />
|
|
74
74
|
/// <reference path="./FeatureList.d.tsx" />
|
|
75
75
|
/// <reference path="./ExtraLinks.d.tsx" />
|
|
76
|
+
/// <reference path="./DropdownButton.d.tsx" />
|
|
76
77
|
/// <reference path="./Datepicker.d.tsx" />
|
|
77
78
|
/// <reference path="./ContentImage.d.tsx" />
|
|
78
79
|
/// <reference path="./ContentArticle.d.tsx" />
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reykjavik/hanna-react",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.139",
|
|
4
4
|
"author": "Reykjavík (http://www.reykjavik.is)",
|
|
5
5
|
"contributors": [
|
|
6
6
|
"Hugsmiðjan ehf (http://www.hugsmidjan.is)",
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
"@floating-ui/react": "^0.19.2",
|
|
18
18
|
"@hugsmidjan/qj": "^4.22.1",
|
|
19
19
|
"@hugsmidjan/react": "^0.4.32",
|
|
20
|
-
"@reykjavik/hanna-css": "^0.4.
|
|
21
|
-
"@reykjavik/hanna-utils": "^0.2.
|
|
20
|
+
"@reykjavik/hanna-css": "^0.4.16",
|
|
21
|
+
"@reykjavik/hanna-utils": "^0.2.18",
|
|
22
22
|
"@types/react-autosuggest": "^10.1.0",
|
|
23
23
|
"@types/react-datepicker": "^4.8.0",
|
|
24
24
|
"@types/react-transition-group": "^4.4.0",
|
|
@@ -345,6 +345,10 @@
|
|
|
345
345
|
"import": "./esm/ExtraLinks.js",
|
|
346
346
|
"require": "./ExtraLinks.js"
|
|
347
347
|
},
|
|
348
|
+
"./DropdownButton": {
|
|
349
|
+
"import": "./esm/DropdownButton.js",
|
|
350
|
+
"require": "./DropdownButton.js"
|
|
351
|
+
},
|
|
348
352
|
"./Datepicker": {
|
|
349
353
|
"import": "./esm/Datepicker.js",
|
|
350
354
|
"require": "./Datepicker.js"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Performs a callback whenever the user hits the ESC key.
|
|
3
3
|
*
|
|
4
|
-
* Pass `undefined` to remove the event listener
|
|
4
|
+
* Pass `undefined` or `false` to remove the event listener.
|
|
5
5
|
*/
|
|
6
|
-
export declare const useCallbackOnEsc: (callback: (() => void) | undefined) => void;
|
|
6
|
+
export declare const useCallbackOnEsc: (callback: (() => void) | undefined | false) => void;
|
|
@@ -5,7 +5,7 @@ const react_1 = require("react");
|
|
|
5
5
|
/**
|
|
6
6
|
* Performs a callback whenever the user hits the ESC key.
|
|
7
7
|
*
|
|
8
|
-
* Pass `undefined` to remove the event listener
|
|
8
|
+
* Pass `undefined` or `false` to remove the event listener.
|
|
9
9
|
*/
|
|
10
10
|
const useCallbackOnEsc = (callback) => {
|
|
11
11
|
const cb = (0, react_1.useRef)(callback);
|
package/utils/useDomid.d.ts
CHANGED
package/utils/useDomid.js
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.useDomid = void 0;
|
|
3
|
+
exports.useDomid = exports.domid = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const react_1 = tslib_1.__importDefault(require("react"));
|
|
6
|
-
const domid_1 = tslib_1.__importDefault(require("@hugsmidjan/qj/domid"));
|
|
7
6
|
// @ts-expect-error (transparently feature-detect useId hook, which is introduced in React@18)
|
|
8
7
|
const useId = react_1.default.useId;
|
|
8
|
+
const domid_prefix = `_${ /*@__PURE__*/`${Date.now()}-`.slice(6)}`;
|
|
9
|
+
let domid_incr = 0;
|
|
10
|
+
/**
|
|
11
|
+
* Returns a short locally-unique ID string.
|
|
12
|
+
*/
|
|
13
|
+
const domid = () => domid_prefix + domid_incr++;
|
|
14
|
+
exports.domid = domid;
|
|
9
15
|
/**
|
|
10
16
|
* Returns a stable, unique ID string.
|
|
11
17
|
*
|
|
@@ -25,7 +31,7 @@ exports.useDomid = useId
|
|
|
25
31
|
: (staticId) => {
|
|
26
32
|
const idRef = react_1.default.useRef();
|
|
27
33
|
if (!idRef.current) {
|
|
28
|
-
idRef.current = staticId || (0,
|
|
34
|
+
idRef.current = staticId || (0, exports.domid)();
|
|
29
35
|
}
|
|
30
36
|
return idRef.current;
|
|
31
37
|
};
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { MutableRefObject, RefObject } from 'react';
|
|
2
|
-
type
|
|
2
|
+
type ClickHandler = (event: globalThis.MouseEvent | globalThis.TouchEvent) => void;
|
|
3
3
|
/**
|
|
4
|
+
* A hook that calls a `handler` function when a click event occurs outside of
|
|
5
|
+
* a given `containerRef`.
|
|
4
6
|
*
|
|
5
|
-
*
|
|
6
|
-
* @param handler callback to run when clicked outside of the ref
|
|
7
|
+
* Pass `undefined` or `false` to remove the event listener.
|
|
7
8
|
*/
|
|
8
|
-
export declare const useOnClickOutside:
|
|
9
|
+
export declare const useOnClickOutside: (containerRef: MutableRefObject<Element> | RefObject<Element>, handler: ClickHandler | undefined | false) => void;
|
|
9
10
|
export {};
|
|
@@ -3,31 +3,31 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.useOnClickOutside = void 0;
|
|
4
4
|
const react_1 = require("react");
|
|
5
5
|
/**
|
|
6
|
+
* A hook that calls a `handler` function when a click event occurs outside of
|
|
7
|
+
* a given `containerRef`.
|
|
6
8
|
*
|
|
7
|
-
*
|
|
8
|
-
* @param handler callback to run when clicked outside of the ref
|
|
9
|
+
* Pass `undefined` or `false` to remove the event listener.
|
|
9
10
|
*/
|
|
10
|
-
const useOnClickOutside = (
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
const useOnClickOutside = (containerRef, handler) => {
|
|
12
|
+
const h = (0, react_1.useRef)(handler);
|
|
13
|
+
const active = !!handler;
|
|
14
|
+
h.current = handler;
|
|
14
15
|
(0, react_1.useEffect)(() => {
|
|
16
|
+
if (!active || !containerRef.current) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
15
19
|
const listener = (event) => {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return node.contains(event.target);
|
|
22
|
-
});
|
|
23
|
-
if (shouldTrigger) {
|
|
24
|
-
handler(event);
|
|
20
|
+
if (!containerRef.current) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
if (!containerRef.current.contains(event.target)) {
|
|
24
|
+
h.current && h.current(event);
|
|
25
25
|
}
|
|
26
26
|
};
|
|
27
27
|
document.addEventListener('click', listener);
|
|
28
28
|
return () => {
|
|
29
29
|
document.removeEventListener('click', listener);
|
|
30
30
|
};
|
|
31
|
-
}, [
|
|
31
|
+
}, [active, containerRef]);
|
|
32
32
|
};
|
|
33
33
|
exports.useOnClickOutside = useOnClickOutside;
|