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

Add formatting API to date column #1381

Merged
merged 30 commits into from
Aug 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
0fc9550
Add format property and custom formatting properties
m-akinc Jul 21, 2023
76d6554
Update storybook
m-akinc Jul 21, 2023
afc04f0
Add initial tests
m-akinc Jul 21, 2023
8a418f3
Add tests for attributes
m-akinc Jul 24, 2023
4e08208
Reuse formatter
m-akinc Jul 24, 2023
99ba2cd
Change files
m-akinc Jul 24, 2023
de3f429
Fix test
m-akinc Jul 24, 2023
e677c5d
Address some feedback
m-akinc Jul 25, 2023
f11f8cc
Define types for custom options
m-akinc Jul 25, 2023
35c2c7c
Catch exception when format options are invalid
m-akinc Jul 25, 2023
b9e4308
Create validity flag for invalid custom configuration
m-akinc Jul 25, 2023
3bcf01d
Fix test
m-akinc Jul 25, 2023
5327f85
remove fit
m-akinc Jul 25, 2023
d5ded7f
Address feedback
m-akinc Jul 26, 2023
45eccb1
Create and reuse single formatter for all cell views and group header…
m-akinc Jul 26, 2023
aac2531
Remove unused import
m-akinc Jul 26, 2023
63b1b30
Whoops. Remove "fit" x3
m-akinc Jul 26, 2023
7a48b38
Must support undefined for customHour12
m-akinc Jul 26, 2023
7206f04
Remove everything but formatter from columnConfig
m-akinc Jul 26, 2023
771c0bb
Translate null attribute values to undefined to work around bug
m-akinc Jul 26, 2023
0bc03ac
Don't pass format
m-akinc Jul 26, 2023
6c8fb20
Feedback
m-akinc Jul 26, 2023
12b152a
Reuse TypeScript types instead of redefining
m-akinc Jul 27, 2023
b1d1189
Fix story
m-akinc Jul 27, 2023
70ae642
Use getSpecTypeByNamedList
m-akinc Jul 27, 2023
3a2ccb1
Merge branch 'main' into users/makinc/datetime-column-configuration
m-akinc Jul 27, 2023
67e3dd3
Updates
m-akinc Aug 1, 2023
4eac47b
Force Chrome tests to run with 'en' locale
m-akinc Aug 1, 2023
a8554d9
Merge branch 'main' into users/makinc/datetime-column-configuration
m-akinc Aug 1, 2023
ddc554b
Merge branch 'main' into users/makinc/datetime-column-configuration
m-akinc Aug 1, 2023
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 @@
{
"type": "patch",
"comment": "Add formatting API to date column",
"packageName": "@ni/nimble-components",
"email": "[email protected]",
"dependentChangeType": "patch"
}
3 changes: 2 additions & 1 deletion packages/nimble-components/karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ const commonChromeFlags = [
'--disable-extensions',
'--disable-infobars',
'--disable-translate',
'--force-prefers-reduced-motion'
'--force-prefers-reduced-motion',
'--lang=en'
];

// Create a webpack environment plugin to use while running tests so that
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
} from '..';
import { styles } from '../../text-base/cell-view/styles';
import { TableColumnTextCellViewBase } from '../../text-base/cell-view';
import { formatNumericDate } from '../models/format-helper';

declare global {
interface HTMLElementTagNameMap {
Expand All @@ -20,20 +21,20 @@ export class TableColumnDateTextCellView extends TableColumnTextCellViewBase<
TableColumnDateTextCellRecord,
TableColumnDateTextColumnConfig
> {
private static readonly formatter = new Intl.DateTimeFormat(undefined, {
dateStyle: 'medium',
timeStyle: 'medium'
});
private columnConfigChanged(): void {
this.updateText();
}

private cellRecordChanged(): void {
if (typeof this.cellRecord.value === 'number') {
try {
this.text = TableColumnDateTextCellView.formatter.format(
this.cellRecord.value
);
} catch (e) {
this.text = '';
}
this.updateText();
}

private updateText(): void {
if (this.columnConfig?.formatter) {
m-akinc marked this conversation as resolved.
Show resolved Hide resolved
this.text = formatNumericDate(
this.columnConfig.formatter,
this.cellRecord.value
);
} else {
this.text = '';
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { TableColumnTextGroupHeaderViewBase } from '../../text-base/group-header
import { template } from '../../text-base/group-header-view/template';
import { styles } from '../../text-base/group-header-view/styles';
import type { TableColumnDateTextColumnConfig } from '..';
import { formatNumericDate } from '../models/format-helper';

declare global {
interface HTMLElementTagNameMap {
Expand All @@ -17,20 +18,20 @@ export class TableColumnDateTextGroupHeaderView extends TableColumnTextGroupHead
TableNumberFieldValue,
TableColumnDateTextColumnConfig
> {
private static readonly formatter = new Intl.DateTimeFormat(undefined, {
dateStyle: 'medium',
timeStyle: 'medium'
});
private columnConfigChanged(): void {
this.updateText();
}

private groupHeaderValueChanged(): void {
if (typeof this.groupHeaderValue === 'number') {
try {
this.text = TableColumnDateTextGroupHeaderView.formatter.format(
this.groupHeaderValue
);
} catch (e) {
this.text = '';
}
this.updateText();
}

private updateText(): void {
if (this.columnConfig?.formatter) {
m-akinc marked this conversation as resolved.
Show resolved Hide resolved
this.text = formatNumericDate(
this.columnConfig.formatter,
this.groupHeaderValue
);
} else {
this.text = '';
}
Expand Down
234 changes: 231 additions & 3 deletions packages/nimble-components/src/table-column/date-text/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,37 @@
import { DesignSystem } from '@microsoft/fast-foundation';
import { attr } from '@microsoft/fast-element';
import { styles } from '../base/styles';
import { template } from '../base/template';
import type { TableNumberField } from '../../table/types';
import { TableColumnTextBase } from '../text-base';
import { TableColumnSortOperation } from '../base/types';
import { TableColumnSortOperation, TableColumnValidity } from '../base/types';
import { tableColumnDateTextGroupHeaderTag } from './group-header-view';
import { tableColumnDateTextCellViewTag } from './cell-view';
import type { ColumnInternalsOptions } from '../base/models/column-internals';
import type {
DateTextFormat,
LocaleMatcherAlgorithm,
EraFormat,
YearFormat,
DayFormat,
HourFormat,
MinuteFormat,
SecondFormat,
TimeZoneNameFormat,
FormatMatcherAlgorithm,
DayPeriodFormat,
DateStyle,
TimeStyle,
HourCycleFormat,
MonthFormat,
WeekdayFormat
} from './types';
import { TableColumnDateTextValidator } from './models/table-column-date-text-validator';

export type TableColumnDateTextCellRecord = TableNumberField<'value'>;
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface TableColumnDateTextColumnConfig {}
export interface TableColumnDateTextColumnConfig {
m-akinc marked this conversation as resolved.
Show resolved Hide resolved
formatter?: Intl.DateTimeFormat;
}

declare global {
interface HTMLElementTagNameMap {
Expand All @@ -22,6 +43,78 @@ declare global {
* The table column for displaying dates/times as text.
*/
export class TableColumnDateText extends TableColumnTextBase {
/** @internal */
public validator = new TableColumnDateTextValidator(this.columnInternals);

@attr
public format: DateTextFormat;

@attr({ attribute: 'custom-locale-matcher' })
public customLocaleMatcher: LocaleMatcherAlgorithm;

@attr({ attribute: 'custom-weekday' })
public customWeekday: WeekdayFormat;

@attr({ attribute: 'custom-era' })
public customEra: EraFormat;

@attr({ attribute: 'custom-year' })
public customYear: YearFormat;

@attr({ attribute: 'custom-month' })
public customMonth: MonthFormat;

@attr({ attribute: 'custom-day' })
public customDay: DayFormat;

@attr({ attribute: 'custom-hour' })
public customHour: HourFormat;

@attr({ attribute: 'custom-minute' })
public customMinute: MinuteFormat;

@attr({ attribute: 'custom-second' })
public customSecond: SecondFormat;

@attr({ attribute: 'custom-time-zone-name' })
public customTimeZoneName: TimeZoneNameFormat;

@attr({ attribute: 'custom-format-matcher' })
public customFormatMatcher: FormatMatcherAlgorithm;

@attr({ attribute: 'custom-hour12', mode: 'boolean' })
public customHour12?: boolean;
m-akinc marked this conversation as resolved.
Show resolved Hide resolved

@attr({ attribute: 'custom-time-zone' })
public customTimeZone?: string;

@attr({ attribute: 'custom-calendar' })
public customCalendar?: string;

@attr({ attribute: 'custom-day-period' })
public customDayPeriod: DayPeriodFormat;

@attr({ attribute: 'custom-numbering-system' })
public customNumberingSystem?: string;

@attr({ attribute: 'custom-date-style' })
public customDateStyle: DateStyle;

@attr({ attribute: 'custom-time-style' })
public customTimeStyle: TimeStyle;

@attr({ attribute: 'custom-hour-cycle' })
public customHourCycle: HourCycleFormat;

public override connectedCallback(): void {
super.connectedCallback();
this.updateColumnConfig();
m-akinc marked this conversation as resolved.
Show resolved Hide resolved
}

public override get validity(): TableColumnValidity {
return this.validator.getValidity();
}

protected override getColumnInternalsOptions(): ColumnInternalsOptions {
return {
cellRecordFieldNames: ['value'],
Expand All @@ -31,6 +124,141 @@ export class TableColumnDateText extends TableColumnTextBase {
sortOperation: TableColumnSortOperation.basic
};
}

protected formatChanged(): void {
this.updateColumnConfig();
}

protected customLocaleMatcherChanged(): void {
this.updateColumnConfig();
}

protected customWeekdayChanged(): void {
this.updateColumnConfig();
}

protected customEraChanged(): void {
this.updateColumnConfig();
}

protected customYearChanged(): void {
this.updateColumnConfig();
}

protected customMonthChanged(): void {
this.updateColumnConfig();
}

protected customDayChanged(): void {
this.updateColumnConfig();
}

protected customHourChanged(): void {
this.updateColumnConfig();
}

protected customMinuteChanged(): void {
this.updateColumnConfig();
}

protected customSecondChanged(): void {
this.updateColumnConfig();
}

protected customTimeZoneNameChanged(): void {
this.updateColumnConfig();
}

protected customFormatMatcherChanged(): void {
this.updateColumnConfig();
}

protected customHour12Changed(): void {
this.updateColumnConfig();
}

protected customTimeZoneChanged(): void {
this.updateColumnConfig();
}

protected customCalendarChanged(): void {
this.updateColumnConfig();
}

protected customDayPeriodChanged(): void {
this.updateColumnConfig();
}

protected customNumberingSystemChanged(): void {
this.updateColumnConfig();
}

protected customDateStyleChanged(): void {
this.updateColumnConfig();
}

protected customTimeStyleChanged(): void {
this.updateColumnConfig();
}

protected customHourCycleChanged(): void {
this.updateColumnConfig();
}

private updateColumnConfig(): void {
const columnConfig: TableColumnDateTextColumnConfig = {
m-akinc marked this conversation as resolved.
Show resolved Hide resolved
formatter: this.createFormatter()
};
this.columnInternals.columnConfig = columnConfig;
this.validator.setCustomOptionsValidity(
columnConfig.formatter !== undefined
);
}

private createFormatter(): Intl.DateTimeFormat | undefined {
let options: Intl.DateTimeFormatOptions;
if (!this.format) {
options = {
dateStyle: 'medium',
timeStyle: 'medium'
};
} else {
options = this.getCustomFormattingOptions();
}
try {
return new Intl.DateTimeFormat(undefined, options);
m-akinc marked this conversation as resolved.
Show resolved Hide resolved
} catch (e) {
return undefined;
}
}

private getCustomFormattingOptions(): Intl.DateTimeFormatOptions {
// There's a FAST bug (https://github.com/microsoft/fast/issues/6630) where removing
// attributes sets their values to null instead of undefined. To work around this,
// translate null values to undefined.
const options: Intl.DateTimeFormatOptions = {
localeMatcher: this.customLocaleMatcher ?? undefined,
weekday: this.customWeekday ?? undefined,
era: this.customEra ?? undefined,
year: this.customYear ?? undefined,
month: this.customMonth ?? undefined,
day: this.customDay ?? undefined,
hour: this.customHour ?? undefined,
minute: this.customMinute ?? undefined,
second: this.customSecond ?? undefined,
timeZoneName: this.customTimeZoneName ?? undefined,
formatMatcher: this.customFormatMatcher ?? undefined,
hour12: this.customHour12 ?? undefined,
timeZone: this.customTimeZone ?? undefined,
calendar: this.customCalendar ?? undefined,
dayPeriod: this.customDayPeriod ?? undefined,
numberingSystem: this.customNumberingSystem ?? undefined,
dateStyle: this.customDateStyle ?? undefined,
timeStyle: this.customTimeStyle ?? undefined,
hourCycle: this.customHourCycle ?? undefined
};
return options;
}
}

const nimbleTableColumnDateText = TableColumnDateText.compose({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { TableNumberFieldValue } from '../../../table/types';

export function formatNumericDate(
formatter: Intl.DateTimeFormat,
date: TableNumberFieldValue
): string {
if (typeof date === 'number') {
try {
return formatter.format(date);
} catch (e) {
return '';
m-akinc marked this conversation as resolved.
Show resolved Hide resolved
}
} else {
return '';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { ColumnInternals } from '../../base/models/column-internals';
import { ColumnValidator } from '../../base/models/column-validator';

const dateTextValidityFlagNames = ['invalidCustomOptionsCombination'] as const;

/**
* Validator for TableColumnDateText.
*/
export class TableColumnDateTextValidator extends ColumnValidator<
typeof dateTextValidityFlagNames
> {
public constructor(columnInternals: ColumnInternals<unknown>) {
super(columnInternals, dateTextValidityFlagNames);
}

public setCustomOptionsValidity(valid: boolean): void {
this.setConditionValue('invalidCustomOptionsCombination', !valid);
}
}
Loading