shelving 1.289.1 → 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 +1 -1
- package/schema/ChoiceSchema.d.ts +14 -1
- package/schema/ChoiceSchema.js +3 -1
- package/ui/input/Input.module.css +9 -8
- package/ui/input/SchemaInput.d.ts +3 -1
- package/ui/input/SchemaInput.js +5 -2
- package/ui/input/SchemaInput.md +2 -2
- package/ui/input/SchemaInput.test.tsx +30 -2
- package/ui/input/SchemaInput.tsx +5 -2
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
|
+
* - 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
|
-
|
|
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
|
*
|
package/schema/ChoiceSchema.js
CHANGED
|
@@ -18,9 +18,11 @@ function _getChoiceOption(k) {
|
|
|
18
18
|
*/
|
|
19
19
|
export class ChoiceSchema extends Schema {
|
|
20
20
|
options;
|
|
21
|
-
|
|
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.
|
|
@@ -167,24 +167,25 @@
|
|
|
167
167
|
pointer-events: none;
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
/* First [data-slot] child sits one indent from the
|
|
170
|
+
/* First [data-slot] child sits one indent from the left, where the text of a plain input starts. Physical, not
|
|
171
|
+
logical: `inset-inline-start` makes bundlers emit a `:lang()` selector for every right-to-left language. */
|
|
171
172
|
& > [data-slot]:first-child {
|
|
172
|
-
|
|
173
|
+
left: var(--input-indent, var(--space-small));
|
|
173
174
|
}
|
|
174
175
|
|
|
175
|
-
/* Last [data-slot] child sits one indent from the
|
|
176
|
+
/* Last [data-slot] child sits one indent from the right. */
|
|
176
177
|
& > [data-slot]:last-child {
|
|
177
|
-
|
|
178
|
+
right: var(--input-indent, var(--space-small));
|
|
178
179
|
}
|
|
179
180
|
|
|
180
|
-
/* Push the inner .input text past the
|
|
181
|
+
/* Push the inner .input text past the left slot, with one indent gap. */
|
|
181
182
|
&:has(> [data-slot]:first-child) > .input {
|
|
182
|
-
padding-
|
|
183
|
+
padding-left: calc(var(--input-icon-size, var(--size-icon)) + var(--input-indent, var(--space-small)));
|
|
183
184
|
}
|
|
184
185
|
|
|
185
|
-
/* Push the inner .input text clear of the
|
|
186
|
+
/* Push the inner .input text clear of the right slot, with one indent gap. */
|
|
186
187
|
&:has(> [data-slot]:last-child) > .input {
|
|
187
|
-
padding-
|
|
188
|
+
padding-right: calc(var(--input-icon-size, var(--size-icon)) + var(--input-indent, var(--space-small)));
|
|
188
189
|
}
|
|
189
190
|
}
|
|
190
191
|
}
|
|
@@ -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
|
|
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.
|
package/ui/input/SchemaInput.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
}
|
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"`, 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
|
+
});
|
package/ui/input/SchemaInput.tsx
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|