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 +1 -1
- package/schema/ChoiceSchema.d.ts +14 -1
- package/schema/ChoiceSchema.js +6 -2
- package/ui/input/SchemaInput.d.ts +2 -1
- package/ui/input/SchemaInput.js +3 -3
- package/ui/input/SchemaInput.md +2 -2
- package/ui/input/SchemaInput.test.tsx +30 -2
- package/ui/input/SchemaInput.tsx +3 -3
package/package.json
CHANGED
package/schema/ChoiceSchema.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
*
|
package/schema/ChoiceSchema.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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.
|
package/ui/input/SchemaInput.js
CHANGED
|
@@ -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
|
|
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 (
|
|
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
|
}
|
package/ui/input/SchemaInput.md
CHANGED
|
@@ -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
|
+
});
|
package/ui/input/SchemaInput.tsx
CHANGED
|
@@ -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
|
|
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 (
|
|
163
|
+
if (choice.input === "radio") return <ChoiceRadioInputs {...schema} value={string} {...props} />;
|
|
164
164
|
return <SelectInput {...schema} value={string} {...props} />;
|
|
165
165
|
}
|
|
166
166
|
|