shelving 1.289.2 → 1.290.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.290.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
+ * - When not set, a UI picks the control from the number of options.
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 | undefined;
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
  *
@@ -18,9 +18,11 @@ function _getChoiceOption(k) {
18
18
  */
19
19
  export class ChoiceSchema extends Schema {
20
20
  options;
21
- constructor({ one = "choice", title = "Choice", placeholder = `No ${one}`, options, value, ...rest }) {
21
+ input;
22
+ constructor({ one = "choice", title = "Choice", placeholder = `No ${one}`, options, value, input, ...rest }) {
22
23
  super({ one, title, value, placeholder, ...rest });
23
24
  this.options = _getChoiceOptions(options);
25
+ this.input = input;
24
26
  }
25
27
  /**
26
28
  * Get an unknown value as one of the allowed choices, or `undefined` if it isn't one.
@@ -80,8 +80,10 @@ 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.
86
+ * - When `input` is not set, it shows radio inputs for up to 8 options, otherwise a select.
85
87
  * - 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
88
  *
87
89
  * @returns A `ChoiceRadioInputs` or `SelectInput` element bound to the schema.
@@ -94,8 +94,10 @@ export function NumberSchemaInput({ schema, value, ...props }) {
94
94
  return _jsx(NumberInput, { ...schema, value: getNumber(value), formatter: num => schema.format(num), ...props });
95
95
  }
96
96
  /**
97
- * Show a choice input for a `ChoiceSchema` — radio inputs for up to 8 options, otherwise a select.
97
+ * Show a choice input for a `ChoiceSchema` — radio inputs or a select.
98
98
  *
99
+ * - The schema's `input` picks the control: `"radio"` for radio inputs, `"select"` for a select.
100
+ * - When `input` is not set, it shows radio inputs for up to 8 options, otherwise a select.
99
101
  * - 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
102
  *
101
103
  * @returns A `ChoiceRadioInputs` or `SelectInput` element bound to the schema.
@@ -106,7 +108,8 @@ export function NumberSchemaInput({ schema, value, ...props }) {
106
108
  export function ChoiceSchemaInput({ schema, value, ...props }) {
107
109
  const choice = requireSource(ChoiceSchema, schema);
108
110
  const string = choice.get(value);
109
- if (getKeys(choice.options).length <= 8)
111
+ const input = choice.input ?? (getKeys(choice.options).length <= 8 ? "radio" : "select");
112
+ if (input === "radio")
110
113
  return _jsx(ChoiceRadioInputs, { ...schema, value: string, ...props });
111
114
  return _jsx(SelectInput, { ...schema, value: string, ...props });
112
115
  }
@@ -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"`, or ≤ 8 options) | `<ChoiceRadioInputs>` |
21
+ | `ChoiceSchema` (`input: "select"`, or > 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
+ });
@@ -148,8 +148,10 @@ export function NumberSchemaInput({ schema, value, ...props }: NumberSchemaInput
148
148
  export interface ChoiceSchemaInputProps extends SchemaInputProps<ChoiceSchema<string>, unknown> {}
149
149
 
150
150
  /**
151
- * Show a choice input for a `ChoiceSchema` — radio inputs for up to 8 options, otherwise a select.
151
+ * Show a choice input for a `ChoiceSchema` — radio inputs or a select.
152
152
  *
153
+ * - The schema's `input` picks the control: `"radio"` for radio inputs, `"select"` for a select.
154
+ * - When `input` is not set, it shows radio inputs for up to 8 options, otherwise a select.
153
155
  * - 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
156
  *
155
157
  * @returns A `ChoiceRadioInputs` or `SelectInput` element bound to the schema.
@@ -160,7 +162,8 @@ export interface ChoiceSchemaInputProps extends SchemaInputProps<ChoiceSchema<st
160
162
  export function ChoiceSchemaInput({ schema, value, ...props }: ChoiceSchemaInputProps): ReactElement {
161
163
  const choice = requireSource(ChoiceSchema, schema);
162
164
  const string = choice.get(value);
163
- if (getKeys(choice.options).length <= 8) return <ChoiceRadioInputs {...schema} value={string} {...props} />;
165
+ const input = choice.input ?? (getKeys(choice.options).length <= 8 ? "radio" : "select");
166
+ if (input === "radio") return <ChoiceRadioInputs {...schema} value={string} {...props} />;
164
167
  return <SelectInput {...schema} value={string} {...props} />;
165
168
  }
166
169