fix(tabs): Fix tab semantics and a11y (#1382)

* fix: Fix tab semantics for Settings page

* fix: Use new tabpanel markup for admin settings

* fix: Remove unused currentTab behavior

* fix: Remove Bootstrap tab JS dependency

* fix: Add tabpanel role to rate limit tab panels

* fix: Fix style of tabs

---------

Co-authored-by: SleeplessOne1917 <abias1122@gmail.com>
Co-authored-by: Dessalines <dessalines@users.noreply.github.com>
This commit is contained in:
Jay Sitter 2023-06-21 08:27:27 -04:00 committed by GitHub
parent f19271eba9
commit 40eefb0c67
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 135 additions and 69 deletions

View file

@ -1,8 +1,9 @@
import classNames from "classnames";
import { Component, InfernoNode, linkEvent } from "inferno"; import { Component, InfernoNode, linkEvent } from "inferno";
interface TabItem { interface TabItem {
key: string; key: string;
getNode: () => InfernoNode; getNode: (isSelected: boolean) => InfernoNode;
label: string; label: string;
} }
@ -30,24 +31,33 @@ export default class Tabs extends Component<TabsProps, TabsState> {
render() { render() {
return ( return (
<div> <div>
<ul className="nav nav-tabs mb-2"> <ul className="nav nav-tabs mb-2" role="tablist">
{this.props.tabs.map(({ key, label }) => ( {this.props.tabs.map(({ key, label }) => (
<li key={key} className="nav-item"> <li key={key} className="nav-item">
<button <button
type="button" type="button"
className={`nav-link btn${ className={classNames("nav-link", {
this.state?.currentTab === key ? " active" : "" active: this.state?.currentTab === key,
}`} })}
onClick={linkEvent({ ctx: this, tab: key }, handleSwitchTab)} onClick={linkEvent({ ctx: this, tab: key }, handleSwitchTab)}
aria-controls={`${key}-tab-pane`}
{...(this.state?.currentTab === key && {
...{
"aria-current": "page",
"aria-selected": "true",
},
})}
> >
{label} {label}
</button> </button>
</li> </li>
))} ))}
</ul> </ul>
{this.props.tabs <div className="tab-content">
.find(tab => tab.key === this.state?.currentTab) {this.props.tabs.map(({ key, getNode }) => {
?.getNode()} return getNode(this.state?.currentTab === key);
})}
</div>
</div> </div>
); );
} }

View file

