Skip to content

add table semantics to workspace data tables for screen readers#93335

Open
aimane-chnaif wants to merge 18 commits into
Expensify:mainfrom
aimane-chnaif:aimane-chnaif/fix-76914-table-a11y
Open

add table semantics to workspace data tables for screen readers#93335
aimane-chnaif wants to merge 18 commits into
Expensify:mainfrom
aimane-chnaif:aimane-chnaif/fix-76914-table-a11y

Conversation

@aimane-chnaif

@aimane-chnaif aimane-chnaif commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Explanation of Change

The workspace data tables (Categories, Distance rates, Company cards, Workspaces list, Rooms, and Domains list) are built from generic View/Pressable elements, so on web they were exposed to screen readers as a flat list of generic containers with no table structure. Screen reader users had no way to perceive row/column relationships, the total number of rows, which column a value belonged to, or how a column was currently sorted.

This PR adds proper ARIA table semantics to those tables:

  • Introduces a shared helper module src/components/Table/tableAccessibility.ts that centralizes the accessibility prop-builders (role="table", rowgroup, row, columnheader, cell, plus aria-label, aria-rowcount, aria-colcount, aria-rowindex, and aria-sort).
  • Applies these roles at the correct levels: the table container (role="table"), the body wrapper (role="rowgroup"), the header and data rows (role="row"), the column headers (role="columnheader" with live aria-sort state), and each cell within a row (role="cell").
  • Because rows are virtualized by FlashList, aria-rowcount is set on the container and aria-rowindex on each row so screen readers can announce the correct position even when only a subset of rows is in the DOM. The header row is index 1, so data rows start at 2.
  • Adds a ROWGROUP entry to CONST.ROLE for the body wrapper.

The semantics are gated behind shouldUseTableSemantics(), which applies them only on web (where React Native Web forwards role/aria-* to the DOM) and only in the wide/grid layout. They are intentionally skipped on native — where these roles are no-ops and would strip the existing button/presentation roles that VoiceOver and TalkBack rely on to announce interactive rows — and in the narrow layout, which already renders a single-column card list that reads correctly.

Fixed Issues

$ #76914

Tests

  1. Table container semantics (web, wide): Inspect the categories table. Verify the container has role="table" with aria-label (the table title), aria-rowcount = number of categories + 1 (for the header), and aria-colcount = number of visible columns.
  2. Rowgroup + rows: Verify the body wrapper has role="rowgroup", the header row has role="row" with aria-rowindex="1", and each data row has role="row" with aria-rowindex starting at 2 and incrementing. Confirm each cell inside a row has role="cell" and each header cell has role="columnheader".
  3. Sort state (aria-sort): Click a sortable column header to sort ascending. Verify that header now has aria-sort="ascending"; click again and verify aria-sort="descending". Verify non-active sortable headers report aria-sort="none" and non-sortable headers have no aria-sort.
  4. Keyboard operability : Using only the keyboard (Tab / Shift+Tab), tab to a sortable column header and press Enter/Space — verify the sort toggles. Then tab to an interactive data row and press Enter/Space — verify it activates (navigates/opens) exactly as a mouse click would. Confirm rows/headers still receive a visible focus outline and remain in the tab order.
  5. Screen reader announcement: With VoiceOver (Safari) or NVDA (Chrome), navigate into the table using table-navigation commands (e.g. VoiceOver Ctrl+Option+arrows). Verify it announces "table", row/column position (e.g. "row 3 of 12, column 2"), the column header when entering a cell, and the sort state on headers.
  6. Narrow layout is unaffected: Resize the window to narrow / use mWeb. Verify the table renders the single-column card list and that none of the role="table"/"row"/"cell" attributes are present (semantics are intentionally disabled here).
  7. Coverage across all six tables: Repeat steps 1–3 (spot-check) on each: Categories, Distance rates, Company cards, Workspaces list, Rooms, and Domains list. Confirm every visible cell in each row has role="cell".
  • Verify that no errors appear in the JS console

Offline tests

  • Load a workspace Categories table while online, then go offline. Verify table semantics remain correct and rows still expose role="row"/aria-rowindex (accessibility props are static and unaffected by network state).

