@reykjavik/hanna-react 0.10.92 → 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.
Files changed (62) hide show
  1. package/ArticleCarousel/_ArticleCarouselCard.d.ts +2 -1
  2. package/CHANGELOG.md +16 -0
  3. package/ContactBubble.d.ts +2 -1
  4. package/ContactBubble.js +4 -6
  5. package/Datepicker.d.ts +31 -3
  6. package/Datepicker.js +25 -6
  7. package/FormField.d.ts +11 -2
  8. package/FormField.js +5 -5
  9. package/MainMenu/_PrimaryPanel.d.ts +2 -2
  10. package/MainMenu.d.ts +2 -1
  11. package/MainMenu.js +5 -6
  12. package/Multiselect/_Multiselect.search.d.ts +19 -0
  13. package/Multiselect/_Multiselect.search.js +80 -0
  14. package/Multiselect.d.ts +64 -0
  15. package/Multiselect.js +236 -0
  16. package/RelatedLinks.d.ts +2 -1
  17. package/Selectbox.d.ts +3 -3
  18. package/TextInput.d.ts +0 -1
  19. package/_abstract/_CardList.d.ts +2 -1
  20. package/_abstract/_CardList.js +2 -2
  21. package/_abstract/_FocusTrap.d.ts +14 -0
  22. package/_abstract/_FocusTrap.js +24 -0
  23. package/_abstract/_TogglerGroup.d.ts +11 -7
  24. package/_abstract/_TogglerGroup.js +11 -3
  25. package/_abstract/_TogglerGroupField.d.ts +4 -4
  26. package/_abstract/_TogglerGroupField.js +2 -2
  27. package/_abstract/_TogglerInput.d.ts +3 -1
  28. package/_abstract/_TogglerInput.js +7 -4
  29. package/esm/ArticleCarousel/_ArticleCarouselCard.d.ts +2 -1
  30. package/esm/ContactBubble.d.ts +2 -1
  31. package/esm/ContactBubble.js +4 -6
  32. package/esm/Datepicker.d.ts +31 -3
  33. package/esm/Datepicker.js +25 -6
  34. package/esm/FormField.d.ts +11 -2
  35. package/esm/FormField.js +5 -5
  36. package/esm/MainMenu/_PrimaryPanel.d.ts +2 -2
  37. package/esm/MainMenu.d.ts +2 -1
  38. package/esm/MainMenu.js +5 -6
  39. package/esm/Multiselect/_Multiselect.search.d.ts +19 -0
  40. package/esm/Multiselect/_Multiselect.search.js +75 -0
  41. package/esm/Multiselect.d.ts +64 -0
  42. package/esm/Multiselect.js +231 -0
  43. package/esm/RelatedLinks.d.ts +2 -1
  44. package/esm/Selectbox.d.ts +3 -3
  45. package/esm/TextInput.d.ts +0 -1
  46. package/esm/_abstract/_CardList.d.ts +2 -1
  47. package/esm/_abstract/_CardList.js +2 -2
  48. package/esm/_abstract/_FocusTrap.d.ts +14 -0
  49. package/esm/_abstract/_FocusTrap.js +19 -0
  50. package/esm/_abstract/_TogglerGroup.d.ts +11 -7
  51. package/esm/_abstract/_TogglerGroup.js +11 -3
  52. package/esm/_abstract/_TogglerGroupField.d.ts +4 -4
  53. package/esm/_abstract/_TogglerGroupField.js +2 -2
  54. package/esm/_abstract/_TogglerInput.d.ts +3 -1
  55. package/esm/_abstract/_TogglerInput.js +7 -4
  56. package/esm/index.d.ts +1 -0
  57. package/esm/utils/useFormatMonitor.d.ts +4 -2
  58. package/esm/utils/useFormatMonitor.js +4 -2
  59. package/index.d.ts +1 -0
  60. package/package.json +7 -3
  61. package/utils/useFormatMonitor.d.ts +4 -2
  62. package/utils/useFormatMonitor.js +4 -2
package/Multiselect.js ADDED
@@ -0,0 +1,236 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Multiselect = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const react_1 = tslib_1.__importStar(require("react"));
6
+ const domid_1 = tslib_1.__importDefault(require("@hugsmidjan/qj/domid"));
7
+ const hooks_1 = require("@hugsmidjan/react/hooks");
8
+ const getBemClass_1 = tslib_1.__importDefault(require("@hugsmidjan/react/utils/getBemClass"));
9
+ const hanna_utils_1 = require("@reykjavik/hanna-utils");
10
+ const i18n_1 = require("@reykjavik/hanna-utils/i18n");
11
+ const _FocusTrap_js_1 = require("./_abstract/_FocusTrap.js");
12
+ const _Multiselect_search_js_1 = require("./Multiselect/_Multiselect.search.js");
13
+ const Checkbox_js_1 = tslib_1.__importDefault(require("./Checkbox.js"));
14
+ const FormField_js_1 = tslib_1.__importDefault(require("./FormField.js"));
15
+ const TagPill_js_1 = tslib_1.__importDefault(require("./TagPill.js"));
16
+ const utils_js_1 = require("./utils.js");
17
+ const metaData = {
18
+ /**
19
+ * The item-count where the list becomes searchable.
20
+ *
21
+ * (The search UI, including the on-screen keyboard, takes up a lot of space
22
+ * on mobile devices, so there's a balance that we want to strike.)
23
+ */
24
+ searchableLimit: 20,
25
+ /**
26
+ * The item-count above which we display a summary of "current" values
27
+ * at the top of the drop-down list.
28
+ *
29
+ * (This summary just gets in the way with ultra short option lists.)
30
+ */
31
+ summaryLimit: 10,
32
+ };
33
+ const { searchableLimit, summaryLimit } = metaData;
34
+ const defaultTexts = {
35
+ pl: {
36
+ search: 'Wyszukaj opcje',
37
+ buttonShow: 'Pokaż opcje',
38
+ // buttonHide: 'Ukryj opcje',
39
+ currentValues: 'Wybrane wartości',
40
+ noneFoundMsg: 'Brak dopasowań',
41
+ },
42
+ en: {
43
+ search: 'Search options',
44
+ buttonShow: 'Show options',
45
+ // buttonHide: 'Hide options',
46
+ currentValues: 'Currently selected',
47
+ noneFoundMsg: 'No matches',
48
+ },
49
+ is: {
50
+ search: 'Leita í valkostum',
51
+ buttonShow: 'Birta valkosti',
52
+ // buttonHide: 'Fela valkosti',
53
+ currentValues: 'Valin gildi',
54
+ noneFoundMsg: 'Ekkert passar',
55
+ },
56
+ };
57
+ const Multiselect = (props) => {
58
+ const { onSelected, options: _options, disabled: _disabled, readOnly } = props;
59
+ const disabled = _disabled === true;
60
+ const disableds = !disabled && _disabled;
61
+ const name = (0, hooks_1.useDomid)(props.name);
62
+ const [values, setValues] = (0, utils_js_1.useMixedControlState)(props, 'value', []);
63
+ const filled = values.length > 0;
64
+ const empty = !filled && !props.placeholder;
65
+ const placeholderText = !values.length ? props.placeholder : undefined;
66
+ const texts = (0, i18n_1.getTexts)(props, defaultTexts);
67
+ const inputRef = (0, react_1.useRef)(null);
68
+ const wrapperRef = (0, react_1.useRef)(null);
69
+ const [activeItemIndex, setActiveItemIndex] = (0, react_1.useState)(-1);
70
+ const [searchQuery, setSearchQuery] = (0, react_1.useState)('');
71
+ const [isOpen, setIsOpen] = (0, react_1.useState)(false);
72
+ const toggleOpen = (newIsOpen) => {
73
+ setIsOpen((isOpen) => {
74
+ newIsOpen = typeof newIsOpen === 'boolean' ? newIsOpen : !isOpen;
75
+ if (!newIsOpen) {
76
+ wrapperRef.current.querySelector('.Multiselect__choices').scrollTo(0, 0);
77
+ setSearchQuery('');
78
+ setActiveItemIndex(-1);
79
+ }
80
+ return newIsOpen;
81
+ });
82
+ };
83
+ (0, hooks_1.useOnClickOutside)(wrapperRef, () => toggleOpen(false));
84
+ const options = (0, react_1.useMemo)(() => _options.map((item) => (typeof item === 'string' ? { value: item } : item)), [_options]);
85
+ const isSearchable = props.forceSearchable || options.length >= searchableLimit;
86
+ /*
87
+ NOTE: he `.MultiSelect__currentvalues` should only be visible when
88
+ there are some items selected, and multiselect is either collapsed,
89
+ or the dropdown has reached `summaryLimit` number of items.
90
+ (For fewer items, the "summary" is just in the way.)
91
+ The `forceSummary` prop overrides this default.
92
+ */
93
+ const showCurrentValues = values.length > 0 &&
94
+ (props.forceSummary || !isOpen || options.length >= summaryLimit);
95
+ const filteredOptions = (0, react_1.useMemo)(() => (0, _Multiselect_search_js_1.filterItems)(options, searchQuery, props.searchScoring), [searchQuery, options, props.searchScoring]);
96
+ const handleCheckboxSelection = (0, react_1.useCallback)((selectedItem) => {
97
+ const selValue = selectedItem.value;
98
+ const isAdding = values.indexOf(selValue) === -1;
99
+ const _newValues = isAdding
100
+ ? [...values, selValue]
101
+ : values.filter((value) => value !== selValue);
102
+ const selectedValues = options
103
+ .filter((item) => _newValues.includes(item.value))
104
+ .map((item) => item.value);
105
+ setValues(selectedValues);
106
+ if (onSelected) {
107
+ onSelected({
108
+ value: selectedItem.value,
109
+ checked: isAdding,
110
+ option: selectedItem,
111
+ selectedValues,
112
+ });
113
+ }
114
+ }, [values, options, onSelected, setValues]);
115
+ const handleInputChange = (event) => {
116
+ const val = event.target.value;
117
+ const fixVal = val === ' ' ? '' : val;
118
+ setSearchQuery(fixVal);
119
+ toggleOpen(true);
120
+ };
121
+ const handleInputKeyDown = (event) => {
122
+ if (searchQuery.length === 0 && [' ', 'Delete', 'Backspace'].includes(event.key)) {
123
+ // setSearchQuery('');
124
+ toggleOpen(activeItemIndex > -1 ? true : !isOpen);
125
+ }
126
+ };
127
+ // When the dropdown is open, add keydown handlers
128
+ (0, react_1.useEffect)(() => {
129
+ if (!isOpen) {
130
+ return;
131
+ }
132
+ const handleKeyDown = (e) => {
133
+ const inputElm = inputRef.current;
134
+ if (e.key === 'ArrowUp') {
135
+ e.preventDefault();
136
+ inputElm.focus();
137
+ setActiveItemIndex((prevIndex) => prevIndex === 0 ? filteredOptions.length - 1 : prevIndex - 1);
138
+ }
139
+ else if (e.key === 'ArrowDown') {
140
+ e.preventDefault();
141
+ inputElm.focus();
142
+ setActiveItemIndex((prevIndex) => prevIndex === filteredOptions.length - 1 ? 0 : prevIndex + 1);
143
+ }
144
+ else if (e.key === 'Escape') {
145
+ e.preventDefault();
146
+ inputElm.blur();
147
+ inputElm.focus();
148
+ toggleOpen(false);
149
+ }
150
+ else if (e.key === 'Enter' || e.key === ' ') {
151
+ if (e.target.closest('.MultiSelect__currentvalues')) {
152
+ return;
153
+ }
154
+ const focusInRange = activeItemIndex >= 0 && activeItemIndex < filteredOptions.length;
155
+ if (focusInRange) {
156
+ e.preventDefault();
157
+ const selItem = filteredOptions[activeItemIndex];
158
+ if (selItem) {
159
+ handleCheckboxSelection(selItem);
160
+ }
161
+ }
162
+ }
163
+ };
164
+ document.addEventListener('keydown', handleKeyDown);
165
+ return () => {
166
+ document.removeEventListener('keydown', handleKeyDown);
167
+ };
168
+ }, [activeItemIndex, filteredOptions, isOpen, handleCheckboxSelection, inputRef]);
169
+ // Auto-close the dropdown when focus has left the building
170
+ (0, react_1.useEffect)(() => {
171
+ const wrapperDiv = wrapperRef.current;
172
+ if (!wrapperDiv) {
173
+ return;
174
+ }
175
+ let closing;
176
+ const cancelClose = () => clearTimeout(closing);
177
+ const closeDropdown = () => {
178
+ closing = setTimeout(() => toggleOpen(false), 200);
179
+ };
180
+ wrapperDiv.addEventListener('focusin', cancelClose);
181
+ wrapperDiv.addEventListener('focusout', closeDropdown);
182
+ return () => {
183
+ wrapperDiv.removeEventListener('focusin', cancelClose);
184
+ wrapperDiv.removeEventListener('focusout', closeDropdown);
185
+ };
186
+ }, []);
187
+ (0, react_1.useEffect)(() => {
188
+ var _a, _b;
189
+ (_b = (_a = wrapperRef.current) === null || _a === void 0 ? void 0 : _a.querySelectorAll('.Multiselect__option')[activeItemIndex]) === null || _b === void 0 ? void 0 : _b.scrollIntoView({
190
+ behavior: 'smooth',
191
+ block: 'nearest',
192
+ });
193
+ }, [activeItemIndex]);
194
+ return (react_1.default.createElement(FormField_js_1.default, { className: (0, getBemClass_1.default)('Multiselect', props.nowrap && 'nowrap', props.className), ssr: props.ssr, group: "inputlike", label: props.label, LabelTag: props.LabelTag, hideLabel: props.hideLabel, small: props.small, filled: filled, empty: empty, disabled: disabled, invalid: props.invalid, errorMessage: props.errorMessage, assistText: props.assistText, readOnly: readOnly, required: props.required, reqText: props.reqText, id: props.id, renderInput: (className, inputProps, addFocusProps, isBrowser) => {
195
+ const { id } = inputProps;
196
+ return (react_1.default.createElement("div", Object.assign({ className: (0, getBemClass_1.default)('Multiselect__input', [isOpen && 'open'], className.input) }, addFocusProps(), { "data-sprinkled": isBrowser, ref: wrapperRef }),
197
+ !isBrowser ? null : isSearchable ? (react_1.default.createElement("input", { className: "Multiselect__search", id: `toggler:${id}`, "aria-label": texts.search, "aria-controls": (0, domid_1.default)(), "data-expanded": isOpen || undefined, onChange: handleInputChange, onKeyDown: handleInputKeyDown, onClick: () => toggleOpen(), value: searchQuery,
198
+ // onFocus={handleInputFocus}
199
+ 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": (0, domid_1.default)(), "aria-expanded": isOpen, onClick: () => toggleOpen(), disabled: disabled,
200
+ // Seems like an innocent hack for visible "placeholder" value.
201
+ // For scren-readers aria-label should take precedence.
202
+ ref: inputRef }, placeholderText || ' ')),
203
+ react_1.default.createElement("div", { className: "Multiselect__choices", tabIndex: -1 },
204
+ isBrowser && showCurrentValues && (react_1.default.createElement("div", { className: "Multiselect__currentvalues", onClick: isOpen || disabled
205
+ ? undefined
206
+ : () => {
207
+ var _a;
208
+ toggleOpen();
209
+ (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
210
+ }, "aria-label": `${texts.currentValues}:` }, values
211
+ .map((value) => options.find((opt) => opt.value === value))
212
+ .filter(hanna_utils_1.notNully)
213
+ .map((item, idx) => (react_1.default.createElement(TagPill_js_1.default, Object.assign({ key: idx, large: true, label: item.label || item.value }, (isOpen && !readOnly
214
+ ? {
215
+ removable: true,
216
+ onRemove: () => {
217
+ handleCheckboxSelection(item);
218
+ },
219
+ }
220
+ : { removable: false }))))))),
221
+ react_1.default.createElement("ul", { id: id, className: "Multiselect__options", "aria-expanded": isBrowser ? isOpen : undefined, hidden: isBrowser && !isOpen, role: "group", "aria-labelledby": inputProps['aria-labelledby'], "aria-describedby": inputProps['aria-describedby'], "aria-required": props.required },
222
+ filteredOptions.length ? (filteredOptions.map((item, idx) => {
223
+ const isDisabled = item.disabled != null
224
+ ? item.disabled
225
+ : disableds && disableds.includes(idx);
226
+ const isChecked = values.includes(item.value);
227
+ return (react_1.default.createElement(Checkbox_js_1.default, Object.assign({ key: idx, className: (0, getBemClass_1.default)('Multiselect__option', activeItemIndex === idx && 'focused'), disabled: isDisabled, readOnly: readOnly, required: props.required, Wrapper: "li", name: name }, item, { checked: isChecked, "aria-invalid": props.invalid, label: item.label || item.value, onChange: () => handleCheckboxSelection(item), onFocus: () => setActiveItemIndex(idx), wrapperProps: {
228
+ onMouseEnter: () => setActiveItemIndex(idx),
229
+ } })));
230
+ })) : searchQuery ? (react_1.default.createElement("li", { className: "Multiselect__noresults" }, texts.noneFoundMsg)) : undefined,
231
+ react_1.default.createElement(_FocusTrap_js_1.FocusTrap, { Tag: "li" })))));
232
+ } }));
233
+ };
234
+ exports.Multiselect = Multiselect;
235
+ /** Configuration constants for the Multiselect components */
236
+ exports.Multiselect.meta = metaData;
package/RelatedLinks.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import React from 'react';
1
2
  declare const types: {
2
3
  readonly external: 1;
3
4
  readonly document: 1;
@@ -8,7 +9,7 @@ export type RelatedLinkType = keyof typeof types;
8
9
  export type RelatedLinkItem = {
9
10
  href: string;
10
11
  label: string;
11
- target?: string;
12
+ target?: React.HTMLAttributeAnchorTarget;
12
13
  type?: RelatedLinkType;
13
14
  };
14
15
  export type RelatedLinksProps = {
package/Selectbox.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import type { OptionOrValue, SelectboxProps as _SelectboxProps } from '@hugsmidjan/react/Selectbox';
1
+ import type { OptionOrValue, SelectboxOptions as _SelectboxOptions, SelectboxProps as _SelectboxProps } from '@hugsmidjan/react/Selectbox';
2
2
  import { FormFieldWrappingProps } from './FormField.js';
3
- export { type SelectboxOption, type SelectboxOptions as SelectboxOptionList,
3
+ export { type SelectboxOption, type SelectboxOptions as SelectboxOptionList, } from '@hugsmidjan/react/Selectbox';
4
4
  /** @deprecated Use `SelectboxOptionList` instead (Will be removed in v0.11) */
5
- type SelectboxOptions, } from '@hugsmidjan/react/Selectbox';
5
+ export type SelectboxOptions = _SelectboxOptions;
6
6
  export type SelectboxProps<O extends OptionOrValue = OptionOrValue> = FormFieldWrappingProps & Omit<_SelectboxProps<O>, 'bem'> & {
7
7
  small?: boolean;
8
8
  };
package/TextInput.d.ts CHANGED
@@ -4,7 +4,6 @@ type InputElmProps = JSX.IntrinsicElements['input'];
4
4
  type TextareaElmProps = JSX.IntrinsicElements['textarea'];
5
5
  export type TextInputProps = {
6
6
  small?: boolean;
7
- children?: never;
8
7
  } & FormFieldWrappingProps & (({
9
8
  type?: 'text' | 'email' | 'tel' | 'number' | 'date' | 'url' | 'password' | 'search';
10
9
  inputRef?: RefObject<HTMLInputElement>;
@@ -1,4 +1,4 @@
1
- import { ReactElement, ReactNode } from 'react';
1
+ import React, { ReactElement, ReactNode } from 'react';
2
2
  import { EitherObj } from '@reykjavik/hanna-utils';
3
3
  import { ImageProps } from './_Image.js';
4
4
  type BaseCardProps = {
@@ -12,6 +12,7 @@ export type ImageCardProps = BaseCardProps & {
12
12
  };
13
13
  export type TextCardProps = BaseCardProps & {
14
14
  summary?: string;
15
+ target?: React.HTMLAttributeAnchorTarget;
15
16
  };
16
17
  export type CardListProps<T> = {
17
18
  cards: Array<T>;
@@ -6,10 +6,10 @@ const react_1 = tslib_1.__importDefault(require("react"));
6
6
  const _Button_js_1 = require("./_Button.js");
7
7
  const _Image_js_1 = require("./_Image.js");
8
8
  const Card = (props) => {
9
- const { bem, href, title, imgPlaceholder, image, meta, summary } = props;
9
+ const { bem, href, title, imgPlaceholder, image, meta, summary, target } = props;
10
10
  const cardClass = `${bem}__card`;
11
11
  return (react_1.default.createElement(react_1.default.Fragment, null,
12
- react_1.default.createElement(_Button_js_1.Button, { bem: cardClass, href: href },
12
+ react_1.default.createElement(_Button_js_1.Button, { bem: cardClass, href: href, target: target },
13
13
  ' ',
14
14
  react_1.default.createElement(_Image_js_1.Image, Object.assign({ className: `${bem}__image` }, image, { placeholder: imgPlaceholder })),
15
15
  react_1.default.createElement("span", { className: `${cardClass}__title` }, title),
@@ -0,0 +1,14 @@
1
+ export type FocusTrapProps = {
2
+ /** The HTML tag to use for the trap element. (Default `<span />`) */
3
+ Tag?: `span` | `li`;
4
+ /** Set to `true` for focus traps positioned at the top of a container. */
5
+ atTop?: boolean;
6
+ /**
7
+ * How deep the trap is placed in the DOM tree beneath its container element.
8
+ *
9
+ * Default: `1`
10
+ */
11
+ depth?: number;
12
+ };
13
+ /** A focus trap element that can be used to keep keyboard focus within a container block. */
14
+ export declare const FocusTrap: (props: FocusTrapProps) => JSX.Element;
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FocusTrap = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const react_1 = tslib_1.__importDefault(require("react"));
6
+ /** A focus trap element that can be used to keep keyboard focus within a container block. */
7
+ const FocusTrap = (props) => {
8
+ const Tag = props.Tag || 'span';
9
+ return (react_1.default.createElement(Tag, { tabIndex: 0, onFocus: (e) => {
10
+ var _a;
11
+ let container = e.currentTarget;
12
+ let depth = Math.max(props.depth || 0, 1);
13
+ while (depth-- && container) {
14
+ container = container.parentElement;
15
+ }
16
+ if (!container) {
17
+ return;
18
+ }
19
+ const focusables = container.querySelectorAll('a,input, select, textarea,button, [tabindex]');
20
+ const targetIdx = props.atTop ? focusables.length - 1 : 0;
21
+ (_a = focusables[targetIdx]) === null || _a === void 0 ? void 0 : _a.focus();
22
+ } }));
23
+ };
24
+ exports.FocusTrap = FocusTrap;
@@ -1,24 +1,28 @@
1
1
  import { FormFieldInputProps } from '../FormField.js';
2
2
  import { HTMLProps } from '../utils.js';
3
3
  import { TogglerInputProps } from './_TogglerInput.js';
4
- export type TogglerGroupOption = {
4
+ export type TogglerGroupOption<T = 'default'> = {
5
5
  value: string;
6
- label?: string | JSX.Element;
6
+ label?: T extends 'default' ? string | JSX.Element : T;
7
7
  disabled?: boolean;
8
8
  id?: string;
9
9
  };
10
- export type TogglerGroupOptions = Array<TogglerGroupOption>;
10
+ export type TogglerGroupOptions<T = 'default'> = Array<TogglerGroupOption<T>>;
11
11
  type RestrictedInputProps = Omit<HTMLProps<'input'>, 'type' | 'value' | 'defaultValue' | 'checked' | 'defaultChecked' | 'className' | 'id' | 'name' | 'children'>;
12
- export type TogglerGroupProps = {
13
- options: TogglerGroupOptions;
12
+ export type TogglerGroupProps<T = 'default'> = {
13
+ options: Array<string> | TogglerGroupOptions<T>;
14
14
  className?: string;
15
- name: string;
15
+ name?: string;
16
16
  disabled?: boolean | ReadonlyArray<number>;
17
17
  inputProps?: RestrictedInputProps;
18
18
  onSelected?: (payload: {
19
+ /** The value of being selected/updated */
19
20
  value: string;
21
+ /** The new checked state of the selected value */
20
22
  checked: boolean;
21
- option: TogglerGroupOption;
23
+ /** The option object being selected */
24
+ option: TogglerGroupOption<T>;
25
+ /** The updated value array */
22
26
  selectedValues: Array<string>;
23
27
  }) => void;
24
28
  } & Omit<FormFieldInputProps, 'disabled'>;
@@ -2,14 +2,22 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.TogglerGroup = void 0;
4
4
  const tslib_1 = require("tslib");
5
- const react_1 = tslib_1.__importDefault(require("react"));
5
+ const react_1 = tslib_1.__importStar(require("react"));
6
+ const hooks_1 = require("@hugsmidjan/react/hooks");
6
7
  const getBemClass_1 = tslib_1.__importDefault(require("@hugsmidjan/react/utils/getBemClass"));
7
8
  const utils_js_1 = require("../utils.js");
8
9
  const TogglerGroup = (props) => {
9
10
  const {
10
11
  // id,
11
- className, bem, name, disabled, Toggler, onSelected, options, isRadio, inputProps = {}, } = props;
12
+ className, bem, disabled, readOnly, Toggler, onSelected, isRadio, inputProps = {}, } = props;
12
13
  const [values, setValues] = (0, utils_js_1.useMixedControlState)(props, 'value', []);
14
+ const name = (0, hooks_1.useDomid)(props.name);
15
+ const options = (0, react_1.useMemo)(() => {
16
+ const _options = props.options;
17
+ return typeof _options[0] === 'string'
18
+ ? _options.map((option) => ({ value: option }))
19
+ : _options;
20
+ }, [props.options]);
13
21
  return (react_1.default.createElement("ul", { className: (0, getBemClass_1.default)(bem, null, className), role: "group", "aria-labelledby": props['aria-labelledby'], "aria-describedby": props['aria-describedby'], "aria-required": props.required }, options.map((option, i) => {
14
22
  const isDisabled = option.disabled != null
15
23
  ? option.disabled
@@ -27,7 +35,7 @@ const TogglerGroup = (props) => {
27
35
  }
28
36
  setValues(selectedValues);
29
37
  onSelected && onSelected({ value, checked, option, selectedValues });
30
- }, disabled: isDisabled, "aria-invalid": props['aria-invalid'], checked: isChecked })));
38
+ }, disabled: isDisabled, readOnly: readOnly, "aria-invalid": props['aria-invalid'], checked: isChecked })));
31
39
  })));
32
40
  };
33
41
  exports.TogglerGroup = TogglerGroup;
@@ -3,9 +3,9 @@ import { BemPropsModifier } from '@hugsmidjan/react/types';
3
3
  import { FormFieldGroupWrappingProps } from '../FormField.js';
4
4
  import { TogglerGroupOption, TogglerGroupOptions, TogglerGroupProps } from './_TogglerGroup.js';
5
5
  import { TogglerInputProps } from './_TogglerInput.js';
6
- export type TogglerGroupFieldProps = {
6
+ export type TogglerGroupFieldProps<T = 'default'> = {
7
7
  className?: string;
8
- } & FormFieldGroupWrappingProps & TogglerGroupProps;
8
+ } & Omit<FormFieldGroupWrappingProps, 'disabled'> & TogglerGroupProps<T>;
9
9
  type _TogglerGroupFieldProps = {
10
10
  Toggler: (props: TogglerInputProps) => ReactElement;
11
11
  isRadio?: true;
@@ -13,7 +13,7 @@ type _TogglerGroupFieldProps = {
13
13
  defaultValue?: string | ReadonlyArray<string>;
14
14
  bem: string;
15
15
  } & BemPropsModifier;
16
- export type TogglerGroupFieldOption = TogglerGroupOption;
17
- export type TogglerGroupFieldOptions = TogglerGroupOptions;
16
+ export type TogglerGroupFieldOption<T = 'default'> = TogglerGroupOption<T>;
17
+ export type TogglerGroupFieldOptions<T = 'default'> = TogglerGroupOptions<T>;
18
18
  export declare const TogglerGroupField: (props: TogglerGroupFieldProps & _TogglerGroupFieldProps) => JSX.Element;
19
19
  export {};
@@ -14,8 +14,8 @@ const TogglerGroupField = (props) => {
14
14
  : typeof defaultValue === 'string'
15
15
  ? [defaultValue]
16
16
  : defaultValue, [defaultValue]);
17
- return (react_1.default.createElement(FormField_js_1.default, { className: (0, getBemClass_1.default)(bem, modifier, className), group: true, label: label, LabelTag: LabelTag, assistText: assistText, hideLabel: hideLabel, disabled: disabled, readOnly: readOnly, invalid: invalid, errorMessage: errorMessage, required: required, reqText: reqText, id: id, renderInput: (className, inputProps) => {
18
- return (react_1.default.createElement(_TogglerGroup_js_1.TogglerGroup, Object.assign({ bem: className.options }, inputProps, togglerGroupProps, { value: _value, defaultValue: _defaultValue, Toggler: Toggler })));
17
+ return (react_1.default.createElement(FormField_js_1.default, { className: (0, getBemClass_1.default)(bem, modifier, className), group: true, label: label, LabelTag: LabelTag, assistText: assistText, hideLabel: hideLabel, disabled: !!disabled, readOnly: readOnly, invalid: invalid, errorMessage: errorMessage, required: required, reqText: reqText, id: id, renderInput: (className, inputProps) => {
18
+ return (react_1.default.createElement(_TogglerGroup_js_1.TogglerGroup, Object.assign({ bem: className.options }, inputProps, togglerGroupProps, { disabled: disabled, value: _value, defaultValue: _defaultValue, Toggler: Toggler })));
19
19
  } }));
20
20
  };
21
21
  exports.TogglerGroupField = TogglerGroupField;
@@ -2,7 +2,6 @@ import { BemPropsModifier } from '@hugsmidjan/react/types';
2
2
  export type TogglerInputProps = {
3
3
  label: string | JSX.Element;
4
4
  children?: never;
5
- Wrapper?: 'div' | 'li';
6
5
  invalid?: boolean;
7
6
  /** Hidden label prefix text to indicate that the field is required.
8
7
  *
@@ -13,6 +12,9 @@ export type TogglerInputProps = {
13
12
  * */
14
13
  reqText?: string | false;
15
14
  errorMessage?: string | JSX.Element;
15
+ Wrapper?: 'div' | 'li';
16
+ wrapperProps?: JSX.IntrinsicElements['div'];
17
+ inputProps?: JSX.IntrinsicElements['input'];
16
18
  } & BemPropsModifier & Omit<JSX.IntrinsicElements['input'], 'type'>;
17
19
  type _TogglerInputProps = {
18
20
  bem: string;
@@ -6,22 +6,25 @@ const react_1 = tslib_1.__importDefault(require("react"));
6
6
  const hooks_1 = require("@hugsmidjan/react/hooks");
7
7
  const getBemClass_1 = tslib_1.__importDefault(require("@hugsmidjan/react/utils/getBemClass"));
8
8
  const TogglerInput = (props) => {
9
- const { bem, modifier, className, label, invalid, errorMessage, Wrapper = 'div', required, reqText, type, id, innerWrap } = props, inputProps = tslib_1.__rest(props, ["bem", "modifier", "className", "label", "invalid", "errorMessage", "Wrapper", "required", "reqText", "type", "id", "innerWrap"]);
9
+ const { bem, modifier, className, label, invalid, errorMessage, Wrapper = 'div', required, reqText, type, id, innerWrap, wrapperProps, inputProps } = props, restInputProps = tslib_1.__rest(props, ["bem", "modifier", "className", "label", "invalid", "errorMessage", "Wrapper", "required", "reqText", "type", "id", "innerWrap", "wrapperProps", "inputProps"]);
10
10
  const domid = (0, hooks_1.useDomid)(id);
11
11
  const errorId = errorMessage && 'error' + domid;
12
12
  const reqStar = required && reqText !== false && (react_1.default.createElement("abbr", { className: bem + '__label__reqstar',
13
13
  // TODO: add mo-better i18n thinking
14
14
  title: (reqText || 'Þarf að haka í') + ': ' }, "*"));
15
+ const readOnly = restInputProps.readOnly || (inputProps || {}).readOnly;
15
16
  const labelContent = (react_1.default.createElement(react_1.default.Fragment, null,
16
17
  ' ',
17
18
  reqStar,
18
19
  " ",
19
20
  label,
20
21
  ' '));
21
- return (react_1.default.createElement(Wrapper, { className: (0, getBemClass_1.default)(bem, modifier, className) },
22
- react_1.default.createElement("input", Object.assign({ className: bem + '__input', type: type, id: domid, "aria-invalid": invalid || !!errorMessage || undefined, "aria-describedby": errorId }, inputProps)),
22
+ return (react_1.default.createElement(Wrapper, Object.assign({}, wrapperProps, { className: (0, getBemClass_1.default)(bem, modifier, className) }),
23
+ react_1.default.createElement("input", Object.assign({ className: bem + '__input', type: type, id: domid, "aria-invalid": invalid || !!errorMessage || undefined, "aria-describedby": errorId }, restInputProps, inputProps, (readOnly && { disabled: true }))),
23
24
  ' ',
24
- react_1.default.createElement("label", { className: bem + '__label', htmlFor: domid }, innerWrap ? (react_1.default.createElement("span", { className: bem + '__label__wrap' }, labelContent)) : (labelContent)),
25
+ react_1.default.createElement("label", { className: bem + '__label', htmlFor: domid },
26
+ innerWrap ? (react_1.default.createElement("span", { className: bem + '__label__wrap' }, labelContent)) : (labelContent),
27
+ readOnly && (react_1.default.createElement("input", { type: "hidden", name: restInputProps.name, value: restInputProps.value }))),
25
28
  errorMessage && (react_1.default.createElement("div", { className: bem + '__error', id: errorId }, errorMessage))));
26
29
  };
27
30
  exports.TogglerInput = TogglerInput;
@@ -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?: string;
14
+ target?: React.HTMLAttributeAnchorTarget;
14
15
  color?: ColorFamily;
15
16
  /** NOTE: if both `color` and `theme` are specified
16
17
  * then `color` takes precedence.
@@ -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?: string;
29
+ target?: React.HTMLAttributeAnchorTarget;
29
30
  } | {
30
31
  onClick: () => void | boolean;
31
32
  href?: undefined;
@@ -2,7 +2,6 @@ import React, { useEffect, useMemo, useRef, useState } from 'react';
2
2
  import { focusElm } from '@hugsmidjan/qj/focusElm';
3
3
  import { useDomid } from '@hugsmidjan/react/hooks';
4
4
  import getBemClass from '@hugsmidjan/react/utils/getBemClass';
5
- import { getPageScrollElm } from '@reykjavik/hanna-utils';
6
5
  import { getTexts } from '@reykjavik/hanna-utils/i18n';
7
6
  import { Link } from './_abstract/_Link.js';
8
7
  import { breakOnNL } from './_abstract/breakOnNL.js';
@@ -61,12 +60,11 @@ export const ContactBubble = (props) => {
61
60
  wrapperElm.dataset.show = 'true';
62
61
  return;
63
62
  }
64
- const scrollElm = getPageScrollElm();
65
63
  let pending = 0;
66
64
  const checkScroll = () => {
67
65
  if (!pending) {
68
66
  pending = requestAnimationFrame(() => {
69
- const { scrollTop, scrollHeight, clientHeight } = scrollElm;
67
+ const { scrollTop, scrollHeight, clientHeight } = document.documentElement;
70
68
  const scrollLength = scrollHeight - clientHeight;
71
69
  // const f = scrollLength > 600 ? 1 : (scrollLength - 200) / 600;
72
70
  const f = 1;
@@ -78,7 +76,7 @@ export const ContactBubble = (props) => {
78
76
  }
79
77
  };
80
78
  checkScroll();
81
- // Set scroll-listeners on both the ´document` and the `scrollElm`
79
+ // Set scroll-listeners on both the ´document` and the `document.documentElement`
82
80
  // because mobile browsers seem to handle CSS height and overflow
83
81
  // rules on <html> and <body> differently from desktop browsers.
84
82
  // Only one of these two handlers seems to trigger though,
@@ -86,10 +84,10 @@ export const ContactBubble = (props) => {
86
84
  // and even if they did, the rAF throttling prevents that from
87
85
  // becoming a problem.
88
86
  document.addEventListener('scroll', checkScroll);
89
- scrollElm.addEventListener('scroll', checkScroll);
87
+ document.documentElement.addEventListener('scroll', checkScroll);
90
88
  return () => {
91
89
  document.removeEventListener('scroll', checkScroll);
92
- scrollElm.removeEventListener('scroll', checkScroll);
90
+ document.documentElement.removeEventListener('scroll', checkScroll);
93
91
  };
94
92
  }, [isBrowser, alwaysShow, closeBubble]);
95
93
  useEffect(() => {
@@ -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: (date?: Date) => void;
36
+ onChange?: (date?: Date) => void;
18
37
  datepickerExtraProps?: Record<string, unknown>;
19
- /** @deprecated use value instead. (Will be removed in v0.11) */
38
+ /** @deprecated Use `value` or `defaultValue` instead. (Will be removed in v0.11) */
20
39
  initialDate?: Date;
21
40
  } & FormFieldWrappingProps;
22
- export declare const getDateDiff: (date: Date, diff: number) => Date;
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;