Skip to content
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

Update mapping column HLD #1992

Merged
merged 22 commits into from
Apr 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
jattasNI marked this conversation as resolved.
Show resolved Hide resolved
mollykreis marked this conversation as resolved.
Show resolved Hide resolved
mollykreis marked this conversation as resolved.
Show resolved Hide resolved
"type": "none",
"comment": "Update mapping column HLD with new plan of supporting icon + text in the same cell",
"packageName": "@ni/nimble-components",
"email": "20542556+mollykreis@users.noreply.github.com",
"dependentChangeType": "none"
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Overview

The `nimble-table-column-enum-text` is a component that supports rendering specific number, boolean, or string values as mapped text. `nimble-table-column-icon` instead maps values to icons and/or spinners. The actual mappings are defined by child elements `nimble-mapping-icon`, `nimble-mapping-spinner`, and `nimble-mapping-text`.
The `nimble-table-column-mapping` is a component that supports rendering specific number, boolean, or string values as an icon, a spinner, text, or an icon/spinner with text. The mappings are defined by child elements of `nimble-mapping-icon`, `nimble-mapping-spinner`, `nimble-mapping-text`, or `nimble-mapping-empty`.

### Background

Expand All @@ -20,15 +20,18 @@ The `nimble-table-column-enum-text` is a component that supports rendering speci
- Text
- Icon
- Spinner
- Icon with text
- Spinner with text
- (empty)

### Non-goals

- Non-Nimble icon support
- Arbitrary icon colors
- Hyperlink icons
- Mixed text and icons
- Non-icon, non-spinner Nimble components
- Specifying different text for an icon/spinner's label than the overall text of the mapping
- Detecting that only icons/spinners can be rendered in the cells and column header and automatically making the column non-resizable and 32px

---

Expand All @@ -38,29 +41,30 @@ Below is an example of how these elements would be used within a `nimble-table`:

```HTML
<nimble-table>
<nimble-table-column-icon field-name="status" key-type="string">
<nimble-table-column-mapping field-name="status" key-type="string">
Status
<nimble-mapping-icon key="fail" icon="nimble-icon-xmark" severity="error" label="Failed"></nimble-mapping-icon>
<nimble-mapping-icon key="error" icon="nimble-icon-xmark" severity="error" label="Errored"></nimble-mapping-icon>
<nimble-mapping-icon key="pass" icon="nimble-icon-check" severity="success" label="Passed"></nimble-mapping-icon>
<nimble-mapping-spinner key="running" label="Running"></nimble-mapping-spinner>
</nimble-table-column-icon>
<nimble-table-column-enum-text field-name="errorCode" key-type="number">
<nimble-mapping-icon key="fail" icon="nimble-icon-xmark" severity="error" text="Failed"></nimble-mapping-icon>
<nimble-mapping-icon key="error" icon="nimble-icon-xmark" severity="error" text="Errored"></nimble-mapping-icon>
<nimble-mapping-icon key="pass" icon="nimble-icon-check" severity="success" text="Passed"></nimble-mapping-icon>
<nimble-mapping-spinner key="running" text="Running"></nimble-mapping-spinner>
</nimble-table-column-mapping>
<nimble-table-column-mapping field-name="errorCode" key-type="number">
Error Code
<nimble-mapping-text key="1" label="A bad thing happened"></nimble-mapping-text>
<nimble-mapping-text key="2" label="A worse thing happened"></nimble-mapping-text>
<nimble-mapping-text key="3" label="A terrible thing happened"></nimble-mapping-text>
</nimble-table-column-enum-text>
<nimble-table-column-icon field-name="archived" key-type="boolean">
<nimble-mapping-text key="1" text="A bad thing happened"></nimble-mapping-text>
<nimble-mapping-text key="2" text="A worse thing happened"></nimble-mapping-text>
<nimble-mapping-text key="3" text="A terrible thing happened"></nimble-mapping-text>
</nimble-table-column-mapping>
<nimble-table-column-mapping field-name="archived" key-type="boolean">
Archived
<nimble-mapping-icon key="true" icon="nimble-icon-database" label="Archived"></nimble-mapping-icon>
</nimble-table-column-icon>
<nimble-mapping-icon key="true" icon="nimble-icon-database" text="Archived" text-hidden></nimble-mapping-icon>
<nimble-mapping-empty key="false" text="Not archived"></nimble-mapping-empty>
</nimble-table-column-mapping>
</nimble-table>
```

Each column contains mapping elements that define what to render when the cell's value matches the given `key` value.

When none of the given mappings match the record value for a cell, that cell will be empty.
When none of the given mappings match the record value for a cell, that cell will be empty. While the table will not enter an error state, this is considered invalid data from the table's perspective and should be fixed within the client application.

