Skip to content

Column Menu & Sort Features #147

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed

Conversation

rembrandtreyes
Copy link

@rembrandtreyes rembrandtreyes commented May 4, 2025

This PR adds an interactive column menu and improved sorting functionality to enhance the table user experience.

Key Changes:

1. New Column Menu Component

  • Added contextual menu for column operations
  • Implemented options for hiding/showing columns
  • Added intuitive sort controls with visual indicators for current sort state

2. Enhanced Column Headers

  • Added menu button that appears on hover
  • Positioned sort indicators next to column text
  • Added right-click context menu support

3. Improved Sort UX

  • Sort options now explicitly show "Sort ascending" and "Sort descending"
  • Added "Clear sort" option when a column is sorted
  • Visual checkmarks indicate the current sort direction
  • Sort indicator appears right after the column text

4. Code Quality

  • Added comprehensive tests for the new components
  • Integrated Prettier for consistent code formatting
  • Fixed ESLint configuration to work with Prettier

5. Technical Architecture

  • Created a flexible adapter pattern between menu actions and existing sorting logic
  • Maintained backward compatibility with click-to-sort behavior
  • Clean component separation with descriptive props and interfaces

This PR improves data exploration by making column operations more discoverable and providing clearer visual feedback for current table state.

Demo

Screen.Recording.2025-05-03.at.11.53.00.PM.mov

No hiding for column.length === 1

Screenshot 2025-05-08 at 10 03 39 AM

@rembrandtreyes rembrandtreyes force-pushed the rembrandtreyes/ColumnMenu branch from 9c7e160 to 92578c0 Compare May 4, 2025 07:26
@platypii platypii requested review from severo and platypii May 4, 2025 22:00
@platypii
Copy link
Contributor

platypii commented May 4, 2025

Super cool thanks @rembrandtreyes! I'll review tonight.

eslint.config.js Outdated
Comment on lines 7 to 8
import prettierPlugin from 'eslint-plugin-prettier'
import prettierConfig from 'eslint-config-prettier'
Copy link
Contributor

Choose a reason for hiding this comment

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

I like prettier, I'm open to considering it. I'm not at all happy with the current eslint situation. But the change belongs in its own PR.

Copy link
Author

Choose a reason for hiding this comment

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

Yeah sounds good, I'll revert, and create an issue to add prettier in.

@@ -0,0 +1,14 @@
.headerText {
Copy link
Contributor

Choose a reason for hiding this comment

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

@severo can say more but... we've been trying to use existing elements and semantic tags (aria-, data-) to style as much as possible in hightable, avoiding adding classNames as much as possible.

The problem is that people want to custom style the table. But css modules obfuscate the classNames. So to expose it outside the component you either need to allow user to pass in custom classes that are populated through the table.

The (better, so far) alternative has been to let the user specify a custom top-level className (eg- className={styles.myhightable}). Then if all the elements have semantic structure you can style from outside like:

.myhightable thead th > span {
  text-overflow: ellipsis;
  ...
}

So basically I'm saying it would be better to do that here, so that users can see how we styled it, and not have to figure out weird css-affinity issues.

<ColumnMenuButton onClick={handleMenuButtonClick} />
<ColumnResizer setWidth={setWidth} onDoubleClick={autoResize} width={width} />
</th>
<ColumnMenu
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason the ColumnMenu is a sibling of the <th>? Why not inside the <th>?

I'm not necessarily opposed but doesn't this result in table that looks like:

<table>
  <thead>
    <th>...</th>
    <ColumnMenu/>
    <th>...</th>
    <ColumnMenu/>
...

Copy link
Author

Choose a reason for hiding this comment

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

It's the createPortal implementation that we have in ColumnMenu. So the Column menu is attached to document.body instead, this is to prevent z-index issues.

I'll move this out into a function to render here instead and I'll add a comment.


if (!isVisible) return null

return createPortal(
Copy link
Contributor

Choose a reason for hiding this comment

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

nice

position: relative;
display: inline-block;
margin-left: 12px;
font-size: 14px;
Copy link
Contributor

@platypii platypii May 4, 2025

Choose a reason for hiding this comment

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

normally would say don't use px for font-size. This is effectively an icon so I think it's okay. But if someone scale up text, should the sort indicator scale with the text?

PS- It looks really nice!

Comment on lines 466 to 468
.map((column, index) => ({ column, index }))
.filter(({ index }) => isColumnVisible(index))
.map(({ column, index: columnIndex }) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

At first I was going to comment on this map/filter/map seems redundant. But as I was writing it I understood that you need the index to be stable even after the filter. So you're right. But maybe add a comment?

Copy link
Author

Choose a reason for hiding this comment

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

Yeah I wasn't sure of a "better" way to handle column visibility while preserving the original indexes. Good news is that it's still O(n) and after looking at the first map().filter() piece we can useMemo to prevent unnecessary recalculations. The bad news is trying to grok this, and memory consideration with extremely large tables (column dependent).

I'll add these lines:

// We use map/filter/map to preserve original column indexes after filtering:
// 1. First map: Annotate each column with its original index
// 2. Filter: Remove hidden columns while keeping original index information
// 3. During render: Use the preserved originalIndex to access properties that 
//    depend on the column's position in the original array (CSS classes, event handlers, etc.)

Comment on lines 40 to 47
if (sortDirection !== undefined) {
// Handle explicit sort directions from the menu
const { prefix, suffix } = partitionOrderBy(orderBy, columnHeader);

if (sortDirection === null) {
Copy link
Contributor

Choose a reason for hiding this comment

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

What's up with undefined in one branch and null in another?

Copy link
Author

Choose a reason for hiding this comment

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

sortDirection !== undefined separates header clicks from column menu actions

(sortDirection === null) handles the clear sort case within the column menu

I'll add better comments as it isn't very clear that the sortDirection is undefined for header clicks.

onOrderByChange([{ column: columnHeader, direction: 'descending' }, ...suffix]);
} else {
// descending -> none
onOrderByChange([...suffix]);
Copy link
Contributor

Choose a reason for hiding this comment

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

no semicolons please... npm run lint:fix

This was referenced May 5, 2025
@rembrandtreyes rembrandtreyes force-pushed the rembrandtreyes/ColumnMenu branch from 661a9ef to 271c8c3 Compare May 6, 2025 08:43
@rembrandtreyes
Copy link
Author

Hey folks. Just got back from vacation. I'll be working through these comments today and rerequest a review. Thanks again @severo for great feedback!

…isabled prop for better control over column visibility. Refactor styles and logic for improved clarity and functionality in the column menu.
…pdate HighTableContext to provide a default portal target. Simplify TableHeader by removing unnecessary hasHiddenColumns logic.
…y sorting functionality by replacing legacy props with a single onSort prop. Simplify event handling for sorting actions and enhance clarity in column visibility management.
…tions. Remove legacy sorting props and ensure correct event handling for sort and clear sort functionalities.
…ing padding values and adding a ref prop for improved table reference handling.
…abIndex of the ColumnMenuButton to -1 for improved accessibility. Reorder ColumnMenuButton rendering in ColumnHeader for better structure.
…ity by renaming TableProps to TableHeaderProps and removing unnecessary comments. Adjust import statements for consistency.
@rembrandtreyes
Copy link
Author

Ok, I was able to merge in latest master and work through the conflicts 😅. I think I have everything included. I tested what the UX is in master and compared it to my branch and everything seems at parity for keyboard nav. At least the unit tests are happy 😄

I did not include the ColumnMenu in the keyboard nav because I think that can be its own issue/PR.

Just combing through my PR now and making sure everything is aligned.

…nnecessary re-renders, enhancing performance through prop comparison.
@rembrandtreyes
Copy link
Author

Damn I keep forgetting to check npm run typecheck fixing in a bit.

@severo
Copy link
Contributor

severo commented May 16, 2025

It's ready for review, right? Looking at it

…dren prop to prevent unnecessary re-renders.
@rembrandtreyes
Copy link
Author

Yeah its ready now

@severo
Copy link
Contributor

severo commented May 16, 2025

After closing the menu clicking outside, the table is stuck:

Screencast.From.2025-05-16.10-11-15.mp4

Somewhat related: we should be able to close the menu by clicking the "..." button. <= already reported above, I can take care of this comment later. But the other comment (table is stuck) is a bug we have to fix here.

@severo
Copy link
Contributor

severo commented May 16, 2025

We should always give a bit more space when a column is sortable (like in master). Otherwise, the sort arrow is hidden by default (note that it appears when clicking autoresize) <= edit: I already mentioned above that I can fix it later. So feel free to ignore in this PR.

Screencast.From.2025-05-16.10-15-23.mp4

@rembrandtreyes
Copy link
Author

rembrandtreyes commented May 16, 2025

@severo just saw your update 😆 I just called autoresize() when we cycle through sort states. What do you think?

autoresize.mov

@severo
Copy link
Contributor

severo commented May 16, 2025

Hmmm, no, I think sorting and resizing should be independent. We persist the column sizes in local storage, and we don't want to autoresize when sorting, it's not related. We should leave enough space in CSS instead.

@rembrandtreyes
Copy link
Author

@severo I just checked master and I am seeing the same issue reported about clicking outside the columns in the whitespace. It appears to be selecting the table scroll container? Only way I can "escape" the lock is tabbing to the next item.

@severo
Copy link
Contributor

severo commented May 16, 2025

Indeed, I reproduce incoherencies in master. Thanks for checking, I'll take care of it in another PR.

edit: I created #167

Copy link
Contributor

@severo severo left a comment

Choose a reason for hiding this comment

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

I think we have several big features in this PR, and it's slowing us down.

Are you OK with opening new PRs instead, so we can focus on each one?

  • Feature: column menu #129 - create a menu element with no new action (the only action would be: toggle column sort, which is a bit silly, but would allow to see if it works as expected). We would ensure the keyboard works as expected and the portal is working well.
  • add actions related to sorting (sort ascending, descending, remove from sort) to the menu. Taking care of handling the sort in sort.ts
  • Feature: hide columns #3 - add the feature to hide columns.
  • Use new icons for sort? #136 - change the sort icons

Sorry for asking for that now, I should have asked before, to save time for everyone. But I think it will help a lot with the integration. Again: thanks for your time, the features are super useful!

@@ -569,6 +616,8 @@ export function HighTableInner({
</tbody>
</table>
</div>
{/* puts a background behind the row labels column */}
<div className={styles.mockRowLabel} style={cornerStyle}>&nbsp;</div>
Copy link
Contributor

Choose a reason for hiding this comment

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

it seems to be duplicated code (see two lines below)

const getOnOrderByClick = useCallback((columnHeader: string) => {
if (!onOrderByChange || !orderBy) return undefined
return () => {
onOrderByChange(toggleColumn(columnHeader, orderBy))
Copy link
Contributor

Choose a reason for hiding this comment

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

This comment should be fixed.

@rembrandtreyes
Copy link
Author

Alright I just pumped out #174 I'll close this PR in favor of the three we discussed @severo. Thanks again for taking the time to review!

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