Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions frontend/javascripts/admin/account/account_profile_view.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { CheckOutlined, DownOutlined, EditOutlined } from "@ant-design/icons";
import ChangeEmailView from "admin/auth/change_email_view";
import { updateSelectedThemeOfUser } from "admin/rest_api";
import { Button, Col, Dropdown, Row } from "antd";
import { Button, Col, Dropdown, Row, Space } from "antd";
import { useWkSelector } from "libs/react_hooks";
import * as Utils from "libs/utils";
import { useState } from "react";
Expand Down Expand Up @@ -94,9 +94,12 @@ function AccountProfileView() {
{
title: "Theme",
content: (
<Dropdown.Button menu={{ items: themeItems }} trigger={["click"]} icon={<DownOutlined />}>
{themeItems.find((item) => item.key === selectedTheme)?.label}
</Dropdown.Button>
<Space.Compact>
<Button>{themeItems.find((item) => item.key === selectedTheme)?.label}</Button>
<Dropdown menu={{ items: themeItems }} trigger={["click"]}>
<Button icon={<DownOutlined />} />
</Dropdown>
</Space.Compact>
Comment on lines +97 to +102
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

❓ Verification inconclusive

Ant Design v6 migration Dropdown.Button replacement pattern recommended


Short answer: AntD v6 removes Dropdown.Button; the recommended replacement pattern is to compose a Dropdown + Button inside Space.Compact (i.e., Space.Compact + Dropdown + Button). [1][2][3]

Example pattern (conceptual):

  • Wrap the primary Button and the Dropdown trigger in Space.Compact.
  • Use Dropdown (menu prop) for the menu and a Button for the left action.

Sources:

  • Ant Design v6 tasks / deprecations (Dropdown.Button removed — use Space.Compact + Dropdown + Button). [1]
  • Release notes / changelog mentioning removal and replacement. [2]
  • Ant Design docs referencing Dropdown/Button usage and v6 migration notes. [3]

References:
[1] GitHub ant-design issue: Ant Design v6 tasks (remove Dropdown.Button).
[2] Release/changelog notes for v6 (rm Dropdown.Button / use Space.Compact).
[3] Ant Design Dropdown docs (usage examples and migration notes).


ARIA labels icon buttons accessibility best practices


Key best practices for accessible icon-only buttons

  • Always give the control an accessible name (visible text, aria-labelledby, or aria-label) so screen readers announce its purpose. If no visible label exists, use aria-label or aria-labelledby. (w3.org)
  • Prefer visible text when practical. Use aria-label only when the label must be hidden; prefer aria-labelledby when you can reference existing visible text. (developer.mozilla.org)
  • Mark purely decorative icons as aria-hidden="true" (and SVG focusable="false") so the icon doesn’t become the accessible name. Provide the name on the button element instead. (developer.mozilla.org)
  • Use concise, action-oriented labels (e.g., "Close", "Search", "Download") that describe the button’s function — not its visual appearance. Avoid empty aria-label values. (getwcag.com)
  • Don’t rely on title/tooltips alone for the accessible name; tooltips may not be exposed to AT and are not a substitute for an accessible name. (developer.mozilla.org)
  • For complex explanations, use aria-describedby (or aria-description) for supplemental info; keep the accessible name short and actionable. (developer.mozilla.org)
  • Ensure keyboard focus and a sufficient hit target (recommended ≥44×44 px for touch) and test with screen readers (NVDA/JAWS/VoiceOver) and automated checks to confirm the accessible name appears in the accessibility tree. (getwcag.com)
  • Enforce component-level rules in your design system (require a label prop for icon-button components) and include automated tests for non-empty accessible names. (getwcag.com)

If you’d like, I can review specific icon-button code examples and give exact fixes.

Citations:


Add accessible label to icon-only Button triggering theme dropdown.

The refactor correctly implements the Ant Design v6 migration pattern (Space.Compact + Dropdown + Button replaces the deprecated Dropdown.Button). However, the icon-only Button at line 100 lacks an accessible name, which prevents screen readers from announcing its purpose.

Apply this fix:

-          <Button icon={<DownOutlined />} />
+          <Button icon={<DownOutlined />} aria-label="Select theme" />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Space.Compact>
<Button>{themeItems.find((item) => item.key === selectedTheme)?.label}</Button>
<Dropdown menu={{ items: themeItems }} trigger={["click"]}>
<Button icon={<DownOutlined />} />
</Dropdown>
</Space.Compact>
<Space.Compact>
<Button>{themeItems.find((item) => item.key === selectedTheme)?.label}</Button>
<Dropdown menu={{ items: themeItems }} trigger={["click"]}>
<Button icon={<DownOutlined />} aria-label="Select theme" />
</Dropdown>
</Space.Compact>
🤖 Prompt for AI Agents
In frontend/javascripts/admin/account/account_profile_view.tsx around lines 97
to 102, the icon-only Button used to trigger the theme Dropdown lacks an
accessible name; add an accessible label by supplying a descriptive aria-label
(or aria-labelledby) prop (preferably using the existing i18n/localization
string for "Change theme" or similar) to the icon-only Button so screen readers
announce its purpose; ensure the label is concise and meaningful and retain the
icon and existing click/trigger behavior.

),
},
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ function AccountSecurityView() {
</FormItem>
<Alert
type="info"
message={messages["auth.reset_logout"]}
title={messages["auth.reset_logout"]}
showIcon
style={{
marginBottom: 24,
Expand Down
8 changes: 5 additions & 3 deletions frontend/javascripts/admin/account/helpers/settings_card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ export function SettingsCard({ title, content, tooltip, action, style }: Setting
{tooltip != null ? (
<Popover
content={tooltip}
overlayInnerStyle={{
maxWidth: 250,
wordWrap: "break-word",
styles={{
container: {
maxWidth: 250,
wordWrap: "break-word",
},
}}
>
<InfoCircleOutlined style={{ marginLeft: 8 }} />
Expand Down
2 changes: 1 addition & 1 deletion frontend/javascripts/admin/auth/authentication_modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export default function AuthenticationModal({
return (
<Modal title={step} onCancel={onCancel} open={isOpen} footer={null} maskClosable={false}>
<Alert
message={alertMessage}
title={alertMessage}
type="info"
showIcon
style={{
Expand Down
2 changes: 1 addition & 1 deletion frontend/javascripts/admin/auth/change_email_view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ function ChangeEmailView({ onCancel }: { onCancel: () => void }) {
</FormItem>
<Alert
type="info"
message="You will be logged out after successfully changing your email address."
title="You will be logged out after successfully changing your email address."
showIcon
style={{
marginBottom: 24,
Expand Down
2 changes: 1 addition & 1 deletion frontend/javascripts/admin/auth/login_form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ function LoginForm({ layout, onLoggedIn, hideFooter, style }: Props) {
const iframeWarning = getIsInIframe() ? (
<Alert
type="warning"
message={
title={
<span>
Authentication within an iFrame probably does not work due to third-party cookies being
forbidden in most browsers. Please
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export default function SelectImportType({
different ways to accomplish this:
<div style={{ margin: 12 }}>
<Radio.Group onChange={onChange} value={composeMode}>
<Space direction="vertical">
<Space orientation="vertical">
<Radio value={"WITHOUT_TRANSFORMS"}>Combine datasets without any transforms</Radio>
<Radio value={"WK_ANNOTATIONS"}>
Combine datasets by using skeleton annotations (NML)
Expand Down
5 changes: 2 additions & 3 deletions frontend/javascripts/admin/dataset/dataset_components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export function CardContainer({
marginLeft: "auto",
marginRight: "auto",
}}
bordered={false}
variant="borderless"
title={
<>
<h3 style={{ lineHeight: "10px", marginTop: subtitle != null ? "22px" : "12px" }}>
Expand Down Expand Up @@ -133,9 +133,8 @@ export function DatastoreFormItem({
initialValue={datastores.length ? datastores[0].url : null}
>
<Select
showSearch
showSearch={{ optionFilterProp: "label" }}
placeholder="Select a Datastore"
optionFilterProp="label"
disabled={disabled}
style={{
width: "100%",
Expand Down
13 changes: 8 additions & 5 deletions frontend/javascripts/admin/dataset/dataset_upload_view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ type UploadFormFieldTypes = {
};

export const dataPrivacyInfo = (
<Space direction="horizontal" size={4}>
<Space orientation="horizontal" size={4}>
Per default, imported data is private and only visible within your organization.
<a
style={{ color: "var(--ant-color-primary)" }}
Expand Down Expand Up @@ -690,7 +690,10 @@ class DatasetUploadView extends React.Component<PropsWithFormAndRouter, State> {
);
};

onFormValueChange = (changedValues: UploadFormFieldTypes) => {
onFormValueChange = (
changedValues: Partial<UploadFormFieldTypes>,
_allValues: UploadFormFieldTypes,
) => {
if (changedValues.datastoreUrl) {
this.setState({ datastoreUrl: changedValues.datastoreUrl });
this.updateUnfinishedUploads();
Expand Down Expand Up @@ -722,7 +725,7 @@ class DatasetUploadView extends React.Component<PropsWithFormAndRouter, State> {
{hasPricingPlanExceededStorage(this.props.organization) ? (
<Alert
type="error"
message={
title={
<>
Your organization has exceeded the available storage. Uploading new datasets is
disabled. Visit the{" "}
Expand All @@ -736,7 +739,7 @@ class DatasetUploadView extends React.Component<PropsWithFormAndRouter, State> {

{unfinishedAndNotSelectedUploads.length > 0 && (
<Alert
message={
title={
<>
Unfinished Dataset Uploads{" "}
<Tooltip
Expand Down Expand Up @@ -810,7 +813,7 @@ class DatasetUploadView extends React.Component<PropsWithFormAndRouter, State> {
>
{features().isWkorgInstance && (
<Alert
message={
title={
<>
We are happy to help!
<br />
Expand Down
10 changes: 3 additions & 7 deletions frontend/javascripts/admin/onboarding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import { Fragment, useCallback, useEffect, useMemo, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import Store from "viewer/store";

const { Step } = Steps;
const FormItem = Form.Item;

function StepHeader({
Expand Down Expand Up @@ -149,7 +148,7 @@ export function OptionCard({ icon, header, children, action, height }: OptionCar
}}
>
<Card
bordered={false}
variant="borderless"
styles={{
body: {
textAlign: "center",
Expand Down Expand Up @@ -685,11 +684,8 @@ function OnboardingView() {
style={{
height: 25,
}}
>
{availableSteps.map(({ title }) => (
<Step title={title} key={title} />
))}
</Steps>
items={availableSteps.map(({ title }) => ({ title, key: title }))}
/>
</Col>
</Row>
<div
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export function PlanExceededAlert({ organization }: { organization: APIOrganizat
<Alert
showIcon
type="error"
message={message}
title={message}
action={activeUser && isUserAllowedToRequestUpgrades(activeUser) ? actionButton : null}
style={{ marginBottom: 20 }}
/>
Expand Down Expand Up @@ -181,7 +181,7 @@ export function PlanAboutToExceedAlert({ organization }: { organization: APIOrga
<Alert
showIcon
type="warning"
message="Your WEBKNOSSOS plan is about to expire soon. Renew your plan now to avoid being downgraded, users being blocked, and losing access to features."
title="Your WEBKNOSSOS plan is about to expire soon. Renew your plan now to avoid being downgraded, users being blocked, and losing access to features."
action={activeUser && isUserAllowedToRequestUpgrades(activeUser) ? actionButton : null}
style={{ marginBottom: 20 }}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import dayjs from "dayjs";
import features from "features";
import { formatCurrency } from "libs/format_utils";

import type { GetRef } from "antd/lib";
import renderIndependently from "libs/render_independently";
import Toast from "libs/toast";
import messages from "messages";
Expand Down Expand Up @@ -76,7 +77,7 @@ export function upgradeUserQuota() {
}

function UpgradeUserQuotaModal({ destroy }: { destroy: () => void }) {
const userInputRef = useRef<HTMLInputElement | null>(null);
const userInputRef = useRef<GetRef<typeof InputNumber> | null>(null);

const handleUserUpgrade = async () => {
if (userInputRef.current) {
Expand Down Expand Up @@ -120,7 +121,7 @@ export function upgradeStorageQuota() {
renderIndependently((destroyCallback) => <UpgradeStorageQuotaModal destroy={destroyCallback} />);
}
function UpgradeStorageQuotaModal({ destroy }: { destroy: () => void }) {
const storageInputRef = useRef<HTMLInputElement | null>(null);
const storageInputRef = useRef<GetRef<typeof InputNumber> | null>(null);

const handleStorageUpgrade = async () => {
if (storageInputRef.current) {
Expand Down Expand Up @@ -315,7 +316,7 @@ export function orderWebknossosCredits() {
}

function OrderWebknossosCreditsModal({ destroy }: { destroy: () => void }) {
const userInputRef = useRef<HTMLInputElement | null>(null);
const userInputRef = useRef<GetRef<typeof InputNumber> | null>(null);
const defaultCostPerCreditInEuro = formatCurrency(features().costPerCreditInEuro, "€");
const defaultCostPerCreditInDollar = formatCurrency(features().costPerCreditInDollar, "$");
const [creditCostAsString, setCreditCostsAsString] = useState<string>(
Expand Down
6 changes: 2 additions & 4 deletions frontend/javascripts/admin/project/project_create_view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,8 @@ function ProjectCreateView() {
]}
>
<Select
showSearch
showSearch={{ optionFilterProp: "label" }}
placeholder="Select a Team"
optionFilterProp="label"
style={fullWidth}
disabled={isEditMode}
loading={isFetchingData}
Expand All @@ -127,9 +126,8 @@ function ProjectCreateView() {
]}
>
<Select
showSearch
placeholder="Select a User"
optionFilterProp="label"
showSearch={{ optionFilterProp: "label" }}
style={fullWidth}
disabled={isEditMode}
loading={isFetchingData}
Expand Down
3 changes: 1 addition & 2 deletions frontend/javascripts/admin/scripts/script_create_view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,8 @@ function ScriptCreateView() {
]}
>
<Select
showSearch
showSearch={{ optionFilterProp: "label" }}
placeholder="Select a User"
optionFilterProp="label"
style={{
width: "100%",
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ function ProjectAndAnnotationTypeDropdown({
placeholder="Filter type or projects"
style={style}
options={filterOptions}
optionFilterProp="label"
showSearch={{ optionFilterProp: "label" }}
value={selectedFilters}
popupMatchSelectWidth={400}
onDeselect={(removedKey: string) => onDeselect(removedKey)}
Expand Down
12 changes: 4 additions & 8 deletions frontend/javascripts/admin/task/task_create_form_view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -542,13 +542,12 @@ function TaskCreateFormView() {
]}
>
<Select
showSearch
placeholder={
specificationType === SpecificationEnum.BaseAnnotation
? "The dataset is inferred from the base annotation."
: "Select a Dataset"
}
optionFilterProp="label"
showSearch={{ optionFilterProp: "label" }}
style={fullWidth}
disabled={isEditingMode || specificationType === SpecificationEnum.BaseAnnotation}
loading={isFetchingData}
Expand Down Expand Up @@ -629,9 +628,8 @@ function TaskCreateFormView() {
]}
>
<Select
showSearch
placeholder="Select a Task Type"
optionFilterProp="label"
showSearch={{ optionFilterProp: "label" }}
style={fullWidth}
disabled={isEditingMode}
loading={isFetchingData}
Expand Down Expand Up @@ -719,9 +717,8 @@ function TaskCreateFormView() {
]}
>
<Select
showSearch
showSearch={{ optionFilterProp: "label" }}
placeholder="Select a Project"
optionFilterProp="label"
style={fullWidth}
disabled={isEditingMode}
loading={isFetchingData}
Expand All @@ -743,9 +740,8 @@ function TaskCreateFormView() {
<Col flex="auto">
<FormItem name="scriptId" label="Script" hasFeedback>
<Select
showSearch
showSearch={{ optionFilterProp: "label" }}
placeholder="Select a Script"
optionFilterProp="label"
style={fullWidth}
disabled={isEditingMode}
loading={isFetchingData}
Expand Down
2 changes: 1 addition & 1 deletion frontend/javascripts/admin/task/task_list_view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ function TaskListView({ initialFieldValues }: Props) {
/>
</div>
<Alert
message="Note, manual assignments will bypass the automated task distribution system and its checks for user experience, access rights and other eligibility criteria."
title="Note, manual assignments will bypass the automated task distribution system and its checks for user experience, access rights and other eligibility criteria."
type="info"
/>
</>
Expand Down
9 changes: 3 additions & 6 deletions frontend/javascripts/admin/task/task_search_form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,9 @@ function TaskSearchForm({ onChange, initialFieldValues, isLoading, onDownloadAll
<Col span={12}>
<FormItem name="taskTypeId" {...formItemLayout} label="Task Type">
<Select
showSearch
showSearch={{ optionFilterProp: "label" }}
allowClear
placeholder="Select a Task Type"
optionFilterProp="label"
style={{
width: "100%",
}}
Expand All @@ -169,9 +168,8 @@ function TaskSearchForm({ onChange, initialFieldValues, isLoading, onDownloadAll
<FormItem name="projectId" {...formItemLayout} label="Project">
<Select
allowClear
showSearch
showSearch={{ optionFilterProp: "label" }}
placeholder="Select a Project"
optionFilterProp="label"
style={{
width: "100%",
}}
Expand All @@ -187,9 +185,8 @@ function TaskSearchForm({ onChange, initialFieldValues, isLoading, onDownloadAll
<FormItem name="userId" {...formItemLayout} label="User">
<Select
allowClear
showSearch
showSearch={{ optionFilterProp: "label" }}
placeholder="Select a User"
optionFilterProp="label"
style={{
width: "100%",
}}
Expand Down
7 changes: 3 additions & 4 deletions frontend/javascripts/admin/tasktype/task_type_create_view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -223,9 +223,8 @@ function TaskTypeCreateView() {
>
<Select
allowClear
showSearch
showSearch={{ optionFilterProp: "label" }}
placeholder="Select a Team"
optionFilterProp="label"
style={{
width: "100%",
}}
Expand Down Expand Up @@ -310,13 +309,13 @@ function TaskTypeCreateView() {
<FormItem name={["settings", "preferredMode"]} label="Preferred Mode" hasFeedback>
<Select
allowClear
optionFilterProp="label"
showSearch={{ optionFilterProp: "label" }}
style={{
width: "100%",
}}
options={[
{
value: null,
value: "",
label: "Any",
},
{
Expand Down
Loading
Loading