@ -1,3 +1,4 @@
import classNames from "classnames";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { import {
BannedPersonsResponse, BannedPersonsResponse,
@ -130,22 +131,30 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
{ {
key: "site", key: "site",
label: i18n.t("site"), label: i18n.t("site"),
getNode: () => ( getNode: isSelected => (
<div className="row"> <div
<div className="col-12 col-md-6"> className={classNames("tab-pane show", {
<SiteForm active: isSelected,
showLocal={showLocal(this.isoData)} })}
allowedInstances={federationData?.allowed} role="tabpanel"
blockedInstances={federationData?.blocked} id="site-tab-pane"
onSaveSite={this.handleEditSite} >
siteRes={this.state.siteRes} <div className="row">
themeList={this.state.themeList} <div className="col-12 col-md-6">
loading={this.state.loading} <SiteForm
/> showLocal={showLocal(this.isoData)}
</div> allowedInstances={federationData?.allowed}
<div className="col-12 col-md-6"> blockedInstances={federationData?.blocked}
{this.admins()} onSaveSite={this.handleEditSite}
{this.bannedUsers()} siteRes={this.state.siteRes}
themeList={this.state.themeList}
loading={this.state.loading}
/>
</div>
<div className="col-12 col-md-6">
{this.admins()}
{this.bannedUsers()}
</div>
</div> </div>
</div> </div>
), ),
@ -153,40 +162,64 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
{ {
key: "rate_limiting", key: "rate_limiting",
label: "Rate Limiting", label: "Rate Limiting",
getNode: () => ( getNode: isSelected => (
<RateLimitForm <div
rateLimits={ className={classNames("tab-pane", {
this.state.siteRes.site_view.local_site_rate_limit active: isSelected,
} })}
onSaveSite={this.handleEditSite} role="tabpanel"
loading={this.state.loading} id="rate_limiting-tab-pane"
/> >
), <RateLimitForm
}, rateLimits={
{ this.state.siteRes.site_view.local_site_rate_limit
key: "taglines", }
label: i18n.t("taglines"),
getNode: () => (
<div className="row">
<TaglineForm
taglines={this.state.siteRes.taglines}
onSaveSite={this.handleEditSite} onSaveSite={this.handleEditSite}
loading={this.state.loading} loading={this.state.loading}
/> />
</div> </div>
), ),
}, },
{
key: "taglines",
label: i18n.t("taglines"),
getNode: isSelected => (
<div
className={classNames("tab-pane", {
active: isSelected,
})}
role="tabpanel"
id="taglines-tab-pane"
>
<div className="row">
<TaglineForm
taglines={this.state.siteRes.taglines}
onSaveSite={this.handleEditSite}
loading={this.state.loading}
/>
</div>
</div>
),
},
{ {
key: "emojis", key: "emojis",
label: i18n.t("emojis"), label: i18n.t("emojis"),
getNode: () => ( getNode: isSelected => (
<div className="row"> <div
<EmojiForm className={classNames("tab-pane", {
onCreate={this.handleCreateEmoji} active: isSelected,
onDelete={this.handleDeleteEmoji} })}
onEdit={this.handleEditEmoji} role="tabpanel"
loading={this.state.emojiLoading} id="emojis-tab-pane"
/> >
<div className="row">
<EmojiForm
onCreate={this.handleCreateEmoji}
onDelete={this.handleDeleteEmoji}
onEdit={this.handleEditEmoji}
loading={this.state.emojiLoading}
/>
</div>
</div> </div>
), ),
}, },

View file

@ -1,3 +1,4 @@
import classNames from "classnames";
import { Component, FormEventHandler, linkEvent } from "inferno"; import { Component, FormEventHandler, linkEvent } from "inferno";
import { EditSite, LocalSiteRateLimit } from "lemmy-js-client"; import { EditSite, LocalSiteRateLimit } from "lemmy-js-client";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
@ -19,6 +20,7 @@ interface RateLimitsProps {
handleRateLimitPerSecond: FormEventHandler<HTMLInputElement>; handleRateLimitPerSecond: FormEventHandler<HTMLInputElement>;
rateLimitValue?: number; rateLimitValue?: number;
rateLimitPerSecondValue?: number; rateLimitPerSecondValue?: number;
className?: string;
} }
interface RateLimitFormProps { interface RateLimitFormProps {
@ -49,9 +51,10 @@ function RateLimits({
handleRateLimitPerSecond, handleRateLimitPerSecond,
rateLimitPerSecondValue, rateLimitPerSecondValue,
rateLimitValue, rateLimitValue,
className,
}: RateLimitsProps) { }: RateLimitsProps) {
return ( return (
<div className="mb-3 row"> <div role="tabpanel" className={classNames("mb-3 row", className)}>
<div className="col-md-6"> <div className="col-md-6">
<label htmlFor="rate-limit">{i18n.t("rate_limit")}</label> <label htmlFor="rate-limit">{i18n.t("rate_limit")}</label>
<input <input
@ -142,8 +145,11 @@ export default class RateLimitsForm extends Component<
tabs={rateLimitTypes.map(rateLimitType => ({ tabs={rateLimitTypes.map(rateLimitType => ({
key: rateLimitType, key: rateLimitType,
label: i18n.t(`rate_limit_${rateLimitType}`), label: i18n.t(`rate_limit_${rateLimitType}`),
getNode: () => ( getNode: isSelected => (
<RateLimits <RateLimits
className={classNames("tab-pane show", {
active: isSelected,
})}
handleRateLimit={linkEvent( handleRateLimit={linkEvent(
{ rateLimitType, ctx: this }, { rateLimitType, ctx: this },
handleRateLimitChange handleRateLimitChange

View file

@ -1,4 +1,5 @@
import { debounce } from "@utils/helpers"; import { debounce } from "@utils/helpers";
import classNames from "classnames";
import { NoOptionI18nKeys } from "i18next"; import { NoOptionI18nKeys } from "i18next";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { import {
@ -265,34 +266,50 @@ export class Settings extends Component<any, SettingsState> {
); );
} }
userSettings() { userSettings(isSelected) {
return ( return (
<div className="row"> <div
<div className="col-12 col-md-6"> className={classNames("tab-pane show", {
<div className="card border-secondary mb-3"> active: isSelected,
<div className="card-body">{this.saveUserSettingsHtmlForm()}</div> })}
role="tabpanel"
id="settings-tab-pane"
>
<div className="row">
<div className="col-12 col-md-6">
<div className="card border-secondary mb-3">
<div className="card-body">{this.saveUserSettingsHtmlForm()}</div>
</div>
</div> </div>
</div> <div className="col-12 col-md-6">
<div className="col-12 col-md-6"> <div className="card border-secondary mb-3">
<div className="card border-secondary mb-3"> <div className="card-body">{this.changePasswordHtmlForm()}</div>
<div className="card-body">{this.changePasswordHtmlForm()}</div> </div>
</div> </div>
</div> </div>
</div> </div>
); );
} }
blockCards() { blockCards(isSelected) {
return ( return (
<div className="row"> <div
<div className="col-12 col-md-6"> className={classNames("tab-pane", {
<div className="card border-secondary mb-3"> active: isSelected,
<div className="card-body">{this.blockUserCard()}</div> })}
role="tabpanel"
id="blocks-tab-pane"
>
<div className="row">
<div className="col-12 col-md-6">
<div className="card border-secondary mb-3">
<div className="card-body">{this.blockUserCard()}</div>
</div>
</div> </div>
</div> <div className="col-12 col-md-6">
<div className="col-12 col-md-6"> <div className="card border-secondary mb-3">
<div className="card border-secondary mb-3"> <div className="card-body">{this.blockCommunityCard()}</div>
<div className="card-body">{this.blockCommunityCard()}</div> </div>
</div> </div>
</div> </div>
</div> </div>