UNPKG

@liveblocks/react-ui

Version:

A set of React pre-built components for the Liveblocks products. Liveblocks is the all-in-one toolkit to build collaborative products like Figma, Notion, and more.

1 lines 10.3 kB
{"version":3,"file":"Duration.cjs","sources":["../../src/primitives/Duration.tsx"],"sourcesContent":["\"use client\";\n\nimport type { Relax } from \"@liveblocks/core\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { forwardRef, type ReactNode, useMemo } from \"react\";\n\nimport type { ComponentPropsWithSlot } from \"../types\";\nimport { numberFormat } from \"../utils/intl\";\nimport { useInterval } from \"../utils/use-interval\";\nimport { useRerender } from \"../utils/use-rerender\";\n\nconst RENDER_INTERVAL = 0.5 * 1000; // 0.5 second\n\nconst DURATION_NAME = \"Duration\";\n\nexport type DurationProps = Omit<\n ComponentPropsWithSlot<\"time\">,\n \"children\" | \"title\"\n> &\n Relax<\n | {\n /**\n * The duration in milliseconds.\n * If provided, `from` and `to` will be ignored.\n */\n duration: number;\n }\n | {\n /**\n * The date at which the duration starts.\n * If provided, `duration` will be ignored.\n * If provided without `to` it means that the duration is in progress,\n * and the component will re-render at an interval, customizable with\n * the `interval` prop.\n */\n from: Date | string | number;\n\n /**\n * The date at which the duration ends.\n * If `from` is provided without `to`, `Date.now()` will be used.\n */\n to?: Date | string | number;\n }\n > & {\n /**\n * A function to format the displayed duration.\n */\n children?: (duration: number, locale?: string) => ReactNode;\n\n /**\n * The `title` attribute's value or a function to format it.\n */\n title?: string | ((duration: number, locale?: string) => string);\n\n /**\n * The interval in milliseconds at which the component will re-render if\n * `from` is provided without `to`, meaning that the duration is in progress.\n * Can be set to `false` to disable re-rendering.\n */\n interval?: number | false;\n\n /**\n * The locale used when formatting the duration.\n */\n locale?: string;\n };\n\ninterface DurationParts {\n weeks: number;\n days: number;\n hours: number;\n minutes: number;\n seconds: number;\n milliseconds: number;\n}\n\nfunction getDurationParts(duration: number): DurationParts {\n let remaining = Math.max(duration, 0);\n\n const milliseconds = remaining % 1000;\n remaining = Math.floor(remaining / 1000);\n\n const seconds = remaining % 60;\n remaining = Math.floor(remaining / 60);\n\n const minutes = remaining % 60;\n remaining = Math.floor(remaining / 60);\n\n const hours = remaining % 24;\n remaining = Math.floor(remaining / 24);\n\n const days = remaining % 7;\n const weeks = Math.floor(remaining / 7);\n\n return { weeks, days, hours, minutes, seconds, milliseconds };\n}\n\nconst durationPartsToNumberFormatOptions: Record<\n keyof DurationParts,\n Intl.NumberFormatOptions[\"unit\"]\n> = {\n weeks: \"week\",\n days: \"day\",\n hours: \"hour\",\n minutes: \"minute\",\n seconds: \"second\",\n milliseconds: \"millisecond\",\n};\n\n/**\n * Formats a duration in a short format.\n * TODO: Use `Intl.DurationFormat` when it's better supported.\n */\nfunction formatShortDuration(duration: number, locale?: string) {\n let resolvedLocale: string;\n\n if (locale) {\n resolvedLocale = locale;\n } else {\n const formatter = numberFormat();\n\n resolvedLocale = formatter.resolvedOptions().locale;\n }\n\n const parts = getDurationParts(duration);\n const formattedParts: string[] = [];\n\n for (const [unit, value] of Object.entries(parts) as [\n keyof DurationParts,\n number,\n ][]) {\n if (value === 0 || unit === \"milliseconds\") {\n continue;\n }\n\n const formatter = numberFormat(resolvedLocale, {\n style: \"unit\",\n unit: durationPartsToNumberFormatOptions[unit],\n unitDisplay: \"narrow\",\n });\n\n formattedParts.push(formatter.format(value));\n }\n\n if (!formattedParts.length) {\n formattedParts.push(\n numberFormat(resolvedLocale, {\n style: \"unit\",\n unit: \"second\",\n unitDisplay: \"narrow\",\n }).format(0)\n );\n }\n\n return formattedParts.join(\" \");\n}\n\n/**\n * Formats a duration in a longer format.\n * TODO: Use `Intl.DurationFormat` when it's better supported.\n */\nfunction formatVerboseDuration(duration: number, locale?: string) {\n let resolvedLocale: string;\n\n if (locale) {\n resolvedLocale = locale;\n } else {\n const formatter = numberFormat();\n\n resolvedLocale = formatter.resolvedOptions().locale;\n }\n\n const parts = getDurationParts(duration);\n const formattedParts: string[] = [];\n\n for (const [unit, value] of Object.entries(parts) as [\n keyof DurationParts,\n number,\n ][]) {\n if (value === 0 || unit === \"milliseconds\") {\n continue;\n }\n\n const formatter = numberFormat(resolvedLocale, {\n style: \"unit\",\n unit: durationPartsToNumberFormatOptions[unit],\n unitDisplay: \"long\",\n });\n\n formattedParts.push(formatter.format(value));\n }\n\n if (!formattedParts.length) {\n formattedParts.push(\n numberFormat(resolvedLocale, {\n style: \"unit\",\n unit: \"second\",\n unitDisplay: \"long\",\n }).format(0)\n );\n }\n\n return formattedParts.join(\" \");\n}\n\n/**\n * Formats a duration as ISO 8601.\n * TODO: Use `Temporal.Duration` when it's better supported.\n */\nexport function formatIso8601Duration(duration: number) {\n const normalizedDuration = Math.max(duration, 0);\n\n if (normalizedDuration === 0) {\n return \"PT0S\";\n }\n\n const { weeks, days, hours, minutes, seconds, milliseconds } =\n getDurationParts(normalizedDuration);\n\n let isoDuration = \"P\";\n\n // 1. Weeks\n if (weeks > 0) {\n isoDuration += `${weeks}W`;\n }\n\n // 2. Days\n if (days > 0) {\n isoDuration += `${days}D`;\n }\n\n if (hours > 0 || minutes > 0 || seconds > 0 || milliseconds > 0) {\n isoDuration += \"T\";\n\n // 3. Hours\n if (hours > 0) {\n isoDuration += `${hours}H`;\n }\n\n // 4. Minutes\n if (minutes > 0) {\n isoDuration += `${minutes}M`;\n }\n\n // 5. Seconds and milliseconds\n if (seconds > 0 || milliseconds > 0) {\n if (milliseconds > 0) {\n isoDuration += `${seconds}.${milliseconds.toString().padStart(3, \"0\").replace(/0+$/, \"\")}S`;\n } else {\n isoDuration += `${seconds}S`;\n }\n }\n }\n\n return isoDuration;\n}\n\n/**\n * Converts a Date or Date-like value to a timestamp in milliseconds.\n */\nfunction getDateTime(date: Date | string | number) {\n if (date instanceof Date) {\n return date.getTime();\n }\n\n return new Date(date).getTime();\n}\n\n/**\n * Get a duration between two Date or Date-like values.\n */\nexport function getDuration(\n from: Date | string | number,\n to: Date | string | number\n) {\n return getDateTime(to) - getDateTime(from);\n}\n\n/**\n * Displays a formatted duration, and automatically re-renders to if the\n * duration is in progress.\n *\n * @example\n * <Duration duration={3 * 60 * 1000} />\n *\n * @example\n * <Duration from={fiveHoursAgoDate} />\n *\n * @example\n * <Duration from={fiveHoursAgoDate} to={oneHourAgoDate} />\n */\nexport const Duration = forwardRef<HTMLTimeElement, DurationProps>(\n (\n {\n duration,\n from,\n to,\n locale,\n dateTime,\n title: renderTitle = formatVerboseDuration,\n children: renderChildren = formatShortDuration,\n interval = RENDER_INTERVAL,\n asChild,\n ...props\n },\n forwardedRef\n ) => {\n const Component = asChild ? Slot : \"time\";\n const [rerender, key] = useRerender();\n const resolvedDuration = useMemo(() => {\n if (duration !== undefined) {\n return duration;\n }\n\n if (from !== undefined) {\n return getDuration(from, to ?? Date.now());\n }\n\n return 0;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [duration, from, to, key]);\n const normalizedDuration = useMemo(\n () => formatIso8601Duration(resolvedDuration),\n [resolvedDuration]\n );\n const title = useMemo(\n () =>\n typeof renderTitle === \"function\"\n ? renderTitle(resolvedDuration, locale)\n : renderTitle,\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [renderTitle, resolvedDuration, locale]\n );\n const children = useMemo(\n () =>\n typeof renderChildren === \"function\"\n ? renderChildren(resolvedDuration, locale)\n : renderChildren,\n\n [renderChildren, resolvedDuration, locale]\n );\n\n // Only re-render if the duration is in progress.\n useInterval(\n rerender,\n from !== undefined && to === undefined ? interval : false\n );\n\n return (\n <Component\n {...props}\n ref={forwardedRef}\n dateTime={dateTime ?? normalizedDuration}\n title={title}\n >\n {children}\n </Component>\n );\n }\n);\n\nif (process.env.NODE_ENV !== \"production\") {\n Duration.displayName = DURATION_NAME;\n}\n"],"names":[],"mappings":";;;;;;;;;;;AAWA;AAEA;AA+DA;AACE;AAEA;AACA;AAEA;AACA;AAEA;AACA;AAEA;AACA;AAEA;AACA;AAEA;AACF;AAEA;AAGI;AACK;AACD;AACC;AACE;AACA;AAEX;AAMA;AACE;AAEA;AACE;AAAiB;AAEjB;AAEA;AAA6C;AAG/C;AACA;AAEA;AAIE;AACE;AAAA;AAGF;AAA+C;AACtC;AACsC;AAChC;AAGf;AAA2C;AAG7C;AACE;AAAe;AACgB;AACpB;AACD;AACO;AACJ;AACb;AAGF;AACF;AAMA;AACE;AAEA;AACE;AAAiB;AAEjB;AAEA;AAA6C;AAG/C;AACA;AAEA;AAIE;AACE;AAAA;AAGF;AAA+C;AACtC;AACsC;AAChC;AAGf;AAA2C;AAG7C;AACE;AAAe;AACgB;AACpB;AACD;AACO;AACJ;AACb;AAGF;AACF;AAMO;AACL;AAEA;AACE;AAAO;AAGT;AAGA;AAGA;AACE;AAAuB;AAIzB;AACE;AAAsB;AAGxB;AACE;AAGA;AACE;AAAuB;AAIzB;AACE;AAAyB;AAI3B;AACE;AACE;AAAwF;AAExF;AAAyB;AAC3B;AACF;AAGF;AACF;AAKA;AACE;AACE;AAAoB;AAGtB;AACF;AAKgB;AAId;AACF;AAeO;AAAiB;AAEpB;AACE;AACA;AACA;AACA;AACA;AACqB;AACM;AAChB;AACX;AACG;AAIL;AACA;AACA;AACE;AACE;AAAO;AAGT;AACE;AAAyC;AAG3C;AAAO;AAGT;AAA2B;AACmB;AAC3B;AAEnB;AAAc;AAIN;AAAA;AAEgC;AAExC;AAAiB;AAIT;AAEmC;AAI3C;AAAA;AACE;AACoD;AAGtD;AACE;AAAC;AAAA;AACK;AACC;AACiB;AACtB;AAEC;AAAA;AACH;AAGN;AAEA;AACE;AACF;;;;"}