Skip to content

Commit 4808050

Browse files
byseif21Miodec
andauthored
refactor: extract LabeledField wrapper to remove label+input boilerplate (@byseif21) (#7784)
Co-authored-by: Miodec <jack@monkeytype.com>
1 parent b008306 commit 4808050

File tree

6 files changed

+125
-55
lines changed

6 files changed

+125
-55
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { render, screen } from "@solidjs/testing-library";
2+
import { describe, it, expect } from "vitest";
3+
4+
import { LabeledField } from "../../../../src/ts/components/ui/form/LabeledField";
5+
6+
describe("LabeledField", () => {
7+
it("renders label text correctly", () => {
8+
render(() => (
9+
<LabeledField label="test label">
10+
<input />
11+
</LabeledField>
12+
));
13+
14+
expect(screen.getByText("test label")).toBeInTheDocument();
15+
});
16+
17+
it("renders children correctly", () => {
18+
render(() => (
19+
<LabeledField label="test">
20+
<div data-testid="child">child content</div>
21+
</LabeledField>
22+
));
23+
24+
expect(screen.getByTestId("child")).toBeInTheDocument();
25+
});
26+
27+
it("renders subtext when provided", () => {
28+
render(() => (
29+
<LabeledField label="test" subLabel="helper text">
30+
<input />
31+
</LabeledField>
32+
));
33+
34+
expect(screen.getByText("helper text")).toBeInTheDocument();
35+
});
36+
37+
it("links label to input when id is provided", () => {
38+
const { container } = render(() => (
39+
<LabeledField label="test" id="test-id">
40+
<input id="test-id" />
41+
</LabeledField>
42+
));
43+
44+
const label = container.querySelector("label");
45+
expect(label).toHaveAttribute("for", "test-id");
46+
});
47+
48+
it("applies custom class to wrapper", () => {
49+
const { container } = render(() => (
50+
<LabeledField label="test" class="custom-wrapper-class">
51+
<input />
52+
</LabeledField>
53+
));
54+
55+
expect(container.firstChild).toHaveClass("custom-wrapper-class");
56+
});
57+
});

frontend/src/ts/components/modals/CustomGeneratorModal.tsx

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { AnimatedModal } from "../common/AnimatedModal";
77
import { Button } from "../common/Button";
88
import { Separator } from "../common/Separator";
99
import { InputField } from "../ui/form/InputField";
10+
import { LabeledField } from "../ui/form/LabeledField";
1011
import { SubmitButton } from "../ui/form/SubmitButton";
1112
import { TextareaField } from "../ui/form/TextareaField";
1213
import SlimSelect from "../ui/SlimSelect";
@@ -159,24 +160,22 @@ export function CustomGeneratorModal(props: {
159160
}}
160161
class="grid gap-4"
161162
>
162-
<div class="grid gap-1">
163-
<div class="text-sub">presets</div>
164-
<div class="grid gap-2">
163+
<LabeledField label="presets">
164+
<div class="grid grid-cols-[1fr_auto] gap-2">
165165
<SlimSelect
166166
options={presetOptions}
167167
selected={selectedPreset()}
168168
onChange={setSelectedPreset}
169169
/>
170170
<Button variant="button" text="apply" onClick={applyPreset} />
171171
</div>
172-
</div>
172+
</LabeledField>
173173
<Separator />
174174
<div class="text-xs text-sub">
175175
Enter characters or strings separated by spaces. Random combinations
176176
will be generated using these inputs.
177177
</div>
178-
<div class="grid gap-1">
179-
<div class="text-sub">character set</div>
178+
<LabeledField label="character set">
180179
<form.Field
181180
name="characterSet"
182181
validators={{
@@ -188,27 +187,24 @@ export function CustomGeneratorModal(props: {
188187
<TextareaField field={field} class="min-h-25 p-2 text-text" />
189188
)}
190189
</form.Field>
191-
</div>
190+
</LabeledField>
192191
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
193-
<div class="grid gap-1">
194-
<div class="text-sub">min length</div>
192+
<LabeledField label="min length">
195193
<form.Field name="minLength">
196194
{(field) => <InputField field={field} type="number" />}
197195
</form.Field>
198-
</div>
199-
<div class="grid gap-1">
200-
<div class="text-sub">max length</div>
196+
</LabeledField>
197+
<LabeledField label="max length">
201198
<form.Field name="maxLength">
202199
{(field) => <InputField field={field} type="number" />}
203200
</form.Field>
204-
</div>
201+
</LabeledField>
205202
</div>
206-
<div class="grid gap-1">
207-
<div class="text-sub">word count</div>
203+
<LabeledField label="word count">
208204
<form.Field name="wordCount">
209205
{(field) => <InputField field={field} type="number" />}
210206
</form.Field>
211-
</div>
207+
</LabeledField>
212208
<div class="text-xs text-sub">
213209
{
214210
'"Set" replaces the current custom text with generated words, "Add" appends generated words to the current custom text.'

frontend/src/ts/components/modals/QuoteReportModal.tsx

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { removeLanguageSize } from "../../utils/strings";
1919
import { AnimatedModal } from "../common/AnimatedModal";
2020
import { Button } from "../common/Button";
2121
import { Separator } from "../common/Separator";
22+
import { LabeledField } from "../ui/form/LabeledField";
2223
import { fieldMandatory } from "../ui/form/utils";
2324
import SlimSelect from "../ui/SlimSelect";
2425

@@ -133,17 +134,15 @@ export function QuoteReportModal(): JSXElement {
133134
<br />
134135
<span class="text-error">Please add comments in English only.</span>
135136
<Separator />
136-
<div class="grid gap-1">
137-
<label class="text-sub">quote</label>
137+
<LabeledField label="quote">
138138
<div class="text-xl text-text" dir="auto">
139139
{quoteText()}
140140
</div>
141-
</div>
141+
</LabeledField>
142142
<form.Field
143143
name="reason"
144144
children={(field) => (
145-
<div class="grid gap-1">
146-
<label class="text-sub">reason</label>
145+
<LabeledField label="reason">
147146
<SlimSelect
148147
options={[
149148
{ value: "Grammatical error", text: "Grammatical error" },
@@ -166,15 +165,14 @@ export function QuoteReportModal(): JSXElement {
166165
}
167166
settings={{ showSearch: false }}
168167
/>
169-
</div>
168+
</LabeledField>
170169
)}
171170
/>
172171
<form.Field
173172
name="comment"
174173
validators={{ onChange: fieldMandatory<string>() }}
175174
children={(field) => (
176-
<div class="grid gap-1">
177-
<label class="text-sub">comment</label>
175+
<LabeledField label="comment">
178176
<div class="relative">
179177
<textarea
180178
class="bg-bg-secondary min-h-50 w-full rounded p-2 text-text"
@@ -189,7 +187,7 @@ export function QuoteReportModal(): JSXElement {
189187
{250 - field().state.value.length}
190188
</div>
191189
</div>
192-
</div>
190+
</LabeledField>
193191
)}
194192
/>
195193
<div ref={captchaRef}></div>

frontend/src/ts/components/modals/QuoteSubmitModal.tsx

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { removeLanguageSize } from "../../utils/strings";
1818
import { AnimatedModal } from "../common/AnimatedModal";
1919
import { Button } from "../common/Button";
2020
import { InputField } from "../ui/form/InputField";
21+
import { LabeledField } from "../ui/form/LabeledField";
2122
import { fieldMandatory } from "../ui/form/utils";
2223
import SlimSelect from "../ui/SlimSelect";
2324

@@ -123,8 +124,7 @@ export function QuoteSubmitModal(): JSXElement {
123124
name="text"
124125
validators={{ onChange: fieldMandatory<string>() }}
125126
children={(field) => (
126-
<div class="grid gap-1">
127-
<label class="text-xs text-sub">quote</label>
127+
<LabeledField label="quote">
128128
<div class="relative">
129129
<textarea
130130
class="bg-bg-secondary w-full rounded p-2 text-text"
@@ -140,35 +140,33 @@ export function QuoteSubmitModal(): JSXElement {
140140
{250 - field().state.value.length}
141141
</div>
142142
</div>
143-
</div>
143+
</LabeledField>
144144
)}
145145
/>
146146
<form.Field
147147
name="source"
148148
validators={{ onChange: fieldMandatory<string>() }}
149149
children={(field) => (
150-
<div class="grid gap-1">
151-
<label class="text-xs text-sub">source</label>
150+
<LabeledField label="source">
152151
<InputField
153152
class="bg-bg-secondary w-full rounded p-2 text-text"
154153
type="text"
155154
field={field}
156155
autocomplete="off"
157156
/>
158-
</div>
157+
</LabeledField>
159158
)}
160159
/>
161160
<form.Field
162161
name="language"
163162
children={(field) => (
164-
<div class="grid gap-1">
165-
<label class="text-xs text-sub">language</label>
163+
<LabeledField label="language">
166164
<SlimSelect
167165
options={languageOptions}
168166
selected={field().state.value}
169167
onChange={(val) => field().handleChange(val ?? "")}
170168
/>
171-
</div>
169+
</LabeledField>
172170
)}
173171
/>
174172
<div ref={captchaRef}></div>

frontend/src/ts/components/modals/WordFilterModal.tsx

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { Button } from "../common/Button";
2020
import { Separator } from "../common/Separator";
2121
import { Checkbox } from "../ui/form/Checkbox";
2222
import { InputField } from "../ui/form/InputField";
23+
import { LabeledField } from "../ui/form/LabeledField";
2324
import { SubmitButton } from "../ui/form/SubmitButton";
2425
import SlimSelect from "../ui/SlimSelect";
2526

@@ -228,15 +229,14 @@ export function WordFilterModal(props: {
228229
}}
229230
>
230231
<fieldset disabled={loading()} class="grid gap-4">
231-
<div class="grid gap-1">
232-
<div class="text-sub">language</div>
232+
<LabeledField label="language">
233233
<SlimSelect
234234
options={languageOptions}
235235
selected={language()}
236236
onChange={setLanguage}
237237
disabled={loading()}
238238
/>
239-
</div>
239+
</LabeledField>
240240
<div class="text-xs text-sub">
241241
You can manually filter words by length, words or characters
242242
(separated by spaces) on the left side. On the right side you can
@@ -246,21 +246,18 @@ export function WordFilterModal(props: {
246246
<div class="grid grid-cols-1 gap-4 md:grid-cols-[1fr_auto_1fr]">
247247
<div class="grid gap-4 self-start">
248248
<div class="grid grid-cols-2 gap-4">
249-
<div class="grid gap-1">
250-
<div class="text-sub">min length</div>
249+
<LabeledField label="min length">
251250
<form.Field name="minLength">
252251
{(field) => <InputField field={field} type="number" />}
253252
</form.Field>
254-
</div>
255-
<div class="grid gap-1">
256-
<div class="text-sub">max length</div>
253+
</LabeledField>
254+
<LabeledField label="max length">
257255
<form.Field name="maxLength">
258256
{(field) => <InputField field={field} type="number" />}
259257
</form.Field>
260-
</div>
258+
</LabeledField>
261259
</div>
262-
<div class="grid gap-1">
263-
<div class="text-sub">include</div>
260+
<LabeledField label="include">
264261
<form.Field name="include">
265262
{(field) => <InputField field={field} />}
266263
</form.Field>
@@ -283,39 +280,36 @@ export function WordFilterModal(props: {
283280
/>
284281
)}
285282
</form.Field>
286-
</div>
287-
<div class="grid gap-1">
288-
<div class="text-sub">exclude</div>
283+
</LabeledField>
284+
<LabeledField label="exclude">
289285
<form.Field name="exclude">
290286
{(field) => (
291287
<InputField field={field} disabled={isExactMatch()} />
292288
)}
293289
</form.Field>
294-
</div>
290+
</LabeledField>
295291
</div>
296292

297293
<Separator vertical class="hidden md:block" />
298294
<Separator class="block md:hidden" />
299295

300296
<div class="grid gap-4 self-start">
301-
<div class="grid gap-1">
302-
<div class="text-sub">presets</div>
297+
<LabeledField label="presets">
303298
<SlimSelect
304299
options={presetOptions}
305300
selected={preset()}
306301
onChange={setPreset}
307302
disabled={loading()}
308303
/>
309-
</div>
310-
<div class="grid gap-1">
311-
<div class="text-sub">layout</div>
304+
</LabeledField>
305+
<LabeledField label="layout">
312306
<SlimSelect
313307
options={layoutOptions}
314308
selected={layout()}
315309
onChange={setLayout}
316310
disabled={loading()}
317311
/>
318-
</div>
312+
</LabeledField>
319313
<Button
320314
variant="button"
321315
text="apply"
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { JSXElement, Show } from "solid-js";
2+
3+
import { cn } from "../../../utils/cn";
4+
5+
export function LabeledField(props: {
6+
label: string;
7+
subLabel?: string;
8+
id?: string;
9+
children: JSXElement;
10+
class?: string;
11+
}): JSXElement {
12+
return (
13+
<div class={cn("grid gap-1", props.class)}>
14+
<label
15+
// oxlint-disable-next-line react/no-unknown-property
16+
for={props.id}
17+
class="text-sub lowercase"
18+
>
19+
{props.label}
20+
</label>
21+
<Show when={props.subLabel}>
22+
<div class="mb-1 text-em-xs text-sub opacity-50">{props.subLabel}</div>
23+
</Show>
24+
{props.children}
25+
</div>
26+
);
27+
}

0 commit comments

Comments
 (0)