@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
|
@@ -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(); };
|
package/esm/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?:
|
|
12
|
+
target?: React.HTMLAttributeAnchorTarget;
|
|
12
13
|
type?: RelatedLinkType;
|
|
13
14
|
};
|
|
14
15
|
export type RelatedLinksProps = {
|
package/esm/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
|
|
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/esm/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>;
|
|
@@ -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
|
|
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
|
|
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,
|
|
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,
|
|
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 },
|
|
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" />
|