@reykjavik/hanna-react 0.10.63 → 0.10.66
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/AccordionList.js +6 -4
- package/Alert.d.ts +4 -4
- package/Alert.js +4 -6
- package/CHANGELOG.md +18 -2
- package/CityBlock.d.ts +6 -8
- package/FileInput/{FileInput.utils.d.ts → _FileInput.utils.d.ts} +0 -0
- package/FileInput/{FileInput.utils.js → _FileInput.utils.js} +0 -0
- package/FileInput/_FileInputFileList.d.ts +11 -0
- package/FileInput/_FileInputFileList.js +24 -0
- package/FileInput.d.ts +22 -6
- package/FileInput.js +65 -46
- package/IframeBlock.d.ts +4 -5
- package/Layout.d.ts +4 -5
- package/PageFilter.d.ts +4 -5
- package/README.md +8 -3
- package/TagPill.d.ts +4 -6
- package/VSpacer.d.ts +4 -6
- package/_abstract/_AbstractCarousel.d.ts +4 -7
- package/package.json +2 -2
- package/utils/useDidChange.d.ts +37 -0
- package/utils/useDidChange.js +47 -0
- package/utils/useMixedControlState.d.ts +75 -0
- package/utils/useMixedControlState.js +168 -0
- package/utils/useScrollbarWidthCSSVar.d.ts +16 -0
- package/utils/useScrollbarWidthCSSVar.js +18 -2
- package/utils.d.ts +3 -0
- package/utils.js +3 -0
package/AccordionList.js
CHANGED
|
@@ -6,13 +6,15 @@ const hooks_1 = require("@hugsmidjan/react/hooks");
|
|
|
6
6
|
const getBemClass_1 = tslib_1.__importDefault(require("@hugsmidjan/react/utils/getBemClass"));
|
|
7
7
|
const seenEffect_1 = require("./utils/seenEffect");
|
|
8
8
|
const AccordionListItem = (props) => {
|
|
9
|
-
const { title, content, id, disabled = false,
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
const { title, content, id, disabled = false, ssr } = props;
|
|
10
|
+
// TODO: Add controlled state support to this component, and then switch
|
|
11
|
+
// to usw the hooks exported from `utils/useMixecControlState.ts`
|
|
12
|
+
const [open, setOpen] = (0, react_1.useState)(props.defaultOpen);
|
|
13
|
+
const defaultOpen = (0, react_1.useRef)(props.defaultOpen);
|
|
12
14
|
const domid = (0, hooks_1.useDomid)();
|
|
13
15
|
const isBrowser = (0, hooks_1.useIsBrowserSide)(ssr);
|
|
14
16
|
const itemDisabled = (isBrowser && disabled) || !content;
|
|
15
|
-
return (react_1.default.createElement("div", { className: (0, getBemClass_1.default)('AccordionList__item', [itemDisabled && 'disabled']), id: id, "data-start-open": defaultOpen
|
|
17
|
+
return (react_1.default.createElement("div", { className: (0, getBemClass_1.default)('AccordionList__item', [itemDisabled && 'disabled']), id: id, "data-start-open": defaultOpen.current, "data-sprinkled": isBrowser },
|
|
16
18
|
react_1.default.createElement("h3", { className: "AccordionList__title" }, isBrowser ? (react_1.default.createElement("button", { type: "button", className: "AccordionList__button", "aria-controls": domid, "aria-expanded": open || undefined, onClick: () => {
|
|
17
19
|
setOpen(!open);
|
|
18
20
|
}, disabled: itemDisabled }, title)) : (title)),
|
package/Alert.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { MouseEvent, ReactNode } from 'react';
|
|
2
2
|
import { SSRSupport } from '@hugsmidjan/react/hooks';
|
|
3
|
+
import { EitherObj } from '@reykjavik/hanna-utils';
|
|
3
4
|
import { DefaultTexts } from '@reykjavik/hanna-utils/i18n';
|
|
4
5
|
export declare type AlertI18n = {
|
|
5
6
|
closeLabel: string;
|
|
@@ -24,7 +25,7 @@ export declare type AlertProps = {
|
|
|
24
25
|
texts?: AlertI18n;
|
|
25
26
|
lang?: string;
|
|
26
27
|
ssr?: SSRSupport;
|
|
27
|
-
} &
|
|
28
|
+
} & EitherObj<{
|
|
28
29
|
/** Seconds until the Alert auto-closes.
|
|
29
30
|
*
|
|
30
31
|
* Mosueover and keyboard focus resets the timer.
|
|
@@ -34,8 +35,7 @@ export declare type AlertProps = {
|
|
|
34
35
|
onClose?: () => void | boolean;
|
|
35
36
|
/** Callback that fires when the alert has closed/transitoned out */
|
|
36
37
|
onClosed: () => void;
|
|
37
|
-
}
|
|
38
|
-
autoClose?: never;
|
|
38
|
+
}, {
|
|
39
39
|
/**
|
|
40
40
|
* @deprecated This signature with the `event` argument will be removed in hanna-react v0.9
|
|
41
41
|
*
|
|
@@ -44,6 +44,6 @@ export declare type AlertProps = {
|
|
|
44
44
|
onClose?(event: MouseEvent): void | boolean;
|
|
45
45
|
/** Callback that fires after the alert has closed/transitoned out */
|
|
46
46
|
onClosed?(): void;
|
|
47
|
-
}
|
|
47
|
+
}>;
|
|
48
48
|
declare const Alert: (props: AlertProps) => JSX.Element;
|
|
49
49
|
export default Alert;
|
package/Alert.js
CHANGED
|
@@ -19,12 +19,10 @@ const useAutoClosing = (autoClose) => {
|
|
|
19
19
|
const freeze = () => setTemp((temp) => temp - 1);
|
|
20
20
|
return {
|
|
21
21
|
autoClosing: temp === 0,
|
|
22
|
-
autoClosingProps: Object.assign({ onMouseEnter: freeze, onMouseLeave: thaw, onFocus: freeze, onBlur: thaw }, (env_1.isPreact
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
: undefined)),
|
|
22
|
+
autoClosingProps: Object.assign({ onMouseEnter: freeze, onMouseLeave: thaw, onFocus: freeze, onBlur: thaw }, (env_1.isPreact && {
|
|
23
|
+
onfocusin: (e) => e.currentTarget !== e.target && freeze(),
|
|
24
|
+
onfocusout: (e) => e.currentTarget !== e.target && thaw(),
|
|
25
|
+
})),
|
|
28
26
|
};
|
|
29
27
|
};
|
|
30
28
|
exports.defaultAlertTexts = {
|
package/CHANGELOG.md
CHANGED
|
@@ -4,11 +4,27 @@
|
|
|
4
4
|
|
|
5
5
|
- ... <!-- Add new lines here. -->
|
|
6
6
|
|
|
7
|
-
## 0.10.
|
|
7
|
+
## 0.10.66
|
|
8
|
+
|
|
9
|
+
_2022-09-01_
|
|
10
|
+
|
|
11
|
+
- feat: Add `utils` hook `useDidChange`
|
|
12
|
+
- feat: Add `utils` hook `useMixedControlState`
|
|
13
|
+
- feat: Add `utils` hook `useScrollbarWidthCSSVar`
|
|
14
|
+
- fix: Stop hard-resetting `AccordionList`'s state on `defaultOpen` changes
|
|
15
|
+
|
|
16
|
+
## 0.10.63 – 0.10.65
|
|
8
17
|
|
|
9
18
|
_2022-08-29_
|
|
10
19
|
|
|
11
|
-
-
|
|
20
|
+
- feat: Changes to `FileInput`
|
|
21
|
+
- feat: Add prop `FileList` to suppress (`false`) or customize its rendering
|
|
22
|
+
- feat: Add props `multiple`, `accept`
|
|
23
|
+
- feat: Deprecate prop `dropzoneProps`
|
|
24
|
+
- fix: report deleted when adding files in single-file mode
|
|
25
|
+
- fix: Make `dropZoneProps` optional, as originally indented
|
|
26
|
+
- fix: Re-populate the file input on `value` prop change
|
|
27
|
+
- feat: Explicitly skip rendering input element when `name` is missing
|
|
12
28
|
- fix: Hide `Carousel` mouse-cursor scroll controls at start/end positions
|
|
13
29
|
- fix: Pass `id` and other HTML props to static (span) `TagPill`s
|
|
14
30
|
|
package/CityBlock.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { EitherObj } from '@reykjavik/hanna-utils';
|
|
1
2
|
import { Illustration } from '@reykjavik/hanna-utils/assets';
|
|
2
3
|
import { BlockItem } from './_abstract/_Block';
|
|
3
4
|
import { ImageProps } from './_abstract/_Image';
|
|
@@ -7,17 +8,14 @@ declare const types: {
|
|
|
7
8
|
largebox: boolean;
|
|
8
9
|
largeimage: boolean;
|
|
9
10
|
};
|
|
10
|
-
declare type CityBlockImageProps = {
|
|
11
|
-
illustration: Illustration;
|
|
12
|
-
image?: never;
|
|
13
|
-
} | {
|
|
14
|
-
image: ImageProps;
|
|
15
|
-
illustration?: never;
|
|
16
|
-
};
|
|
17
11
|
export declare type CityBlockProps = {
|
|
18
12
|
align?: Alignment;
|
|
19
13
|
type?: keyof typeof types;
|
|
20
14
|
content: BlockItem;
|
|
21
|
-
} &
|
|
15
|
+
} & EitherObj<{
|
|
16
|
+
illustration: Illustration;
|
|
17
|
+
}, {
|
|
18
|
+
image: ImageProps;
|
|
19
|
+
}> & SeenProp;
|
|
22
20
|
declare const CityBlock: (props: CityBlockProps) => JSX.Element;
|
|
23
21
|
export default CityBlock;
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { formatBytes } from './_FileInput.utils';
|
|
2
|
+
import { CustomFile } from './_FileInput.utils';
|
|
3
|
+
export declare type FileListProps = {
|
|
4
|
+
files: Array<CustomFile>;
|
|
5
|
+
showFileSize?: boolean;
|
|
6
|
+
showImagePreviews?: boolean;
|
|
7
|
+
removeFileText: string;
|
|
8
|
+
removeFile: (file: File | string) => void;
|
|
9
|
+
formatBytes: typeof formatBytes;
|
|
10
|
+
};
|
|
11
|
+
export declare const DefaultFileList: (props: FileListProps) => JSX.Element | null;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DefaultFileList = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const react_1 = tslib_1.__importDefault(require("react"));
|
|
6
|
+
const DefaultFileList = (props) => {
|
|
7
|
+
const { files, showFileSize, showImagePreviews, removeFileText, removeFile, formatBytes, } = props;
|
|
8
|
+
if (!files.length) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
return (react_1.default.createElement("ul", { className: "FileInput__filelist" }, files.map((file) => (react_1.default.createElement("li", { key: file.name, className: "FileInput__file" },
|
|
12
|
+
react_1.default.createElement("button", { className: "FileInput__file-remove", type: "button", onClick: () => removeFile(file), "aria-label": removeFileText }, removeFileText),
|
|
13
|
+
react_1.default.createElement("span", { className: "FileInput__fileinfo" },
|
|
14
|
+
showImagePreviews && file.preview && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
15
|
+
react_1.default.createElement("span", { className: "FileInput__preview" },
|
|
16
|
+
react_1.default.createElement("img", { src: file.preview })),
|
|
17
|
+
' ')),
|
|
18
|
+
react_1.default.createElement("span", { className: "FileInput__filename" }, file.name),
|
|
19
|
+
showFileSize && (react_1.default.createElement("small", { className: "FileInput__filesize" },
|
|
20
|
+
" - (",
|
|
21
|
+
formatBytes(file.size),
|
|
22
|
+
")"))))))));
|
|
23
|
+
};
|
|
24
|
+
exports.DefaultFileList = DefaultFileList;
|
package/FileInput.d.ts
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
1
|
+
import { FileListProps } from './FileInput/_FileInputFileList';
|
|
1
2
|
import { FormFieldWrappingProps } from './FormField';
|
|
2
|
-
declare type DropzonePropsProps = {
|
|
3
|
-
accept?: string;
|
|
4
|
-
multiple?: boolean;
|
|
5
|
-
};
|
|
6
3
|
export declare type FileInputProps = {
|
|
7
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Flags if the input should accept multiple, or just a single file at a time.
|
|
6
|
+
*
|
|
7
|
+
* Default: `true`
|
|
8
|
+
*/
|
|
9
|
+
multiple?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Accepted file mime type(s).
|
|
12
|
+
*
|
|
13
|
+
* Default: no restrictions.
|
|
14
|
+
*/
|
|
15
|
+
accept?: string | Array<string>;
|
|
8
16
|
dropzoneText: string | JSX.Element;
|
|
17
|
+
removeFileText: string;
|
|
9
18
|
showFileSize?: boolean;
|
|
10
19
|
showImagePreviews?: boolean;
|
|
11
|
-
|
|
20
|
+
FileList?: false | ((props: FileListProps) => JSX.Element | null);
|
|
12
21
|
onFilesUpdated?: (
|
|
13
22
|
/** Updated, full list of Files. */
|
|
14
23
|
files: Array<File>,
|
|
@@ -27,6 +36,13 @@ export declare type FileInputProps = {
|
|
|
27
36
|
}) => void;
|
|
28
37
|
name?: string;
|
|
29
38
|
value?: ReadonlyArray<File>;
|
|
39
|
+
/**
|
|
40
|
+
* @deprecated Use props `multiple`, `accept` instead (Will be removed in v0.11)
|
|
41
|
+
*/
|
|
42
|
+
dropzoneProps?: {
|
|
43
|
+
accept?: string;
|
|
44
|
+
multiple?: boolean;
|
|
45
|
+
};
|
|
30
46
|
} & FormFieldWrappingProps;
|
|
31
47
|
declare const FileInput: (props: FileInputProps) => JSX.Element;
|
|
32
48
|
export default FileInput;
|
package/FileInput.js
CHANGED
|
@@ -5,7 +5,8 @@ const react_1 = tslib_1.__importStar(require("react"));
|
|
|
5
5
|
const react_dropzone_1 = require("react-dropzone"); // https://react-dropzone.js.org/#!/Dropzone
|
|
6
6
|
const hooks_1 = require("@hugsmidjan/react/hooks");
|
|
7
7
|
const getBemClass_1 = tslib_1.__importDefault(require("@hugsmidjan/react/utils/getBemClass"));
|
|
8
|
-
const
|
|
8
|
+
const _FileInput_utils_1 = require("./FileInput/_FileInput.utils");
|
|
9
|
+
const _FileInputFileList_1 = require("./FileInput/_FileInputFileList");
|
|
9
10
|
const FormField_1 = tslib_1.__importDefault(require("./FormField"));
|
|
10
11
|
const arrayToFileList = (arr) => {
|
|
11
12
|
const fileList = new DataTransfer();
|
|
@@ -15,92 +16,110 @@ const arrayToFileList = (arr) => {
|
|
|
15
16
|
return fileList.files;
|
|
16
17
|
};
|
|
17
18
|
const FileInput = (props) => {
|
|
18
|
-
const { className, id, label, hideLabel, dropzoneProps = { multiple: true }, dropzoneText, removeFileText, assistText, disabled, invalid, errorMessage, required, reqText, onFilesUpdated = () => undefined, showFileSize, showImagePreviews, value = [] } = props, inputElementProps = tslib_1.__rest(props, ["className", "id", "label", "hideLabel", "dropzoneProps", "dropzoneText", "removeFileText", "assistText", "disabled", "invalid", "errorMessage", "required", "reqText", "onFilesUpdated", "showFileSize", "showImagePreviews", "value"]);
|
|
19
|
+
const { className, id, label, hideLabel, dropzoneProps = { multiple: true }, multiple = dropzoneProps.multiple, accept, dropzoneText, removeFileText, assistText, disabled, invalid, errorMessage, required, reqText, FileList = _FileInputFileList_1.DefaultFileList, onFilesUpdated = () => undefined, showFileSize, showImagePreviews, value = [] } = props, inputElementProps = tslib_1.__rest(props, ["className", "id", "label", "hideLabel", "dropzoneProps", "multiple", "accept", "dropzoneText", "removeFileText", "assistText", "disabled", "invalid", "errorMessage", "required", "reqText", "FileList", "onFilesUpdated", "showFileSize", "showImagePreviews", "value"]);
|
|
19
20
|
const domid = (0, hooks_1.useDomid)(id);
|
|
20
21
|
const fileInputWrapper = (0, react_1.useRef)(null);
|
|
21
22
|
const fileInput = (0, react_1.useRef)(null);
|
|
22
23
|
const files = value;
|
|
23
24
|
const [isHover, setIsHover] = (0, react_1.useState)(false);
|
|
24
|
-
const { getRootProps, getInputProps, isDragReject, inputRef } = (0, react_dropzone_1.useDropzone)(
|
|
25
|
+
const { getRootProps, getInputProps, isDragReject, inputRef } = (0, react_dropzone_1.useDropzone)({
|
|
26
|
+
onDrop: (acceptedFiles) => {
|
|
25
27
|
acceptedFiles = acceptedFiles.map((file) => {
|
|
26
|
-
(0,
|
|
28
|
+
(0, _FileInput_utils_1.addPreview)(file);
|
|
27
29
|
return file;
|
|
28
30
|
});
|
|
29
|
-
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
32
|
+
addFiles(acceptedFiles);
|
|
30
33
|
setIsHover(false);
|
|
31
|
-
},
|
|
34
|
+
},
|
|
35
|
+
onDropRejected: (rejectedFiles) => {
|
|
32
36
|
window.alert('Error:\n' +
|
|
33
37
|
rejectedFiles
|
|
34
38
|
.map((elm) => {
|
|
35
39
|
return elm.name;
|
|
36
40
|
})
|
|
37
41
|
.join(', '));
|
|
38
|
-
},
|
|
42
|
+
},
|
|
43
|
+
onDragEnter: () => {
|
|
39
44
|
// 'dragLeave' always fires right after 'dragEnter', use 'dragOver' instead
|
|
40
45
|
// console.log('enter');
|
|
41
46
|
// setIsHover(true);
|
|
42
|
-
},
|
|
47
|
+
},
|
|
48
|
+
onDragLeave: () => {
|
|
43
49
|
// console.log('leave');
|
|
44
50
|
setIsHover(false);
|
|
45
|
-
},
|
|
51
|
+
},
|
|
52
|
+
onDragOver: () => {
|
|
46
53
|
// TODO: add error icon? 'isDragReject' gives unstable results
|
|
47
54
|
// console.log(isDragReject);
|
|
48
55
|
setIsHover(true);
|
|
49
|
-
}
|
|
50
|
-
|
|
56
|
+
},
|
|
57
|
+
multiple,
|
|
58
|
+
accept,
|
|
59
|
+
});
|
|
60
|
+
// Synchronoyusly add previews on incoming files
|
|
51
61
|
// (NOTE: `addPreview` ignores files that already have preview.)
|
|
52
|
-
files.forEach(
|
|
53
|
-
(0, react_1.useEffect)(() =>
|
|
54
|
-
|
|
55
|
-
|
|
62
|
+
files.forEach(_FileInput_utils_1.addPreview);
|
|
63
|
+
(0, react_1.useEffect)(() => {
|
|
64
|
+
if (fileInput.current) {
|
|
65
|
+
fileInput.current.files = arrayToFileList(files);
|
|
66
|
+
}
|
|
67
|
+
return () => {
|
|
68
|
+
// Make sure to revoke the data uris on unmount to avoid memory leaks
|
|
69
|
+
files.forEach(_FileInput_utils_1.releasePreview);
|
|
70
|
+
};
|
|
56
71
|
}, [files]);
|
|
57
|
-
const removeFile = (
|
|
72
|
+
const removeFile = (removeTarget) => {
|
|
73
|
+
const deleted = [];
|
|
74
|
+
const targetName = typeof removeTarget !== 'string' ? removeTarget.name : removeTarget;
|
|
75
|
+
const fileList = files.filter((file) => {
|
|
76
|
+
if (file.name !== targetName) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
deleted.push(file);
|
|
80
|
+
(0, _FileInput_utils_1.releasePreview)(file);
|
|
81
|
+
return false;
|
|
82
|
+
});
|
|
58
83
|
if (fileInput.current) {
|
|
59
|
-
|
|
60
|
-
const newFileList = files.filter((file) => {
|
|
61
|
-
if (file.name !== name) {
|
|
62
|
-
return true;
|
|
63
|
-
}
|
|
64
|
-
deleted.push(file);
|
|
65
|
-
(0, FileInput_utils_1.releasePreview)(file);
|
|
66
|
-
return false;
|
|
67
|
-
});
|
|
68
|
-
fileInput.current.files = arrayToFileList(newFileList);
|
|
69
|
-
onFilesUpdated(newFileList, { deleted });
|
|
84
|
+
fileInput.current.files = arrayToFileList(fileList);
|
|
70
85
|
}
|
|
86
|
+
onFilesUpdated(fileList, { deleted });
|
|
71
87
|
};
|
|
72
88
|
const addFiles = (added) => {
|
|
89
|
+
const { fileList, diff } = (0, _FileInput_utils_1.getFileListUpdate)(files, added, !multiple);
|
|
73
90
|
if (fileInput.current) {
|
|
74
|
-
const { fileList, diff } = (0, FileInput_utils_1.getFileListUpdate)(files, added, !dropzoneProps.multiple);
|
|
75
91
|
fileInput.current.files = arrayToFileList(fileList);
|
|
76
|
-
onFilesUpdated(fileList, diff);
|
|
77
92
|
}
|
|
78
93
|
if (inputRef.current) {
|
|
79
94
|
// Empty on every add
|
|
80
95
|
inputRef.current.files = arrayToFileList([]);
|
|
81
96
|
}
|
|
97
|
+
onFilesUpdated(fileList, diff);
|
|
82
98
|
};
|
|
83
|
-
|
|
84
|
-
react_1.default.createElement("button", { className: "FileInput__file-remove", type: "button", onClick: () => removeFile(file.name), "aria-label": removeFileText }, removeFileText),
|
|
85
|
-
react_1.default.createElement("span", { className: "FileInput__fileinfo" },
|
|
86
|
-
showImagePreviews && file.preview && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
87
|
-
react_1.default.createElement("span", { className: "FileInput__preview" },
|
|
88
|
-
react_1.default.createElement("img", { src: file.preview })),
|
|
89
|
-
' ')),
|
|
90
|
-
react_1.default.createElement("span", { className: "FileInput__filename" }, file.name),
|
|
91
|
-
showFileSize && (react_1.default.createElement("small", { className: "FileInput__filesize" },
|
|
92
|
-
" - (",
|
|
93
|
-
(0, FileInput_utils_1.formatBytes)(file.size),
|
|
94
|
-
")"))))));
|
|
95
|
-
return (react_1.default.createElement(FormField_1.default, { className: (0, getBemClass_1.default)('FileInput', [dropzoneProps.multiple && 'multi'], className), label: label, id: domid + '-fake', LabelTag: "h4", assistText: assistText, hideLabel: hideLabel, disabled: disabled, invalid: invalid, errorMessage: errorMessage, required: required, reqText: reqText, renderInput: (className, inputProps /* , addFocusProps */) => {
|
|
99
|
+
return (react_1.default.createElement(FormField_1.default, { className: (0, getBemClass_1.default)('FileInput', [multiple && 'multi'], className), label: label, id: domid + '-fake', LabelTag: "h4", assistText: assistText, hideLabel: hideLabel, disabled: disabled, invalid: invalid, errorMessage: errorMessage, required: required, reqText: reqText, renderInput: (className, inputProps /* , addFocusProps */) => {
|
|
96
100
|
return (react_1.default.createElement("div", { className: className.control, ref: fileInputWrapper },
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
101
|
+
// Explicitly skip rendering of input element if no
|
|
102
|
+
// name prop is provided. This is implicitly what the
|
|
103
|
+
// browser does on form submit.
|
|
104
|
+
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#name
|
|
105
|
+
// In such cases we assume the application controls the upload/submit
|
|
106
|
+
// behavior separately outside of this component.
|
|
107
|
+
inputElementProps.name ? (react_1.default.createElement("input", { className: "FileInput__input", name: inputElementProps.name, id: domid, ref: fileInput, type: "file", style: { display: 'none' }, multiple: multiple || undefined, required: inputProps.required })) : null,
|
|
108
|
+
react_1.default.createElement("input", Object.assign({
|
|
109
|
+
// fake input exclusively used to capture clicks and file drops.
|
|
110
|
+
// it's contents are wiped on every "add" action.
|
|
111
|
+
className: "FileInput__input--fake" }, getInputProps(), { tabIndex: undefined, style: undefined, multiple: multiple || undefined }, inputProps, { required: undefined })),
|
|
100
112
|
' ',
|
|
101
113
|
react_1.default.createElement("div", Object.assign({ className: (0, getBemClass_1.default)('FileInput__dropzone', [isHover && 'highlight']) }, getRootProps({ isDragReject }), { tabIndex: undefined }),
|
|
102
114
|
react_1.default.createElement("p", { className: "FileInput__droptext" }, dropzoneText)),
|
|
103
|
-
|
|
115
|
+
FileList && (react_1.default.createElement(FileList, Object.assign({}, {
|
|
116
|
+
files,
|
|
117
|
+
showFileSize,
|
|
118
|
+
showImagePreviews,
|
|
119
|
+
removeFileText,
|
|
120
|
+
removeFile,
|
|
121
|
+
formatBytes: _FileInput_utils_1.formatBytes,
|
|
122
|
+
})))));
|
|
104
123
|
} }));
|
|
105
124
|
};
|
|
106
125
|
exports.default = FileInput;
|
package/IframeBlock.d.ts
CHANGED
|
@@ -1,20 +1,19 @@
|
|
|
1
|
+
import { EitherObj } from '@reykjavik/hanna-utils';
|
|
1
2
|
import { ResizerOptions } from 'iframe-resizer-react';
|
|
2
3
|
export declare type IframeBlockProps = {
|
|
3
4
|
src: string;
|
|
4
5
|
framed?: boolean;
|
|
5
6
|
compact?: boolean;
|
|
6
7
|
align?: 'right';
|
|
7
|
-
} &
|
|
8
|
+
} & EitherObj<{
|
|
8
9
|
/** Default: `'auto'` ... which initializes "iframe-resizer" script */
|
|
9
10
|
height?: 'auto';
|
|
10
|
-
scrolling?: never;
|
|
11
11
|
/** Default: `false` ... Set to `true` for same-site only, or provide array of allowed domain-names */
|
|
12
12
|
checkOrigin?: ResizerOptions['checkOrigin'];
|
|
13
|
-
}
|
|
13
|
+
}, {
|
|
14
14
|
height: number;
|
|
15
15
|
scrolling?: boolean | 'no' | 'yes';
|
|
16
|
-
|
|
17
|
-
});
|
|
16
|
+
}>;
|
|
18
17
|
/**
|
|
19
18
|
* When `height` is undefined or "auto", then Add the following code-snipped to the iframed page:
|
|
20
19
|
*
|
package/Layout.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { ReactNode } from 'react';
|
|
|
2
2
|
import { SSRSupport } from '@hugsmidjan/react/hooks';
|
|
3
3
|
import { BemPropsModifier } from '@hugsmidjan/react/types';
|
|
4
4
|
import { HannaColorTheme } from '@reykjavik/hanna-css';
|
|
5
|
+
import { EitherObj } from '@reykjavik/hanna-utils';
|
|
5
6
|
import { DefaultTexts } from '@reykjavik/hanna-utils/i18n';
|
|
6
7
|
export declare type LayoutI18n = {
|
|
7
8
|
lang?: string;
|
|
@@ -22,12 +23,10 @@ declare type LayoutProps = {
|
|
|
22
23
|
ssr?: SSRSupport;
|
|
23
24
|
texts?: LayoutI18n;
|
|
24
25
|
lang?: string;
|
|
25
|
-
} &
|
|
26
|
+
} & EitherObj<{
|
|
26
27
|
mainChildren: ReactNode;
|
|
27
|
-
|
|
28
|
-
} | {
|
|
29
|
-
mainChildren?: never;
|
|
28
|
+
}, {
|
|
30
29
|
children: ReactNode;
|
|
31
|
-
}
|
|
30
|
+
}>;
|
|
32
31
|
declare const Layout: (props: LayoutProps) => JSX.Element;
|
|
33
32
|
export default Layout;
|
package/PageFilter.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { EitherObj } from '@reykjavik/hanna-utils';
|
|
2
3
|
import { SeenProp } from './utils/seenEffect';
|
|
3
4
|
export declare type PageFilterProps = {
|
|
4
5
|
title: string;
|
|
@@ -6,12 +7,10 @@ export declare type PageFilterProps = {
|
|
|
6
7
|
footnote?: React.ReactNode;
|
|
7
8
|
buttonRow?: React.ReactNode;
|
|
8
9
|
underlap?: boolean;
|
|
9
|
-
} &
|
|
10
|
+
} & EitherObj<{
|
|
10
11
|
filters: React.ReactNode;
|
|
11
|
-
|
|
12
|
-
} | {
|
|
13
|
-
filters?: never;
|
|
12
|
+
}, {
|
|
14
13
|
children: React.ReactNode;
|
|
15
|
-
}
|
|
14
|
+
}> & SeenProp;
|
|
16
15
|
declare const PageFilter: (props: PageFilterProps) => JSX.Element;
|
|
17
16
|
export default PageFilter;
|
package/README.md
CHANGED
|
@@ -3,16 +3,21 @@
|
|
|
3
3
|
The official React components for Hanna – Reykjavík's design-system
|
|
4
4
|
|
|
5
5
|
```
|
|
6
|
-
|
|
6
|
+
yarn add @reykjavik/hanna-react
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
+
Components aim to be framework-agnostic and avoid unneccessary local state –
|
|
10
|
+
always preferring "controlled" use.
|
|
11
|
+
|
|
12
|
+
(See [README-conventions.md](./README-conventions.md) for more info.)
|
|
13
|
+
|
|
9
14
|
## Versioning
|
|
10
15
|
|
|
11
16
|
This module always targets the most recent version of the Hanna markup
|
|
12
17
|
patterns (currently **Hanna 0.8**).
|
|
13
18
|
|
|
14
19
|
<!--
|
|
15
|
-
NOTE
|
|
20
|
+
**NOTE:**
|
|
16
21
|
If need arises we may decide to branch the repo and publish separate
|
|
17
22
|
legacy modules (i.e. `@reykjavik/hanna_1-react`) that provide active
|
|
18
23
|
long-term-support for older major-versions of Hanna's markup patterns.
|
|
@@ -32,7 +37,7 @@ version, you'll find the appropriate package version in the
|
|
|
32
37
|
## CSS
|
|
33
38
|
|
|
34
39
|
Each component is paired with a CSS file that can be loaded via the Hanna CSS
|
|
35
|
-
server – https://styles.reykjavik.is
|
|
40
|
+
server – <https://styles.reykjavik.is>
|
|
36
41
|
|
|
37
42
|
If your project uses `<Layout/>`, `<HeroBlock/>`, `<TextInput/>`,
|
|
38
43
|
`<Selectbox/>` and `<ButtonPrimary/>` you can load the required CSS by linking
|
package/TagPill.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
|
+
import { EitherObj } from '@reykjavik/hanna-utils';
|
|
2
3
|
import { ButtonProps } from './_abstract/_Button';
|
|
3
4
|
declare const colors: {
|
|
4
5
|
readonly normal: "";
|
|
@@ -12,16 +13,13 @@ export declare type TagPillProps = ButtonProps & {
|
|
|
12
13
|
children?: ReactNode;
|
|
13
14
|
large?: boolean;
|
|
14
15
|
color?: TagPillColor;
|
|
15
|
-
} &
|
|
16
|
+
} & EitherObj<{
|
|
16
17
|
removable?: false;
|
|
17
|
-
|
|
18
|
-
removeLabel?: never;
|
|
19
|
-
removeLabelLong?: never;
|
|
20
|
-
} | {
|
|
18
|
+
}, {
|
|
21
19
|
removable: true;
|
|
22
20
|
onRemove?: () => void;
|
|
23
21
|
removeLabel?: string;
|
|
24
22
|
removeLabelLong?: string;
|
|
25
|
-
}
|
|
23
|
+
}>;
|
|
26
24
|
declare const TagPill: (props: TagPillProps) => JSX.Element;
|
|
27
25
|
export default TagPill;
|
package/VSpacer.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
|
+
import { EitherObj } from '@reykjavik/hanna-utils';
|
|
2
3
|
declare const sizes: {
|
|
3
4
|
readonly none: "none";
|
|
4
5
|
readonly small: "small";
|
|
@@ -9,16 +10,13 @@ declare const sizes: {
|
|
|
9
10
|
};
|
|
10
11
|
declare type VSpacerSize = keyof typeof sizes;
|
|
11
12
|
declare type VSpacerSizePos = Exclude<VSpacerSize, 'none'>;
|
|
12
|
-
export declare type VSpacerProps = {
|
|
13
|
-
children?: never;
|
|
13
|
+
export declare type VSpacerProps = EitherObj<{
|
|
14
14
|
size?: VSpacerSizePos;
|
|
15
|
-
|
|
16
|
-
bottom?: never;
|
|
17
|
-
} | {
|
|
15
|
+
}, {
|
|
18
16
|
children: ReactNode;
|
|
19
17
|
size?: VSpacerSizePos;
|
|
20
18
|
top?: VSpacerSize;
|
|
21
19
|
bottom?: VSpacerSize;
|
|
22
|
-
}
|
|
20
|
+
}>;
|
|
23
21
|
declare const VSpacer: (props: VSpacerProps) => JSX.Element;
|
|
24
22
|
export default VSpacer;
|
|
@@ -1,23 +1,20 @@
|
|
|
1
1
|
import { ReactElement } from 'react';
|
|
2
2
|
import { SSRSupport } from '@hugsmidjan/react/hooks';
|
|
3
3
|
import { BemProps } from '@hugsmidjan/react/types';
|
|
4
|
+
import { EitherObj } from '@reykjavik/hanna-utils';
|
|
4
5
|
import { SeenProp } from '../utils/seenEffect';
|
|
5
6
|
export declare type CarouselProps<I extends Record<string, unknown> = {}, P extends Record<string, unknown> | undefined = {}> = {
|
|
6
7
|
className?: string;
|
|
7
8
|
ssr?: SSRSupport;
|
|
8
9
|
/** @deprecated Ingored because never used (Will be removed in v0.11) */
|
|
9
10
|
scrollRight?: boolean;
|
|
10
|
-
} &
|
|
11
|
-
children?: never;
|
|
11
|
+
} & EitherObj<{
|
|
12
12
|
items: Array<I>;
|
|
13
13
|
Component: (props: P extends undefined ? I : I & P) => ReactElement | null;
|
|
14
14
|
ComponentProps?: P;
|
|
15
|
-
}
|
|
15
|
+
}, {
|
|
16
16
|
children: Array<ReactElement>;
|
|
17
|
-
|
|
18
|
-
Component?: never;
|
|
19
|
-
ComponentProps?: never;
|
|
20
|
-
}) & SeenProp;
|
|
17
|
+
}> & SeenProp;
|
|
21
18
|
declare type AbstractCarouselProps<I extends Record<string, unknown> = {}, P extends Record<string, unknown> | undefined = {}> = CarouselProps<I, P> & BemProps & {
|
|
22
19
|
title?: string;
|
|
23
20
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reykjavik/hanna-react",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.66",
|
|
4
4
|
"author": "Reykjavík (http://www.reykjavik.is)",
|
|
5
5
|
"contributors": [
|
|
6
6
|
"Hugsmiðjan ehf (http://www.hugsmidjan.is)",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"@hugsmidjan/qj": "^4.10.2",
|
|
17
17
|
"@hugsmidjan/react": "^0.4.17",
|
|
18
18
|
"@reykjavik/hanna-css": "^0.3.7",
|
|
19
|
-
"@reykjavik/hanna-utils": "^0.1.
|
|
19
|
+
"@reykjavik/hanna-utils": "^0.1.12",
|
|
20
20
|
"@types/react": "^17.0.24",
|
|
21
21
|
"@types/react-autosuggest": "^10.1.0",
|
|
22
22
|
"@types/react-datepicker": "^3.0.2",
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reports if value changed since last time the hook was called.
|
|
3
|
+
*
|
|
4
|
+
* Returns an `{ lastValue }` shaped object, when change is detected.
|
|
5
|
+
* Returns `undefined` otherwise
|
|
6
|
+
*
|
|
7
|
+
* Common usage is if you want an component which is effectively uncontrolled,
|
|
8
|
+
* but resets/changes its internal state whenever a certain prop value changes.
|
|
9
|
+
*
|
|
10
|
+
* ```tsx
|
|
11
|
+
* import { useDidChange } from './utils';
|
|
12
|
+
* // import { useDidChange } from '@reykjavik/hanna-react/utils';
|
|
13
|
+
*
|
|
14
|
+
* // inside your component/hook
|
|
15
|
+
* const [visible, setVisible] = useState(props.visible);
|
|
16
|
+
* if (useDidChange(props.visible)) {
|
|
17
|
+
* setVisible(props.visible);
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* Another use case might be to capture not only IF but HOW a prop value changed
|
|
22
|
+
* in a controlled component
|
|
23
|
+
*
|
|
24
|
+
* ```tsx
|
|
25
|
+
* const [trend, setTrend] = useState(null);
|
|
26
|
+
* const countChanged = useDidChange(props.count);
|
|
27
|
+
* if (countChanged) {
|
|
28
|
+
* setTrend(props.count > countChanged.lastValue ? 'increasing' : 'decreasing');
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* **NOTE:** This hook should be handled with care, as its overuse can easily lead
|
|
33
|
+
* to poorly structured and buggy component behavior.
|
|
34
|
+
*/
|
|
35
|
+
export declare const useDidChange: <T>(value: T) => {
|
|
36
|
+
lastValue: T;
|
|
37
|
+
} | undefined;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useDidChange = void 0;
|
|
4
|
+
const react_1 = require("react");
|
|
5
|
+
/**
|
|
6
|
+
* Reports if value changed since last time the hook was called.
|
|
7
|
+
*
|
|
8
|
+
* Returns an `{ lastValue }` shaped object, when change is detected.
|
|
9
|
+
* Returns `undefined` otherwise
|
|
10
|
+
*
|
|
11
|
+
* Common usage is if you want an component which is effectively uncontrolled,
|
|
12
|
+
* but resets/changes its internal state whenever a certain prop value changes.
|
|
13
|
+
*
|
|
14
|
+
* ```tsx
|
|
15
|
+
* import { useDidChange } from './utils';
|
|
16
|
+
* // import { useDidChange } from '@reykjavik/hanna-react/utils';
|
|
17
|
+
*
|
|
18
|
+
* // inside your component/hook
|
|
19
|
+
* const [visible, setVisible] = useState(props.visible);
|
|
20
|
+
* if (useDidChange(props.visible)) {
|
|
21
|
+
* setVisible(props.visible);
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* Another use case might be to capture not only IF but HOW a prop value changed
|
|
26
|
+
* in a controlled component
|
|
27
|
+
*
|
|
28
|
+
* ```tsx
|
|
29
|
+
* const [trend, setTrend] = useState(null);
|
|
30
|
+
* const countChanged = useDidChange(props.count);
|
|
31
|
+
* if (countChanged) {
|
|
32
|
+
* setTrend(props.count > countChanged.lastValue ? 'increasing' : 'decreasing');
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* **NOTE:** This hook should be handled with care, as its overuse can easily lead
|
|
37
|
+
* to poorly structured and buggy component behavior.
|
|
38
|
+
*/
|
|
39
|
+
const useDidChange = (value) => {
|
|
40
|
+
const lastValueRef = (0, react_1.useRef)(value);
|
|
41
|
+
const lastValue = lastValueRef.current;
|
|
42
|
+
if (value !== lastValue) {
|
|
43
|
+
lastValueRef.current = value;
|
|
44
|
+
return { lastValue };
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
exports.useDidChange = useDidChange;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Dispatch, SetStateAction } from 'react';
|
|
2
|
+
declare type DefaultProp<N extends string> = `default${Capitalize<N>}`;
|
|
3
|
+
declare type PropPair<N extends string> = N | DefaultProp<N>;
|
|
4
|
+
declare type StrictKeys<P extends Record<string, unknown>, N extends string> = PropPair<N> extends keyof P ? P : {
|
|
5
|
+
[Key in PropPair<N>]: P[Key];
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* State hook to simplify dealing with a the complexities of supporting a mixture
|
|
9
|
+
* of "controlled" and "uncontrolled" component state.
|
|
10
|
+
*
|
|
11
|
+
* The returned value and dispatcher/setter function return the controlled
|
|
12
|
+
* `value`, but gracefully handle changes in defaultValue in uncontrolled mode,
|
|
13
|
+
* and handles (unexpected) "mode-changes" in a predictable manner.
|
|
14
|
+
*
|
|
15
|
+
* It assumes (by default) that the calling component has
|
|
16
|
+
* a pair of props following the naming convention `foo` and `defaultFoo` —
|
|
17
|
+
* similar to React's own `<input/>` and `<select/>` HTML components warn about
|
|
18
|
+
* their `value` and `defaultValue` props being misused.
|
|
19
|
+
*
|
|
20
|
+
* NOTE: This hook also exposes a slightly lower-level helper hook
|
|
21
|
+
* `useMixedControlState.raw(value, defaultValue)`, for cases where you don't
|
|
22
|
+
* have a neatly-shaped props object as described above, or you need to do
|
|
23
|
+
* some sort of pre-processing of either prop value.
|
|
24
|
+
*
|
|
25
|
+
* ```tsx
|
|
26
|
+
* import React, { FC, ReactNode } from 'react';
|
|
27
|
+
* import { useMixedControlState } from '@reykjavik/hanna-react/utils';
|
|
28
|
+
*
|
|
29
|
+
* type FooBarProps = {
|
|
30
|
+
* visible?: boolean;
|
|
31
|
+
* onChange?: (newVisible: boolean) => void;
|
|
32
|
+
* defaultVisible?: boolean;
|
|
33
|
+
* };
|
|
34
|
+
*
|
|
35
|
+
* export const FooBar: FC<FooBarProps> = (props) => {
|
|
36
|
+
* const [visible, setVisible] = useMixedControlState(props, 'visible', true);
|
|
37
|
+
*
|
|
38
|
+
* const handleToggle = () => {
|
|
39
|
+
* props.onChange?.(!visible);
|
|
40
|
+
* setVisible(!visible);
|
|
41
|
+
* };
|
|
42
|
+
* return (
|
|
43
|
+
* <div>
|
|
44
|
+
* <button onClick={handleToggle}>Toggle</button>
|
|
45
|
+
* <div hidden={!visible}>{props.children}</div>
|
|
46
|
+
* </div>
|
|
47
|
+
* );
|
|
48
|
+
* };
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export declare const useMixedControlState: {
|
|
52
|
+
<N extends string, P extends { [x in PropPair<N>]?: unknown; }>(props: StrictKeys<P, N>, name: N, defaultDefault?: P[`default${Capitalize<N>}`] | undefined): [StrictKeys<P, N>[N] | StrictKeys<P, N>[`default${Capitalize<N>}`] | undefined, Dispatch<SetStateAction<StrictKeys<P, N>[N] | StrictKeys<P, N>[`default${Capitalize<N>}`] | undefined>>];
|
|
53
|
+
/**
|
|
54
|
+
* a slightly lower-level hook alternative to
|
|
55
|
+
* `useMixedControlState(props, name)`, for cases where you don't
|
|
56
|
+
* have a neatly-/conventionally-shaped props object, or if you need to do
|
|
57
|
+
* some sort of pre-processing of either prop value.
|
|
58
|
+
*
|
|
59
|
+
* ```tsx
|
|
60
|
+
* import { useMixedControlState } from '@reykjavik/hanna-react/utils';
|
|
61
|
+
*
|
|
62
|
+
* declare const props: { visible?: boolean; defaultVisible?: boolean };
|
|
63
|
+
*
|
|
64
|
+
* const [vislble, setVisible] = useMixedControlState.raw(
|
|
65
|
+
* props.vislble,
|
|
66
|
+
* props.defaultVisible,
|
|
67
|
+
* 'visible'
|
|
68
|
+
* );
|
|
69
|
+
* // has the same effect as this:
|
|
70
|
+
* const [visible, setVisible] = useMixedControlState(props, 'visible');
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
raw<C, U>(value: C, defaultValue: U, warningPropName?: string): [C | U, Dispatch<SetStateAction<C | U>>];
|
|
74
|
+
};
|
|
75
|
+
export {};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useMixedControlState = void 0;
|
|
4
|
+
const react_1 = require("react");
|
|
5
|
+
const hanna_utils_1 = require("@reykjavik/hanna-utils");
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
/**
|
|
8
|
+
* State hook to simplify dealing with a the complexities of supporting a mixture
|
|
9
|
+
* of "controlled" and "uncontrolled" component state.
|
|
10
|
+
*
|
|
11
|
+
* The returned value and dispatcher/setter function return the controlled
|
|
12
|
+
* `value`, but gracefully handle changes in defaultValue in uncontrolled mode,
|
|
13
|
+
* and handles (unexpected) "mode-changes" in a predictable manner.
|
|
14
|
+
*
|
|
15
|
+
* It assumes (by default) that the calling component has
|
|
16
|
+
* a pair of props following the naming convention `foo` and `defaultFoo` —
|
|
17
|
+
* similar to React's own `<input/>` and `<select/>` HTML components warn about
|
|
18
|
+
* their `value` and `defaultValue` props being misused.
|
|
19
|
+
*
|
|
20
|
+
* NOTE: This hook also exposes a slightly lower-level helper hook
|
|
21
|
+
* `useMixedControlState.raw(value, defaultValue)`, for cases where you don't
|
|
22
|
+
* have a neatly-shaped props object as described above, or you need to do
|
|
23
|
+
* some sort of pre-processing of either prop value.
|
|
24
|
+
*
|
|
25
|
+
* ```tsx
|
|
26
|
+
* import React, { FC, ReactNode } from 'react';
|
|
27
|
+
* import { useMixedControlState } from '@reykjavik/hanna-react/utils';
|
|
28
|
+
*
|
|
29
|
+
* type FooBarProps = {
|
|
30
|
+
* visible?: boolean;
|
|
31
|
+
* onChange?: (newVisible: boolean) => void;
|
|
32
|
+
* defaultVisible?: boolean;
|
|
33
|
+
* };
|
|
34
|
+
*
|
|
35
|
+
* export const FooBar: FC<FooBarProps> = (props) => {
|
|
36
|
+
* const [visible, setVisible] = useMixedControlState(props, 'visible', true);
|
|
37
|
+
*
|
|
38
|
+
* const handleToggle = () => {
|
|
39
|
+
* props.onChange?.(!visible);
|
|
40
|
+
* setVisible(!visible);
|
|
41
|
+
* };
|
|
42
|
+
* return (
|
|
43
|
+
* <div>
|
|
44
|
+
* <button onClick={handleToggle}>Toggle</button>
|
|
45
|
+
* <div hidden={!visible}>{props.children}</div>
|
|
46
|
+
* </div>
|
|
47
|
+
* );
|
|
48
|
+
* };
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
const useMixedControlState = (
|
|
52
|
+
/** The props object of your component */
|
|
53
|
+
props,
|
|
54
|
+
/** Name of the prop for the controlled value */
|
|
55
|
+
name,
|
|
56
|
+
/**
|
|
57
|
+
* A last-resort default value for the defaultValue prop
|
|
58
|
+
*
|
|
59
|
+
* Used as uncontrolled default if the `default${capitalize(name)}` value
|
|
60
|
+
* of `props` is missing/undefined.
|
|
61
|
+
*/
|
|
62
|
+
defaultDefault) => {
|
|
63
|
+
let defaultValue = props[`default${(0, hanna_utils_1.capitalize)(name)}`];
|
|
64
|
+
if (defaultValue === undefined) {
|
|
65
|
+
defaultValue = defaultDefault;
|
|
66
|
+
}
|
|
67
|
+
return exports.useMixedControlState.raw(props[name], defaultValue, name);
|
|
68
|
+
};
|
|
69
|
+
exports.useMixedControlState = useMixedControlState;
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
/**
|
|
72
|
+
* a slightly lower-level hook alternative to
|
|
73
|
+
* `useMixedControlState(props, name)`, for cases where you don't
|
|
74
|
+
* have a neatly-/conventionally-shaped props object, or if you need to do
|
|
75
|
+
* some sort of pre-processing of either prop value.
|
|
76
|
+
*
|
|
77
|
+
* ```tsx
|
|
78
|
+
* import { useMixedControlState } from '@reykjavik/hanna-react/utils';
|
|
79
|
+
*
|
|
80
|
+
* declare const props: { visible?: boolean; defaultVisible?: boolean };
|
|
81
|
+
*
|
|
82
|
+
* const [vislble, setVisible] = useMixedControlState.raw(
|
|
83
|
+
* props.vislble,
|
|
84
|
+
* props.defaultVisible,
|
|
85
|
+
* 'visible'
|
|
86
|
+
* );
|
|
87
|
+
* // has the same effect as this:
|
|
88
|
+
* const [visible, setVisible] = useMixedControlState(props, 'visible');
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
exports.useMixedControlState.raw = (
|
|
92
|
+
/** Controlled value. */
|
|
93
|
+
value,
|
|
94
|
+
/** Default/initial value for uncontrolled use. */
|
|
95
|
+
defaultValue,
|
|
96
|
+
/**
|
|
97
|
+
* Prop name to display more meaningful warnings about when value
|
|
98
|
+
* and defaultValue are both defined, or if the component switches
|
|
99
|
+
* between modes mid-stream.
|
|
100
|
+
*
|
|
101
|
+
* If left undefined, the hook emits more generic/vague warnings
|
|
102
|
+
*/
|
|
103
|
+
warningPropName) => {
|
|
104
|
+
/* eslint-disable react-hooks/rules-of-hooks */
|
|
105
|
+
const meta = (0, react_1.useRef)({
|
|
106
|
+
lastMode: undefined,
|
|
107
|
+
lastDefault: defaultValue,
|
|
108
|
+
// lastValue: value,
|
|
109
|
+
}).current;
|
|
110
|
+
const { lastMode, lastDefault /*, lastValue */ } = meta;
|
|
111
|
+
const mode = value !== undefined
|
|
112
|
+
? 'controlled'
|
|
113
|
+
: defaultValue !== undefined
|
|
114
|
+
? 'uncontrolled'
|
|
115
|
+
: lastMode;
|
|
116
|
+
// Validate sane use of the component, during development.
|
|
117
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
118
|
+
if (value !== undefined && defaultValue !== undefined) {
|
|
119
|
+
console.error(`WARNING:` +
|
|
120
|
+
` Don't mix` +
|
|
121
|
+
(warningPropName
|
|
122
|
+
? ` \`${warningPropName}\` and \`default${(0, hanna_utils_1.capitalize)(warningPropName)}\` props`
|
|
123
|
+
: 'controlled and uncontrolled mode') +
|
|
124
|
+
`\n` +
|
|
125
|
+
`Use one or the other.`);
|
|
126
|
+
}
|
|
127
|
+
if (lastMode && lastMode !== mode) {
|
|
128
|
+
console.error(`WARNING:` +
|
|
129
|
+
`A component is changing from ${lastMode} to ${mode} mode.` +
|
|
130
|
+
`\n` +
|
|
131
|
+
(warningPropName
|
|
132
|
+
? `Decide between using \`${warningPropName}\` (controlled) prop` +
|
|
133
|
+
` OR \`default${(0, hanna_utils_1.capitalize)(warningPropName)}\` (uncontrolled)`
|
|
134
|
+
: `Decide between using either controlled OR uncontrolled mode`) +
|
|
135
|
+
` for the lifetime of the component.`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const [localValue, _setLocalValue] = (0, react_1.useState)(defaultValue);
|
|
139
|
+
const setLocalValue = (0, react_1.useCallback)((newState) => {
|
|
140
|
+
if (mode === 'controlled' && typeof newState === 'function') {
|
|
141
|
+
// @ts-expect-error (TS needs a bit of help here, it seems,
|
|
142
|
+
// because the C and U gernerics are too …err… generic?)
|
|
143
|
+
const action = newState;
|
|
144
|
+
newState = action(value);
|
|
145
|
+
}
|
|
146
|
+
_setLocalValue.$called = true;
|
|
147
|
+
_setLocalValue(newState);
|
|
148
|
+
}, [value, mode]);
|
|
149
|
+
// The mode can change but it should never go back to `undefined` state
|
|
150
|
+
// this is similar to what React does with it's <input> and <select>
|
|
151
|
+
// elements.
|
|
152
|
+
// In dev-mode an WARNING gets logged whenever the mode changes.
|
|
153
|
+
meta.lastMode = mode;
|
|
154
|
+
if (mode === 'uncontrolled') {
|
|
155
|
+
// only update lastDefault when in unconrolled mode
|
|
156
|
+
// to guarantee capture of changes that might happen during
|
|
157
|
+
// controlled mode. Something that should ideally not happen
|
|
158
|
+
// but is worth keeping as sane as possible nonetheless.
|
|
159
|
+
meta.lastDefault = defaultValue;
|
|
160
|
+
if (!_setLocalValue.$called && defaultValue !== lastDefault) {
|
|
161
|
+
_setLocalValue(defaultValue); // Immediately exits and re-renders the component
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// meta.lastValue = value;
|
|
165
|
+
const retValue = mode === 'controlled' ? value : localValue;
|
|
166
|
+
return [retValue, setLocalValue];
|
|
167
|
+
/* eslint-enable react-hooks/rules-of-hooks */
|
|
168
|
+
};
|
|
@@ -1 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Measures the scrollbar width and sets it as a CSS variable on
|
|
3
|
+
* the `<html/>` element.
|
|
4
|
+
*
|
|
5
|
+
* Use this hook inside all of your top-level layout components
|
|
6
|
+
*
|
|
7
|
+
* The name of the variable is `--browser-scrollbar-width`, and you can
|
|
8
|
+
* reference it manually in your CSS, or via the hanna-css variable helper.
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { hannaVars } from '@reykjavik/hanna-css';
|
|
12
|
+
*
|
|
13
|
+
* console.log(hannaVars.browser_scrollbar_width.toString())
|
|
14
|
+
* // "var(--browser-scrollbar-width)"
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
1
17
|
export declare const useScrollbarWidthCSSVar: () => void;
|
|
@@ -2,7 +2,23 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.useScrollbarWidthCSSVar = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
|
+
const react_1 = require("react");
|
|
5
6
|
const getScrollbarWidth_1 = tslib_1.__importDefault(require("@hugsmidjan/qj/getScrollbarWidth"));
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Measures the scrollbar width and sets it as a CSS variable on
|
|
9
|
+
* the `<html/>` element.
|
|
10
|
+
*
|
|
11
|
+
* Use this hook inside all of your top-level layout components
|
|
12
|
+
*
|
|
13
|
+
* The name of the variable is `--browser-scrollbar-width`, and you can
|
|
14
|
+
* reference it manually in your CSS, or via the hanna-css variable helper.
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* import { hannaVars } from '@reykjavik/hanna-css';
|
|
18
|
+
*
|
|
19
|
+
* console.log(hannaVars.browser_scrollbar_width.toString())
|
|
20
|
+
* // "var(--browser-scrollbar-width)"
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
const useScrollbarWidthCSSVar = () => (0, react_1.useEffect)(() => getScrollbarWidth_1.default.setCSSvar(), []);
|
|
8
24
|
exports.useScrollbarWidthCSSVar = useScrollbarWidthCSSVar;
|
package/utils.d.ts
CHANGED
package/utils.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const tslib_1 = require("tslib");
|
|
4
|
+
tslib_1.__exportStar(require("./utils/useDidChange"), exports);
|
|
4
5
|
tslib_1.__exportStar(require("./utils/useFormatMonitor"), exports);
|
|
5
6
|
tslib_1.__exportStar(require("./utils/useGetSVGtext"), exports);
|
|
7
|
+
tslib_1.__exportStar(require("./utils/useMixedControlState"), exports);
|
|
8
|
+
tslib_1.__exportStar(require("./utils/useScrollbarWidthCSSVar"), exports);
|