diff --git a/src/main/java/com/example/workutils/system/ui/EmptyStatePane.java b/src/main/java/com/example/workutils/system/ui/EmptyStatePane.java index ec5375a..b2e6863 100644 --- a/src/main/java/com/example/workutils/system/ui/EmptyStatePane.java +++ b/src/main/java/com/example/workutils/system/ui/EmptyStatePane.java @@ -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); } diff --git a/src/main/java/com/example/workutils/system/ui/async/OperationStatusPane.java b/src/main/java/com/example/workutils/system/ui/async/OperationStatusPane.java index 60bbca5..20b2a50 100644 --- a/src/main/java/com/example/workutils/system/ui/async/OperationStatusPane.java +++ b/src/main/java/com/example/workutils/system/ui/async/OperationStatusPane.java @@ -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()); diff --git a/src/main/java/com/example/workutils/system/ui/history/ExportHistoryPane.java b/src/main/java/com/example/workutils/system/ui/history/ExportHistoryPane.java index 242e0c2..127c420 100644 --- a/src/main/java/com/example/workutils/system/ui/history/ExportHistoryPane.java +++ b/src/main/java/com/example/workutils/system/ui/history/ExportHistoryPane.java @@ -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; diff --git a/src/test/java/com/example/workutils/system/ui/SharedPaneAccessibilityTest.java b/src/test/java/com/example/workutils/system/ui/SharedPaneAccessibilityTest.java new file mode 100644 index 0000000..b253cee --- /dev/null +++ b/src/test/java/com/example/workutils/system/ui/SharedPaneAccessibilityTest.java @@ -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 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 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 task = new FutureTask<>(() -> { + action.run(); + return null; + }); + Platform.runLater(task); + task.get(); + } + + @FunctionalInterface + private interface ThrowingRunnable { + void run() throws Exception; + } +}