Skip to content

Commit

Permalink
DatePickerCell Implementation (#627)
Browse files Browse the repository at this point in the history
* DatePickerCell Implementation with prettier

* Suggestions. Note, I do need to add more unit tests

* Actually remove the unit test beacuse infra does not support this as of yet.

* Rename variable to dateKind

* Add support.ts duplicate method into cells packagea and refactor import

* Put back try and catch. oops

* readd format parameter to support backwards compat and incorporate Lukas Feedback

* Adding basic unit test

* Add todos for myself

* lint

* Add max, min, step, readonly, and change cached displayDate behavior to undefined behavior

* Add more tests and dataid for date-picker-cell

* Add tests

* Add date, time, datetime tests

* cleanup

* Remove console.log

* Add more test cases for onPaste

* Add time test case for onPaste

* Add implementation for time in onPaste

* Cleanup tests a bit

* Add additional columns to storybook

* Apply correct formatting

* Apply correct formatting

* Clean up with correct prettier file

* Remove need for support functions

* Clean imports

* Revert "Clean up with correct prettier file"

This reverts commit 32e5b93.

* Add assert method

* Fix paste issue

* Delete support ts

* Fix cell stories display

* Add styling to apply gdg theme

* Fix tests

* Use outside read-only

* Remove theme

* Add cell tests to CI workflow

* Fix tests and remove extra test

* Remove tests because they're failing based on timezone

* Update date picker cell (#5)

* Fix unit tests

* Fix tests

* Remove min / max

* Add support for Date as min / max value (#6)

* Fix unit tests

* Fix tests

* Remove min / max

* Add support for date in min/max

---------

Co-authored-by: lukasmasuch <lukas.masuch@gmail.com>
Co-authored-by: Jason Smith <jassmith@gmail.com>
  • Loading branch information
3 people committed Aug 18, 2023
1 parent 2325dcd commit 9495a25
Show file tree
Hide file tree
Showing 5 changed files with 399 additions and 45 deletions.
1 change: 1 addition & 0 deletions .github/workflows/node.js.yml
Expand Up @@ -18,6 +18,7 @@ jobs:
- run: npm run build
- run: npm run test -- --coverage
- run: npm run test-source
- run: npm run test-cells
- run: npm run test-projects
- name: Coveralls
uses: coverallsapp/github-action@master
Expand Down
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -13,6 +13,7 @@
"test": "cd packages/core && npm run test --",
"test-18": "./setup-react-18-test.sh && cd packages/core && npm run test --",
"test-source": "cd packages/source && npm run test",
"test-cells": "cd packages/cells && npm run test",
"test-projects": "cd test-projects/ && ./bootstrap-projects.sh"
},
"author": "Glide",
Expand Down
42 changes: 40 additions & 2 deletions packages/cells/src/cell.stories.tsx
Expand Up @@ -329,6 +329,36 @@ export const CustomCells: React.VFC = () => {
};
return d;
} else if (col === 10) {
num = row + 1;
rand();
const d: DatePickerCell = {
kind: GridCellKind.Custom,
allowOverlay: true,
copyData: "4",
data: {
kind: "date-picker-cell",
date: new Date(),
displayDate: new Date().toISOString().split("T")[0],
format: "date",
},
};
return d;
} else if (col === 11) {
num = row + 1;
rand();
const d: DatePickerCell = {
kind: GridCellKind.Custom,
allowOverlay: true,
copyData: "4",
data: {
kind: "date-picker-cell",
date: new Date(),
displayDate: new Date().toISOString().split("T")[1].replace("Z", ""),
format: "time",
},
};
return d;
} else if (col === 12) {
num = row + 1;
rand();
const d: LinksCell = {
Expand All @@ -351,7 +381,7 @@ export const CustomCells: React.VFC = () => {
},
};
return d;
} else if (col === 11) {
} else if (col === 13) {
num = row + 1;
rand();
const d: ButtonCell = {
Expand Down Expand Up @@ -415,8 +445,16 @@ export const CustomCells: React.VFC = () => {
width: 150,
},
{
id: "datetime-picker",
title: "Datetime Picker",
},
{
id: "date-picker",
title: "Date Picker",
width: 150,
},
{
id: "time-picker",
title: "Time Picker",
},
{
title: "Links",
Expand Down
192 changes: 149 additions & 43 deletions packages/cells/src/cells/date-picker-cell.tsx
@@ -1,15 +1,137 @@
import * as React from "react";
import { CustomCell, CustomRenderer, drawTextCell, GridCellKind } from "@glideapps/glide-data-grid";
import React from "react";
import { styled } from "@linaria/react";

interface DatePickerCellProps {
import {
CustomCell,
CustomRenderer,
drawTextCell,
GridCellKind,
ProvideEditorCallback,
TextCellEntry,
} from "@glideapps/glide-data-grid";

export const StyledInputBox = styled.input`
min-height: 26px;
border: none;
outline: none;
background-color: transparent;
font-size: var(--gdg-editor-font-size);
font-family: var(--gdg-font-family);
color: var(--gdg-text-dark);
::-webkit-calendar-picker-indicator {
background-color: white;
}
`;

export interface DatePickerCellProps {
readonly kind: "date-picker-cell";
readonly date: Date | undefined;
/* The current value of the datetime cell. */
readonly date: Date | undefined | null;
/* The current display value of the datetime cell. */
readonly displayDate: string;
readonly format: "date" | "datetime-local";
/* Defines the type of the HTML input element. */
readonly format: DateKind;
/* Timezone offset in minutes.
This can be used to adjust the date by a given timezone offset. */
readonly timezoneOffset?: number;
/* Minimum value that can be entered by the user.
This is passed to the min attribute of the HTML input element. */
readonly min?: string | Date;
/* Maximum value that can be entered by the user.
This is passed to the max attribute of the HTML input element. */
readonly max?: string | Date;
/* Granularity that the date must adhere.
This is passed to the step attribute of the HTML input element. */
readonly step?: string;
}

export type DateKind = "date" | "time" | "datetime-local";

export const formatValueForHTMLInput = (dateKind: DateKind, date: Date | undefined | null): string => {
if (date === undefined || date === null) {
return "";
}
const isoDate = date.toISOString();
switch (dateKind) {
case "date":
return isoDate.split("T")[0];
case "datetime-local":
return isoDate.replace("Z", "");
case "time":
return isoDate.split("T")[1].replace("Z", "");
default:
throw new Error(`Unknown date kind ${dateKind}`);
}
};

export type DatePickerCell = CustomCell<DatePickerCellProps>;

const Editor: ReturnType<ProvideEditorCallback<DatePickerCell>> = cell => {
const cellData = cell.value.data;
const { format, displayDate } = cellData;
const step =
cellData.step !== undefined && !Number.isNaN(Number(cellData.step)) ? Number(cellData.step) : undefined;

const minValue = cellData.min instanceof Date ? formatValueForHTMLInput(format, cellData.min) : cellData.min;

const maxValue = cellData.max instanceof Date ? formatValueForHTMLInput(format, cellData.max) : cellData.max;

let date = cellData.date;
// Convert timezone offset to milliseconds
const timezoneOffsetMs = cellData.timezoneOffset ? cellData.timezoneOffset * 60 * 1000 : 0;
if (timezoneOffsetMs && date) {
// Adjust based on the timezone offset
date = new Date(date.getTime() + timezoneOffsetMs);
}
const value = formatValueForHTMLInput(format, date);
if (cell.value.readonly) {
return (
<TextCellEntry
highlight={true}
autoFocus={false}
disabled={true}
value={displayDate ?? ""}
onChange={() => undefined}
/>
);
}

return (
<StyledInputBox
data-testid={"date-picker-cell"}
required
type={format}
defaultValue={value}
min={minValue}
max={maxValue}
step={step}
autoFocus={true}
onChange={event => {
if (isNaN(event.target.valueAsNumber)) {
// The user has cleared the date, contribute as undefined
cell.onChange({
...cell.value,
data: {
...cell.value.data,
date: undefined,
},
});
} else {
cell.onChange({
...cell.value,
data: {
...cell.value.data,
// use valueAsNumber because valueAsDate is null for "datetime-local"
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/datetime-local#technical_summary
date: new Date(event.target.valueAsNumber - timezoneOffsetMs),
},
});
}
}}
/>
);
};

const renderer: CustomRenderer<DatePickerCell> = {
kind: GridCellKind.Custom,
isMatch: (cell: CustomCell): cell is DatePickerCell => (cell.data as any).kind === "date-picker-cell",
Expand All @@ -18,49 +140,33 @@ const renderer: CustomRenderer<DatePickerCell> = {
drawTextCell(args, displayDate, cell.contentAlign);
return true;
},
// eslint-disable-next-line react/display-name
provideEditor: () => p => {
const cellData = p.value.data;
const { format, date } = cellData;

let val = "";
if (date !== undefined) {
val = date.toISOString();
if (format === "date") {
val = val.split("T")[0];
} else {
val = val.substring(0, 23);
}
}
return (
<input
style={{ minHeight: 26, border: "none", outline: "none" }}
type={format}
autoFocus={true}
value={val}
onChange={e => {
p.onChange({
...p.value,
data: {
...p.value.data,
date: e.target.valueAsDate ?? undefined,
},
});
}}
/>
);
measure: (ctx, cell) => {
const { displayDate } = cell.data;
return ctx.measureText(displayDate).width + 16;
},
provideEditor: () => ({
editor: Editor,
}),
onPaste: (v, d) => {
let newDate: Date | undefined;
try {
newDate = new Date(v);
} catch {
/* do nothing */
}
let parseDateTimestamp = NaN;
// We only try to parse the value if it is not empty/undefined/null:
if (v) {
// Support for unix timestamps (milliseconds since 1970-01-01):
parseDateTimestamp = Number(v).valueOf();

if (Number.isNaN(parseDateTimestamp)) {
// Support for parsing ISO 8601 date strings:
parseDateTimestamp = Date.parse(v);
if (d.format === "time" && Number.isNaN(parseDateTimestamp)) {
// The pasted value was not a valid date string
// Try to interpret value as time string instead (HH:mm:ss)
parseDateTimestamp = Date.parse(`1970-01-01T${v}Z`);
}
}
}
return {
...d,
date: Number.isNaN(newDate) ? undefined : newDate,
date: Number.isNaN(parseDateTimestamp) ? undefined : new Date(parseDateTimestamp),
};
},
};
Expand Down

0 comments on commit 9495a25

Please sign in to comment.