@gjermundgaraba-designs/pixel-paper 0.11.0 → 0.13.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/DESIGN.md +13 -13
- package/dist/src/emptyState/emptyState.d.ts +4 -0
- package/dist/src/emptyState/emptyState.d.ts.map +1 -1
- package/dist/src/emptyState/emptyState.js +4 -2
- package/dist/src/field/field.d.ts +14 -6
- package/dist/src/field/field.d.ts.map +1 -1
- package/dist/src/field/field.js +17 -5
- package/dist/src/fieldset/fieldset.d.ts +7 -2
- package/dist/src/fieldset/fieldset.d.ts.map +1 -1
- package/dist/src/fieldset/fieldset.js +10 -4
- package/dist/src/lineDiff/diff.d.ts +26 -31
- package/dist/src/lineDiff/diff.d.ts.map +1 -1
- package/dist/src/lineDiff/diff.js +62 -41
- package/dist/src/lineDiff/index.d.ts +2 -2
- package/dist/src/lineDiff/index.d.ts.map +1 -1
- package/dist/src/lineDiff/index.js +1 -1
- package/dist/src/lineDiff/lineDiff.d.ts +14 -0
- package/dist/src/lineDiff/lineDiff.d.ts.map +1 -1
- package/dist/src/lineDiff/lineDiff.js +43 -21
- package/dist/src/passwordField/passwordField.d.ts +11 -6
- package/dist/src/passwordField/passwordField.d.ts.map +1 -1
- package/dist/src/passwordField/passwordField.js +10 -7
- package/dist/src/places/places.d.ts +15 -5
- package/dist/src/places/places.d.ts.map +1 -1
- package/dist/src/places/places.js +19 -17
- package/dist/src/radioChoices/radioChoices.d.ts +4 -4
- package/dist/src/radioChoices/radioChoices.d.ts.map +1 -1
- package/dist/src/rail/rail.d.ts +2 -2
- package/dist/src/rail/rail.d.ts.map +1 -1
- package/dist/src/rail/rail.js +5 -3
- package/dist/src/rows/index.d.ts +1 -1
- package/dist/src/rows/index.d.ts.map +1 -1
- package/dist/src/rows/rows.d.ts +31 -5
- package/dist/src/rows/rows.d.ts.map +1 -1
- package/dist/src/rows/rows.js +46 -17
- package/dist/src/selectField/selectField.d.ts +7 -5
- package/dist/src/selectField/selectField.d.ts.map +1 -1
- package/dist/src/selectField/selectField.js +12 -6
- package/dist/src/sheet/sheet.d.ts +3 -2
- package/dist/src/sheet/sheet.d.ts.map +1 -1
- package/dist/src/sheet/sheet.js +3 -2
- package/dist/src/slot/slot.d.ts +2 -1
- package/dist/src/slot/slot.d.ts.map +1 -1
- package/dist/src/slot/slot.js +2 -2
- package/dist/src/textareaField/textareaField.d.ts +15 -5
- package/dist/src/textareaField/textareaField.d.ts.map +1 -1
- package/dist/src/textareaField/textareaField.js +15 -6
- package/package.json +1 -1
- package/src/emptyState/emptyState.ts +8 -2
- package/src/field/field.ts +37 -8
- package/src/fieldset/fieldset.ts +16 -4
- package/src/lineDiff/diff.ts +104 -48
- package/src/lineDiff/index.ts +2 -2
- package/src/lineDiff/lineDiff.ts +76 -31
- package/src/passwordField/passwordField.ts +18 -9
- package/src/pixel-paper.css +41 -6
- package/src/places/places.ts +43 -30
- package/src/radioChoices/radioChoices.ts +2 -2
- package/src/rail/rail.ts +5 -3
- package/src/rows/index.ts +1 -0
- package/src/rows/rows.ts +95 -25
- package/src/selectField/selectField.ts +17 -17
- package/src/sheet/sheet.ts +3 -2
- package/src/slot/slot.ts +4 -1
- package/src/textareaField/textareaField.ts +39 -9
package/src/rows/rows.ts
CHANGED
|
@@ -14,14 +14,19 @@ export type ViewConfig = Readonly<{
|
|
|
14
14
|
rows: ReadonlyArray<Html>;
|
|
15
15
|
/** An outline: nested rows indent 26px and hang on a 1px rule guide. */
|
|
16
16
|
isOutline?: boolean;
|
|
17
|
-
/** Names the list for screen readers. */
|
|
17
|
+
/** Names the list for screen readers; a treegrid needs it. */
|
|
18
18
|
label?: string;
|
|
19
|
+
/** A keyboard-driven list, whose cursor the application moves (see Keys in DESIGN.md): a
|
|
20
|
+
* `role="treegrid"`, so a screen reader hands it the arrow and letter keys. Give every row
|
|
21
|
+
* its `treegrid` place. */
|
|
22
|
+
isTreegrid?: boolean;
|
|
19
23
|
}>;
|
|
20
24
|
|
|
21
25
|
/** A list of 34px rows. */
|
|
22
26
|
export const view = <Message>(config: ViewConfig, h: HtmlBuilder<Message>): Html =>
|
|
23
27
|
h.ul(
|
|
24
28
|
[
|
|
29
|
+
...(config.isTreegrid === true ? [h.Role("treegrid")] : []),
|
|
25
30
|
...(config.label === undefined ? [] : [h.AriaLabel(config.label)]),
|
|
26
31
|
h.Class(config.isOutline === true ? "rows tree" : "rows"),
|
|
27
32
|
],
|
|
@@ -45,6 +50,20 @@ export type RowSelect<Message> = Readonly<{
|
|
|
45
50
|
label?: string;
|
|
46
51
|
}>;
|
|
47
52
|
|
|
53
|
+
/** A row's place in a treegrid (`view`'s `isTreegrid`): its line is a `role="row"` and each
|
|
54
|
+
* of its parts a `role="gridcell"`, while the list items around them step aside
|
|
55
|
+
* (`role="none"`). */
|
|
56
|
+
export type TreegridPlace = Readonly<{
|
|
57
|
+
/** Its depth, from 1 at the list's roots: `aria-level`. */
|
|
58
|
+
level: number;
|
|
59
|
+
/** Its place among its siblings, from 1: `aria-posinset`. */
|
|
60
|
+
position: number;
|
|
61
|
+
/** How many siblings it has, itself included: `aria-setsize`. */
|
|
62
|
+
setSize: number;
|
|
63
|
+
/** Whether the rows under it show, for a row that folds: `aria-expanded`. */
|
|
64
|
+
isExpanded?: boolean;
|
|
65
|
+
}>;
|
|
66
|
+
|
|
48
67
|
/** Configuration for rendering a row with `rowView`. */
|
|
49
68
|
export type RowConfig<Message> = Readonly<{
|
|
50
69
|
/** The row's stable id among its siblings. */
|
|
@@ -55,6 +74,10 @@ export type RowConfig<Message> = Readonly<{
|
|
|
55
74
|
body: ReadonlyArray<Html | string>;
|
|
56
75
|
/** Right-aligned chips, marks, counts, due days. */
|
|
57
76
|
slot?: ReadonlyArray<Html | string>;
|
|
77
|
+
/** Further attributes for the slot's `span.slot`, such as an id the row's line is described
|
|
78
|
+
* by (`aria-describedby`); there is no span, and so nothing to point at, while `slot` is
|
|
79
|
+
* empty. Never a class. */
|
|
80
|
+
slotAttributes?: Attributes<Message>;
|
|
58
81
|
/** Controls that belong to the row's job, before the tools (an approve and reject pair). */
|
|
59
82
|
actions?: ReadonlyArray<Html | string>;
|
|
60
83
|
/** Icon buttons at the row's right end, dimmed until the row is hovered or focused (full
|
|
@@ -68,12 +91,45 @@ export type RowConfig<Message> = Readonly<{
|
|
|
68
91
|
isContext?: boolean;
|
|
69
92
|
/** Makes the row selectable: a box at its left edge. */
|
|
70
93
|
select?: RowSelect<Message>;
|
|
94
|
+
/** The row's place in a treegrid; every row of a treegrid has one. */
|
|
95
|
+
treegrid?: TreegridPlace;
|
|
71
96
|
/** Further attributes for the row's line, the `div.row`, not its `li`: a keyboard list's
|
|
72
97
|
* cursor (`aria-current="true"`, which the stylesheet outlines, and a roving `tabindex`),
|
|
73
|
-
* `role="group"`
|
|
98
|
+
* its name (with `role="group"` outside a treegrid, whose row is already a named `row`), data
|
|
99
|
+
* attributes, key and focus handlers. Never a class. */
|
|
74
100
|
attributes?: Attributes<Message>;
|
|
75
101
|
}>;
|
|
76
102
|
|
|
103
|
+
const cellRole = <Message>(
|
|
104
|
+
config: RowConfig<Message>,
|
|
105
|
+
h: HtmlBuilder<Message>,
|
|
106
|
+
): Attributes<Message> => (config.treegrid === undefined ? [] : [h.Role("gridcell")]);
|
|
107
|
+
|
|
108
|
+
const rowRole = <Message>(
|
|
109
|
+
place: TreegridPlace | undefined,
|
|
110
|
+
h: HtmlBuilder<Message>,
|
|
111
|
+
): Attributes<Message> =>
|
|
112
|
+
place === undefined
|
|
113
|
+
? []
|
|
114
|
+
: [
|
|
115
|
+
h.Role("row"),
|
|
116
|
+
h.AriaLevel(place.level),
|
|
117
|
+
h.AriaPosinset(place.position),
|
|
118
|
+
h.AriaSetsize(place.setSize),
|
|
119
|
+
...(place.isExpanded === undefined ? [] : [h.AriaExpanded(place.isExpanded)]),
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
/** A part the caller gives (a glyph, the actions) in its own cell, in a treegrid. */
|
|
123
|
+
const cellView = <Message>(
|
|
124
|
+
config: RowConfig<Message>,
|
|
125
|
+
className: string,
|
|
126
|
+
content: ReadonlyArray<Html | string>,
|
|
127
|
+
h: HtmlBuilder<Message>,
|
|
128
|
+
): ReadonlyArray<Html | string> =>
|
|
129
|
+
config.treegrid === undefined
|
|
130
|
+
? content
|
|
131
|
+
: slotView("span", className, content, h, cellRole(config, h));
|
|
132
|
+
|
|
77
133
|
/** The row's text, with the words that say a context row is one. */
|
|
78
134
|
const textOf = <Message>(
|
|
79
135
|
config: RowConfig<Message>,
|
|
@@ -90,14 +146,21 @@ const lineView = <Message>(
|
|
|
90
146
|
h: HtmlBuilder<Message>,
|
|
91
147
|
): Html =>
|
|
92
148
|
h.div(
|
|
93
|
-
[
|
|
149
|
+
[
|
|
150
|
+
...(config.attributes ?? []),
|
|
151
|
+
...rowRole(config.treegrid, h),
|
|
152
|
+
h.Class(config.isContext === true ? "row context" : "row"),
|
|
153
|
+
],
|
|
94
154
|
[
|
|
95
155
|
...box,
|
|
96
|
-
...(config.glyph === undefined ? [] : [config.glyph]),
|
|
156
|
+
...cellView(config, "cell", config.glyph === undefined ? [] : [config.glyph], h),
|
|
97
157
|
text,
|
|
98
|
-
...slotView("span", "slot", config.slot ?? [], h
|
|
99
|
-
|
|
100
|
-
|
|
158
|
+
...slotView("span", "slot", config.slot ?? [], h, [
|
|
159
|
+
...(config.slotAttributes ?? []),
|
|
160
|
+
...cellRole(config, h),
|
|
161
|
+
]),
|
|
162
|
+
...cellView(config, "cell actions", config.actions ?? [], h),
|
|
163
|
+
...slotView("span", "tools", config.tools ?? [], h, cellRole(config, h)),
|
|
101
164
|
],
|
|
102
165
|
);
|
|
103
166
|
|
|
@@ -113,19 +176,25 @@ const selectableLineView = <Message>(
|
|
|
113
176
|
onToggle: select.onToggle,
|
|
114
177
|
toView: (groups) => {
|
|
115
178
|
const boxView = (name: ReadonlyArray<Html>): Html =>
|
|
116
|
-
h.span(
|
|
179
|
+
h.span(
|
|
180
|
+
[...cellRole(config, h), h.Class("row-check")],
|
|
181
|
+
[h.button(groups.checkbox), ...name],
|
|
182
|
+
);
|
|
183
|
+
const label = h.label([...groups.label, h.Class("grow")], textOf(config, h));
|
|
117
184
|
|
|
118
185
|
return select.label === undefined
|
|
119
186
|
? lineView(
|
|
120
187
|
config,
|
|
121
188
|
[boxView([])],
|
|
122
|
-
|
|
189
|
+
config.treegrid === undefined
|
|
190
|
+
? label
|
|
191
|
+
: h.span([...cellRole(config, h), h.Class("cell grow-cell")], [label]),
|
|
123
192
|
h,
|
|
124
193
|
)
|
|
125
194
|
: lineView(
|
|
126
195
|
config,
|
|
127
196
|
[boxView([h.span([...groups.label, h.Class("sr-only")], [select.label])])],
|
|
128
|
-
h.div([h.Class("grow")], textOf(config, h)),
|
|
197
|
+
h.div([...cellRole(config, h), h.Class("grow")], textOf(config, h)),
|
|
129
198
|
h,
|
|
130
199
|
);
|
|
131
200
|
},
|
|
@@ -135,18 +204,18 @@ const selectableLineView = <Message>(
|
|
|
135
204
|
|
|
136
205
|
/** One 34px row: box, glyph, text, slot, actions, tools, and the rows nested under it. Hover
|
|
137
206
|
* and focus tint it head. The box of a selectable row is built on `@foldkit/ui`'s Checkbox:
|
|
138
|
-
* a `role="checkbox"` button (`aria-checked`) whose whole cell is its target.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
207
|
+
* a `role="checkbox"` button (`aria-checked`) whose whole cell is its target. In a treegrid
|
|
208
|
+
* the line is the `role="row"` and each part a `role="gridcell"`. */
|
|
209
|
+
export const rowView = <Message>(config: RowConfig<Message>, h: HtmlBuilder<Message>): Html => {
|
|
210
|
+
const listRole = config.treegrid === undefined ? [] : [h.Role("none")];
|
|
211
|
+
|
|
212
|
+
return h.keyed("li")(config.key, listRole, [
|
|
213
|
+
config.select === undefined
|
|
214
|
+
? lineView(config, [], h.div([...cellRole(config, h), h.Class("grow")], textOf(config, h)), h)
|
|
215
|
+
: selectableLineView(config, config.select, h),
|
|
216
|
+
...slotView("ul", "rows", config.nested ?? [], h, listRole),
|
|
217
|
+
]);
|
|
218
|
+
};
|
|
150
219
|
|
|
151
220
|
// GLYPH SLOT
|
|
152
221
|
|
|
@@ -162,13 +231,14 @@ export const glyphView = <Message>(config: GlyphConfig, h: HtmlBuilder<Message>)
|
|
|
162
231
|
/** Configuration for rendering a fold toggle with `toggleView`. */
|
|
163
232
|
export type ToggleConfig<Message> = Readonly<{
|
|
164
233
|
isExpanded: boolean;
|
|
165
|
-
/** What
|
|
234
|
+
/** What the nested rows hang under: the toggle is named `"Rows under Notes"` whether open
|
|
235
|
+
* or not, and `aria-expanded` says which. */
|
|
166
236
|
label: string;
|
|
167
237
|
onToggle: Message;
|
|
168
238
|
}>;
|
|
169
239
|
|
|
170
240
|
/** The glyph slot as a fold toggle, built on `@foldkit/ui`'s Button: a ring, open or closed,
|
|
171
|
-
* `aria-expanded`, named for
|
|
241
|
+
* `aria-expanded`, named for the rows it shows. */
|
|
172
242
|
export const toggleView = <Message>(config: ToggleConfig<Message>, h: HtmlBuilder<Message>): Html =>
|
|
173
243
|
UiButton.view(
|
|
174
244
|
{
|
|
@@ -178,7 +248,7 @@ export const toggleView = <Message>(config: ToggleConfig<Message>, h: HtmlBuilde
|
|
|
178
248
|
[
|
|
179
249
|
...groups.button,
|
|
180
250
|
h.AriaExpanded(config.isExpanded),
|
|
181
|
-
h.AriaLabel(
|
|
251
|
+
h.AriaLabel(`Rows under ${config.label}`),
|
|
182
252
|
h.Class("glyph"),
|
|
183
253
|
],
|
|
184
254
|
[Glyph.view({ name: config.isExpanded ? "ring" : "ring-closed" }, h)],
|
|
@@ -2,6 +2,8 @@ import { Select as UiSelect } from "@foldkit/ui";
|
|
|
2
2
|
import { Array } from "effect";
|
|
3
3
|
import type { Html, HtmlBuilder } from "foldkit/html";
|
|
4
4
|
|
|
5
|
+
import { describedByError, errorView } from "../field/field.ts";
|
|
6
|
+
|
|
5
7
|
// VIEW
|
|
6
8
|
|
|
7
9
|
/** One choice in a `SelectField`. */
|
|
@@ -31,8 +33,10 @@ export type ViewConfig<Message> = Readonly<{
|
|
|
31
33
|
options: ReadonlyArray<SelectOption | SelectGroup>;
|
|
32
34
|
/** A sans sentence under the control. */
|
|
33
35
|
help?: string;
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
+
/** Why the value was refused: a red sentence under the control, which the control is
|
|
37
|
+
* described by, with red on its box and its label (`aria-invalid`). Say it again in an
|
|
38
|
+
* ErrorSummary above the form. */
|
|
39
|
+
error?: string | undefined;
|
|
36
40
|
/** The form is not sent without it: screen readers hear "required" (`aria-required`). */
|
|
37
41
|
isRequired?: boolean;
|
|
38
42
|
}>;
|
|
@@ -56,27 +60,18 @@ const entryView =
|
|
|
56
60
|
: optionView(h)(entry);
|
|
57
61
|
|
|
58
62
|
/** A labelled select with its help line, built on `@foldkit/ui`'s `Select`: the label names
|
|
59
|
-
* the select, the help line describes it (`aria-describedby`), `
|
|
60
|
-
* `aria-invalid` and `isRequired` sets `aria-required`.
|
|
61
|
-
* caret and the picker. */
|
|
63
|
+
* the select, the help line describes it (`aria-describedby`), an `error` sets
|
|
64
|
+
* `aria-invalid` and describes it before the help, and `isRequired` sets `aria-required`.
|
|
65
|
+
* The stylesheet draws the box, the caret and the picker. */
|
|
62
66
|
export const view = <Message>(config: ViewConfig<Message>, h: HtmlBuilder<Message>): Html => {
|
|
63
|
-
const {
|
|
64
|
-
id,
|
|
65
|
-
label,
|
|
66
|
-
value,
|
|
67
|
-
onChange,
|
|
68
|
-
options,
|
|
69
|
-
help,
|
|
70
|
-
isInvalid = false,
|
|
71
|
-
isRequired = false,
|
|
72
|
-
} = config;
|
|
67
|
+
const { id, label, value, onChange, options, help, error, isRequired = false } = config;
|
|
73
68
|
|
|
74
69
|
return UiSelect.view(
|
|
75
70
|
{
|
|
76
71
|
id,
|
|
77
72
|
value,
|
|
78
73
|
onChange,
|
|
79
|
-
isInvalid,
|
|
74
|
+
isInvalid: error !== undefined,
|
|
80
75
|
hasDescription: help !== undefined,
|
|
81
76
|
toView: (groups) =>
|
|
82
77
|
h.div(
|
|
@@ -85,9 +80,14 @@ export const view = <Message>(config: ViewConfig<Message>, h: HtmlBuilder<Messag
|
|
|
85
80
|
h.label(groups.label, [label]),
|
|
86
81
|
// NOTE: @foldkit/ui gap: Select has no required option, so the wrapper says it.
|
|
87
82
|
h.select(
|
|
88
|
-
[
|
|
83
|
+
[
|
|
84
|
+
...groups.select,
|
|
85
|
+
...describedByError(id, error, help !== undefined, h),
|
|
86
|
+
...(isRequired ? [h.AriaRequired(true)] : []),
|
|
87
|
+
],
|
|
89
88
|
Array.map(options, entryView(h)),
|
|
90
89
|
),
|
|
90
|
+
...errorView(id, error, h),
|
|
91
91
|
...(help === undefined
|
|
92
92
|
? []
|
|
93
93
|
: [h.p([...groups.description, h.Class("field-help")], [help])]),
|
package/src/sheet/sheet.ts
CHANGED
|
@@ -16,8 +16,9 @@ export type ViewConfig = Readonly<{
|
|
|
16
16
|
body: ReadonlyArray<Html | string>;
|
|
17
17
|
}>;
|
|
18
18
|
|
|
19
|
-
/** An authored page
|
|
20
|
-
* system slows to reading pace.
|
|
19
|
+
/** An authored page, an `<article>` under its `h1` on its own sheet of paper, framed as a
|
|
20
|
+
* panel is: the one place the system slows to reading pace. It stands in a panel's place,
|
|
21
|
+
* never inside one, so its `h1` opens the page instead of following a panel's `h2`. */
|
|
21
22
|
export const view = <Message>(config: ViewConfig, h: HtmlBuilder<Message>): Html => {
|
|
22
23
|
const { title, byline = [], actions = [], body } = config;
|
|
23
24
|
|
package/src/slot/slot.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Array } from "effect";
|
|
2
2
|
import type { Html, HtmlBuilder } from "foldkit/html";
|
|
3
3
|
|
|
4
|
+
import type { Attributes } from "../attributes/attributes.ts";
|
|
5
|
+
|
|
4
6
|
/** The elements a slot is wrapped in. */
|
|
5
7
|
type SlotTag = "div" | "span" | "p" | "dd" | "ul";
|
|
6
8
|
|
|
@@ -11,8 +13,9 @@ export const slotView = <Message>(
|
|
|
11
13
|
slotClass: string,
|
|
12
14
|
slot: ReadonlyArray<Html | string>,
|
|
13
15
|
h: HtmlBuilder<Message>,
|
|
16
|
+
attributes: Attributes<Message> = [],
|
|
14
17
|
): ReadonlyArray<Html> =>
|
|
15
18
|
Array.match(slot, {
|
|
16
19
|
onEmpty: () => [],
|
|
17
|
-
onNonEmpty: (content) => [h[tag]([h.Class(slotClass)], content)],
|
|
20
|
+
onNonEmpty: (content) => [h[tag]([...attributes, h.Class(slotClass)], content)],
|
|
18
21
|
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Textarea as UiTextarea } from "@foldkit/ui";
|
|
2
|
-
import type { Html, HtmlBuilder } from "foldkit/html";
|
|
2
|
+
import type { ChildAttribute, Html, HtmlBuilder, TextareaAttribute } from "foldkit/html";
|
|
3
|
+
|
|
4
|
+
import { describedByError, errorView } from "../field/field.ts";
|
|
3
5
|
|
|
4
6
|
// VIEW
|
|
5
7
|
|
|
@@ -11,32 +13,60 @@ export type ViewConfig<Message> = Readonly<{
|
|
|
11
13
|
onInput: (value: string) => Message;
|
|
12
14
|
/** A sans sentence under the control. */
|
|
13
15
|
help?: string;
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
+
/** Why the value was refused: a red sentence under the control, which the control is
|
|
17
|
+
* described by, with red on its rule and its label (`aria-invalid`). Say it again in an
|
|
18
|
+
* ErrorSummary above the form. */
|
|
19
|
+
error?: string | undefined;
|
|
20
|
+
/** The form is not sent without it: screen readers hear "required" (`aria-required`). */
|
|
21
|
+
isRequired?: boolean;
|
|
22
|
+
/** Code and identifiers: the system monospace. */
|
|
23
|
+
isMono?: boolean;
|
|
16
24
|
/** The lines it opens at; it grows with its text from there. */
|
|
17
25
|
rows?: number;
|
|
26
|
+
/** Further attributes for the `<textarea>`, such as a placeholder or spelling off; never a
|
|
27
|
+
* class, and never its text, which is `value`. */
|
|
28
|
+
textareaAttributes?: ReadonlyArray<TextareaAttribute<Message> | ChildAttribute>;
|
|
18
29
|
}>;
|
|
19
30
|
|
|
20
31
|
/** A labelled textarea with its help line, built on `@foldkit/ui`'s `Textarea`: the label
|
|
21
|
-
* names the textarea, the help line describes it (`aria-describedby`)
|
|
22
|
-
* `aria-invalid
|
|
32
|
+
* names the textarea, the help line describes it (`aria-describedby`), an `error` sets
|
|
33
|
+
* `aria-invalid` and describes it before the help, and `isRequired` sets `aria-required`.
|
|
34
|
+
* Boxless like a Field, on a dotted baseline, with an ink corner grip. */
|
|
23
35
|
export const view = <Message>(config: ViewConfig<Message>, h: HtmlBuilder<Message>): Html => {
|
|
24
|
-
const {
|
|
36
|
+
const {
|
|
37
|
+
id,
|
|
38
|
+
label,
|
|
39
|
+
value,
|
|
40
|
+
onInput,
|
|
41
|
+
help,
|
|
42
|
+
error,
|
|
43
|
+
isRequired = false,
|
|
44
|
+
isMono = false,
|
|
45
|
+
rows,
|
|
46
|
+
textareaAttributes = [],
|
|
47
|
+
} = config;
|
|
25
48
|
|
|
26
49
|
return UiTextarea.view(
|
|
27
50
|
{
|
|
28
51
|
id,
|
|
29
52
|
value,
|
|
30
53
|
onInput,
|
|
31
|
-
isInvalid,
|
|
54
|
+
isInvalid: error !== undefined,
|
|
32
55
|
hasDescription: help !== undefined,
|
|
33
56
|
...(rows !== undefined && { rows }),
|
|
34
57
|
toView: (groups) =>
|
|
35
58
|
h.div(
|
|
36
|
-
[h.Class(
|
|
59
|
+
[h.Class(`field${isMono ? " mono" : ""}`)],
|
|
37
60
|
[
|
|
38
61
|
h.label(groups.label, [label]),
|
|
39
|
-
|
|
62
|
+
// NOTE: @foldkit/ui gap: Textarea has no required option, so the wrapper says it.
|
|
63
|
+
h.textarea([
|
|
64
|
+
...textareaAttributes,
|
|
65
|
+
...groups.textarea,
|
|
66
|
+
...describedByError(id, error, help !== undefined, h),
|
|
67
|
+
...(isRequired ? [h.AriaRequired(true)] : []),
|
|
68
|
+
]),
|
|
69
|
+
...errorView(id, error, h),
|
|
40
70
|
...(help === undefined
|
|
41
71
|
? []
|
|
42
72
|
: [h.p([...groups.description, h.Class("field-help")], [help])]),
|