Fix empty-state alignment shift on Tags, Per diems, and Workspaces tables - #97451
Conversation
Co-authored-by: Aimane Chnaif <aimane-chnaif@users.noreply.github.com>
|
@codex review |
|
@MelvinBot review |
Codecov Report✅ Changes either increased or maintained existing code coverage, great job!
|
DetailsExplanation of ChangePR #93335 (table ARIA semantics for screen readers) wrapped the contiguous This change short-circuits Fixed Issues$ #97428 TestsPrecondition:
Offline testsSame as Tests QA StepsSame as Tests
PR Author Checklist
Screenshots/Videos@MelvinBot please set the PR body to the content in the details section above |
ReviewThe fix is correct, minimal, and well-targeted. 👍 The root-cause analysis holds up against the code. Verification of the claim — when
The guard is also placed correctly — after Two things worth a look before merge (neither blocking)1. Missing test for the new branch. 2. Since this is a layout deploy blocker, the human co-author should still complete the PR's own test steps and confirm the empty-state illustration is re-centered on wide web (Tags, Per diems, empty Workspaces list) before marking ready. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6368f1503a
ℹ️ 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".
| if (rowCount === 0) { | ||
| return children; |
There was a problem hiding this comment.
Preserve the table wrapper when the body still renders
On wide web this early return also applies when processedData.length is zero because a search/filter removed all rows, but Table.Body can still render instead of returning null whenever a ListHeaderComponent is supplied (for example, WorkspaceExpensifyCardsTable passes one). In that case TableBody still applies role="rowgroup", while this guard suppresses the enclosing role="table", leaving invalid orphaned table semantics for screen-reader users. Either keep the wrapper whenever the body will render, or suppress the rowgroup semantics in the same empty case.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
There was a problem hiding this comment.
Good catch — this is a real orphaned-semantics bug, fixed in 590484f.
The unconditional rowCount === 0 guard removed the role="table" wrapper even when TableBody still renders (it only returns null when empty and no ListEmptyComponent/ListHeaderComponent is supplied — WorkspaceExpensifyCardsTable passes a ListHeaderComponent), leaving its role="rowgroup" without a role="table" parent on wide web.
I went with your first option (keep the wrapper when the body still renders):
Table.tsxnow derivesrendersBodyWhenEmpty = !!listProps.ListEmptyComponent || !!listProps.ListHeaderComponentand passes it down.TableSemanticContainer.tsxonly short-circuits whenrowCount === 0 && !rendersBodyWhenEmpty.
This keeps the semantic wrapper (and valid table → rowgroup nesting) for the WorkspaceExpensifyCardsTable empty state, while still dropping the extra flex:1 node for the sibling-empty-state cases (Tags, Per diems, Workspaces) that were the deploy blocker — those pass no list slot, so TableBody renders null and the alignment fix is unchanged.
Added two unit tests in TableSemanticContainerTest.tsx covering both empty-table branches (5/5 pass locally). fmt, typecheck, and ESLint are clean.
| // An empty table (no data rows) has no tabular content to expose to a screen reader, and the header/body | ||
| // render nothing in that case. Skipping the wrapper avoids inserting an extra flex:1 layout node next to the | ||
| // empty-state view, which would otherwise share the available height and shift the empty state upward. |
|
I looked into the failing checks. There's no code fix to push — every real check is green and the only failure is the author-checklist gate, which needs a human. The only failing check is
This gate is meant for the human co-author, so I can't clear it by pushing code (and I won't tick the boxes on your behalf). To make it pass, you'll need to fill out the Tests / QA sections in the PR body and check the author-checklist items once you've verified the change. All other checks pass — |
|
Nothing has changed since my previous comment — there's still no code fix to push. The only failing check is To clear it, you'll need to fill out the Tests / QA sections in the PR body and check the author-checklist items yourself once you've verified the change. I can't tick those boxes on your behalf. |
|
✅ Updated the PR description with the content you provided. |
Address review feedback: skipping the role=table wrapper for all empty tables
left an orphaned role=rowgroup when TableBody still renders via a ListEmptyComponent
or ListHeaderComponent (e.g. WorkspaceExpensifyCardsTable). Now the wrapper is only
skipped when the body renders nothing, so those tables keep valid table semantics
while the empty-state alignment fix is preserved for the sibling-empty-state cases.
Co-authored-by: Aimane Chnaif <aimane-chnaif@users.noreply.github.com>
Co-authored-by: {"message":"Not Found","documentation_url":"https://docs.github.com/rest/issues/comments#get-an-issue-comment","status":"404"} <{"message":"Not Found","documentation_url":"https://docs.github.com/rest/issues/comments#get-an-issue-comment","status":"404"}@users.noreply.github.com>
|
@MelvinBot while we're here, let's also fix alignment issues in table header
For example, in categories table:
This also happens on other tables.
|
|
Re-checked after the latest commit (
Everything else passes on the current head, including the new |
Apply column containerStyles to the flexRow sort button instead of the column-direction cell wrapper so justify-content aligns the header label horizontally, restoring right-docked and vertically centered headers. Co-authored-by: Aimane Chnaif <aimane-chnaif@users.noreply.github.com>
|
Done — pushed Root cause: In the web/wide ARIA path, each column's Fix: Apply Verified on wide web (1920px) — pixel-checked, both Categories and Tags tables:
Checks run locally: |
| ? // The column's `containerStyles` (e.g. `justifyContentEnd`) are horizontal-alignment styles meant for the | ||
| // header row, so they belong on this `flexRow` button rather than the column-direction cell wrapper below, | ||
| // where `justify-content` would act on the vertical axis and leave the label mis-aligned. | ||
| [styles.flexRow, styles.alignItemsCenter, styles.tableHeaderContentHeight, styles.flex1, column.styling?.containerStyles, !column.sortable && styles.cursorDefault] |
There was a problem hiding this comment.
@MelvinBot PressableWithFeedback still doesn't take full height.
Should we add wrapperStyle={styles.flex1} when table semantics enabled?
Also, to reuse already existing tableHeaderStyles, let's define like this:
styles=[]
containerStyles={[column.styling?.flex ? {flex: column.styling.flex} : styles.flex1]}
So split existing tableHeaderStyles into 2.
There was a problem hiding this comment.
Done — pushed 450adce. You're right on both counts.
Full height: PressableWithFeedback renders its style on the inner GenericPressable but wraps it in an OpacityView whose style comes from wrapperStyle, which defaults to []. So the button's flex1 was measured against a wrapper that had collapsed to content height — the pressable never filled the cell, which is why the label wasn't vertically centered and the last column wasn't right-docked. Added wrapperStyle={styles.flex1} in the semantic path so the wrapper fills the columnheader cell and the button (also flex1) fills the wrapper.
Dedup: split tableHeaderStyles into a shared base (the visual/alignment styles, including column.styling?.containerStyles) plus a separate flex piece, applied per path:
- non-semantic →
[...tableHeaderStyles, columnFlexStyle](flex on the button itself, unchanged behavior) - semantic →
[...tableHeaderStyles, styles.flex1]on the button +wrapperStyle={styles.flex1}, withcolumnFlexStyle(styling.flexorflex1) living on the columnheader cell wrapper
I read your containerStyles={[...]} as "put the flex on the container/wrapper" — since PressableWithFeedback's wrapper prop is wrapperStyle, I used that. If you actually meant a different split, say the word and I'll reshape it.
One thing I couldn't self-verify: the exact pixel centering on wide web. The reasoning is solid, but since you're looking at it live, a quick confirm on the categories table (Name ⬆️ centered, Enabled right-docked + centered) would be good before we call it done.
|
Re-checked on the current head ( The sole remaining failure is |
…tyles
Co-authored-by: Aimane Chnaif <aimane-chnaif@users.noreply.github.com>
Co-authored-by: {"message":"Not Found","documentation_url":"https://docs.github.com/rest/issues/comments#get-an-issue-comment","status":"404"} <{"message":"Not Found","documentation_url":"https://docs.github.com/rest/issues/comments#get-an-issue-comment","status":"404"}@users.noreply.github.com>
|
@codex review |
|
@MelvinBot review |
Review (updated for
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 450adce472
ℹ️ 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".
| <View | ||
| style={[column.styling?.flex ? {flex: column.styling.flex} : styles.flex1, column.styling?.containerStyles]} | ||
| style={columnFlexStyle} |
There was a problem hiding this comment.
Keep grid sizing styles on semantic header cells
On wide web, this semantic path now applies every column.styling.containerStyles value to the inner pressable and leaves the actual role="columnheader" grid item with only flex. WorkspaceExpensifyCardsTable uses containerStyles: [styles.mnw0] on the Limit type and Status columns specifically so the grid tracks can shrink/equalize; minWidth: 0 has to be on the grid item itself, so those headers can again force uneven/overflowing tracks when their labels are wider. Keep sizing constraints like mnw0 on this wrapper while moving only the horizontal alignment styles to the button.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Good catch — fixed in 24d945b.
You're right that mnw0 is a grid-track sizing constraint, not a horizontal-alignment one, so it has to sit on the role="columnheader" grid item, not the inner button. In the non-semantic path the button is the grid item (so production was fine); the ARIA wrapper broke that.
Fix: forward only the sizing keys from containerStyles onto the wrapper cell, and keep the alignment styles on the flexRow button — TableHeader.tsx:229-234 and :300:
const {minWidth, maxWidth} = StyleSheet.flatten(column.styling?.containerStyles) ?? {};
const columnCellSizingStyle = {minWidth, maxWidth};
// ...
<View style={[columnFlexStyle, columnCellSizingStyle]} {...columnHeaderProps}>I forwarded the sizing keys rather than duplicating the whole containerStyles onto both nodes on purpose: the alignment columns pair justifyContentEnd with pr3, and putting that pr3 on the wrapper and the button would double the right padding and shift the right-docked headers (enabled/actions) off from the body cells. Sizing (mnw0) and alignment (justifyContent*/pr3) happen to be mutually exclusive per column today, but extracting only the sizing keys keeps it correct even if that changes.
Verification: oxfmt, lint-changed, typecheck (tsgo) clean, and the Table suites pass (tests/unit/components/Table + TableTest + WorkspaceCompanyCardsTableTest + WorkspaceViewTagsTableTest, 72 tests). React Compiler compliance couldn't run in this sandbox (its base-ref git lookup is blocked), but this is a pure style-array change adding no new hooks/components, so it introduces no memoization.
Since this is a grid-sizing change, worth a visual pass on Workspace ▸ Expensify Cards (wide web) to confirm the Limit type and Status headers stay equal-width and their labels truncate rather than overflow when narrow.
|
Re-checked — still no code fix to push. The one remaining red check is the checklist gate, and it's a human gate, not a code defect. The failing check is now All 35 real checks are green — |
|
Re-checked — still no code fix to push, and the situation moved forward. The author-checklist gate is now green; the only remaining red check is the That's a human reviewer gate, not a code defect — it clears when the PR reviewer posts/completes their reviewer checklist on the PR. I can't satisfy it by pushing code. All 35 other checks are green — |
Reviewer Checklist
Screenshots/VideosAndroid: HybridAppAndroid: mWeb ChromeiOS: HybridAppiOS: mWeb SafariMacOS: Chrome / Safariweb.mov |
Forward containerStyles' minWidth/maxWidth onto the role="columnheader"
grid item so 1fr tracks size from their share, not content. Horizontal
alignment stays on the sort button to avoid double-applying padding.
Co-authored-by: Aimane Chnaif <aimane-chnaif@users.noreply.github.com>
Co-authored-by: {"message":"Not Found","documentation_url":"https://docs.github.com/rest/issues/comments#get-an-issue-comment","status":"404"} <{"message":"Not Found","documentation_url":"https://docs.github.com/rest/issues/comments#get-an-issue-comment","status":"404"}@users.noreply.github.com>
|
MelvinBot has made 6 consecutive commits trying to fix CI failures. Automatic CI fix dispatches are now paused for this PR. Next steps: A human should review the recent commits and resolve the conflicting CI errors manually. |
|
@MelvinBot fix eslint check |
Review (head
|
|
Codex Review: Didn't find any major issues. Breezy! Reviewed commit: ℹ️ About Codex in GitHubCodex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
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". |
Co-authored-by: Aimane Chnaif <aimane-chnaif@users.noreply.github.com>
|
Fixed the failing ESLint check. The three Rather than add explicit keys (which would stop the test faithfully mirroring how tables pass Verified |
Regression scanNo functional regression found in the empty-state fix itself — and notably, the two regressions a naive version of this fix would have introduced are already handled and covered by tests:
I also confirmed the blocker fix still lands: the reported tables (Tags, View tags, Per diems, Workspaces list) don't supply The real regression risk: bundled
|
aimane-chnaif
left a comment
There was a problem hiding this comment.
@NikkiWines along with #97428, I found minor style issue in table header after comparing with production and fixed them too.
NikkiWines
left a comment
There was a problem hiding this comment.
Looks good but several comment blocks explain history/alternatives. Comments should explain why, not what was tried, so a fair few of the comments could be trimmed
| const columnCellSizingStyle = {minWidth, maxWidth}; | ||
|
|
||
| // Base sort-button styles shared by both the semantic and non-semantic paths. The column's `containerStyles` | ||
| // (e.g. `justifyContentEnd`) are horizontal-alignment styles for the header row, so they belong on this `flexRow` |
There was a problem hiding this comment.
| // (e.g. `justifyContentEnd`) are horizontal-alignment styles for the header row, so they belong on this `flexRow` | |
| // are horizontal-alignment styles for the header row, so they belong on this `flexRow` |
There was a problem hiding this comment.
Applied in e150d93 — dropped the (e.g. justifyContentEnd) parenthetical and trimmed the rest of the block down to the "why" (the containerStyles are row-axis alignment, so they go on the flexRow button, not the column-direction cell wrapper).
|
@MelvinBot address #97451 (comment), #97451 (review) |
Co-authored-by: Aimane Chnaif <aimane-chnaif@users.noreply.github.com>
|
Trimmed the comment blocks that were explaining history/alternatives rather than just the "why" — pushed in e150d93. What changed
Comment-only changes — no logic touched. Lint passes (only pre-existing seatbelt warnings). |
|
@NikkiWines comments look better now |
joekaufmanexpensify
left a comment
There was a problem hiding this comment.
Fixing deploy blockers
|
✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release. |
|
🚧 NikkiWines has triggered a test Expensify/App build. You can view the workflow run here. |
|
🧪🧪 Use the links below to test this adhoc build on Android, iOS, and Web. Happy testing! 🧪🧪
|
Fix empty-state alignment shift on Tags, Per diems, and Workspaces tables (cherry picked from commit fdbb9ee) (cherry-picked to staging by marcaaron)
|
🚀 Cherry-picked to staging by https://github.com/marcaaron in version: 9.4.46-8 🚀
|
|
🤖 I reviewed the changes in this PR against the help site content under No help site changes are required. This PR is a purely internal rendering/layout fix: it short-circuits Because nothing about how these features work or how they're described has changed, there's no documentation to update, so I did not create a draft docs PR. @aimane-chnaif, please review the linked help site PR and confirm it reflects the current behavior. Then mark the linked help site PR |
|
🚀 Cherry-picked to staging by https://github.com/marcaaron in version: 9.4.46-10 🚀
|
|
🤖 Re-reviewed the full current diff (all commits, including the later table-header vertical/right-alignment fixes) against the help site content under Still no help site changes are required. Everything in this PR is internal rendering/layout work on the shared @aimane-chnaif, please review the linked help site PR and confirm it reflects the current behavior. Then mark the linked help site PR |
|
🚀 Deployed to production by https://github.com/marcaaron in version: 9.4.46-10 🚀
Bundle Size Analysis (Sentry): |
|
🚀 Cherry-picked to staging by https://github.com/marcaaron in version: 9.4.47-0 🚀
|
|
🤖 Nothing has changed since my previous docs review — no help site changes are required. This PR is entirely internal rendering/layout work on the shared @aimane-chnaif, please review the linked help site PR and confirm it reflects the current behavior. Then mark the linked help site PR |





Explanation of Change
PR #93335 (table ARIA semantics for screen readers) wrapped the contiguous
TableHeader/TableBodyrun in an extra<View style={[styles.flex1, styles.mnh0]}>insideTableSemanticContainer. When a table is empty,TableHeaderandTableBodyboth rendernull, but they're still valid React elements, so the wrapper was rendered anyway withflex: 1. That put a secondflexGrow: 1sibling next to the empty-stateScrollView(alsoflexGrow: 1), roughly halving the ScrollView's height and shifting its vertically-centered illustration up. The markup is gated behindshouldUseTableSemantics()(web + wide only), which is why it reproduced only on wide web.This change short-circuits
TableSemanticContainerto render its children directly whenrowCount === 0, restoring the pre-#93335 layout for empty states. An empty table has no data rows to expose to a screen reader, so no ARIA table semantics are lost; populated tables are unaffected.Fixed Issues
$ #97428
Tests
Precondition:
Offline tests
Same as Tests
QA Steps
Same as Tests
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectiontoggleReportand notonIconClick)Avatar, I verified the components usingAvatarare working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))npm run compress-svg)Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos