完善公共组件无障碍交互
This commit is contained in:
@@ -25,6 +25,10 @@ public final class EmptyStatePane extends VBox {
|
|||||||
Objects.requireNonNull(content, "content");
|
Objects.requireNonNull(content, "content");
|
||||||
setAlignment(Pos.CENTER);
|
setAlignment(Pos.CENTER);
|
||||||
getStyleClass().add("empty-state");
|
getStyleClass().add("empty-state");
|
||||||
|
setAccessibleText(content.description().isEmpty()
|
||||||
|
? content.title()
|
||||||
|
: content.title() + "。" + content.description());
|
||||||
|
setFocusTraversable(false);
|
||||||
|
|
||||||
Label title = new Label(content.title());
|
Label title = new Label(content.title());
|
||||||
title.setFont(Theme.heading(fontSize));
|
title.setFont(Theme.heading(fontSize));
|
||||||
@@ -43,6 +47,8 @@ public final class EmptyStatePane extends VBox {
|
|||||||
}
|
}
|
||||||
Button button = StyledButton.secondary(
|
Button button = StyledButton.secondary(
|
||||||
content.actionLabel(), Theme.base(fontSize - 1));
|
content.actionLabel(), Theme.base(fontSize - 1));
|
||||||
|
button.setAccessibleText(content.actionLabel());
|
||||||
|
button.setFocusTraversable(true);
|
||||||
button.setOnAction(event -> action.run());
|
button.setOnAction(event -> action.run());
|
||||||
getChildren().add(button);
|
getChildren().add(button);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,15 +27,21 @@ public final class OperationStatusPane extends HBox {
|
|||||||
getStyleClass().add("operation-status");
|
getStyleClass().add("operation-status");
|
||||||
|
|
||||||
progressBar.setPrefWidth(240);
|
progressBar.setPrefWidth(240);
|
||||||
|
progressBar.setAccessibleText("任务进度");
|
||||||
messageLabel.setFont(Theme.base(fontSize));
|
messageLabel.setFont(Theme.base(fontSize));
|
||||||
messageLabel.setTextFill(Theme.TEXT_SECONDARY);
|
messageLabel.setTextFill(Theme.TEXT_SECONDARY);
|
||||||
|
messageLabel.setAccessibleText("任务状态");
|
||||||
HBox.setHgrow(messageLabel, Priority.ALWAYS);
|
HBox.setHgrow(messageLabel, Priority.ALWAYS);
|
||||||
|
|
||||||
cancelButton = com.example.workutils.system.ui.StyledButton.danger(
|
cancelButton = com.example.workutils.system.ui.StyledButton.danger(
|
||||||
"停止", Theme.base(fontSize));
|
"停止", Theme.base(fontSize));
|
||||||
|
cancelButton.setAccessibleText("停止当前任务");
|
||||||
cancelButton.setOnAction(event -> controller.cancel());
|
cancelButton.setOnAction(event -> controller.cancel());
|
||||||
|
|
||||||
getChildren().addAll(progressBar, messageLabel, cancelButton);
|
getChildren().addAll(progressBar, messageLabel);
|
||||||
|
if (cancellable) {
|
||||||
|
getChildren().add(cancelButton);
|
||||||
|
}
|
||||||
controller.snapshotProperty().addListener(
|
controller.snapshotProperty().addListener(
|
||||||
(observable, oldValue, value) -> render(value));
|
(observable, oldValue, value) -> render(value));
|
||||||
render(controller.snapshot());
|
render(controller.snapshot());
|
||||||
@@ -45,6 +51,10 @@ public final class OperationStatusPane extends HBox {
|
|||||||
progressBar.setProgress(snapshot.progress());
|
progressBar.setProgress(snapshot.progress());
|
||||||
messageLabel.setText(snapshot.message().isBlank() ? defaultMessage(snapshot.state())
|
messageLabel.setText(snapshot.message().isBlank() ? defaultMessage(snapshot.state())
|
||||||
: snapshot.message());
|
: snapshot.message());
|
||||||
|
String stateDescription = "任务状态:" + messageLabel.getText();
|
||||||
|
setAccessibleText(stateDescription);
|
||||||
|
messageLabel.setAccessibleText(stateDescription);
|
||||||
|
progressBar.setAccessibleText("任务进度:" + stateDescription);
|
||||||
cancelButton.setVisible(cancellable
|
cancelButton.setVisible(cancellable
|
||||||
&& snapshot.state() == OperationState.RUNNING);
|
&& snapshot.state() == OperationState.RUNNING);
|
||||||
cancelButton.setManaged(cancelButton.isVisible());
|
cancelButton.setManaged(cancelButton.isVisible());
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import javafx.scene.control.TableCell;
|
|||||||
import javafx.scene.control.TableColumn;
|
import javafx.scene.control.TableColumn;
|
||||||
import javafx.scene.control.TableView;
|
import javafx.scene.control.TableView;
|
||||||
import javafx.scene.control.Tooltip;
|
import javafx.scene.control.Tooltip;
|
||||||
|
import javafx.scene.input.KeyCode;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.Priority;
|
import javafx.scene.layout.Priority;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
@@ -45,6 +46,17 @@ public final class ExportHistoryPane extends VBox {
|
|||||||
Objects.requireNonNull(commands, "commands");
|
Objects.requireNonNull(commands, "commands");
|
||||||
|
|
||||||
table.setItems(items);
|
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.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
|
||||||
table.setPlaceholder(new EmptyStatePane(
|
table.setPlaceholder(new EmptyStatePane(
|
||||||
new EmptyStatePane.Content(
|
new EmptyStatePane.Content(
|
||||||
@@ -85,6 +97,7 @@ public final class ExportHistoryPane extends VBox {
|
|||||||
setText(empty ? null : value);
|
setText(empty ? null : value);
|
||||||
HistoryItem row = empty ? null : getTableRow().getItem();
|
HistoryItem row = empty ? null : getTableRow().getItem();
|
||||||
setTooltip(row == null ? null : new Tooltip(row.filePath()));
|
setTooltip(row == null ? null : new Tooltip(row.filePath()));
|
||||||
|
setAccessibleText(row == null ? null : "生成文件:" + row.filePath());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return column;
|
return column;
|
||||||
@@ -106,6 +119,8 @@ public final class ExportHistoryPane extends VBox {
|
|||||||
|
|
||||||
{
|
{
|
||||||
actions.setAlignment(Pos.CENTER);
|
actions.setAlignment(Pos.CENTER);
|
||||||
|
open.setAccessibleText("打开生成文件");
|
||||||
|
folder.setAccessibleText("打开生成文件所在目录");
|
||||||
folder.setTooltip(new Tooltip("打开文件所在目录"));
|
folder.setTooltip(new Tooltip("打开文件所在目录"));
|
||||||
MenuItem edit = new MenuItem("用系统编辑器打开");
|
MenuItem edit = new MenuItem("用系统编辑器打开");
|
||||||
MenuItem copy = new MenuItem("复制文件路径");
|
MenuItem copy = new MenuItem("复制文件路径");
|
||||||
@@ -123,6 +138,7 @@ public final class ExportHistoryPane extends VBox {
|
|||||||
super.updateItem(value, empty);
|
super.updateItem(value, empty);
|
||||||
row = empty ? null : getTableRow().getItem();
|
row = empty ? null : getTableRow().getItem();
|
||||||
setGraphic(row == null ? null : actions);
|
setGraphic(row == null ? null : actions);
|
||||||
|
setAccessibleText(row == null ? null : "历史记录操作:" + row.filePath());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return column;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user