diff --git a/change/@ni-nimble-components-e43548e1-3b16-4f3a-8d46-42769f66627d.json b/change/@ni-nimble-components-e43548e1-3b16-4f3a-8d46-42769f66627d.json new file mode 100644 index 0000000000..48ab202d5e --- /dev/null +++ b/change/@ni-nimble-components-e43548e1-3b16-4f3a-8d46-42769f66627d.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "HLD updates for number and date table columns", + "packageName": "@ni/nimble-components", + "email": "jattasNI@users.noreply.github.com", + "dependentChangeType": "none" +} diff --git a/packages/nimble-components/src/table/specs/table-column-specs/table-column-formatted-text.md b/packages/nimble-components/src/table/specs/table-column-specs/table-column-formatted-text.md index 3ec7ab5b03..a777adbbac 100644 --- a/packages/nimble-components/src/table/specs/table-column-specs/table-column-formatted-text.md +++ b/packages/nimble-components/src/table/specs/table-column-specs/table-column-formatted-text.md @@ -43,7 +43,7 @@ We may not choose to support all of the above initially but we should design our - Editable numbers. This is not supported by the text column yet either. - Numeric formatting for `nimble-number-field`. While we may choose to expose similar APIs for its numeric formatting, the complexities of it being an editable input control make this out of scope for now. - Customizing the styling of the column content (other than text alignment). This is not supported by the text column yet either. -- Enum and boolean values formatted as localized strings (0 -> "Fail", 1 -> "Pass"). These will likely use the [mapping column being discussed elsewhere](https://github.com/ni/nimble/pull/1220). +- Enum and boolean values formatted as localized strings (0 -> "Fail", 1 -> "Pass"). These will likely use the [mapping column](./table-column-mapping.md). --- @@ -53,14 +53,12 @@ We may not choose to support all of the above initially but we should design our Nimble will provide base classes that can be derived from to define columns that call a formatting function to render their data as text. -Nimble will provide several columns that derive from these base classes and provide higher level formatting APIs for specific data types. We plan to provide column implementations that can handle the above use cases 1-4 (numeric formatting and static units) in a first pass with 6 (date) coming later. +Nimble will provide several columns that derive from these base classes and provide higher level formatting APIs for specific data types. We plan to provide column implementations that can handle the above use cases 1-4 (numeric formatting and static units) and 6 (date). -Clients which require app-specific formatting logic to support above use cases like 5 (custom unit logic) will define custom columns in their application that derive from these base classes. +Nimble may add additional column types or configurations in future to support additional cases. These cases may be contributed by client teams or include application-specific logic (possibly as "incubating" components). Defining columns in Nimble and configuring their formatting in apps via attributes is preferable to applications defining custom columns with JS formatting logic for several reasons: -We'll strongly encourage clients to prefer columns defined in Nimble as these will be easier to use than custom columns defined in their application: - -- the columns will be configurable via HTML attributes, saving clients from writing JS code (a particular challenge in Blazor) -- they provide strict type validation of the data field +- the columns can be made available to other applications written in any of the UI frameworks that Nimble supports +- the columns can be built using Nimble infrastructure, saving apps from incorporating FAST or JS code (a particular challenge in Blazor) - clients don't need to manage the lifecycle of registering a new column custom element in their application ### Formatted text column base classes @@ -163,73 +161,127 @@ This is prototyped in the [formatted-text-column branch](https://github.com/ni/n Nimble will provide several columns that derive from the above base classes and provide higher level text formatting APIs for specific data types. +All columns will include a `field-name` string attribute to select the field to display and a `placeholder` string attribute to specify the text to display if the field value isn't in the expected format (more details about expected formats for each column type). + +All columns will offer standard Angular and Blazor integration. + #### Column naming -Follows the [Column Type Philosophy](/packages/nimble-components/src/table/specs -/table-columns-hld.md#column-type-philosophy) where: +Follows the [Column Type Philosophy](/packages/nimble-components/src/table/specs/table-columns-hld.md#column-type-philosophy) where: - category: `table-column` - presentation: `text` -- variants: Different variants are allowed for configurations that vary significantly / don't make sense to add to `table-column-text`, ie for a `table-column-numeric-text` or `table-column-date-text` +- variants: Different variants are allowed for configurations that vary significantly / don't make sense to add to `table-column-text`, ie for a `table-column-number-text` or `table-column-date-text` #### Text column `nimble-table-column-text` will continue to present the same API it does today, but will derive from the base classes described above. -#### Numeric column +#### Number column + +Nimble will introduce `nimble-table-column-number-text` which formats a numeric field value and displays it as text. It will offer sufficient configuration to support use cases 1-4 above. + +##### API -Nimble could introduce `nimble-table-column-numeric-text` which formats a numeric field value and displays it as text. It will offer sufficient configuration to support use cases 1-4 above. +- `prefix` - a string which will be appended before each value (e.g. `'$'`). Defaults to `''`. Should only be used if formatting can't be achieved via `format`. +- `suffix` - a string which will be appended after each value (e.g. `'%'` or `' V'`). Defaults to `''`. Should only be used if formatting can't be achieved via `format`. + - Spacing will be at the discretion of clients, but Nimble will recommend including a space before the unit except for symbol units like `%`, `"`, `°`, and `°C`. This [matches the Chicago Manual of Style](https://www.chicagomanualofstyle.org/book/ed17/part2/ch09/psec016.html) (requires VPN) which is NI's documentation style guide. + - If we ever support RTL languages, our [initial research](https://linguistics.stackexchange.com/questions/16865/order-of-components-within-measurement-units-in-rtl-languages) suggests that numbers are still formatted using LTR, so the prefix would still appear on the left and the suffix on the right. +- `alignment` - a string value matching `"left"`, `"right"`, or `undefined` (the default, meaning `"automatic"`) which controls whether values and column headers are left or right aligned within the column. If set to `undefined` Nimble will choose left or right based on the value of `format`. Clients should select `right` if it is known that the decimal separators of all values in the column will align in the given the `format`. +- `format` - a string which controls how the number is formatted for display. It can take one of the following values: + - `undefined` - use a default formatter, which will format similarly to [`Number.toString()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toString#description). This displays integers with no trailing zeros, limits to about 16 significant digits, and switches to exponential notation for very large and small numbers. Instead of using `toString()` it will be implemented using `Intl.NumberFormat` to achieve more consistent i18n (`toString` always uses a `.` separator and displays the English word for "Infinity"). Will be displayed left-aligned by default (since numbers will display an inconsistent number of fractional digits). + - `'integer'` - format all values as integers, rounding to nearest if the value isn't an integer and never displaying exponential notation. Will be displayed right-aligned by default. + - `'decimal'` - format all values as decimal values (e.g. 123.45), always displaying `decimal-digits` digits after the separator and never displaying exponential notation. Will be displayed right-aligned by default. + - `'custom'` - use [`Intl.NumberFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat) configured with values specified in other attributes. Will be displayed left-aligned by default (since Nimble can't know if the provided configuration will produce consistent decimal separators). + - This could be extended to other pre-configured formats in future. Their configuration attributes would similarly be prefixed with the name of the format mode. + - **Note:** all of the above will be implemented using a `Intl.NumberFormat` formatter. For all modes besides `custom` Nimble will configure the formatter with defaults to match the [visual design spec](https://github.com/ni/nimble/issues/887) (e.g. `useGrouping: false` to achieve `1000` rather than `1,000` and `signDisplay: auto` to achieve `1` rather than `+1`). +- `decimal-digits` - when format is `decimal`, a number that controls how many digits are shown to the right of the decimal separator. Defaults to 2. +- `custom-*` - when format is `custom`, these attribute-cased values will be passed to the equivalent camelCased fields of the `options` parameter of the `Intl.NumberFormat` constructor. For example, `options.maximumFractionDigits` will be set to the value of `custom-maximum-fraction-digits`. These fields are all string, boolean, or number and their property equivalents will be strictly typed. Initially the column will expose configurations that are [not marked experimental](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#browser_compatibility). -The API will be specified in a future update to this document. Below is **an example API** that leverages the native browser [`Intl.NumberFormat` API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat): this is intended to help visualize what the API _could_ look like, but isn't yet finalized. +This column will display the `placeholder` when `typeof` the value is not `"number"` (i.e. if the value is `null`, `undefined`, not present, or has a different runtime data type). Note that IEE 754 numbers like Infinity, NaN, and -0 are type `"number"` so will be displayed how each formatter converts them. This will preserve values like `"∞"`, `"NaN"` and `"-0"`. + +This column will trigger `invalidColumnConfiguration` on the table's validity state and will include flags in the column's validity state if its configuration can't be translated to a valid `Intl.NumberFormat` object. To provide better developer feedback about what's wrong with the configuration, the column could expose a public method like `createNumberFormat()` which would use the column's configuration to construct the formatter but allow any exceptions to be thrown. + +##### Examples ```html - + Tag + + + Count - + - Voltage - + ``` -##### Date Column +#### Date Column -Nimble could introduce `nimble-table-column-date-text` which maps date-time values to localized strings. The API will be specified in a future update to this document. It will need to consider cases like date formatting (both for locale and other reasons) and how to provide localized strings, possibly by exposing [the native browser `Intl.DateTimeFormat` API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat). +Nimble will introduce `nimble-table-column-date-text` which maps numbers containing Unix timestamp values (milliseconds since the Unix epoch) to localized date/time strings. ---- +##### Date types + +We considered allowing clients to provide date values using native date types like [JavaScript `Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) or [.NET `DateTime`](https://learn.microsoft.com/en-us/dotnet/api/system.datetime?view=net-7.0). We opted not to allow this in the `setData()` function to ensure the best possible performance and to avoid ambiguities about copying values. For now, clients can convert those values to Unix timestamps. In future we may choose to support these types in a more first-class way. That would require a future HLD update to answer some of the questions posed [in this discussion thread](https://github.com/ni/nimble/pull/1268#discussion_r1204674219). + +##### API -_These sections will be populated in a future update_ +- `format` - a string which can take one of the following values + - `undefined` - use the default formatter, which will display values similar to `Dec 19, 2020, 9:23:16 PM` and include support for localization. It will be implemented using `Intl.DateTimeFormat` with `options.dateStyle` and `options.timeStyle` set to `"medium"`. + - `'custom'` - use [`Intl.DateTimeFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat) configured with values specified in other attributes. + - This could be extended to other pre-configured formats in future. Their configuration attributes would similarly be prefixed with the name of the format mode. +- `custom-*` - when format is `custom`, these attribute-cased values will be passed to the equivalent camelCased fields of the `options` parameter of the `Intl.DateTimeFormat` constructor. For example, `options.dateStyle` will be set to the value of `custom-date-style`. These fields are all string, boolean, or number and their property equivalents will be strictly typed. -### API +This column will display the `placeholder` for a date when the value is `null` or `isNaN(new Date(value))` returns `true` (i.e. if the value is `null`, `undefined`, not present, has a non-`number` runtime data type, or is a `number` that produces an invalid `Date` like `NaN`/`Infinity`) (see ["Detecting an invalid Date instance"](https://stackoverflow.com/a/1353711)). Note that `new Date(null)` is behaves the same as `new Date(0)` so Nimble treating `null` as invalid will require a special case. -_Component Name_ +This column will trigger `invalidColumnConfiguration` on the table's validity state and will include flags in the column's validity state if its configuration can't be translated to a valid `Intl.DateTimeFormat` object. To provide better developer feedback about what's wrong with the configuration, the column could expose a public method like `createDateTimeFormat()` which would use the column's configuration to construct the formatter but allow any exceptions to be thrown. -_*Props/Attrs*_ +##### Examples + +```html + + + Start + + + + Due Date + + +``` -_Type Reference_ +#### Elapsed Time Column -### Anatomy +Nimble could introduce a column type that maps time duration to localized strings (e.g. "1 minute, 10 seconds"). Its API will be discussed in a future update to this document. It might use [Temporal duration values](https://tc39.es/proposal-temporal/docs/duration.html) formatted with [`Intl.DurationFormat`](https://tc39.es/proposal-intl-duration-format/) but those APIs are not yet approved for implementation in browsers. The [`Intl.RelativeTimeFormat` API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat) also looks promising but it includes relative words like "ago" and "in" which are difficult to remove, even when using [`formatToParts`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat/formatToParts). -### Angular integration +#### Specifying locale -### Blazor integration +The columns described above will format numbers, dates, and times differently depending on the locale. To specify the locale, the column implementations will read a Nimble configuration design token for locale and pass its value to the `locales` input of the `Intl.NumberFormat` or `Intl.DateTimeFormat` constructor. If the value of that token changes on the column element, the column will re-render with the new locale. -### Visual Appearance +The design token for configuring locale will be a new token following the patterns described in [Automatic theme / Runtime Configuration HLD](https://github.com/ni/nimble/pull/1257) for tokens like `theme` and `direction`. Its attribute name on the theme provider will be `lang`. It will be a string expected to conform with the [BCP 47 locale format](https://en.wikipedia.org/wiki/IETF_language_tag) (e.g. `en-us` or `de`). Its initial value will be set by reading the `lang` attribute from the page's `` element. + +_Alternative_: We considered naming the token attribute `locale` to distinguish it from the [HTML global `lang` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang) since it won't behave identically (for example, the HTML attribute can be set on any element but Nimble will only respect setting it on the HTML element or a theme provider). But we decided the behavior was similar enough to use the same name and to rely on documentation to convey those differences. --- @@ -301,35 +353,70 @@ Nimble already has a mechanism for clients to provide custom columns by deriving - Difficult to enforce styling differences between string and numeric columns (e.g. right vs left text alignment) - Potential cross-app inconsistency if formatting code isn't shared ---- +### Other number/date/time formatting APIs + +1. Existing SLE applications use the [Angular DatePipe](https://angular.io/api/common/DatePipe). This offers similar options to the `Intl.DateTimeFormat` (including locale-specific formatting) but would be a challenge to support in non-Angular applications. +2. The TC39 ECMAScript standard is working on [a proposal for Temporal](https://github.com/tc39/proposal-temporal), which includes a new [immutable format for representing time](https://tc39.es/proposal-temporal/docs/index.html) and namespace for manipulating it. It's in Stage 3 meaning the API isn't expected to change and [polyfills](https://github.com/tc39/proposal-temporal#polyfills) are available. Since the formatting APIs that browsers support today are sufficient, we propose to use them instead. +3. There are numerous libraries for formatting including the standards-based [Format.js](https://formatjs.io/), the opinionated [moment.js](https://momentjs.com/docs/), and [various successors](https://momentjs.com/docs/#/-project-status/recommendations/). We may choose to depend on one in the future to expose additional formatting options but will start with browser APIs as they require no additional dependencies. -_These sections will be populated in a future update_ +### Additional unit and placeholder APIs + +We considered adding attributes `prefix-field-name`, `suffix-field-name`, and `placeholder-field-name` which would allow those values to be populated with a dynamic field value rather than a constant string. This could help use cases where values have disparate types or units. We decided this is not necessary initially but could revisit in the future. See [this thread](https://github.com/ni/nimble/pull/1268#discussion_r1212385898) for more discussion. + +--- ### States +N/A + ### Accessibility +No unique considerations + ### Globalization +Formatting APIs support locale-specific formatting and include user-visible strings in the browser. + ### Security +No unique considerations + ### Performance +We will do manual testing to ensure the browser formatting functions don't impact scroll performance. + ### Dependencies +None + ### Test Plan -This will be filled out in more detail, but some reminders: +We will add standard unit tests, Blazor/Angular tests, and Chromatic tests for new column types. The unit tests will exercise formatting corner cases including: + +#### Number column -- handle number edge cases (-Inf, Inf, -0, +0, NaN, Number.MAX_SAFE_INTEGER + n, Number.MIN_SAFE_INTEGER -n) as numbers -- handle non-number edge cases (strings containing numbers, undefined, null) +- number edge cases (-Inf, Inf, -0, +0, NaN, Number.MAX_SAFE_INTEGER + n, Number.MIN_SAFE_INTEGER -n) should render as numbers +- non-number edge cases (e.g. strings containing numbers, undefined, null) should display the placeholder +- formatting should change with different locales +- invalid formatter configuration should be reflected in column validity state + +#### Date column + +- date edge cases (min and max date, different time zones) +- non-date edge cases (e.g. non-dates, invalid dates) should display the placeholder +- formatting should change with different locales +- invalid formatter configuration should be reflected in column validity state ### Tooling +N/A + ### Documentation +Standard Storybook documentation for column APIs. + --- ## Open Issues -1. API to configure text alignment of column content, placeholder text, and column headers (e.g. right align numeric columns but left align string columns and numeric columns with non-uniform formatting; [more discussion here](https://github.com/ni/nimble/issues/887)). We'll update the HLD with a recommendation when we start working on columns that need it (you're welcome to comment with ideas now though). +1. Visual design recommends that column header text alignment match data alignment. Once we prototype this we may hit implementation concerns (e.g. clash with proposed header menu button). diff --git a/packages/nimble-components/src/table/specs/table-columns-hld.md b/packages/nimble-components/src/table/specs/table-columns-hld.md index c76d0d3059..60e83bbb9a 100644 --- a/packages/nimble-components/src/table/specs/table-columns-hld.md +++ b/packages/nimble-components/src/table/specs/table-columns-hld.md @@ -91,7 +91,7 @@ Some potential column names following this convention are listed below. ``` nimble-table-column-anchor nimble-table-column-text -nimble-table-column-numeric-text +nimble-table-column-number-text nimble-table-column-date-text nimble-table-column-progress nimble-table-column-text-field