@reykjavik/hanna-react 0.10.68 → 0.10.70

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.
@@ -1,6 +1,6 @@
1
1
  import { SSRSupport } from '@hugsmidjan/react/hooks';
2
2
  import { SeenProp } from './utils/seenEffect';
3
- declare type AccordionListItemProps = {
3
+ export declare type AccordionListItemProps = {
4
4
  title: string | JSX.Element;
5
5
  content: string | JSX.Element | undefined;
6
6
  id?: string;
@@ -8,10 +8,18 @@ declare type AccordionListItemProps = {
8
8
  };
9
9
  export declare type AccordionListProps = {
10
10
  items: Array<AccordionListItemProps>;
11
- ssr?: SSRSupport;
12
- wide?: boolean;
13
- /** Index of those items that should start open */
11
+ /** Index of the currently open items (controlled use) */
12
+ open?: Array<number>;
13
+ /** Called whenever an AccodrionList item is toggled */
14
+ onToggle?: (data: {
15
+ newOpen: Array<number>;
16
+ index: number;
17
+ opened: boolean;
18
+ }) => void;
19
+ /** Index of those items that should start open (uncontrolled use) */
14
20
  defaultOpen?: Array<number>;
21
+ wide?: boolean;
22
+ ssr?: SSRSupport;
15
23
  } & SeenProp;
16
24
  declare const AccordionList: (props: AccordionListProps) => JSX.Element;
17
25
  export default AccordionList;
package/AccordionList.js CHANGED
@@ -5,25 +5,31 @@ const react_1 = tslib_1.__importStar(require("react"));
5
5
  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
+ const utils_1 = require("./utils");
8
9
  const AccordionListItem = (props) => {
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);
10
+ const { title, content, id, disabled = false, ssr, open, onToggle } = props;
13
11
  const defaultOpen = (0, react_1.useRef)(props.defaultOpen);
14
12
  const domid = (0, hooks_1.useDomid)();
15
13
  const isBrowser = (0, hooks_1.useIsBrowserSide)(ssr);
16
14
  const itemDisabled = (isBrowser && disabled) || !content;
17
15
  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 },
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: () => {
19
- setOpen(!open);
20
- }, disabled: itemDisabled }, title)) : (title)),
16
+ 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: onToggle, disabled: itemDisabled }, title)) : (title)),
21
17
  react_1.default.createElement("div", { id: isBrowser && domid, className: "AccordionList__content", hidden: isBrowser && (!open || itemDisabled) }, content)));
22
18
  };
23
- // ---------------------------------------------------------------------------
24
19
  const AccordionList = (props) => {
25
- const { items, ssr, wide, defaultOpen, startSeen } = props;
20
+ const { items, ssr, wide, startSeen, defaultOpen } = props;
26
21
  const [ref] = (0, seenEffect_1.useSeenEffect)(startSeen);
27
- return (react_1.default.createElement("div", { className: (0, getBemClass_1.default)('AccordionList', [wide && 'wide']), ref: ref }, items.map((item, i) => (react_1.default.createElement(AccordionListItem, Object.assign({ key: i }, item, { ssr: ssr, defaultOpen: defaultOpen && defaultOpen.includes(i) }))))));
22
+ const [open, setOpenArray, mode] = (0, utils_1.useMixedControlState)(props, 'open', []);
23
+ const onToggle = (index) => {
24
+ setOpenArray((prevOpen) => {
25
+ const opened = !prevOpen.includes(index);
26
+ const newOpen = opened
27
+ ? prevOpen.concat(index)
28
+ : prevOpen.filter((idx) => idx !== index);
29
+ props.onToggle && props.onToggle({ newOpen, index, opened });
30
+ return newOpen;
31
+ });
32
+ };
33
+ return (react_1.default.createElement("div", { className: (0, getBemClass_1.default)('AccordionList', [wide && 'wide']), ref: ref }, items.map((item, i) => (react_1.default.createElement(AccordionListItem, Object.assign({ key: i }, item, { ssr: ssr, open: open.includes(i), onToggle: () => onToggle(i), defaultOpen: defaultOpen && defaultOpen.includes(i) }))))));
28
34
  };
29
35
  exports.default = AccordionList;
package/ArticleCards.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import type { Resolve } from '@reykjavik/hanna-utils';
1
+ import type { Cleanup } from '@reykjavik/hanna-utils';
2
2
  import { ImageCardListProps, ImageCardProps } from './_abstract/_CardList';
3
3
  export declare type ArticleCardProps = ImageCardProps;
4
- export declare type ArticleCardsProps = Resolve<Pick<ImageCardListProps, 'cards' | 'imgPlaceholder'>>;
4
+ export declare type ArticleCardsProps = Cleanup<Pick<ImageCardListProps, 'cards' | 'imgPlaceholder'>>;
5
5
  declare const ArticleCards: (props: ArticleCardsProps) => JSX.Element;
6
6
  export default ArticleCards;
package/CHANGELOG.md CHANGED
@@ -4,6 +4,20 @@
4
4
 
5
5
  - ... <!-- Add new lines here. -->
6
6
 
7
+ ## 0.10.70
8
+
9
+ _2022-09-28_
10
+
11
+ - fix: Remove leftover dev logging
12
+
13
+ ## 0.10.69
14
+
15
+ _2022-09-27_
16
+
17
+ - feat: Add controlled props `open` and `onToggle` to `AccordionList` — it now
18
+ supports either [controlled or uncontrolled](./README-conventions.md) use.
19
+ - fix: Squash `useMixedControlState` bugs/misbehavior and improve its typing
20
+
7
21
  ## 0.10.68
8
22
 
9
23
  _2022-09-26_
@@ -20,5 +20,5 @@ export declare type SiteSearchInputProps = {
20
20
  children?: undefined;
21
21
  ssr?: SSRSupport;
22
22
  } & WrappingProps & InputElmProps;
