Skip to content

Conversation

@patclarknvidia
Copy link
Contributor

@patclarknvidia patclarknvidia commented Nov 21, 2025

Description

Many UI improvements and Accessibility compliance changes.

  • Link to users workflows from submitted by (also wf and task tables)
  • Fixes for focus control when closing modals/slideouts and returning to table entries
  • Collapsed header on small screens
  • Screen-reader fixes for live regions (persistent)
  • Support for zoom up to 200%
  • Reflow compliance allowing only tables to scroll hor & ver on small screens
  • HTML semantics with landmarks, regions, headings and lists
  • Fix responsive UI for modals
  • Add skip to content
  • Collapse menu on small screens
  • Workflow details page for 1 task view

Issue - None

Checklist

  • I am familiar with the Contributing Guidelines.
  • New or existing tests cover these changes.
  • [] The documentation is up to date with these changes.

Summary by CodeRabbit

Release Notes

  • New Features

    • Enhanced filtering interface with improved status filtering across Tasks, Workflows, and Datasets.
    • Added keyboard focus management and navigation improvements for better accessibility.
    • Improved header navigation with consistent component styling across application.
  • Accessibility Improvements

    • Enhanced screen reader support with ARIA labels and semantic HTML throughout.
    • Better focus restoration and keyboard navigation in modals and panels.
  • UI/UX Enhancements

    • Unified spacing and visual consistency across components.
    • Improved responsive design and layout behavior.
    • Added pinning capability to filter and detail panels.

✏️ Tip: You can customize this high-level summary in your review settings.

@patclarknvidia patclarknvidia requested a review from a team November 21, 2025 21:18
@github-actions
Copy link

github-actions bot commented Dec 2, 2025

PR Preview Action v1.6.3

🚀 View preview at
https://NVIDIA.github.io/OSMO/pr-preview/pr-73/

Built to branch gh-pages/documentation at 2025-12-10 22:28 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@elookpotts-nvidia
Copy link
Contributor

@coderabbitai full review

@coderabbitai
Copy link

coderabbitai bot commented Dec 17, 2025

✅ Actions performed

Full review triggered.

@coderabbitai
Copy link

coderabbitai bot commented Dec 17, 2025

Walkthrough

Comprehensive UI refactoring restructures layout architecture, standardizes spacing tokens globally (gap-3/p-3 → gap-global/p-global), replaces header components (GenericHeader → PageHeader), removes Layout wrappers across routes, introduces status filtering via enums (StatusFilterType) replacing boolean flags, adds row identification to React Tables via getRowId, and enhances accessibility through ARIA attributes and semantic markup across the application.

Changes

