@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.
Files changed (66) hide show
  1. package/ArticleCarousel/_ArticleCarouselCard.d.ts +2 -1
  2. package/CHANGELOG.md +23 -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/ReadSpeakerPlayer.d.ts +64 -0
  17. package/ReadSpeakerPlayer.js +78 -0
  18. package/RelatedLinks.d.ts +2 -1
  19. package/Selectbox.d.ts +3 -3
  20. package/TextInput.d.ts +0 -1
  21. package/_abstract/_CardList.d.ts +2 -1
  22. package/_abstract/_CardList.js +2 -2
  23. package/_abstract/_FocusTrap.d.ts +14 -0
  24. package/_abstract/_FocusTrap.js +24 -0
  25. package/_abstract/_TogglerGroup.d.ts +11 -7
  26. package/_abstract/_TogglerGroup.js +11 -3
  27. package/_abstract/_TogglerGroupField.d.ts +4 -4
  28. package/_abstract/_TogglerGroupField.js +2 -2
  29. package/_abstract/_TogglerInput.d.ts +3 -1
  30. package/_abstract/_TogglerInput.js +7 -4
  31. package/esm/ArticleCarousel/_ArticleCarouselCard.d.ts +2 -1
  32. package/esm/ContactBubble.d.ts +2 -1
  33. package/esm/ContactBubble.js +4 -6
  34. package/esm/Datepicker.d.ts +31 -3
  35. package/esm/Datepicker.js +25 -6
  36. package/esm/FormField.d.ts +11 -2
  37. package/esm/FormField.js +5 -5
  38. package/esm/MainMenu/_PrimaryPanel.d.ts +2 -2
  39. package/esm/MainMenu.d.ts +2 -1
  40. package/esm/MainMenu.js +5 -6
  41. package/esm/Multiselect/_Multiselect.search.d.ts +19 -0
  42. package/esm/Multiselect/_Multiselect.search.js +75 -0
  43. package/esm/Multiselect.d.ts +64 -0
  44. package/esm/Multiselect.js +231 -0
  45. package/esm/ReadSpeakerPlayer.d.ts +64 -0
  46. package/esm/ReadSpeakerPlayer.js +72 -0
  47. package/esm/RelatedLinks.d.ts +2 -1
  48. package/esm/Selectbox.d.ts +3 -3
  49. package/esm/TextInput.d.ts +0 -1
  50. package/esm/_abstract/_CardList.d.ts +2 -1
  51. package/esm/_abstract/_CardList.js +2 -2
  52. package/esm/_abstract/_FocusTrap.d.ts +14 -0
  53. package/esm/_abstract/_FocusTrap.js +19 -0
  54. package/esm/_abstract/_TogglerGroup.d.ts +11 -7
  55. package/esm/_abstract/_TogglerGroup.js +11 -3
  56. package/esm/_abstract/_TogglerGroupField.d.ts +4 -4
  57. package/esm/_abstract/_TogglerGroupField.js +2 -2
  58. package/esm/_abstract/_TogglerInput.d.ts +3 -1
  59. package/esm/_abstract/_TogglerInput.js +7 -4
  60. package/esm/index.d.ts +2 -0
  61. package/esm/utils/useFormatMonitor.d.ts +4 -2
  62. package/esm/utils/useFormatMonitor.js +4 -2
  63. package/index.d.ts +2 -0
  64. package/package.json +13 -5
  65. package/utils/useFormatMonitor.d.ts +4 -2
  66. package/utils/useFormatMonitor.js +4 -2
@@ -0,0 +1,231 @@
1
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import domId from '@hugsmidjan/qj/domid';
3
+ import { useDomid, useOnClickOutside } from '@hugsmidjan/react/hooks';
4
+ import getBemClass from '@hugsmidjan/react/utils/getBemClass';
5
+ import { notNully } from '@reykjavik/hanna-utils';
6
+ import { getTexts } from '@reykjavik/hanna-utils/i18n';
7
+ import { FocusTrap } from './_abstract/_FocusTrap.js';
8
+ import { filterItems } from './Multiselect/_Multiselect.search.js';
9
+ import Checkbox from './Checkbox.js';
10
+ import FormField from './FormField.js';
11
+ import TagPill from './TagPill.js';
12
+ import { useMixedControlState } from './utils.js';
13
+ const metaData = {
14
+ /**
15
+ * The item-count where the list becomes searchable.
16
+ *
17
+ * (The search UI, including the on-screen keyboard, takes up a lot of space
18
+ * on mobile devices, so there's a balance that we want to strike.)
19
+ */
20
+ searchableLimit: 20,
21
+ /**
22
+ * The item-count above which we display a summary of "current" values
23
+ * at the top of the drop-down list.
24
+ *
25
+ * (This summary just gets in the way with ultra short option lists.)
26
+ */
27
+ summaryLimit: 10,
28
+ };
29
+ const { searchableLimit, summaryLimit } = metaData;
30
+ const defaultTexts = {
31
+ pl: {
32
+ search: 'Wyszukaj opcje',
33
+ buttonShow: 'Pokaż opcje',
34
+ // buttonHide: 'Ukryj opcje',
35
+ currentValues: 'Wybrane wartości',
36
+ noneFoundMsg: 'Brak dopasowań',
37
+ },
38
+ en: {
39
+ search: 'Search options',
40
+ buttonShow: 'Show options',
41
+ // buttonHide: 'Hide options',
42
+ currentValues: 'Currently selected',
43
+ noneFoundMsg: 'No matches',
44
+ },
45
+ is: {
46
+ search: 'Leita í valkostum',
47
+ buttonShow: 'Birta valkosti',
48
+ // buttonHide: 'Fela valkosti',
49
+ currentValues: 'Valin gildi',
50
+ noneFoundMsg: 'Ekkert passar',
51
+ },
52
+ };
53
+ export const Multiselect = (props) => {
54
+ const { onSelected, options: _options, disabled: _disabled, readOnly } = props;
55
+ const disabled = _disabled === true;
56
+ const disableds = !disabled && _disabled;
57
+ const name = useDomid(props.name);
58
+ const [values, setValues] = useMixedControlState(props, 'value', []);
59
+ const filled = values.length > 0;
60
+ const empty = !filled && !props.placeholder;
61
+ const placeholderText = !values.length ? props.placeholder : undefined;
62
+ const texts = getTexts(props, defaultTexts);
63
+ const inputRef = useRef(null);
64
+ const wrapperRef = useRef(null);
65
+ const [activeItemIndex, setActiveItemIndex] = useState(-1);
66
+ const [searchQuery, setSearchQuery] = useState('');
67
+ const [isOpen, setIsOpen] = useState(false);
68
+ const toggleOpen = (newIsOpen) => {
69
+ setIsOpen((isOpen) => {
70
+ newIsOpen = typeof newIsOpen === 'boolean' ? newIsOpen : !isOpen;
71
+ if (!newIsOpen) {
72
+ wrapperRef.current.querySelector('.Multiselect__choices').scrollTo(0, 0);
73
+ setSearchQuery('');
74
+ setActiveItemIndex(-1);
75
+ }
76
+ return newIsOpen;
77
+ });
78
+ };
79
+ useOnClickOutside(wrapperRef, () => toggleOpen(false));
80
+ const options = useMemo(() => _options.map((item) => (typeof item === 'string' ? { value: item } : item)), [_options]);
81
+ const isSearchable = props.forceSearchable || options.length >= searchableLimit;
82
+ /*
83
+ NOTE: he `.MultiSelect__currentvalues` should only be visible when
84
+ there are some items selected, and multiselect is either collapsed,
85
+ or the dropdown has reached `summaryLimit` number of items.
86
+ (For fewer items, the "summary" is just in the way.)
87
+ The `forceSummary` prop overrides this default.
88
+ */
89
+ const showCurrentValues = values.length > 0 &&
90
+ (props.forceSummary || !isOpen || options.length >= summaryLimit);
91
+ const filteredOptions = useMemo(() => filterItems(options, searchQuery, props.searchScoring), [searchQuery, options, props.searchScoring]);
92
+ const handleCheckboxSelection = useCallback((selectedItem) => {
93
+ const selValue = selectedItem.value;
94
+ const isAdding = values.indexOf(selValue) === -1;
95
+ const _newValues = isAdding
96
+ ? [...values, selValue]
97
+ : values.filter((value) => value !== selValue);
98
+ const selectedValues = options
99
+ .filter((item) => _newValues.includes(item.value))
100
+ .map((item) => item.value);
101
+ setValues(selectedValues);
102
+ if (onSelected) {
103
+ onSelected({
104
+ value: selectedItem.value,
105
+ checked: isAdding,
106
+ option: selectedItem,
107
+ selectedValues,
108
+ });
109
+ }
110
+ }, [values, options, onSelected, setValues]);
111
+ const handleInputChange = (event) => {
112
+ const val = event.target.value;
113
+ const fixVal = val === ' ' ? '' : val;
114
+ setSearchQuery(fixVal);
115
+ toggleOpen(true);
116
+ };
117
+ const handleInputKeyDown = (event) => {
118
+ if (searchQuery.length === 0 && [' ', 'Delete', 'Backspace'].includes(event.key)) {
119
+ // setSearchQuery('');
120
+ toggleOpen(activeItemIndex > -1 ? true : !isOpen);
121
+ }
122
+ };
123
+ // When the dropdown is open, add keydown handlers
124
+ useEffect(() => {
125
+ if (!isOpen) {
126
+ return;
127
+ }
128
+ const handleKeyDown = (e) => {
129
+ const inputElm = inputRef.current;
130
+ if (e.key === 'ArrowUp') {
131
+ e.preventDefault();
132
+ inputElm.focus();
133
+ setActiveItemIndex((prevIndex) => prevIndex === 0 ? filteredOptions.length - 1 : prevIndex - 1);
134
+ }
135
+ else if (e.key === 'ArrowDown') {
136
+ e.preventDefault();
137
+ inputElm.focus();
138
+ setActiveItemIndex((prevIndex) => prevIndex === filteredOptions.length - 1 ? 0 : prevIndex + 1);
139
+ }
140
+ else if (e.key === 'Escape') {
141
+ e.preventDefault();
142
+ inputElm.blur();
143
+ inputElm.focus();
144
+ toggleOpen(false);
145
+ }
146
+ else if (e.key === 'Enter' || e.key === ' ') {
147
+ if (e.target.closest('.MultiSelect__currentvalues')) {
148
+ return;
149
+ }
150
+ const focusInRange = activeItemIndex >= 0 && activeItemIndex < filteredOptions.length;
151
+ if (focusInRange) {
152
+ e.preventDefault();
153
+ const selItem = filteredOptions[activeItemIndex];
154
+ if (selItem) {
155
+ handleCheckboxSelection(selItem);
156
+ }
157
+ }
158
+ }
159
+ };
160
+ document.addEventListener('keydown', handleKeyDown);
161
+ return () => {
162
+ document.removeEventListener('keydown', handleKeyDown);
163
+ };
164
+ }, [activeItemIndex, filteredOptions, isOpen, handleCheckboxSelection, inputRef]);
165
+ // Auto-close the dropdown when focus has left the building
166
+ useEffect(() => {
167
+ const wrapperDiv = wrapperRef.current;
168
+ if (!wrapperDiv) {
169
+ return;
170
+ }
171
+ let closing;
172
+ const cancelClose = () => clearTimeout(closing);
173
+ const closeDropdown = () => {
174
+ closing = setTimeout(() => toggleOpen(false), 200);
175
+ };
176
+ wrapperDiv.addEventListener('focusin', cancelClose);
177
+ wrapperDiv.addEventListener('focusout', closeDropdown);
178
+ return () => {
179
+ wrapperDiv.removeEventListener('focusin', cancelClose);
180
+ wrapperDiv.removeEventListener('focusout', closeDropdown);
181
+ };
182
+ }, []);
183
+ useEffect(() => {
184
+ var _a, _b;
185
+ (_b = (_a = wrapperRef.current) === null || _a === void 0 ? void 0 : _a.querySelectorAll('.Multiselect__option')[activeItemIndex]) === null || _b === void 0 ? void 0 : _b.scrollIntoView({
186
+ behavior: 'smooth',
187
+ block: 'nearest',
188
+ });
189
+ }, [activeItemIndex]);
190
+ return (React.createElement(FormField, { className: getBemClass('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) => {
191
+ const { id } = inputProps;
192
+ return (React.createElement("div", Object.assign({ className: getBemClass('Multiselect__input', [isOpen && 'open'], className.input) }, addFocusProps(), { "data-sprinkled": isBrowser, ref: wrapperRef }),
193
+ !isBrowser ? null : isSearchable ? (React.createElement("input", { className: "Multiselect__search", id: `toggler:${id}`, "aria-label": texts.search, "aria-controls": domId(), "data-expanded": isOpen || undefined, onChange: handleInputChange, onKeyDown: handleInputKeyDown, onClick: () => toggleOpen(), value: searchQuery,
194
+ // onFocus={handleInputFocus}
195
+ placeholder: placeholderText, disabled: disabled, ref: inputRef })) : (React.createElement("button", { className: "Multiselect__toggler", id: `toggler:${id}`, type: "button", "aria-label": texts.buttonShow, "aria-controls": domId(), "aria-expanded": isOpen, onClick: () => toggleOpen(), disabled: disabled,
196
+ // Seems like an innocent hack for visible "placeholder" value.
197
+ // For scren-readers aria-label should take precedence.
198
+ ref: inputRef }, placeholderText || ' ')),
199
+ React.createElement("div", { className: "Multiselect__choices", tabIndex: -1 },
200
+ isBrowser && showCurrentValues && (React.createElement("div", { className: "Multiselect__currentvalues", onClick: isOpen || disabled
201
+ ? undefined
202
+ : () => {
203
+ var _a;
204
+ toggleOpen();
205
+ (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
206
+ }, "aria-label": `${texts.currentValues}:` }, values
207
+ .map((value) => options.find((opt) => opt.value === value))
208
+ .filter(notNully)
209
+ .map((item, idx) => (React.createElement(TagPill, Object.assign({ key: idx, large: true, label: item.label || item.value }, (isOpen && !readOnly
210
+ ? {
211
+ removable: true,
212
+ onRemove: () => {
213
+ handleCheckboxSelection(item);
214
+ },
215
+ }
216
+ : { removable: false }))))))),
217
+ React.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 },
218
+ filteredOptions.length ? (filteredOptions.map((item, idx) => {
219
+ const isDisabled = item.disabled != null
220
+ ? item.disabled
221
+ : disableds && disableds.includes(idx);
222
+ const isChecked = values.includes(item.value);
223
+ return (React.createElement(Checkbox, Object.assign({ key: idx, className: getBemClass('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: {
224
+ onMouseEnter: () => setActiveItemIndex(idx),
225
+ } })));
226
+ })) : searchQuery ? (React.createElement("li", { className: "Multiselect__noresults" }, texts.noneFoundMsg)) : undefined,
227
+ React.createElement(FocusTrap, { Tag: "li" })))));
228
+ } }));
229
+ };
230
+ /** Configuration constants for the Multiselect components */
231
+ Multiselect.meta = metaData;
@@ -0,0 +1,64 @@
1
+ import { DefaultTexts } from '@reykjavik/hanna-utils/i18n';
2
+ import { HTMLProps } from './utils.js';
3
+ export type ReadSpeakerPlayerI18n = {
4
+ linkText: string;
5
+ linkLabel?: string;
6
+ };
7
+ export declare const defaultReadSpeakerPlayerTexts: DefaultTexts<ReadSpeakerPlayerI18n>;
8
+ export type ReadSpeakerPlayerProps = {
9
+ /**
10
+ * Your ReadSpeaker account/customer ID
11
+ *
12
+ * @see https://docs.typo3.org/p/readspeaker/readspeaker-services/main/en-us/Configuration/Index.html#customer-id
13
+ */
14
+ customerId?: string;
15
+ /**
16
+ * Reading language/locale for the ReadSpeaker player.
17
+ *
18
+ * If you don't specify a `lang`, the player will try to auto-detect
19
+ * the language of the page, and pick a default `voice` for that language.
20
+ *
21
+ * @see https://docs.typo3.org/p/readspeaker/readspeaker-services/main/en-us/Configuration/Index.html#reading-language
22
+ */
23
+ lang?: Lowercase<string>;
24
+ /**
25
+ * Reading voice for the ReadSpeaker player.
26
+ *
27
+ * This prop only makes sense if you specfy a reading `lang`, as
28
+ * the voices are language-specific.
29
+ *
30
+ * @see https://docs.typo3.org/p/readspeaker/readspeaker-services/main/en-us/Configuration/Index.html#voice
31
+ */
32
+ voice?: string;
33
+ /**
34
+ * The DOM `id=""` of the element to read.
35
+ *
36
+ * @see https://docs.typo3.org/p/readspeaker/readspeaker-services/main/en-us/Configuration/Index.html#reading-area-id
37
+ */
38
+ readId?: string;
39
+ /**
40
+ * The DOM class-name of the element(s) to read
41
+ *
42
+ * Comma-separated list of class-names may also work...
43
+ *
44
+ * @see https://docs.typo3.org/p/readspeaker/readspeaker-services/main/en-us/Configuration/Index.html#reading-area-class
45
+ */
46
+ readClass?: string;
47
+ texts?: ReadSpeakerPlayerI18n;
48
+ align?: 'left' | 'right';
49
+ /** Tooggles CSS float layout */
50
+ float?: boolean;
51
+ /** Custom HTML attributes for the wrapper element. */
52
+ wrapperProps?: HTMLProps<'div'>;
53
+ };
54
+ /**
55
+ * Embeds a ReadSpeaker player in the page
56
+ *
57
+ * @see https://docs.typo3.org/p/readspeaker/readspeaker-services/main/en-us/Configuration/Index.html
58
+ */
59
+ export declare const ReadSpeakerPlayer: (props: ReadSpeakerPlayerProps) => JSX.Element;
60
+ /**
61
+ * Run this function if you find that the player keeps reading when a user
62
+ * swaps pages.
63
+ */
64
+ export declare const stopReading: () => void | undefined;
@@ -0,0 +1,72 @@
1
+ import React, { useEffect } from 'react';
2
+ import getBemClass from '@hugsmidjan/react/utils/getBemClass';
3
+ import { getTexts } from '@reykjavik/hanna-utils/i18n';
4
+ const scriptTagId = 'rs_req_Init';
5
+ const scriptTagSelector = `script#${scriptTagId}`;
6
+ let buttons = 0;
7
+ export const defaultReadSpeakerPlayerTexts = {
8
+ en: { linkText: 'Listen', linkLabel: 'Listen to this page read outloud' },
9
+ is: { linkText: 'Hlusta', linkLabel: 'Hlusta á þessa síðu lesna upphátt' },
10
+ pl: { linkText: 'Posłuchaj', linkLabel: 'Posłuchaj tej strony odczytanej na głos' },
11
+ };
12
+ /**
13
+ * Embeds a ReadSpeaker player in the page
14
+ *
15
+ * @see https://docs.typo3.org/p/readspeaker/readspeaker-services/main/en-us/Configuration/Index.html
16
+ */
17
+ export const ReadSpeakerPlayer = (props) => {
18
+ const { align, float, customerId = '11315', lang = '', voice = /^is(?:_is)?$/i.test(lang) ? 'is_dora' : '', readId = '', readClass = readId ? '' : 'Layout__main', wrapperProps = {}, texts, } = props;
19
+ const { linkText, linkLabel } = getTexts({ lang: lang.slice(0, 2), texts }, defaultReadSpeakerPlayerTexts);
20
+ useEffect(() => {
21
+ var _a, _b;
22
+ if (buttons < 0) {
23
+ return;
24
+ }
25
+ if (buttons === 0) {
26
+ if (document.querySelector(scriptTagSelector)) {
27
+ buttons = -1;
28
+ return;
29
+ }
30
+ const script = document.createElement('script');
31
+ script.id = scriptTagId;
32
+ script.src = `https://cdn-eu.readspeaker.com/script/${customerId}/webReader/webReader.js?pids=wr`;
33
+ script.onload = () => { var _a, _b; return (_b = (_a = window.rspkr) === null || _a === void 0 ? void 0 : _a.ui) === null || _b === void 0 ? void 0 : _b.addClickEvents(); };
34
+ script.async = true;
35
+ document.head.appendChild(script);
36
+ }
37
+ buttons++;
38
+ (_b = (_a = window.rspkr) === null || _a === void 0 ? void 0 : _a.ui) === null || _b === void 0 ? void 0 : _b.addClickEvents();
39
+ return () => {
40
+ var _a;
41
+ buttons--;
42
+ if (buttons === 0) {
43
+ (_a = document.querySelector(scriptTagSelector)) === null || _a === void 0 ? void 0 : _a.remove();
44
+ }
45
+ };
46
+ },
47
+ // eslint-disable-next-line react-hooks/exhaustive-deps
48
+ [
49
+ // We're not trying to support dynamic changes to `customerId`
50
+ // or multiple different `customerId`s on the same page.
51
+ // If you try that, things will be weird and wonky.
52
+ ]);
53
+ return (React.createElement("div", Object.assign({}, wrapperProps, { className: getBemClass('ReadSpeakerPlayer', [align === 'right' && `align-${align}`, float && 'float'], wrapperProps.className) }),
54
+ React.createElement("div", { id: "readspeaker_button1", className: "rs_skip rsbtn rs_preserve" },
55
+ React.createElement("a", { rel: "nofollow", className: "rsbtn_play", accessKey: "L", title: linkLabel || linkText, href: `https://app-eu.readspeaker.com/cgi-bin/rsent?${new URLSearchParams({
56
+ customerid: customerId,
57
+ lang,
58
+ voice: lang && voice,
59
+ autoLang: !lang ? 'true' : 'false',
60
+ readclass: readClass,
61
+ readid: readId,
62
+ })}` },
63
+ React.createElement("span", { className: "rsbtn_left rsimg rspart" },
64
+ React.createElement("span", { className: "rsbtn_text" },
65
+ React.createElement("span", null, linkText))),
66
+ React.createElement("span", { className: "rsbtn_right rsimg rsplay rspart" })))));
67
+ };
68
+ /**
69
+ * Run this function if you find that the player keeps reading when a user
70
+ * swaps pages.
71
+ */
72
+ export const stopReading = () => { var _a, _b; return (_b = (_a = window.rspkr) === null || _a === void 0 ? void 0 : _a.ui) === null || _b === void 0 ? void 0 : _b.destroyActivePlayer(); };
@@ -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 = {
@@ -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
  };
@@ -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>;
@@ -2,10 +2,10 @@ import React from 'react';
2
2
  import { Button } from './_Button.js';
3
3
  import { Image } from './_Image.js';
4
4
  const Card = (props) => {
5
- const { bem, href, title, imgPlaceholder, image, meta, summary } = props;
5
+ const { bem, href, title, imgPlaceholder, image, meta, summary, target } = props;
6
6
  const cardClass = `${bem}__card`;
7
7
  return (React.createElement(React.Fragment, null,
8
- React.createElement(Button, { bem: cardClass, href: href },
8
+ React.createElement(Button, { bem: cardClass, href: href, target: target },
9
9
  ' ',
10
10
  React.createElement(Image, Object.assign({ className: `${bem}__image` }, image, { placeholder: imgPlaceholder })),
11
11
  React.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,19 @@
1
+ import React from 'react';
2
+ /** A focus trap element that can be used to keep keyboard focus within a container block. */
3
+ export const FocusTrap = (props) => {
4
+ const Tag = props.Tag || 'span';
5
+ return (React.createElement(Tag, { tabIndex: 0, onFocus: (e) => {
6
+ var _a;
7
+ let container = e.currentTarget;
8
+ let depth = Math.max(props.depth || 0, 1);
9
+ while (depth-- && container) {
10
+ container = container.parentElement;
11
+ }
12
+ if (!container) {
13
+ return;
14
+ }
15
+ const focusables = container.querySelectorAll('a,input, select, textarea,button, [tabindex]');
16
+ const targetIdx = props.atTop ? focusables.length - 1 : 0;
17
+ (_a = focusables[targetIdx]) === null || _a === void 0 ? void 0 : _a.focus();
18
+ } }));
19
+ };
@@ -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'>;
@@ -1,11 +1,19 @@
1
- import React from 'react';
1
+ import React, { useMemo } from 'react';
2
+ import { useDomid } from '@hugsmidjan/react/hooks';
2
3
  import getBemClass from '@hugsmidjan/react/utils/getBemClass';
3
4
  import { useMixedControlState } from '../utils.js';
4
5
  export const TogglerGroup = (props) => {
5
6
  const {
6
7
  // id,
7
- className, bem, name, disabled, Toggler, onSelected, options, isRadio, inputProps = {}, } = props;
8
+ className, bem, disabled, readOnly, Toggler, onSelected, isRadio, inputProps = {}, } = props;
8
9
  const [values, setValues] = useMixedControlState(props, 'value', []);
10
+ const name = useDomid(props.name);
11
+ const options = useMemo(() => {
12
+ const _options = props.options;
13
+ return typeof _options[0] === 'string'
14
+ ? _options.map((option) => ({ value: option }))
15
+ : _options;
16
+ }, [props.options]);
9
17
  return (React.createElement("ul", { className: getBemClass(bem, null, className), role: "group", "aria-labelledby": props['aria-labelledby'], "aria-describedby": props['aria-describedby'], "aria-required": props.required }, options.map((option, i) => {
10
18
  const isDisabled = option.disabled != null
11
19
  ? option.disabled
@@ -23,6 +31,6 @@ export const TogglerGroup = (props) => {
23
31
  }
24
32
  setValues(selectedValues);
25
33
  onSelected && onSelected({ value, checked, option, selectedValues });
26
- }, disabled: isDisabled, "aria-invalid": props['aria-invalid'], checked: isChecked })));
34
+ }, disabled: isDisabled, readOnly: readOnly, "aria-invalid": props['aria-invalid'], checked: isChecked })));
27
35
  })));
28
36
  };
@@ -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 {};
@@ -11,7 +11,7 @@ export const TogglerGroupField = (props) => {
11
11
  : typeof defaultValue === 'string'
12
12
  ? [defaultValue]
13
13
  : defaultValue, [defaultValue]);
14
- return (React.createElement(FormField, { className: getBemClass(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) => {
15
- return (React.createElement(TogglerGroup, Object.assign({ bem: className.options }, inputProps, togglerGroupProps, { value: _value, defaultValue: _defaultValue, Toggler: Toggler })));
14
+ return (React.createElement(FormField, { className: getBemClass(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) => {
15
+ return (React.createElement(TogglerGroup, Object.assign({ bem: className.options }, inputProps, togglerGroupProps, { disabled: disabled, value: _value, defaultValue: _defaultValue, Toggler: Toggler })));
16
16
  } }));
17
17
  };
@@ -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;
@@ -3,21 +3,24 @@ import React from 'react';
3
3
  import { useDomid } from '@hugsmidjan/react/hooks';
4
4
  import getBemClass from '@hugsmidjan/react/utils/getBemClass';
5
5
  export const TogglerInput = (props) => {
6
- const { bem, modifier, className, label, invalid, errorMessage, Wrapper = 'div', required, reqText, type, id, innerWrap } = props, inputProps = __rest(props, ["bem", "modifier", "className", "label", "invalid", "errorMessage", "Wrapper", "required", "reqText", "type", "id", "innerWrap"]);
6
+ const { bem, modifier, className, label, invalid, errorMessage, Wrapper = 'div', required, reqText, type, id, innerWrap, wrapperProps, inputProps } = props, restInputProps = __rest(props, ["bem", "modifier", "className", "label", "invalid", "errorMessage", "Wrapper", "required", "reqText", "type", "id", "innerWrap", "wrapperProps", "inputProps"]);
7
7
  const domid = useDomid(id);
8
8
  const errorId = errorMessage && 'error' + domid;
9
9
  const reqStar = required && reqText !== false && (React.createElement("abbr", { className: bem + '__label__reqstar',
10
10
  // TODO: add mo-better i18n thinking
11
11
  title: (reqText || 'Þarf að haka í') + ': ' }, "*"));
12
+ const readOnly = restInputProps.readOnly || (inputProps || {}).readOnly;
12
13
  const labelContent = (React.createElement(React.Fragment, null,
13
14
  ' ',
14
15
  reqStar,
15
16
  " ",
16
17
  label,
17
18
  ' '));
18
- return (React.createElement(Wrapper, { className: getBemClass(bem, modifier, className) },
19
- React.createElement("input", Object.assign({ className: bem + '__input', type: type, id: domid, "aria-invalid": invalid || !!errorMessage || undefined, "aria-describedby": errorId }, inputProps)),
19
+ return (React.createElement(Wrapper, Object.assign({}, wrapperProps, { className: getBemClass(bem, modifier, className) }),
20
+ React.createElement("input", Object.assign({ className: bem + '__input', type: type, id: domid, "aria-invalid": invalid || !!errorMessage || undefined, "aria-describedby": errorId }, restInputProps, inputProps, (readOnly && { disabled: true }))),
20
21
  ' ',
21
- React.createElement("label", { className: bem + '__label', htmlFor: domid }, innerWrap ? (React.createElement("span", { className: bem + '__label__wrap' }, labelContent)) : (labelContent)),
22
+ React.createElement("label", { className: bem + '__label', htmlFor: domid },
23
+ innerWrap ? (React.createElement("span", { className: bem + '__label__wrap' }, labelContent)) : (labelContent),
24
+ readOnly && (React.createElement("input", { type: "hidden", name: restInputProps.name, value: restInputProps.value }))),
22
25
  errorMessage && (React.createElement("div", { className: bem + '__error', id: errorId }, errorMessage))));
23
26
  };
package/esm/index.d.ts CHANGED
@@ -27,6 +27,7 @@
27
27
  /// <reference path="./RowBlockColumn.d.tsx" />
28
28
  /// <reference path="./RowBlock.d.tsx" />
29
29
  /// <reference path="./RelatedLinks.d.tsx" />
30
+ /// <reference path="./ReadSpeakerPlayer.d.tsx" />
30
31
  /// <reference path="./RadioGroup.d.tsx" />
31
32
  /// <reference path="./RadioButtonsGroup.d.tsx" />
32
33
  /// <reference path="./PullQuote.d.tsx" />
@@ -37,6 +38,7 @@
37
38
  /// <reference path="./NewsHero.d.tsx" />
38
39
  /// <reference path="./NameCards.d.tsx" />
39
40
  /// <reference path="./NameCard.d.tsx" />
41
+ /// <reference path="./Multiselect.d.tsx" />
40
42
  /// <reference path="./Modal.d.tsx" />
41
43
  /// <reference path="./MiniMetrics.d.tsx" />
42
44
  /// <reference path="./MainMenu.d.tsx" />