23
- declare const SiteSearchInput: React.ForwardRefExoticComponent<Pick<SiteSearchInputProps, "title" | "id" | "ssr" | "form" | "label" | "slot" | "style" | "pattern" | "children" | "hidden" | "onClick" | "is" | "key" | "list" | "autoFocus" | "formAction" | "formEncType" | "formMethod" | "formNoValidate" | "formTarget" | "name" | "value" | "defaultChecked" | "defaultValue" | "suppressContentEditableWarning" | "suppressHydrationWarning" | "accessKey" | "contentEditable" | "contextMenu" | "dir" | "draggable" | "lang" | "placeholder" | "spellCheck" | "tabIndex" | "translate" | "radioGroup" | "role" | "about" | "datatype" | "inlist" | "prefix" | "property" | "resource" | "typeof" | "vocab" | "autoCapitalize" | "autoCorrect" | "autoSave" | "color" | "itemProp" | "itemScope" | "itemType" | "itemID" | "itemRef" | "results" | "security" | "unselectable" | "inputMode" | "aria-activedescendant" | "aria-atomic" | "aria-autocomplete" | "aria-busy" | "aria-checked" | "aria-colcount" | "aria-colindex" | "aria-colspan" | "aria-controls" | "aria-current" | "aria-describedby" | "aria-details" | "aria-disabled" | "aria-dropeffect" | "aria-errormessage" | "aria-expanded" | "aria-flowto" | "aria-grabbed" | "aria-haspopup" | "aria-hidden" | "aria-invalid" | "aria-keyshortcuts" | "aria-label" | "aria-labelledby" | "aria-level" | "aria-live" | "aria-modal" | "aria-multiline" | "aria-multiselectable" | "aria-orientation" | "aria-owns" | "aria-placeholder" | "aria-posinset" | "aria-pressed" | "aria-readonly" | "aria-relevant" | "aria-required" | "aria-roledescription" | "aria-rowcount" | "aria-rowindex" | "aria-rowspan" | "aria-selected" | "aria-setsize" | "aria-sort" | "aria-valuemax" | "aria-valuemin" | "aria-valuenow" | "aria-valuetext" | "dangerouslySetInnerHTML" | "onCopy" | "onCopyCapture" | "onCut" | "onCutCapture" | "onPaste" | "onPasteCapture" | "onCompositionEnd" | "onCompositionEndCapture" | "onCompositionStart" | "onCompositionStartCapture" | "onCompositionUpdate" | "onCompositionUpdateCapture" | "onFocus" | "onFocusCapture" | "onBlur" | "onBlurCapture" | "onChange" | "onChangeCapture" | "onBeforeInput" | "onBeforeInputCapture" | "onInput" | "onInputCapture" | "onReset" | "onResetCapture" | "onSubmit" | "onSubmitCapture" | "onInvalid" | "onInvalidCapture" | "onLoad" | "onLoadCapture" | "onError" | "onErrorCapture" | "onKeyDown" | "onKeyDownCapture" | "onKeyPress" | "onKeyPressCapture" | "onKeyUp" | "onKeyUpCapture" | "onAbort" | "onAbortCapture" | "onCanPlay" | "onCanPlayCapture" | "onCanPlayThrough" | "onCanPlayThroughCapture" | "onDurationChange" | "onDurationChangeCapture" | "onEmptied" | "onEmptiedCapture" | "onEncrypted" | "onEncryptedCapture" | "onEnded" | "onEndedCapture" | "onLoadedData" | "onLoadedDataCapture" | "onLoadedMetadata" | "onLoadedMetadataCapture" | "onLoadStart" | "onLoadStartCapture" | "onPause" | "onPauseCapture" | "onPlay" | "onPlayCapture" | "onPlaying" | "onPlayingCapture" | "onProgress" | "onProgressCapture" | "onRateChange" | "onRateChangeCapture" | "onSeeked" | "onSeekedCapture" | "onSeeking" | "onSeekingCapture" | "onStalled" | "onStalledCapture" | "onSuspend" | "onSuspendCapture" | "onTimeUpdate" | "onTimeUpdateCapture" | "onVolumeChange" | "onVolumeChangeCapture" | "onWaiting" | "onWaitingCapture" | "onAuxClick" | "onAuxClickCapture" | "onClickCapture" | "onContextMenu" | "onContextMenuCapture" | "onDoubleClick" | "onDoubleClickCapture" | "onDrag" | "onDragCapture" | "onDragEnd" | "onDragEndCapture" | "onDragEnter" | "onDragEnterCapture" | "onDragExit" | "onDragExitCapture" | "onDragLeave" | "onDragLeaveCapture" | "onDragOver" | "onDragOverCapture" | "onDragStart" | "onDragStartCapture" | "onDrop" | "onDropCapture" | "onMouseDown" | "onMouseDownCapture" | "onMouseEnter" | "onMouseLeave" | "onMouseMove" | "onMouseMoveCapture" | "onMouseOut" | "onMouseOutCapture" | "onMouseOver" | "onMouseOverCapture" | "onMouseUp" | "onMouseUpCapture" | "onSelect" | "onSelectCapture" | "onTouchCancel" | "onTouchCancelCapture" | "onTouchEnd" | "onTouchEndCapture" | "onTouchMove" | "onTouchMoveCapture" | "onTouchStart" | "onTouchStartCapture" | "onPointerDown" | "onPointerDownCapture" | "onPointerMove" | "onPointerMoveCapture" | "onPointerUp" | "onPointerUpCapture" | "onPointerCancel" | "onPointerCancelCapture" | "onPointerEnter" | "onPointerEnterCapture" | "onPointerLeave" | "onPointerLeaveCapture" | "onPointerOver" | "onPointerOverCapture" | "onPointerOut" | "onPointerOutCapture" | "onGotPointerCapture" | "onGotPointerCaptureCapture" | "onLostPointerCapture" | "onLostPointerCaptureCapture" | "onScroll" | "onScrollCapture" | "onWheel" | "onWheelCapture" | "onAnimationStart" | "onAnimationStartCapture" | "onAnimationEnd" | "onAnimationEndCapture" | "onAnimationIteration" | "onAnimationIterationCapture" | "onTransitionEnd" | "onTransitionEndCapture" | "size" | "step" | "src" | "multiple" | "alt" | "width" | "accept" | "autoComplete" | "capture" | "checked" | "crossOrigin" | "enterKeyHint" | "height" | "max" | "maxLength" | "min" | "minLength" | "onButtonClick" | "buttonText"> & React.RefAttributes<HTMLInputElement>>;
23
+ declare const SiteSearchInput: React.ForwardRefExoticComponent<Pick<SiteSearchInputProps, "form" | "label" | "slot" | "style" | "title" | "pattern" | "is" | "value" | "defaultValue" | "id" | "ssr" | "children" | "hidden" | "onClick" | "key" | "list" | "autoFocus" | "formAction" | "formEncType" | "formMethod" | "formNoValidate" | "formTarget" | "name" | "defaultChecked" | "suppressContentEditableWarning" | "suppressHydrationWarning" | "accessKey" | "contentEditable" | "contextMenu" | "dir" | "draggable" | "lang" | "placeholder" | "spellCheck" | "tabIndex" | "translate" | "radioGroup" | "role" | "about" | "datatype" | "inlist" | "prefix" | "property" | "resource" | "typeof" | "vocab" | "autoCapitalize" | "autoCorrect" | "autoSave" | "color" | "itemProp" | "itemScope" | "itemType" | "itemID" | "itemRef" | "results" | "security" | "unselectable" | "inputMode" | "aria-activedescendant" | "aria-atomic" | "aria-autocomplete" | "aria-busy" | "aria-checked" | "aria-colcount" | "aria-colindex" | "aria-colspan" | "aria-controls" | "aria-current" | "aria-describedby" | "aria-details" | "aria-disabled" | "aria-dropeffect" | "aria-errormessage" | "aria-expanded" | "aria-flowto" | "aria-grabbed" | "aria-haspopup" | "aria-hidden" | "aria-invalid" | "aria-keyshortcuts" | "aria-label" | "aria-labelledby" | "aria-level" | "aria-live" | "aria-modal" | "aria-multiline" | "aria-multiselectable" | "aria-orientation" | "aria-owns" | "aria-placeholder" | "aria-posinset" | "aria-pressed" | "aria-readonly" | "aria-relevant" | "aria-required" | "aria-roledescription" | "aria-rowcount" | "aria-rowindex" | "aria-rowspan" | "aria-selected" | "aria-setsize" | "aria-sort" | "aria-valuemax" | "aria-valuemin" | "aria-valuenow" | "aria-valuetext" | "dangerouslySetInnerHTML" | "onCopy" | "onCopyCapture" | "onCut" | "onCutCapture" | "onPaste" | "onPasteCapture" | "onCompositionEnd" | "onCompositionEndCapture" | "onCompositionStart" | "onCompositionStartCapture" | "onCompositionUpdate" | "onCompositionUpdateCapture" | "onFocus" | "onFocusCapture" | "onBlur" | "onBlurCapture" | "onChange" | "onChangeCapture" | "onBeforeInput" | "onBeforeInputCapture" | "onInput" | "onInputCapture" | "onReset" | "onResetCapture" | "onSubmit" | "onSubmitCapture" | "onInvalid" | "onInvalidCapture" | "onLoad" | "onLoadCapture" | "onError" | "onErrorCapture" | "onKeyDown" | "onKeyDownCapture" | "onKeyPress" | "onKeyPressCapture" | "onKeyUp" | "onKeyUpCapture" | "onAbort" | "onAbortCapture" | "onCanPlay" | "onCanPlayCapture" | "onCanPlayThrough" | "onCanPlayThroughCapture" | "onDurationChange" | "onDurationChangeCapture" | "onEmptied" | "onEmptiedCapture" | "onEncrypted" | "onEncryptedCapture" | "onEnded" | "onEndedCapture" | "onLoadedData" | "onLoadedDataCapture" | "onLoadedMetadata" | "onLoadedMetadataCapture" | "onLoadStart" | "onLoadStartCapture" | "onPause" | "onPauseCapture" | "onPlay" | "onPlayCapture" | "onPlaying" | "onPlayingCapture" | "onProgress" | "onProgressCapture" | "onRateChange" | "onRateChangeCapture" | "onSeeked" | "onSeekedCapture" | "onSeeking" | "onSeekingCapture" | "onStalled" | "onStalledCapture" | "onSuspend" | "onSuspendCapture" | "onTimeUpdate" | "onTimeUpdateCapture" | "onVolumeChange" | "onVolumeChangeCapture" | "onWaiting" | "onWaitingCapture" | "onAuxClick" | "onAuxClickCapture" | "onClickCapture" | "onContextMenu" | "onContextMenuCapture" | "onDoubleClick" | "onDoubleClickCapture" | "onDrag" | "onDragCapture" | "onDragEnd" | "onDragEndCapture" | "onDragEnter" | "onDragEnterCapture" | "onDragExit" | "onDragExitCapture" | "onDragLeave" | "onDragLeaveCapture" | "onDragOver" | "onDragOverCapture" | "onDragStart" | "onDragStartCapture" | "onDrop" | "onDropCapture" | "onMouseDown" | "onMouseDownCapture" | "onMouseEnter" | "onMouseLeave" | "onMouseMove" | "onMouseMoveCapture" | "onMouseOut" | "onMouseOutCapture" | "onMouseOver" | "onMouseOverCapture" | "onMouseUp" | "onMouseUpCapture" | "onSelect" | "onSelectCapture" | "onTouchCancel" | "onTouchCancelCapture" | "onTouchEnd" | "onTouchEndCapture" | "onTouchMove" | "onTouchMoveCapture" | "onTouchStart" | "onTouchStartCapture" | "onPointerDown" | "onPointerDownCapture" | "onPointerMove" | "onPointerMoveCapture" | "onPointerUp" | "onPointerUpCapture" | "onPointerCancel" | "onPointerCancelCapture" | "onPointerEnter" | "onPointerEnterCapture" | "onPointerLeave" | "onPointerLeaveCapture" | "onPointerOver" | "onPointerOverCapture" | "onPointerOut" | "onPointerOutCapture" | "onGotPointerCapture" | "onGotPointerCaptureCapture" | "onLostPointerCapture" | "onLostPointerCaptureCapture" | "onScroll" | "onScrollCapture" | "onWheel" | "onWheelCapture" | "onAnimationStart" | "onAnimationStartCapture" | "onAnimationEnd" | "onAnimationEndCapture" | "onAnimationIteration" | "onAnimationIterationCapture" | "onTransitionEnd" | "onTransitionEndCapture" | "size" | "step" | "src" | "multiple" | "alt" | "width" | "accept" | "autoComplete" | "capture" | "checked" | "crossOrigin" | "enterKeyHint" | "height" | "max" | "maxLength" | "min" | "minLength" | "onButtonClick" | "buttonText"> & React.RefAttributes<HTMLInputElement>>;
24
24
  export default SiteSearchInput;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reykjavik/hanna-react",
