@reykjavik/hanna-react 0.10.157 → 0.10.159
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -1
- package/FileInput/_FileInput.utils.d.ts +0 -2
- package/FileInput/_FileInput.utils.js +0 -2
- package/FileInput.d.ts +8 -1
- package/FileInput.js +18 -3
- package/Multiselect.d.ts +2 -0
- package/Multiselect.js +46 -49
- package/esm/FileInput/_FileInput.utils.d.ts +0 -2
- package/esm/FileInput/_FileInput.utils.js +0 -2
- package/esm/FileInput.d.ts +8 -1
- package/esm/FileInput.js +18 -3
- package/esm/Multiselect.d.ts +2 -0
- package/esm/Multiselect.js +46 -49
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,12 +4,21 @@
|
|
|
4
4
|
|
|
5
5
|
- ... <!-- Add new lines here. -->
|
|
6
6
|
|
|
7
|
-
## 0.10.
|
|
7
|
+
## 0.10.159
|
|
8
|
+
|
|
9
|
+
_2025-10-09_
|
|
10
|
+
|
|
11
|
+
- `FileUpload`:
|
|
12
|
+
- feat: Add prop `unstable_confirmReplace` for multi-upload name-conflicts
|
|
13
|
+
|
|
14
|
+
## 0.10.157 – 0.10.158
|
|
8
15
|
|
|
9
16
|
_2025-09-16_
|
|
10
17
|
|
|
11
18
|
- `Multiselect`
|
|
19
|
+
- feat: Add prop `onDropdown` prop
|
|
12
20
|
- fix: Sync `.checked` prop of keyboard-toggled `input`s with visual state
|
|
21
|
+
- fix: Scope "global" keyboard event listener to the component's element
|
|
13
22
|
- `Selectbox`:
|
|
14
23
|
- fix: Remove stray `modifier` prop from `SelectboxProps`
|
|
15
24
|
- docs: Add JSDoc comments for `FormfieldProps.renderInput` parameters
|
|
@@ -21,8 +21,6 @@ export declare const formatBytes: (bytes: number, lang?: string, decimals?: numb
|
|
|
21
21
|
* Figures out how to handle adding files to a FileInput
|
|
22
22
|
* Which files to retaine, which too delete, and
|
|
23
23
|
* what the updated fileList should look like.
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
24
|
*/
|
|
27
25
|
export declare const getFileListUpdate: (oldFileList: Array<File>, added: Array<File>, replaceMode: boolean) => {
|
|
28
26
|
fileList: File[];
|
|
@@ -46,8 +46,6 @@ exports.formatBytes = formatBytes;
|
|
|
46
46
|
* Figures out how to handle adding files to a FileInput
|
|
47
47
|
* Which files to retaine, which too delete, and
|
|
48
48
|
* what the updated fileList should look like.
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
49
|
*/
|
|
52
50
|
const getFileListUpdate = (oldFileList, added,
|
|
53
51
|
/**
|
package/FileInput.d.ts
CHANGED
|
@@ -23,7 +23,8 @@ export type FileInputProps = FormFieldWrappingProps & {
|
|
|
23
23
|
onFilesUpdated?: (
|
|
24
24
|
/** Updated, full list of Files. */
|
|
25
25
|
files: Array<File>,
|
|
26
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Information about which Files were added or removed during this update.
|
|
27
28
|
*
|
|
28
29
|
* NOTE: When a diff contains both added and deleted files, this indicates a
|
|
29
30
|
* name-conflict occurred — i.e. one of the added files has a name that
|
|
@@ -36,6 +37,12 @@ export type FileInputProps = FormFieldWrappingProps & {
|
|
|
36
37
|
deleted?: Array<File>;
|
|
37
38
|
added?: Array<File>;
|
|
38
39
|
}) => void;
|
|
40
|
+
/**
|
|
41
|
+
* Confirm replacing existing (name-conflicting) files in multi-upload mode
|
|
42
|
+
*
|
|
43
|
+
* This feature is "unstable" because its behavior might change in the future.
|
|
44
|
+
*/
|
|
45
|
+
unstable_confirmReplace?: boolean;
|
|
39
46
|
onFilesRejected?: (rejectedFiles: Array<File>) => void;
|
|
40
47
|
name?: string;
|
|
41
48
|
value?: ReadonlyArray<File>;
|
package/FileInput.js
CHANGED
|
@@ -29,6 +29,11 @@ const defaultDropzoneText = {
|
|
|
29
29
|
react_1.default.createElement("strong", null, "kliknij"),
|
|
30
30
|
" by wybra\u0107.")),
|
|
31
31
|
};
|
|
32
|
+
const replaceFileTest = {
|
|
33
|
+
is: 'Yfirskrifa núverandi skrá',
|
|
34
|
+
en: 'Replace existing file',
|
|
35
|
+
pl: 'Zastąp istniejący plik',
|
|
36
|
+
};
|
|
32
37
|
const defaultOnFilesRejected = (rejectedFiles) => {
|
|
33
38
|
window.alert(`Error:\n${rejectedFiles
|
|
34
39
|
.map((elm) => {
|
|
@@ -49,7 +54,7 @@ const FileInput = (props) => {
|
|
|
49
54
|
const _d = (0, FormField_js_1.groupFormFieldWrapperProps)(props), { dropzoneProps, // eslint-disable-line deprecation/deprecation
|
|
50
55
|
multiple = (_b = (_a = props.dropzoneProps) === null || _a === void 0 ? void 0 : _a.multiple) !== null && _b !== void 0 ? _b : true, // eslint-disable-line deprecation/deprecation
|
|
51
56
|
accept = (_c = props.dropzoneProps) === null || _c === void 0 ? void 0 : _c.accept, // eslint-disable-line deprecation/deprecation
|
|
52
|
-
dropzoneText = defaultDropzoneText[lang](), removeFileText = defaultRemoveFileText[lang], FileList = _FileInputFileList_js_1.DefaultFileList, onFilesUpdated = () => undefined, onFilesRejected, showFileSize, showImagePreviews, value = [], fieldWrapperProps } = _d, inputElementProps = tslib_1.__rest(_d, ["dropzoneProps", "multiple", "accept", "dropzoneText", "removeFileText", "FileList", "onFilesUpdated", "onFilesRejected", "showFileSize", "showImagePreviews", "value", "fieldWrapperProps"]);
|
|
57
|
+
dropzoneText = defaultDropzoneText[lang](), unstable_confirmReplace, removeFileText = defaultRemoveFileText[lang], FileList = _FileInputFileList_js_1.DefaultFileList, onFilesUpdated = () => undefined, onFilesRejected, showFileSize, showImagePreviews, value = [], fieldWrapperProps } = _d, inputElementProps = tslib_1.__rest(_d, ["dropzoneProps", "multiple", "accept", "dropzoneText", "unstable_confirmReplace", "removeFileText", "FileList", "onFilesUpdated", "onFilesRejected", "showFileSize", "showImagePreviews", "value", "fieldWrapperProps"]);
|
|
53
58
|
const domid = (0, useDomid_js_1.useDomid)(props.id);
|
|
54
59
|
const fileInputWrapper = (0, react_1.useRef)(null);
|
|
55
60
|
const fileInput = (0, react_1.useRef)(null);
|
|
@@ -113,14 +118,24 @@ const FileInput = (props) => {
|
|
|
113
118
|
};
|
|
114
119
|
const addFiles = (added) => {
|
|
115
120
|
const { fileList, diff } = (0, _FileInput_utils_js_1.getFileListUpdate)(files, added, !multiple);
|
|
121
|
+
let updatedFileList = fileList;
|
|
122
|
+
if (diff.deleted && multiple && props.unstable_confirmReplace) {
|
|
123
|
+
diff.deleted = diff.deleted.filter((file) => window.confirm(`${replaceFileTest} "${file.name}"?`));
|
|
124
|
+
// When unstable_confirmReplace becomes stable, we should stop returning
|
|
125
|
+
// `fileList` from getFileListUpdate and just always do this thing.
|
|
126
|
+
// That change will require changing the tests too, so let's wait.
|
|
127
|
+
updatedFileList = files
|
|
128
|
+
.filter((file) => !diff.deleted.includes(file))
|
|
129
|
+
.concat(added);
|
|
130
|
+
}
|
|
116
131
|
if (fileInput.current) {
|
|
117
|
-
fileInput.current.files = arrayToFileList(
|
|
132
|
+
fileInput.current.files = arrayToFileList(updatedFileList);
|
|
118
133
|
}
|
|
119
134
|
if (inputRef.current) {
|
|
120
135
|
// Empty on every add
|
|
121
136
|
inputRef.current.files = arrayToFileList([]);
|
|
122
137
|
}
|
|
123
|
-
onFilesUpdated(
|
|
138
|
+
onFilesUpdated(updatedFileList, diff);
|
|
124
139
|
};
|
|
125
140
|
return (react_1.default.createElement(FormField_js_1.default, Object.assign({ extraClassName: (0, hanna_utils_1.modifiedClass)('FileInput', [multiple && 'multi']) }, fieldWrapperProps, { id: `${domid}-fake`, LabelTag: "h4", renderInput: (className, inputProps /* , addFocusProps */) => {
|
|
126
141
|
return (react_1.default.createElement("div", { className: className.control, ref: fileInputWrapper },
|
package/Multiselect.d.ts
CHANGED
|
@@ -55,6 +55,8 @@ export type MultiselectProps = TogglerGroupFieldProps<string, {
|
|
|
55
55
|
forceSearchable?: boolean;
|
|
56
56
|
texts?: MultiselectI18n;
|
|
57
57
|
lang?: HannaLang;
|
|
58
|
+
/** Fires whenever the dropdown menu is opened or closed */
|
|
59
|
+
onDropdown?: (isOpen: boolean) => void;
|
|
58
60
|
};
|
|
59
61
|
export declare const Multiselect: {
|
|
60
62
|
(props: MultiselectProps): JSX.Element;
|
package/Multiselect.js
CHANGED
|
@@ -58,7 +58,7 @@ const defaultTexts = {
|
|
|
58
58
|
},
|
|
59
59
|
};
|
|
60
60
|
const Multiselect = (props) => {
|
|
61
|
-
const { onSelected, options: _options, disabled: _disabled, readOnly } = props;
|
|
61
|
+
const { onSelected, onDropdown, options: _options, disabled: _disabled, readOnly, } = props;
|
|
62
62
|
const disabled = _disabled === true;
|
|
63
63
|
const disableds = !disabled && _disabled;
|
|
64
64
|
const name = (0, useDomid_js_1.useDomid)(props.name);
|
|
@@ -80,6 +80,9 @@ const Multiselect = (props) => {
|
|
|
80
80
|
setSearchQuery('');
|
|
81
81
|
setActiveItemIndex(-1);
|
|
82
82
|
}
|
|
83
|
+
if (onDropdown && newIsOpen !== isOpen) {
|
|
84
|
+
onDropdown(newIsOpen);
|
|
85
|
+
}
|
|
83
86
|
return newIsOpen;
|
|
84
87
|
});
|
|
85
88
|
};
|
|
@@ -131,59 +134,53 @@ const Multiselect = (props) => {
|
|
|
131
134
|
toggleOpen(activeItemIndex > -1 ? true : !isOpen);
|
|
132
135
|
}
|
|
133
136
|
};
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (!isOpen) {
|
|
137
|
+
const handleWrapperKeyDown = (e) => {
|
|
138
|
+
var _a;
|
|
139
|
+
if (!isOpen || !((_a = inputWrapperRef.current) === null || _a === void 0 ? void 0 : _a.contains(e.target))) {
|
|
137
140
|
return;
|
|
138
141
|
}
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
142
|
+
const inputElm = inputRef.current;
|
|
143
|
+
if (e.key === 'ArrowUp') {
|
|
144
|
+
e.preventDefault();
|
|
145
|
+
inputElm.focus();
|
|
146
|
+
setActiveItemIndex((prevIndex) => prevIndex === 0 ? filteredOptions.length - 1 : prevIndex - 1);
|
|
147
|
+
}
|
|
148
|
+
else if (e.key === 'ArrowDown') {
|
|
149
|
+
e.preventDefault();
|
|
150
|
+
inputElm.focus();
|
|
151
|
+
setActiveItemIndex((prevIndex) => prevIndex === filteredOptions.length - 1 ? 0 : prevIndex + 1);
|
|
152
|
+
}
|
|
153
|
+
else if (e.key === 'Escape') {
|
|
154
|
+
e.preventDefault();
|
|
155
|
+
inputElm.blur();
|
|
156
|
+
inputElm.focus();
|
|
157
|
+
toggleOpen(false);
|
|
158
|
+
}
|
|
159
|
+
else if (e.key === 'Enter' || e.key === ' ') {
|
|
160
|
+
if (e.target.closest('.Multiselect__currentvalues')) {
|
|
161
|
+
return;
|
|
150
162
|
}
|
|
151
|
-
|
|
163
|
+
const focusInRange = activeItemIndex >= 0 && activeItemIndex < filteredOptions.length;
|
|
164
|
+
if (focusInRange) {
|
|
152
165
|
e.preventDefault();
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
// components (e.g. screen readers) are in sync with the visual state.
|
|
168
|
-
let input;
|
|
169
|
-
inputWrapperRef
|
|
170
|
-
.current.querySelectorAll(`input[type="checkbox"]`)
|
|
171
|
-
.forEach((elm) => {
|
|
172
|
-
if (elm.value === selItem.value) {
|
|
173
|
-
input = elm;
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
|
-
input.checked = !input.checked;
|
|
177
|
-
handleCheckboxSelection(selItem);
|
|
178
|
-
}
|
|
166
|
+
const selItem = filteredOptions[activeItemIndex];
|
|
167
|
+
if (selItem) {
|
|
168
|
+
// Manually toggle the checkbox, to ensure that uncontrolled
|
|
169
|
+
// components (e.g. screen readers) are in sync with the visual state.
|
|
170
|
+
let input;
|
|
171
|
+
e.currentTarget
|
|
172
|
+
.querySelectorAll(`input[type="checkbox"]`)
|
|
173
|
+
.forEach((elm) => {
|
|
174
|
+
if (elm.value === selItem.value) {
|
|
175
|
+
input = elm;
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
input.checked = !input.checked;
|
|
179
|
+
handleCheckboxSelection(selItem);
|
|
179
180
|
}
|
|
180
181
|
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
return () => {
|
|
184
|
-
document.removeEventListener('keydown', handleKeyDown);
|
|
185
|
-
};
|
|
186
|
-
}, [activeItemIndex, filteredOptions, isOpen, handleCheckboxSelection, inputRef]);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
187
184
|
// Auto-close the dropdown when focus has left the building
|
|
188
185
|
(0, react_1.useEffect)(() => {
|
|
189
186
|
const wrapperDiv = inputWrapperRef.current;
|
|
@@ -211,7 +208,7 @@ const Multiselect = (props) => {
|
|
|
211
208
|
}, [activeItemIndex]);
|
|
212
209
|
return (react_1.default.createElement(FormField_js_1.default, Object.assign({ extraClassName: (0, hanna_utils_1.modifiedClass)('Multiselect', props.nowrap && 'nowrap'), group: "inputlike", filled: filled, empty: empty }, (0, FormField_js_1.getFormFieldWrapperProps)(props), { renderInput: (className, inputProps, addFocusProps, isBrowser) => {
|
|
213
210
|
const { id } = inputProps;
|
|
214
|
-
return (react_1.default.createElement("div", Object.assign({ className: (0, hanna_utils_1.modifiedClass)('Multiselect__input', [isOpen && 'open'], className.input) }, addFocusProps(), { "data-sprinkled": isBrowser, ref: inputWrapperRef }),
|
|
211
|
+
return (react_1.default.createElement("div", Object.assign({ className: (0, hanna_utils_1.modifiedClass)('Multiselect__input', [isOpen && 'open'], className.input) }, addFocusProps(), { "data-sprinkled": isBrowser, ref: inputWrapperRef, onKeyDown: handleWrapperKeyDown }),
|
|
215
212
|
!isBrowser ? null : isSearchable ? (react_1.default.createElement("input", { className: "Multiselect__search", id: `toggler:${id}`, "aria-label": texts.search, "aria-controls": id, "data-expanded": isOpen || undefined, onChange: handleInputChange, onKeyDown: handleInputKeyDown, onClick: () => toggleOpen(), value: searchQuery,
|
|
216
213
|
// onFocus={handleInputFocus}
|
|
217
214
|
placeholder: placeholderText, disabled: disabled, ref: inputRef })) : (react_1.default.createElement("button", { className: "Multiselect__toggler", id: `toggler:${id}`, type: "button", "aria-label": texts.buttonShow, "aria-controls": id, "aria-expanded": isOpen, onClick: () => toggleOpen(), disabled: disabled,
|
|
@@ -21,8 +21,6 @@ export declare const formatBytes: (bytes: number, lang?: string, decimals?: numb
|
|
|
21
21
|
* Figures out how to handle adding files to a FileInput
|
|
22
22
|
* Which files to retaine, which too delete, and
|
|
23
23
|
* what the updated fileList should look like.
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
24
|
*/
|
|
27
25
|
export declare const getFileListUpdate: (oldFileList: Array<File>, added: Array<File>, replaceMode: boolean) => {
|
|
28
26
|
fileList: File[];
|
|
@@ -40,8 +40,6 @@ export const formatBytes = (bytes, lang = 'is', decimals = 2) => {
|
|
|
40
40
|
* Figures out how to handle adding files to a FileInput
|
|
41
41
|
* Which files to retaine, which too delete, and
|
|
42
42
|
* what the updated fileList should look like.
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
43
|
*/
|
|
46
44
|
export const getFileListUpdate = (oldFileList, added,
|
|
47
45
|
/**
|
package/esm/FileInput.d.ts
CHANGED
|
@@ -23,7 +23,8 @@ export type FileInputProps = FormFieldWrappingProps & {
|
|
|
23
23
|
onFilesUpdated?: (
|
|
24
24
|
/** Updated, full list of Files. */
|
|
25
25
|
files: Array<File>,
|
|
26
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Information about which Files were added or removed during this update.
|
|
27
28
|
*
|
|
28
29
|
* NOTE: When a diff contains both added and deleted files, this indicates a
|
|
29
30
|
* name-conflict occurred — i.e. one of the added files has a name that
|
|
@@ -36,6 +37,12 @@ export type FileInputProps = FormFieldWrappingProps & {
|
|
|
36
37
|
deleted?: Array<File>;
|
|
37
38
|
added?: Array<File>;
|
|
38
39
|
}) => void;
|
|
40
|
+
/**
|
|
41
|
+
* Confirm replacing existing (name-conflicting) files in multi-upload mode
|
|
42
|
+
*
|
|
43
|
+
* This feature is "unstable" because its behavior might change in the future.
|
|
44
|
+
*/
|
|
45
|
+
unstable_confirmReplace?: boolean;
|
|
39
46
|
onFilesRejected?: (rejectedFiles: Array<File>) => void;
|
|
40
47
|
name?: string;
|
|
41
48
|
value?: ReadonlyArray<File>;
|
package/esm/FileInput.js
CHANGED
|
@@ -26,6 +26,11 @@ const defaultDropzoneText = {
|
|
|
26
26
|
React.createElement("strong", null, "kliknij"),
|
|
27
27
|
" by wybra\u0107.")),
|
|
28
28
|
};
|
|
29
|
+
const replaceFileTest = {
|
|
30
|
+
is: 'Yfirskrifa núverandi skrá',
|
|
31
|
+
en: 'Replace existing file',
|
|
32
|
+
pl: 'Zastąp istniejący plik',
|
|
33
|
+
};
|
|
29
34
|
const defaultOnFilesRejected = (rejectedFiles) => {
|
|
30
35
|
window.alert(`Error:\n${rejectedFiles
|
|
31
36
|
.map((elm) => {
|
|
@@ -46,7 +51,7 @@ export const FileInput = (props) => {
|
|
|
46
51
|
const _d = groupFormFieldWrapperProps(props), { dropzoneProps, // eslint-disable-line deprecation/deprecation
|
|
47
52
|
multiple = (_b = (_a = props.dropzoneProps) === null || _a === void 0 ? void 0 : _a.multiple) !== null && _b !== void 0 ? _b : true, // eslint-disable-line deprecation/deprecation
|
|
48
53
|
accept = (_c = props.dropzoneProps) === null || _c === void 0 ? void 0 : _c.accept, // eslint-disable-line deprecation/deprecation
|
|
49
|
-
dropzoneText = defaultDropzoneText[lang](), removeFileText = defaultRemoveFileText[lang], FileList = DefaultFileList, onFilesUpdated = () => undefined, onFilesRejected, showFileSize, showImagePreviews, value = [], fieldWrapperProps } = _d, inputElementProps = __rest(_d, ["dropzoneProps", "multiple", "accept", "dropzoneText", "removeFileText", "FileList", "onFilesUpdated", "onFilesRejected", "showFileSize", "showImagePreviews", "value", "fieldWrapperProps"]);
|
|
54
|
+
dropzoneText = defaultDropzoneText[lang](), unstable_confirmReplace, removeFileText = defaultRemoveFileText[lang], FileList = DefaultFileList, onFilesUpdated = () => undefined, onFilesRejected, showFileSize, showImagePreviews, value = [], fieldWrapperProps } = _d, inputElementProps = __rest(_d, ["dropzoneProps", "multiple", "accept", "dropzoneText", "unstable_confirmReplace", "removeFileText", "FileList", "onFilesUpdated", "onFilesRejected", "showFileSize", "showImagePreviews", "value", "fieldWrapperProps"]);
|
|
50
55
|
const domid = useDomid(props.id);
|
|
51
56
|
const fileInputWrapper = useRef(null);
|
|
52
57
|
const fileInput = useRef(null);
|
|
@@ -110,14 +115,24 @@ export const FileInput = (props) => {
|
|
|
110
115
|
};
|
|
111
116
|
const addFiles = (added) => {
|
|
112
117
|
const { fileList, diff } = getFileListUpdate(files, added, !multiple);
|
|
118
|
+
let updatedFileList = fileList;
|
|
119
|
+
if (diff.deleted && multiple && props.unstable_confirmReplace) {
|
|
120
|
+
diff.deleted = diff.deleted.filter((file) => window.confirm(`${replaceFileTest} "${file.name}"?`));
|
|
121
|
+
// When unstable_confirmReplace becomes stable, we should stop returning
|
|
122
|
+
// `fileList` from getFileListUpdate and just always do this thing.
|
|
123
|
+
// That change will require changing the tests too, so let's wait.
|
|
124
|
+
updatedFileList = files
|
|
125
|
+
.filter((file) => !diff.deleted.includes(file))
|
|
126
|
+
.concat(added);
|
|
127
|
+
}
|
|
113
128
|
if (fileInput.current) {
|
|
114
|
-
fileInput.current.files = arrayToFileList(
|
|
129
|
+
fileInput.current.files = arrayToFileList(updatedFileList);
|
|
115
130
|
}
|
|
116
131
|
if (inputRef.current) {
|
|
117
132
|
// Empty on every add
|
|
118
133
|
inputRef.current.files = arrayToFileList([]);
|
|
119
134
|
}
|
|
120
|
-
onFilesUpdated(
|
|
135
|
+
onFilesUpdated(updatedFileList, diff);
|
|
121
136
|
};
|
|
122
137
|
return (React.createElement(FormField, Object.assign({ extraClassName: modifiedClass('FileInput', [multiple && 'multi']) }, fieldWrapperProps, { id: `${domid}-fake`, LabelTag: "h4", renderInput: (className, inputProps /* , addFocusProps */) => {
|
|
123
138
|
return (React.createElement("div", { className: className.control, ref: fileInputWrapper },
|
package/esm/Multiselect.d.ts
CHANGED
|
@@ -55,6 +55,8 @@ export type MultiselectProps = TogglerGroupFieldProps<string, {
|
|
|
55
55
|
forceSearchable?: boolean;
|
|
56
56
|
texts?: MultiselectI18n;
|
|
57
57
|
lang?: HannaLang;
|
|
58
|
+
/** Fires whenever the dropdown menu is opened or closed */
|
|
59
|
+
onDropdown?: (isOpen: boolean) => void;
|
|
58
60
|
};
|
|
59
61
|
export declare const Multiselect: {
|
|
60
62
|
(props: MultiselectProps): JSX.Element;
|
package/esm/Multiselect.js
CHANGED
|
@@ -54,7 +54,7 @@ const defaultTexts = {
|
|
|
54
54
|
},
|
|
55
55
|
};
|
|
56
56
|
export const Multiselect = (props) => {
|
|
57
|
-
const { onSelected, options: _options, disabled: _disabled, readOnly } = props;
|
|
57
|
+
const { onSelected, onDropdown, options: _options, disabled: _disabled, readOnly, } = props;
|
|
58
58
|
const disabled = _disabled === true;
|
|
59
59
|
const disableds = !disabled && _disabled;
|
|
60
60
|
const name = useDomid(props.name);
|
|
@@ -76,6 +76,9 @@ export const Multiselect = (props) => {
|
|
|
76
76
|
setSearchQuery('');
|
|
77
77
|
setActiveItemIndex(-1);
|
|
78
78
|
}
|
|
79
|
+
if (onDropdown && newIsOpen !== isOpen) {
|
|
80
|
+
onDropdown(newIsOpen);
|
|
81
|
+
}
|
|
79
82
|
return newIsOpen;
|
|
80
83
|
});
|
|
81
84
|
};
|
|
@@ -127,59 +130,53 @@ export const Multiselect = (props) => {
|
|
|
127
130
|
toggleOpen(activeItemIndex > -1 ? true : !isOpen);
|
|
128
131
|
}
|
|
129
132
|
};
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (!isOpen) {
|
|
133
|
+
const handleWrapperKeyDown = (e) => {
|
|
134
|
+
var _a;
|
|
135
|
+
if (!isOpen || !((_a = inputWrapperRef.current) === null || _a === void 0 ? void 0 : _a.contains(e.target))) {
|
|
133
136
|
return;
|
|
134
137
|
}
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
138
|
+
const inputElm = inputRef.current;
|
|
139
|
+
if (e.key === 'ArrowUp') {
|
|
140
|
+
e.preventDefault();
|
|
141
|
+
inputElm.focus();
|
|
142
|
+
setActiveItemIndex((prevIndex) => prevIndex === 0 ? filteredOptions.length - 1 : prevIndex - 1);
|
|
143
|
+
}
|
|
144
|
+
else if (e.key === 'ArrowDown') {
|
|
145
|
+
e.preventDefault();
|
|
146
|
+
inputElm.focus();
|
|
147
|
+
setActiveItemIndex((prevIndex) => prevIndex === filteredOptions.length - 1 ? 0 : prevIndex + 1);
|
|
148
|
+
}
|
|
149
|
+
else if (e.key === 'Escape') {
|
|
150
|
+
e.preventDefault();
|
|
151
|
+
inputElm.blur();
|
|
152
|
+
inputElm.focus();
|
|
153
|
+
toggleOpen(false);
|
|
154
|
+
}
|
|
155
|
+
else if (e.key === 'Enter' || e.key === ' ') {
|
|
156
|
+
if (e.target.closest('.Multiselect__currentvalues')) {
|
|
157
|
+
return;
|
|
146
158
|
}
|
|
147
|
-
|
|
159
|
+
const focusInRange = activeItemIndex >= 0 && activeItemIndex < filteredOptions.length;
|
|
160
|
+
if (focusInRange) {
|
|
148
161
|
e.preventDefault();
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
// components (e.g. screen readers) are in sync with the visual state.
|
|
164
|
-
let input;
|
|
165
|
-
inputWrapperRef
|
|
166
|
-
.current.querySelectorAll(`input[type="checkbox"]`)
|
|
167
|
-
.forEach((elm) => {
|
|
168
|
-
if (elm.value === selItem.value) {
|
|
169
|
-
input = elm;
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
|
-
input.checked = !input.checked;
|
|
173
|
-
handleCheckboxSelection(selItem);
|
|
174
|
-
}
|
|
162
|
+
const selItem = filteredOptions[activeItemIndex];
|
|
163
|
+
if (selItem) {
|
|
164
|
+
// Manually toggle the checkbox, to ensure that uncontrolled
|
|
165
|
+
// components (e.g. screen readers) are in sync with the visual state.
|
|
166
|
+
let input;
|
|
167
|
+
e.currentTarget
|
|
168
|
+
.querySelectorAll(`input[type="checkbox"]`)
|
|
169
|
+
.forEach((elm) => {
|
|
170
|
+
if (elm.value === selItem.value) {
|
|
171
|
+
input = elm;
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
input.checked = !input.checked;
|
|
175
|
+
handleCheckboxSelection(selItem);
|
|
175
176
|
}
|
|
176
177
|
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
return () => {
|
|
180
|
-
document.removeEventListener('keydown', handleKeyDown);
|
|
181
|
-
};
|
|
182
|
-
}, [activeItemIndex, filteredOptions, isOpen, handleCheckboxSelection, inputRef]);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
183
180
|
// Auto-close the dropdown when focus has left the building
|
|
184
181
|
useEffect(() => {
|
|
185
182
|
const wrapperDiv = inputWrapperRef.current;
|
|
@@ -207,7 +204,7 @@ export const Multiselect = (props) => {
|
|
|
207
204
|
}, [activeItemIndex]);
|
|
208
205
|
return (React.createElement(FormField, Object.assign({ extraClassName: modifiedClass('Multiselect', props.nowrap && 'nowrap'), group: "inputlike", filled: filled, empty: empty }, getFormFieldWrapperProps(props), { renderInput: (className, inputProps, addFocusProps, isBrowser) => {
|
|
209
206
|
const { id } = inputProps;
|
|
210
|
-
return (React.createElement("div", Object.assign({ className: modifiedClass('Multiselect__input', [isOpen && 'open'], className.input) }, addFocusProps(), { "data-sprinkled": isBrowser, ref: inputWrapperRef }),
|
|
207
|
+
return (React.createElement("div", Object.assign({ className: modifiedClass('Multiselect__input', [isOpen && 'open'], className.input) }, addFocusProps(), { "data-sprinkled": isBrowser, ref: inputWrapperRef, onKeyDown: handleWrapperKeyDown }),
|
|
211
208
|
!isBrowser ? null : isSearchable ? (React.createElement("input", { className: "Multiselect__search", id: `toggler:${id}`, "aria-label": texts.search, "aria-controls": id, "data-expanded": isOpen || undefined, onChange: handleInputChange, onKeyDown: handleInputKeyDown, onClick: () => toggleOpen(), value: searchQuery,
|
|
212
209
|
// onFocus={handleInputFocus}
|
|
213
210
|
placeholder: placeholderText, disabled: disabled, ref: inputRef })) : (React.createElement("button", { className: "Multiselect__toggler", id: `toggler:${id}`, type: "button", "aria-label": texts.buttonShow, "aria-controls": id, "aria-expanded": isOpen, onClick: () => toggleOpen(), disabled: disabled,
|