@reykjavik/hanna-react 0.10.65 → 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 +9 -0
- package/CityBlock.d.ts +6 -8
- package/FileInput.js +4 -1
- 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,6 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
- ... <!-- Add new lines here. -->
|
|
6
6
|
|
|
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
|
+
|
|
7
16
|
## 0.10.63 – 0.10.65
|
|
8
17
|
|
|
9
18
|
_2022-08-29_
|
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;
|
package/FileInput.js
CHANGED
|
@@ -102,9 +102,12 @@ const FileInput = (props) => {
|
|
|
102
102
|
// name prop is provided. This is implicitly what the
|
|
103
103
|
// browser does on form submit.
|
|
104
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.
|
|
105
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,
|
|
106
108
|
react_1.default.createElement("input", Object.assign({
|
|
107
|
-
//
|
|
109
|
+
// fake input exclusively used to capture clicks and file drops.
|
|
110
|
+
// it's contents are wiped on every "add" action.
|
|
108
111
|
className: "FileInput__input--fake" }, getInputProps(), { tabIndex: undefined, style: undefined, multiple: multiple || undefined }, inputProps, { required: undefined })),
|
|
109
112
|
' ',
|
|
110
113
|
react_1.default.createElement("div", Object.assign({ className: (0, getBemClass_1.default)('FileInput__dropzone', [isHover && 'highlight']) }, getRootProps({ isDragReject }), { tabIndex: undefined }),
|
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);
|