@mpen/react-basic-inputs 0.1.6 → 0.1.8

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/dist/bundle.cjs CHANGED
@@ -63,13 +63,6 @@ var useUpdateEffect = function(effect, deps) {
63
63
 
64
64
  var useUpdateEffect$1 = useUpdateEffect;
65
65
 
66
- const defaultMakeInvalidValueOption = value => ({
67
- value,
68
- text: String(value),
69
- disabled: true,
70
- key: INVALID_OPTION_KEY
71
- });
72
-
73
66
  function defaultMakeKey(opt, idx) {
74
67
  if (opt.key != null) {
75
68
  return resolveValue(opt.key, opt, idx);
@@ -79,6 +72,30 @@ function defaultMakeKey(opt, idx) {
79
72
  return idx;
80
73
  }
81
74
 
75
+ class KeyFixer {
76
+ usedKeys=new Map;
77
+ fix(opt, idx) {
78
+ let fixedKey = defaultMakeKey(opt, idx);
79
+ for (;;) {
80
+ let suffix = this.usedKeys.get(fixedKey);
81
+ if (suffix === undefined) {
82
+ this.usedKeys.set(fixedKey, 1);
83
+ break;
84
+ }
85
+ this.usedKeys.set(fixedKey, ++suffix);
86
+ fixedKey = `${fixedKey}(${suffix})`;
87
+ }
88
+ return fixedKey;
89
+ }
90
+ }
91
+
92
+ const defaultMakeInvalidValueOption = value => ({
93
+ value,
94
+ text: String(value),
95
+ disabled: true,
96
+ key: INVALID_OPTION_KEY
97
+ });
98
+
82
99
  const PLACEHOLDER_KEY = "3c9369b7-0a5e-46ea-93c2-e8b9fec67fdb";
83
100
 
84
101
  const INVALID_OPTION_KEY = "1a53f789-77f5-4ce6-a829-b00e563f1ee8";
@@ -132,23 +149,14 @@ function Select({options, value, invalidValueOption = defaultMakeInvalidValueOpt
132
149
  useUpdateEffect$1((() => {
133
150
  refreshSelectedIndex();
134
151
  }), [ refreshSelectedIndex ]);
135
- const usedKeys = new Map;
152
+ const fixer = new KeyFixer;
136
153
  return jsxRuntime.jsx("select", {
137
154
  ...selectAttrs,
138
155
  onChange: handleChange,
139
156
  ref: setRef,
140
157
  children: fixedOptions.map(((opt, idx) => {
141
158
  const {value, text, key, ...optAttrs} = opt;
142
- let fixedKey = defaultMakeKey(opt, idx);
143
- for (;;) {
144
- let suffix = usedKeys.get(fixedKey);
145
- if (suffix === undefined) {
146
- usedKeys.set(fixedKey, 1);
147
- break;
148
- }
149
- usedKeys.set(fixedKey, ++suffix);
150
- fixedKey = `${fixedKey}(${suffix})`;
151
- }
159
+ const fixedKey = fixer.fix(opt, idx);
152
160
  return React.createElement("option", {
153
161
  ...optAttrs,
154
162
  key: fixedKey
@@ -215,35 +223,43 @@ function TextInput({formatOnChange = collapseWhitespace, ...otherProps}) {
215
223
  });
216
224
  }
217
225
 
218
- const TextArea = React.forwardRef((function TextArea({onInput, style, ...rest}, fwdRef) {
226
+ const TextArea = React.forwardRef((function TextArea({onInput, style, initialHeight = "auto", ...rest}, fwdRef) {
219
227
  const ref = React.useRef(null);
220
- const [height, setHeight] = React.useState("auto");
221
- const adjustHeight = () => {
228
+ const [height, setHeight] = React.useState(initialHeight);
229
+ const adjustHeight = React.useCallback((() => {
222
230
  const textarea = ref.current;
223
231
  if (!textarea) return;
224
- textarea.style.height = "auto";
232
+ textarea.style.height = initialHeight;
225
233
  const newHeight = `${textarea.scrollHeight}px`;
226
234
  setHeight(newHeight);
227
235
  textarea.style.height = newHeight;
228
- };
236
+ }), [ initialHeight ]);
229
237
  React.useImperativeHandle(fwdRef, (() => ({
230
238
  element: ref.current,
231
- resize: adjustHeight
232
- })), [ setHeight, ref.current ]);
239
+ adjustHeight
240
+ })), [ adjustHeight ]);
233
241
  const input = useEventHandler((ev => {
234
242
  adjustHeight();
235
243
  onInput?.(ev);
236
244
  }));
237
245
  React.useLayoutEffect((() => {
238
246
  adjustHeight();
239
- }), []);
247
+ const textarea = ref.current;
248
+ if (!textarea) return;
249
+ const resizeObserver = new ResizeObserver((entries => {
250
+ adjustHeight();
251
+ }));
252
+ resizeObserver.observe(textarea);
253
+ return () => {
254
+ resizeObserver.unobserve(textarea);
255
+ };
256
+ }), [ adjustHeight ]);
240
257
  return jsxRuntime.jsx("textarea", {
241
- rows: 1,
242
258
  ...rest,
243
259
  style: {
244
- ...style,
245
260
  overflow: "hidden",
246
261
  resize: "none",
262
+ ...style,
247
263
  height
248
264
  },
249
265
  onInput: input,
@@ -251,8 +267,55 @@ const TextArea = React.forwardRef((function TextArea({onInput, style, ...rest},
251
267
  });
252
268
  }));
253
269
 
270
+ function RadioMenu(menu) {
271
+ const defaultId = React.useId();
272
+ const name = menu.name ?? defaultId;
273
+ const eq = menu.valueEquals ?? Object.is;
274
+ const fixedOptions = menu.options ?? [];
275
+ const fixer = new KeyFixer;
276
+ const onChange = useEventHandler((ev => {
277
+ const selectedIndex = Number(ev.target.value);
278
+ const selectedOption = fixedOptions[selectedIndex];
279
+ if (selectedOption != null && menu.onChange != null) {
280
+ menu.onChange({
281
+ value: selectedOption.value,
282
+ index: selectedIndex,
283
+ type: "change",
284
+ timeStamp: ev.timeStamp,
285
+ target: ev.target
286
+ });
287
+ }
288
+ }));
289
+ return jsxRuntime.jsx("ul", {
290
+ className: menu.className,
291
+ children: fixedOptions.map(((opt, idx) => {
292
+ const {value, text, key, itemClassName, ...rest} = opt;
293
+ const fixedKey = fixer.fix(opt, idx);
294
+ if (menu.value !== undefined) {
295
+ rest.checked = eq(value, menu.value);
296
+ }
297
+ return jsxRuntime.jsx("li", {
298
+ className: itemClassName,
299
+ children: jsxRuntime.jsxs("label", {
300
+ children: [ jsxRuntime.jsx("input", {
301
+ ...rest,
302
+ value: idx,
303
+ onChange,
304
+ name,
305
+ type: "radio"
306
+ }), jsxRuntime.jsx("span", {
307
+ children: text
308
+ }) ]
309
+ })
310
+ }, fixedKey);
311
+ }))
312
+ });
313
+ }
314
+
254
315
  exports.Input = Input;
255
316
 
317
+ exports.RadioMenu = RadioMenu;
318
+
256
319
  exports.Select = Select;
257
320
 
258
321
  exports.TextArea = TextArea;
package/dist/bundle.d.ts CHANGED
@@ -2,3 +2,4 @@ export * from './components/Select';
2
2
  export * from './components/TextInput';
3
3
  export * from './components/Input';
4
4
  export * from './components/TextArea';
5
+ export * from './components/RadioMenu';
package/dist/bundle.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { jsx } from "react/jsx-runtime";
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
2
 
3
- import { useDebugValue, useRef, useInsertionEffect, useEffect, useMemo, useCallback, createElement, forwardRef, useState, useImperativeHandle, useLayoutEffect } from "react";
3
+ import { useDebugValue, useRef, useInsertionEffect, useEffect, useMemo, useCallback, createElement, forwardRef, useState, useImperativeHandle, useLayoutEffect, useId } from "react";
4
4
 
5
5
  const NOOP = Object.freeze((() => {}));
6
6
 
@@ -61,13 +61,6 @@ var useUpdateEffect = function(effect, deps) {
61
61
 
62
62
  var useUpdateEffect$1 = useUpdateEffect;
63
63
 
64
- const defaultMakeInvalidValueOption = value => ({
65
- value,
66
- text: String(value),
67
- disabled: true,
68
- key: INVALID_OPTION_KEY
69
- });
70
-
71
64
  function defaultMakeKey(opt, idx) {
72
65
  if (opt.key != null) {
73
66
  return resolveValue(opt.key, opt, idx);
@@ -77,6 +70,30 @@ function defaultMakeKey(opt, idx) {
77
70
  return idx;
78
71
  }
79
72
 
73
+ class KeyFixer {
74
+ usedKeys=new Map;
75
+ fix(opt, idx) {
76
+ let fixedKey = defaultMakeKey(opt, idx);
77
+ for (;;) {
78
+ let suffix = this.usedKeys.get(fixedKey);
79
+ if (suffix === undefined) {
80
+ this.usedKeys.set(fixedKey, 1);
81
+ break;
82
+ }
83
+ this.usedKeys.set(fixedKey, ++suffix);
84
+ fixedKey = `${fixedKey}(${suffix})`;
85
+ }
86
+ return fixedKey;
87
+ }
88
+ }
89
+
90
+ const defaultMakeInvalidValueOption = value => ({
91
+ value,
92
+ text: String(value),
93
+ disabled: true,
94
+ key: INVALID_OPTION_KEY
95
+ });
96
+
80
97
  const PLACEHOLDER_KEY = "3c9369b7-0a5e-46ea-93c2-e8b9fec67fdb";
81
98
 
82
99
  const INVALID_OPTION_KEY = "1a53f789-77f5-4ce6-a829-b00e563f1ee8";
@@ -130,23 +147,14 @@ function Select({options, value, invalidValueOption = defaultMakeInvalidValueOpt
130
147
  useUpdateEffect$1((() => {
131
148
  refreshSelectedIndex();
132
149
  }), [ refreshSelectedIndex ]);
133
- const usedKeys = new Map;
150
+ const fixer = new KeyFixer;
134
151
  return jsx("select", {
135
152
  ...selectAttrs,
136
153
  onChange: handleChange,
137
154
  ref: setRef,
138
155
  children: fixedOptions.map(((opt, idx) => {
139
156
  const {value, text, key, ...optAttrs} = opt;
140
- let fixedKey = defaultMakeKey(opt, idx);
141
- for (;;) {
142
- let suffix = usedKeys.get(fixedKey);
143
- if (suffix === undefined) {
144
- usedKeys.set(fixedKey, 1);
145
- break;
146
- }
147
- usedKeys.set(fixedKey, ++suffix);
148
- fixedKey = `${fixedKey}(${suffix})`;
149
- }
157
+ const fixedKey = fixer.fix(opt, idx);
150
158
  return createElement("option", {
151
159
  ...optAttrs,
152
160
  key: fixedKey
@@ -213,35 +221,43 @@ function TextInput({formatOnChange = collapseWhitespace, ...otherProps}) {
213
221
  });
214
222
  }
215
223
 
216
- const TextArea = forwardRef((function TextArea({onInput, style, ...rest}, fwdRef) {
224
+ const TextArea = forwardRef((function TextArea({onInput, style, initialHeight = "auto", ...rest}, fwdRef) {
217
225
  const ref = useRef(null);
218
- const [height, setHeight] = useState("auto");
219
- const adjustHeight = () => {
226
+ const [height, setHeight] = useState(initialHeight);
227
+ const adjustHeight = useCallback((() => {
220
228
  const textarea = ref.current;
221
229
  if (!textarea) return;
222
- textarea.style.height = "auto";
230
+ textarea.style.height = initialHeight;
223
231
  const newHeight = `${textarea.scrollHeight}px`;
224
232
  setHeight(newHeight);
225
233
  textarea.style.height = newHeight;
226
- };
234
+ }), [ initialHeight ]);
227
235
  useImperativeHandle(fwdRef, (() => ({
228
236
  element: ref.current,
229
- resize: adjustHeight
230
- })), [ setHeight, ref.current ]);
237
+ adjustHeight
238
+ })), [ adjustHeight ]);
231
239
  const input = useEventHandler((ev => {
232
240
  adjustHeight();
233
241
  onInput?.(ev);
234
242
  }));
235
243
  useLayoutEffect((() => {
236
244
  adjustHeight();
237
- }), []);
245
+ const textarea = ref.current;
246
+ if (!textarea) return;
247
+ const resizeObserver = new ResizeObserver((entries => {
248
+ adjustHeight();
249
+ }));
250
+ resizeObserver.observe(textarea);
251
+ return () => {
252
+ resizeObserver.unobserve(textarea);
253
+ };
254
+ }), [ adjustHeight ]);
238
255
  return jsx("textarea", {
239
- rows: 1,
240
256
  ...rest,
241
257
  style: {
242
- ...style,
243
258
  overflow: "hidden",
244
259
  resize: "none",
260
+ ...style,
245
261
  height
246
262
  },
247
263
  onInput: input,
@@ -249,4 +265,49 @@ const TextArea = forwardRef((function TextArea({onInput, style, ...rest}, fwdRef
249
265
  });
250
266
  }));
251
267
 
252
- export { Input, Select, TextArea, TextInput };
268
+ function RadioMenu(menu) {
269
+ const defaultId = useId();
270
+ const name = menu.name ?? defaultId;
271
+ const eq = menu.valueEquals ?? Object.is;
272
+ const fixedOptions = menu.options ?? [];
273
+ const fixer = new KeyFixer;
274
+ const onChange = useEventHandler((ev => {
275
+ const selectedIndex = Number(ev.target.value);
276
+ const selectedOption = fixedOptions[selectedIndex];
277
+ if (selectedOption != null && menu.onChange != null) {
278
+ menu.onChange({
279
+ value: selectedOption.value,
280
+ index: selectedIndex,
281
+ type: "change",
282
+ timeStamp: ev.timeStamp,
283
+ target: ev.target
284
+ });
285
+ }
286
+ }));
287
+ return jsx("ul", {
288
+ className: menu.className,
289
+ children: fixedOptions.map(((opt, idx) => {
290
+ const {value, text, key, itemClassName, ...rest} = opt;
291
+ const fixedKey = fixer.fix(opt, idx);
292
+ if (menu.value !== undefined) {
293
+ rest.checked = eq(value, menu.value);
294
+ }
295
+ return jsx("li", {
296
+ className: itemClassName,
297
+ children: jsxs("label", {
298
+ children: [ jsx("input", {
299
+ ...rest,
300
+ value: idx,
301
+ onChange,
302
+ name,
303
+ type: "radio"
304
+ }), jsx("span", {
305
+ children: text
306
+ }) ]
307
+ })
308
+ }, fixedKey);
309
+ }))
310
+ });
311
+ }
312
+
313
+ export { Input, RadioMenu, Select, TextArea, TextInput };
@@ -0,0 +1,30 @@
1
+ import { EventCallback, HtmlInputElement, NonNil, OverrideProps } from '../types/utility';
2
+ import { Key, ReactNode } from 'react';
3
+ import { Resolvable } from '../util/resolvable';
4
+ import { JSX } from 'react/jsx-runtime';
5
+ export type RadioMenuOption<T extends NonNil> = OverrideProps<'input', {
6
+ value: T;
7
+ text: ReactNode;
8
+ key?: Resolvable<Key, [RadioMenuOption<T>, number]>;
9
+ itemClassName?: string;
10
+ }, 'type' | 'children' | 'checked' | 'name'>;
11
+ export type RadioMenuChangeEvent<T> = {
12
+ value: T;
13
+ index: number;
14
+ type: 'change';
15
+ timeStamp: number;
16
+ target: HtmlInputElement;
17
+ };
18
+ export type RadioMenuChangeEventHandler<T> = EventCallback<RadioMenuChangeEvent<T>>;
19
+ export type RadioMenuProps<T extends NonNil> = {
20
+ options: RadioMenuOption<T>[];
21
+ value?: T | null;
22
+ className?: string;
23
+ /**
24
+ * Value comparison function. Defaults to {@linkcode https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is|Object.is}
25
+ */
26
+ valueEquals?: (a: T, b: T) => boolean;
27
+ onChange?: RadioMenuChangeEventHandler<T>;
28
+ name?: string;
29
+ };
30
+ export declare function RadioMenu<T extends NonNil>(menu: RadioMenuProps<T>): JSX.Element;
@@ -2,7 +2,13 @@
2
2
  import { HtmlTextAreaElement, OverrideProps, VoidFn } from '../types/utility';
3
3
  export type TextAreaRef = {
4
4
  element: HtmlTextAreaElement;
5
- resize: VoidFn;
5
+ adjustHeight: VoidFn;
6
6
  };
7
- export type TextAreaProps = OverrideProps<'textarea', {}>;
8
- export declare const TextArea: import("react").ForwardRefExoticComponent<Omit<Omit<import("react").DetailedHTMLProps<import("react").TextareaHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement>, "ref">, never> & import("react").RefAttributes<TextAreaRef>>;
7
+ export type TextAreaProps = OverrideProps<'textarea', {
8
+ /** Initial/minimum height. "0" or "auto" are good choices. Defaults to "auto" */
9
+ initialHeight?: string;
10
+ }>;
11
+ export declare const TextArea: import("react").ForwardRefExoticComponent<Omit<Omit<import("react").DetailedHTMLProps<import("react").TextareaHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement>, "ref">, "initialHeight"> & {
12
+ /** Initial/minimum height. "0" or "auto" are good choices. Defaults to "auto" */
13
+ initialHeight?: string;
14
+ } & import("react").RefAttributes<TextAreaRef>>;
@@ -0,0 +1 @@
1
+ export declare function cast<T>(val: any): asserts val is T;
@@ -0,0 +1,8 @@
1
+ import { Key } from 'react';
2
+ /**
3
+ * Produces unique React Keys from an option.
4
+ */
5
+ export declare class KeyFixer {
6
+ usedKeys: Map<Key, number>;
7
+ fix(opt: any, idx: number): Key;
8
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mpen/react-basic-inputs",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "packageManager": "yarn@3.5.0",
5
5
  "exports": {
6
6
  ".": {
@@ -54,5 +54,8 @@
54
54
  "peerDependencies": {
55
55
  "react": ">=17 <19",
56
56
  "react-dom": ">=17 <19"
57
+ },
58
+ "dependencies": {
59
+ "classcat": "^5.0.4"
57
60
  }
58
61
  }