From 19cfa8d126a2ff54298150529e58e5e4f5495f09 Mon Sep 17 00:00:00 2001 From: tobi <31960611+tsmethurst@users.noreply.github.com> Date: Wed, 9 Apr 2025 14:14:20 +0200 Subject: [bugfix] Fix a couple accessibility issues with `:focus` elements (#3979) * [bugfix/frontend] Fix accessibility/focus issues in settings + web ui * fix little error * tweaks --- web/source/settings/components/form/inputs.tsx | 29 +++++++--- web/source/settings/components/status.tsx | 64 +++++++++++++++++----- .../settings/components/username-lozenge.tsx | 22 ++++---- web/source/settings/lib/form/file.tsx | 2 +- web/source/settings/style.css | 46 +++++++--------- .../admin/http-header-permissions/overview.tsx | 23 ++++---- .../moderation/domain-permissions/drafts/index.tsx | 23 ++++---- .../domain-permissions/excludes/index.tsx | 23 ++++---- .../views/moderation/domain-permissions/form.tsx | 16 +++++- .../domain-permissions/subscriptions/common.tsx | 23 ++++---- .../settings/views/moderation/reports/search.tsx | 23 ++++---- .../settings/views/user/applications/search.tsx | 23 ++++---- .../settings/views/user/export-import/export.tsx | 5 -- .../settings/views/user/interactions/search.tsx | 23 ++++---- web/source/settings/views/user/profile/profile.tsx | 4 +- 15 files changed, 213 insertions(+), 136 deletions(-) (limited to 'web/source/settings') diff --git a/web/source/settings/components/form/inputs.tsx b/web/source/settings/components/form/inputs.tsx index 498499db6..c26b88f6a 100644 --- a/web/source/settings/components/form/inputs.tsx +++ b/web/source/settings/components/form/inputs.tsx @@ -17,7 +17,7 @@ along with this program. If not, see . */ -import React from "react"; +import React, { useRef } from "react"; import type { ReactNode, @@ -119,23 +119,36 @@ export interface FileInputProps extends React.DetailedHTMLProps< } export function FileInput({ label, field, ...props }: FileInputProps) { - const { onChange, ref, infoComponent } = field; + const ref = useRef(null); + const { onChange, infoComponent } = field; const id = nanoid(); + const onClick = () => { + ref.current?.click(); + }; return ( - - {label} - - - Browse + e.key === "Enter" && onClick()} + role="button" + > + + {label} + + + Browse + : undefined} + ref={ref} {...props} /> {infoComponent} diff --git a/web/source/settings/components/status.tsx b/web/source/settings/components/status.tsx index 701a9f8b7..04c5933d5 100644 --- a/web/source/settings/components/status.tsx +++ b/web/source/settings/components/status.tsx @@ -17,7 +17,7 @@ along with this program. If not, see . */ -import React from "react"; +import React, { useRef } from "react"; import { useVerifyCredentialsQuery } from "../lib/query/login"; import { MediaAttachment, Status as StatusType } from "../lib/types/status"; import sanitize from "sanitize-html"; @@ -122,10 +122,26 @@ function StatusBody({ status }: { status: StatusType }) { content = sanitize(status.content); } + const detailsRef = useRef(null); + const detailsOnClick = () => { + detailsRef.current?.click(); + }; + + const summaryRef = useRef(null); + const summaryOnClick = () => { + summaryRef.current?.click(); + }; + return ( - - + + e.key === "Enter" && summaryOnClick()} > Toggle content visibility @@ -183,23 +201,41 @@ function StatusMedia({ status }: { status: StatusType }) { } function StatusMediaEntry({ media }: { media: MediaAttachment }) { + const detailsRef = useRef(null); + const detailsOnClick = () => { + detailsRef.current?.click(); + }; + + const summaryRef = useRef(null); + const summaryOnClick = () => { + summaryRef.current?.click(); + }; + return ( - - Show media - + + e.key === "Enter" && summaryOnClick()} + > + Show media + + e.key === "Enter" && summaryOnClick()} + > - { + // When clicking on an account, direct + // to the detail view for that account. + setLocation(linkTo, { + // Store the back location in history so + // the detail view can use it to return to + // this page (including query parameters). + state: { backLocation: backLocation } + }); + }; return ( { - // When clicking on an account, direct - // to the detail view for that account. - setLocation(linkTo, { - // Store the back location in history so - // the detail view can use it to return to - // this page (including query parameters). - state: { backLocation: backLocation } - }); - }} + onClick={onClick} + onKeyDown={e => e.key === "Enter" && onClick()} role="link" tabIndex={0} > diff --git a/web/source/settings/lib/form/file.tsx b/web/source/settings/lib/form/file.tsx index cf9407827..5ecca3084 100644 --- a/web/source/settings/lib/form/file.tsx +++ b/web/source/settings/lib/form/file.tsx @@ -84,7 +84,7 @@ export default function useFileInput( } const infoComponent = ( - + {info ? info : initialInfo diff --git a/web/source/settings/style.css b/web/source/settings/style.css index 2afb44c42..a87d4813c 100644 --- a/web/source/settings/style.css +++ b/web/source/settings/style.css @@ -567,40 +567,34 @@ form { } .form-field.file { - display: grid; - grid-template-columns: auto 1fr; - grid-template-rows: auto auto; - grid-template-areas: - "label-label label-label" - "label-button file-info" - ; - - .label-label { - grid-area: label-label; - } + display: flex; + position: relative; + overflow: hidden; - .label-button { - grid-area: label-button; + .label-wrapper { + width: fit-content; + display: flex; + flex-direction: column; + &:focus-visible { + outline: 0.15rem dashed $button-focus-border; + outline-offset: -0.15rem; + } } .form-info { - grid-area: file-info; + position: absolute; + font-weight: initial; + align-self: end; + margin-left: 4.25rem; + margin-bottom: 0.3rem; + .error { padding: 0.1rem; - line-height: 1.4rem; + line-height: 1.4rem; } } } -span.form-info { - flex: 1 1 auto; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - padding: 0.3rem 0; - font-weight: initial; -} - .checkbox-list { .header, .entry { display: grid; @@ -1337,6 +1331,9 @@ button.tab-button { .pseudolink { cursor: pointer; text-decoration: none; + &:focus-visible { + outline: 0.15rem dotted $button-focus-border; + } } .info-list { @@ -1738,7 +1735,6 @@ button.tab-button { .mutation-button { width: 100%; - overflow-x: hidden; button { font-size: 1rem; diff --git a/web/source/settings/views/admin/http-header-permissions/overview.tsx b/web/source/settings/views/admin/http-header-permissions/overview.tsx index b2d8b7372..0b708aa8c 100644 --- a/web/source/settings/views/admin/http-header-permissions/overview.tsx +++ b/web/source/settings/views/admin/http-header-permissions/overview.tsx @@ -65,20 +65,23 @@ export default function HeaderPermsOverview() { } = useGetHeaderAllowsQuery(NoArg, { skip: permType !== "allow" }); const itemToEntry = (perm: HeaderPermission) => { + const onClick = () => { + // When clicking on a header perm, + // go to the detail view for perm. + setLocation(`/${permType}s/${perm.id}`, { + // Store the back location in + // history so the detail view + // can use it to return here. + state: { backLocation: location } + }); + }; + return ( { - // When clicking on a header perm, - // go to the detail view for perm. - setLocation(`/${permType}s/${perm.id}`, { - // Store the back location in - // history so the detail view - // can use it to return here. - state: { backLocation: location } - }); - }} + onClick={onClick} + onKeyDown={e => e.key === "Enter" && onClick()} role="link" tabIndex={0} > diff --git a/web/source/settings/views/moderation/domain-permissions/drafts/index.tsx b/web/source/settings/views/moderation/domain-permissions/drafts/index.tsx index 19dbe0d88..36bc7ee01 100644 --- a/web/source/settings/views/moderation/domain-permissions/drafts/index.tsx +++ b/web/source/settings/views/moderation/domain-permissions/drafts/index.tsx @@ -211,21 +211,24 @@ function DraftListEntry({ permDraft, linkTo, backLocation }: DraftEntryProps) { const title = `${permTypeUpper} ${domain}`; + const onClick = () => { + // When clicking on a draft, direct + // to the detail view for that draft. + setLocation(linkTo, { + // Store the back location in history so + // the detail view can use it to return to + // this page (including query parameters). + state: { backLocation: backLocation } + }); + }; + return ( { - // When clicking on a draft, direct - // to the detail view for that draft. - setLocation(linkTo, { - // Store the back location in history so - // the detail view can use it to return to - // this page (including query parameters). - state: { backLocation: backLocation } - }); - }} + onClick={onClick} + onKeyDown={e => e.key === "Enter" && onClick()} role="link" tabIndex={0} > diff --git a/web/source/settings/views/moderation/domain-permissions/excludes/index.tsx b/web/source/settings/views/moderation/domain-permissions/excludes/index.tsx index 915d6f5cc..207f94d61 100644 --- a/web/source/settings/views/moderation/domain-permissions/excludes/index.tsx +++ b/web/source/settings/views/moderation/domain-permissions/excludes/index.tsx @@ -186,21 +186,24 @@ function ExcludeListEntry({ permExclude, linkTo, backLocation }: ExcludeEntryPro return ; } + const onClick = () => { + // When clicking on a exclude, direct + // to the detail view for that exclude. + setLocation(linkTo, { + // Store the back location in history so + // the detail view can use it to return to + // this page (including query parameters). + state: { backLocation: backLocation } + }); + }; + return ( { - // When clicking on a exclude, direct - // to the detail view for that exclude. - setLocation(linkTo, { - // Store the back location in history so - // the detail view can use it to return to - // this page (including query parameters). - state: { backLocation: backLocation } - }); - }} + onClick={onClick} + onKeyDown={e => e.key === "Enter" && onClick()} role="link" tabIndex={0} > diff --git a/web/source/settings/views/moderation/domain-permissions/form.tsx b/web/source/settings/views/moderation/domain-permissions/form.tsx index 204e9510c..cf1447cfd 100644 --- a/web/source/settings/views/moderation/domain-permissions/form.tsx +++ b/web/source/settings/views/moderation/domain-permissions/form.tsx @@ -17,7 +17,7 @@ along with this program. If not, see . */ -import React from "react"; +import React, { useRef } from "react"; import { useEffect } from "react"; import { useExportDomainListMutation } from "../../../lib/query/admin/domain-permissions/export"; @@ -70,6 +70,11 @@ export default function ImportExportForm({ form, submitParse, parseResult }: Imp /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [exportResult]); + const importFileRef = useRef(null); + const importFileOnClick = () => { + importFileRef.current?.click(); + }; + return ( <> Import / Export domain permissions @@ -101,7 +106,13 @@ export default function ImportExportForm({ form, submitParse, parseResult }: Imp showError={false} disabled={form.permType.value === undefined || form.permType.value.length === 0} /> - + e.key === "Enter" && importFileOnClick()} + role="button" + > Import file {/* grid filler */} diff --git a/web/source/settings/views/moderation/domain-permissions/subscriptions/common.tsx b/web/source/settings/views/moderation/domain-permissions/subscriptions/common.tsx index 8668caa4b..808d03dcf 100644 --- a/web/source/settings/views/moderation/domain-permissions/subscriptions/common.tsx +++ b/web/source/settings/views/moderation/domain-permissions/subscriptions/common.tsx @@ -109,21 +109,24 @@ export function SubscriptionListEntry({ permSub, linkTo, backLocation }: Subscri successfullyFetchedAtStr = new Date(successfullyFetchedAt).toDateString(); } + const onClick = () => { + // When clicking on a subscription, direct + // to the detail view for that subscription. + setLocation(linkTo, { + // Store the back location in history so + // the detail view can use it to return to + // this page (including query parameters). + state: { backLocation: backLocation } + }); + }; + return ( { - // When clicking on a subscription, direct - // to the detail view for that subscription. - setLocation(linkTo, { - // Store the back location in history so - // the detail view can use it to return to - // this page (including query parameters). - state: { backLocation: backLocation } - }); - }} + onClick={onClick} + onKeyDown={e => e.key === "Enter" && onClick()} role="link" tabIndex={0} > diff --git a/web/source/settings/views/moderation/reports/search.tsx b/web/source/settings/views/moderation/reports/search.tsx index 0ae3ec0e0..bae0a2004 100644 --- a/web/source/settings/views/moderation/reports/search.tsx +++ b/web/source/settings/views/moderation/reports/search.tsx @@ -184,21 +184,24 @@ function ReportListEntry({ report, linkTo, backLocation }: ReportEntryProps) { const created = new Date(report.created_at).toLocaleString(); const title = `${status}. @${target.account.acct} was reported by @${from.account.acct} on ${created}. Reason: "${comment}"`; + const onClick = () => { + // When clicking on a report, direct + // to the detail view for that report. + setLocation(linkTo, { + // Store the back location in history so + // the detail view can use it to return to + // this page (including query parameters). + state: { backLocation: backLocation } + }); + }; + return ( { - // When clicking on a report, direct - // to the detail view for that report. - setLocation(linkTo, { - // Store the back location in history so - // the detail view can use it to return to - // this page (including query parameters). - state: { backLocation: backLocation } - }); - }} + onClick={onClick} + onKeyDown={e => e.key === "Enter" && onClick()} role="link" tabIndex={0} > diff --git a/web/source/settings/views/user/applications/search.tsx b/web/source/settings/views/user/applications/search.tsx index 819d96391..afbb0d326 100644 --- a/web/source/settings/views/user/applications/search.tsx +++ b/web/source/settings/views/user/applications/search.tsx @@ -139,21 +139,24 @@ function ApplicationListEntry({ app, linkTo, backLocation }: ApplicationListEntr const created = useCreated(app); const redirectURIs = useRedirectURIs(app); + const onClick = () => { + // When clicking on an app, direct + // to the detail view for that app. + setLocation(linkTo, { + // Store the back location in history so + // the detail view can use it to return to + // this page (including query parameters). + state: { backLocation: backLocation } + }); + }; + return ( { - // When clicking on an app, direct - // to the detail view for that app. - setLocation(linkTo, { - // Store the back location in history so - // the detail view can use it to return to - // this page (including query parameters). - state: { backLocation: backLocation } - }); - }} + onClick={onClick} + onKeyDown={e => e.key === "Enter" && onClick()} role="link" tabIndex={0} > diff --git a/web/source/settings/views/user/export-import/export.tsx b/web/source/settings/views/user/export-import/export.tsx index be302fec8..b4cf7355b 100644 --- a/web/source/settings/views/user/export-import/export.tsx +++ b/web/source/settings/views/user/export-import/export.tsx @@ -102,7 +102,6 @@ export default function Export({ exportStats }: { exportStats: AccountExportStat Following {exportStats.following_count} account{ exportStats.following_count !== 1 && "s" } exportFollowing()} @@ -116,7 +115,6 @@ export default function Export({ exportStats }: { exportStats: AccountExportStat Followed by {exportStats.followers_count} account{ exportStats.followers_count !== 1 && "s" } exportFollowers()} @@ -130,7 +128,6 @@ export default function Export({ exportStats }: { exportStats: AccountExportStat Created {exportStats.lists_count} list{ exportStats.lists_count !== 1 && "s" } exportLists()} @@ -144,7 +141,6 @@ export default function Export({ exportStats }: { exportStats: AccountExportStat Blocking {exportStats.blocks_count} account{ exportStats.blocks_count !== 1 && "s" } exportBlocks()} @@ -158,7 +154,6 @@ export default function Export({ exportStats }: { exportStats: AccountExportStat Muting {exportStats.mutes_count} account{ exportStats.mutes_count !== 1 && "s" } exportMutes()} diff --git a/web/source/settings/views/user/interactions/search.tsx b/web/source/settings/views/user/interactions/search.tsx index b97899c51..5d2da7563 100644 --- a/web/source/settings/views/user/interactions/search.tsx +++ b/web/source/settings/views/user/interactions/search.tsx @@ -174,21 +174,24 @@ function ReqsListEntry({ req, linkTo, backLocation }: ReqsListEntryProps) { const ourContent = useContent(req.status); const theirContent = useContent(req.reply); + const onClick = () => { + // When clicking on a request, direct + // to the detail view for that request. + setLocation(linkTo, { + // Store the back location in history so + // the detail view can use it to return to + // this page (including query parameters). + state: { backLocation: backLocation } + }); + }; + return ( { - // When clicking on a request, direct - // to the detail view for that request. - setLocation(linkTo, { - // Store the back location in history so - // the detail view can use it to return to - // this page (including query parameters). - state: { backLocation: backLocation } - }); - }} + onClick={onClick} + onKeyDown={e => e.key === "Enter" && onClick()} role="link" tabIndex={0} > diff --git a/web/source/settings/views/user/profile/profile.tsx b/web/source/settings/views/user/profile/profile.tsx index 6f99a17db..36e307193 100644 --- a/web/source/settings/views/user/profile/profile.tsx +++ b/web/source/settings/views/user/profile/profile.tsx @@ -167,6 +167,7 @@ function ProfileForm({ data: profile }: ProfileFormProps) { { @@ -179,7 +180,7 @@ function ProfileForm({ data: profile }: ProfileFormProps) { }} /> - + Avatar { -- cgit v1.2.3