The column will translate its contained mapping elements into a map that is stored in the `columnConfig`.

Expand All @@ -70,8 +74,6 @@ If multiple mappings in a column have the same key, an error flag will be set on

If an invalid `icon` value is passed to `nimble-mapping-icon`, an error flag will be set on the column's validity object. An invalid `icon` value is any element that cannot be resolved or that does not derive from `Icon`.

`nimble-table-column-icon` supports only `nimble-mapping-icon` and `nimble-mapping-spinner` as mapping elements. `nimble-table-column-enum-text` supports only `nimble-mapping-text`. Unsupported mappings will result in an error flag being set on the column's validity object.

Text in a grouping header or in the cell will be ellipsized and gain a tooltip when the full text is too long to display.

**Alternatives:**
Expand All @@ -92,106 +94,92 @@ Cons:

### API

#### Icon column element:
#### Enum column element:

_Component Name_

- `nimble-table-column-icon`

_Props/Attrs_

- `field-name`: string
- `key-type`: 'string' | 'number' | 'boolean'

_Content_

- column title (icon)
- 1 or more `nimble-mapping-icon` or `nimble-mapping-spinner` elements

#### General mapping column element:

_Component Name_

- `nimble-table-column-enum-text`
- `nimble-table-column-mapping`

_Props/Attrs_

- `field-name`: string
- `key-type`: 'string' | 'number' | 'boolean'
- `width-mode`: enum - `default` (`undefined`) | `iconSize` (`'icon-size'`) - When set to `iconSize`, the column will have a fixed width that makes the column the appropriate width to render only a single icon in the cell. This should only be set when the header contains a single icon (no text) and none of the child mapping elements will result in text being rendered in a cell. When unset or set to `default`, the column will be resizable and be sized based on its `fractional-width` and `min-pixel-width` values.
- `fractional-width`: number (defaults to 1)
- `min-pixel-width`: number (defaults to minimum supported by table)

_Content_

- column title (text or icon)
- 1 or more `nimble-mapping-text` elements
- column title (icon and/or text)
- 1 or more `nimble-mapping-icon`, `nimble-mapping-spinner`, `nimble-mapping-text`, or `nimble-mapping-empty` elements

#### Mapping element (icon):

The icon mapping element will support displaying an icon, icon with text, or only text in a cell. If only text is displayed in a cell, space will be reserved for an icon so that the text associated with all icon mapping elements is aligned. A group row associated with an icon mapping element will always display the icon, if specified, and text.
mollykreis marked this conversation as resolved.
Show resolved Hide resolved

_Component Name_

- `nimble-mapping-icon`

_Props/Attrs_

- `key`: string | number | boolean | undefined
- `icon`: string - name of the Nimble icon element
- `icon`: string | undefined - name of the Nimble icon element. If `undefined`, no icon will be associated with the given `key`, but space will be reserved in the cell for an icon.
mollykreis marked this conversation as resolved.
Show resolved Hide resolved
- `severity`: string - one of the supported enum values. Controls color of the icon.
mollykreis marked this conversation as resolved.
Show resolved Hide resolved
- `label`: string - localized value used as the accessible name and `title` of the icon. Will also be displayed in the group header.
- `text`: string - localized value associated with the given `key`.
- `text-hidden`: boolean - When set, the mapping's text will not be rendered within a cell. When unset, the text will be rendered in a cell. This does not affect the rendering of group rows; group rows will always display the text associated with the mapping.

The text will be used in the following places:

- In the group row for a mapping
- If `text-hidden` is set, as the accessible name and `title` of the icon within a cell
- If `text-hidden` is not set, as text rendered next to the icon within a cell

#### Mapping element (spinner):

The spinner mapping element will support displaying a spinner or spinner with text in a cell. A group row associated with a spinner mapping element will always display the spinner and text.

_Component Name_

- `nimble-mapping-spinner`

_Props/Attrs_

- `key`: string | number | boolean | undefined
- `label`: string - localized value used as the accessible name and `title` of the spinner. Will also be displayed in the group header.
- `text`: string - localized value associated with the given `key`.
- `text-hidden`: boolean - When set, the mapping's text will not be rendered within a cell. When unset, the text will be rendered in a cell. This does not affect the rendering of group rows; group rows will always display the text associated with the mapping.

The text will be used in the following places:

- In the group row for a mapping
- If `text-hidden` is set, as the accessible name and `title` of the spinner within a cell
- If `text-hidden` is not set, as text rendered next to the spinner within a cell

#### Mapping element (text):

The text mapping element will support displaying text in a cell. A group row associated with a text mapping will also display the text associated with the mapping.