3
- "version": "0.10.68",
3
+ "version": "0.10.70",
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.13",
19
+ "@reykjavik/hanna-utils": "^0.1.14",
20
20
  "@types/react": "^17.0.24",
21
21
  "@types/react-autosuggest": "^10.1.0",
22
22
  "@types/react-datepicker": "^3.0.2",
@@ -1,4 +1,6 @@
1
1
  import { Dispatch, SetStateAction } from 'react';
2
+ declare type CtrlMode = 'controlled' | 'uncontrolled' | undefined;
3
+ declare type RetArray<R> = [value: R, setValue: Dispatch<SetStateAction<R>>, mode: CtrlMode];
2
4
  declare type DefaultProp<N extends string> = `default${Capitalize<N>}`;
3
5
  declare type PropPair<N extends string> = N | DefaultProp<N>;
4
6
  declare type StrictKeys<P extends Record<string, unknown>, N extends string> = PropPair<N> extends keyof P ? P : {
@@ -48,28 +50,88 @@ declare type StrictKeys<P extends Record<string, unknown>, N extends string> = P
48
50
  * };
49
51
  * ```
50
52
  */
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
- };
53
+ export declare function useMixedControlState<N extends string, P extends {
54
+ [x in PropPair<N>]?: unknown;
55
+ }, D extends Exclude<V, undefined>, V = P[DefaultProp<N>] | P[N]>(
56
+ /** The props object of your component */
57
+ props: StrictKeys<P, N>, // StrictKeys give nicer error messages to users.
58
+ /** Name of the prop for the controlled value */
59
+ name: N,
60
+ /**
61
+ * A last-resort default value to use instead of `undefined`. \
62
+ * **NOTE:** All post-factum changes/updates to this value are ignored!
63
+ */
64
+ emptyValue: D): RetArray<Exclude<V, undefined>>;
65
+ export declare function useMixedControlState<N extends string, P extends {
66
+ [x in PropPair<N>]?: unknown;
67
+ }, D extends Exclude<V, undefined>, V = P[DefaultProp<N>] | P[N]>(
68
+ /** The props object of your component */
69
+ props: StrictKeys<P, N>, // StrictKeys give nicer error messages to users.
70
+ /** Name of the prop for the controlled value */
71
+ name: N,
72
+ /**
73
+ * A last-resort default value to use instead of `undefined`. \
74
+ * **NOTE:** All post-factum changes/updates to this value are ignored!
75
+ */
76
+ emptyValue?: D): RetArray<V>;
77
+ export declare namespace useMixedControlState {
78
+ var $warningLogger: ((message: string) => void) | undefined;
79
+ var raw: typeof useRaw;
80
+ }
81
+ /**
82
+ * A slightly lower-level hook alternative to
83
+ * `useMixedControlState(props, name)`, for cases where you don't
84
+ * have a neatly-/conventionally-shaped props object, or if you need to do
85
+ * some sort of pre-processing of either prop value.
86
+ *
87
+ * ```tsx
88
+ * import { useMixedControlState } from '@reykjavik/hanna-react/utils';
89
+ *
90
+ * declare const props: { visible?: boolean; defaultVisible?: boolean };
91
+ *
92
+ * const [vislble, setVisible] = useMixedControlState.raw(
93
+ * props.vislble,
94
+ * props.defaultVisible,
95
+ * 'visible'
96
+ * );
97
+ * // has the same effect as this:
98
+ * const [visible, setVisible] = useMixedControlState(props, 'visible');
99
+ * ```
100
+ */
101
+ declare function useRaw<C, U, D extends Exclude<C | U, undefined>>(
102
+ /** Controlled value. */
103
+ value: C,
104
+ /** Default/initial value for uncontrolled use. */
105
+ defaultValue: U,
106
+ /**
107
+ * Prop name to display more meaningful warnings about when value
108
+ * and defaultValue are both defined, or if the component switches
109
+ * between modes mid-stream.
110
+ *
111
+ * If left undefined, the hook emits more generic/vague warnings
112
+ */
113
+ warningPropName: string | undefined,
114
+ /**
115
+ * A last-resort default value to use instead of `undefined`. \
116
+ * **NOTE:** All post-factum changes/updates to this value are ignored!
117
+ */
118
+ emptyValue: D): RetArray<Exclude<C | U, undefined>>;
119
+ declare function useRaw<C, U, D extends C | U>(
120
+ /** Controlled value. */
121
+ value: C,
122
+ /** Default/initial value for uncontrolled use. */
123
+ defaultValue: U,
124
+ /**
125
+ * Prop name to display more meaningful warnings about when value
126
+ * and defaultValue are both defined, or if the component switches
127
+ * between modes mid-stream.
128
+ *
129
+ * If left undefined, the hook emits more generic/vague warnings
130
+ */
131
+ warningPropName?: string,
132
+ /**
133
+ * A last-resort default value to use instead of `undefined`. \
134
+ * **NOTE:** All post-factum changes/updates to this value are ignored!
135
+ */
136
+ emptyValue?: D): RetArray<C | U>;
75
137
  export {};
@@ -4,151 +4,87 @@ exports.useMixedControlState = void 0;
4
4
  const react_1 = require("react");
5
5
  const hanna_utils_1 = require("@reykjavik/hanna-utils");
6
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;
7
+ function useMixedControlState(props, name, emptyValue) {
8
+ const value = props[name];
9
+ const defaultValue = props[`default${(0, hanna_utils_1.capitalize)(name)}`];
10
+ return useMixedControlState.raw(value, defaultValue, name, emptyValue);
11
+ }
12
+ exports.useMixedControlState = useMixedControlState;
13
+ // ===========================================================================
14
+ //
15
+ //
16
+ // ===========================================================================
17
+ const defaultWarningLogger = (message) => console.error(message);
18
+ useMixedControlState.$warningLogger = defaultWarningLogger;
19
+ /** Validate sane use of the component, during development. */
20
+ const validateSaneUse = ({ warningPropName, value, defaultValue, lastMode, }) => {
21
+ const warn = useMixedControlState.$warningLogger || defaultWarningLogger;
22
+ if (value !== undefined && defaultValue !== undefined) {
23
+ warn(`WARNING:` +
24
+ ` Don't mix` +
25
+ (warningPropName
26
+ ? ` \`${warningPropName}\` and \`default${(0, hanna_utils_1.capitalize)(warningPropName)}\` props`
27
+ : 'controlled and uncontrolled mode') +
28
+ `\n` +
29
+ `Use one or the other.`);
30
+ return;
31
+ }
32
+ const C_to_U = lastMode === 'controlled' && defaultValue !== undefined;
33
+ const U_to_C = lastMode === 'uncontrolled' && value !== undefined;
34
+ if (C_to_U || U_to_C) {
35
+ warn(`WARNING:` +
36
+ (C_to_U
37
+ ? `A component seems to be attempting to change ` +
38
+ `from controlled to uncontrolled mode. ` +
39
+ `This is not possible.`
40
+ : `A component is changing ` + `from uncontrolled to controlled mode.`) +
41
+ `\n` +
42
+ `Decide between using ` +
43
+ (warningPropName
44
+ ? `\`${warningPropName}\` (controlled) prop` +
45
+ ` OR \`default${(0, hanna_utils_1.capitalize)(warningPropName)}\` (uncontrolled)`
46
+ : `either controlled OR uncontrolled mode`) +
47
+ ` for the lifetime of the component.`);
66
48
  }
