@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 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, defaultOpen, ssr } = props;
10
- const [open, setOpen] = (0, react_1.useState)(defaultOpen);
11
- (0, react_1.useEffect)(() => setOpen(defaultOpen), [defaultOpen]);
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 || undefined, "data-sprinkled": isBrowser },
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
- onfocusin: (e) => e.currentTarget !== e.target && freeze(),
25
- onfocusout: (e) => e.currentTarget !== e.target && thaw(),
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
- } & CityBlockImageProps & SeenProp;
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
- // fa
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
- checkOrigin?: never;
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
- children?: never;
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
- children?: never;
12
- } | {
13
- filters?: never;
12
+ }, {
14
13
  children: React.ReactNode;
15
- }) & SeenProp;
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
- npm install --save @reykjavik/hanna-react
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
- onRemove?: never;
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
- top?: never;
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
- items?: never;
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.65",
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.11",
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
- const hooks_1 = require("@hugsmidjan/react/hooks");
7
- const useScrollbarWidthCSSVar = () => (0, hooks_1.useOnMount)(() => getScrollbarWidth_1.default.setCSSvar());
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
@@ -1,2 +1,5 @@
1
+ export * from './utils/useDidChange';
1
2
  export * from './utils/useFormatMonitor';
2
3
  export * from './utils/useGetSVGtext';
4
+ export * from './utils/useMixedControlState';
5
+ export * from './utils/useScrollbarWidthCSSVar';
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);