Page MenuHomePhabricator

Table: Investigate open implementation questions
Closed, ResolvedPublic5 Estimated Story Points

Description

Summary

There are several open questions blocking the completion of the design of the Table component. Answering these questions will require research and prototyping on the implementation end. This task covers investigating and answering these questions so design can move forward and so we have a better idea of how the Table component will be built.

Resolved questions

1. Column and table width

  1. What happens when the table is too wide to fit into the viewport?
    1. Once columns have become as narrow as possible (determined by the browser's table sizing algorithm), the entire table will overflow its container and will be visible via horizontal scroll
  2. How will the width of individual columns be determined? Is this configurable?
    1. By default, tables will use the auto table-layout, which sizes columns according to the table sizing algorithm
    2. If desired, users can pass in width and/or minWidth properties for columns. If they do, the fixed table-layout will be used and the styles will be set according to the props. See demos: width, min-width

2. Cell content

  1. Should we have a single slot for cells, or multiple slots?
    1. A single dynamic, scoped slot, as demonstrated here. Multiple slots are unnecessary, overly complicated, and confusing.
  2. How will data/content be passed into the table component? Props, slots, subcomponents, etc? How do we cover a simple use case with text-based data while enabling a complex use case with multiple components per cell?
    1. The Table component will have a prop called columns, an array of column definitions (id, label, sortable, width, minWidth)
    2. Another prop, data will be an array of row data. Each row property corresponds to a column ID
    3. You can pass in these 2 props and the Table component will output everything in a table
    4. You can customize the display of cells via the dynamic, named slots
    5. You can pass in columns then use the default slot for the table body
    6. You can omit both columns and data and pass in all table sub-elements
    7. We'll highlight the props + item slots path as the default/ideal for most cases

3. Pagination

  1. A general Pagination component is not being designed right now - should pagination live in the Table component, or should we create an internal Pagination component that could later be generalized and made public?
    1. For ease, we could create an internal Pagination component that will be used in the Table component but nowhere else, and not exported for use. This could eventually be generalized and exported for use.
  2. How should pagination look across languages? In RTL contexts?
    1. Previous/next will flip for RTL.
    2. All strings will need to be translatable, and most of them will require the use of arguments, e.g. $1-$2 of $3 items. We should make these strings as simple as possible to reduce the risk of human error.
  3. How will pagination work on the implementation end? Will the Table component handle showing the right items, or will it require the parent component to respond to user action and provide only the rows that should be displayed in the proper order?
    1. We can handle this in the Table component. We'll need props for page (current page) and itemsPerPage. Since the user can control both of these, we should probably bind them via v-model so they can be updated both ways. The table component can then determine which rows to show. In cases where the data prop is not used, the parent component can respond to events and provide the correct items to show.
    2. This will work when all rows are passed in at once via the data prop - we will also need to consider how to support tables where only the current page's rows are provided, and other pages' rows would need to be fetched via an API.
    3. We will also need to enable users to build their own pagination in the table footer if they wish to

4. Sorting

  1. How will sorting work on the implementation end? Will the Table component provide handling for sorting, or only emit events to alert the parent component that the user has triggered a sort?
    1. For MVP, we will require the parent component to handle the actual sorting behavior. In the future, we may add an option to use sorting functionality built into the Table component (but the option to handle sorting yourself must still be available)
  2. Will sorting by multiple columns be supported?
    1. Yes, this can be supported by allowing the sort prop to be an array of objects (see the Vuetify implementation)

5. Accessibility

  1. Should a caption be required?
    1. Yes, but it can be visually hidden
  2. How should the "select all" checkbox for row selection work? E.g. when it's selected, then a row is unselected, should the "select all" box be indeterminate?
    1. It should be indeterminate, according to our Checkbox guidelines

Event Timeline

AnneT set the point value for this task to 5.Feb 15 2024, 4:55 PM
AnneT moved this task from Inbox to Up Next on the Design-System-Team board.
AnneT added subscribers: mwilliams, DTorsani-WMF.

On 3.1. I'd strongly suggest taking a path that will enable us to generalize pagination later. I see more UX commonalities than differences in table and other pagination.
Just passed by a detailed article on pagination design in general that captures a number of thoughts that went into existing paginations and might be helpful here again.

On 4.1.a. we should require a caption, but make it AT/screenreader only visible through a prop.

CCiufo-WMF triaged this task as Medium priority.Feb 20 2024, 3:51 PM
CCiufo-WMF raised the priority of this task from Medium to High.Feb 20 2024, 3:55 PM

Change 1005854 had a related patch set uploaded (by Anne Tomasevich; author: Anne Tomasevich):

[design/codex@main] [prototype] WIP Table

https://gerrit.wikimedia.org/r/1005854

I've been exploring options for table and column width and have some suggestions. I've built a prototype with few demos to aid in the discussion.

Column width

Default behavior

The natural behavior of the HTML table is to use an algorithm to size columns according to their content and the available width for the whole table. I strongly suggest this be the default behavior of our table component. We don't want to make assumptions about what kind of content might be in the table, and I think it's better to default to the natural behavior, then give easy options for customization, which I think is very doable. The other option is that we default to making all columns equal width.

Note that a fixed table layout, which sets all columns to an equal width by default and enables you to set the widths of each column, is more performant than the auto layout described above. I'm not sure how much more, though, and if performance is a concern for a particular table, the dev user can simply add table-layout: fixed.

Customization

It's easy to customize column widths with CSS in the following ways:

  • Set table-layout: fixed on the table element. This enables setting the width of the cells in the first row, which the rest of the rows will respect.
  • Set the width of each th element. You can do this in percentages (demo)
  • Optionally set min-width on some or all th elements. This can prevent cells, especially those expected to have a lot of content, from becoming too small. Note that this takes precedent over any set widths. (demo)

That said, we could also fairly easily build support for column width and min-width into the Table component. In my exploration, I've determined a need for a columns prop that provides at least the following data for each column:

  1. An ID (this is necessary to ensure that row data is presented in the right order, among other things)
  2. A label (some component libraries derive this from the machine-readable ID, but we can't do that since we need labels to be translatable)

We could add any number of additional properties for column definitions, including width and minWidth. If those properties are provided, we could set table-layout: fixed on the table element and the other properties on the appropriate th elements via nth-child. I recommend we do this.

Min-width

I don't think we should set a min-width for columns, or if we do, it should be very small, like 50px or less. Even 100px made some columns overly large in my testing, so we should only set a min-width to prevent a column so narrow that it would be distracting or make the whole table look off.

Overall table width + scroll

We want to ensure a couple of things:

  • Tables are at least 100% the width of their parent container
  • Tables can overflow their container, becoming horizontally scrollable

If we wanted the entire table to do this, we could:

  • Add a wrapper div around the table, set overflow-x to auto
  • Set the min-width of the table element to 100%

With these styles, the table becomes horizontally scrollable when the algorithm determines that none of the columns can shrink any further, and the table needs to be wider than its container. This happens automatically, without the dev user needing to "turn on" scroll.

However, the specs define the scroll behavior as applying to the thead and tbody elements, but not the caption or tfoot. This could be extremely difficult to achieve while maintaining all other necessary table layout behavior and adherence to web accessibility standards.

We can't set overflow rules on the thead and tbody elements without also setting display: block, removing table layout behavior. We also can't wrap the thead and tbody in a div that has an overflow rule for the same reason - it breaks the natural layout.

The only thing I can think of is:

  • Make the <caption> visually-hidden, but accessible to assistive tech
  • Also render the caption text in an element that's visible but hidden to assistive tech, and placed outside the <table> element. This will always have 100% width
  • Wrap the <table> element in a div with overflow-x: auto
  • Don't use <tfoot> for the footer, instead use an element outside of <table>
  • We'd probably also need a wrapper <div> around all of this

The big problems with this plan:

  • You lose the semantic <tfoot>, which could hurt accessibility
  • It makes the markup MUCH more complex, which is especially problematic for the CSS-only version

It's much, much easier and more accessible to make the entire table scrollable. For this reason, I think we should consider making the whole table scrollable instead of just the thead and tbody. Check out a couple examples of tables with horizontal scroll.

Early notes on data flow; more on this next week.

There are a few ways we could enable users to pass in table data:

  1. By using normal table sub-elements, like thead, th, tr, etc. in the default slot
  2. By passing in an array of data, which gets output wrapped in appropriate table elements
  3. Same as 2, but also using slots to customize the layout of certain cells

I think we should definitely support 2 and 3, and I think we can fairly easily support 1 too. The details of slot implementation for 3 need to be ironed out, but there are a few ways we could approach this and I think it's quite doable.

Early notes on data flow; more on this next week.

There are a few ways we could enable users to pass in table data:

  1. By using normal table sub-elements, like thead, th, tr, etc. in the default slot
  2. By passing in an array of data, which gets output wrapped in appropriate table elements
  3. Same as 2, but also using slots to customize the layout of certain cells

I think we should definitely support 2 and 3, and I think we can fairly easily support 1 too. The details of slot implementation for 3 need to be ironed out, but there are a few ways we could approach this and I think it's quite doable.

Would we need 1 to support a CSS-only version of the Table component? 2 and 3 make sense for the Vue version.

Early notes on data flow; more on this next week.

There are a few ways we could enable users to pass in table data:

  1. By using normal table sub-elements, like thead, th, tr, etc. in the default slot
  2. By passing in an array of data, which gets output wrapped in appropriate table elements
  3. Same as 2, but also using slots to customize the layout of certain cells

I think we should definitely support 2 and 3, and I think we can fairly easily support 1 too. The details of slot implementation for 3 need to be ironed out, but there are a few ways we could approach this and I think it's quite doable.

Would we need 1 to support a CSS-only version of the Table component? 2 and 3 make sense for the Vue version.

Good question - that's how it will work in the CSS-only version regardless, and how the Vue component will output the markup. The question is: should we also allow users to pass in table sub-elements to the Vue component?

I think the answers is yes, and it should "just work" if we create a default slot in the table component for such sub-elements. I do think this is the lowest-priority input method to support in the Vue version, though, so if we run into major barriers we could forego it. But I've been testing it in my prototype and it works so far.

Passing in table data

I've explored a few different ways for users to pass in table data:

Default: using props

Data is passed in using 2 props:

  1. columns: an array of column definitions. A column must have an id and a label. Other data that could be included in the future: width, minWidth, sortable, and probably more.
  2. data: an array of rows. Each row is an object keyed on column ID. Each value can be a single string/number, or an object of data that will be output via a custom template in a slot (more on this below).

This method should be the default because it's the most succinct way of passing data into the Table component for display - you just need properly formatted objects, no extra markup or components needed.

Customizing cell content

We can use dynamic, scoped slots to enable users to customize the display of table cells. Cell content can be customized per-column, or even for a single cell in a column.

<!-- in Table.vue -->
<tr v-for="( row, rowIndex ) in data" :key="rowIndex">
    <td v-for="( item, columnId ) in row" :key="rowIndex + '-' + columnId">
        <!--
            @slot Table cell content, per column.
            @binding item Data for the cell
        -->
        <slot :name="'item-' + columnId" :item="item">
            {{ item }}
        </slot>
    </td>
</tr>

To override the template for each cell in a column, you can do this (or see this demo)—note that this table has columns with ids name and status:

<cdx-table
    caption="Table using item slots"
    :columns="columns"
    :data="data"
>
    <template #item-name="{ item }">
        <a :href="item.url">{{ item.label }}</a>
    </template>
    <template #item-status="{ item }">
        <cdx-info-chip :status="getChipStatus( item )">
            {{ item }}
        </cdx-info-chip>
    </template>
</cdx-table>

You could also override a single cell by using v-if on the <template>.

Use the default slot to provide the table body

This strategy involves using the columns prop to define the columns, which will enable you to continue using features like setting the width/min-width or making the column sortable, while using the Table's default slot to provide the table body. This could be useful if you need to do table formatting that's unsupported in the Table component, like making cells span multiple rows or columns. Alternately, we could try to find a way to build that functionality into the Table component, but I think that would be really tricky and might not cover all use cases. See the demo of this technique.

Use the default slot to provide all table content

Finally, you can forego use of either prop, and just provide everything inside the <table> element via the default slot. This could be useful if you want to do a lot of custom formatting, including in the <thead> or using the <colgroup> element. Alternately, we could try to support this in Table, but for the same reasons as above, I'm not sure this is worth it. Here's a demo. Note that this demo doesn't look quite right since there are no vertical borders within the table - something we should think about from a design standpoint (cc @Sarai-WMDE).


Open questions

  1. Should we support all 3 of these techniques? I think we should - this will give people maximum flexibility. I think we should highlight the happy path, which is using the props and dynamic slots.
  2. Are there any other techniques we could/should offer? Some component libraries provide sub-components like TableRow or TableColumn, but I think this is more complicated and less user-friendly than the dynamic slot technique.
  3. Should we support complex formatting, like spanning multiple columns/rows? I'm not sure how common this is. I also think it would be hard to seamlessly support all the possible combinations users could want, so I think it's probably better to just give people the default slot to use if they need it.
  4. Do we need better styles when cells span multiple columns/rows? Per[[ https://1005854--wikimedia-codex.netlify.app/components/demos/table.html#customizing-all-table-content | this demo ]], it looks a bit funky right now. Users can certainly add their own styles, but I think we should consider building it in to prevent inconsistencies.
  5. Should we provide even more granular slots for items? The design inventory identified cells that have some combination of component + text + component. We could provide slots for all 3, but I think we should just provide a single slot and require dev users to style custom cell content. We can provide guidelines for this, but I don't think it should be built in because it could end up being confusing/overly complicated.

Pagination

In the open questions I asked if pagination and sorting logic (i.e. determining which items to show and in what order) should be supported in Table, but I see now that they are totally separate issues. Pagination logic should be relatively easy to support in Table.

Vuetify accomplishes this by enabling the parent component to pass in 2 props:

  • page, passed in via v-model. This is the current page
  • itemsPerPage

Using v-model for the page enables the Table component to control the page in response to UI interactions, while allowing the parent component to tell the Table component which page to start on if needed. Aside from that, it's just a matter of calculating which items to show, how many pages there are, etc.

Sorting

Supporting sort inside the Table component, in a way that would work across all languages, would be extraordinarily difficult. We should consider whether there is a reliable way to do this (like a library we could use) and how sort is handled in existing tables in the Wikiverse. Ultimately, we can do one of these options:

  1. Fully support sorting in the Table component (across languages, multisort, etc.)
  2. Support some simple sorting with limitations. Users could opt into simple sorting, or just listen for events and handle sorting on their end.
  3. Just provide the sort icons and emit events when they're clicked. We could provide guidelines on how people should handle sorting on their end.

I need to do some more research, but my instinct is that we should go for 3 for MVP. Providing robust, built-in support for sorting that works across all users/tables could be incredibly difficult to both build and maintain, and it might be more appropriate to have something inside MediaWiki to handle this (more on that below).

Sorting in the wikiverse

I've found a couple of examples of table sorting in MediaWiki:

jQuery tablesorter

In MediaWiki there's a ResourceLoader module called jquery.tablesorter that can be used with tables generated by the TablePager class. jquery.tablesorter.js was written in 2011 and is based on a plugin written in 2007. It has several parsers to handle different types of content (text, IP addresses, currency, dates, time, and numbers). As you might imagine, it's fairly extensive, and writing something similar would be difficult. There may be a JavaScript library we could use, but we might need to adapt it to the wikiverse. jquery.tablesorter depends on a bunch of stuff in MediaWiki: mw.config (wgDigitTransformTable, wgDefaultDateFormat, wgPageContentLanguage) and mw.language.months.

Check out Special:TrackingCategories for an example.

Mentor dashboard

The GrowthExperiments mentor dashboard has a sortable table built with Vue. On sort, a new API call is made to fetch data in the right order. This is a good example of offloading sorting to another part of the ecosystem, rather than handling it in the Table component.

@Catrope do you have any experience with table sorting in MediaWiki? Curious if you have any thoughts on my last comment.

Re sorting I agree that #3 is the best approach for MVP, and possibly also the best approach long term. I think we should not pursue #1 given how complex it would be. We could do #2, but and we could consider that as a post-MVP thing, and we need to keep the door open for custom sorting regardless, because that's what we'll want in MediaWiki. One possible advantage of #2 is that we could allow the environment to provide() a default sorting function, which would allow for automagic tablesorter support in MediaWiki (meaning, if you put use a <cdx-table> component in a MediaWiki context and make it sortable, it will sort using MediaWiki's advanced sorting logic, without the developer having to do anything to make that happen).

I think one big question though is how we manage the relationship between sorting and the data prop. Here are the options I see (maybe there are others):

  1. If built-in sorting is not provided (or if it is, but the parent component would like to use a custom sorting function),
    1. We could emit an event, and expect the parent component to respond by modifying the data prop to reorder the rows. If the parent component modifies the data prop (e.g. adding new rows), it would be responsible for re-sorting.
    2. We could make the parent component pass in a sorting function as a prop, call that function to obtain the new order of the rows, and then do either 2A or 2B.
  2. If built-in sorting is provided and used:
    1. We could have the Table component modify the data prop. That would be similar to how custom sorting works, but it also means that the data prop needs to be passed with v-model:data. It's not great to require v-model to be used only in some cases, especially since there are legitimate use cases for non-sortable table where a parent component would want to pass in read-only data.
    2. The Table component could maintain a second copy of the data (either as an actual ref or as a computed ref) that reflects the order in which the rows are displayed. The data prop would be unmodified, and would not match the order in which the rows are displayed. If the parent component modifies the data prop, the Table component would be responsible for re-sorting (this could require a watcher, or maybe just a computed ref).

Between 1A and 1B I feel like I should prefer 1A, because emitting an event is much more Vue-like than taking a function as a prop. But between 2A and 2B I like the idea of 2B better, because it's easier for parent components to deal with. That doesn't feel super consistent, but now that I think about it some more, I think it might actually be fine?

Thanks @Catrope, this is super helpful. I agree that 1A makes sense for MVP and we could pursue 2B post-MVP if we want to. 2B does feel better than 2A because it keeps the dev experience consistent whether you're using sorting or not.

  1. How should the "select all" checkbox for row selection work? E.g. when it's selected, then a row is unselected, should the "select all" box be indeterminate?

I think it should be indeterminate based on our definitions in the guidelines.

  1. Rules for inputs inside cells? Labels?

What does this mean?

  1. How should the "select all" checkbox for row selection work? E.g. when it's selected, then a row is unselected, should the "select all" box be indeterminate?

I think it should be indeterminate based on our definitions in the guidelines.

Makes sense and I agree!

  1. Rules for inputs inside cells? Labels?

What does this mean?

Sorry, this is vague - I wrote this after seeing the tables in the Adiutor extension that contain form fields (each row a set of related inputs). I do think we should consider providing guidelines for complex form layouts like this, but as part of the Field component, not table, so I'm removing this from the list.

AnneT updated the task description. (Show Details)

@Sarai-WMDE after discussing the open issues with the team, I've listed answers for all of the open questions in the task description. I think sorting was the biggest question mark after our last meeting, and it does seem like we agree that we shouldn't implement support for sorting logic in the Table component as part of MVP. Please check out the resolutions listed in the task description and let me know if you have further comments!

  1. Pagination

[...]

Aside from that, it's just a matter of calculating which items to show, how many pages there are, etc.

I think this part may not be as simple. There seems to be a hidden assumption here that all the rows are already in the data prop, and you're showing a subset of those rows for visual space economy reasons. Maybe there are use cases for that, but the first thing that comes to my mind when someone says "pagination" is something like a table of all pages on a wiki (of which there can be millions). In that case, pagination involves fetching the next N results from an API and putting them in the data prop. We could have the parent component just keep adding stuff to the data prop, but I'm concerned that that also has limitations if the user paginates many times (accumulating thousands of rows in the data prop) or if it's possible for the table to start in the middle and paginate backwards.

Maybe this is a bit like sorting, where we should offer both built-in pagination for static data, and custom pagination handling for dynamic data?

@Catrope that's a really good point - we will need to offer both options for pagination. I'll update the task description to note this.

Thanks so much again for the thorough investigation, and the clear documentation of the outcomes @AnneT! 🙏🏻

Regarding sorting, as mentioned during our last meeting, I believe it sounds perfectly acceptable (and probably more scalable) to only emit events as part of the MVP. It would be very helpful, as I believe you mentioned, to collect feedback on how this "limitation" is welcomed by reusers of the component. Maybe we could reach out to the Growth team at some point? On the other hand, this is most likely a given, but I hope we are able to showcase the full sorting behavior in the Table's demo page. It would be ideal for clarity.

Just for the record, regarding pagination. The designers and Chris discussed implementing this feature not even as an internal component, but recreating it as a composition of elements to be showcased in a custom version of the table. So, in this option, we'd be using our own footer slots to display how Table pagination s/could look, but not incorporating it as a feature by default. The whole reason behind this option was giving teams full flexibility over what parts of the pagination composition they wanted to include when recreating it, and have total control over the footer's content. At the same time, the documented solution sounds way more complete and convenient for reusers. My interpretation is that all that flexibility is preserved in the current approach too, and that Table users will be able to customize and/or replace pagination elements (e.g. remove the Page range selector and include other content instead). Is that correct? Probably to be discussed in our next sync!

Screenshot 2024-03-01 at 19.48.42.png (334×1 px, 58 KB)

Thanks so much again for the thorough investigation, and the clear documentation of the outcomes @AnneT! 🙏🏻

Regarding sorting, as mentioned during our last meeting, I believe it sounds perfectly acceptable (and probably more scalable) to only emit events as part of the MVP. It would be very helpful, as I believe you mentioned, to collect feedback on how this "limitation" is welcomed by reusers of the component. Maybe we could reach out to the Growth team at some point? On the other hand, this is most likely a given, but I hope we are able to showcase the full sorting behavior in the Table's demo page. It would be ideal for clarity.

Totally agree that we should ask for feedback, and demonstrate at least one implementation of sorting on the demo page - we should probably show people how to do some kind of simple sorting, then give them options for other ways of handling it (e.g. libraries to use, or that if they're using an API to fetch data they can use the sorting functionality of that API).

Just for the record, regarding pagination. The designers and Chris discussed implementing this feature not even as an internal component, but recreating it as a composition of elements to be showcased in a custom version of the table. So, in this option, we'd be using our own footer slots to display how Table pagination s/could look, but not incorporating it as a feature by default. The whole reason behind this option was giving teams full flexibility over what parts of the pagination composition they wanted to include when recreating it, and have total control over the footer's content. At the same time, the documented solution sounds way more complete and convenient for reusers. My interpretation is that all that flexibility is preserved in the current approach too, and that Table users will be able to customize and/or replace pagination elements (e.g. remove the Page range selector and include other content instead). Is that correct? Probably to be discussed in our next sync!

This is interesting - we can definitely support both a built-in, default version of pagination, and give users the freedom to add their own in the footer if they wish. My instinct is that we should offer a built-in version for ease of use and consistency, but this isn't based on talking to Codex users, so it would be good to validate that. Let's discuss this week!

AnneT updated the task description. (Show Details)

I'm resolving this task since we've done the exploration and provided a path forward on all the open questions. Anything that is not completely clear yet can be discussed in subtasks of the Table component for each feature. Thanks to everyone who contributed to this work!

Change 1005854 abandoned by Anne Tomasevich:

[design/codex@main] [prototype] WIP Table

Reason:

Superseded by https://gerrit.wikimedia.org/r/c/design/codex/+/1009620

https://gerrit.wikimedia.org/r/1005854