Cohort / File(s) Summary
Spacing Token Standardization
ui/src/app/auth/success/page.tsx, ui/src/app/datasets/[bucket]/[name]/components/... (CollectionDetails, CollectionOverview, CollectionVersionsTable, DatasetDetails, DatasetOverview, DatasetVersionDetails, DatasetVersionsTable, DeleteCollection, Rename, ToolsModal), ui/src/app/datasets/[bucket]/[name]/page.tsx, ui/src/app/datasets/components/... (CreateCollection, DatasetPicker, DatasetsFilter, PoolsFilter), ui/src/app/pools/components/... (PoolPlatform, PoolsFilter, UsedFreeToggle), ui/src/app/profile/components/... (CredentialForm, DeleteCredential, ProfileEditor, ProfileSettings), ui/src/app/resources/components/... (AggregatePanels, PlatformDetails, ResourceDetails, ResourcesFilter), ui/src/app/workflows/components/... (CancelWorkflow, ExecTerminal, LogFilter, LogPopupHeader, PortForward, ShellPicker), ui/src/components/... (Accordion, Breadcrumbs, Checkbox, Container, DateRangePicker, InlineBanner, JSONEditor, Multiselect, PageError, Spinner)
Replaced hardcoded spacing utilities (gap-3, p-3, gap-2) with global spacing tokens (gap-global, p-global, gap-radios, gap-radios) and adjusted padding from mt-4 to mt-5 in select components for consistent design system application.
Header Component Refactoring: GenericHeader → PageHeader
ui/src/app/resources/[name]/page.tsx, ui/src/app/workflows/submit/[name]/page.tsx, ui/src/app/workflows/submit/groot/page.tsx, ui/src/app/workflows/submit/isaac_sim_sdg/page.tsx, ui/src/app/workflows/submit/mnist_training/page.tsx, ui/src/app/workflows/submit/page.tsx, ui/src/app/workflows/submit/reinforcement_learning/page.tsx
Replaced GenericHeader component with new PageHeader wrapper, consolidating header rendering logic and improving semantic markup with h2 titles inside PageHeader containers.
New PageHeader Architecture & Components
ui/src/components/PageHeader.tsx, ui/src/components/PageHeaderProvider.tsx, ui/src/components/IconButton.tsx, ui/src/components/FilterButton.tsx
Introduced new PageHeader context-based system (PageHeader, PageHeaderProvider, HeaderOutlet, TitleOutlet) and companion components (IconButton for icon+text buttons, FilterButton for filter UI controls) to centralize and standardize header rendering across routes.
Layout Wrapper Removal
ui/src/app/datasets/layout.tsx, ui/src/app/layout.tsx, ui/src/app/pools/layout.tsx, ui/src/app/profile/layout.tsx, ui/src/app/resources/layout.tsx, ui/src/app/tasks/layout.tsx, ui/src/app/taskssummary/layout.tsx, ui/src/app/tutorials/layout.tsx, ui/src/app/workflows/layout.tsx
Removed or restructured Layout wrapper imports and usage: datasets/pools/profile/resources/tasks/taskssummary/workflows layouts now return children directly; app layout now wraps children in Layout; tutorials layout added with new metadata.
Status Filtering System Refactoring
ui/src/app/tasks/components/StatusFilter.tsx, ui/src/app/tasks/components/TasksFilters.tsx, ui/src/app/tasks/components/TasksTable.tsx, ui/src/app/tasks/page.tsx, ui/src/app/taskssummary/page.tsx, ui/src/app/workflows/components/StatusFilter.tsx, ui/src/app/workflows/components/TasksFilter.tsx, ui/src/app/workflows/components/TasksTable.tsx, ui/src/app/workflows/components/WorkflowsFilters.tsx, ui/src/app/workflows/page.tsx, ui/src/app/workflows/hooks/useToolParamUpdater.ts
Replaced boolean allStatuses flag with StatusFilterType enum (ALL, CURRENT, COMPLETED, FAILED, CUSTOM); introduced getMapFromStatusArray/getTaskStatusArray/getWorkflowStatusArray utilities; added StatusFilter component for custom status selection with Map-based state management. Updated TasksFilters, WorkflowsFilters, and dependent components to use new status typing.
React Table Row Identification
ui/src/app/datasets/[bucket]/[name]/components/CollectionVersionsTable.tsx, ui/src/app/datasets/[bucket]/[name]/components/DatasetVersionsTable.tsx, ui/src/app/profile/components/CredentialsTable.tsx, ui/src/app/pools/components/PoolsTable.tsx, ui/src/app/resources/components/ResourcesTable.tsx, ui/src/app/tasks/components/TasksTable.tsx, ui/src/app/taskssummary/components/TasksTable.tsx, ui/src/app/workflows/components/TasksTable.tsx, ui/src/app/workflows/components/WorkflowsTable.tsx
Added getRowId configuration to useReactTable in all table components to provide stable row identifiers (e.g., getRowId: (row) => row.version, row.name, etc.); removed paddingOffset props from TableBase in select tables.
TableLoader Replacement with isLoading
ui/src/app/datasets/components/DatasetsTable.tsx, ui/src/app/pools/components/PoolsTable.tsx, ui/src/app/resources/components/ResourcesTable.tsx, ui/src/app/tasks/components/TasksTable.tsx, ui/src/app/taskssummary/components/TasksTable.tsx, ui/src/app/workflows/components/WorkflowsTable.tsx
Removed conditional TableLoader rendering and replaced with direct TableBase usage accepting isLoading prop; removed TableLoader imports and simplified surrounding wrapper divs.
Workflow & Task Navigation Enhancements
ui/src/app/workflows/[name]/page.tsx, ui/src/app/workflows/components/TaskDetails.tsx, ui/src/app/workflows/components/TaskTableRowAction.tsx, ui/src/app/workflows/components/WorkflowTableRowAction.tsx
Introduced task navigation in workflow view (SingleTask, List, Graph modes); added hasNavigation, onNext, onPrevious props to TaskDetails; removed auto-scroll-into-view behavior from TaskTableRowAction/WorkflowTableRowAction; added id prop to row action components; removed disableScrollIntoView prop.
Accessibility & Semantic Markup Enhancements
ui/src/app/auth/success/page.tsx, ui/src/app/datasets/[bucket]/[name]/components/..., ui/src/app/workflows/components/..., ui/src/components/... (CheckboxWithLabel, Checkbox, HomepageCards, HomepageHero, InlineBanner, PageError, RecordBuilder)
Added ARIA labels, roles (alert, list, listitem, status), aria-labelledby associations, and semantic section elements; introduced id attributes on headers and form controls; updated role and aria-expanded attributes on interactive elements for better assistive technology support.
Workflow User Linking
ui/src/app/workflows/components/WorkflowDetails.tsx, ui/src/app/workflows/components/WorkflowsTable.tsx, ui/src/app/tasks/components/TasksTable.tsx
Exported new linkToUserWorkflows(username: string) helper function and introduced getActionId helpers; updated User column rendering in tables to wrap usernames in Links to user-specific workflows pages using FilledIcon indicators.
InlineBanner Consolidation
ui/src/app/datasets/[bucket]/[name]/components/DeleteCollection.tsx, ui/src/app/datasets/[bucket]/[name]/components/Rename.tsx, ui/src/app/datasets/components/CreateCollection.tsx, ui/src/app/datasets/components/DatasetsFilter.tsx, ui/src/app/workflows/components/CancelWorkflow.tsx, ui/src/app/workflows/components/PortForward.tsx, ui/src/app/workflows/components/ShellPicker.tsx, ui/src/components/JSONEditor.tsx
Simplified InlineBanner rendering from multiple conditional branches to single always-rendered banner with dynamic status prop ("error", "success", "none"); unified error/success messaging logic.
Accessibility Button Props
ui/src/app/datasets/[bucket]/[name]/components/DeleteCollection.tsx, ui/src/app/datasets/[bucket]/[name]/components/Rename.tsx, ui/src/app/datasets/components/CreateCollection.tsx, ui/src/app/workflows/components/RecordBuilder.tsx
Changed disabled attribute to aria-disabled on buttons to improve accessibility; maintained functional behavior through ARIA attributes instead of DOM disabled state.
Collection & Dataset Details Layout
ui/src/app/datasets/[bucket]/[name]/components/CollectionDetails.tsx, ui/src/app/datasets/[bucket]/[name]/components/DatasetDetails.tsx, ui/src/app/datasets/[bucket]/[name]/components/DatasetVersionDetails.tsx
Replaced div-based layout with semantic section elements; added aria-labelledby attributes; introduced dedicated h2 headers with ids; added Labels sections with colored Tag rendering; reorganized action buttons into standardized layout with ToolType triggering.
Page Overview Header Consolidation
ui/src/app/datasets/[bucket]/[name]/components/CollectionOverview.tsx, ui/src/app/datasets/[bucket]/[name]/components/DatasetOverview.tsx, ui/src/app/pools/components/PoolPlatform.tsx
Replaced custom header markup with PageHeader wrapper; consolidated DatasetTag/Tag components into new header structure; removed useRef and header ref calculations; adjusted grid layout and removed manual padding calculations.
Datasets & Pools UI Refactoring
ui/src/app/datasets/components/Datasets.tsx, ui/src/app/datasets/[bucket]/[name]/page.tsx, ui/src/app/pools/page.tsx, ui/src/app/pools/components/PoolActions.tsx, ui/src/app/pools/components/PoolDetails.tsx
Introduced PageHeader with action buttons (IconButton, FilterButton); moved Create Collection modal to FullPageModal; replaced inline header refs with component-driven layout; reorganized grid containers; added semantic sections for platform/resource panels; reordered PoolActions to front.
Profile & Resources Pages
ui/src/app/profile/page.tsx, ui/src/app/resources/[name]/page.tsx, ui/src/app/resources/page.tsx
Replaced generic/header components with PageHeader; introduced aria-labelledby on sections; added responsive label visibility for action buttons; reorganized layout into flex/grid structures; added resource gauges and filter panels with SlideOut integration.
SlideOut Component Enhancement
ui/src/components/SlideOut.tsx
Introduced pinning capability via canPin/pinned/onPinChange props; added new public hooks useMinScreenWidth; extended API with animate, position, bodyClassName, headerClassName, canClose, returnFocusOnDeactivate, ariaLabel; improved focus management; removed containerRef and heightOffset deprecations.
Layout Component Refactoring
ui/src/components/Layout.tsx
Refactored to PageHeaderProvider-wrapped structure; introduced stateful main menu with localStorage persistence and pin support; replaced inline header with HeaderOutlet/TitleOutlet context-based rendering; added focus restoration and delayed menu reopening logic; integrated new SlideOut pinning behavior.
FullPageModal Updates
ui/src/components/FullPageModal.tsx
Extended props interface with React.HTMLAttributes; removed onHeightChange and onEscapeDeactivate props; relocated FocusTrap wrapping; adjusted padding from p-3 to p-global; simplified content overflow handling; added max-height/width constraints.
Component Removals
ui/src/components/NavbarProfileMenu.tsx
Deleted entire NavbarProfileMenu component; previously provided CLI instructions, version display, settings/sign-out options within a SlideOut menu.
Miscellaneous Component Updates
ui/src/.gitignore, ui/.prettierrc.json, ui/src/components/HomepageCards.tsx, ui/src/components/Select.tsx, ui/src/app/workflows/components/ToolsModal.tsx
Updated .gitignore with Windows ADS and diff patterns; renamed Prettier jsxBracketSameLine to bracketSameLine; added id prop to HomepageCard for ARIA linkage; added containerClassName default to Select; refactored ToolsModal to use memoized toolUrl/toolTitle.

Sequence Diagram(s)

sequenceDiagram
    participant User as User/UI
    participant PageHeader as PageHeader<br/>Component
    participant PageHeaderProvider as PageHeaderProvider<br/>Context
    participant HeaderOutlet as HeaderOutlet<br/>Renderer
    participant Route as Route Page

    User->>Route: Navigate to page
    Route->>PageHeaderProvider: Render with provider wrap
    Route->>PageHeader: Mount PageHeader
    PageHeader->>PageHeaderProvider: setHeaderNode(headerDOM)<br/>setTitle(title)
    PageHeaderProvider->>PageHeaderProvider: Update context state
    PageHeaderProvider->>HeaderOutlet: Trigger re-render
    HeaderOutlet->>HeaderOutlet: Render current headerNode from context
    User->>User: See updated header
    
    Note over PageHeader,HeaderOutlet: Header rendering is decoupled<br/>from route via context provider
