完善公共组件无障碍交互

This commit is contained in:
张家训
2026-07-31 19:50:08 +08:00
parent a9e9d1cc3e
commit aa2455b3ef
4 changed files with 129 additions and 1 deletions
@@ -25,6 +25,10 @@ public final class EmptyStatePane extends VBox {
Objects.requireNonNull(content, "content");
setAlignment(Pos.CENTER);
getStyleClass().add("empty-state");
setAccessibleText(content.description().isEmpty()
? content.title()
: content.title() + "。" + content.description());
setFocusTraversable(false);
Label title = new Label(content.title());
title.setFont(Theme.heading(fontSize));
@@ -43,6 +47,8 @@ public final class EmptyStatePane extends VBox {
}
Button button = StyledButton.secondary(
content.actionLabel(), Theme.base(fontSize - 1));
button.setAccessibleText(content.actionLabel());
button.setFocusTraversable(true);
button.setOnAction(event -> action.run());
getChildren().add(button);
}
@@ -27,15 +27,21 @@ public final class OperationStatusPane extends HBox {
getStyleClass().add("operation-status");
progressBar.setPrefWidth(240);
progressBar.setAccessibleText("任务进度");
messageLabel.setFont(Theme.base(fontSize));
messageLabel.setTextFill(Theme.TEXT_SECONDARY);
messageLabel.setAccessibleText("任务状态");
HBox.setHgrow(messageLabel, Priority.ALWAYS);
cancelButton = com.example.workutils.system.ui.StyledButton.danger(
"停止", Theme.base(fontSize));
cancelButton.setAccessibleText("停止当前任务");
cancelButton.setOnAction(event -> controller.cancel());
getChildren().addAll(progressBar, messageLabel, cancelButton);
getChildren().addAll(progressBar, messageLabel);
if (cancellable) {
getChildren().add(cancelButton);
}
controller.snapshotProperty().addListener(
(observable, oldValue, value) -> render(value));
render(controller.snapshot());
@@ -45,6 +51,10 @@ public final class OperationStatusPane extends HBox {
progressBar.setProgress(snapshot.progress());
messageLabel.setText(snapshot.message().isBlank() ? defaultMessage(snapshot.state())
: snapshot.message());
String stateDescription = "任务状态:" + messageLabel.getText();
setAccessibleText(stateDescription);
messageLabel.setAccessibleText(stateDescription);
progressBar.setAccessibleText("任务进度:" + stateDescription);
cancelButton.setVisible(cancellable
&& snapshot.state() == OperationState.RUNNING);
cancelButton.setManaged(cancelButton.isVisible());
@@ -13,6 +13,7 @@ import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.Tooltip;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
@@ -45,6 +46,17 @@ public final class ExportHistoryPane extends VBox {
Objects.requireNonNull(commands, "commands");
table.setItems(items);
table.setAccessibleText("导出历史记录");
table.setFocusTraversable(true);
table.setOnKeyPressed(event -> {
if (event.getCode() == KeyCode.ENTER) {
HistoryItem selected = table.getSelectionModel().getSelectedItem();
if (selected != null) {
commands.openFile(selected.filePath());
event.consume();
}
}
});
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
table.setPlaceholder(new EmptyStatePane(
new EmptyStatePane.Content(
@@ -85,6 +97,7 @@ public final class ExportHistoryPane extends VBox {
setText(empty ? null : value);
HistoryItem row = empty ? null : getTableRow().getItem();
setTooltip(row == null ? null : new Tooltip(row.filePath()));
setAccessibleText(row == null ? null : "生成文件:" + row.filePath());
}
});
return column;
@@ -106,6 +119,8 @@ public final class ExportHistoryPane extends VBox {
{
actions.setAlignment(Pos.CENTER);
open.setAccessibleText("打开生成文件");
folder.setAccessibleText("打开生成文件所在目录");
folder.setTooltip(new Tooltip("打开文件所在目录"));
MenuItem edit = new MenuItem("用系统编辑器打开");
MenuItem copy = new MenuItem("复制文件路径");
@@ -123,6 +138,7 @@ public final class ExportHistoryPane extends VBox {
super.updateItem(value, empty);
row = empty ? null : getTableRow().getItem();
setGraphic(row == null ? null : actions);
setAccessibleText(row == null ? null : "历史记录操作:" + row.filePath());
}
});
return column;
@@ -0,0 +1,96 @@
package com.example.workutils.system.ui;
import com.example.workutils.system.ui.async.AsyncOperationController;
import com.example.workutils.system.ui.async.OperationStatusPane;
import com.example.workutils.system.ui.history.ExportHistoryPane;
import com.example.workutils.system.ui.history.HistoryFileCommands;
import com.example.workutils.system.ui.history.HistoryItem;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.scene.control.Button;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.FutureTask;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
class SharedPaneAccessibilityTest {
@BeforeAll
static void startJavaFx() throws Exception {
CountDownLatch started = new CountDownLatch(1);
Platform.startup(started::countDown);
started.await();
}
@Test
void emptyStateExposesCombinedDescriptionWithoutTakingFocus() throws Exception {
runFx(() -> {
EmptyStatePane pane = new EmptyStatePane(
new EmptyStatePane.Content("暂无结果", "请先执行任务", ""),
null,
14);
assertEquals("暂无结果。请先执行任务", pane.getAccessibleText());
assertFalse(pane.isFocusTraversable());
});
}
@Test
void nonCancellableStatusHasNoHiddenButton() throws Exception {
runFx(() -> {
AsyncOperationController<Void> controller =
new AsyncOperationController<>(Runnable::run, Runnable::run);
OperationStatusPane pane = new OperationStatusPane(controller, 14, false);
long buttons = pane.getChildren().stream()
.filter(Button.class::isInstance)
.count();
assertEquals(0, buttons);
assertEquals("任务状态:就绪", pane.getAccessibleText());
});
}
@Test
void enterOpensSelectedHistoryResult() throws Exception {
runFx(() -> {
List<String> opened = new ArrayList<>();
HistoryFileCommands commands = new HistoryFileCommands(
opened::add, path -> { }, path -> { }, path -> { });
HistoryItem item = new HistoryItem(
"1", "全部", "D:/export/result.csv", "2", "2026-07-31 19:00:00");
ExportHistoryPane pane = new ExportHistoryPane(
"数量", FXCollections.observableArrayList(item), commands, 14);
pane.table().getSelectionModel().select(item);
pane.table().fireEvent(new KeyEvent(
KeyEvent.KEY_PRESSED, "", "", KeyCode.ENTER,
false, false, false, false));
assertEquals(List.of("D:/export/result.csv"), opened);
assertEquals("导出历史记录", pane.table().getAccessibleText());
});
}
private static void runFx(ThrowingRunnable action) throws Exception {
FutureTask<Void> task = new FutureTask<>(() -> {
action.run();
return null;
});
Platform.runLater(task);
task.get();
}
@FunctionalInterface
private interface ThrowingRunnable {
void run() throws Exception;
}
}