shelving 1.289.2 → 1.291.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shelving",
3
- "version": "1.289.2",
3
+ "version": "1.291.0",
4
4
  "author": "Dave Houlbrooke <dave@shax.com>",
5
5
  "repository": {
6
6
  "type": "git",
@@ -18,11 +18,18 @@ export type ChoiceOptions<K extends string> = {
18
18
  * @see https://shelving.cc/schema/PossibleChoiceOptions
19
19
  */
20
20
  export type PossibleChoiceOptions<K extends string> = ImmutableArray<K> | ChoiceOptions<K>;
21
+ /**
22
+ * Input control hint for a `ChoiceSchema`: `"radio"` for radio inputs, or `"select"` for a `<select>`.
23
+ *
24
+ * @see https://shelving.cc/schema/ChoiceInputType
25
+ */
26
+ export type ChoiceInputType = "radio" | "select";
21
27
  /**
22
28
  * Options for `ChoiceSchema`.
23
29
  *
24
30
  * - `options` — the allowed choices, as a `{ key: title }` dictionary or an array of keys.
25
31
  * - `value` — default option used when the input is `undefined`.
32
+ * - `input` — control hint for downstream UIs (radios or a select).
26
33
  *
27
34
  * @see https://shelving.cc/schema/ChoiceSchemaOptions
28
35
  */
@@ -31,6 +38,11 @@ export interface ChoiceSchemaOptions<O extends string, I = never> extends Schema
31
38
  readonly options: PossibleChoiceOptions<O>;
32
39
  /** Default option for the value. */
33
40
  readonly value?: O | I;
41
+ /**
42
+ * Input control hint for downstream UIs.
43
+ * @default "radio" for 8 or fewer options, otherwise "select"
44
+ */
45
+ readonly input?: ChoiceInputType | undefined;
34
46
  }
35
47
  /**
36
48
  * Schema that validates a value against a fixed set of allowed string choices.
@@ -43,7 +55,8 @@ export interface ChoiceSchemaOptions<O extends string, I = never> extends Schema
43
55
  export declare class ChoiceSchema<O extends string, I = never> extends Schema<O> {
44
56
  readonly value: O | I | undefined;
45
57
  readonly options: ChoiceOptions<O>;
46
- constructor({ one, title, placeholder, options, value, ...rest }: ChoiceSchemaOptions<O, I>);
58
+ readonly input: ChoiceInputType;
59
+ constructor({ one, title, placeholder, options, value, input, ...rest }: ChoiceSchemaOptions<O, I>);
47
60
  /**
48
61
  * Get an unknown value as one of the allowed choices, or `undefined` if it isn't one.
49
62
  *
@@ -1,5 +1,5 @@
1
1
  import { isArray } from "../util/array.js";
2
- import { isProp } from "../util/object.js";
2
+ import { getKeys, isProp } from "../util/object.js";
3
3
  import { Schema } from "./Schema.js";
4
4
  /** Get a `ChoiceOptions` object for a set of `PossibleChoiceOptions`. */
5
5
  function _getChoiceOptions(options) {
@@ -8,6 +8,8 @@ function _getChoiceOptions(options) {
8
8
  function _getChoiceOption(k) {
9
9
  return [k, k];
10
10
  }
11
+ /** Maximum number of options for which a `ChoiceSchema` defaults to radio inputs. */
12
+ const MAX_RADIO_OPTIONS = 8;
11
13
  /**
12
14
  * Schema that validates a value against a fixed set of allowed string choices.
13
15
  *
@@ -18,9 +20,11 @@ function _getChoiceOption(k) {
18
20
  */
19
21
  export class ChoiceSchema extends Schema {
20
22
  options;
21
- constructor({ one = "choice", title = "Choice", placeholder = `No ${one}`, options, value, ...rest }) {
23
+ input;
24
+ constructor({ one = "choice", title = "Choice", placeholder = `No ${one}`, options, value, input, ...rest }) {
22
25
  super({ one, title, value, placeholder, ...rest });
23
26
  this.options = _getChoiceOptions(options);
27
+ this.input = input ?? (getKeys(this.options).length <= MAX_RADIO_OPTIONS ? "radio" : "select");
24
28
  }
25
29
  /**
26
30
  * Get an unknown value as one of the allowed choices, or `undefined` if it isn't one.
@@ -80,8 +80,9 @@ export declare function NumberSchemaInput({ schema, value, ...props }: NumberSch
80
80
  export interface ChoiceSchemaInputProps extends SchemaInputProps<ChoiceSchema<string>, unknown> {
81
81
  }
82
82
  /**
83
- * Show a choice input for a `ChoiceSchema` — radio inputs for up to 8 options, otherwise a select.
83
+ * Show a choice input for a `ChoiceSchema` — radio inputs or a select.
84
84
  *
85
+ * - The schema's `input` picks the control: `"radio"` for radio inputs, `"select"` for a select.
85
86
  * - The value is normalised through the schema's `get()`, so invalid or sentinel values (e.g. a `CountrySchema`'s `"detect"`) resolve to a real option or fall back to the placeholder rather than mis-selecting the first option.
86
87
  *
87
88
  * @returns A `ChoiceRadioInputs` or `SelectInput` element bound to the schema.
@@ -14,7 +14,6 @@ import { isArray } from "../../util/array.js";
14
14
  import { isData } from "../../util/data.js";
15
15
  import { isDictionary } from "../../util/dictionary.js";
16
16
  import { getNumber } from "../../util/number.js";
17
- import { getKeys } from "../../util/object.js";
18
17
  import { getSource, requireSource } from "../../util/source.js";
19
18
  import { getString } from "../../util/string.js";
20
19
  import { Field } from "../form/Field.js";
@@ -94,8 +93,9 @@ export function NumberSchemaInput({ schema, value, ...props }) {
94
93
  return _jsx(NumberInput, { ...schema, value: getNumber(value), formatter: num => schema.format(num), ...props });
95
94
  }
96
95
  /**
97
- * Show a choice input for a `ChoiceSchema` — radio inputs for up to 8 options, otherwise a select.
96
+ * Show a choice input for a `ChoiceSchema` — radio inputs or a select.
98
97
  *
98
+ * - The schema's `input` picks the control: `"radio"` for radio inputs, `"select"` for a select.
99
99
  * - The value is normalised through the schema's `get()`, so invalid or sentinel values (e.g. a `CountrySchema`'s `"detect"`) resolve to a real option or fall back to the placeholder rather than mis-selecting the first option.
100
100
  *
101
101
  * @returns A `ChoiceRadioInputs` or `SelectInput` element bound to the schema.
@@ -106,7 +106,7 @@ export function NumberSchemaInput({ schema, value, ...props }) {
106
106
  export function ChoiceSchemaInput({ schema, value, ...props }) {
107
107
  const choice = requireSource(ChoiceSchema, schema);
108
108
  const string = choice.get(value);
109
- if (getKeys(choice.options).length <= 8)
109
+ if (choice.input === "radio")
110
110
  return _jsx(ChoiceRadioInputs, { ...schema, value: string, ...props });
111
111
  return _jsx(SelectInput, { ...schema, value: string, ...props });
112
112
  }
@@ -17,8 +17,8 @@ The schema-to-input mapping:
17
17
  | `NumberSchema` | `<NumberInput>` (formatted on blur) |
18
18
  | `DateSchema` | `<DateInput>` |
19
19
  | `BooleanSchema` | `<CheckboxInput>` |
20
- | `ChoiceSchema` (≤ 8 options) | `<ChoiceRadioInputs>` |
21
- | `ChoiceSchema` (> 8 options) | `<SelectInput>` |
20
+ | `ChoiceSchema` (`input: "radio"`, the default for ≤ 8 options) | `<ChoiceRadioInputs>` |
21
+ | `ChoiceSchema` (`input: "select"`, the default for > 8 options) | `<SelectInput>` |
22
22
  | `ArraySchema` | `<ArrayInput>` |
23
23
  | `DictionarySchema` | `<DictionaryInput>` |
24
24
  | `DataSchema` | `<DataInput>` |
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import { renderToStaticMarkup } from "react-dom/server";
3
- import { PASSWORD, StringSchema, URL_SCHEMA } from "shelving/schema";
4
- import { StringSchemaInput, TextInput, type TextInputProps } from "shelving/ui";
3
+ import { ChoiceSchema, PASSWORD, StringSchema, URL_SCHEMA } from "shelving/schema";
4
+ import { ChoiceSchemaInput, StringSchemaInput, TextInput, type TextInputProps } from "shelving/ui";
5
5
  import { PASSTHROUGH } from "shelving/util/function";
6
6
 
7
7
  /** A `StringSchema` with a non-identity `format()` (wraps in brackets) so display and published values differ. */
@@ -100,3 +100,31 @@ describe("StringSchemaInput", () => {
100
100
  expect(currentTarget.value).toBe("example.com/path");
101
101
  });
102
102
  });
103
+
104
+ describe("ChoiceSchemaInput", () => {
105
+ const FEW = ["a", "b", "c"];
106
+ const MANY = ["a", "b", "c", "d", "e", "f", "g", "h", "i"];
107
+
108
+ test("shows radios for 8 or fewer options when input is not set", () => {
109
+ const html = renderToStaticMarkup(<ChoiceSchemaInput name="x" schema={new ChoiceSchema({ options: FEW })} onValue={PASSTHROUGH} />);
110
+ expect(html).toContain('type="radio"');
111
+ expect(html).not.toContain("<select");
112
+ });
113
+ test("shows a select for more than 8 options when input is not set", () => {
114
+ const html = renderToStaticMarkup(<ChoiceSchemaInput name="x" schema={new ChoiceSchema({ options: MANY })} onValue={PASSTHROUGH} />);
115
+ expect(html).toContain("<select");
116
+ expect(html).not.toContain('type="radio"');
117
+ });
118
+ test("shows a select for few options when input is select", () => {
119
+ const schema = new ChoiceSchema({ options: FEW, input: "select" });
120
+ const html = renderToStaticMarkup(<ChoiceSchemaInput name="x" schema={schema} onValue={PASSTHROUGH} />);
121
+ expect(html).toContain("<select");
122
+ expect(html).not.toContain('type="radio"');
123
+ });
124
+ test("shows radios for many options when input is radio", () => {
125
+ const schema = new ChoiceSchema({ options: MANY, input: "radio" });
126
+ const html = renderToStaticMarkup(<ChoiceSchemaInput name="x" schema={schema} onValue={PASSTHROUGH} />);
127
+ expect(html).toContain('type="radio"');
128
+ expect(html).not.toContain("<select");
129
+ });
130
+ });
@@ -15,7 +15,6 @@ import { isArray } from "../../util/array.js";
15
15
  import { type Data, isData } from "../../util/data.js";
16
16
  import { isDictionary } from "../../util/dictionary.js";
17
17
  import { getNumber } from "../../util/number.js";
18
- import { getKeys } from "../../util/object.js";
19
18
  import { getSource, requireSource } from "../../util/source.js";
20
19
  import { getString } from "../../util/string.js";
21
20
  import type { ValidatorType } from "../../util/validate.js";
@@ -148,8 +147,9 @@ export function NumberSchemaInput({ schema, value, ...props }: NumberSchemaInput
148
147
  export interface ChoiceSchemaInputProps extends SchemaInputProps<ChoiceSchema<string>, unknown> {}
149
148
 
150
149
  /**
151
- * Show a choice input for a `ChoiceSchema` — radio inputs for up to 8 options, otherwise a select.
150
+ * Show a choice input for a `ChoiceSchema` — radio inputs or a select.
152
151
  *
152
+ * - The schema's `input` picks the control: `"radio"` for radio inputs, `"select"` for a select.
153
153
  * - The value is normalised through the schema's `get()`, so invalid or sentinel values (e.g. a `CountrySchema`'s `"detect"`) resolve to a real option or fall back to the placeholder rather than mis-selecting the first option.
154
154
  *
155
155
  * @returns A `ChoiceRadioInputs` or `SelectInput` element bound to the schema.
@@ -160,7 +160,7 @@ export interface ChoiceSchemaInputProps extends SchemaInputProps<ChoiceSchema<st
160
160
  export function ChoiceSchemaInput({ schema, value, ...props }: ChoiceSchemaInputProps): ReactElement {
161
161
  const choice = requireSource(ChoiceSchema, schema);
162
162
  const string = choice.get(value);
163
- if (getKeys(choice.options).length <= 8) return <ChoiceRadioInputs {...schema} value={string} {...props} />;
163
+ if (choice.input === "radio") return <ChoiceRadioInputs {...schema} value={string} {...props} />;
164
164
  return <SelectInput {...schema} value={string} {...props} />;
165
165
  }
166
166