67
- return exports.useMixedControlState.raw(props[name], defaultValue, name);
68
49
  };
69
- exports.useMixedControlState = useMixedControlState;
70
50
  // ---------------------------------------------------------------------------
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)({
51
+ function useRaw(value, defaultValue, warningPropName, emptyValue) {
52
+ const metaRef = (0, react_1.useRef)({
106
53
  lastMode: undefined,
107
54
  lastDefault: defaultValue,
108
55
  // 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.
56
+ _emptyValue: emptyValue,
57
+ });
58
+ const meta = metaRef.current;
59
+ const { lastMode, lastDefault, _emptyValue /*, lastValue */ } = meta;
117
60
  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
- }
61
+ validateSaneUse({ warningPropName, value, defaultValue, lastMode });
137
62
  }
138
- const [localValue, _setLocalValue] = (0, react_1.useState)(defaultValue);
63
+ const mode = lastMode === 'controlled'
64
+ ? 'controlled'
65
+ : value !== undefined
66
+ ? 'controlled'
67
+ : defaultValue !== undefined
68
+ ? 'uncontrolled'
69
+ : lastMode;
70
+ const [localValue, _setLocalValue] = (0, react_1.useState)(defaultValue !== undefined ? defaultValue : _emptyValue);
139
71
  const setLocalValue = (0, react_1.useCallback)((newState) => {
140
72
  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);
73
+ newState = newState(value !== undefined ? value : _emptyValue);
145
74
  }
146
- _setLocalValue.$called = true;
147
- _setLocalValue(newState);
148
- }, [value, mode]);
75
+ metaRef.current.lastMode = mode || 'uncontrolled';
76
+ const setterFn = typeof newState === 'function' ? newState : () => newState;
77
+ _setLocalValue((prevState) => {
78
+ let newState = setterFn(prevState);
79
+ newState = newState !== undefined ? newState : _emptyValue;
80
+ if (prevState !== newState) {
81
+ _setLocalValue.$isCalled = true;
82
+ }
83
+ return newState;
84
+ });
85
+ }, [value, mode, _emptyValue]);
149
86
  // 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.
87
+ // this is similar to what React does with <input/> and <select/> elements.
152
88
  // In dev-mode an WARNING gets logged whenever the mode changes.
153
89
  meta.lastMode = mode;
154
90
  if (mode === 'uncontrolled') {
@@ -157,12 +93,13 @@ warningPropName) => {
157
93
  // controlled mode. Something that should ideally not happen
158
94
  // but is worth keeping as sane as possible nonetheless.
159
95
  meta.lastDefault = defaultValue;
160
- if (!_setLocalValue.$called && defaultValue !== lastDefault) {
161
- _setLocalValue(defaultValue); // Immediately exits and re-renders the component
96
+ if (!_setLocalValue.$isCalled && defaultValue !== lastDefault) {
97
+ // Immediately exits and re-renders the component
98
+ _setLocalValue(defaultValue !== undefined ? defaultValue : _emptyValue);
162
99
  }
163
100
  }
164
101
  // meta.lastValue = value;
165
102
  const retValue = mode === 'controlled' ? value : localValue;
166
- return [retValue, setLocalValue];
167
- /* eslint-enable react-hooks/rules-of-hooks */
168
- };
103
+ return [retValue, setLocalValue, mode];
104
+ }
105
+ useMixedControlState.raw = useRaw;