[bugfix] Fixes to tablist, fileinput, checkbox (#4139)

Some fixes to various frontend things:

- Fix signup checkbox being height 0 on webkit - closes https://codeberg.org/superseriousbusiness/gotosocial/issues/4136
- Fix wonky file input on chrome and webkit - closes https://codeberg.org/superseriousbusiness/gotosocial/issues/4138
- Make tablist in interaction policies keyboard accessible with proper left/right + focus handling, see https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/tablist_role

Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4139
Co-authored-by: tobi <tobi.smethurst@protonmail.com>
Co-committed-by: tobi <tobi.smethurst@protonmail.com>
This commit is contained in:
tobi
2025-05-06 08:06:52 +00:00
committed by tobi
parent 73aa62581e
commit 4a6b357501
4 changed files with 85 additions and 45 deletions

View File

@ -493,9 +493,8 @@ section.with-form {
gap: 0.4rem; gap: 0.4rem;
& > input { & > input {
height: 100%; height: 1rem;
width: 5%; width: 1rem;
min-width: 1.2rem;
align-self: center; align-self: center;
} }
} }

View File

@ -122,10 +122,6 @@ export function FileInput({ label, field, ...props }: FileInputProps) {
const ref = useRef<HTMLInputElement>(null); const ref = useRef<HTMLInputElement>(null);
const { onChange, infoComponent } = field; const { onChange, infoComponent } = field;
const id = nanoid(); const id = nanoid();
const onClick = (e) => {
e.preventDefault();
ref.current?.click();
};
return ( return (
<div className="form-field file"> <div className="form-field file">
@ -133,11 +129,9 @@ export function FileInput({ label, field, ...props }: FileInputProps) {
className="label-wrapper" className="label-wrapper"
htmlFor={id} htmlFor={id}
tabIndex={0} tabIndex={0}
onClick={onClick}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter") { if (e.key === "Enter") {
e.preventDefault(); ref.current?.click();
onClick(e);
} }
}} }}
role="button" role="button"

View File

@ -71,9 +71,6 @@ export default function ImportExportForm({ form, submitParse, parseResult }: Imp
}, [exportResult]); }, [exportResult]);
const importFileRef = useRef<HTMLInputElement>(null); const importFileRef = useRef<HTMLInputElement>(null);
const importFileOnClick = () => {
importFileRef.current?.click();
};
return ( return (
<> <>
@ -109,11 +106,9 @@ export default function ImportExportForm({ form, submitParse, parseResult }: Imp
<label <label
className={`button with-icon${form.permType.value === undefined || form.permType.value.length === 0 ? " disabled" : ""}`} className={`button with-icon${form.permType.value === undefined || form.permType.value.length === 0 ? " disabled" : ""}`}
tabIndex={0} tabIndex={0}
onClick={importFileOnClick}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter") { if (e.key === "Enter") {
e.preventDefault(); importFileRef.current?.click();
importFileOnClick();
} }
}} }}
role="button" role="button"

View File

@ -17,7 +17,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import React, { useCallback, useMemo } from "react"; import React, { forwardRef, useCallback, useMemo, useRef } from "react";
import { import {
useDefaultInteractionPoliciesQuery, useDefaultInteractionPoliciesQuery,
useResetDefaultInteractionPoliciesMutation, useResetDefaultInteractionPoliciesMutation,
@ -191,57 +191,109 @@ function InteractionPoliciesForm({ defaultPolicies }: InteractionPoliciesFormPro
// A tablist of tab buttons, one for each visibility. // A tablist of tab buttons, one for each visibility.
function PolicyPanelsTablist({ selectedVis }: { selectedVis: TextFormInputHook}) { function PolicyPanelsTablist({ selectedVis }: { selectedVis: TextFormInputHook}) {
const publicRef = useRef<HTMLButtonElement>(null);
const unlistedRef = useRef<HTMLButtonElement>(null);
const privateRef = useRef<HTMLButtonElement>(null);
return ( return (
<div className="tab-buttons" role="tablist"> <div className="tab-buttons" role="tablist">
<Tab <Tab
thisVisibility="public"
label="Public" label="Public"
selectedVis={selectedVis} selectedVis={selectedVis}
prevVis="private"
thisVis="public"
nextVis="unlisted"
prevRef={privateRef}
thisRef={publicRef}
nextRef={unlistedRef}
/> />
<Tab <Tab
thisVisibility="unlisted"
label="Unlisted" label="Unlisted"
selectedVis={selectedVis} selectedVis={selectedVis}
prevVis="public"
thisVis="unlisted"
nextVis="private"
prevRef={publicRef}
thisRef={unlistedRef}
nextRef={privateRef}
/> />
<Tab <Tab
thisVisibility="private"
label="Followers-only" label="Followers-only"
selectedVis={selectedVis} selectedVis={selectedVis}
prevVis="unlisted"
thisVis="private"
nextVis="public"
prevRef={unlistedRef}
thisRef={privateRef}
nextRef={publicRef}
/> />
</div> </div>
); );
} }
interface TabProps { interface TabProps {
thisVisibility: string; label: string;
label: string, selectedVis: TextFormInputHook;
selectedVis: TextFormInputHook prevVis: string;
thisVis: string;
nextVis: string;
prevRef: React.RefObject<HTMLButtonElement>;
thisRef: React.RefObject<HTMLButtonElement>;
nextRef: React.RefObject<HTMLButtonElement>;
} }
// One tab in a tablist, corresponding to the given thisVisibility. // One tab in a tablist, corresponding to the given thisVisibility.
function Tab({ thisVisibility, label, selectedVis }: TabProps) { const Tab = forwardRef(
const selected = useMemo(() => { function Tab({
return selectedVis.value === thisVisibility; label,
}, [selectedVis, thisVisibility]); selectedVis,
prevVis,
thisVis,
nextVis,
prevRef,
thisRef,
nextRef,
}: TabProps) {
const selected = useMemo(() => {
return selectedVis.value === thisVis;
}, [selectedVis, thisVis]);
return ( return (
<button <button
id={`tab-${thisVisibility}`} id={`tab-${thisVis}`}
title={label} title={label}
role="tab" role="tab"
className={`tab-button ${selected && "active"}`} ref={thisRef}
onClick={(e) => { className={`tab-button ${selected && "active"}`}
e.preventDefault(); onClick={(e) => {
selectedVis.setter(thisVisibility); // Allow tab to be clicked.
}} e.preventDefault();
aria-selected={selected} selectedVis.setter(thisVis);
aria-controls={`panel-${thisVisibility}`} }}
tabIndex={selected ? 0 : -1} onKeyDown={(e) => {
> // Allow cycling through
{label} // tabs with arrow keys.
</button> if (e.key === "ArrowLeft") {
); // Select and set
} // focus on previous tab.
selectedVis.setter(prevVis);
prevRef.current?.focus();
} else if (e.key === "ArrowRight") {
// Select and set
// focus on next tab.
selectedVis.setter(nextVis);
nextRef.current?.focus();
}
}}
aria-selected={selected}
aria-controls={`panel-${thisVis}`}
tabIndex={selected ? 0 : -1}
>
{label}
</button>
);
}
);
interface PolicyPanelProps { interface PolicyPanelProps {
policyForm: PolicyForm; policyForm: PolicyForm;