_Component Name_

- `nimble-mapping-text`

_Props/Attrs_

- `key`: string | number | boolean | undefined
- `label`: string - display text
- `text`: string - display text

### Anatomy
#### Mapping element (empty):

#### `nimble-table-column-enum-text`
The empty mapping element will display an empty cell. A group row associated with an empty mapping will display the mapping's text. The purpose of the empty mapping element is to allow clients to avoid cluttering their table with information that isn't particularly helpful to a user (e.g. that the state of a system is 'idle') while still having a good grouping experience that ensures group rows are not empty.

```HTML
<template slot="${x => x.columnInternals.uniqueId}">
<slot
${slotted('mappings')}
name="mapping">
</slot>
<span class="header-content">
<slot></slot>
</span>
</template>
```

Cell view:

The cell view relies on the matched mapping to provide a template to render.

```HTML
html<TableColumnMappingCellView>`${x => x.getMappingToRender().cellViewTemplate}`
```
_Component Name_

Group header view:
- `nimble-mapping-empty`

Similarly, the group header view relies on the matched mapping to provide a template to render.
_Props/Attrs_

```HTML
html<TableColumnMappingHeaderView>`${x => x.getMappingToRender().groupHeaderViewTemplate}`
```
- `key`: string | number | boolean | undefined
- `text`: string - display text

#### `nimble-mapping-*`

Expand All @@ -205,38 +193,34 @@ html<TableColumnMappingHeaderView>`${x => x.getMappingToRender().groupHeaderView

```HTML
<${this.icon}
title="${x => x.label}"
aria-label="${x => x.label}"
title="${x => x.textHidden ? x.text: null}"
role="img"
aria-label="${x => x.textHidden ? x.text: null}"
aria-hidden="${x => x.textHidden ? 'false' : 'true'}"
severity="${x => x.severity}">
</${this.icon}>
${when(() => !x.textHidden, html`
<span>${x => x.text}</span>
`)}
```

`mapping.groupHeaderViewTemplate`:

```HTML
<${this.icon}
title="${x => x.label}"
aria-label="${x => x.label}"
aria-hidden="true"
severity="${x => x.severity}">
</${this.icon}>
<span
${ref('span')}
@mouseover="${x => {
x.isValidContentAndHasOverflow = !!x.label && x.span!.offsetWidth < x.span!.scrollWidth;
}}"
@mouseout="${x => {
x.isValidContentAndHasOverflow = false;
}}"
title=${x => (x.isValidContentAndHasOverflow ? x.label : null)}>
${x => x.label}
<span>
mollykreis marked this conversation as resolved.
Show resolved Hide resolved
${x => x.text}
</span>`;
```

### Grouping

Grouping will be based on the record value. The grouping header will display the rendered icon/spinner/text. In the case of an icon/spinner, it will also be followed by the `label` text. This will disambiguate cases where multiple record values map to the same icon (assuming the labels are different).
Grouping will be based on the record value. The grouping header will display the rendered text along with the icon/spinner, if configured.

For values that do not match any mapping, we will display the raw data value. While this introduces inconsistency, it seems preferable to the alternative, which is having multiple, separate groupings with a blank header (well, with just the item count in parens). Even in the case where there is a default mapping, we would still end up with separate groups with the identical default mapped icon and/or text, which is just as bad.
For values that do not match any mapping, the group row will display only the count. This is considered invalid data from the table's perspective and should be fixed within the client application.
mollykreis marked this conversation as resolved.
Show resolved Hide resolved

Text in a grouping header will be ellipsized and gain a tooltip if there is not enough room to display it all.

Expand All @@ -245,9 +229,9 @@ Text in a grouping header will be ellipsized and gain a tooltip if there is not
Sorting will be based on the record value. For boolean and number values, a basic sort (just using basic comparison/equality operators) is the clear choice. For string values, it is less clear. In the case where the strings are enum values, they are likely to be non-localized, English strings. Using a basic (alphabetical) sort is not unreasonable. However, if there is a semantically meaningful sort order (e.g. "NOT_STARTED" < "RUNNING" < "DONE"), it would be nice to sort by that. We can only infer that semantic order from the order in which the mappings are given, e.g.:

```HTML
<nimble-mapping-text key="NOT_STARTED" label="Not Started"></nimble-mapping-text>
<nimble-mapping-text key="RUNNING" label="Running"></nimble-mapping-text>
<nimble-mapping-text key="DONE" label="Done"></nimble-mapping-text>
<nimble-mapping-text key="NOT_STARTED" text="Not Started"></nimble-mapping-text>
<nimble-mapping-text key="RUNNING" text="Running"></nimble-mapping-text>
<nimble-mapping-text key="DONE" text="Done"></nimble-mapping-text>
```

