diff --git a/packages/react/package.json b/packages/react/package.json
index 55f88b35d..8a9cafe84 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -50,8 +50,6 @@
"@0no-co/graphql.web": "^1.0.4",
"@gadgetinc/api-client-core": "^0.15.24",
"@hookform/resolvers": "^3.3.1",
- "date-fns": "^2.30.0",
- "date-fns-tz": "^2.0.0",
"filesize": "^10.1.2",
"pluralize": "^8.0.0",
"react-fast-compare": "^3.2.2",
@@ -113,7 +111,9 @@
"setup-polly-jest": "^0.11.0",
"storybook": "^8.1.6",
"tmp": "^0.2.3",
- "wonka": "^6.3.2"
+ "wonka": "^6.3.2",
+ "date-fns": "^2.30.0",
+ "date-fns-tz": "^2.0.0"
},
"peerDependencies": {
"@mdxeditor/editor": "^3.8.0",
diff --git a/packages/react/src/auto/mui/inputs/MUIAutoDateTimePicker.tsx b/packages/react/src/auto/mui/inputs/MUIAutoDateTimePicker.tsx
index 5ba4b8e15..0fdf76cd5 100644
--- a/packages/react/src/auto/mui/inputs/MUIAutoDateTimePicker.tsx
+++ b/packages/react/src/auto/mui/inputs/MUIAutoDateTimePicker.tsx
@@ -1,8 +1,8 @@
import { Box } from "@mui/material";
import { DatePicker, TimePicker } from "@mui/x-date-pickers";
-import { zonedTimeToUtc } from "date-fns-tz";
import React from "react";
import { useController } from "react-hook-form";
+import { zonedTimeToUtc } from "../../../dateTimeUtils.js";
import type { GadgetDateTimeConfig } from "../../../internal/gql/graphql.js";
import { useFieldMetadata } from "../../hooks/useFieldMetadata.js";
diff --git a/packages/react/src/auto/polaris/inputs/PolarisAutoDateTimePicker.tsx b/packages/react/src/auto/polaris/inputs/PolarisAutoDateTimePicker.tsx
index 46f911c00..b726a7d8b 100644
--- a/packages/react/src/auto/polaris/inputs/PolarisAutoDateTimePicker.tsx
+++ b/packages/react/src/auto/polaris/inputs/PolarisAutoDateTimePicker.tsx
@@ -1,10 +1,9 @@
import type { DatePickerProps, TextFieldProps } from "@shopify/polaris";
import { DatePicker, Icon, InlineStack, Popover, TextField } from "@shopify/polaris";
import { CalendarIcon } from "@shopify/polaris-icons";
-import { format, isValid } from "date-fns";
-import { utcToZonedTime, zonedTimeToUtc } from "date-fns-tz";
import React, { useCallback, useMemo, useState } from "react";
import { useController } from "react-hook-form";
+import { formatShortDateString, isValidDate, utcToZonedTime, zonedTimeToUtc } from "../../../dateTimeUtils.js";
import type { GadgetDateTimeConfig } from "../../../internal/gql/graphql.js";
import { useFieldMetadata } from "../../hooks/useFieldMetadata.js";
import type { DateTimeState } from "./PolarisAutoTimePicker.js";
@@ -60,7 +59,7 @@ export const PolarisAutoDateTimePicker = (props: {
const { onChange, value } = props;
const localTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
const localTime = useMemo(() => {
- return value ? value : isValid(new Date(fieldProps.value)) ? new Date(fieldProps.value) : undefined;
+ return value ? value : isValidDate(new Date(fieldProps.value)) ? new Date(fieldProps.value) : undefined;
}, [value, fieldProps.value]);
const [datePopoverActive, setDatePopoverActive] = useState(false);
@@ -74,7 +73,7 @@ export const PolarisAutoDateTimePicker = (props: {
(range) => {
(fieldProps || value) && copyTime(range.start, zonedTimeToUtc(range.start, localTz));
const dateOverride = value ?? new Date(fieldProps.value);
- if (isValid(dateOverride)) {
+ if (isValidDate(dateOverride)) {
range.start.setHours(dateOverride.getHours());
range.start.setMinutes(dateOverride.getMinutes());
range.start.setSeconds(dateOverride.getSeconds());
@@ -88,8 +87,8 @@ export const PolarisAutoDateTimePicker = (props: {
);
const toggleDatePopoverActive = useCallback(() => {
- setPopoverMonth(getDateTimeObjectFromDate(isValid(localTime) && localTime ? localTime : new Date()).month);
- setPopoverYear(getDateTimeObjectFromDate(isValid(localTime) && localTime ? localTime : new Date()).year);
+ setPopoverMonth(getDateTimeObjectFromDate(isValidDate(localTime) && localTime ? localTime : new Date()).month);
+ setPopoverYear(getDateTimeObjectFromDate(isValidDate(localTime) && localTime ? localTime : new Date()).year);
setDatePopoverActive((active) => !active);
}, [localTime]);
const handleMonthChange = useCallback((month: number, year: number) => {
@@ -108,7 +107,7 @@ export const PolarisAutoDateTimePicker = (props: {
label={metadata.name ?? "Date"}
prefix={}
autoComplete="off"
- value={localTime ? format(localTime, "yyyy-MM-dd") : ""}
+ value={localTime ? formatShortDateString(localTime) : ""}
onFocus={toggleDatePopoverActive}
error={props.error}
/>
diff --git a/packages/react/src/auto/polaris/inputs/PolarisAutoTimePicker.tsx b/packages/react/src/auto/polaris/inputs/PolarisAutoTimePicker.tsx
index 70b822add..e01f9915d 100644
--- a/packages/react/src/auto/polaris/inputs/PolarisAutoTimePicker.tsx
+++ b/packages/react/src/auto/polaris/inputs/PolarisAutoTimePicker.tsx
@@ -1,9 +1,9 @@
import type { TextFieldProps } from "@shopify/polaris";
import { Box, Icon, Listbox, Popover, Scrollable, Text, TextField } from "@shopify/polaris";
import { ClockIcon } from "@shopify/polaris-icons";
-import { zonedTimeToUtc } from "date-fns-tz";
import React, { useEffect, useState } from "react";
import type { ControllerRenderProps, FieldValues } from "react-hook-form";
+import { zonedTimeToUtc } from "../../../dateTimeUtils.js";
import { copyTime, getDateFromDateTimeObject, getDateTimeObjectFromDate } from "./PolarisAutoDateTimePicker.js";
const createMarkup = (
diff --git a/packages/react/src/auto/polaris/tableCells/PolarisAutoTableDateTimeCell.tsx b/packages/react/src/auto/polaris/tableCells/PolarisAutoTableDateTimeCell.tsx
index 5f9c5c5f0..c14b59c1e 100644
--- a/packages/react/src/auto/polaris/tableCells/PolarisAutoTableDateTimeCell.tsx
+++ b/packages/react/src/auto/polaris/tableCells/PolarisAutoTableDateTimeCell.tsx
@@ -1,11 +1,9 @@
-import { format } from "date-fns";
import React from "react";
+import { formatLongDateTimeString } from "../../../dateTimeUtils.js";
import { PolarisAutoTableTextCell } from "./PolarisAutoTableTextCell.js";
export const PolarisAutoTableDateTimeCell = (props: { value: Date; includeTime: boolean }) => {
const { value, includeTime } = props;
- const timeFormat = includeTime ? "LLL d, y K:mm a" : "LLL d, y";
-
- return value instanceof Date ? : null;
+ return value instanceof Date ? : null;
};
diff --git a/packages/react/src/dateTimeUtils.ts b/packages/react/src/dateTimeUtils.ts
new file mode 100644
index 000000000..b19d5bbf0
--- /dev/null
+++ b/packages/react/src/dateTimeUtils.ts
@@ -0,0 +1,375 @@
+/**
+ * Formats a date object in a "yyyy-MM-dd" format
+ * Ex: 2000-01-30
+ * @param date
+ * @returns Formatted date string, if date is undefined, formatShortDateString returns an empty string
+ */
+export const formatShortDateString = (date: Date) => {
+ if (!date) return "";
+
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, "0");
+ const day = String(date.getDate()).padStart(2, "0");
+
+ return `${year}-${month}-${day}`;
+};
+
+/**
+ * Formats a date object in a "LLL d, y K:mm a" format
+ * Ex: Jun 30, 2024 8:00 PM
+ * @param date
+ * @returns Formatted date string, if date is undefined, formatShortDateString returns an empty string
+ */
+export const formatLongDateTimeString = (date: Date, includeTime: boolean) => {
+ if (!date) return "";
+
+ const dateString = date.toLocaleDateString("en-US", {
+ year: "numeric",
+ month: "short",
+ day: "numeric",
+ });
+
+ if (includeTime) {
+ const timeString = date.toLocaleTimeString("en-US", {
+ hour: "numeric",
+ minute: "2-digit",
+ hour12: true,
+ });
+
+ return `${dateString} ${timeString}`;
+ } else return dateString;
+};
+
+/**
+ *
+ * @param date
+ * @returns
+ */
+export const isValidDate = (date: unknown) => {
+ return date instanceof Date && !isNaN(date.getTime());
+};
+
+/**
+ * @name zonedTimeToUtc
+ * (called "toZonedTime" in the original date-fns-tz repo)
+ * @category Time Zone Helpers
+ * @summary Get the UTC date/time from a date representing local time in a given time zone
+ * @author https://github.com/marnusw
+ * Taken from https://github.com/marnusw/date-fns-tz
+ *
+ * @description
+ * Returns a date instance with the UTC time of the provided date of which the values
+ * represented the local time in the time zone specified. In other words, if the input
+ * date represented local time in time zone, the timestamp of the output date will
+ * give the equivalent UTC of that local time regardless of the current system time zone.
+ *
+ * @param date the date with values representing the local time
+ * @param timeZone the time zone of this local time, can be an offset or IANA time zone
+ * @param options the object with options. See [Options]{@link https://date-fns.org/docs/Options}
+ * @param {0|1|2} [options.additionalDigits=2] - passed to `toDate`. See [toDate]{@link https://date-fns.org/docs/toDate}
+ * @throws {TypeError} 2 arguments required
+ * @throws {RangeError} `options.additionalDigits` must be 0, 1 or 2
+ *
+ * @example
+ * // In June 10am in Los Angeles is 5pm UTC
+ * const result = fromZonedTime(new Date(2014, 5, 25, 10, 0, 0), 'America/Los_Angeles')
+ * //=> 2014-06-25T17:00:00.000Z
+ */
+export const zonedTimeToUtc = (date: Date, timeZone: string): Date => {
+ const utc = newDateUTC(
+ date.getFullYear(),
+ date.getMonth(),
+ date.getDate(),
+ date.getHours(),
+ date.getMinutes(),
+ date.getSeconds(),
+ date.getMilliseconds()
+ ).getTime();
+
+ const offsetMilliseconds = tzParseTimezone(timeZone, new Date(utc));
+
+ return new Date(utc + offsetMilliseconds);
+};
+
+/**
+ * @name utcToZonedTime
+ * (called "toZonedTime" in the original date-fns-tz repo)
+ * @category Time Zone Helpers
+ * @summary Get a date/time representing local time in a given time zone from the UTC date
+ * @author https://github.com/marnusw
+ * Taken from https://github.com/marnusw/date-fns-tz
+ *
+ * @description
+ * Returns a date instance with values representing the local time in the time zone
+ * specified of the UTC time from the date provided. In other words, when the new date
+ * is formatted it will show the equivalent hours in the target time zone regardless
+ * of the current system time zone.
+ *
+ * @param date the date with the relevant UTC time
+ * @param timeZone the time zone to get local time for, can be an offset or IANA time zone
+ * @param options the object with options. See [Options]{@link https://date-fns.org/docs/Options}
+ * @param {0|1|2} [options.additionalDigits=2] - passed to `toDate`. See [toDate]{@link https://date-fns.org/docs/toDate}
+ *
+ * @throws {TypeError} 2 arguments required
+ * @throws {RangeError} `options.additionalDigits` must be 0, 1 or 2
+ *
+ * @example
+ * // In June 10am UTC is 6am in New York (-04:00)
+ * const result = toZonedTime('2014-06-25T10:00:00.000Z', 'America/New_York')
+ * //=> Jun 25 2014 06:00:00
+ */
+export const utcToZonedTime = (date: Date, timeZone: string): Date => {
+ const offsetMilliseconds = tzParseTimezone(timeZone, date, true);
+
+ const d = new Date(date.getTime() - offsetMilliseconds);
+
+ const resultDate = new Date(0);
+
+ resultDate.setFullYear(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
+
+ resultDate.setHours(d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), d.getUTCMilliseconds());
+
+ return resultDate;
+};
+
+// Helper functions for utcToZonedTime
+const patterns = {
+ timezone: /([Z+-].*)$/,
+ timezoneZ: /^(Z)$/,
+ timezoneHH: /^([+-]\d{2})$/,
+ timezoneHHMM: /^([+-])(\d{2}):?(\d{2})$/,
+};
+
+const validIANATimezoneCache: Record = {};
+
+const MILLISECONDS_IN_HOUR = 3600000;
+const MILLISECONDS_IN_MINUTE = 60000;
+
+const isValidTimezoneIANAString = (timeZoneString: string) => {
+ if (validIANATimezoneCache[timeZoneString]) return true;
+ try {
+ new Intl.DateTimeFormat(undefined, { timeZone: timeZoneString });
+ validIANATimezoneCache[timeZoneString] = true;
+ return true;
+ } catch (error) {
+ return false;
+ }
+};
+
+export const tzParseTimezone = (timezoneString: string | undefined, date: Date | number | undefined, isUtcDate?: boolean): number => {
+ // Empty string
+ if (!timezoneString) {
+ return 0;
+ }
+
+ // Z
+ let token = patterns.timezoneZ.exec(timezoneString);
+ if (token) {
+ return 0;
+ }
+
+ let hours: number;
+ let absoluteOffset: number;
+
+ // ±hh
+ token = patterns.timezoneHH.exec(timezoneString);
+ if (token) {
+ hours = parseInt(token[1], 10);
+
+ if (!validateTimezone(hours)) {
+ return NaN;
+ }
+
+ return -(hours * MILLISECONDS_IN_HOUR);
+ }
+
+ // ±hh:mm or ±hhmm
+ token = patterns.timezoneHHMM.exec(timezoneString);
+ if (token) {
+ hours = parseInt(token[2], 10);
+ const minutes = parseInt(token[3], 10);
+
+ if (!validateTimezone(hours, minutes)) {
+ return NaN;
+ }
+
+ absoluteOffset = Math.abs(hours) * MILLISECONDS_IN_HOUR + minutes * MILLISECONDS_IN_MINUTE;
+ return token[1] === "+" ? -absoluteOffset : absoluteOffset;
+ }
+
+ const fixOffset = (date: Date, offset: number, timezoneString: string) => {
+ const localTS = date.getTime();
+
+ // Our UTC time is just a guess because our offset is just a guess
+ let utcGuess = localTS - offset;
+
+ // Test whether the zone matches the offset for this ts
+ const o2 = calcOffset(new Date(utcGuess), timezoneString);
+
+ // If so, offset didn't change, and we're done
+ if (offset === o2) {
+ return offset;
+ }
+
+ // If not, change the ts by the difference in the offset
+ utcGuess -= o2 - offset;
+
+ // If that gives us the local time we want, we're done
+ const o3 = calcOffset(new Date(utcGuess), timezoneString);
+ if (o2 === o3) {
+ return o2;
+ }
+
+ // If it's different, we're in a hole time. The offset has changed, but we don't adjust the time
+ return Math.max(o2, o3);
+ };
+
+ // IANA time zone
+ if (isValidTimezoneIANAString(timezoneString)) {
+ date = new Date(date || Date.now());
+ const utcDate = isUtcDate ? date : toUtcDate(date);
+
+ const offset = calcOffset(utcDate, timezoneString);
+
+ const fixedOffset = isUtcDate ? offset : fixOffset(date, offset, timezoneString);
+
+ return -fixedOffset;
+ }
+
+ return NaN;
+};
+
+const validateTimezone = (hours: number, minutes?: number | null) => {
+ return -23 <= hours && hours <= 23 && (minutes == null || (0 <= minutes && minutes <= 59));
+};
+
+const calcOffset = (date: Date, timezoneString: string) => {
+ const tokens = tzTokenizeDate(date, timezoneString);
+
+ // ms dropped because it's not provided by tzTokenizeDate
+ const asUTC = newDateUTC(tokens[0], tokens[1] - 1, tokens[2], tokens[3] % 24, tokens[4], tokens[5], 0).getTime();
+
+ let asTS = date.getTime();
+ const over = asTS % 1000;
+ asTS -= over >= 0 ? over : 1000 + over;
+ return asUTC - asTS;
+};
+
+const toUtcDate = (date: Date) => {
+ return newDateUTC(
+ date.getFullYear(),
+ date.getMonth(),
+ date.getDate(),
+ date.getHours(),
+ date.getMinutes(),
+ date.getSeconds(),
+ date.getMilliseconds()
+ );
+};
+
+const newDateUTC = (
+ fullYear: number,
+ month: number,
+ day: number,
+ hour: number,
+ minute: number,
+ second: number,
+ millisecond: number
+): Date => {
+ const utcDate = new Date(0);
+ utcDate.setUTCFullYear(fullYear, month, day);
+ utcDate.setUTCHours(hour, minute, second, millisecond);
+ return utcDate;
+};
+
+const tzTokenizeDate = (date: Date, timeZone: string): number[] => {
+ const dtf = getDateTimeFormat(timeZone);
+ return "formatToParts" in dtf ? partsOffset(dtf, date) : hackyOffset(dtf, date);
+};
+
+const dtfCache: Record = {};
+
+const getDateTimeFormat = (timeZone: string) => {
+ if (!dtfCache[timeZone]) {
+ // New browsers use `hourCycle`, IE and Chrome <73 does not support it and uses `hour12`
+ const testDateFormatted = new Intl.DateTimeFormat("en-US", {
+ hourCycle: "h23",
+ timeZone: "America/New_York",
+ year: "numeric",
+ month: "2-digit",
+ day: "2-digit",
+ hour: "2-digit",
+ minute: "2-digit",
+ second: "2-digit",
+ }).format(new Date("2014-06-25T04:00:00.123Z"));
+ const hourCycleSupported = testDateFormatted === "06/25/2014, 00:00:00" || testDateFormatted === "06/25/2014 00:00:00";
+
+ dtfCache[timeZone] = hourCycleSupported
+ ? new Intl.DateTimeFormat("en-US", {
+ hourCycle: "h23",
+ timeZone: timeZone,
+ year: "numeric",
+ month: "numeric",
+ day: "2-digit",
+ hour: "2-digit",
+ minute: "2-digit",
+ second: "2-digit",
+ })
+ : new Intl.DateTimeFormat("en-US", {
+ hour12: false,
+ timeZone: timeZone,
+ year: "numeric",
+ month: "numeric",
+ day: "2-digit",
+ hour: "2-digit",
+ minute: "2-digit",
+ second: "2-digit",
+ });
+ }
+ return dtfCache[timeZone];
+};
+
+const typeToPos: { [type in keyof Intl.DateTimeFormatPartTypesRegistry]?: number } = {
+ year: 0,
+ month: 1,
+ day: 2,
+ hour: 3,
+ minute: 4,
+ second: 5,
+};
+
+const partsOffset = (dtf: Intl.DateTimeFormat, date: Date) => {
+ try {
+ const formatted = dtf.formatToParts(date);
+ const filled: number[] = [];
+ for (let i = 0; i < formatted.length; i++) {
+ const pos = typeToPos[formatted[i].type];
+
+ if (pos !== undefined) {
+ filled[pos] = parseInt(formatted[i].value, 10);
+ }
+ }
+ return filled;
+ } catch (error) {
+ if (error instanceof RangeError) {
+ return [NaN];
+ }
+ throw error;
+ }
+};
+
+const hackyOffset = (dtf: Intl.DateTimeFormat, date: Date) => {
+ const formatted = dtf.format(date);
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const parsed = /(\d+)\/(\d+)\/(\d+),? (\d+):(\d+):(\d+)/.exec(formatted)!;
+ // const [, fMonth, fDay, fYear, fHour, fMinute, fSecond] = parsed
+ // return [fYear, fMonth, fDay, fHour, fMinute, fSecond]
+ return [
+ parseInt(parsed[3], 10),
+ parseInt(parsed[1], 10),
+ parseInt(parsed[2], 10),
+ parseInt(parsed[4], 10),
+ parseInt(parsed[5], 10),
+ parseInt(parsed[6], 10),
+ ];
+};
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 19b3eff72..20d34a303 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -244,12 +244,6 @@ importers:
'@mui/x-date-pickers':
specifier: ^6.14.0
version: 6.20.1(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@mui/material@5.15.19)(@mui/system@5.15.20)(@types/react@18.2.79)(date-fns@2.30.0)(react-dom@18.2.0)(react@18.2.0)
- date-fns:
- specifier: ^2.30.0
- version: 2.30.0
- date-fns-tz:
- specifier: ^2.0.0
- version: 2.0.1(date-fns@2.30.0)
filesize:
specifier: ^10.1.2
version: 10.1.2
@@ -395,6 +389,12 @@ importers:
cypress-each:
specifier: ^1.13.3
version: 1.14.0
+ date-fns:
+ specifier: ^2.30.0
+ version: 2.30.0
+ date-fns-tz:
+ specifier: ^2.0.0
+ version: 2.0.1(date-fns@2.30.0)
execa:
specifier: ^5.1.1
version: 5.1.1
@@ -10796,14 +10796,13 @@ packages:
date-fns: 2.x
dependencies:
date-fns: 2.30.0
- dev: false
+ dev: true
/date-fns@2.30.0:
resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==}
engines: {node: '>=0.11'}
dependencies:
'@babel/runtime': 7.24.4
- dev: false
/dayjs@1.11.11:
resolution: {integrity: sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==}