@purpurds/date-field 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/LICENSE.txt +74 -0
- package/dist/date-field.cjs.js +42 -0
- package/dist/date-field.cjs.js.map +1 -0
- package/dist/date-field.d.ts +12 -0
- package/dist/date-field.d.ts.map +1 -0
- package/dist/date-field.es.js +3882 -0
- package/dist/date-field.es.js.map +1 -0
- package/dist/metadata.js +7 -0
- package/dist/styles.css +1 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.d.ts.map +1 -0
- package/eslint.config.mjs +2 -0
- package/package.json +76 -0
- package/src/date-field.module.scss +18 -0
- package/src/date-field.stories.tsx +71 -0
- package/src/date-field.test.tsx +63 -0
- package/src/date-field.tsx +138 -0
- package/src/global.d.ts +4 -0
- package/src/utils.ts +30 -0
package/dist/metadata.js
ADDED
package/dist/styles.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
._purpur-text-field_3gl1k_1{position:relative;display:flex;flex-direction:column;gap:var(--purpur-spacing-50)}._purpur-text-field__label_3gl1k_7{display:flex;gap:var(--purpur-spacing-25);width:fit-content}._purpur-text-field__label-text_3gl1k_12{width:fit-content}._purpur-text-field__field-row_3gl1k_15{display:flex;width:100%}._purpur-text-field__frame_3gl1k_19{position:absolute;top:0;right:0;bottom:0;left:0;border-radius:var(--purpur-border-radius-sm);border:var(--purpur-border-width-xs) solid var(--purpur-color-border-interactive-subtle);pointer-events:none}._purpur-text-field__input-container_3gl1k_26{position:relative;display:flex;align-items:center;width:100%;border-radius:var(--purpur-border-radius-sm);background:var(--purpur-color-background-primary)}._purpur-text-field__input-container_3gl1k_26:has(:-internal-autofill-previewed),._purpur-text-field__input-container_3gl1k_26:has(:-internal-autofill-selected){background-color:var(--purpur-color-background-interactive-auto-fill)!important}._purpur-text-field__input-container--disabled_3gl1k_37{background:var(--purpur-color-background-interactive-disabled)}._purpur-text-field__input-container--readonly_3gl1k_40{background:var(--purpur-color-background-interactive-read-only)}._purpur-text-field__input-container--has-clear-button_3gl1k_43{padding-right:var(--purpur-spacing-25)}._purpur-text-field__input-container--end-adornment_3gl1k_46{padding-right:var(--purpur-spacing-150)}._purpur-text-field__input-container--end-adornment_3gl1k_46 ._purpur-text-field__input_3gl1k_26{padding-right:var(--purpur-spacing-100)}._purpur-text-field__input-container--start-adornment_3gl1k_52{padding-left:var(--purpur-spacing-150)}._purpur-text-field__input-container--start-adornment_3gl1k_52 ._purpur-text-field__input_3gl1k_26{padding-left:var(--purpur-spacing-100)}._purpur-text-field__input_3gl1k_26{border-radius:var(--purpur-border-radius-sm);padding:calc(var(--purpur-spacing-100) + var(--purpur-spacing-25)) var(--purpur-spacing-150);border:none;width:100%;box-sizing:border-box;font-family:var(--purpur-typography-family-default);font-size:var(--purpur-typography-scale-100);line-height:150%;outline:none;background:transparent;color:var(--purpur-color-text-default)}._purpur-text-field__input_3gl1k_26:hover~._purpur-text-field__frame_3gl1k_19{border-width:var(--purpur-border-width-sm);border-color:var(--purpur-color-border-interactive-subtle-hover)}._purpur-text-field__input_3gl1k_26:active:not(:disabled):not(:read-only)~._purpur-text-field__frame_3gl1k_19,._purpur-text-field__input_3gl1k_26:focus:not(:disabled):not(:read-only)~._purpur-text-field__frame_3gl1k_19{outline:var(--purpur-border-width-sm) solid var(--purpur-color-border-interactive-focus);outline-offset:calc(var(--purpur-spacing-10) * 2);border-width:var(--purpur-border-width-xs);border-color:var(--purpur-color-border-interactive-subtle-hover)}._purpur-text-field__input_3gl1k_26:disabled{color:var(--purpur-color-text-weak)}._purpur-text-field__input_3gl1k_26:disabled~._purpur-text-field__frame_3gl1k_19{border-width:var(--purpur-border-width-xs);border-color:var(--purpur-color-border-medium)}._purpur-text-field__input_3gl1k_26:read-only:not(:disabled){color:var(--purpur-color-text-default)}._purpur-text-field__input_3gl1k_26:read-only:not(:disabled)~._purpur-text-field__frame_3gl1k_19{border-width:var(--purpur-border-width-xs)}._purpur-text-field__input_3gl1k_26:read-only:not(:disabled):not(._purpur-text-field__input--valid_3gl1k_94):not(._purpur-text-field__input--error_3gl1k_94)~._purpur-text-field__frame_3gl1k_19{border-color:var(--purpur-color-border-medium)}._purpur-text-field__input_3gl1k_26:-internal-autofill-previewed,._purpur-text-field__input_3gl1k_26:-internal-autofill-selected{box-shadow:inset 0 0 0 1px transparent,inset 0 0 0 100px var(--purpur-color-background-interactive-auto-fill)}._purpur-text-field__input_3gl1k_26._purpur-text-field__input--valid_3gl1k_94~._purpur-text-field__frame_3gl1k_19{border-color:var(--purpur-color-border-status-success)}._purpur-text-field__input_3gl1k_26._purpur-text-field__input--error_3gl1k_94:not(:hover)~._purpur-text-field__frame_3gl1k_19{border-color:var(--purpur-color-border-status-error)}._purpur-text-field__adornment-container_3gl1k_106{display:flex;align-items:center;gap:var(--purpur-spacing-50)}._purpur-text-field__valid-icon_3gl1k_111{color:var(--purpur-color-text-status-success-medium)}._purpur-date-field__container_ci2f4_1{position:relative}._purpur-date-field__overlay_ci2f4_5{pointer-events:none;box-sizing:border-box;font-family:var(--purpur-typography-family-default);font-size:var(--purpur-typography-scale-100);font-weight:var(--purpur-typography-scale-400);color:var(--purpur-color-text-default);line-height:var(--purpur-typography-line-height-loose);white-space:pre}._purpur-date-field__start-adornment_ci2f4_16{color:var(--purpur-color-text-weak)}
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const parseToDate: (date: string) => Date;
|
|
2
|
+
export declare const parseToString: (date: Date | undefined) => string;
|
|
3
|
+
export declare const mergeRefs: <T>(...inputRefs: (React.Ref<T> | undefined)[]) => React.Ref<T> | React.RefCallback<T>;
|
|
4
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,WAAW,SAAU,MAAM,SAEvC,CAAC;AAEF,eAAO,MAAM,aAAa,SAAU,IAAI,GAAG,SAAS,WAEnD,CAAC;AAEF,eAAO,MAAM,SAAS,GAAI,CAAC,gBACX,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,EAAE,KACzC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,WAAW,CAAC,CAAC,CAiBpC,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@purpurds/date-field",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"license": "AGPL-3.0-only",
|
|
5
|
+
"main": "./dist/date-field.cjs.js",
|
|
6
|
+
"types": "./dist/date-field.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"require": "./dist/date-field.cjs.js",
|
|
10
|
+
"types": "./dist/date-field.d.ts",
|
|
11
|
+
"default": "./dist/date-field.es.js"
|
|
12
|
+
},
|
|
13
|
+
"./styles": "./dist/styles.css",
|
|
14
|
+
"./metadata": "./dist/metadata.js"
|
|
15
|
+
},
|
|
16
|
+
"source": "src/date-field.tsx",
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"classnames": "~2.5.0",
|
|
19
|
+
"date-fns": "~4.1.0",
|
|
20
|
+
"@purpurds/tokens": "7.7.0",
|
|
21
|
+
"@purpurds/field-error-text": "7.7.0",
|
|
22
|
+
"@purpurds/text-field": "7.7.0",
|
|
23
|
+
"@purpurds/field-helper-text": "7.7.0",
|
|
24
|
+
"@purpurds/icon": "7.7.0",
|
|
25
|
+
"@purpurds/label": "7.7.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"eslint": "9.24.0",
|
|
29
|
+
"@storybook/react-vite": "^9.0.18",
|
|
30
|
+
"@testing-library/dom": "~10.4.0",
|
|
31
|
+
"@testing-library/jest-dom": "~6.4.0",
|
|
32
|
+
"@testing-library/react": "~16.2.0",
|
|
33
|
+
"@testing-library/user-event": "~14.5.1",
|
|
34
|
+
"@types/node": "20.12.12",
|
|
35
|
+
"@types/react-dom": "^19.0.4",
|
|
36
|
+
"@types/react": "^19.0.10",
|
|
37
|
+
"jsdom": "~22.1.0",
|
|
38
|
+
"lint-staged": "15.5.0",
|
|
39
|
+
"prettier": "~2.8.8",
|
|
40
|
+
"react-dom": "^19.0.0",
|
|
41
|
+
"react": "^19.0.0",
|
|
42
|
+
"storybook": "^9.0.18",
|
|
43
|
+
"typescript": "^5.6.3",
|
|
44
|
+
"vite": "^6.2.1",
|
|
45
|
+
"vitest": "^3.1.2",
|
|
46
|
+
"@purpurds/component-rig": "1.0.0"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"@types/react": "^18 || ^19",
|
|
50
|
+
"@types/react-dom": "^18 || ^19",
|
|
51
|
+
"react": "^18 || ^19",
|
|
52
|
+
"react-dom": "^18 || ^19"
|
|
53
|
+
},
|
|
54
|
+
"peerDependenciesMeta": {
|
|
55
|
+
"@types/react": {
|
|
56
|
+
"optional": true
|
|
57
|
+
},
|
|
58
|
+
"@types/react-dom": {
|
|
59
|
+
"optional": true
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"scripts": {
|
|
63
|
+
"build:dev": "vite",
|
|
64
|
+
"build:watch": "vite build --watch",
|
|
65
|
+
"build": "vite build",
|
|
66
|
+
"ci:build": "rushx build",
|
|
67
|
+
"coverage": "vitest run --coverage",
|
|
68
|
+
"lint:fix": "eslint . --fix",
|
|
69
|
+
"lint": "lint-staged --no-stash 2>&1",
|
|
70
|
+
"sbdev": "rush sbdev",
|
|
71
|
+
"test:unit": "vitest run --passWithNoTests",
|
|
72
|
+
"test:watch": "vitest --watch",
|
|
73
|
+
"test": "rushx test:unit",
|
|
74
|
+
"typecheck": "tsc -p ./tsconfig.json"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
.purpur-date-field__container {
|
|
2
|
+
position: relative;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.purpur-date-field__overlay {
|
|
6
|
+
pointer-events: none;
|
|
7
|
+
box-sizing: border-box;
|
|
8
|
+
font-family: var(--purpur-typography-family-default);
|
|
9
|
+
font-size: var(--purpur-typography-scale-100);
|
|
10
|
+
font-weight: var(--purpur-typography-scale-400);
|
|
11
|
+
color: var(--purpur-color-text-default);
|
|
12
|
+
line-height: var(--purpur-typography-line-height-loose);
|
|
13
|
+
white-space: pre;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.purpur-date-field__start-adornment {
|
|
17
|
+
color: var(--purpur-color-text-weak);
|
|
18
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { IconCalendar } from "@purpurds/icon/calendar";
|
|
3
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
4
|
+
|
|
5
|
+
import "@purpurds/text-field/styles";
|
|
6
|
+
import "@purpurds/date-field/styles";
|
|
7
|
+
import "@purpurds/label/styles";
|
|
8
|
+
import "@purpurds/field-helper-text/styles";
|
|
9
|
+
import { DateField } from "./date-field";
|
|
10
|
+
|
|
11
|
+
const meta = {
|
|
12
|
+
title: "Forms and Inputs/DateField",
|
|
13
|
+
component: DateField,
|
|
14
|
+
parameters: {
|
|
15
|
+
design: [
|
|
16
|
+
{
|
|
17
|
+
name: "DateField",
|
|
18
|
+
type: "figma",
|
|
19
|
+
url: "https://www.figma.com/design/XEaIIFskrrxIBHMZDkIuIg/Purpur-DS---Component-library---guidelines?node-id=39042-5999&p=f&t=5D0qSGRGqXJgdAxm-0",
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
args: {
|
|
24
|
+
label: "Date field label",
|
|
25
|
+
helperText: "Helper text",
|
|
26
|
+
valid: false,
|
|
27
|
+
errorText: undefined,
|
|
28
|
+
disabled: false,
|
|
29
|
+
readOnly: false,
|
|
30
|
+
required: false,
|
|
31
|
+
},
|
|
32
|
+
argTypes: {
|
|
33
|
+
startAdornment: { table: { disable: true } },
|
|
34
|
+
endAdornment: { table: { disable: true } },
|
|
35
|
+
},
|
|
36
|
+
decorators: [
|
|
37
|
+
(Story) => (
|
|
38
|
+
<div style={{ maxWidth: "18.5rem" }}>
|
|
39
|
+
<Story />
|
|
40
|
+
</div>
|
|
41
|
+
),
|
|
42
|
+
],
|
|
43
|
+
} satisfies Meta<typeof DateField>;
|
|
44
|
+
|
|
45
|
+
export default meta;
|
|
46
|
+
type Story = StoryObj<typeof DateField>;
|
|
47
|
+
|
|
48
|
+
export const Showcase: Story = {
|
|
49
|
+
args: {
|
|
50
|
+
label: "Date",
|
|
51
|
+
helperText: "This is a date field",
|
|
52
|
+
},
|
|
53
|
+
render: (args) => {
|
|
54
|
+
return <DateField {...args} startAdornment={<IconCalendar size="sm" />} />;
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const Controlled: Story = {
|
|
59
|
+
args: {
|
|
60
|
+
label: "Today",
|
|
61
|
+
helperText: "This is a controlled date field",
|
|
62
|
+
},
|
|
63
|
+
render: (args) => {
|
|
64
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
65
|
+
const [value, setValue] = React.useState<Date | undefined>(new Date());
|
|
66
|
+
const selectHandler = (date: Date | undefined) => {
|
|
67
|
+
setValue(date);
|
|
68
|
+
};
|
|
69
|
+
return <DateField {...args} value={value} onChange={selectHandler} />;
|
|
70
|
+
},
|
|
71
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { DateField } from "./date-field";
|
|
6
|
+
|
|
7
|
+
describe("DateField", () => {
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
cleanup();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("renders with placeholder overlay", () => {
|
|
13
|
+
render(<DateField label="Date" />);
|
|
14
|
+
expect(screen.getByLabelText("Date")).toBeTruthy();
|
|
15
|
+
expect(screen.getAllByText("Y").length).toBe(4);
|
|
16
|
+
expect(screen.getAllByText("M").length).toBe(2);
|
|
17
|
+
expect(screen.getAllByText("D").length).toBe(2);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("renders with initial value", () => {
|
|
21
|
+
render(<DateField label="Date" value={new Date("2023-12-25")} />);
|
|
22
|
+
expect(screen.getByDisplayValue("2023-12-25")).toBeTruthy();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("calls onChange with valid date", () => {
|
|
26
|
+
const handleChange = vi.fn();
|
|
27
|
+
render(<DateField label="Date" onChange={handleChange} />);
|
|
28
|
+
const input = screen.getByLabelText("Date");
|
|
29
|
+
fireEvent.change(input, { target: { value: "2023-12-31" } });
|
|
30
|
+
expect(handleChange).toHaveBeenCalled();
|
|
31
|
+
const calledDate = handleChange.mock.calls[0][0];
|
|
32
|
+
expect(calledDate.getFullYear()).toBe(2023);
|
|
33
|
+
expect(calledDate.getMonth()).toBe(11); // December is 11
|
|
34
|
+
expect(calledDate.getDate()).toBe(31);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("does not call onChange for incomplete date", () => {
|
|
38
|
+
const handleChange = vi.fn();
|
|
39
|
+
render(<DateField label="Date" onChange={handleChange} />);
|
|
40
|
+
const input = screen.getByLabelText("Date");
|
|
41
|
+
fireEvent.change(input, { target: { value: "2023-12" } });
|
|
42
|
+
expect(handleChange).not.toHaveBeenCalled();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("formats input as user types", () => {
|
|
46
|
+
render(<DateField label="Date" />);
|
|
47
|
+
const input = screen.getByLabelText("Date") as HTMLInputElement;
|
|
48
|
+
fireEvent.change(input, { target: { value: "20231225" } });
|
|
49
|
+
expect(input.value).toBe("2023-12-25");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("forwards ref to input", () => {
|
|
53
|
+
const ref = React.createRef<HTMLInputElement>();
|
|
54
|
+
render(<DateField label="Date" ref={ref} />);
|
|
55
|
+
expect(ref.current?.tagName).toBe("INPUT");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("applies data-testid prop", () => {
|
|
59
|
+
render(<DateField label="Date" data-testid="date-field" />);
|
|
60
|
+
expect(screen.getByTestId("date-field-label")).toBeTruthy();
|
|
61
|
+
expect(screen.getByTestId("date-field-input")).toBeTruthy();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
type ForwardedRef,
|
|
3
|
+
forwardRef,
|
|
4
|
+
useEffect,
|
|
5
|
+
useLayoutEffect,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
} from "react";
|
|
9
|
+
import { TextField, type TextFieldProps } from "@purpurds/text-field";
|
|
10
|
+
import c from "classnames/bind";
|
|
11
|
+
import { isValid } from "date-fns";
|
|
12
|
+
|
|
13
|
+
import "@purpurds/text-field/styles";
|
|
14
|
+
import styles from "./date-field.module.scss";
|
|
15
|
+
import { mergeRefs, parseToDate, parseToString } from "./utils";
|
|
16
|
+
|
|
17
|
+
const cx = c.bind(styles);
|
|
18
|
+
|
|
19
|
+
type DateFieldPropsTemp = Omit<
|
|
20
|
+
TextFieldProps,
|
|
21
|
+
"type" | "value" | "onChange" | "clearButtonAriaLabel" | "clearButtonLabel" | "onClear"
|
|
22
|
+
>;
|
|
23
|
+
|
|
24
|
+
export type DateFieldProps = DateFieldPropsTemp & {
|
|
25
|
+
["data-testid"]?: string;
|
|
26
|
+
value?: Date | undefined;
|
|
27
|
+
onChange?: (date: Date | undefined) => void;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const rootClassName = "purpur-date-field";
|
|
31
|
+
const PLACEHOLDER = "YYYY-MM-DD";
|
|
32
|
+
|
|
33
|
+
export const DateField = forwardRef(
|
|
34
|
+
(
|
|
35
|
+
{ value, label, onChange, startAdornment, ...props }: DateFieldProps,
|
|
36
|
+
ref: ForwardedRef<HTMLInputElement>
|
|
37
|
+
) => {
|
|
38
|
+
const [inputValue, setInputValue] = useState<string>(parseToString(value));
|
|
39
|
+
|
|
40
|
+
// update inputValue when value prop changes
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
setInputValue(value ? parseToString(value) : "");
|
|
43
|
+
}, [value]);
|
|
44
|
+
|
|
45
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
46
|
+
const mergedRef = mergeRefs(ref, inputRef);
|
|
47
|
+
const [overlayStyle, setOverlayStyle] = useState<React.CSSProperties>({});
|
|
48
|
+
|
|
49
|
+
useLayoutEffect(() => {
|
|
50
|
+
if (inputRef.current) {
|
|
51
|
+
const labelElement = window.document.querySelector(
|
|
52
|
+
`label[for="${inputRef.current.id}"]`
|
|
53
|
+
) as HTMLLabelElement | null;
|
|
54
|
+
const labelHeight = labelElement ? labelElement.offsetHeight + 4 : 0;
|
|
55
|
+
|
|
56
|
+
const style = window.getComputedStyle(inputRef.current);
|
|
57
|
+
|
|
58
|
+
setOverlayStyle({
|
|
59
|
+
position: "absolute",
|
|
60
|
+
top: inputRef.current.offsetTop + labelHeight,
|
|
61
|
+
left: inputRef.current.offsetLeft,
|
|
62
|
+
padding: style.padding,
|
|
63
|
+
width: inputRef.current.offsetWidth,
|
|
64
|
+
height: inputRef.current.offsetHeight,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}, [inputValue, label, inputRef]);
|
|
68
|
+
|
|
69
|
+
const selectedDateCallback = (date: string) => {
|
|
70
|
+
if (date.length === 0) {
|
|
71
|
+
onChange?.(undefined);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (date.length !== PLACEHOLDER.length) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const parsedDate = parseToDate(date);
|
|
79
|
+
if (isValid(parsedDate)) {
|
|
80
|
+
onChange?.(parsedDate);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
85
|
+
const raw = e.target.value.replace(/\D/g, "");
|
|
86
|
+
// Build value with dashes at correct positions
|
|
87
|
+
let formatted = "";
|
|
88
|
+
if (raw.length > 0) formatted += raw.slice(0, 4);
|
|
89
|
+
if (raw.length > 4) formatted += "-" + raw.slice(4, 6);
|
|
90
|
+
if (raw.length > 6) formatted += "-" + raw.slice(6, 8);
|
|
91
|
+
setInputValue(formatted);
|
|
92
|
+
|
|
93
|
+
selectedDateCallback(e.target.value);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const formatOverlayText = () => {
|
|
97
|
+
const chars = PLACEHOLDER.split("");
|
|
98
|
+
return chars.map((char, i) => {
|
|
99
|
+
const userChar = inputValue[i];
|
|
100
|
+
return (
|
|
101
|
+
<span
|
|
102
|
+
key={i}
|
|
103
|
+
style={{
|
|
104
|
+
color: userChar ? "transparent" : "var(--purpur-color-text-weak)",
|
|
105
|
+
}}
|
|
106
|
+
>
|
|
107
|
+
{userChar || char}
|
|
108
|
+
</span>
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<div className={cx(`${rootClassName}__container`)}>
|
|
115
|
+
<TextField
|
|
116
|
+
{...props}
|
|
117
|
+
startAdornment={
|
|
118
|
+
startAdornment ? (
|
|
119
|
+
<span className={cx(`${rootClassName}__start-adornment`)}>{startAdornment}</span>
|
|
120
|
+
) : undefined
|
|
121
|
+
}
|
|
122
|
+
ref={mergedRef}
|
|
123
|
+
value={inputValue}
|
|
124
|
+
onChange={handleInputChange}
|
|
125
|
+
type="text"
|
|
126
|
+
maxLength={PLACEHOLDER.length}
|
|
127
|
+
inputMode="numeric"
|
|
128
|
+
label={label}
|
|
129
|
+
/>
|
|
130
|
+
<div role="presentation" style={overlayStyle} className={cx(`${rootClassName}__overlay`)}>
|
|
131
|
+
{formatOverlayText()}
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
DateField.displayName = "DateField";
|
package/src/global.d.ts
ADDED
package/src/utils.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { format, parse } from "date-fns";
|
|
2
|
+
|
|
3
|
+
export const parseToDate = (date: string) => {
|
|
4
|
+
return parse(date, "yyyy-MM-dd", new Date());
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const parseToString = (date: Date | undefined) => {
|
|
8
|
+
return date ? format(date, "yyyy-MM-dd") : "";
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const mergeRefs = <T>(
|
|
12
|
+
...inputRefs: (React.Ref<T> | undefined)[]
|
|
13
|
+
): React.Ref<T> | React.RefCallback<T> => {
|
|
14
|
+
const filteredInputRefs = inputRefs.filter(Boolean);
|
|
15
|
+
|
|
16
|
+
if (filteredInputRefs.length <= 1) {
|
|
17
|
+
const firstRef = filteredInputRefs[0];
|
|
18
|
+
return firstRef || null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return (ref: T | null) => {
|
|
22
|
+
for (const inputRef of filteredInputRefs) {
|
|
23
|
+
if (typeof inputRef === "function") {
|
|
24
|
+
inputRef(ref);
|
|
25
|
+
} else if (inputRef) {
|
|
26
|
+
(inputRef as React.RefObject<T | null>).current = ref;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
};
|