We would need new support for sorting this way. We could define a new sorting option, "enumerated sort", where the column provides an ordered list of values, and the table sorts the column based on that given order. To compare the relative order of two values, we have to search the list, making the sort operation a bit more expensive, but still probably reasonable except in the case of enums with many values.
Expand All @@ -258,25 +242,20 @@ For icons, if multiple values map to the same icon, it is possible that sorting

### Sizing

`nimble-table-column-icon` will be a fixed pixel size (32px) and will not be resizable. The 32px fixed size allows room from a single icon along with left and right cell padding of 8px each.
By default, the `nimble-table-column-mapping` will be a fractional width column with a fractional width of 1. However, it can be configured to be a fixed pixel size (32px) and not be resizable by setting `width-mode` to `iconSize`. The 32px fixed size allows room from a single icon or spinner along with left and right cell padding of 8px each.

This has the following implications:
When the column is a fixed 32px:

- The grouping indicator and sorting indicator will always be hidden on the icon column.
- The grouping indicator and sorting indicator will be hidden in the column header.
- A client is expected to only place an icon as the header content of an icon column.
- A user cannot resize an icon column.
- There will be no public API exposed on the icon column related to sizing. Unlike other columns today, the icon column will not have attributes for `min-pixel-width` or `fractional-width`.
- Column sizing configuration on the column, such as `fractional-width`, will be ignored.

This will be accomplished through the following configuration on the column:

- The icon column will not use the `mixinFractionalWidthColumnAPI` mixin because it will not expose a sizing API.
- The icon column will set `columnInternals.resizingDisabled` to `true`.
- The icon column will set both `columnInternals.pixelWidth` and `columnInternals.minPixelWidth` to `32`, which is equal to the icon size plus left and right paddings of 8px

In the future, we can consider adding an API to allow the icon column to have its size configured by a user and/or client, but that is currently out of scope.

`nimble-table-column-enum-text` will support fixed or fractional widths. If `pixel-width` is set, the column will have a fixed width, otherwise it defaults to a fractional width of 1. The client may configure `fractional-width` and/or `min-pixel-width`.

### Angular integration

Angular directives will be created for the column components and the mapping components. No component has form association, so a `ControlValueAccessor` will not be created.
Expand All @@ -286,10 +265,10 @@ Angular directives will be created for the column components and the mapping com
Blazor wrappers will be created for the components. Columns will be generic in the type of the key, and will cascade that type parameter to contained mapping elements (see [`CascadingTypeParameter`](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/generic-type-support?view=aspnetcore-7.0#cascaded-generic-type-support)):

```HTML
<NimbleTableColumnEnumText TKey=int Field="NumberData">
<NimbleMappingText Key="1" Label="foo"></NimbleMappingText>
<NimbleMappingText Key="2" Label="bar"></NimbleMappingText>
</NimbleTableColumnEnumText>
<NimbleTableColumnMapping TKey=int Field="NumberData">
<NimbleMappingText Key="1" Text="foo"></NimbleMappingText>
<NimbleMappingText Key="2" Text="bar"></NimbleMappingText>
</NimbleTableColumnMapping>
```

### Visual Appearance
Expand All @@ -306,7 +285,9 @@ N/A

### Accessibility

Text, icons, and spinner are not interactive and cannot receive keyboard focus. These items already have proper accessibility roles, and we will set accessible names (`aria-label`) based on the `label` value provided by the client.
- Text, icons, and spinner are not interactive and cannot receive keyboard focus.
- In group rows, the rendered icon/spinner will have `aria-hidden="true"` set because the text is displayed directly next to the icon. The icon/spinner is purely decorative, and it does not contain any additional information that needs to be available with a screen reader.
- In table cells, if an icon/spinner is displayed with the text, the icon/spinner will have `aria-hidden="true"` set for the same reason explained for group rows above. However, if the text is not displayed, the icon/spinner will have a role of `img` and use the text as its `title` and `aria-label`.

### Globalization

Expand All @@ -330,11 +311,8 @@ None
- renders mapping matching the cell value (string, number, and boolean)
- nothing rendered when value matches no mappings
- validation error when non-unique mapping keys exist
- validation error when multiple mappings marked as default
- validation error when mapping key is null and not marked default
- validation error when invalid icon name given
- validation error when icon column has a `nimble-mapping-text` element
- grouping header for icon column includes label
- grouping header for icon column includes text
- Verify manually that the column content appears in the accessibility tree and can be read by a screen reader.
- Verify manually that several mapping columns with thousands of elements scrolls performantly.
- Visual Chromatic tests will be created
Expand Down
Loading