refactor(react/settings): associate IDs for labels

This commit is contained in:
Elian Doran 2025-08-19 22:54:15 +03:00
parent 0841603be0
commit 51291a61e6
No known key found for this signature in database
8 changed files with 46 additions and 27 deletions

View File

@ -144,6 +144,10 @@ textarea,
pointer-events: none;
}
.form-group {
margin-bottom: 15px;
}
/* Add a gap between consecutive radios / check boxes */
label.tn-radio + label.tn-radio,
label.tn-checkbox + label.tn-checkbox {

View File

@ -5,7 +5,7 @@ import { ComponentChildren } from "preact";
import { CSSProperties, memo } from "preact/compat";
interface FormCheckboxProps {
name: string;
id?: string;
label: string | ComponentChildren;
/**
* If set, the checkbox label will be underlined and dotted, indicating a hint. When hovered, it will show the hint text.
@ -17,7 +17,7 @@ interface FormCheckboxProps {
containerStyle?: CSSProperties;
}
const FormCheckbox = memo(({ name, disabled, label, currentValue, onChange, hint, containerStyle }: FormCheckboxProps) => {
const FormCheckbox = memo(({ id, disabled, label, currentValue, onChange, hint, containerStyle }: FormCheckboxProps) => {
const labelRef = useRef<HTMLLabelElement>(null);
// Fix: Move useEffect outside conditional
@ -55,9 +55,10 @@ const FormCheckbox = memo(({ name, disabled, label, currentValue, onChange, hint
ref={labelRef}
>
<input
id={id}
className="form-check-input"
type="checkbox"
name={name}
name={id}
checked={currentValue || false}
value="1"
disabled={disabled}

View File

@ -1,25 +1,29 @@
import { ComponentChildren, RefObject } from "preact";
import { cloneElement, ComponentChildren, RefObject, VNode } from "preact";
import { CSSProperties } from "preact/compat";
import { useUniqueName } from "./hooks";
interface FormGroupProps {
name: string;
labelRef?: RefObject<HTMLLabelElement>;
label?: string;
title?: string;
className?: string;
children: ComponentChildren;
children: VNode<any>;
description?: string | ComponentChildren;
disabled?: boolean;
style?: CSSProperties;
}
export default function FormGroup({ label, title, className, children, description, labelRef, disabled }: FormGroupProps) {
export default function FormGroup({ name, label, title, className, children, description, labelRef, disabled, style }: FormGroupProps) {
const id = useUniqueName(name);
const childWithId = cloneElement(children, { id });
return (
<div className={`form-group ${className} ${disabled ? "disabled" : ""}`} title={title}
style={{ "margin-bottom": "15px" }}>
{ label
? <label style={{ width: "100%" }} ref={labelRef}>
{label && <div style={{ "margin-bottom": "10px" }}>{label}</div> }
{children}
</label>
: children}
<div className={`form-group ${className} ${disabled ? "disabled" : ""}`} title={title} style={style}>
{ label &&
<label style={{ width: "100%" }} ref={labelRef} htmlFor={id}>{label}</label>}
{childWithId}
{description && <small className="form-text">{description}</small>}
</div>

View File

@ -19,6 +19,7 @@ interface ValueConfig<T, Q> {
}
interface FormSelectProps<T, Q> extends ValueConfig<T, Q> {
id?: string;
onChange: OnChangeListener;
style?: CSSProperties;
}
@ -26,9 +27,9 @@ interface FormSelectProps<T, Q> extends ValueConfig<T, Q> {
/**
* Combobox component that takes in any object array as data. Each item of the array is rendered as an item, and the key and values are obtained by looking into the object by a specified key.
*/
export default function FormSelect<T>({ onChange, style, ...restProps }: FormSelectProps<T, T>) {
export default function FormSelect<T>({ id, onChange, style, ...restProps }: FormSelectProps<T, T>) {
return (
<FormSelectBody onChange={onChange} style={style}>
<FormSelectBody id={id} onChange={onChange} style={style}>
<FormSelectGroup {...restProps} />
</FormSelectBody>
);
@ -37,9 +38,9 @@ export default function FormSelect<T>({ onChange, style, ...restProps }: FormSel
/**
* Similar to {@link FormSelect}, but the top-level elements are actually groups.
*/
export function FormSelectWithGroups<T>({ values, keyProperty, titleProperty, currentValue, onChange }: FormSelectProps<T, FormSelectGroup<T>>) {
export function FormSelectWithGroups<T>({ id, values, keyProperty, titleProperty, currentValue, onChange }: FormSelectProps<T, FormSelectGroup<T>>) {
return (
<FormSelectBody onChange={onChange}>
<FormSelectBody id={id} onChange={onChange}>
{values.map(({ title, items }) => {
return (
<optgroup label={title}>
@ -51,9 +52,10 @@ export function FormSelectWithGroups<T>({ values, keyProperty, titleProperty, cu
)
}
function FormSelectBody({ children, onChange, style }: { children: ComponentChildren, onChange: OnChangeListener, style?: CSSProperties }) {
function FormSelectBody({ id, children, onChange, style }: { id?: string, children: ComponentChildren, onChange: OnChangeListener, style?: CSSProperties }) {
return (
<select
id={id}
class="form-select"
onChange={e => onChange((e.target as HTMLInputElement).value)}
style={style}

View File

@ -1,11 +1,13 @@
interface FormTextAreaProps {
id?: string;
currentValue: string;
onBlur?(newValue: string): void;
rows: number;
}
export default function FormTextArea({ onBlur, rows, currentValue }: FormTextAreaProps) {
export default function FormTextArea({ id, onBlur, rows, currentValue }: FormTextAreaProps) {
return (
<textarea
id={id}
rows={rows}
onBlur={(e) => {
onBlur?.(e.currentTarget.value);

View File

@ -191,5 +191,5 @@ export function useTriliumOptions<T extends OptionNames>(...names: T[]) {
* @returns a name with the given prefix and a random alpanumeric string appended to it.
*/
export function useUniqueName(prefix: string) {
return useMemo(() => prefix + utils.randomString(10), [ prefix]);
return useMemo(() => prefix + "-" + utils.randomString(10), [ prefix]);
}

View File

@ -1,17 +1,22 @@
import { ComponentChildren } from "preact";
import { cloneElement, VNode } from "preact";
import "./OptionsRow.css";
import { useUniqueName } from "../../../react/hooks";
interface OptionsRowProps {
name: string;
label?: string;
children: ComponentChildren;
children: VNode;
centered?: boolean;
}
export default function OptionsRow({ label, children, centered }: OptionsRowProps) {
export default function OptionsRow({ name, label, children, centered }: OptionsRowProps) {
const id = useUniqueName(name);
const childWithId = cloneElement(children, { id });
return (
<div className={`option-row ${centered ? "centered" : ""}`}>
{label && <label>{label}</label>}
{children}
{label && <label for={id}>{label}</label>}
{childWithId}
</div>
);
}

View File

@ -49,8 +49,9 @@ function LocalizationOptions() {
)
}
function LocaleSelector({ locales, currentValue, onChange }: { locales: Locale[], currentValue: string, onChange: (newLocale: string) => void }) {
function LocaleSelector({ id, locales, currentValue, onChange }: { id?: string; locales: Locale[], currentValue: string, onChange: (newLocale: string) => void }) {
return <FormSelect
id={id}
values={locales}
keyProperty="id" titleProperty="name"
currentValue={currentValue} onChange={onChange}