FormSwitch: make entire Switch clickable & add focus rings

This commit is contained in:
Vendicated 2025-10-11 18:54:01 +02:00
parent c1556f0949
commit b881b60ff7
No known key found for this signature in database
GPG key ID: D66986BAF75ECF18
6 changed files with 36 additions and 13 deletions

View file

@ -1,5 +1,6 @@
.vc-form-switch-wrapper {
margin-bottom: 20px;
cursor: pointer;
}
.vc-form-switch {

View file

@ -26,7 +26,7 @@ export interface FormSwitchProps {
export function FormSwitch({ onChange, title, value, description, disabled, className, hideBorder }: FormSwitchProps) {
return (
<div className="vc-form-switch-wrapper">
<label className="vc-form-switch-wrapper">
<div className={classes("vc-form-switch", className, disabled && "vc-form-switch-disabled")}>
<div className={"vc-form-switch-text"}>
<Span size="md" weight="medium">{title}</Span>
@ -36,7 +36,7 @@ export function FormSwitch({ onChange, title, value, description, disabled, clas
<Switch checked={value} onChange={onChange} disabled={disabled} />
</div>
{!hideBorder && <Divider className="vc-form-switch-border" />}
</div>
</label>
);
}

View file

@ -9,13 +9,13 @@
width: 44px;
.high-contrast-mode & {
border-color: var(--border-strong)
border-color: var(--border-strong);
}
}
.vc-switch-checked {
background: var(--brand-500);
border-color: var(--control-border-primary-default)
border-color: var(--control-border-primary-default);
}
.vc-switch-disabled {
@ -23,6 +23,11 @@
opacity: 0.3;
}
.vc-switch-focusVisible {
/* stylelint-disable-next-line custom-property-pattern */
box-shadow: 0 0 0 4px var(--__adaptive-focus-ring-color, var(--focus-primary, #00b0f4));
}
.vc-switch-slider {
display: block;
height: 20px;
@ -31,10 +36,9 @@
position: absolute;
width: 28px;
transition: 100ms transform ease-in-out;
overflow: visible
overflow: visible;
}
.vc-switch-input {
border-radius: 14px;
cursor: pointer;
@ -46,7 +50,8 @@
top: 0;
width: 100%;
&[disabled] {
pointer-events: none
&:disabled {
pointer-events: none;
cursor: not-allowed
}
}

View file

@ -20,6 +20,8 @@ import "./Switch.css";
import { classNameFactory } from "@api/Styles";
import { classes } from "@utils/misc";
import { useState } from "@webpack/common";
import type { FocusEvent } from "react";
const switchCls = classNameFactory("vc-switch-");
@ -33,9 +35,21 @@ export interface SwitchProps {
}
export function Switch({ checked, onChange, disabled }: SwitchProps) {
const [focusVisible, setFocusVisible] = useState(false);
// Due to how we wrap the invisible input, there is no good way to do this with css.
// We need it on the parent, not the input itself. For this, you can use either:
// - :focus-within ~ this shows also when clicking, not just on keyboard focus => SUCKS
// - :has(:focus-visible) ~ works but :has performs terribly inside Discord
// - JS event handlers ~ what we are using now
const handleFocusChange = (event: FocusEvent<HTMLInputElement>) => {
const target = event.currentTarget;
setFocusVisible(target.matches(":focus-visible"));
};
return (
<div>
<div className={classes(switchCls("container"), "default-colors", switchCls({ checked, disabled }))}>
<div className={classes(switchCls("container", { checked, disabled, focusVisible }))}>
<svg
className={switchCls("slider")}
viewBox="0 0 28 20"
@ -62,6 +76,8 @@ export function Switch({ checked, onChange, disabled }: SwitchProps) {
</svg>
</svg>
<input
onFocus={handleFocusChange}
onBlur={handleFocusChange}
disabled={disabled}
type="checkbox"
className={switchCls("input")}

View file

@ -40,7 +40,7 @@ export function BooleanSetting({ option, pluginSettings, definedSettings, id, on
}
return (
<SettingsSection name={id} description={option.description} error={error} inlineSetting>
<SettingsSection tag="label" name={id} description={option.description} error={error} inlineSetting>
<Switch checked={state} onChange={handleChange} />
</SettingsSection>
);

View file

@ -38,11 +38,12 @@ interface SettingsSectionProps extends PropsWithChildren {
description: string;
error?: string | null;
inlineSetting?: boolean;
tag?: "label" | "div";
}
export function SettingsSection({ name, description, error, inlineSetting, children }: SettingsSectionProps) {
export function SettingsSection({ tag: Tag = "div", name, description, error, inlineSetting, children }: SettingsSectionProps) {
return (
<div className={cl("section")}>
<Tag className={cl("section")}>
<div className={classes(cl("content"), inlineSetting && cl("inline"))}>
<div className={cl("label")}>
{name && <Text className={cl("title")} variant="text-md/medium">{wordsToTitle(wordsFromCamel(name))}</Text>}
@ -51,6 +52,6 @@ export function SettingsSection({ name, description, error, inlineSetting, child
{children}
</div>
{error && <Text className={cl("error")} variant="text-sm/normal">{error}</Text>}
</div>
</Tag>
);
}