QA Steps

Same as Tests, with emphasis on:

  1. On desktop web (Chrome + Safari), open Categories, Distance rates, and Company cards tables and confirm a screen reader announces them as tables with correct row/column counts and reads column headers when moving between cells.
  2. Confirm keyboard-only users can sort columns (Enter/Space on a header) and activate rows.
  3. On iOS Native, Android Native, iOS mWeb Safari, and Android mWeb Chrome, confirm there is no regression: rows are still tappable, VoiceOver/TalkBack still announce interactive rows as buttons, and sorting still works. (Native intentionally gets no ARIA table roles, so behavior should be identical to before this PR.)
  4. Verify no visual/layout change on any platform (the new wrapping View around the table body should not shift spacing).
  • Verify that no errors appear in the JS console

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
    • MacOS: Desktop
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I verified there are no new alerts related to the canBeMissing param for useOnyx
  • I followed proper code patterns (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text shown in the product is localized by adding it to src/languages/* files and using the translation method
      • If any non-english text was added/modified, I used JaimeGPT to get English > Spanish translation. I then posted it in #expensify-open-source and it was approved by an internal Expensify engineer. Link to Slack message:
    • I verified all numbers, amounts, dates and phone numbers shown in the product are using the localization methods
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
    • I verified proper file naming conventions were followed for any new files or renamed files. All non-platform specific files are named after what they export and are not named "index.js". All platform-specific files are named for the platform the code supports as outlined in the README.
    • I verified the JSDocs style guidelines (in STYLE.md) were followed
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • I verified all code is DRY (the PR doesn't include any logic written more than once, with the exception of tests)
  • I verified any variables that can be defined as constants (ie. in CONST.ts or at the top of the file that uses the constant) are defined as such
  • I verified that if a function's arguments changed that all usages have also been updated correctly
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • If a new page is added, I verified it's using the ScrollView component to make it scrollable when more elements are added to the page.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari
web.mov

@codecov

codecov Bot commented Jun 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ Changes either increased or maintained existing code coverage, great job!

Files with missing lines Coverage Δ
src/CONST/index.ts 94.81% <ø> (ø)
src/components/Badge.tsx 97.43% <100.00%> (+0.06%) ⬆️
src/components/Table/Table.tsx 95.83% <100.00%> (+0.27%) ⬆️
src/components/Table/TableBody.tsx 100.00% <100.00%> (ø)
src/components/Table/middlewares/sorting.ts 95.65% <ø> (ø)
...c/components/Tables/AgentsTable/AgentsTableRow.tsx 100.00% <100.00%> (ø)
...aceCategoriesTable/WorkspaceCategoriesTableRow.tsx 85.00% <100.00%> (+0.78%) ⬆️
...ts/Tables/WorkspaceListTable/WorkspaceTableRow.tsx 91.11% <100.00%> (+0.20%) ⬆️
...WorkspaceMembersTable/WorkspaceMembersTableRow.tsx 92.30% <100.00%> (+0.30%) ⬆️
...ables/WorkspaceTagsTable/WorkspaceTagsTableRow.tsx 90.62% <100.00%> (+0.30%) ⬆️
... and 21 more
... and 12 files with indirect coverage changes

@rushatgabhane

Copy link
Copy Markdown
Member

hey @aimane-chnaif this PR hasn't been updated since 3 weeks. Can you please give us an ETA

@aimane-chnaif

aimane-chnaif commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

ETA today or tomorrow

@aimane-chnaif

Copy link
Copy Markdown
Contributor Author

@codex review

@aimane-chnaif

Copy link
Copy Markdown
Contributor Author

@MelvinBot review

@MelvinBot

Copy link
Copy Markdown
Contributor

🤖 Reviewed the change. This is a clean, well-documented, and consistently applied approach — the tableAccessibility helper centralizes the logic nicely, the "web + wide layout only" gating is sound, and the unit tests cover the pure prop builders well. A few things worth verifying before merge:

1. Verify the semantics actually reach a screen reader (highest priority)

The body rows are rendered inside a FlashList, and the role="rowgroup" is on the outer View wrapping it. FlashList inserts its own scroll/content container <div>s between that wrapper and the actual role="row" elements. ARIA table roles require a proper nesting chain (tablerowgrouprowcell); intervening generic elements without role="presentation" can break the association, so a screen reader may not announce the table structure at all in practice. This is exactly the failure mode that unit tests can't catch — please attach a short recording of VoiceOver/NVDA reading a table (row/column position, sort state) so we can confirm the semantics survive virtualization. The Screenshots/Videos sections in the PR body are currently empty, which for an a11y PR is the main missing evidence.

2. The container <View> is added on all platforms, not just web

In Table.tsx the new <View style={[styles.flex1, styles.mnh0]}> wrapping {children} renders unconditionally — the accessibility props are gated, but the extra flex wrapper is not. Since Table is shared by every workspace table on native + narrow layouts too, this adds a layout node everywhere. Worth confirming there's no layout/scroll regression on native and in the narrow card layout (or gate the wrapper to shouldUseTableSemantics since it only exists for the a11y props).

3. Selection checkbox breaks the row/column count

When selection is enabled, each row renders a leading checkbox that is a direct child of the role="row" element but has no role="cell". Per ARIA, a row should only contain cell/columnheader children, and aria-colcount is set to columns.length (tableAccessibility.ts) which excludes it — so the declared count won't match the rendered columns when a checkbox column is present. The header has the same extra checkbox column. Consider wrapping the checkbox cell with getCellAccessibilityProps (and/or emitting aria-colindex on cells) so the structure stays consistent. The doc comment acknowledges this trade-off, but the mismatch can make a screen reader miscount columns.

4. Interactive rows lose their button role

In TableRow the spread getRowAccessibilityProps(...) (role: 'row') comes after role={interactive ? BUTTON : PRESENTATION}, so it overrides it — clickable rows become role="row" under a static role="table". That's the correct table role, but it means the row's press-to-navigate affordance is no longer announced as actionable. Please confirm this is the intended UX (a static table with clickable rows is a slightly unusual pattern — if actionability matters, role="grid" + gridcell is the more standard fit). This is also the kind of thing the recording in #1 would settle.

Minor

  • Making accessibilityLabel required on TableRow (and narrowing accessible to true) is a nice improvement over the old hardcoded "row" label — just make sure typecheck (currently pending) goes green, since it's the gate ensuring every consumer passes a meaningful label.

Nothing here is blocking on its own, but #1 (screen-reader evidence) is the one I'd want resolved before merge, since it's the whole point of the change.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2e4235226a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/components/Table/Table.tsx Outdated
Comment thread src/components/Table/TableRow.tsx
@aimane-chnaif

Copy link
Copy Markdown
Contributor Author

@codex review

@aimane-chnaif

Copy link
Copy Markdown
Contributor Author

@MelvinBot review

@MelvinBot

Copy link
Copy Markdown
Contributor

🤖 Re-reviewed after the address review comments commit (c8b6922). The two structural findings from my last pass are now properly resolved:

  • ✅ Container <View> no longer added on native/narrow. renderChildren() only wraps the header/body run in the role="table" container when isTableSemanticsEnabled, and returns children untouched otherwise. It also correctly keeps the FilterBar/EmptyState/NoResultsState outside the ARIA table so a screen reader doesn't walk into them as table content. I checked the consumers (WorkspaceCategories, List, Tags, …) and Table.Header/Table.Body are always adjacent, so the contiguous-run grouping produces a single table container as intended.
  • ✅ Selection column now counted consistently. aria-colcount is columns.length + (selectionEnabled ? 1 : 0), the header select-all is exposed as a non-sortable columnheader, and the row checkbox is wrapped as a cell, so header cells, data cells, and the declared count all line up. Nicely done.

Tests are green (all 8 test jobs pass, including the updated tableAccessibilityTest). Two items from the first review remain open:

1. Still need screen-reader evidence (the main gate)

The Screenshots/Videos sections in the PR body are still empty. The correctness of ARIA table semantics under FlashList virtualization can only be confirmed by an actual screen reader — please attach a short VoiceOver/NVDA recording showing a table announcing its row/column position and sort state. That's the one piece of proof I can't derive from the code.

2. Minor / confirm intent: interactive rows report role="row", not button

In TableRow the getRowAccessibilityProps spread still overrides role={interactive ? BUTTON : PRESENTATION} with role="row", so a clickable row's press-to-navigate affordance isn't announced as actionable. That's the correct role for a static role="table", so this is fine if intended — just flagging it since some of these tables have clickable rows and a grid/gridcell model conveys actionability. Not blocking.

Maintainability nit

The grouping in renderChildren() depends on Table.Header/Table.Body being rendered as direct, contiguous children (type-identity match). That holds today, but a future consumer that wraps them or interleaves another element between them would silently split the rows into a separate role="table" (or drop semantics). A one-line comment near the consumer contract — or a dev-only warning if a TableBody renders without an enclosing container — would prevent a silent regression later.

Overall this is in good shape; #1 (a screen-reader recording) is the only thing I'd still want before merge.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c8b6922ed4

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/components/Table/TableBody.tsx
@aimane-chnaif

Copy link
Copy Markdown
Contributor Author

I've still been testing all affected pages but this is ready for initial review

@aimane-chnaif
aimane-chnaif marked this pull request as ready for review July 10, 2026 17:36
@aimane-chnaif
aimane-chnaif requested review from a team as code owners July 10, 2026 17:36
@melvin-bot
melvin-bot Bot requested review from joekaufmanexpensify and parasharrajat and removed request for a team and joekaufmanexpensify July 10, 2026 17:36
@melvin-bot

melvin-bot Bot commented Jul 10, 2026

Copy link
Copy Markdown

@parasharrajat Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@melvin-bot
melvin-bot Bot removed the request for review from a team July 10, 2026 17:36
@github-actions

Copy link
Copy Markdown
Contributor

🚧 jasperhuangg has triggered a test Expensify/App build. You can view the workflow run here.

@github-actions

This comment has been minimized.

@gijoe0295

gijoe0295 commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Accessibility shortcuts on Windows are great but buggy on MacOS. I'll post list of bugs below.

@gijoe0295

gijoe0295 commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Ctrl + Option and arrows should navigate between the cells in the table but it only navigates the cells in a row. For example:

  1. I'm on Workspaces column header
  2. Ctrl + Option + Down arrow should navigate to the below cell ✅ but it navigates to the next right cell (Owner column header) ❌
shortcut.mov

Expected behavior

  • Ctrl + Option/Alt + Up/Down arrows navigate cells in a column
  • Ctrl + Option/Alt + Left/Right arrows navigate cells in a row

This works perfectly on Windows but not on MacOS.

Windows.Shortcuts.mp4

@gijoe0295

This comment was marked as low quality.

@gijoe0295

Copy link
Copy Markdown
Contributor

Shift ⇧ traverses each column header → next rows ✅ But when I'm on a row, using Ctrl + Option/Alt + arrows navigates between the cells in the previous row:

row.navigation.mov

Also using Shift ⇧, the column header is announced twice:

Screenshot 2026-07-22 at 13 56 09

Expected behavior

Ctrl + Option/Alt + arrows should navigate between the cells in the focused row.

Windows is working perfectly:

Windows.Row.Navigation.mp4

Comment thread src/components/Table/TableHeader.tsx Outdated
Comment thread src/components/Table/TableRow.tsx
@aimane-chnaif
aimane-chnaif requested a review from gijoe0295 July 22, 2026 11:36
@gijoe0295

gijoe0295 commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Column header is not focused when announced (it only focuses on the column sort icon) via Ctrl + Option + arrows:

Header.not.focused.mov

The column header is also announced as: Owner: blank, column 2 of 4. Expected: Owner, column 2 of 4 - no blank

@gijoe0295

Copy link
Copy Markdown
Contributor

The Default badge takes 3 shortcuts (Ctrl + Option + Right arrow three times) to navigate through:

Default.group.mov

Please fix this for any other badges in other pages as well.

@gijoe0295

gijoe0295 commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Reviewer Checklist

  • I have verified the author checklist is complete (all boxes are checked off).
  • I verified the correct issue is linked in the ### Fixed Issues section above
  • I verified testing steps are clear and they cover the changes made in this PR
    • I verified the steps for local testing are in the Tests section
    • I verified the steps for Staging and/or Production testing are in the QA steps section
    • I verified the steps cover any possible failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
  • I checked that screenshots or videos are included for tests on all platforms
  • I included screenshots or videos for tests on all platforms
  • I verified that the composer does not automatically focus or open the keyboard on mobile unless explicitly intended. This includes checking that returning the app from the background does not unexpectedly open the keyboard.
  • I verified tests pass on all platforms & I tested again on:
    • Android: HybridApp
    • Android: mWeb Chrome
    • iOS: HybridApp
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • If there are any errors in the console that are unrelated to this PR, I either fixed them (preferred) or linked to where I reported them in Slack
  • I verified proper code patterns were followed (see Reviewing the code)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I verified that this PR follows the guidelines as stated in the Review Guidelines
  • I verified other components that can be impacted by these changes have been tested, and I retested again (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar have been tested & I retested again)
  • If a new component is created I verified that:
    • A similar component doesn't exist in the codebase
    • All props are defined accurately
    • The component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone
    • The only data being stored in the state is data necessary for rendering and nothing else
    • The component has the minimum amount of code necessary for its purpose, and it is broken down into smaller components in order to separate concerns and functions
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG)
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • For any bug fix or new feature in this PR, I verified that sufficient unit tests are included to prevent regressions in this flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.
  • I have checked off every checkbox in the PR reviewer checklist, including those that don't apply to this PR.

Screenshots/Videos

Android: HybridApp Screenshot_1784798396
Android: mWeb Chrome Screenshot_1784798363
iOS: HybridApp Simulator Screenshot - iPhone 17 - 2026-07-23 at 15 41 29
iOS: mWeb Safari Simulator Screenshot - iPhone 17 - 2026-07-23 at 15 50 20
MacOS: Chrome / Safari
Windows: Chrome
Windows.Row.Navigation.mp4
Windows.Shortcuts.mp4

@gijoe0295

Copy link
Copy Markdown
Contributor

Ctrl + Alt + Shift + Down arrow should focus on the first cell which is the Workspaces column header but it focuses on the first data cell instead:

Enter.table.mov

Steps:

  1. Ctrl + Alt + Cmd + T to locate and focus the whole table
  2. VoiceOver instruction is: • You are currently on a table. To enter this table, press Control-Option-Shift-Down Arrow.
  3. Press Control-Option-Shift-Down Arrow
  4. It focus on the first workspace data cell, not the Workspaces column header

@github-actions

Copy link
Copy Markdown
Contributor

🚧 jasperhuangg has triggered a test Expensify/App build. You can view the workflow run here.

@github-actions

Copy link
Copy Markdown
Contributor

🧪🧪 Use the links below to test this adhoc build on Android, iOS, and Web. Happy testing! 🧪🧪
Built from App PR #93335.

Android 🤖 iOS 🍎
⏩ SKIPPED ⏩ ⏩ SKIPPED ⏩
The build for Android was skipped The build for iOS was skipped
Web 🕸️
https://93335.pr-testing.expensify.com
Web

👀 View the workflow run that generated this build 👀

@gijoe0295

Copy link
Copy Markdown
Contributor

Checkbox is misaligned:

Simulator Screenshot - iPhone 17 - 2026-07-23 at 15 40 03

Staging:

Screenshot 2026-07-23 at 15 40 26

@aimane-chnaif

Copy link
Copy Markdown
Contributor Author

@gijoe0295 it's fixed now

@gijoe0295 gijoe0295 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

@melvin-bot

melvin-bot Bot commented Jul 23, 2026

Copy link
Copy Markdown

We did not find an internal engineer to review this PR, trying to assign a random engineer to #76914 as well as to this PR... Please reach out for help on Slack if no one gets assigned!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants