@reykjavik/hanna-react 0.10.91 → 0.10.93
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/ArticleCarousel/_ArticleCarouselCard.d.ts +2 -1
- package/CHANGELOG.md +23 -0
- package/ContactBubble.d.ts +2 -1
- package/ContactBubble.js +4 -6
- package/Datepicker.d.ts +31 -3
- package/Datepicker.js +25 -6
- package/FormField.d.ts +11 -2
- package/FormField.js +5 -5
- package/MainMenu/_PrimaryPanel.d.ts +2 -2
- package/MainMenu.d.ts +2 -1
- package/MainMenu.js +5 -6
- package/Multiselect/_Multiselect.search.d.ts +19 -0
- package/Multiselect/_Multiselect.search.js +80 -0
- package/Multiselect.d.ts +64 -0
- package/Multiselect.js +236 -0
- package/ReadSpeakerPlayer.d.ts +64 -0
- package/ReadSpeakerPlayer.js +78 -0
- package/RelatedLinks.d.ts +2 -1
- package/Selectbox.d.ts +3 -3
- package/TextInput.d.ts +0 -1
- package/_abstract/_CardList.d.ts +2 -1
- package/_abstract/_CardList.js +2 -2
- package/_abstract/_FocusTrap.d.ts +14 -0
- package/_abstract/_FocusTrap.js +24 -0
- package/_abstract/_TogglerGroup.d.ts +11 -7
- package/_abstract/_TogglerGroup.js +11 -3
- package/_abstract/_TogglerGroupField.d.ts +4 -4
- package/_abstract/_TogglerGroupField.js +2 -2
- package/_abstract/_TogglerInput.d.ts +3 -1
- package/_abstract/_TogglerInput.js +7 -4
- package/esm/ArticleCarousel/_ArticleCarouselCard.d.ts +2 -1
- package/esm/ContactBubble.d.ts +2 -1
- package/esm/ContactBubble.js +4 -6
- package/esm/Datepicker.d.ts +31 -3
- package/esm/Datepicker.js +25 -6
- package/esm/FormField.d.ts +11 -2
- package/esm/FormField.js +5 -5
- package/esm/MainMenu/_PrimaryPanel.d.ts +2 -2
- package/esm/MainMenu.d.ts +2 -1
- package/esm/MainMenu.js +5 -6
- package/esm/Multiselect/_Multiselect.search.d.ts +19 -0
- package/esm/Multiselect/_Multiselect.search.js +75 -0
- package/esm/Multiselect.d.ts +64 -0
- package/esm/Multiselect.js +231 -0
- package/esm/ReadSpeakerPlayer.d.ts +64 -0
- package/esm/ReadSpeakerPlayer.js +72 -0
- package/esm/RelatedLinks.d.ts +2 -1
- package/esm/Selectbox.d.ts +3 -3
- package/esm/TextInput.d.ts +0 -1
- package/esm/_abstract/_CardList.d.ts +2 -1
- package/esm/_abstract/_CardList.js +2 -2
- package/esm/_abstract/_FocusTrap.d.ts +14 -0
- package/esm/_abstract/_FocusTrap.js +19 -0
- package/esm/_abstract/_TogglerGroup.d.ts +11 -7
- package/esm/_abstract/_TogglerGroup.js +11 -3
- package/esm/_abstract/_TogglerGroupField.d.ts +4 -4
- package/esm/_abstract/_TogglerGroupField.js +2 -2
- package/esm/_abstract/_TogglerInput.d.ts +3 -1
- package/esm/_abstract/_TogglerInput.js +7 -4
- package/esm/index.d.ts +2 -0
- package/esm/utils/useFormatMonitor.d.ts +4 -2
- package/esm/utils/useFormatMonitor.js +4 -2
- package/index.d.ts +2 -0
- package/package.json +13 -5
- package/utils/useFormatMonitor.d.ts +4 -2
- package/utils/useFormatMonitor.js +4 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import React from 'react';
|
|
1
2
|
import type { HannaColorTheme } from '@reykjavik/hanna-css';
|
|
2
3
|
import { Illustration } from '@reykjavik/hanna-utils/assets';
|
|
3
4
|
import { ImageProps } from '../_abstract/_Image.js';
|
|
@@ -10,7 +11,7 @@ export type ArticleCarouselCardProps = {
|
|
|
10
11
|
title: string;
|
|
11
12
|
summary: string;
|
|
12
13
|
href: string;
|
|
13
|
-
target?:
|
|
14
|
+
target?: React.HTMLAttributeAnchorTarget;
|
|
14
15
|
color?: ColorFamily;
|
|
15
16
|
/** NOTE: if both `color` and `theme` are specified
|
|
16
17
|
* then `color` takes precedence.
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,29 @@
|
|
|
4
4
|
|
|
5
5
|
- ... <!-- Add new lines here. -->
|
|
6
6
|
|
|
7
|
+
## 0.10.93
|
|
8
|
+
|
|
9
|
+
_2023-07-06_
|
|
10
|
+
|
|
11
|
+
- feat: Add component `Multiselect`
|
|
12
|
+
- fix: Add `target` prop to all "\*Cards" components that have `href`s
|
|
13
|
+
- `Datepicker`:
|
|
14
|
+
- feat: Add support for "uncontrolled" mode — (add prop `defaultValue`, make
|
|
15
|
+
`value` and `onChange` optional.)
|
|
16
|
+
- feat: Add prop `isoMode` to generate/submit ISO-8601 `<input/>` values
|
|
17
|
+
- Checkboxes and Radio buttons
|
|
18
|
+
- feat: Support `readOnly` — using `disabled` + `input[type=hidden]`
|
|
19
|
+
- feat: Support passing `options` as simple string array
|
|
20
|
+
- feat: Make `name` prop optional for groups
|
|
21
|
+
- fix: Handling of `disabled` as array of indexes
|
|
22
|
+
|
|
23
|
+
## 0.10.92
|
|
24
|
+
|
|
25
|
+
_2023-06-06_
|
|
26
|
+
|
|
27
|
+
- feat: Add component `ReadSpeakerPlayer` and a `stopReading` helper
|
|
28
|
+
- fix: Update dependencies for minor esm and `Modal`-related bugfixes
|
|
29
|
+
|
|
7
30
|
## 0.10.90 – 0.10.91
|
|
8
31
|
|
|
9
32
|
_2023-06-01_
|
package/ContactBubble.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import React from 'react';
|
|
1
2
|
import { DefaultTexts } from '@reykjavik/hanna-utils/i18n';
|
|
2
3
|
import { SSRSupport } from './utils.js';
|
|
3
4
|
export type ContactBubbleI18n = {
|
|
@@ -25,7 +26,7 @@ export type ContactBubbleItem = {
|
|
|
25
26
|
href: string;
|
|
26
27
|
/** Prevents default link behavior unless the handler function returns `true` */
|
|
27
28
|
onClick?: () => void | boolean;
|
|
28
|
-
target?:
|
|
29
|
+
target?: React.HTMLAttributeAnchorTarget;
|
|
29
30
|
} | {
|
|
30
31
|
onClick: () => void | boolean;
|
|
31
32
|
href?: undefined;
|
package/ContactBubble.js
CHANGED
|
@@ -6,7 +6,6 @@ const react_1 = tslib_1.__importStar(require("react"));
|
|
|
6
6
|
const focusElm_1 = require("@hugsmidjan/qj/focusElm");
|
|
7
7
|
const hooks_1 = require("@hugsmidjan/react/hooks");
|
|
8
8
|
const getBemClass_1 = tslib_1.__importDefault(require("@hugsmidjan/react/utils/getBemClass"));
|
|
9
|
-
const hanna_utils_1 = require("@reykjavik/hanna-utils");
|
|
10
9
|
const i18n_1 = require("@reykjavik/hanna-utils/i18n");
|
|
11
10
|
const _Link_js_1 = require("./_abstract/_Link.js");
|
|
12
11
|
const breakOnNL_js_1 = require("./_abstract/breakOnNL.js");
|
|
@@ -66,12 +65,11 @@ const ContactBubble = (props) => {
|
|
|
66
65
|
wrapperElm.dataset.show = 'true';
|
|
67
66
|
return;
|
|
68
67
|
}
|
|
69
|
-
const scrollElm = (0, hanna_utils_1.getPageScrollElm)();
|
|
70
68
|
let pending = 0;
|
|
71
69
|
const checkScroll = () => {
|
|
72
70
|
if (!pending) {
|
|
73
71
|
pending = requestAnimationFrame(() => {
|
|
74
|
-
const { scrollTop, scrollHeight, clientHeight } =
|
|
72
|
+
const { scrollTop, scrollHeight, clientHeight } = document.documentElement;
|
|
75
73
|
const scrollLength = scrollHeight - clientHeight;
|
|
76
74
|
// const f = scrollLength > 600 ? 1 : (scrollLength - 200) / 600;
|
|
77
75
|
const f = 1;
|
|
@@ -83,7 +81,7 @@ const ContactBubble = (props) => {
|
|
|
83
81
|
}
|
|
84
82
|
};
|
|
85
83
|
checkScroll();
|
|
86
|
-
// Set scroll-listeners on both the ´document` and the `
|
|
84
|
+
// Set scroll-listeners on both the ´document` and the `document.documentElement`
|
|
87
85
|
// because mobile browsers seem to handle CSS height and overflow
|
|
88
86
|
// rules on <html> and <body> differently from desktop browsers.
|
|
89
87
|
// Only one of these two handlers seems to trigger though,
|
|
@@ -91,10 +89,10 @@ const ContactBubble = (props) => {
|
|
|
91
89
|
// and even if they did, the rAF throttling prevents that from
|
|
92
90
|
// becoming a problem.
|
|
93
91
|
document.addEventListener('scroll', checkScroll);
|
|
94
|
-
|
|
92
|
+
document.documentElement.addEventListener('scroll', checkScroll);
|
|
95
93
|
return () => {
|
|
96
94
|
document.removeEventListener('scroll', checkScroll);
|
|
97
|
-
|
|
95
|
+
document.documentElement.removeEventListener('scroll', checkScroll);
|
|
98
96
|
};
|
|
99
97
|
}, [isBrowser, alwaysShow, closeBubble]);
|
|
100
98
|
(0, react_1.useEffect)(() => {
|
package/Datepicker.d.ts
CHANGED
|
@@ -4,22 +4,45 @@ export type DatepickerProps = {
|
|
|
4
4
|
small?: boolean;
|
|
5
5
|
placeholder?: string;
|
|
6
6
|
value?: Date;
|
|
7
|
+
/**
|
|
8
|
+
* Default value for "uncontrolled" mode.
|
|
9
|
+
*
|
|
10
|
+
* NOTE: Even though defaultValue and the `onChange` value are both `Date`
|
|
11
|
+
* the `<input/>` element is still `type="text"` and it's `.value` is
|
|
12
|
+
* the human-readable (parsed) date `string`.
|
|
13
|
+
*
|
|
14
|
+
* Use this incombination with the `isoMode` prop to submit ISO-8601
|
|
15
|
+
* formatted input values
|
|
16
|
+
*/
|
|
17
|
+
defaultValue?: Date;
|
|
7
18
|
name?: string;
|
|
8
19
|
startDate?: Date;
|
|
9
20
|
endDate?: Date;
|
|
10
21
|
minDate?: Date;
|
|
11
22
|
maxDate?: Date;
|
|
23
|
+
/**
|
|
24
|
+
* Turn this on to generate a form <input/> that contains (and submits)
|
|
25
|
+
* an ISO-date formatted string value, instead of the default "human
|
|
26
|
+
* readable" format.
|
|
27
|
+
*
|
|
28
|
+
* NOTE: This will be the default mode in v0.11.
|
|
29
|
+
*/
|
|
30
|
+
isoMode?: boolean;
|
|
12
31
|
localeCode?: 'is' | 'en' | 'pl';
|
|
13
32
|
dateFormat?: string | Array<string>;
|
|
14
33
|
isStartDate?: boolean;
|
|
15
34
|
isEndDate?: boolean;
|
|
16
35
|
inputRef?: RefObject<HTMLInputElement>;
|
|
17
|
-
onChange
|
|
36
|
+
onChange?: (date?: Date) => void;
|
|
18
37
|
datepickerExtraProps?: Record<string, unknown>;
|
|
19
|
-
/** @deprecated
|
|
38
|
+
/** @deprecated Use `value` or `defaultValue` instead. (Will be removed in v0.11) */
|
|
20
39
|
initialDate?: Date;
|
|
21
40
|
} & FormFieldWrappingProps;
|
|
22
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Dumb utility function that returns a new Date that's `dayOffset` days away
|
|
43
|
+
* from the input `date`.
|
|
44
|
+
*/
|
|
45
|
+
export declare const getDateDiff: (refDate: Date, dayOffset: number) => Date;
|
|
23
46
|
export type DatepickerLocaleProps = {
|
|
24
47
|
nextMonthAriaLabel: string;
|
|
25
48
|
nextMonthButtonLabel: string;
|
|
@@ -36,5 +59,10 @@ export type DatepickerLocaleProps = {
|
|
|
36
59
|
chooseDayAriaLabelPrefix: string;
|
|
37
60
|
disabledDayAriaLabelPrefix: string;
|
|
38
61
|
};
|
|
62
|
+
/**
|
|
63
|
+
* A compo
|
|
64
|
+
*
|
|
65
|
+
* Internally, this component uses the [`react-datepicker`](https://reactdatepicker.com/) component.
|
|
66
|
+
*/
|
|
39
67
|
export declare const Datepicker: (props: DatepickerProps) => JSX.Element;
|
|
40
68
|
export default Datepicker;
|
package/Datepicker.js
CHANGED
|
@@ -10,11 +10,16 @@ const index_js_1 = tslib_1.__importDefault(require("date-fns/locale/is/index.js"
|
|
|
10
10
|
const index_js_2 = tslib_1.__importDefault(require("date-fns/locale/pl/index.js"));
|
|
11
11
|
const ReactDatepicker_js_1 = require("./_mixed_export_resolution_/ReactDatepicker.js"); // Docs: https://reactdatepicker.com/
|
|
12
12
|
const FormField_js_1 = require("./FormField.js");
|
|
13
|
+
const utils_js_1 = require("./utils.js");
|
|
13
14
|
(0, ReactDatepicker_js_1.registerLocale)('is', index_js_1.default);
|
|
14
15
|
(0, ReactDatepicker_js_1.registerLocale)('pl', index_js_2.default);
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Dumb utility function that returns a new Date that's `dayOffset` days away
|
|
18
|
+
* from the input `date`.
|
|
19
|
+
*/
|
|
20
|
+
const getDateDiff = (refDate, dayOffset) => {
|
|
21
|
+
const newDate = new Date(refDate);
|
|
22
|
+
newDate.setDate(newDate.getDate() + dayOffset);
|
|
18
23
|
return newDate;
|
|
19
24
|
};
|
|
20
25
|
exports.getDateDiff = getDateDiff;
|
|
@@ -52,8 +57,19 @@ const i18n = {
|
|
|
52
57
|
disabledDayAriaLabelPrefix: 'Data niedostępna',
|
|
53
58
|
},
|
|
54
59
|
};
|
|
60
|
+
/**
|
|
61
|
+
* A compo
|
|
62
|
+
*
|
|
63
|
+
* Internally, this component uses the [`react-datepicker`](https://reactdatepicker.com/) component.
|
|
64
|
+
*/
|
|
55
65
|
const Datepicker = (props) => {
|
|
56
|
-
const { className, id, label, hideLabel, assistText, disabled, readOnly, invalid, errorMessage, required, reqText, placeholder, small, localeCode = 'is', dateFormat = 'd.M.yyyy',
|
|
66
|
+
const { className, id, label, hideLabel, assistText, disabled, readOnly, invalid, errorMessage, required, reqText, placeholder, small, localeCode = 'is', dateFormat = 'd.M.yyyy', name, startDate, endDate, minDate, maxDate, isStartDate = false, isEndDate = false, onChange, datepickerExtraProps, ssr, inputRef, isoMode, } = props;
|
|
67
|
+
const [value, setValue] = utils_js_1.useMixedControlState.raw(props.value || props.initialDate, props.defaultValue, 'value');
|
|
68
|
+
/*
|
|
69
|
+
TODO: Revert to this simpler pattern once we hit v0.11
|
|
70
|
+
and `props.initialDate` is removed:
|
|
71
|
+
*/
|
|
72
|
+
// const [value, setValue] = useMixedControlState(props, 'value');
|
|
57
73
|
const domid = (0, hooks_1.useDomid)(id);
|
|
58
74
|
const txts = i18n[localeCode] || {};
|
|
59
75
|
const filled = !!value;
|
|
@@ -65,7 +81,8 @@ const Datepicker = (props) => {
|
|
|
65
81
|
(elm === null || elm === void 0 ? void 0 : elm.querySelector('input')) || undefined;
|
|
66
82
|
return elm;
|
|
67
83
|
}) }, addFocusProps()),
|
|
68
|
-
react_1.default.createElement(
|
|
84
|
+
isoMode && (react_1.default.createElement("input", { type: "hidden", name: name, value: value === null || value === void 0 ? void 0 : value.toISOString().slice(0, 10) })),
|
|
85
|
+
react_1.default.createElement(ReactDatepicker_js_1.ReactDatePicker, Object.assign({ id: domid, required: inputProps.required, disabled: inputProps.disabled, readOnly: inputProps.readOnly, selected: value, name: isoMode ? undefined : name, locale: localeCode, dateFormat:
|
|
69
86
|
// NOTE: Force all dateFormat values into Array<string> to temporarily work around
|
|
70
87
|
// a bug in the current version of react-datepicker where invalid **string** values
|
|
71
88
|
// are re-parsed with `new Date()`, causing surprising (weird) false positives
|
|
@@ -76,7 +93,9 @@ const Datepicker = (props) => {
|
|
|
76
93
|
typeof dateFormat === 'string'
|
|
77
94
|
? [dateFormat]
|
|
78
95
|
: dateFormat.slice(0).reverse(), onChange: (date) => {
|
|
79
|
-
|
|
96
|
+
date = date || undefined;
|
|
97
|
+
setValue(date);
|
|
98
|
+
onChange && onChange(date);
|
|
80
99
|
const inputElm = inputRef === null || inputRef === void 0 ? void 0 : inputRef.current;
|
|
81
100
|
if (inputElm) {
|
|
82
101
|
inputElm.dispatchEvent(new Event('change', { bubbles: true }));
|
package/FormField.d.ts
CHANGED
|
@@ -24,6 +24,10 @@ type FocusEvents = {
|
|
|
24
24
|
onBlur?: (e: any) => void;
|
|
25
25
|
};
|
|
26
26
|
type FocusPropMaker = <P extends FocusEvents>(ownProps?: P) => P & Required<FocusEvents>;
|
|
27
|
+
/**
|
|
28
|
+
* Mixin base props type for components using the `FormField` to contain
|
|
29
|
+
* simple, singular input widgets. E.g. `TextInput`, `Seleccbox`, etc.
|
|
30
|
+
*/
|
|
27
31
|
export type FormFieldWrappingProps = {
|
|
28
32
|
/** Container className - alongside "FormField" */
|
|
29
33
|
className?: string;
|
|
@@ -49,14 +53,19 @@ export type FormFieldWrappingProps = {
|
|
|
49
53
|
wrapperRef?: RefObject<HTMLElement>;
|
|
50
54
|
ssr?: SSRSupport;
|
|
51
55
|
};
|
|
56
|
+
/**
|
|
57
|
+
* Mixin base props type for components using `FormField` to contain
|
|
58
|
+
* more complex multi-element, grouped choices, that require a Heading
|
|
59
|
+
* E.g. `RadioGroup`, `CheckboxGroup`, etc.
|
|
60
|
+
*/
|
|
52
61
|
export type FormFieldGroupWrappingProps = FormFieldWrappingProps & {
|
|
53
62
|
LabelTag?: 'h3' | 'h4' | 'h5';
|
|
54
63
|
};
|
|
55
|
-
|
|
64
|
+
type FormFieldProps = FormFieldGroupWrappingProps & {
|
|
56
65
|
/** Container className - alongside "FormField" */
|
|
57
66
|
className: string;
|
|
58
67
|
small?: boolean;
|
|
59
|
-
group?: boolean;
|
|
68
|
+
group?: boolean | 'inputlike';
|
|
60
69
|
empty?: boolean;
|
|
61
70
|
filled?: boolean;
|
|
62
71
|
renderInput(className: InputClassNames, inputProps: FormFieldInputProps, addFocusProps: FocusPropMaker, isBrowser?: boolean): JSX.Element;
|
package/FormField.js
CHANGED
|
@@ -52,12 +52,12 @@ const FormField = (props) => {
|
|
|
52
52
|
}
|
|
53
53
|
return focusProps;
|
|
54
54
|
}, []);
|
|
55
|
-
const errorId = errorMessage ?
|
|
56
|
-
const assistTextId = assistText ?
|
|
57
|
-
const labelId = LabelTag ?
|
|
55
|
+
const errorId = errorMessage ? `error:${domid}` : undefined;
|
|
56
|
+
const assistTextId = assistText ? `assist:${domid}` : undefined;
|
|
57
|
+
const labelId = LabelTag ? `label:${domid}` : undefined;
|
|
58
58
|
const reqStar = required && reqText !== false && (react_1.default.createElement("abbr", { className: "FormField__label__reqstar",
|
|
59
59
|
// TODO: add mo-better i18n thinking
|
|
60
|
-
title:
|
|
60
|
+
title: `${reqText || 'Þarf að fylla út'}: ` }, "*"));
|
|
61
61
|
const inputProps = {
|
|
62
62
|
id: domid,
|
|
63
63
|
disabled: disabled,
|
|
@@ -77,7 +77,7 @@ const FormField = (props) => {
|
|
|
77
77
|
isBrowser && filled && 'filled',
|
|
78
78
|
isBrowser && focused && 'focused',
|
|
79
79
|
], className), ref: props.wrapperRef },
|
|
80
|
-
LabelTag ? (react_1.default.createElement(LabelTag, { className: "FormField__label", id: labelId },
|
|
80
|
+
LabelTag ? (react_1.default.createElement(LabelTag, { className: "FormField__label", "data-inputlabel": group === 'inputlike' || undefined, id: labelId },
|
|
81
81
|
' ',
|
|
82
82
|
reqStar,
|
|
83
83
|
" ",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RefObject } from 'react';
|
|
1
|
+
import React, { RefObject } from 'react';
|
|
2
2
|
export type PrimaryPanelI18n = {
|
|
3
3
|
backToMenu: string;
|
|
4
4
|
backToMenuLong?: string;
|
|
@@ -9,7 +9,7 @@ export type MegaMenuItem = {
|
|
|
9
9
|
href: string;
|
|
10
10
|
lang?: string;
|
|
11
11
|
current?: boolean;
|
|
12
|
-
target?:
|
|
12
|
+
target?: React.HTMLAttributeAnchorTarget;
|
|
13
13
|
};
|
|
14
14
|
export type MegaMenuPanel = {
|
|
15
15
|
title: string;
|
package/MainMenu.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import React from 'react';
|
|
1
2
|
import { Cleanup } from '@reykjavik/hanna-utils';
|
|
2
3
|
import { DefaultTexts } from '@reykjavik/hanna-utils/i18n';
|
|
3
4
|
import { AuxilaryPanelIllustration, AuxiliaryPanelProps } from './MainMenu/_Auxiliary.js';
|
|
@@ -34,7 +35,7 @@ export type MainMenuItem = {
|
|
|
34
35
|
*/
|
|
35
36
|
onClick?: (index: number, item: MainMenuItem) => void | boolean;
|
|
36
37
|
controlsId?: string;
|
|
37
|
-
target?:
|
|
38
|
+
target?: React.HTMLAttributeAnchorTarget;
|
|
38
39
|
};
|
|
39
40
|
export type MainMenuSeparator = '---';
|
|
40
41
|
export type MainMenuItemList = Array<MainMenuItem | MainMenuSeparator>;
|
package/MainMenu.js
CHANGED
|
@@ -6,7 +6,6 @@ const react_1 = tslib_1.__importStar(require("react"));
|
|
|
6
6
|
const focusElm_1 = require("@hugsmidjan/qj/focusElm");
|
|
7
7
|
const useShortState_1 = tslib_1.__importDefault(require("@hugsmidjan/react/hooks/useShortState"));
|
|
8
8
|
const getBemClass_1 = tslib_1.__importDefault(require("@hugsmidjan/react/utils/getBemClass"));
|
|
9
|
-
const hanna_utils_1 = require("@reykjavik/hanna-utils");
|
|
10
9
|
const i18n_1 = require("@reykjavik/hanna-utils/i18n");
|
|
11
10
|
const _Link_js_1 = require("./_abstract/_Link.js");
|
|
12
11
|
const _Auxiliary_js_1 = require("./MainMenu/_Auxiliary.js");
|
|
@@ -98,20 +97,20 @@ const MainMenu = (props) => {
|
|
|
98
97
|
const [laggyActivePanel, setLaggyActivePanel] = (0, useShortState_1.default)();
|
|
99
98
|
const setActivePanel = (0, react_1.useMemo)(() => isBrowser
|
|
100
99
|
? (newActive, setFocus = true) => {
|
|
101
|
-
const
|
|
100
|
+
const htmlElm = document.documentElement;
|
|
101
|
+
const htmlElmDataset = htmlElm.dataset;
|
|
102
102
|
// const menuElm = menuElmRef.current as HTMLElement;
|
|
103
103
|
_setActivePanel((activePanel) => {
|
|
104
|
-
const scrollElm = (0, hanna_utils_1.getPageScrollElm)();
|
|
105
104
|
if (!newActive) {
|
|
106
105
|
activePanel && setLaggyActivePanel(activePanel, 1000);
|
|
107
|
-
|
|
106
|
+
htmlElm.scrollTop = parseInt(htmlElmDataset.scrollTop || '') || 0;
|
|
108
107
|
delete htmlElmDataset.scrollTop;
|
|
109
108
|
delete htmlElmDataset.megaPanelActive;
|
|
110
109
|
}
|
|
111
110
|
else {
|
|
112
111
|
setLaggyActivePanel(undefined, 0);
|
|
113
|
-
htmlElmDataset.scrollTop = String(
|
|
114
|
-
|
|
112
|
+
htmlElmDataset.scrollTop = String(htmlElm.scrollTop);
|
|
113
|
+
htmlElm.scrollTop = 0;
|
|
115
114
|
htmlElmDataset.megaPanelActive = '';
|
|
116
115
|
}
|
|
117
116
|
if (setFocus) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { TogglerGroupOptions } from '../_abstract/_TogglerGroup.js';
|
|
2
|
+
import { TogglerGroupFieldOption } from '../_abstract/_TogglerGroupField.js';
|
|
3
|
+
export declare const _weights: {
|
|
4
|
+
WHOLE_WORD: number;
|
|
5
|
+
STARTS_WITH: number;
|
|
6
|
+
CONTAINS: number;
|
|
7
|
+
VALUE_WEIGHT: number;
|
|
8
|
+
wordWeight: (wordIndex: number) => number;
|
|
9
|
+
};
|
|
10
|
+
export type SearchScoringfn = (
|
|
11
|
+
/** The Multiselect item object to calculate search score for */
|
|
12
|
+
item: TogglerGroupFieldOption<string>,
|
|
13
|
+
/** Trimmed list of `toLowerCase`d query words */
|
|
14
|
+
queryWords: Array<string>,
|
|
15
|
+
/** The raw, untouched search query as typed by the user */
|
|
16
|
+
rawQuery: string) => number;
|
|
17
|
+
export declare const defaultSearchScoring: SearchScoringfn;
|
|
18
|
+
/** Returns a normalized, filtered list of options */
|
|
19
|
+
export declare const filterItems: (options: TogglerGroupOptions<string>, searchQuery: string, searchScoringFn?: SearchScoringfn) => TogglerGroupOptions<string>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.filterItems = exports.defaultSearchScoring = exports._weights = void 0;
|
|
4
|
+
const WHOLE_WORD = 10000;
|
|
5
|
+
const STARTS_WITH = 100;
|
|
6
|
+
const CONTAINS = 1;
|
|
7
|
+
const VALUE_WEIGHT = 1 / 10;
|
|
8
|
+
/** Scoring weight modifier based on a word's positional index within the value */
|
|
9
|
+
const wordWeight = (wordIndex) => 10 / (10 + wordIndex);
|
|
10
|
+
// Exported for testing purposes
|
|
11
|
+
exports._weights = {
|
|
12
|
+
WHOLE_WORD,
|
|
13
|
+
STARTS_WITH,
|
|
14
|
+
CONTAINS,
|
|
15
|
+
VALUE_WEIGHT,
|
|
16
|
+
wordWeight,
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Calculates a score based on how well an item string (either label or value)
|
|
20
|
+
* matches a given list of query words.
|
|
21
|
+
* Splits the item string into words and scores each word.
|
|
22
|
+
* Favors full matches and matches at the start of the word.
|
|
23
|
+
* Weighs earlier words higher than words near the end.
|
|
24
|
+
*
|
|
25
|
+
* Limitation: Does currently not give extra points when query words
|
|
26
|
+
* appear in the correct order in the item string.
|
|
27
|
+
*/
|
|
28
|
+
const calcScore = (itemString, queryWords) => {
|
|
29
|
+
let score = 0;
|
|
30
|
+
queryWords.forEach((queryWord) => {
|
|
31
|
+
itemString.split(/\s+/).forEach((word, wi) => {
|
|
32
|
+
let wordScore = 0;
|
|
33
|
+
if (word === queryWord) {
|
|
34
|
+
wordScore += WHOLE_WORD;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
const pos = word.indexOf(queryWord);
|
|
38
|
+
if (pos === 0) {
|
|
39
|
+
wordScore += STARTS_WITH;
|
|
40
|
+
}
|
|
41
|
+
else if (pos > 0) {
|
|
42
|
+
wordScore += CONTAINS;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
score += wordScore * wordWeight(wi);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
return score;
|
|
49
|
+
};
|
|
50
|
+
const defaultSearchScoring = (item, queryWords) => {
|
|
51
|
+
var _a;
|
|
52
|
+
if (!item.value) {
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
const value = item.value.toLowerCase().trim();
|
|
56
|
+
const label = ((_a = item.label) === null || _a === void 0 ? void 0 : _a.toLowerCase().trim()) || value;
|
|
57
|
+
let score = calcScore(label, queryWords);
|
|
58
|
+
if (!score) {
|
|
59
|
+
score = VALUE_WEIGHT * calcScore(value, queryWords);
|
|
60
|
+
}
|
|
61
|
+
return score;
|
|
62
|
+
};
|
|
63
|
+
exports.defaultSearchScoring = defaultSearchScoring;
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
/** Returns a normalized, filtered list of options */
|
|
66
|
+
const filterItems = (options, searchQuery, searchScoringFn = exports.defaultSearchScoring) => {
|
|
67
|
+
if (!searchQuery.trim()) {
|
|
68
|
+
return [...options];
|
|
69
|
+
}
|
|
70
|
+
const queryWords = searchQuery.toLowerCase().trim().split(/\s+/);
|
|
71
|
+
return options
|
|
72
|
+
.map((item) => ({
|
|
73
|
+
item,
|
|
74
|
+
score: searchScoringFn(item, queryWords, searchQuery),
|
|
75
|
+
}))
|
|
76
|
+
.filter(({ score }) => score > 0)
|
|
77
|
+
.sort((a, b) => (a.score === b.score ? 0 : a.score < b.score ? 1 : -1))
|
|
78
|
+
.map(({ item }) => item);
|
|
79
|
+
};
|
|
80
|
+
exports.filterItems = filterItems;
|
package/Multiselect.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { TogglerGroupFieldProps } from './_abstract/_TogglerGroupField.js';
|
|
2
|
+
import { SearchScoringfn } from './Multiselect/_Multiselect.search.js';
|
|
3
|
+
export type MultiselectI18n = {
|
|
4
|
+
search: string;
|
|
5
|
+
buttonShow: string;
|
|
6
|
+
currentValues: string;
|
|
7
|
+
noneFoundMsg: string;
|
|
8
|
+
};
|
|
9
|
+
export type MultiselectProps = TogglerGroupFieldProps<string> & {
|
|
10
|
+
value?: Array<string>;
|
|
11
|
+
defaultValue?: Array<string>;
|
|
12
|
+
placeholder?: string;
|
|
13
|
+
small?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Prevent the selected items from wrapping into multiple lines.
|
|
16
|
+
* Use this option when vertical space is limited.
|
|
17
|
+
*/
|
|
18
|
+
nowrap?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Custom function to calculate a search score for a given option item.
|
|
21
|
+
* Higher scores mean better matches.
|
|
22
|
+
*
|
|
23
|
+
* A score of zero (or less) means the item is not a valid match.
|
|
24
|
+
*/
|
|
25
|
+
searchScoring?: SearchScoringfn;
|
|
26
|
+
/**
|
|
27
|
+
* Force display the current values at the top of the dropdown,
|
|
28
|
+
* even when the total options are fewer than
|
|
29
|
+
* `Multiselect.meta.summaryLimit`.
|
|
30
|
+
*
|
|
31
|
+
* NOTE: Using this option is generally not recommended.
|
|
32
|
+
*/
|
|
33
|
+
forceSummary?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Force the options to be searchable, even when they're
|
|
36
|
+
* fewer than `Multiselect.meta.searchableLimit`.
|
|
37
|
+
*
|
|
38
|
+
* NOTE: Using this option is generally not recommended.
|
|
39
|
+
*/
|
|
40
|
+
forceSearchable?: boolean;
|
|
41
|
+
texts?: MultiselectI18n;
|
|
42
|
+
lang?: string;
|
|
43
|
+
};
|
|
44
|
+
export type MultiSelectOption = Exclude<MultiselectProps['options'][number], string>;
|
|
45
|
+
export declare const Multiselect: {
|
|
46
|
+
(props: MultiselectProps): JSX.Element;
|
|
47
|
+
/** Configuration constants for the Multiselect components */
|
|
48
|
+
meta: {
|
|
49
|
+
/**
|
|
50
|
+
* The item-count where the list becomes searchable.
|
|
51
|
+
*
|
|
52
|
+
* (The search UI, including the on-screen keyboard, takes up a lot of space
|
|
53
|
+
* on mobile devices, so there's a balance that we want to strike.)
|
|
54
|
+
*/
|
|
55
|
+
searchableLimit: number;
|
|
56
|
+
/**
|
|
57
|
+
* The item-count above which we display a summary of "current" values
|
|
58
|
+
* at the top of the drop-down list.
|
|
59
|
+
*
|
|
60
|
+
* (This summary just gets in the way with ultra short option lists.)
|
|
61
|
+
*/
|
|
62
|
+
summaryLimit: number;
|
|
63
|
+
};
|
|
64
|
+
};
|