Loading
sequenceDiagram
    participant User as User
    participant TaskFilter as TasksFilter<br/>Component
    participant StatusFilter as StatusFilter<br/>Component
    participant StateUpdate as URL/State<br/>Manager

    User->>TaskFilter: Open filter panel
    TaskFilter->>StatusFilter: Render with statusFilterType
    User->>StatusFilter: Select filter type (ALL/CURRENT/COMPLETED/FAILED/CUSTOM)
    
    alt statusFilterType is CUSTOM
        User->>StatusFilter: Toggle individual status checkboxes
        StatusFilter->>StatusFilter: Update statusMap<br/>(Map<TaskStatusType, boolean>)
        StatusFilter->>TaskFilter: Emit updated statusMap
    else statusFilterType is preset
        StatusFilter->>StatusFilter: Use predefined status array<br/>via getTaskStatusArray()
    end
    
    TaskFilter->>StateUpdate: Submit filters
    StateUpdate->>StateUpdate: Derive statuses from<br/>statusFilterType + statusMap
    StateUpdate->>User: Update URL with new filters
Loading
sequenceDiagram
    participant User as User
    participant Workflow as Workflow<br/>Page
    participant TaskNav as Task<br/>Navigation
    participant TaskDetails as TaskDetails<br/>Component
    participant FocusManager as Focus<br/>Manager

    User->>Workflow: View workflow with multiple tasks
    Workflow->>Workflow: Extract flatTasks array
    Workflow->>Workflow: Set view = SingleTask
    Workflow->>TaskDetails: Render with hasNavigation=true<br/>+ navigation callbacks

    User->>TaskDetails: Click Next button
    TaskDetails->>TaskNav: Call onNext()
    TaskNav->>Workflow: Update URL with next task
    Workflow->>Workflow: Increment selectedTaskIndex
    Workflow->>TaskDetails: Re-render with new task

    Note over Workflow,FocusManager: Focus restoration on<br/>closing task details

    User->>Workflow: Close task details
    Workflow->>FocusManager: Restore focus to<br/>captured action element<br/>via getActionId()
    FocusManager->>User: Return focus to action button
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–90 minutes

Areas requiring extra attention:

  • Status filtering refactor (ui/src/app/tasks/components/TasksFilters.tsx, ui/src/app/workflows/components/WorkflowsFilters.tsx, useToolParamUpdater.ts): Complete replacement of allStatuses boolean with StatusFilterType enum; verify StatusFilterType derivation logic and URL parameter mapping consistency across all entry points (Tasks, Workflows, TasksSummary).
  • PageHeader context architecture (ui/src/components/PageHeader.tsx, PageHeaderProvider.tsx, Layout.tsx): New context-based header rendering system; ensure HeaderOutlet/TitleOutlet are properly integrated in all route layouts and that focus/title management works across page transitions.
  • SlideOut pinning & persistence (ui/src/components/SlideOut.tsx, Layout.tsx): New localStorage-backed pinning; verify inert attribute management, focus restoration logic, and animation state transitions, especially interaction with menu open/close delays.
  • Table row identification (all *Table.tsx files): Added getRowId to 9+ table components; verify row keys are stable and unique per table (e.g., version, name, composite keys for tasks).
  • Navigation in workflow view (ui/src/app/workflows/[name]/page.tsx, TaskDetails.tsx, TaskTableRowAction.tsx): New view modes (SingleTask/List/Graph) with task navigation; validate boundary conditions (first/last task), focus management on navigation, and URL state consistency.
  • Remove Layout wrapper implications (layout.tsx across 8 routes, app/layout.tsx): Verify that new Layout placement in app/layout.tsx doesn't break child routes and that component tree nesting is correct after refactoring.
  • Accessibility attribute wiring (ARIA labels/roles): Added aria-labelledby, role attributes across many components; spot-check associations are correct and labels/ids match (e.g., HomepageCards, Checkbox, RecordBuilder).

🐰 Beneath the headers, new patterns grow,
StatusFilters dance in constants' glow,
Tables now bear names, SlideOuts can pin,
Spacing runs global—a design win!
With contexts that guide and focus restored,
The UI's refactored, beautifully poured. 🎨✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.56% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main objective of the PR: accessibility improvements for WCAG 2.1 compliance across the UI.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch patc/a11y

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (9)
ui/src/app/datasets/components/FileBrowser.tsx (1)

304-309: Move setChonkyDefaults outside the render path.

setChonkyDefaults is a global configuration that should be called once, not on every render. Calling it repeatedly can cause performance issues and unexpected behavior.

Move this configuration to module level or inside a useEffect with an empty dependency array:

+// Module level (outside component)
+setChonkyDefaults({
+  iconComponent: ChonkyIconFA as any,
+  disableDragAndDropProvider: true,
+});
+
 const FileBrowser: React.FC<DatasetFileBrowserProps> = ({ dataset, currentVersion, onOpenFile }) => {

Or inside the component:

+  useEffect(() => {
+    setChonkyDefaults({
+      iconComponent: ChonkyIconFA as any,
+      disableDragAndDropProvider: true,
+    });
+  }, []);
+
   if (!isSuccess) {
     return null;
   }
 
   return (
-    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
-    // @ts-ignore
     <ChonkyFileBrowser
ui/src/components/Multiselect.tsx (2)

73-102: Avoid role="listitem" on interactive elements like buttons.

Applying role="listitem" to <button> elements (line 80) overrides the button's native semantics and can confuse screen readers. Interactive elements should retain their semantic meaning.

Consider one of these approaches:

Option 1 (Recommended): Wrap the button in a proper <li> element:

-          <div
+          <ul
             className="flex flex-row flex-wrap gap-1 p-1"
-            role="list"
             aria-label="Selected options"
           >
             {selectedOptions.map((o) => (
-              <button
-                role="listitem"
+              <li key={o.value}>
+                <button
                 type="button"
                 className="tag-container"
-                key={o.value}
                 onClick={() => {
                   setFilter(new Map(filter.set(o.value, false)));
                   searchInputRef.current?.focus();
                   setLastActionText(`Removed ${o.label}`);
                   setTimeout(() => {
                     setLastActionText("");
                   }, 3000);
                 }}
               >
                 <Tag
                   color={Colors.pool}
                   size={TagSizes.xs}
                 >
                   {o.label}
                   <OutlinedIcon name="close" />
                 </Tag>
               </button>
+              </li>
             ))}
-          </div>
+          </ul>

Option 2: Use a native <ul> with <li> elements containing buttons (no role attributes needed).


119-148: Apply the same list/button structure fix to available options.

This section has the same semantic issue as the selected options: role="listitem" is applied to <button> elements (line 127), which overrides native button semantics.

Apply the same structural fix recommended for lines 73-102:

-          <div
+          <ul
             className="flex flex-row flex-wrap gap-1 p-1"
-            role="list"
             aria-label="Available options"
           >
             {filteredOptions.map((o) => (
-              <button
+              <li key={o.value}>
+                <button
                 type="button"
-                role="listitem"
                 className="tag-container"
-                key={o.value}
                 onClick={() => {
                   setFilter(new Map(filter.set(o.value, true)));
                   searchInputRef.current?.focus();
                   setLastActionText(`Added ${o.label}`);
                   setTimeout(() => {
                     setLastActionText("");
                   }, 3000);
                 }}
               >
                 <Tag
                   color={Colors.pool}
                   size={TagSizes.xs}
                 >
                   <OutlinedIcon name="add" />
                   {o.label}
                 </Tag>
               </button>
+              </li>
             ))}
-          </div>
+          </ul>

Note: The timeout cleanup issue mentioned in the previous comment also applies to lines 133-136.

ui/src/app/datasets/[bucket]/[name]/components/PagedFilePreviewer.tsx (1)

35-60: Add aria-label attributes to icon-only navigation buttons.

The "Previous" and "Next" navigation buttons contain only icons and lack aria-label attributes. While title attributes provide tooltips on hover, they aren't reliably announced by screen readers. This is a WCAG 2.4.4 and 4.1.2 compliance gap in a PR specifically targeting accessibility.

Apply this diff:

         <button
           className="btn btn-action"
           onClick={() => {
             updateIndex(-1);
           }}
           title="Previous file"
+          aria-label="Previous file"
         >
           <OutlinedIcon
             name="keyboard_double_arrow_left"
             className="text-lg!"
           />
         </button>
         <h2>{selectedFile.name}</h2>
         <button
           className="btn btn-action"
           onClick={() => {
             updateIndex(1);
           }}
           title="Next file"
+          aria-label="Next file"
         >
           <OutlinedIcon
             name="keyboard_double_arrow_right"
             className="text-lg!"
           />
         </button>
ui/src/components/RecordBuilder.tsx (3)

87-95: Critical: Use function parameters instead of cleared state variables.

Line 93 references newFieldKey and newFieldValue, but these were cleared on lines 91-92. This causes the screen reader announcement to be empty or contain stale values instead of the actual added field.

Apply this diff to use the correct variables:

   const addField = (fieldKey: string, fieldValue: unknown) => {
     if (fieldKey && fieldValue) {
       setFlattenedData({ ...flattenedData, [fieldKey]: fieldValue });
       delete deletedFields[fieldKey];
+      setLastActionText(`Added ${fieldKey}: ${fieldValue as string}`);
       setNewFieldKey("");
       setNewFieldValue("");
-      setLastActionText(`Added ${newFieldKey}: ${newFieldValue}`);
     }
   };

133-159: Invalid ARIA: role="listitem" requires parent with role="list".

Line 141 applies role="listitem" to buttons, but the parent container (line 135) has role="group". According to ARIA specification, listitem elements must be contained within an element with role="list".

Apply this diff to fix the ARIA structure:

           <div
             className="flex flex-wrap gap-1"
-            role="group"
+            role="list"
             aria-labelledby="current-labels"
           >

Note: The same issue exists in the "Deleted Labels" section at line 205 (though it doesn't currently have a role attribute, adding listitem roles there would also require the parent to be a list).


240-255: aria-disabled without disabled allows unwanted interaction.

Lines 243 and 251 use aria-disabled but not the disabled attribute on Reset and Save buttons. This means users can still focus and click these buttons even when isModified is false, though the click handlers have guards that will prevent any action. For proper accessibility and UX, these buttons should be fully disabled.

Apply this diff to properly disable both buttons:

           <button
             onClick={handleUndoChanges}
             className="btn btn-secondary"
-            aria-disabled={!isModified}
+            disabled={!isModified}
           >
             <OutlinedIcon name="undo" />
             Reset
           </button>
           <button
             onClick={handleSaveChanges}
             className="btn btn-primary"
-            aria-disabled={!isModified}
+            disabled={!isModified}
           >
ui/src/app/datasets/[bucket]/[name]/components/DatasetVersionDetails.tsx (1)

94-115: Incorrect use of ARIA list roles on interactive elements.

Using role="list" on a container with role="listitem" on <button> elements is semantically incorrect. Buttons are interactive controls, not list items. This can confuse assistive technologies.

Additionally, there's a typo in the aria-label: "Dataset VersionActions" should be "Dataset Version Actions".

Consider using a proper list structure or removing the list semantics:

      <div
        className={`dag-actions body-footer w-full`}
-        role="list"
-        aria-label="Dataset VersionActions"
+        aria-label="Dataset Version Actions"
      >
        <button
          className="btn btn-action whitespace-nowrap"
-          role="listitem"
          onClick={() => toolParamUpdater({ tool: ToolType.Tags })}
        >
          <OutlinedIcon name="data_object" />
          Edit Tags
        </button>
        <button
          className="btn btn-action whitespace-nowrap"
-          role="listitem"
          onClick={() => toolParamUpdater({ tool: ToolType.Metadata })}
        >
          <OutlinedIcon name="data_object" />
          Metadata
        </button>
      </div>
ui/src/app/taskssummary/page.tsx (1)

336-358: Fix aria-labelledby referencing non-existent ID.

The <dl aria-labelledby="total-resources-header"> references an ID that doesn't exist. The header="Total Resources" prop on SlideOut is a string, not an element with an ID.

Either add the ID to the header or remove the invalid reference:

        <SlideOut
          animate={true}
          id="total-resources"
          open={showTotalResources}
          onClose={() => setShowTotalResources(false)}
-         header="Total Resources"
+         header={<span id="total-resources-header">Total Resources</span>}
          className="mr-20 border-t-0"
        >

Or remove the aria-labelledby:

          <dl
            className="grid-cols-2"
-           aria-labelledby="total-resources-header"
          >
♻️ Duplicate comments (1)
ui/src/components/RecordBuilder.tsx (1)

205-228: Invalid ARIA: role="listitem" requires parent with role="list" (duplicate issue).

Line 209 has the same ARIA violation as line 141. The role="listitem" requires a parent with role="list", but the parent div (line 205) has no role attribute.

Apply this diff to fix the ARIA structure:

-          <div className="flex flex-wrap gap-1">
+          <div className="flex flex-wrap gap-1" role="list">
🟡 Minor comments (11)
ui/src/components/Spinner.tsx-33-33 (1)

33-33: Remove redundant aria-label attribute from status region.

The gap-global token is correctly defined in your Tailwind configuration and CSS variables, so no action needed there. However, the aria-label attribute on the status div should be removed as it's redundant. The aria-label attribute should be used to provide a text alternative to an element that has no visible text on the screen. Here, the element only renders when description is truthy (line 37 guard), so aria-label={description ?? "Loading..."} will always duplicate the visible text content. If the element has text content, you DON'T need to add aria-label, as it is redundant. The role="status" combined with visible text content is sufficient for screen readers.

Remove the aria-label attribute entirely.

ui/src/app/profile/components/ProfileSettings.tsx-31-31 (1)

31-31: Grid columns utility requires display: grid.

The grid-cols-2 utility defines the column template but won't take effect without grid (or inline-grid) on the same element.

Apply this diff to fix the grid layout:

-    <dl className="grid-cols-2">
+    <dl className="grid grid-cols-2">
ui/src/app/resources/[name]/page.tsx-47-54 (1)

47-54: Incorrect aria-label on span element.

The aria-label="List" on the span is problematic:

  1. The value "List" doesn't match the visible content "Task History"
  2. aria-label on a <span> containing visible text creates a disconnect for screen readers

Since the text is hidden on small screens (hidden lg:block), consider adding a visually-hidden accessible label instead:

         <Link
           href={getTaskHistoryUrl(params.name)}
           className="btn btn-secondary"
+          aria-label="Task History"
         >
           <FilledIcon name="history" />
-          <span
-            className="hidden lg:block"
-            aria-label="List"
-          >
+          <span className="hidden lg:block">
             Task History
           </span>
         </Link>

This ensures the link is always announced as "Task History" regardless of viewport size.

Committable suggestion skipped: line range outside the PR's diff.

ui/src/app/datasets/[bucket]/[name]/components/CollectionDetails.tsx-89-124 (1)

89-124: Incorrect ARIA role for actions container.

The actions container uses role="list" with buttons having role="listitem", but a collection of action buttons should use semantic markup (nav or just div) without list roles, or use an actual <ul> element.

Apply this diff:

       <div
-        className={`dag-actions body-footer w-full`}
-        role="list"
-        aria-label="Collection Actions"
+        className="dag-actions body-footer w-full"
       >
         <button
           className="btn btn-action whitespace-nowrap"
-          role="listitem"
           onClick={() => {
             toolParamUpdater({ tool: ToolType.Labels });
           }}
         >
           <OutlinedIcon name="snippet_folder" />
           Edit Labels
         </button>
         <button
           className="btn btn-action whitespace-nowrap"
-          role="listitem"
           onClick={() => {
             toolParamUpdater({ tool: ToolType.Delete });
           }}
         >
           <OutlinedIcon name="delete" />
           Delete Collection
         </button>
         <button
           className="btn btn-action whitespace-nowrap"
-          role="listitem"
           onClick={() => {
             toolParamUpdater({ tool: ToolType.Rename });
           }}
         >
           <OutlinedIcon name="snippet_folder" />
           Rename Collection
         </button>
       </div>
ui/src/app/pools/components/PoolDetails.tsx-131-141 (1)

131-141: Misuse of role="list" on grid container.

The grid container uses role="list" but wraps AggregatePanels which itself renders items with role="listitem". The grid should not have a list role; instead, AggregatePanels should handle the list semantics internally (which it already does based on the snippet).

Apply this diff:

               <div
-                className="grid lg:grid-cols-2 overflow-x-hidden"
-                role="list"
+                className="grid lg:grid-cols-2 overflow-x-hidden"
               >
                 <AggregatePanels
                   totals={pool.resource_usage}
                   isLoading={false}
                   isShowingUsed={isShowingUsed ?? true}
                 />
               </div>
ui/src/components/FilterButton.tsx-23-24 (1)

23-24: Props spread could override accessibility label.

Spreading {...props} after aria-label allows callers to accidentally override the dynamically computed accessibility label, which includes the active filter count.

Apply this diff to protect the aria-label:

+      {...props}
       className={`relative btn ${showFilters ? "btn-primary" : ""}`}
       onClick={() => {
         setShowFilters(!showFilters);
       }}
-      {...props}
       aria-label={`Filters${filterCount > 0 ? ` (${filterCount} active filters)` : ""}`}

Committable suggestion skipped: line range outside the PR's diff.

ui/src/app/datasets/components/Datasets.tsx-253-266 (1)

253-266: aria-controls and SlideOut id mismatch.

The FilterButton specifies aria-controls="datasets-filters", but the SlideOut has id="workflows-filters". This appears to be a copy-paste artifact. The IDs should match and reflect the current page context (datasets).

         <FilterButton
           showFilters={showFilters}
           setShowFilters={setShowFilters}
           filterCount={filterCount}
           aria-controls="datasets-filters"
         />
       </PageHeader>
       <div className="h-full w-full overflow-x-auto relative">
         <SlideOut
-          id="workflows-filters"
+          id="datasets-filters"
           open={showFilters}
           onClose={() => setShowFilters(false)}
           className="w-100 border-t-0"
           aria-label="Datasets Filter"
         >
ui/src/app/resources/page.tsx-166-175 (1)

166-175: aria-controls ID mismatch with SlideOut.

The FilterButton specifies aria-controls="resources-filters" (plural), but the SlideOut component has id="resources-filter" (singular). These must match for the ARIA relationship to work correctly.

         <FilterButton
           showFilters={showFilters}
           setShowFilters={setShowFilters}
           filterCount={filterCount}
-          aria-controls="resources-filters"
+          aria-controls="resources-filter"
         />
ui/src/app/datasets/[bucket]/[name]/components/ToolsModal.tsx-44-57 (1)

44-57: Misleading header id for non-Delete tools.

Both branches of the header memo use id="delete-collection-header", which is semantically incorrect when the tool is not Delete (e.g., Metadata, Tags, Labels, Rename). Consider using a generic or dynamic ID.

   const header = useMemo(() => {
     if (tool === ToolType.Delete) {
-      return <h2 id="delete-collection-header">Delete Collection</h2>;
+      return <h2 id="tools-modal-header">Delete Collection</h2>;
     }

     return (
       <h2
         className="capitalize"
-        id="delete-collection-header"
+        id="tools-modal-header"
       >
         {tool}
       </h2>
     );
   }, [tool]);

And update the aria-labelledby reference accordingly:

-      aria-labelledby="delete-collection-header"
+      aria-labelledby="tools-modal-header"

Committable suggestion skipped: line range outside the PR's diff.

ui/src/app/pools/page.tsx-83-92 (1)

83-92: Fix aria-controls and id mismatch.

The FilterButton's aria-controls="pools-filters" (plural) doesn't match the SlideOut's id="pools-filter" (singular). This breaks the ARIA relationship for screen readers.

       <FilterButton
         showFilters={showFilters}
         setShowFilters={setShowFilters}
         filterCount={filterCount}
-        aria-controls="pools-filters"
+        aria-controls="pools-filter"
       />
ui/src/app/workflows/[name]/page.tsx-144-154 (1)

144-154: Potential issue: selectedTaskIndex set to -1 when task not found.

When findIndex returns -1 (task not found), selectedTaskIndex is set to -1 rather than undefined. This could cause issues in nextTask/previousTask calculations since selectedTaskIndex !== undefined would be true even when the task isn't found.

Apply this diff to handle the not-found case explicitly:

   useEffect(() => {
     if (flatTasks && selectedTaskName && retryId !== undefined) {
       const taskIndex = flatTasks.findIndex((t) => t.name === selectedTaskName && t.retry_id === retryId);
 
-      setSelectedTask(taskIndex === undefined ? undefined : flatTasks[taskIndex]);
-      setSelectedTaskIndex(taskIndex);
+      if (taskIndex === -1) {
+        setSelectedTask(undefined);
+        setSelectedTaskIndex(undefined);
+      } else {
+        setSelectedTask(flatTasks[taskIndex]);
+        setSelectedTaskIndex(taskIndex);
+      }
     } else {
       setSelectedTask(undefined);
       setSelectedTaskIndex(undefined);
     }
   }, [flatTasks, selectedTaskName, retryId]);
🧹 Nitpick comments (32)
ui/src/app/workflows/components/CancelWorkflow.tsx (1)

92-103: Persistent live region pattern looks good, but consider spacing in error display.

The refactored InlineBanner with persistent aria-live="polite" region (even when empty) aligns with the PR's accessibility objectives for screen reader announcements. The simplified status prop is cleaner than the previous conditional rendering.

However, the error display div (line 96) appears to be missing spacing between the two paragraphs:

<div className="flex flex-col">
  <p>Error canceling workflow</p>
  <p>{error}</p>
</div>

Consider adding gap spacing for better readability:

-<div className="flex flex-col">
+<div className="flex flex-col gap-2">
  <p>Error canceling workflow</p>
  <p>{error}</p>
</div>
ui/src/components/Accordion.tsx (1)

84-96: Consider using stable keys instead of array indices.

Using array indices as React keys (line 86) can lead to issues if the items array is reordered or items are added/removed dynamically. If AccordionItemData items have or could have a unique identifier, consider adding an id field to the interface and using it as the key.

If stable identifiers are available, apply this pattern:

 interface AccordionItemData {
+  id?: string;
   title?: string | React.ReactNode;
   ariaLabel?: string;
   content: React.ReactNode;
   slotLeft?: React.ReactNode;
   className?: string;
   disabled?: boolean;
 }
       {items.map((item, index) => (
         <AccordionItem
-          key={index}
+          key={item.id ?? index}
           {...item}
           isOpen={openIndex === index}
ui/src/app/datasets/components/FileBrowser.tsx (2)

232-238: Non-null assertion before guard checks is confusing.

Line 232 uses filesData! before any guard checks (lines 254, 265, 300), which suggests the data is guaranteed to exist when it's not. While transformFilesData handles undefined via optional chaining at line 78, this pattern is confusing for maintainers.

Consider moving these transformations after the guard checks, or ensure consistent use of optional chaining/type narrowing.


311-313: Redundant guard check.

The isSuccess check is unnecessary because line 300 already ensures filesData exists. If filesData is defined, the query must have succeeded (isSuccess would be true).

Consider removing this redundant check:

-  if (!isSuccess) {
-    return null;
-  }
-
   return (
ui/src/app/datasets/components/DatasetPicker.tsx (1)

74-104: Consider adding form labeling for enhanced accessibility.

While the Select has a proper aria-label, the form itself lacks an accessible name. For full WCAG 2.1 compliance, consider wrapping the form content in a <fieldset> with a <legend> describing the form's purpose (e.g., "Select Dataset Tag"). This would provide better context for screen reader users.

Example enhancement:

 <form
   onSubmit={handleSubmit}
   className="p-global w-full"
 >
+  <fieldset className="border-0 p-0 m-0">
+    <legend className="sr-only">Select Dataset Tag</legend>
     <div className="grid grid-cols-[1fr_auto] gap-global w-full">
       <Select
         id="tag"
         aria-label="Tag"
         value={selectedTag}
         onChange={(e: React.ChangeEvent<HTMLSelectElement>) => setSelectedTag(e.target.value)}
         className="w-full"
       >
         {Array.from(tags).map((tag) => (
           <option
             key={tag}
             value={tag}
           >
             {tag}
           </option>
         ))}
       </Select>
       <button
         className="btn btn-primary"
         title="Save"
         type="submit"
       >
         Update
       </button>
     </div>
+  </fieldset>
 </form>

Note: The sr-only class provides visually hidden but screen-reader accessible text, and border/padding resets prevent default fieldset styling.

ui/src/components/Multiselect.tsx (1)

116-116: Consider aria-described-by for broader screen reader compatibility if supporting older or less common assistive technologies.

While Chrome, Edge, and Firefox currently appear to have good support for aria-description, screen reader support remains surprisingly limited, with only NVDA and iOS VoiceOver currently handling it well. For environments targeting only modern browsers and commonly-used screen readers, the current approach is acceptable. However, if broader compatibility is needed, consider using aria-describedby pointing to a visually-hidden live region as a more established pattern with consistent support across assistive technologies.

ui/src/app/datasets/[bucket]/[name]/components/PagedFilePreviewer.tsx (1)

62-63: Consider adding accessibility attributes to the file counter.

The file position counter could benefit from an aria-live="polite" attribute or a descriptive aria-label on its container to announce position changes to screen reader users when navigating between files.

Example:

-         <p>
+         <p aria-live="polite" aria-atomic="true">
            {selectedIndex + 1}/{files.length}
          </p>
ui/src/components/Breadcrumbs.tsx (1)

24-51: Enhance breadcrumbs accessibility to align with WCAG 2.1 and ARIA best practices.

Since this PR focuses on WCAG 2.1 compliance, consider improving the breadcrumbs component to follow the ARIA breadcrumb pattern:

  1. Wrap in a <nav> element with aria-label="Breadcrumb"
  2. Add aria-current="page" to the last breadcrumb item
  3. Mark the chevron icon as aria-hidden="true" (decorative separator)

Apply this diff to enhance accessibility:

 const Breadcrumbs = ({ segments }: { segments: ReactNode[] }) => {
   const count = segments.length;
   return (
-    <div className="flex flex-row gap-global items-center breadcrumbs">
+    <nav aria-label="Breadcrumb" className="flex flex-row gap-global items-center breadcrumbs">
       {segments.map((segment, index) =>
         index < count - 1 ? (
           <Link
             href={`/${segments.slice(0, index + 1).join("/")}`}
             key={index}
             className="flex flex-row gap-1 items-center capitalize"
           >
             {segment}
             <FilledIcon
               name="chevron_right"
               className="text-base opacity-50"
+              aria-hidden="true"
             />
           </Link>
         ) : (
           <div
             key={index}
             className="capitalize font-semibold"
+            aria-current="page"
           >
             {segment}
           </div>
         ),
       )}
-    </div>
+    </nav>
   );
 };
ui/src/components/Spinner.tsx (1)

37-45: Remove redundant nullish coalescing and consider removing duplicate aria-label.

Two minor refinements:

  1. Line 41: The nullish coalescing operator ?? "Loading..." is redundant because Line 37 already ensures description is truthy.
  2. Accessibility pattern: When using role="status" with meaningful text content, adding an aria-label with identical text is redundant—screen readers will announce the text content. The aria-label typically overrides the content, so having both with the same value adds unnecessary duplication.

Apply this diff to simplify the code:

       {description && (
         <div
           className="text-sm"
           role="status"
-          aria-label={description ?? "Loading..."}
         >
           {description}
         </div>
       )}

Note: If you prefer to keep explicit labeling, you could alternatively remove the nullish coalescing:

-          aria-label={description ?? "Loading..."}
+          aria-label={description}
ui/src/app/workflows/components/WorkflowActions.tsx (1)

52-52: Accessibility improvements look good.

The added id and aria-labelledby attributes properly associate the action lists with their container. However, both inner lists share the same label via aria-labelledby, which means screen readers will announce both as "Workflow Actions". Consider adding distinct labels (e.g., "Workflow information" for the first list and "Workflow operations" for the second) to help users distinguish between the two action groups.

Optional: Add distinct labels for each action list

      <div
        className="flex flex-col gap-1"
        role="list"
-        aria-labelledby="workflow-actions-list"
+        aria-label="Workflow information"
      >
      <div
        className="flex flex-col gap-1"
        role="list"
-        aria-labelledby="workflow-actions-list"
+        aria-label="Workflow operations"
      >

Also applies to: 57-57, 121-121

ui/src/app/datasets/components/DatasetsFilter.tsx (1)

232-238: Consider conditionally rendering the banner to avoid empty DOM elements.

The InlineBanner is always rendered, even when there are no errors. While this preserves the aria-live region for screen reader announcements (which is good for accessibility), it results in an empty container with nested divs when errors.length === 0.

If the persistent live region is intentional for accessibility, this is fine. Otherwise, consider conditional rendering:

-      <InlineBanner status={errors.length > 0 ? "error" : "none"}>
-        <div className="flex flex-col">
-          {errors.map((error, index) => (
-            <div key={index}>{error}</div>
-          ))}
-        </div>
-      </InlineBanner>
+      {errors.length > 0 && (
+        <InlineBanner status="error">
+          <div className="flex flex-col">
+            {errors.map((error, index) => (
+              <div key={index}>{error}</div>
+            ))}
+          </div>
+        </InlineBanner>
+      )}
ui/src/app/workflows/components/WorkflowTableRowAction.tsx (1)

31-31: Remove unused buttonRef.

The buttonRef is created but no longer used after removing the useEffect that handled scroll-into-view behavior.

-import { useRef } from "react";
+
 import { formatForWrapping } from "~/utils/string";
 export const WorkflowTableRowAction = ({ id, name, selected, updateUrl, extraParams }: WorkflowTableRowActionProps) => {
-  const buttonRef = useRef<HTMLButtonElement>(null);
-
   return (
     <button
       id={id}
       className={`btn ${selected ? "btn-primary" : "btn-secondary"} table-action`}
-      ref={buttonRef}
       onClick={(event) => {
ui/src/app/workflows/components/TaskActions.tsx (1)

39-84: Consider refining the ARIA labeling structure.

The current implementation has both inner lists using aria-labelledby="task-actions-list" to reference the parent container's id, while the parent also has aria-label="Task Actions". This creates some redundancy:

  • Parent div has both aria-label="Task Actions" (line 41) and id="task-actions-list" (line 42)
  • Both child role="list" elements reference this id via aria-labelledby (lines 47, 83)

Optional refinement: Consider one of these approaches:

  1. Keep the parent's aria-label and remove aria-labelledby from children (they inherit context)
  2. Create a visible heading with the id and remove the parent's aria-label
  3. Give each list its own descriptive aria-label (e.g., "Log Actions", "External Actions")

The current implementation is functional but could be clearer.

ui/src/app/workflows/submit/groot/page.tsx (1)

24-26: Consider using the title prop for consistency.

The PageHeader component accepts an optional title prop. Using it instead of passing an h2 as children would be more consistent with the component's API design.

Apply this diff for a cleaner approach:

-      <PageHeader>
-        <h2 className="grow">Groot</h2>
-      </PageHeader>
+      <PageHeader title="Groot" />

Note: If additional header controls are needed in the future, the children approach provides more flexibility.

ui/src/app/workflows/submit/isaac_sim_sdg/page.tsx (1)

24-26: Consider using the title prop for consistency.

Similar to other workflow submit pages, consider using the PageHeader's title prop instead of passing an h2 as children for a more consistent API usage.

Apply this diff:

-      <PageHeader>
-        <h2 className="grow">Isaac Sim SDG</h2>
-      </PageHeader>
+      <PageHeader title="Isaac Sim SDG" />
ui/src/app/workflows/submit/reinforcement_learning/page.tsx (1)

21-29: Pattern works correctly, but note the inconsistency with sibling pages.

Using PageHeader with an h2 child provides good semantic structure. However, this differs from ui/src/app/workflows/submit/page.tsx which uses the title prop instead. Consider standardizing on one approach across submit pages for consistency—either always use the title prop for simple headers or always use children with h2 elements.

ui/src/app/datasets/[bucket]/[name]/components/DatasetDetails.tsx (2)

33-45: Good semantic structure with section and heading.

The use of <section> with aria-labelledby and a proper heading hierarchy improves accessibility. However, the aria-labelledby on the <dl> element (line 45) referencing the same title is redundant since the <dl> is already within the labeled section.

       <dl
         className="p-global grow"
-        aria-labelledby="dataset-details-title"
       >

92-96: Reconsider role="list" for action buttons.

Using role="list" with role="listitem" on action buttons is semantically misleading. These are interactive actions, not list items. Consider using a more appropriate pattern:

       <div
         className={`dag-actions body-footer w-full`}
-        role="list"
-        aria-label="Dataset Actions"
+        role="group"
+        aria-label="Dataset Actions"
       >

And remove role="listitem" from the buttons (lines 99, 109).

ui/src/app/taskssummary/components/TasksTable.tsx (1)

155-155: Consider extracting ID generation for consistency.

Unlike ui/src/app/tasks/components/TasksTable.tsx which exports a getActionId helper, this file hardcodes the ID inline. For consistency and testability across the codebase, consider extracting the ID generation logic.

Example refactor:

export const getWorkflowActionId = (workflowId: string) => {
  return `workflow-${workflowId}`;
};

Then use it in the cell renderer:

           <WorkflowTableRowAction
-            id={`workflow-${row.original.workflow_id}`}
+            id={getWorkflowActionId(row.original.workflow_id)}
             name={row.original.workflow_id}
ui/src/app/workflows/components/TaskTableRowAction.tsx (1)

16-44: Unused useRef and buttonRef.

The buttonRef is declared and assigned to the button but never used elsewhere in the component. The auto-scroll behavior that previously used this ref was removed. Consider removing the unused ref to clean up the code.

-import { useRef } from "react";
+
 export const TaskTableRowAction = ({
   id,
   name,
   retry_id,
   lead,
   selected,
   verbose,
   updateUrl,
   extraParams,
 }: TaskTableRowActionProps) => {
-  const buttonRef = useRef<HTMLButtonElement>(null);

   return (
     <button
       id={id}
       className={`btn ${selected ? "btn-primary disabled:opacity-100" : "btn-secondary"} relative border-gray-400 enabled:hover:border-black`}
-      ref={buttonRef}
       onClick={() => {
ui/src/app/datasets/[bucket]/[name]/components/DatasetOverview.tsx (1)

134-139: Consider adding aria-label to the Versions IconButton.

For accessibility, the IconButton should have an aria-label to describe its action, especially since it controls the versions modal.

        <IconButton
          className="btn btn-primary"
          onClick={() => toolParamUpdater({ showVersions: true })}
          icon="layers"
          text="Versions"
+         aria-label="Show all versions"
+         aria-haspopup="dialog"
        />
ui/src/app/workflows/components/TaskDetails.tsx (1)

72-103: Consider using disabled attribute alongside aria-disabled.

The navigation buttons use aria-disabled but remain clickable (the click handler checks the condition). For better accessibility, consider using the disabled attribute which prevents interaction entirely, or add visual styling to indicate the disabled state.

            <button
              className="no-underline p-0 m-1"
              onClick={() => {
                if (hasPrevious) {
                  onPrevious?.();
                }
              }}
              title="Previous Task"
              aria-disabled={!hasPrevious}
+             disabled={!hasPrevious}
            >

Alternatively, keep aria-disabled but add conditional styling:

            <button
-             className="no-underline p-0 m-1"
+             className={`no-underline p-0 m-1 ${!hasPrevious ? "opacity-50 cursor-not-allowed" : ""}`}
ui/src/app/workflows/page.tsx (1)

312-318: Consider adding a comment explaining the 500ms timeout rationale.

The 500ms delay is presumably to allow the slide-out close animation to complete before focusing. While this works, it couples the timing to the animation duration. A minor documentation note would help future maintainers understand this dependency.

 onClose={() => {
   setSafeTimeout(() => {
-    // focus-trap-react does not work well with useReactTable as the ref for button inside the table that triggered the slideout is not persistent
+    // focus-trap-react does not work well with useReactTable as the ref for button inside the table that triggered the slideout is not persistent.
+    // The 500ms delay allows the slide-out close animation to complete before restoring focus.
     const el = focusReturnIdRef.current ? document.getElementById(focusReturnIdRef.current) : undefined;
     el?.focus();
   }, 500);
   updateUrl({ workflow: null });
 }}
ui/src/app/workflows/components/StatusFilter.tsx (1)

53-91: Consider adding accessible labeling to the checkbox group.

For screen reader users, wrapping the custom checkboxes in a fieldset with a legend would provide better context about what the checkboxes represent.

   return (
     <div className={className}>
       <StatusFilterCommon
         statusFilterType={statusFilterType}
         setStatusFilterType={setStatusFilterType}
       />
       {statusFilterType === StatusFilterType.CUSTOM &&
+        <fieldset>
+          <legend className="sr-only">Custom workflow status filters</legend>
         WorkflowStatusValues.map((name) => {
           const checked = Boolean(statusMap.get(name));
           return (
             <CheckboxWithLabel
               key={name}
               label={name}
               checked={checked}
               containerClassName="p-1"
               onChange={(event) => {
                 const newMap = new Map(statusMap);
                 newMap.set(name, Boolean(event.target.checked));
                 setStatusMap(newMap);
               }}
             />
           );
         })}
+        </fieldset>
+      }
     </div>
   );
ui/src/app/workflows/hooks/useToolParamUpdater.ts (1)

149-149: Casting URL param directly to ViewType may cause issues.

If an invalid or malicious value is passed in the URL, casting as ViewType doesn't validate it. Consider validating against known values.

-    setView(urlParams.get(PARAM_KEYS.view) as ViewType);
+    const viewParam = urlParams.get(PARAM_KEYS.view);
+    setView(Object.values(ViewType).includes(viewParam as ViewType) ? viewParam as ViewType : undefined);
ui/src/app/tasks/components/StatusFilter.tsx (1)

20-51: Consider consolidating duplicate status filter utilities.

This file closely mirrors ui/src/app/workflows/components/StatusFilter.tsx with the same structure but different status values. A generic utility accepting status values as a parameter could reduce duplication.

Example approach for a shared utility:

// ~/components/StatusFilterUtils.ts
export const createStatusArrayGetter = <T extends string>(
  allValues: readonly T[],
  currentStatuses: T[],
  failedPredicate: (status: T) => boolean,
  additionalFailedStatuses: T[] = []
) => (
  statusFilterType?: StatusFilterType,
  statusMap?: Map<T, boolean>
): string[] => {
  if (statusFilterType === StatusFilterType.ALL) return [...allValues];
  if (statusFilterType === StatusFilterType.CURRENT) return currentStatuses;
  if (statusFilterType === StatusFilterType.COMPLETED) return ["COMPLETED"];
  if (statusFilterType === StatusFilterType.FAILED) {
    return allValues.filter(failedPredicate).concat(additionalFailedStatuses as T[]);
  }
  if (!statusFilterType || !statusMap) return [];
  return Array.from(statusMap.entries())
    .filter(([_, enabled]) => enabled)
    .map(([status]) => status);
};

This is optional given the PR's accessibility focus, but worth considering for future maintainability.

ui/src/app/workflows/components/TasksFilter.tsx (2)

122-136: Reset sets StatusFilterType.ALL but verify this matches expected defaults.

The handleReset function sets localStatusFilterType to StatusFilterType.ALL and resets all local state. However, the updateUrl call (lines 130-135) doesn't explicitly set statusFilterType in the URL parameters. This could cause a mismatch between local state and URL state after reset.

Consider including statusFilterType in the reset URL update:

     updateUrl({
       filterName: null,
       nodes: undefined,
       status: undefined,
       pod_ip: undefined,
+      statusFilterType: StatusFilterType.ALL,
     });

194-204: Avoid rendering empty content inside InlineBanner.

When there are no errors, an empty string is rendered (line 202). This results in the InlineBanner still being rendered with status="none", which may still occupy space due to min-h-10 in the component's styling.

Consider conditionally rendering the banner only when errors exist:

-      <InlineBanner status={errors.length > 0 ? "error" : "none"}>
-        {errors.length > 0 ? (
-          <div className="flex flex-col">
-            {errors.map((error, index) => (
-              <div key={index}>{error}</div>
-            ))}
-          </div>
-        ) : (
-          ""
-        )}
-      </InlineBanner>
+      {errors.length > 0 && (
+        <InlineBanner status="error">
+          <div className="flex flex-col">
+            {errors.map((error, index) => (
+              <div key={index}>{error}</div>
+            ))}
+          </div>
+        </InlineBanner>
+      )}
ui/src/app/workflows/[name]/page.tsx (1)

403-409: Consider using aria-live only when content changes dynamically.

The "No tasks found" message has role="alert", which announces immediately to screen readers. If this is displayed on initial load rather than after a user action, consider using role="status" instead to avoid disruptive announcements.

ui/src/app/tasks/page.tsx (1)

430-436: Magic number timeout for focus restoration.

The 500ms timeout for focus restoration is a workaround for DOM timing issues. Consider extracting this to a named constant and adding a comment explaining why this specific duration was chosen, or using a more robust approach like requestAnimationFrame with a fallback.

+const FOCUS_RESTORE_DELAY_MS = 500; // Allow DOM to settle after slideout close animation
+
 // ... in component
           returnFocusOnDeactivate={false}
           onClose={() => {
             setSafeTimeout(() => {
               // focus-trap-react does not work well with useReactTable as the ref for button inside the table that triggered the slideout is not persistent
               const el = focusReturnIdRef.current ? document.getElementById(focusReturnIdRef.current) : undefined;
               el?.focus();
-            }, 500);
+            }, FOCUS_RESTORE_DELAY_MS);
ui/src/app/tasks/components/TasksFilters.tsx (2)

252-280: Reset logic retains current nodes value in localNodes initialization.

In handleReset, initNodes(nodes, poolNodes ?? []) is called with the current nodes prop value (line 261), which means local node selections may not fully reset if nodes still contains values from the URL. Consider passing an empty string to clear selections:

-    setLocalNodes(initNodes(nodes, poolNodes ?? []));
+    setLocalNodes(initNodes("", poolNodes ?? []));

383-389: Same InlineBanner pattern as TasksFilter - consider consistency.

This follows the same pattern as TasksFilter.tsx where the banner is always rendered. For consistency and to avoid empty banner rendering, consider the conditional render pattern suggested earlier.

-      <InlineBanner status={errors.length > 0 ? "error" : "none"}>
-        <div className="flex flex-col">
-          {errors.map((error, index) => (
-            <div key={index}>{error}</div>
-          ))}
-        </div>
-      </InlineBanner>
+      {errors.length > 0 && (
+        <InlineBanner status="error">
+          <div className="flex flex-col">
+            {errors.map((error, index) => (
+              <div key={index}>{error}</div>
+            ))}
+          </div>
+        </InlineBanner>
+      )}

Copy link
Contributor

@elookpotts-nvidia elookpotts-nvidia left a comment

Choose a reason for hiding this comment

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

First pass - seems like layout files are now unused except for page titles which can be moved into the page.tsx.

@patclarknvidia patclarknvidia merged commit 82004ac into main Dec 18, 2025
6 checks passed
@patclarknvidia patclarknvidia deleted the patc/a11y branch December 18, 2025 17:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants