Skip to content

Commit

Permalink
Initial version of date column (#1363)
Browse files Browse the repository at this point in the history
## 🀨 Rationale

Part of #1014 

## πŸ‘©β€πŸ’» Implementation

This is the first, basic version of the column. It does not support any
configuration. It always formats the datetime in the way that will
become the default when we later add format configuration. The column
derives from `TableColumnTextBase`.
- component defined
- initial Storybook stories/documentation created
- unit tests written

## πŸ§ͺ Testing

Tested in Storybook and created unit tests.

## βœ… Checklist

- [x] I have updated the project documentation to reflect my changes or
determined no changes are needed.

---------

Co-authored-by: Jesse Attas <[email protected]>
  • Loading branch information
m-akinc and jattasNI authored Jul 24, 2023
1 parent 8d4f6da commit 44e7d0d
Show file tree
Hide file tree
Showing 11 changed files with 635 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Initial version of date column",
"packageName": "@ni/nimble-components",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,11 @@ describe('TableColumnAnchor', () => {
const noValueData = [
{ description: 'field not present', data: [{ unused: 'foo' }] },
{ description: 'value is null', data: [{ label: null }] },
{ description: 'value is undefined', data: [{ label: undefined }] }
{ description: 'value is undefined', data: [{ label: undefined }] },
{
description: 'value is not a string',
data: [{ label: 10 as unknown as string }]
}
];
for (const testData of noValueData) {
// eslint-disable-next-line @typescript-eslint/no-loop-func
Expand Down Expand Up @@ -192,6 +196,16 @@ describe('TableColumnAnchor', () => {
});

describe('with href', () => {
it('displays label when href is not string', async () => {
await element.setData([
{ label: 'foo', link: 10 as unknown as string }
]);
await connect();
await waitForUpdatesAsync();

expect(pageObject.getRenderedCellContent(0, 0)).toBe('foo');
});

it('changing labelFieldName updates display', async () => {
await element.setData([
{ label: 'foo', otherLabel: 'bar', link: 'url' }
Expand Down Expand Up @@ -254,6 +268,14 @@ describe('TableColumnAnchor', () => {
}

describe('with no label', () => {
it('displays empty string when href is not string', async () => {
await element.setData([{ link: 10 as unknown as string }]);
await connect();
await waitForUpdatesAsync();

expect(pageObject.getRenderedCellContent(0, 0)).toBe('');
});

it('displays url', async () => {
await element.setData([{ link: 'foo' }]);
await connect();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { DesignSystem } from '@microsoft/fast-foundation';
import { template } from '../../text-base/cell-view/template';
import type {
TableColumnDateTextCellRecord,
TableColumnDateTextColumnConfig
} from '..';
import { styles } from '../../text-base/cell-view/styles';
import { TableColumnTextCellViewBase } from '../../text-base/cell-view';

declare global {
interface HTMLElementTagNameMap {
'nimble-table-column-date-text-cell-view': TableColumnDateTextCellView;
}
}

/**
* A cell view for displaying date/time fields as text
*/
export class TableColumnDateTextCellView extends TableColumnTextCellViewBase<
TableColumnDateTextCellRecord,
TableColumnDateTextColumnConfig
> {
private static readonly formatter = new Intl.DateTimeFormat(undefined, {
dateStyle: 'medium',
timeStyle: 'medium'
});

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

const dateTextCellView = TableColumnDateTextCellView.compose({
baseName: 'table-column-date-text-cell-view',
template,
styles
});
DesignSystem.getOrCreate().withPrefix('nimble').register(dateTextCellView());
export const tableColumnDateTextCellViewTag = DesignSystem.tagFor(
TableColumnDateTextCellView
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { DesignSystem } from '@microsoft/fast-foundation';
import type { TableNumberFieldValue } from '../../../table/types';
import { TableColumnTextGroupHeaderViewBase } from '../../text-base/group-header-view';
import { template } from '../../text-base/group-header-view/template';
import { styles } from '../../text-base/group-header-view/styles';
import type { TableColumnDateTextColumnConfig } from '..';

declare global {
interface HTMLElementTagNameMap {
'nimble-table-column-date-text-group-header': TableColumnDateTextGroupHeaderView;
}
}
/**
* The group header view for displaying date/time fields as text.
*/
export class TableColumnDateTextGroupHeaderView extends TableColumnTextGroupHeaderViewBase<
TableNumberFieldValue,
TableColumnDateTextColumnConfig
> {
private static readonly formatter = new Intl.DateTimeFormat(undefined, {
dateStyle: 'medium',
timeStyle: 'medium'
});

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

const tableColumnDateTextGroupHeaderView = TableColumnDateTextGroupHeaderView.compose({
baseName: 'table-column-date-text-group-header',
template,
styles
});
DesignSystem.getOrCreate()
.withPrefix('nimble')
.register(tableColumnDateTextGroupHeaderView());
export const tableColumnDateTextGroupHeaderTag = DesignSystem.tagFor(
TableColumnDateTextGroupHeaderView
);
45 changes: 45 additions & 0 deletions packages/nimble-components/src/table-column/date-text/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { DesignSystem } from '@microsoft/fast-foundation';
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 { tableColumnDateTextGroupHeaderTag } from './group-header-view';
import { tableColumnDateTextCellViewTag } from './cell-view';
import type { ColumnInternalsOptions } from '../base/models/column-internals';

export type TableColumnDateTextCellRecord = TableNumberField<'value'>;
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface TableColumnDateTextColumnConfig {}

declare global {
interface HTMLElementTagNameMap {
'nimble-table-column-date-text': TableColumnDateText;
}
}

/**
* The table column for displaying dates/times as text.
*/
export class TableColumnDateText extends TableColumnTextBase {
protected override getColumnInternalsOptions(): ColumnInternalsOptions {
return {
cellRecordFieldNames: ['value'],
cellViewTag: tableColumnDateTextCellViewTag,
groupHeaderViewTag: tableColumnDateTextGroupHeaderTag,
delegatedEvents: [],
sortOperation: TableColumnSortOperation.basic
};
}
}

const nimbleTableColumnDateText = TableColumnDateText.compose({
baseName: 'table-column-date-text',
template,
styles
});

DesignSystem.getOrCreate()
.withPrefix('nimble')
.register(nimbleTableColumnDateText());
export const tableColumnDateTextTag = DesignSystem.tagFor(TableColumnDateText);
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { TablePageObject } from '../../../table/testing/table.pageobject';
import type { TableRecord } from '../../../table/types';
import { TableColumnDateTextCellView } from '../cell-view';

/**
* Page object for date text table column.
*/
export class TableColumnDateTextPageObject<T extends TableRecord> {
// On Chrome, in a formatted date, the space before AM/PM is a narrow non-breaking space.
// For testing consistency across browsers, replace it with a regular space.
private readonly narrowNonBreakingSpace = '\u202f';

public constructor(private readonly tablePageObject: TablePageObject<T>) {}

public getRenderedCellContent(
rowIndex: number,
columnIndex: number
): string {
this.verifyCellType(rowIndex, columnIndex);
return this.tablePageObject
.getRenderedCellContent(rowIndex, columnIndex)
.replace(this.narrowNonBreakingSpace, ' ');
}

public getRenderedGroupHeaderContent(groupRowIndex: number): string {
return this.tablePageObject
.getRenderedGroupHeaderContent(groupRowIndex)
.replace(this.narrowNonBreakingSpace, ' ');
}

public getCellTitle(rowIndex: number, columnIndex: number): string {
this.verifyCellType(rowIndex, columnIndex);
return this.tablePageObject
.getCellTitle(rowIndex, columnIndex)
.replace(this.narrowNonBreakingSpace, ' ');
}

private verifyCellType(rowIndex: number, columnIndex: number): void {
const cell = this.tablePageObject.getRenderedCellView(
rowIndex,
columnIndex
);
if (!(cell instanceof TableColumnDateTextCellView)) {
throw new Error('Cell is not in a date text column');
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { StoryFn, Meta } from '@storybook/html';
import { html, ViewTemplate } from '@microsoft/fast-element';
import { createMatrixThemeStory } from '../../../utilities/tests/storybook';
import {
createMatrix,
sharedMatrixParameters
} from '../../../utilities/tests/matrix';
import { tableColumnDateTextTag } from '..';
import { iconUserTag } from '../../../icons/user';
import { Table, tableTag } from '../../../table';
import {
controlLabelFont,
controlLabelFontColor
} from '../../../theme-provider/design-tokens';

const metadata: Meta = {
title: 'Tests/Table Column Types',
parameters: {
...sharedMatrixParameters()
}
};

export default metadata;

const data = [
{
id: '0',
date: new Date('Dec 21, 2020, 3:45:03 PM').valueOf()
},
{
id: '1',
date: new Date('Dec 21, 2020, 3:45:03 PM').valueOf()
},
{
id: '2'
}
] as const;

// prettier-ignore
const component = (): ViewTemplate => html`
<label style="color: var(${controlLabelFontColor.cssCustomProperty}); font: var(${controlLabelFont.cssCustomProperty})">Date Text Table Column</label>
<${tableTag} id-field-name="id" style="height: 250px">
<${tableColumnDateTextTag}
field-name="date"
group-index="0"
>
<${iconUserTag}></${iconUserTag}>
</${tableColumnDateTextTag}>
</${tableTag}>
`;

export const tableColumnDateTextThemeMatrix: StoryFn = createMatrixThemeStory(
createMatrix(component)
);

tableColumnDateTextThemeMatrix.play = async (): Promise<void> => {
await Promise.all(
Array.from(document.querySelectorAll<Table>('nimble-table')).map(
async table => {
await table.setData(data);
}
)
);
};
Loading

0 comments on commit 44e7d0d

Please sign in to comment.