Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions frontend/__tests__/components/ui/form/LabeledField.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { render, screen } from "@solidjs/testing-library";
import { describe, it, expect } from "vitest";

import { LabeledField } from "../../../../src/ts/components/ui/form/LabeledField";

describe("LabeledField", () => {
it("renders label text correctly", () => {
render(() => (
<LabeledField label="test label">
<input />
</LabeledField>
));

expect(screen.getByText("test label")).toBeInTheDocument();
});

it("renders children correctly", () => {
render(() => (
<LabeledField label="test">
<div data-testid="child">child content</div>
</LabeledField>
));

expect(screen.getByTestId("child")).toBeInTheDocument();
});

it("renders subtext when provided", () => {
render(() => (
<LabeledField label="test" subLabel="helper text">
<input />
</LabeledField>
));

expect(screen.getByText("helper text")).toBeInTheDocument();
});

it("links label to input when id is provided", () => {
const { container } = render(() => (
<LabeledField label="test" id="test-id">
<input id="test-id" />
</LabeledField>
));

const label = container.querySelector("label");
expect(label).toHaveAttribute("for", "test-id");
});

it("applies custom class to wrapper", () => {
const { container } = render(() => (
<LabeledField label="test" class="custom-wrapper-class">
<input />
</LabeledField>
));

expect(container.firstChild).toHaveClass("custom-wrapper-class");
});
});
28 changes: 12 additions & 16 deletions frontend/src/ts/components/modals/CustomGeneratorModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { AnimatedModal } from "../common/AnimatedModal";
import { Button } from "../common/Button";
import { Separator } from "../common/Separator";
import { InputField } from "../ui/form/InputField";
import { LabeledField } from "../ui/form/LabeledField";
import { SubmitButton } from "../ui/form/SubmitButton";
import { TextareaField } from "../ui/form/TextareaField";
import SlimSelect from "../ui/SlimSelect";
Expand Down Expand Up @@ -159,24 +160,22 @@ export function CustomGeneratorModal(props: {
}}
class="grid gap-4"
>
<div class="grid gap-1">
<div class="text-sub">presets</div>
<div class="grid gap-2">
<LabeledField label="presets">
<div class="grid grid-cols-[1fr_auto] gap-2">
<SlimSelect
options={presetOptions}
selected={selectedPreset()}
onChange={setSelectedPreset}
/>
<Button variant="button" text="apply" onClick={applyPreset} />
</div>
</div>
</LabeledField>
<Separator />
<div class="text-xs text-sub">
Enter characters or strings separated by spaces. Random combinations
will be generated using these inputs.
</div>
<div class="grid gap-1">
<div class="text-sub">character set</div>
<LabeledField label="character set">
<form.Field
name="characterSet"
validators={{
Expand All @@ -188,27 +187,24 @@ export function CustomGeneratorModal(props: {
<TextareaField field={field} class="min-h-25 p-2 text-text" />
)}
</form.Field>
</div>
</LabeledField>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="grid gap-1">
<div class="text-sub">min length</div>
<LabeledField label="min length">
<form.Field name="minLength">
{(field) => <InputField field={field} type="number" />}
</form.Field>
</div>
<div class="grid gap-1">
<div class="text-sub">max length</div>
</LabeledField>
<LabeledField label="max length">
<form.Field name="maxLength">
{(field) => <InputField field={field} type="number" />}
</form.Field>
</div>
</LabeledField>
</div>
<div class="grid gap-1">
<div class="text-sub">word count</div>
<LabeledField label="word count">
<form.Field name="wordCount">
{(field) => <InputField field={field} type="number" />}
</form.Field>
</div>
</LabeledField>
<div class="text-xs text-sub">
{
'"Set" replaces the current custom text with generated words, "Add" appends generated words to the current custom text.'
Expand Down
16 changes: 7 additions & 9 deletions frontend/src/ts/components/modals/QuoteReportModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { removeLanguageSize } from "../../utils/strings";
import { AnimatedModal } from "../common/AnimatedModal";
import { Button } from "../common/Button";
import { Separator } from "../common/Separator";
import { LabeledField } from "../ui/form/LabeledField";
import { fieldMandatory } from "../ui/form/utils";
import SlimSelect from "../ui/SlimSelect";

Expand Down Expand Up @@ -133,17 +134,15 @@ export function QuoteReportModal(): JSXElement {
<br />
<span class="text-error">Please add comments in English only.</span>
<Separator />
<div class="grid gap-1">
<label class="text-sub">quote</label>
<LabeledField label="quote">
<div class="text-xl text-text" dir="auto">
{quoteText()}
</div>
</div>
</LabeledField>
<form.Field
name="reason"
children={(field) => (
<div class="grid gap-1">
<label class="text-sub">reason</label>
<LabeledField label="reason">
<SlimSelect
options={[
{ value: "Grammatical error", text: "Grammatical error" },
Expand All @@ -166,15 +165,14 @@ export function QuoteReportModal(): JSXElement {
}
settings={{ showSearch: false }}
/>
</div>
</LabeledField>
)}
/>
<form.Field
name="comment"
validators={{ onChange: fieldMandatory<string>() }}
children={(field) => (
<div class="grid gap-1">
<label class="text-sub">comment</label>
<LabeledField label="comment">
<div class="relative">
<textarea
class="bg-bg-secondary min-h-50 w-full rounded p-2 text-text"
Expand All @@ -189,7 +187,7 @@ export function QuoteReportModal(): JSXElement {
{250 - field().state.value.length}
</div>
</div>
</div>
</LabeledField>
)}
/>
<div ref={captchaRef}></div>
Expand Down
16 changes: 7 additions & 9 deletions frontend/src/ts/components/modals/QuoteSubmitModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { removeLanguageSize } from "../../utils/strings";
import { AnimatedModal } from "../common/AnimatedModal";
import { Button } from "../common/Button";
import { InputField } from "../ui/form/InputField";
import { LabeledField } from "../ui/form/LabeledField";
import { fieldMandatory } from "../ui/form/utils";
import SlimSelect from "../ui/SlimSelect";

Expand Down Expand Up @@ -123,8 +124,7 @@ export function QuoteSubmitModal(): JSXElement {
name="text"
validators={{ onChange: fieldMandatory<string>() }}
children={(field) => (
<div class="grid gap-1">
<label class="text-xs text-sub">quote</label>
<LabeledField label="quote">
<div class="relative">
<textarea
class="bg-bg-secondary w-full rounded p-2 text-text"
Expand All @@ -140,35 +140,33 @@ export function QuoteSubmitModal(): JSXElement {
{250 - field().state.value.length}
</div>
</div>
</div>
</LabeledField>
)}
/>
<form.Field
name="source"
validators={{ onChange: fieldMandatory<string>() }}
children={(field) => (
<div class="grid gap-1">
<label class="text-xs text-sub">source</label>
<LabeledField label="source">
<InputField
class="bg-bg-secondary w-full rounded p-2 text-text"
type="text"
field={field}
autocomplete="off"
/>
</div>
</LabeledField>
)}
/>
<form.Field
name="language"
children={(field) => (
<div class="grid gap-1">
<label class="text-xs text-sub">language</label>
<LabeledField label="language">
<SlimSelect
options={languageOptions}
selected={field().state.value}
onChange={(val) => field().handleChange(val ?? "")}
/>
</div>
</LabeledField>
)}
/>
<div ref={captchaRef}></div>
Expand Down
36 changes: 15 additions & 21 deletions frontend/src/ts/components/modals/WordFilterModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { Button } from "../common/Button";
import { Separator } from "../common/Separator";
import { Checkbox } from "../ui/form/Checkbox";
import { InputField } from "../ui/form/InputField";
import { LabeledField } from "../ui/form/LabeledField";
import { SubmitButton } from "../ui/form/SubmitButton";
import SlimSelect from "../ui/SlimSelect";

Expand Down Expand Up @@ -228,15 +229,14 @@ export function WordFilterModal(props: {
}}
>
<fieldset disabled={loading()} class="grid gap-4">
<div class="grid gap-1">
<div class="text-sub">language</div>
<LabeledField label="language">
<SlimSelect
options={languageOptions}
selected={language()}
onChange={setLanguage}
disabled={loading()}
/>
</div>
</LabeledField>
<div class="text-xs text-sub">
You can manually filter words by length, words or characters
(separated by spaces) on the left side. On the right side you can
Expand All @@ -246,21 +246,18 @@ export function WordFilterModal(props: {
<div class="grid grid-cols-1 gap-4 md:grid-cols-[1fr_auto_1fr]">
<div class="grid gap-4 self-start">
<div class="grid grid-cols-2 gap-4">
<div class="grid gap-1">
<div class="text-sub">min length</div>
<LabeledField label="min length">
<form.Field name="minLength">
{(field) => <InputField field={field} type="number" />}
</form.Field>
</div>
<div class="grid gap-1">
<div class="text-sub">max length</div>
</LabeledField>
<LabeledField label="max length">
<form.Field name="maxLength">
{(field) => <InputField field={field} type="number" />}
</form.Field>
</div>
</LabeledField>
</div>
<div class="grid gap-1">
<div class="text-sub">include</div>
<LabeledField label="include">
<form.Field name="include">
{(field) => <InputField field={field} />}
</form.Field>
Expand All @@ -283,39 +280,36 @@ export function WordFilterModal(props: {
/>
)}
</form.Field>
</div>
<div class="grid gap-1">
<div class="text-sub">exclude</div>
</LabeledField>
<LabeledField label="exclude">
<form.Field name="exclude">
{(field) => (
<InputField field={field} disabled={isExactMatch()} />
)}
</form.Field>
</div>
</LabeledField>
</div>

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

<div class="grid gap-4 self-start">
<div class="grid gap-1">
<div class="text-sub">presets</div>
<LabeledField label="presets">
<SlimSelect
options={presetOptions}
selected={preset()}
onChange={setPreset}
disabled={loading()}
/>
</div>
<div class="grid gap-1">
<div class="text-sub">layout</div>
</LabeledField>
<LabeledField label="layout">
<SlimSelect
options={layoutOptions}
selected={layout()}
onChange={setLayout}
disabled={loading()}
/>
</div>
</LabeledField>
<Button
variant="button"
text="apply"
Expand Down
27 changes: 27 additions & 0 deletions frontend/src/ts/components/ui/form/LabeledField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { JSXElement, Show } from "solid-js";

import { cn } from "../../../utils/cn";

export function LabeledField(props: {
label: string;
subLabel?: string;
id?: string;
children: JSXElement;
class?: string;
}): JSXElement {
return (
<div class={cn("grid gap-1", props.class)}>
<label
// oxlint-disable-next-line react/no-unknown-property
for={props.id}
class="text-sub lowercase"
>
{props.label}
</label>
<Show when={props.subLabel}>
<div class="mb-1 text-em-xs text-sub opacity-50">{props.subLabel}</div>
</Show>
{props.children}
</div>
);
}
Loading