fix(ui,dashboard): Revamp DatePicker component (#7891)
**What** - Revamps the DatePicker component. - Addresses all issues with broken DatePickers across admin. **Note** - Part of this PR is adding a I18nProvider which is used to set the locale that is used for our DatePicker and Calendar components. Per default they use the browser locale. In the current implementation, we are grabbing the locale to use from the language that is picked in the "Profile" section. This means that currently the only possible locale is "en-US", meaning times uses AM/PM. This is likely not what we want, but we need to make a decision on how we want to handle this globally, will create a ticket for it and we can then clean it up later on. - This PR does not include "presets" or a DateRange picker that were part of the old implementation. Will open tickets to re-add this later on, but since we aren't using it in admin any where it makes sense to address later. - This PR also bumps and pin every `@radix-ui` dependency in `@medusajs/ui` and `@medusajs/dashboard`. Our different versions were pulling in multiple versions of internal radix dependencies which were breaking Popover and Dialog behaviour across admin. One thing to note is that Radix have started to print warnings for missing Descriptions and Titles in dialogs. We should add these as we go, for better accessibility. Its not an urgent task but something we can add as we clean up admin over the following weeks. CLOSES CORE-2382
This commit is contained in:
1 parent
074e4a888e
commit
a84e5a6ced
47 files changed
+2874
-2424
No files matched your search
@@ -35,14 +35,14 @@
|
|||||||
"@medusajs/icons": "1.2.1",
|
"@medusajs/icons": "1.2.1",
|
||||||
"@medusajs/js-sdk": "0.0.1",
|
"@medusajs/js-sdk": "0.0.1",
|
||||||
"@medusajs/ui": "3.0.0",
|
"@medusajs/ui": "3.0.0",
|
||||||
"@radix-ui/react-collapsible": "1.0.3",
|
"@radix-ui/react-collapsible": "1.1.0",
|
||||||
"@radix-ui/react-hover-card": "^1.0.7",
|
"@radix-ui/react-hover-card": "1.1.1",
|
||||||
"@tanstack/react-query": "^5.28.14",
|
"@tanstack/react-query": "^5.28.14",
|
||||||
"@tanstack/react-table": "8.10.7",
|
"@tanstack/react-table": "8.10.7",
|
||||||
"@tanstack/react-virtual": "^3.0.4",
|
"@tanstack/react-virtual": "^3.0.4",
|
||||||
"@uiw/react-json-view": "^2.0.0-alpha.17",
|
"@uiw/react-json-view": "^2.0.0-alpha.17",
|
||||||
"cmdk": "^0.2.0",
|
"cmdk": "^0.2.0",
|
||||||
"date-fns": "^3.2.0",
|
"date-fns": "^3.6.0",
|
||||||
"framer-motion": "^11.0.3",
|
"framer-motion": "^11.0.3",
|
||||||
"i18next": "23.7.11",
|
"i18next": "23.7.11",
|
||||||
"i18next-browser-languagedetector": "7.2.0",
|
"i18next-browser-languagedetector": "7.2.0",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { QueryClientProvider } from "@tanstack/react-query"
|
|||||||
|
|
||||||
import { I18n } from "./components/utilities/i18n"
|
import { I18n } from "./components/utilities/i18n"
|
||||||
import { queryClient } from "./lib/query-client"
|
import { queryClient } from "./lib/query-client"
|
||||||
|
import { I18nProvider } from "./providers/i18n-provider"
|
||||||
import { RouterProvider } from "./providers/router-provider"
|
import { RouterProvider } from "./providers/router-provider"
|
||||||
import { ThemeProvider } from "./providers/theme-provider"
|
import { ThemeProvider } from "./providers/theme-provider"
|
||||||
|
|
||||||
@@ -14,7 +15,9 @@ function App() {
|
|||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<I18n />
|
<I18n />
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<RouterProvider />
|
<I18nProvider>
|
||||||
|
<RouterProvider />
|
||||||
|
</I18nProvider>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
|||||||
+2
-2
@@ -27,7 +27,7 @@ export const DateRangeDisplay = ({
|
|||||||
<Text weight="plus" size="small">
|
<Text weight="plus" size="small">
|
||||||
{t("fields.startDate")}
|
{t("fields.startDate")}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="small">
|
<Text size="small" className="tabular-nums">
|
||||||
{startDate
|
{startDate
|
||||||
? getFullDate({
|
? getFullDate({
|
||||||
date: startDate,
|
date: startDate,
|
||||||
@@ -44,7 +44,7 @@ export const DateRangeDisplay = ({
|
|||||||
<Text size="small" weight="plus">
|
<Text size="small" weight="plus">
|
||||||
{t("fields.endDate")}
|
{t("fields.endDate")}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="small">
|
<Text size="small" className="tabular-nums">
|
||||||
{endDate
|
{endDate
|
||||||
? getFullDate({
|
? getFullDate({
|
||||||
date: endDate,
|
date: endDate,
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ const useFormField = () => {
|
|||||||
id,
|
id,
|
||||||
name: fieldContext.name,
|
name: fieldContext.name,
|
||||||
formItemId: `${id}-form-item`,
|
formItemId: `${id}-form-item`,
|
||||||
|
formLabelId: `${id}-form-item-label`,
|
||||||
formDescriptionId: `${id}-form-item-description`,
|
formDescriptionId: `${id}-form-item-description`,
|
||||||
formErrorMessageId: `${id}-form-item-message`,
|
formErrorMessageId: `${id}-form-item-message`,
|
||||||
...fieldState,
|
...fieldState,
|
||||||
@@ -109,12 +110,13 @@ const Label = forwardRef<
|
|||||||
icon?: ReactNode
|
icon?: ReactNode
|
||||||
}
|
}
|
||||||
>(({ className, optional = false, tooltip, icon, ...props }, ref) => {
|
>(({ className, optional = false, tooltip, icon, ...props }, ref) => {
|
||||||
const { formItemId } = useFormField()
|
const { formLabelId, formItemId } = useFormField()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-x-1">
|
<div className="flex items-center gap-x-1">
|
||||||
<LabelComponent
|
<LabelComponent
|
||||||
|
id={formLabelId}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={clx(className)}
|
className={clx(className)}
|
||||||
htmlFor={formItemId}
|
htmlFor={formItemId}
|
||||||
@@ -142,8 +144,13 @@ const Control = forwardRef<
|
|||||||
React.ElementRef<typeof Slot>,
|
React.ElementRef<typeof Slot>,
|
||||||
React.ComponentPropsWithoutRef<typeof Slot>
|
React.ComponentPropsWithoutRef<typeof Slot>
|
||||||
>(({ ...props }, ref) => {
|
>(({ ...props }, ref) => {
|
||||||
const { error, formItemId, formDescriptionId, formErrorMessageId } =
|
const {
|
||||||
useFormField()
|
error,
|
||||||
|
formItemId,
|
||||||
|
formDescriptionId,
|
||||||
|
formErrorMessageId,
|
||||||
|
formLabelId,
|
||||||
|
} = useFormField()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Slot
|
<Slot
|
||||||
@@ -155,6 +162,7 @@ const Control = forwardRef<
|
|||||||
: `${formDescriptionId} ${formErrorMessageId}`
|
: `${formDescriptionId} ${formErrorMessageId}`
|
||||||
}
|
}
|
||||||
aria-invalid={!!error}
|
aria-invalid={!!error}
|
||||||
|
aria-labelledby={formLabelId}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
-1
@@ -1 +0,0 @@
|
|||||||
export * from "./localized-date-picker"
|
|
||||||
-37
@@ -1,37 +0,0 @@
|
|||||||
import { DatePicker } from "@medusajs/ui"
|
|
||||||
import { ComponentPropsWithoutRef } from "react"
|
|
||||||
import { useTranslation } from "react-i18next"
|
|
||||||
import { languages } from "../../../i18n/languages"
|
|
||||||
|
|
||||||
type LocalizedDatePickerProps = Omit<
|
|
||||||
ComponentPropsWithoutRef<typeof DatePicker>,
|
|
||||||
"translations" | "locale"
|
|
||||||
>
|
|
||||||
|
|
||||||
export const LocalizedDatePicker = ({
|
|
||||||
mode = "single",
|
|
||||||
...props
|
|
||||||
}: LocalizedDatePickerProps) => {
|
|
||||||
const { i18n, t } = useTranslation()
|
|
||||||
|
|
||||||
const locale = languages.find(
|
|
||||||
(lang) => lang.code === i18n.language
|
|
||||||
)?.date_locale
|
|
||||||
|
|
||||||
const translations = {
|
|
||||||
cancel: t("actions.cancel"),
|
|
||||||
apply: t("general.apply"),
|
|
||||||
end: t("general.end"),
|
|
||||||
start: t("general.start"),
|
|
||||||
range: t("general.range"),
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DatePicker
|
|
||||||
mode={mode}
|
|
||||||
translations={translations}
|
|
||||||
locale={locale}
|
|
||||||
{...(props as any)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1 +1 @@
|
|||||||
export * from "./stacked-foucs-modal"
|
export * from "./stacked-focus-modal"
|
||||||
File renamed without changes.
+3
-8
@@ -65,10 +65,7 @@ export const DateFilter = ({
|
|||||||
const customStartValue = getDateFromComparison(currentDateComparison, "$gte")
|
const customStartValue = getDateFromComparison(currentDateComparison, "$gte")
|
||||||
const customEndValue = getDateFromComparison(currentDateComparison, "$lte")
|
const customEndValue = getDateFromComparison(currentDateComparison, "$lte")
|
||||||
|
|
||||||
const handleCustomDateChange = (
|
const handleCustomDateChange = (value: Date | null, pos: "start" | "end") => {
|
||||||
value: Date | undefined,
|
|
||||||
pos: "start" | "end"
|
|
||||||
) => {
|
|
||||||
const key = pos === "start" ? "$gte" : "$lte"
|
const key = pos === "start" ? "$gte" : "$lte"
|
||||||
const dateValue = value ? value.toISOString() : undefined
|
const dateValue = value ? value.toISOString() : undefined
|
||||||
|
|
||||||
@@ -201,8 +198,7 @@ export const DateFilter = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="px-2 py-1">
|
<div className="px-2 py-1">
|
||||||
<DatePicker
|
<DatePicker
|
||||||
// placeholder="MM/DD/YYYY" TODO: Fix DatePicker component not working with placeholder
|
maxValue={customEndValue}
|
||||||
toDate={customEndValue}
|
|
||||||
value={customStartValue}
|
value={customStartValue}
|
||||||
onChange={(d) => handleCustomDateChange(d, "start")}
|
onChange={(d) => handleCustomDateChange(d, "start")}
|
||||||
/>
|
/>
|
||||||
@@ -216,8 +212,7 @@ export const DateFilter = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="px-2 py-1">
|
<div className="px-2 py-1">
|
||||||
<DatePicker
|
<DatePicker
|
||||||
// placeholder="MM/DD/YYYY"
|
minValue={customStartValue}
|
||||||
fromDate={customStartValue}
|
|
||||||
value={customEndValue || undefined}
|
value={customEndValue || undefined}
|
||||||
onChange={(d) => {
|
onChange={(d) => {
|
||||||
handleCustomDateChange(d, "end")
|
handleCustomDateChange(d, "end")
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { useTranslation } from "react-i18next"
|
|||||||
|
|
||||||
import { languages } from "../i18n/languages"
|
import { languages } from "../i18n/languages"
|
||||||
|
|
||||||
|
// TODO: We rely on the current language to determine the date locale. This is not ideal, as we use en-US for the english translation.
|
||||||
|
// We either need to also have an en-GB translation or we need to separate the date locale from the translation language.
|
||||||
export const useDate = () => {
|
export const useDate = () => {
|
||||||
const { i18n } = useTranslation()
|
const { i18n } = useTranslation()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { I18nProvider as Provider } from "@medusajs/ui"
|
||||||
|
import { PropsWithChildren } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { languages } from "../../i18n/languages"
|
||||||
|
|
||||||
|
type I18nProviderProps = PropsWithChildren
|
||||||
|
|
||||||
|
export const I18nProvider = ({ children }: I18nProviderProps) => {
|
||||||
|
const { i18n } = useTranslation()
|
||||||
|
|
||||||
|
const locale =
|
||||||
|
languages.find((lan) => lan.code === i18n.language)?.code ||
|
||||||
|
languages[0].code
|
||||||
|
|
||||||
|
return <Provider locale={locale}>{children}</Provider>
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./i18n-provider"
|
||||||
+8
-14
@@ -5,10 +5,7 @@ import { useForm } from "react-hook-form"
|
|||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import * as zod from "zod"
|
import * as zod from "zod"
|
||||||
import { Form } from "../../../../../components/common/form"
|
import { Form } from "../../../../../components/common/form"
|
||||||
import {
|
import { RouteDrawer, useRouteModal } from "../../../../../components/modals"
|
||||||
RouteDrawer,
|
|
||||||
useRouteModal,
|
|
||||||
} from "../../../../../components/modals"
|
|
||||||
import { useUpdateCampaign } from "../../../../../hooks/api/campaigns"
|
import { useUpdateCampaign } from "../../../../../hooks/api/campaigns"
|
||||||
|
|
||||||
type EditCampaignFormProps = {
|
type EditCampaignFormProps = {
|
||||||
@@ -133,18 +130,16 @@ export const EditCampaignForm = ({ campaign }: EditCampaignFormProps) => {
|
|||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="starts_at"
|
name="starts_at"
|
||||||
render={({ field: { value, onChange, ref: _ref, ...field } }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Form.Label>{t("campaigns.fields.start_date")}</Form.Label>
|
<Form.Label>{t("campaigns.fields.start_date")}</Form.Label>
|
||||||
|
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
showTimePicker
|
granularity="minute"
|
||||||
value={value ?? undefined}
|
hourCycle={12}
|
||||||
onChange={(v) => {
|
shouldCloseOnSelect={false}
|
||||||
onChange(v ?? null)
|
|
||||||
}}
|
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
@@ -158,16 +153,15 @@ export const EditCampaignForm = ({ campaign }: EditCampaignFormProps) => {
|
|||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="ends_at"
|
name="ends_at"
|
||||||
render={({ field: { value, onChange, ref: _ref, ...field } }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Form.Label>{t("campaigns.fields.end_date")}</Form.Label>
|
<Form.Label>{t("campaigns.fields.end_date")}</Form.Label>
|
||||||
|
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
showTimePicker
|
granularity="minute"
|
||||||
value={value ?? undefined}
|
shouldCloseOnSelect={false}
|
||||||
onChange={(v) => onChange(v ?? null)}
|
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
|
|||||||
+6
-10
@@ -138,18 +138,15 @@ export const CreateCampaignFormFields = ({ form, fieldScope = "" }) => {
|
|||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name={`${fieldScope}starts_at`}
|
name={`${fieldScope}starts_at`}
|
||||||
render={({ field: { value, onChange, ref: _ref, ...field } }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Form.Label>{t("campaigns.fields.start_date")}</Form.Label>
|
<Form.Label>{t("campaigns.fields.start_date")}</Form.Label>
|
||||||
|
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
showTimePicker
|
granularity="minute"
|
||||||
value={value ?? undefined}
|
shouldCloseOnSelect={false}
|
||||||
onChange={(v) => {
|
|
||||||
onChange(v ?? null)
|
|
||||||
}}
|
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
@@ -163,16 +160,15 @@ export const CreateCampaignFormFields = ({ form, fieldScope = "" }) => {
|
|||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name={`${fieldScope}ends_at`}
|
name={`${fieldScope}ends_at`}
|
||||||
render={({ field: { value, onChange, ref: _ref, ...field } }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Form.Label>{t("campaigns.fields.end_date")}</Form.Label>
|
<Form.Label>{t("campaigns.fields.end_date")}</Form.Label>
|
||||||
|
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
showTimePicker
|
granularity="minute"
|
||||||
value={value ?? undefined}
|
shouldCloseOnSelect={false}
|
||||||
onChange={(v) => onChange(v ?? null)}
|
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
|
|||||||
+6
-10
@@ -41,7 +41,6 @@ const PriceListConfigurationSchema = z.object({
|
|||||||
|
|
||||||
const STACKED_MODAL_ID = "cg"
|
const STACKED_MODAL_ID = "cg"
|
||||||
|
|
||||||
// TODO: Fix DatePickers once new version is merged.
|
|
||||||
export const PriceListConfigurationForm = ({
|
export const PriceListConfigurationForm = ({
|
||||||
priceList,
|
priceList,
|
||||||
customerGroups,
|
customerGroups,
|
||||||
@@ -122,7 +121,7 @@ export const PriceListConfigurationForm = ({
|
|||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="starts_at"
|
name="starts_at"
|
||||||
render={({ field: { value, onChange, ...field } }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<div className="grid grid-cols-1 gap-3">
|
<div className="grid grid-cols-1 gap-3">
|
||||||
@@ -135,12 +134,10 @@ export const PriceListConfigurationForm = ({
|
|||||||
</Form.Hint>
|
</Form.Hint>
|
||||||
</div>
|
</div>
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
{/* TODO: Add timepicker see CORE-2382 */}
|
|
||||||
<DatePicker
|
<DatePicker
|
||||||
mode="single"
|
granularity="minute"
|
||||||
|
shouldCloseOnSelect={false}
|
||||||
{...field}
|
{...field}
|
||||||
onChange={(value) => onChange(value ?? null)}
|
|
||||||
value={value ?? undefined}
|
|
||||||
/>
|
/>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
</div>
|
</div>
|
||||||
@@ -153,7 +150,7 @@ export const PriceListConfigurationForm = ({
|
|||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="ends_at"
|
name="ends_at"
|
||||||
render={({ field: { value, onChange, ...field } }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<div className="grid grid-cols-1 gap-3">
|
<div className="grid grid-cols-1 gap-3">
|
||||||
@@ -167,10 +164,9 @@ export const PriceListConfigurationForm = ({
|
|||||||
</div>
|
</div>
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
mode="single"
|
granularity="minute"
|
||||||
|
shouldCloseOnSelect={false}
|
||||||
{...field}
|
{...field}
|
||||||
onChange={(value) => onChange(value ?? null)}
|
|
||||||
value={value ?? undefined}
|
|
||||||
/>
|
/>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
-1
@@ -46,7 +46,6 @@ type PriceListCreateFormProps = {
|
|||||||
currencies: HttpTypes.AdminStoreCurrency[]
|
currencies: HttpTypes.AdminStoreCurrency[]
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Fix DatePickers once new version is merged.
|
|
||||||
export const PriceListCreateForm = ({
|
export const PriceListCreateForm = ({
|
||||||
regions,
|
regions,
|
||||||
currencies,
|
currencies,
|
||||||
|
|||||||
+6
-7
@@ -173,7 +173,7 @@ export const PriceListDetailsForm = ({ form }: PriceListDetailsFormProps) => {
|
|||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="starts_at"
|
name="starts_at"
|
||||||
render={({ field: { value, ...field } }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
|
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||||
@@ -186,11 +186,10 @@ export const PriceListDetailsForm = ({ form }: PriceListDetailsFormProps) => {
|
|||||||
</Form.Hint>
|
</Form.Hint>
|
||||||
</div>
|
</div>
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
{/* TODO: Add timepicker see CORE-2382 */}
|
|
||||||
<DatePicker
|
<DatePicker
|
||||||
mode="single"
|
granularity="minute"
|
||||||
|
shouldCloseOnSelect={false}
|
||||||
{...field}
|
{...field}
|
||||||
value={value ?? undefined}
|
|
||||||
/>
|
/>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
</div>
|
</div>
|
||||||
@@ -203,7 +202,7 @@ export const PriceListDetailsForm = ({ form }: PriceListDetailsFormProps) => {
|
|||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="ends_at"
|
name="ends_at"
|
||||||
render={({ field: { value, ...field } }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
|
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||||
@@ -215,9 +214,9 @@ export const PriceListDetailsForm = ({ form }: PriceListDetailsFormProps) => {
|
|||||||
</div>
|
</div>
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
mode="single"
|
granularity="minute"
|
||||||
|
shouldCloseOnSelect={false}
|
||||||
{...field}
|
{...field}
|
||||||
value={value ?? undefined}
|
|
||||||
/>
|
/>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+2
-2
@@ -34,7 +34,7 @@ export const PriceListDetails = () => {
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
<div className="flex flex-col gap-x-4 xl:flex-row xl:items-start">
|
<div className="flex flex-col gap-x-4 xl:flex-row xl:items-start">
|
||||||
<div className="flex w-full flex-col gap-y-3">
|
<div className="flex flex-1 flex-col gap-y-3">
|
||||||
<PriceListGeneralSection priceList={price_list} />
|
<PriceListGeneralSection priceList={price_list} />
|
||||||
<PriceListProductSection priceList={price_list} />
|
<PriceListProductSection priceList={price_list} />
|
||||||
{after.widgets.map((w, i) => {
|
{after.widgets.map((w, i) => {
|
||||||
@@ -48,7 +48,7 @@ export const PriceListDetails = () => {
|
|||||||
<JsonViewSection data={price_list} />
|
<JsonViewSection data={price_list} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 flex w-full max-w-[100%] flex-col gap-y-2 xl:mt-0 xl:max-w-[423px]">
|
<div className="mt-2 flex w-full max-w-[100%] flex-col gap-y-2 xl:mt-0 xl:max-w-[440px]">
|
||||||
{sideBefore.widgets.map((w, i) => {
|
{sideBefore.widgets.map((w, i) => {
|
||||||
return (
|
return (
|
||||||
<div key={i}>
|
<div key={i}>
|
||||||
|
|||||||
@@ -81,34 +81,35 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@medusajs/icons": "^1.2.1",
|
"@medusajs/icons": "^1.2.1",
|
||||||
"@radix-ui/react-accordion": "^1.1.2",
|
"@radix-ui/react-accordion": "1.2.0",
|
||||||
"@radix-ui/react-alert-dialog": "1.0.4",
|
"@radix-ui/react-alert-dialog": "1.1.1",
|
||||||
"@radix-ui/react-avatar": "^1.0.3",
|
"@radix-ui/react-avatar": "1.1.0",
|
||||||
"@radix-ui/react-checkbox": "^1.0.4",
|
"@radix-ui/react-checkbox": "1.1.1",
|
||||||
"@radix-ui/react-dialog": "1.0.4",
|
"@radix-ui/react-dialog": "1.1.1",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.0.5",
|
"@radix-ui/react-dropdown-menu": "2.1.1",
|
||||||
"@radix-ui/react-label": "^2.0.2",
|
"@radix-ui/react-label": "2.1.0",
|
||||||
"@radix-ui/react-popover": "^1.0.6",
|
"@radix-ui/react-popover": "1.1.1",
|
||||||
"@radix-ui/react-portal": "^1.0.3",
|
"@radix-ui/react-portal": "1.1.1",
|
||||||
"@radix-ui/react-radio-group": "^1.1.3",
|
"@radix-ui/react-radio-group": "1.2.0",
|
||||||
"@radix-ui/react-scroll-area": "^1.0.4",
|
"@radix-ui/react-scroll-area": "1.1.0",
|
||||||
"@radix-ui/react-select": "^2.0.0",
|
"@radix-ui/react-select": "2.1.1",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "1.1.0",
|
||||||
"@radix-ui/react-switch": "^1.0.3",
|
"@radix-ui/react-switch": "1.1.0",
|
||||||
"@radix-ui/react-tabs": "^1.0.4",
|
"@radix-ui/react-tabs": "1.1.0",
|
||||||
"@radix-ui/react-tooltip": "^1.0.6",
|
"@radix-ui/react-tooltip": "1.1.2",
|
||||||
"@react-aria/datepicker": "^3.5.0",
|
|
||||||
"@react-stately/datepicker": "^3.5.0",
|
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
"copy-to-clipboard": "^3.3.3",
|
"copy-to-clipboard": "^3.3.3",
|
||||||
"cva": "1.0.0-beta.1",
|
"cva": "1.0.0-beta.1",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
"prism-react-renderer": "^2.0.6",
|
"prism-react-renderer": "^2.0.6",
|
||||||
"prismjs": "^1.29.0",
|
"prismjs": "^1.29.0",
|
||||||
|
"react-aria": "^3.33.1",
|
||||||
"react-currency-input-field": "^3.6.11",
|
"react-currency-input-field": "^3.6.11",
|
||||||
"react-day-picker": "^8.8.0",
|
"react-stately": "^3.31.1",
|
||||||
"sonner": "^1.4.41",
|
"sonner": "^1.4.41",
|
||||||
"tailwind-merge": "^2.2.1"
|
"tailwind-merge": "^2.2.1",
|
||||||
|
"upgrade": "^1.1.0",
|
||||||
|
"yarn": "^1.22.22"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^18.0.0",
|
"react": "^18.0.0",
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
import type { Meta, StoryObj } from "@storybook/react"
|
|
||||||
import * as React from "react"
|
|
||||||
|
|
||||||
import { Text } from "@/components/text"
|
|
||||||
import { DateRange } from "react-day-picker"
|
|
||||||
import { Calendar } from "./calendar"
|
|
||||||
|
|
||||||
const Demo = ({ mode, ...args }: Parameters<typeof Calendar>[0]) => {
|
|
||||||
const [date, setDate] = React.useState<Date | undefined>(new Date())
|
|
||||||
const [dateRange, setDateRange] = React.useState<DateRange | undefined>(
|
|
||||||
undefined
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-center gap-y-4">
|
|
||||||
<Calendar
|
|
||||||
{...(args as any)}
|
|
||||||
mode={mode as "single" | "range"}
|
|
||||||
selected={mode === "single" ? date : dateRange}
|
|
||||||
onSelect={mode === "single" ? setDate : setDateRange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{mode === "single" && (
|
|
||||||
<Text className="text-ui-fg-base">
|
|
||||||
Selected Date: {date ? date.toDateString() : "None"}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
{mode === "range" && (
|
|
||||||
<Text className="text-ui-fg-base">
|
|
||||||
Selected Range:{" "}
|
|
||||||
{dateRange
|
|
||||||
? `${dateRange.from?.toDateString()} – ${
|
|
||||||
dateRange.to?.toDateString() ?? ""
|
|
||||||
}`
|
|
||||||
: "None"}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const meta: Meta<typeof Calendar> = {
|
|
||||||
title: "Components/Calendar",
|
|
||||||
component: Calendar,
|
|
||||||
render: Demo,
|
|
||||||
parameters: {
|
|
||||||
layout: "centered",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export default meta
|
|
||||||
|
|
||||||
type Story = StoryObj<typeof Calendar>
|
|
||||||
|
|
||||||
export const Single: Story = {
|
|
||||||
args: {
|
|
||||||
mode: "single",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TwoMonthSingle: Story = {
|
|
||||||
args: {
|
|
||||||
mode: "single",
|
|
||||||
numberOfMonths: 2,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Range: Story = {
|
|
||||||
args: {
|
|
||||||
mode: "range",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TwoMonthRange: Story = {
|
|
||||||
args: {
|
|
||||||
mode: "range",
|
|
||||||
numberOfMonths: 2,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { TriangleLeftMini, TriangleRightMini } from "@medusajs/icons"
|
|
||||||
import * as React from "react"
|
|
||||||
import {
|
|
||||||
DayPicker,
|
|
||||||
useDayRender,
|
|
||||||
type DayPickerRangeProps,
|
|
||||||
type DayPickerSingleProps,
|
|
||||||
type DayProps,
|
|
||||||
} from "react-day-picker"
|
|
||||||
|
|
||||||
import { clx } from "@/utils/clx"
|
|
||||||
import { iconButtonVariants } from "../icon-button"
|
|
||||||
|
|
||||||
type OmitKeys<T, K extends keyof T> = {
|
|
||||||
[P in keyof T as P extends K ? never : P]: T[P]
|
|
||||||
}
|
|
||||||
|
|
||||||
type KeysToOmit = "showWeekNumber" | "captionLayout" | "mode"
|
|
||||||
|
|
||||||
type SingleProps = OmitKeys<DayPickerSingleProps, KeysToOmit>
|
|
||||||
type RangeProps = OmitKeys<DayPickerRangeProps, KeysToOmit>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @interface
|
|
||||||
*/
|
|
||||||
type CalendarProps =
|
|
||||||
| ({
|
|
||||||
mode: "single"
|
|
||||||
} & SingleProps)
|
|
||||||
| ({
|
|
||||||
mode?: undefined
|
|
||||||
} & SingleProps)
|
|
||||||
| ({
|
|
||||||
mode: "range"
|
|
||||||
} & RangeProps)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This component is based on the [react-date-picker](https://www.npmjs.com/package/react-date-picker) package.
|
|
||||||
*
|
|
||||||
* @excludeExternal
|
|
||||||
*/
|
|
||||||
const Calendar = ({
|
|
||||||
/**
|
|
||||||
* @ignore
|
|
||||||
*/
|
|
||||||
className,
|
|
||||||
/**
|
|
||||||
* @ignore
|
|
||||||
*/
|
|
||||||
classNames,
|
|
||||||
/**
|
|
||||||
* The calendar's mode.
|
|
||||||
*/
|
|
||||||
mode = "single",
|
|
||||||
/**
|
|
||||||
* Whether to show days of previous and next months.
|
|
||||||
*
|
|
||||||
* @keep
|
|
||||||
*/
|
|
||||||
showOutsideDays = true,
|
|
||||||
/**
|
|
||||||
* The locale to use for formatting dates. To change the locale pass a date-fns locale object.
|
|
||||||
*
|
|
||||||
* @keep
|
|
||||||
*/
|
|
||||||
locale,
|
|
||||||
...props
|
|
||||||
}: CalendarProps) => {
|
|
||||||
return (
|
|
||||||
<DayPicker
|
|
||||||
mode={mode}
|
|
||||||
showOutsideDays={showOutsideDays}
|
|
||||||
className={clx(className)}
|
|
||||||
classNames={{
|
|
||||||
months: "flex flex-col sm:flex-row",
|
|
||||||
month: "space-y-2 p-3",
|
|
||||||
caption: "flex justify-center relative items-center h-8",
|
|
||||||
caption_label:
|
|
||||||
"txt-compact-small-plus absolute bottom-0 left-0 right-0 top-0 flex items-center justify-center text-ui-fg-base",
|
|
||||||
nav: "space-x-1 flex items-center bg-ui-bg-base-pressed rounded-md w-full h-full justify-between p-0.5",
|
|
||||||
nav_button: clx(
|
|
||||||
iconButtonVariants({ variant: "transparent", size: "small" })
|
|
||||||
),
|
|
||||||
nav_button_previous: "!absolute left-0.5",
|
|
||||||
nav_button_next: "!absolute right-0.5",
|
|
||||||
table: "w-full border-collapse space-y-1",
|
|
||||||
head_row: "flex w-full gap-x-2",
|
|
||||||
head_cell: clx(
|
|
||||||
"txt-compact-small-plus text-ui-fg-muted m-0 box-border flex h-8 w-8 items-center justify-center p-0"
|
|
||||||
),
|
|
||||||
row: "flex w-full mt-2 gap-x-2",
|
|
||||||
cell: "txt-compact-small-plus relative rounded-md p-0 text-center focus-within:relative",
|
|
||||||
day: "txt-compact-small-plus text-ui-fg-base bg-ui-bg-base hover:bg-ui-bg-base-hover focus-visible:shadow-borders-interactive-with-focus h-8 w-8 rounded-md p-0 text-center outline-none transition-all",
|
|
||||||
day_selected:
|
|
||||||
"bg-ui-bg-interactive text-ui-fg-on-color hover:bg-ui-bg-interactive focus-visible:bg-ui-bg-interactive",
|
|
||||||
day_outside: "text-ui-fg-disabled aria-selected:text-ui-fg-on-color",
|
|
||||||
day_disabled: "text-ui-fg-disabled",
|
|
||||||
day_range_middle:
|
|
||||||
"aria-selected:!bg-ui-bg-highlight aria-selected:!text-ui-fg-interactive",
|
|
||||||
day_hidden: "invisible",
|
|
||||||
...classNames,
|
|
||||||
}}
|
|
||||||
locale={locale}
|
|
||||||
components={{
|
|
||||||
IconLeft: () => <TriangleLeftMini />,
|
|
||||||
IconRight: () => <TriangleRightMini />,
|
|
||||||
Day: Day,
|
|
||||||
}}
|
|
||||||
{...(props as SingleProps & RangeProps)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Calendar.displayName = "Calendar"
|
|
||||||
|
|
||||||
const Day = ({ date, displayMonth }: DayProps) => {
|
|
||||||
const ref = React.useRef<HTMLButtonElement>(null)
|
|
||||||
const { activeModifiers, buttonProps, divProps, isButton, isHidden } =
|
|
||||||
useDayRender(date, displayMonth, ref)
|
|
||||||
|
|
||||||
const { selected, today, disabled, range_middle } = activeModifiers
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (selected) {
|
|
||||||
ref.current?.focus()
|
|
||||||
}
|
|
||||||
}, [selected])
|
|
||||||
|
|
||||||
if (isHidden) {
|
|
||||||
return <></>
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isButton) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
{...divProps}
|
|
||||||
className={clx("flex items-center justify-center", divProps.className)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
children: buttonChildren,
|
|
||||||
className: buttonClassName,
|
|
||||||
...buttonPropsRest
|
|
||||||
} = buttonProps
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
ref={ref}
|
|
||||||
{...buttonPropsRest}
|
|
||||||
type="button"
|
|
||||||
className={clx("relative", buttonClassName)}
|
|
||||||
>
|
|
||||||
{buttonChildren}
|
|
||||||
{today && (
|
|
||||||
<span
|
|
||||||
className={clx(
|
|
||||||
"absolute right-[5px] top-[5px] h-1 w-1 rounded-full",
|
|
||||||
{
|
|
||||||
"bg-ui-fg-interactive": !selected,
|
|
||||||
"bg-ui-fg-on-color": selected,
|
|
||||||
"!bg-ui-fg-interactive": selected && range_middle,
|
|
||||||
"bg-ui-fg-disabled": disabled,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Calendar }
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from "./calendar"
|
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { createCalendar } from "@internationalized/date"
|
||||||
|
import { TriangleLeftMini, TriangleRightMini } from "@medusajs/icons"
|
||||||
|
import * as React from "react"
|
||||||
|
import {
|
||||||
|
DateValue,
|
||||||
|
useCalendar,
|
||||||
|
useLocale,
|
||||||
|
type CalendarProps,
|
||||||
|
} from "react-aria"
|
||||||
|
import { useCalendarState } from "react-stately"
|
||||||
|
|
||||||
|
import { CalendarButton } from "./calendar-button"
|
||||||
|
import { CalendarGrid } from "./calendar-grid"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* InternalCalendar is the internal implementation of the Calendar component.
|
||||||
|
* It's not for public use, but only used for other components like DatePicker.
|
||||||
|
*/
|
||||||
|
const InternalCalendar = <TDateValue extends DateValue>(
|
||||||
|
props: CalendarProps<TDateValue>
|
||||||
|
) => {
|
||||||
|
const { locale } = useLocale()
|
||||||
|
|
||||||
|
const state = useCalendarState({
|
||||||
|
...props,
|
||||||
|
locale,
|
||||||
|
createCalendar,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { calendarProps, prevButtonProps, nextButtonProps, title } =
|
||||||
|
useCalendar(props, state)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div {...calendarProps} className="flex flex-col gap-y-2">
|
||||||
|
<div className="bg-ui-bg-field border-base grid grid-cols-[28px_1fr_28px] items-center gap-1 rounded-md border p-0.5">
|
||||||
|
<CalendarButton {...prevButtonProps}>
|
||||||
|
<TriangleLeftMini />
|
||||||
|
</CalendarButton>
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<h2 className="txt-compact-small-plus">{title}</h2>
|
||||||
|
</div>
|
||||||
|
<CalendarButton {...nextButtonProps}>
|
||||||
|
<TriangleRightMini />
|
||||||
|
</CalendarButton>
|
||||||
|
</div>
|
||||||
|
<CalendarGrid state={state} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { InternalCalendar }
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { AriaButtonProps, useButton } from "react-aria"
|
||||||
|
|
||||||
|
import { IconButton } from "@/components/icon-button"
|
||||||
|
|
||||||
|
interface CalendarButtonProps extends AriaButtonProps<"button"> {}
|
||||||
|
|
||||||
|
const CalendarButton = React.forwardRef<HTMLButtonElement, CalendarButtonProps>(
|
||||||
|
({ children, ...props }, ref) => {
|
||||||
|
const innerRef = React.useRef<HTMLButtonElement>(null)
|
||||||
|
React.useImperativeHandle(ref, () => innerRef.current as HTMLButtonElement)
|
||||||
|
|
||||||
|
const { buttonProps } = useButton(props, innerRef)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
variant="transparent"
|
||||||
|
className="rounded-[4px]"
|
||||||
|
{...buttonProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</IconButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
CalendarButton.displayName = "CalendarButton"
|
||||||
|
|
||||||
|
export { CalendarButton }
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { CalendarDate } from "@internationalized/date"
|
||||||
|
import * as React from "react"
|
||||||
|
import { useCalendarCell } from "react-aria"
|
||||||
|
import { CalendarState } from "react-stately"
|
||||||
|
|
||||||
|
import { clx } from "@/utils/clx"
|
||||||
|
|
||||||
|
interface CalendarCellProps {
|
||||||
|
date: CalendarDate
|
||||||
|
state: CalendarState
|
||||||
|
}
|
||||||
|
|
||||||
|
const CalendarCell = ({ state, date }: CalendarCellProps) => {
|
||||||
|
const ref = React.useRef(null)
|
||||||
|
const {
|
||||||
|
cellProps,
|
||||||
|
buttonProps,
|
||||||
|
isSelected,
|
||||||
|
isOutsideVisibleRange,
|
||||||
|
isDisabled,
|
||||||
|
isUnavailable,
|
||||||
|
formattedDate,
|
||||||
|
} = useCalendarCell({ date }, state, ref)
|
||||||
|
|
||||||
|
const isToday = getIsToday(date)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<td {...cellProps} className="p-1">
|
||||||
|
<div
|
||||||
|
{...buttonProps}
|
||||||
|
ref={ref}
|
||||||
|
hidden={isOutsideVisibleRange}
|
||||||
|
className={clx(
|
||||||
|
"bg-ui-bg-base txt-compact-small relative flex size-8 items-center justify-center rounded-md outline-none transition-fg border border-transparent",
|
||||||
|
"hover:bg-ui-bg-base-hover",
|
||||||
|
"focus-visible:shadow-borders-focus focus-visible:border-ui-border-interactive",
|
||||||
|
{
|
||||||
|
"!bg-ui-bg-interactive !text-ui-fg-on-color": isSelected,
|
||||||
|
"hidden": isOutsideVisibleRange,
|
||||||
|
"text-ui-fg-muted hover:!bg-ui-bg-base cursor-default": isDisabled || isUnavailable,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{formattedDate}
|
||||||
|
{isToday && (
|
||||||
|
<div
|
||||||
|
role="none"
|
||||||
|
className={clx(
|
||||||
|
"bg-ui-bg-interactive absolute bottom-[3px] left-1/2 size-[3px] -translate-x-1/2 rounded-full transition-fg",
|
||||||
|
{
|
||||||
|
"bg-ui-fg-on-color": isSelected,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the date is today. The CalendarDate is using a 1-based index for the month.
|
||||||
|
* @returns Whether the CalendarDate is today.
|
||||||
|
*/
|
||||||
|
function getIsToday(date: CalendarDate) {
|
||||||
|
const today = new Date()
|
||||||
|
return (
|
||||||
|
[date.year, date.month, date.day].join("-") ===
|
||||||
|
[today.getFullYear(), today.getMonth() + 1, today.getDate()].join("-")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { CalendarCell }
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { getWeeksInMonth } from "@internationalized/date"
|
||||||
|
import * as React from "react"
|
||||||
|
import { AriaCalendarGridProps, useCalendarGrid, useLocale } from "react-aria"
|
||||||
|
|
||||||
|
import { CalendarState } from "react-stately"
|
||||||
|
import { CalendarCell } from "./calendar-cell"
|
||||||
|
|
||||||
|
interface CalendarGridProps extends AriaCalendarGridProps {
|
||||||
|
state: CalendarState
|
||||||
|
}
|
||||||
|
|
||||||
|
const CalendarGrid = ({ state, ...props }: CalendarGridProps) => {
|
||||||
|
const { locale } = useLocale()
|
||||||
|
const { gridProps, headerProps, weekDays } = useCalendarGrid(props, state)
|
||||||
|
|
||||||
|
const weeksInMonth = getWeeksInMonth(state.visibleRange.start, locale)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<table {...gridProps}>
|
||||||
|
<thead {...headerProps}>
|
||||||
|
<tr>
|
||||||
|
{weekDays.map((day, index) => (
|
||||||
|
<th key={index} className="txt-compact-small-plus text-ui-fg-muted size-8 p-1 rounded-md">{day}</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{[...new Array(weeksInMonth).keys()].map((weekIndex) => (
|
||||||
|
<tr key={weekIndex}>
|
||||||
|
{state
|
||||||
|
.getDatesInWeek(weekIndex)
|
||||||
|
.map((date, i) =>
|
||||||
|
date ? (
|
||||||
|
<CalendarCell key={i} state={state} date={date} />
|
||||||
|
) : (
|
||||||
|
<td key={i} />
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { CalendarGrid }
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import type { Meta, StoryObj } from "@storybook/react"
|
||||||
|
import * as React from "react"
|
||||||
|
import { ComponentPropsWithoutRef } from "react"
|
||||||
|
import { Button } from "../button"
|
||||||
|
import { Calendar } from "./calendar"
|
||||||
|
|
||||||
|
const meta: Meta<typeof Calendar> = {
|
||||||
|
title: "Components/CalendarNew",
|
||||||
|
component: Calendar,
|
||||||
|
parameters: {
|
||||||
|
layout: "centered",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof Calendar>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
const ControlledDemo = (args: ComponentPropsWithoutRef<typeof Calendar>) => {
|
||||||
|
const [date, setDate] = React.useState<Date | null>(new Date())
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Calendar {...args} value={date} onChange={setDate} />
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<pre className="font-mono txt-compact-small">{date ? date.toDateString() : "null"}</pre>
|
||||||
|
<Button variant="secondary" size="small" onClick={() => setDate(null)}>Reset</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Controlled: Story = {
|
||||||
|
render: ControlledDemo,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MinValue: Story = {
|
||||||
|
args: {
|
||||||
|
minValue: new Date(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MaxValue: Story = {
|
||||||
|
args: {
|
||||||
|
maxValue: new Date(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DisabledDates: Story = {
|
||||||
|
args: {
|
||||||
|
isDateUnavailable: (date: Date) => {
|
||||||
|
const unavailable = date.getDay() === 0
|
||||||
|
|
||||||
|
return unavailable
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import {
|
||||||
|
CalendarDate,
|
||||||
|
createCalendar,
|
||||||
|
getLocalTimeZone
|
||||||
|
} from "@internationalized/date"
|
||||||
|
import { TriangleLeftMini, TriangleRightMini } from "@medusajs/icons"
|
||||||
|
import * as React from "react"
|
||||||
|
import {
|
||||||
|
DateValue,
|
||||||
|
useCalendar,
|
||||||
|
useLocale,
|
||||||
|
type CalendarProps as BaseCalendarProps,
|
||||||
|
} from "react-aria"
|
||||||
|
import { useCalendarState } from "react-stately"
|
||||||
|
|
||||||
|
import { createCalendarDate, getDefaultCalendarDate, updateCalendarDate } from "@/utils/calendar"
|
||||||
|
|
||||||
|
import { CalendarButton } from "./calendar-button"
|
||||||
|
import { CalendarGrid } from "./calendar-grid"
|
||||||
|
|
||||||
|
interface CalendarValueProps {
|
||||||
|
value?: Date | null
|
||||||
|
defaultValue?: Date | null
|
||||||
|
onChange?: (value: Date | null) => void
|
||||||
|
isDateUnavailable?: (date: Date) => boolean
|
||||||
|
minValue?: Date
|
||||||
|
maxValue?: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CalendarProps
|
||||||
|
extends Omit<BaseCalendarProps<CalendarDate>, keyof CalendarValueProps>,
|
||||||
|
CalendarValueProps {}
|
||||||
|
|
||||||
|
const Calendar = (props: CalendarProps) => {
|
||||||
|
const [value, setValue] = React.useState<CalendarDate | null | undefined>(
|
||||||
|
() => getDefaultCalendarDate(props.value, props.defaultValue)
|
||||||
|
)
|
||||||
|
|
||||||
|
const { locale } = useLocale()
|
||||||
|
const _props = React.useMemo(() => convertProps(props, setValue), [props])
|
||||||
|
|
||||||
|
const state = useCalendarState({
|
||||||
|
..._props,
|
||||||
|
value,
|
||||||
|
locale,
|
||||||
|
createCalendar,
|
||||||
|
})
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setValue(props.value ? updateCalendarDate(value, props.value) : null)
|
||||||
|
}, [props.value])
|
||||||
|
|
||||||
|
const { calendarProps, prevButtonProps, nextButtonProps, title } =
|
||||||
|
useCalendar({ value, ..._props }, state)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div {...calendarProps} className="flex flex-col gap-y-2">
|
||||||
|
<div className="bg-ui-bg-field border-base grid grid-cols-[28px_1fr_28px] items-center gap-1 rounded-md border p-0.5">
|
||||||
|
<CalendarButton {...prevButtonProps}>
|
||||||
|
<TriangleLeftMini />
|
||||||
|
</CalendarButton>
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<h2 className="txt-compact-small-plus">{title}</h2>
|
||||||
|
</div>
|
||||||
|
<CalendarButton {...nextButtonProps}>
|
||||||
|
<TriangleRightMini />
|
||||||
|
</CalendarButton>
|
||||||
|
</div>
|
||||||
|
<CalendarGrid state={state} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertProps(
|
||||||
|
props: CalendarProps,
|
||||||
|
setValue: React.Dispatch<
|
||||||
|
React.SetStateAction<CalendarDate | null | undefined>
|
||||||
|
>
|
||||||
|
): BaseCalendarProps<CalendarDate> {
|
||||||
|
const {
|
||||||
|
minValue,
|
||||||
|
maxValue,
|
||||||
|
isDateUnavailable: _isDateUnavailable,
|
||||||
|
onChange: _onChange,
|
||||||
|
value: __value__,
|
||||||
|
defaultValue: __defaultValue__,
|
||||||
|
...rest
|
||||||
|
} = props
|
||||||
|
|
||||||
|
const onChange = (value: CalendarDate | null) => {
|
||||||
|
setValue(value)
|
||||||
|
_onChange?.(value ? value.toDate(getLocalTimeZone()) : null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDateUnavailable = (date: DateValue) => {
|
||||||
|
const _date = date.toDate(getLocalTimeZone())
|
||||||
|
|
||||||
|
return _isDateUnavailable ? _isDateUnavailable(_date) : false
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
onChange,
|
||||||
|
isDateUnavailable,
|
||||||
|
minValue: minValue ? createCalendarDate(minValue) : minValue,
|
||||||
|
maxValue: maxValue ? createCalendarDate(maxValue) : maxValue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Calendar }
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./_internal-calendar"
|
||||||
|
export * from "./calendar"
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { clx } from "@/utils/clx"
|
||||||
|
import * as React from "react"
|
||||||
|
import { AriaButtonProps, useButton } from "react-aria"
|
||||||
|
|
||||||
|
interface CalendarButtonProps extends AriaButtonProps<"button"> {
|
||||||
|
size?: "base" | "small"
|
||||||
|
}
|
||||||
|
|
||||||
|
const DatePickerButton = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
CalendarButtonProps
|
||||||
|
>(({ children, size = "base", ...props }, ref) => {
|
||||||
|
const innerRef = React.useRef<HTMLButtonElement>(null)
|
||||||
|
React.useImperativeHandle(ref, () => innerRef.current as HTMLButtonElement)
|
||||||
|
|
||||||
|
const { buttonProps } = useButton(props, innerRef)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={clx(
|
||||||
|
"text-ui-fg-muted transition-fg flex items-center justify-center border-r outline-none",
|
||||||
|
"disabled:text-ui-fg-disabled",
|
||||||
|
"hover:bg-ui-button-transparent-hover",
|
||||||
|
"focus-visible:bg-ui-bg-interactive focus-visible:text-ui-fg-on-color",
|
||||||
|
{
|
||||||
|
"size-7": size === "small",
|
||||||
|
"size-8": size === "base",
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
aria-label="Open calendar"
|
||||||
|
{...buttonProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
DatePickerButton.displayName = "DatePickerButton"
|
||||||
|
|
||||||
|
export { DatePickerButton }
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { clx } from "@/utils/clx"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
const ALLOWED_KEYS = ["ArrowDown", "ArrowUp", "ArrowLeft", "ArrowRight"]
|
||||||
|
|
||||||
|
export const DatePickerClearButton = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
React.ComponentPropsWithoutRef<"button">
|
||||||
|
>(({ type = "button", className, children, ...props }, ref) => {
|
||||||
|
/**
|
||||||
|
* Allows the button to be used with only the keyboard.
|
||||||
|
* Otherwise the wrapping component will hijack the event.
|
||||||
|
*/
|
||||||
|
const stopPropagation = (e: React.KeyboardEvent) => {
|
||||||
|
if (!ALLOWED_KEYS.includes(e.key)) {
|
||||||
|
e.stopPropagation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
ref={ref}
|
||||||
|
type={type}
|
||||||
|
className={clx(
|
||||||
|
"text-ui-fg-muted transition-fg flex size-full items-center justify-center outline-none",
|
||||||
|
"hover:bg-ui-button-transparent-hover",
|
||||||
|
"focus-visible:bg-ui-bg-interactive focus-visible:text-ui-fg-on-color",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
aria-label="Clear date"
|
||||||
|
onKeyDown={stopPropagation}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
DatePickerClearButton.displayName = "DatePickerClearButton"
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { createCalendar } from "@internationalized/date"
|
||||||
|
import * as React from "react"
|
||||||
|
import {
|
||||||
|
AriaDatePickerProps,
|
||||||
|
DateValue,
|
||||||
|
useDateField,
|
||||||
|
useLocale,
|
||||||
|
} from "react-aria"
|
||||||
|
import { useDateFieldState } from "react-stately"
|
||||||
|
|
||||||
|
import { DateSegment } from "@/components/date-segment"
|
||||||
|
import { cva } from "cva"
|
||||||
|
|
||||||
|
interface DatePickerFieldProps extends AriaDatePickerProps<DateValue> {
|
||||||
|
size?: "base" | "small"
|
||||||
|
}
|
||||||
|
|
||||||
|
const datePickerFieldStyles = cva({
|
||||||
|
base: "flex items-center tabular-nums",
|
||||||
|
variants: {
|
||||||
|
size: {
|
||||||
|
small: "py-1",
|
||||||
|
base: "py-1.5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
size: "base",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const DatePickerField = ({ size = "base", ...props }: DatePickerFieldProps) => {
|
||||||
|
const { locale } = useLocale()
|
||||||
|
|
||||||
|
|
||||||
|
const state = useDateFieldState({
|
||||||
|
...props,
|
||||||
|
locale,
|
||||||
|
createCalendar,
|
||||||
|
})
|
||||||
|
|
||||||
|
const ref = React.useRef<HTMLDivElement>(null)
|
||||||
|
const { fieldProps } = useDateField(props, state, ref)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref} aria-label="Date input" className={datePickerFieldStyles({ size })} {...fieldProps}>
|
||||||
|
{state.segments.map((segment, index) => {
|
||||||
|
return <DateSegment key={index} segment={segment} state={state} />
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { DatePickerField }
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import { render, screen } from "@testing-library/react"
|
|
||||||
import * as React from "react"
|
|
||||||
|
|
||||||
import { DatePicker } from "./date-picker"
|
|
||||||
|
|
||||||
describe("DatePicker", () => {
|
|
||||||
describe("Preset validation", () => {
|
|
||||||
it("should throw an error if a preset is before the min year", async () => {
|
|
||||||
expect(() =>
|
|
||||||
render(
|
|
||||||
<DatePicker
|
|
||||||
fromYear={1800}
|
|
||||||
presets={[
|
|
||||||
{
|
|
||||||
label: "Year of the first US census",
|
|
||||||
date: new Date(1790, 0, 1),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
).toThrowError(
|
|
||||||
/Preset Year of the first US census is before fromYear 1800./
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should throw an error if a preset is after the max year", async () => {
|
|
||||||
expect(() =>
|
|
||||||
render(
|
|
||||||
<DatePicker
|
|
||||||
toYear={2012}
|
|
||||||
presets={[
|
|
||||||
{
|
|
||||||
label: "End of the Mayan calendar",
|
|
||||||
date: new Date(2025, 0, 1),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
).toThrowError(/Preset End of the Mayan calendar is after toYear 2012./)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("Single", () => {
|
|
||||||
it("should render", async () => {
|
|
||||||
render(<DatePicker />)
|
|
||||||
|
|
||||||
expect(screen.getByRole("button")).toBeInTheDocument()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("Range", () => {
|
|
||||||
it("should render", async () => {
|
|
||||||
render(<DatePicker mode={"range"} />)
|
|
||||||
|
|
||||||
expect(screen.getByRole("button")).toBeInTheDocument()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,243 +1,137 @@
|
|||||||
import type { Meta, StoryObj } from "@storybook/react"
|
import type { Meta, StoryObj } from "@storybook/react"
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { DateRange } from "react-day-picker"
|
import { ComponentPropsWithoutRef } from "react"
|
||||||
|
import { Button } from "../button"
|
||||||
import { Button } from "@/components/button"
|
import { Drawer } from "../drawer"
|
||||||
|
import { Label } from "../label"
|
||||||
import { DatePicker } from "./date-picker"
|
import { DatePicker } from "./date-picker"
|
||||||
import { Popover } from "@/components/popover"
|
|
||||||
|
|
||||||
const meta: Meta<typeof DatePicker> = {
|
const meta: Meta<typeof DatePicker> = {
|
||||||
title: "Components/DatePicker",
|
title: "Components/DatePickerNew",
|
||||||
component: DatePicker,
|
component: DatePicker,
|
||||||
parameters: {
|
parameters: {
|
||||||
layout: "centered",
|
layout: "centered",
|
||||||
},
|
},
|
||||||
render: (args) => {
|
|
||||||
return (
|
|
||||||
<div className="w-[200px]">
|
|
||||||
<DatePicker {...args} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default meta
|
export default meta
|
||||||
|
|
||||||
type Story = StoryObj<typeof DatePicker>
|
type Story = StoryObj<typeof DatePicker>
|
||||||
|
|
||||||
const presets = [
|
export const Default: Story = {
|
||||||
{
|
|
||||||
label: "Today",
|
|
||||||
date: new Date(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Tomorrow",
|
|
||||||
date: new Date(new Date().setDate(new Date().getDate() + 1)),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "A week from now",
|
|
||||||
date: new Date(new Date().setDate(new Date().getDate() + 7)),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "A month from now",
|
|
||||||
date: new Date(new Date().setMonth(new Date().getMonth() + 1)),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "6 months from now",
|
|
||||||
date: new Date(new Date().setMonth(new Date().getMonth() + 6)),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "A year from now",
|
|
||||||
date: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export const Single: Story = {
|
|
||||||
args: {},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SingleWithPresets: Story = {
|
|
||||||
args: {
|
args: {
|
||||||
presets,
|
"aria-label": "Select a date",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SingleWithTimePicker: Story = {
|
const today = new Date() // Today
|
||||||
args: {
|
const oneWeekFromToday = new Date(new Date(today as Date).setDate(today.getDate() + 7)) // Today + 7 days
|
||||||
showTimePicker: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SingleWithTimePickerAndPresets: Story = {
|
const ControlledDemo = (args: ComponentPropsWithoutRef<typeof DatePicker>) => {
|
||||||
args: {
|
const [startDate, setStartDate] = React.useState<Date | null>(today)
|
||||||
showTimePicker: true,
|
const [endDate, setEndDate] = React.useState<Date | null>(oneWeekFromToday)
|
||||||
presets,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const rangePresets = [
|
|
||||||
{
|
|
||||||
label: "Today",
|
|
||||||
dateRange: {
|
|
||||||
from: new Date(),
|
|
||||||
to: new Date(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Last 7 days",
|
|
||||||
dateRange: {
|
|
||||||
from: new Date(new Date().setDate(new Date().getDate() - 7)),
|
|
||||||
to: new Date(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Last 30 days",
|
|
||||||
dateRange: {
|
|
||||||
from: new Date(new Date().setDate(new Date().getDate() - 30)),
|
|
||||||
to: new Date(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Last 3 months",
|
|
||||||
dateRange: {
|
|
||||||
from: new Date(new Date().setMonth(new Date().getMonth() - 3)),
|
|
||||||
to: new Date(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Last 6 months",
|
|
||||||
dateRange: {
|
|
||||||
from: new Date(new Date().setMonth(new Date().getMonth() - 6)),
|
|
||||||
to: new Date(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Month to date",
|
|
||||||
dateRange: {
|
|
||||||
from: new Date(new Date().setDate(1)),
|
|
||||||
to: new Date(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Year to date",
|
|
||||||
dateRange: {
|
|
||||||
from: new Date(new Date().setFullYear(new Date().getFullYear(), 0, 1)),
|
|
||||||
to: new Date(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export const Range: Story = {
|
|
||||||
args: {
|
|
||||||
mode: "range",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const RangeWithPresets: Story = {
|
|
||||||
args: {
|
|
||||||
mode: "range",
|
|
||||||
presets: rangePresets,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const RangeWithTimePicker: Story = {
|
|
||||||
args: {
|
|
||||||
mode: "range",
|
|
||||||
showTimePicker: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const RangeWithTimePickerAndPresets: Story = {
|
|
||||||
args: {
|
|
||||||
mode: "range",
|
|
||||||
showTimePicker: true,
|
|
||||||
presets: rangePresets,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const ControlledDemo = () => {
|
|
||||||
const [value, setValue] = React.useState<Date | undefined>(undefined)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-[200px] flex-col gap-y-4">
|
<div className="text-ui-fg-subtle grid max-w-[576px] gap-4 md:grid-cols-2">
|
||||||
<DatePicker
|
<fieldset className="flex flex-col gap-y-0.5">
|
||||||
value={value}
|
<Label id="starts_at:r1:label" htmlFor="starts_at:r1">Starts at</Label>
|
||||||
onChange={(value) => {
|
<DatePicker
|
||||||
setValue(value)
|
id="starts_at:r1"
|
||||||
}}
|
aria-labelledby="starts_at:r1:label"
|
||||||
/>
|
{...args}
|
||||||
<Button onClick={() => setValue(undefined)}>Reset</Button>
|
maxValue={endDate || undefined}
|
||||||
|
value={startDate}
|
||||||
|
onChange={setStartDate}
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset className="flex flex-col gap-y-0.5">
|
||||||
|
<Label id="ends_at:r1:label" htmlFor="ends_at:r1">Ends at</Label>
|
||||||
|
<DatePicker
|
||||||
|
id="ends_at:r1"
|
||||||
|
name="ends_at"
|
||||||
|
aria-labelledby="ends_at:r1:label"
|
||||||
|
minValue={startDate || undefined}
|
||||||
|
{...args}
|
||||||
|
value={endDate}
|
||||||
|
onChange={setEndDate}
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Controlled: Story = {
|
export const Controlled: Story = {
|
||||||
render: () => <ControlledDemo />,
|
render: ControlledDemo,
|
||||||
|
args: {
|
||||||
|
className: "w-[230px]",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const ControlledRangeDemo = () => {
|
export const MinValue: Story = {
|
||||||
const [value, setValue] = React.useState<DateRange | undefined>(undefined)
|
args: {
|
||||||
|
"aria-label": "Select a date",
|
||||||
|
className: "w-[230px]",
|
||||||
|
minValue: new Date(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
export const MaxValue: Story = {
|
||||||
console.log("Value changed: ", value)
|
args: {
|
||||||
}, [value])
|
"aria-label": "Select a date",
|
||||||
|
className: "w-[230px]",
|
||||||
|
maxValue: new Date(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DisabledDates: Story = {
|
||||||
|
args: {
|
||||||
|
isDateUnavailable: (date: Date) => {
|
||||||
|
const unavailable = date.getDay() === 0
|
||||||
|
|
||||||
|
return unavailable
|
||||||
|
},
|
||||||
|
"aria-label": "Select a date",
|
||||||
|
className: "w-[230px]",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithTime: Story = {
|
||||||
|
args: {
|
||||||
|
granularity: "minute",
|
||||||
|
"aria-label": "Select a date",
|
||||||
|
className: "w-[230px]",
|
||||||
|
value: new Date(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Small: Story = {
|
||||||
|
args: {
|
||||||
|
size: "small",
|
||||||
|
"aria-label": "Select a date",
|
||||||
|
className: "w-[230px]",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const InDrawerDemo = (args: ComponentPropsWithoutRef<typeof DatePicker>) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex w-[200px] flex-col gap-y-4">
|
<Drawer>
|
||||||
<DatePicker
|
<Drawer.Trigger asChild>
|
||||||
mode="range"
|
<Button size="small">Open Drawer</Button>
|
||||||
value={value}
|
</Drawer.Trigger>
|
||||||
onChange={(value) => {
|
<Drawer.Content>
|
||||||
setValue(value)
|
<Drawer.Header>
|
||||||
}}
|
<Drawer.Title>Select a date</Drawer.Title>
|
||||||
/>
|
</Drawer.Header>
|
||||||
<Button onClick={() => setValue(undefined)}>Reset</Button>
|
<Drawer.Body>
|
||||||
</div>
|
<div className="p-4">
|
||||||
|
<DatePicker {...args} />
|
||||||
|
</div>
|
||||||
|
</Drawer.Body>
|
||||||
|
</Drawer.Content>
|
||||||
|
</Drawer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ControlledRange: Story = {
|
export const InDrawer: Story = {
|
||||||
render: () => <ControlledRangeDemo />,
|
render: InDrawerDemo,
|
||||||
}
|
|
||||||
|
|
||||||
type NestedProps = {
|
|
||||||
value?: Date
|
|
||||||
onChange?: (value: Date | undefined) => void
|
|
||||||
}
|
|
||||||
const Nested = ({ value, onChange }: NestedProps) => {
|
|
||||||
return (
|
|
||||||
<Popover>
|
|
||||||
<Popover.Trigger asChild>
|
|
||||||
<Button>Open</Button>
|
|
||||||
</Popover.Trigger>
|
|
||||||
<Popover.Content>
|
|
||||||
<div className="px-3 py-2">
|
|
||||||
<DatePicker value={value} onChange={onChange} />
|
|
||||||
</div>
|
|
||||||
<Popover.Seperator />
|
|
||||||
<div className="px-3 py-2">
|
|
||||||
<DatePicker value={value} onChange={onChange} />
|
|
||||||
</div>
|
|
||||||
<Popover.Seperator />
|
|
||||||
<div className="flex items-center justify-between gap-x-2 px-3 py-2 [&_button]:w-full">
|
|
||||||
<Button variant="secondary">Clear</Button>
|
|
||||||
<Button>Apply</Button>
|
|
||||||
</div>
|
|
||||||
</Popover.Content>
|
|
||||||
</Popover>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const NestedDemo = () => {
|
|
||||||
const [value, setValue] = React.useState<Date | undefined>(undefined)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex w-[200px] flex-col gap-y-4">
|
|
||||||
<Nested value={value} onChange={setValue} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const NestedControlled: Story = {
|
|
||||||
render: () => <NestedDemo />,
|
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,54 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { useDateSegment } from "react-aria"
|
||||||
|
import { DateFieldState, DateSegment as Segment } from "react-stately"
|
||||||
|
|
||||||
|
import { clx } from "@/utils/clx"
|
||||||
|
|
||||||
|
interface DateSegmentProps extends React.ComponentPropsWithoutRef<"div"> {
|
||||||
|
segment: Segment
|
||||||
|
state: DateFieldState
|
||||||
|
}
|
||||||
|
|
||||||
|
const DateSegment = ({ segment, state }: DateSegmentProps) => {
|
||||||
|
const ref = React.useRef<HTMLDivElement>(null)
|
||||||
|
const { segmentProps } = useDateSegment(segment, state, ref)
|
||||||
|
|
||||||
|
const isComma = segment.type === "literal" && segment.text === ", "
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We render an empty span with a margin to maintain the correct spacing
|
||||||
|
* between date and time segments.
|
||||||
|
*/
|
||||||
|
if (isComma) {
|
||||||
|
return <span className="mx-1" />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
/**
|
||||||
|
* We wrap the segment in a span to prevent the segment from being
|
||||||
|
* focused when the user clicks outside of the component.
|
||||||
|
*
|
||||||
|
* See: https://github.com/adobe/react-spectrum/issues/3164
|
||||||
|
*/
|
||||||
|
<span>
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={clx(
|
||||||
|
"transition-fg outline-none",
|
||||||
|
"focus-visible:bg-ui-bg-interactive focus-visible:text-ui-fg-on-color",
|
||||||
|
{
|
||||||
|
"text-ui-fg-muted uppercase": segment.isPlaceholder,
|
||||||
|
"text-ui-fg-muted": !segment.isEditable && !state.value,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
{...segmentProps}
|
||||||
|
>
|
||||||
|
{segment.text}
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { DateSegment }
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./date-segment"
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { I18nProvider as Primitive, I18nProviderProps as Props } from "react-aria"
|
||||||
|
|
||||||
|
interface I18nProviderProps extends Props {}
|
||||||
|
|
||||||
|
const I18nProvider = (props: I18nProviderProps) => {
|
||||||
|
return <Primitive {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export { I18nProvider }
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./i18n-provider"
|
||||||
@@ -37,7 +37,8 @@ const Close = React.forwardRef<
|
|||||||
})
|
})
|
||||||
Close.displayName = "Popover.Close"
|
Close.displayName = "Popover.Close"
|
||||||
|
|
||||||
interface ContentProps extends React.ComponentPropsWithoutRef<typeof Primitives.Content> {}
|
interface ContentProps
|
||||||
|
extends React.ComponentPropsWithoutRef<typeof Primitives.Content> {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @excludeExternal
|
* @excludeExternal
|
||||||
|
|||||||
@@ -56,8 +56,8 @@ Root.displayName = "Prompt"
|
|||||||
const Trigger = Primitives.Trigger
|
const Trigger = Primitives.Trigger
|
||||||
Trigger.displayName = "Prompt.Trigger"
|
Trigger.displayName = "Prompt.Trigger"
|
||||||
|
|
||||||
const Portal = ({ className, ...props }: Primitives.AlertDialogPortalProps) => {
|
const Portal = (props: Primitives.AlertDialogPortalProps) => {
|
||||||
return <Primitives.AlertDialogPortal className={clx(className)} {...props} />
|
return <Primitives.AlertDialogPortal {...props} />
|
||||||
}
|
}
|
||||||
Portal.displayName = "Prompt.Portal"
|
Portal.displayName = "Prompt.Portal"
|
||||||
|
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
import type { Meta, StoryObj } from "@storybook/react"
|
|
||||||
import * as React from "react"
|
|
||||||
|
|
||||||
import { isBrowserLocaleClockType24h } from "../../utils/is-browser-locale-hour-cycle-24h"
|
|
||||||
import { TimeInput } from "./time-input"
|
|
||||||
|
|
||||||
const meta: Meta<typeof TimeInput> = {
|
|
||||||
title: "Components/TimeInput",
|
|
||||||
component: TimeInput,
|
|
||||||
parameters: {
|
|
||||||
layout: "centered",
|
|
||||||
},
|
|
||||||
render: (args) => (
|
|
||||||
<div className="w-[300px]">
|
|
||||||
<TimeInput aria-labelledby="time" {...args} />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
export default meta
|
|
||||||
|
|
||||||
type Story = StoryObj<typeof TimeInput>
|
|
||||||
|
|
||||||
// Hour Cycle defaults to your browser's locale.
|
|
||||||
export const Default: Story = {
|
|
||||||
render: (args) => {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-y-4">
|
|
||||||
<TimeInput aria-labelledby="time" {...args} />
|
|
||||||
<div className="flex flex-col gap-y-2">
|
|
||||||
<p className="text-xs">
|
|
||||||
Will use 24h or 12h cycle depending on your locale
|
|
||||||
</p>
|
|
||||||
<div className="text-xs">
|
|
||||||
<pre>
|
|
||||||
Locale: {window.navigator.language}, Hour Cycle:{" "}
|
|
||||||
{isBrowserLocaleClockType24h() ? "24h" : "12h"}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Hour24: Story = {
|
|
||||||
args: {
|
|
||||||
hourCycle: 24,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Hour12: Story = {
|
|
||||||
args: {
|
|
||||||
hourCycle: 12,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,131 +1,44 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
import {
|
import {
|
||||||
AriaTimeFieldProps,
|
AriaTimeFieldProps,
|
||||||
TimeValue,
|
TimeValue,
|
||||||
useDateSegment,
|
useLocale,
|
||||||
useTimeField,
|
useTimeField,
|
||||||
} from "@react-aria/datepicker"
|
} from "react-aria"
|
||||||
import {
|
import { useTimeFieldState } from "react-stately"
|
||||||
useTimeFieldState,
|
|
||||||
type DateFieldState,
|
|
||||||
type DateSegment,
|
|
||||||
} from "@react-stately/datepicker"
|
|
||||||
import * as React from "react"
|
|
||||||
|
|
||||||
import { inputBaseStyles } from "@/components/input"
|
import { DateSegment } from "@/components/date-segment"
|
||||||
import { clx } from "@/utils/clx"
|
import { clx } from "@/utils/clx"
|
||||||
|
|
||||||
type TimeSegmentProps = {
|
const TimeInput = (props: AriaTimeFieldProps<TimeValue>) => {
|
||||||
segment: DateSegment
|
|
||||||
state: DateFieldState
|
|
||||||
}
|
|
||||||
|
|
||||||
const TimeSegment = ({ segment, state }: TimeSegmentProps) => {
|
|
||||||
const ref = React.useRef<HTMLDivElement>(null)
|
const ref = React.useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const { segmentProps } = useDateSegment(segment, state, ref)
|
const { locale } = useLocale()
|
||||||
|
const state = useTimeFieldState({
|
||||||
const isColon = segment.type === "literal" && segment.text === ":"
|
...props,
|
||||||
const isSpace = segment.type === "literal" && segment.text === " "
|
locale,
|
||||||
|
})
|
||||||
const isDecorator = isColon || isSpace
|
const { fieldProps } = useTimeField(props, state, ref)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
{...segmentProps}
|
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
{...fieldProps}
|
||||||
|
aria-label="Time input"
|
||||||
className={clx(
|
className={clx(
|
||||||
"txt-compact-small w-full rounded-md px-2 py-1 text-left uppercase tabular-nums",
|
"bg-ui-bg-field shadow-borders-base txt-compact-small flex items-center rounded-md px-2 py-1",
|
||||||
inputBaseStyles,
|
|
||||||
"group-aria-[invalid=true]/time-input:!shadow-borders-error group-invalid/time-input:!shadow-borders-error",
|
|
||||||
{
|
{
|
||||||
"text-ui-fg-muted !w-fit border-none bg-transparent px-0 shadow-none":
|
"": props.isDisabled,
|
||||||
isDecorator,
|
|
||||||
hidden: isSpace,
|
|
||||||
"text-ui-fg-disabled bg-ui-bg-disabled border-ui-border-base shadow-none":
|
|
||||||
state.isDisabled,
|
|
||||||
"!text-ui-fg-muted !bg-transparent": !segment.isEditable,
|
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span
|
{state.segments.map((segment, index) => {
|
||||||
aria-hidden="true"
|
return <DateSegment key={index} segment={segment} state={state} />
|
||||||
className={clx(
|
})}
|
||||||
"txt-compact-small text-ui-fg-muted pointer-events-none block w-full text-left",
|
|
||||||
{
|
|
||||||
hidden: !segment.isPlaceholder,
|
|
||||||
"h-0": !segment.isPlaceholder,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{segment.placeholder}
|
|
||||||
</span>
|
|
||||||
{segment.isPlaceholder ? "" : segment.text}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type TimeInputProps = Omit<
|
|
||||||
AriaTimeFieldProps<TimeValue>,
|
|
||||||
"label" | "shouldForceLeadingZeros" | "description" | "errorMessage"
|
|
||||||
>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This component is based on the `div` element and supports all of its props.
|
|
||||||
*/
|
|
||||||
const TimeInput = React.forwardRef<HTMLDivElement, TimeInputProps>(
|
|
||||||
(
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The time's format. If no value is specified, the format is
|
|
||||||
* set based on the user's locale.
|
|
||||||
*/
|
|
||||||
hourCycle,
|
|
||||||
...props
|
|
||||||
}: TimeInputProps,
|
|
||||||
ref
|
|
||||||
) => {
|
|
||||||
const innerRef = React.useRef<HTMLDivElement>(null)
|
|
||||||
|
|
||||||
React.useImperativeHandle<HTMLDivElement | null, HTMLDivElement | null>(
|
|
||||||
ref,
|
|
||||||
() => innerRef?.current
|
|
||||||
)
|
|
||||||
|
|
||||||
const locale = window !== undefined ? window.navigator.language : "en-US"
|
|
||||||
|
|
||||||
const state = useTimeFieldState({
|
|
||||||
hourCycle: hourCycle,
|
|
||||||
locale: locale,
|
|
||||||
shouldForceLeadingZeros: true,
|
|
||||||
autoFocus: true,
|
|
||||||
...props,
|
|
||||||
})
|
|
||||||
|
|
||||||
const { fieldProps } = useTimeField(
|
|
||||||
{
|
|
||||||
...props,
|
|
||||||
hourCycle: hourCycle,
|
|
||||||
shouldForceLeadingZeros: true,
|
|
||||||
},
|
|
||||||
state,
|
|
||||||
innerRef
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
{...fieldProps}
|
|
||||||
ref={innerRef}
|
|
||||||
className="group/time-input inline-flex w-full gap-x-2"
|
|
||||||
>
|
|
||||||
{state.segments.map((segment, i) => (
|
|
||||||
<TimeSegment key={i} segment={segment} state={state} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
TimeInput.displayName = "TimeInput"
|
|
||||||
|
|
||||||
export { TimeInput }
|
export { TimeInput }
|
||||||
@@ -2,7 +2,7 @@ export { Alert } from "./components/alert"
|
|||||||
export { Avatar } from "./components/avatar"
|
export { Avatar } from "./components/avatar"
|
||||||
export { Badge } from "./components/badge"
|
export { Badge } from "./components/badge"
|
||||||
export { Button } from "./components/button"
|
export { Button } from "./components/button"
|
||||||
export { Calendar } from "./components/calendar"
|
export { Calendar } from "./components/calender"
|
||||||
export { Checkbox } from "./components/checkbox"
|
export { Checkbox } from "./components/checkbox"
|
||||||
export { Code } from "./components/code"
|
export { Code } from "./components/code"
|
||||||
export { CodeBlock } from "./components/code-block"
|
export { CodeBlock } from "./components/code-block"
|
||||||
@@ -17,6 +17,7 @@ export { DropdownMenu } from "./components/dropdown-menu"
|
|||||||
export { FocusModal } from "./components/focus-modal"
|
export { FocusModal } from "./components/focus-modal"
|
||||||
export { Heading } from "./components/heading"
|
export { Heading } from "./components/heading"
|
||||||
export { Hint } from "./components/hint"
|
export { Hint } from "./components/hint"
|
||||||
|
export { I18nProvider } from "./components/i18n-provider"
|
||||||
export { IconBadge } from "./components/icon-badge"
|
export { IconBadge } from "./components/icon-badge"
|
||||||
export { IconButton } from "./components/icon-button"
|
export { IconButton } from "./components/icon-button"
|
||||||
export { Input } from "./components/input"
|
export { Input } from "./components/input"
|
||||||
|
|||||||
@@ -53,3 +53,5 @@ export type ToastAction = {
|
|||||||
*/
|
*/
|
||||||
variant?: ToastActionVariant
|
variant?: ToastActionVariant
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Granularity = "day" | "hour" | "minute" | "second"
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import { CalendarDate, CalendarDateTime } from "@internationalized/date"
|
||||||
|
|
||||||
|
import { Granularity } from "@/types"
|
||||||
|
|
||||||
|
function getDefaultCalendarDateTime(
|
||||||
|
value: Date | null | undefined,
|
||||||
|
defaultValue: Date | null | undefined
|
||||||
|
) {
|
||||||
|
if (value) {
|
||||||
|
return createCalendarDateTime(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defaultValue) {
|
||||||
|
return createCalendarDateTime(defaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCalendarDateTime(date: Date) {
|
||||||
|
return new CalendarDateTime(
|
||||||
|
date.getFullYear(),
|
||||||
|
date.getMonth() + 1,
|
||||||
|
date.getDate(),
|
||||||
|
date.getHours(),
|
||||||
|
date.getMinutes(),
|
||||||
|
date.getSeconds(),
|
||||||
|
date.getMilliseconds()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCalendarDateTime(
|
||||||
|
date: CalendarDateTime | null | undefined,
|
||||||
|
value: Date
|
||||||
|
) {
|
||||||
|
if (!date) {
|
||||||
|
return createCalendarDateTime(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
date.set({
|
||||||
|
day: value.getDate(),
|
||||||
|
month: value.getMonth() + 1,
|
||||||
|
year: value.getFullYear(),
|
||||||
|
hour: value.getHours(),
|
||||||
|
minute: value.getMinutes(),
|
||||||
|
second: value.getSeconds(),
|
||||||
|
millisecond: value.getMilliseconds(),
|
||||||
|
})
|
||||||
|
|
||||||
|
return date
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultCalendarDate(
|
||||||
|
value: Date | null | undefined,
|
||||||
|
defaultValue: Date | null | undefined
|
||||||
|
) {
|
||||||
|
if (value) {
|
||||||
|
return createCalendarDate(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defaultValue) {
|
||||||
|
return createCalendarDate(defaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCalendarDate(date: Date) {
|
||||||
|
return new CalendarDate(
|
||||||
|
date.getFullYear(),
|
||||||
|
date.getMonth() + 1,
|
||||||
|
date.getDate()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCalendarDate(
|
||||||
|
date: CalendarDate | null | undefined,
|
||||||
|
value: Date
|
||||||
|
) {
|
||||||
|
if (!date) {
|
||||||
|
return createCalendarDate(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
date.set({
|
||||||
|
day: value.getDate(),
|
||||||
|
month: value.getMonth() + 1,
|
||||||
|
year: value.getFullYear(),
|
||||||
|
})
|
||||||
|
|
||||||
|
return date
|
||||||
|
}
|
||||||
|
|
||||||
|
const USES_TIME = new Set<Granularity>(["hour", "minute", "second"])
|
||||||
|
|
||||||
|
function createCalendarDateFromDate(date: Date, granularity?: Granularity) {
|
||||||
|
if (granularity && USES_TIME.has(granularity)) {
|
||||||
|
return createCalendarDateTime(date)
|
||||||
|
}
|
||||||
|
|
||||||
|
return createCalendarDate(date)
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCalendarDateFromDate(
|
||||||
|
date: CalendarDate | CalendarDateTime | null | undefined,
|
||||||
|
value: Date,
|
||||||
|
granularity?: Granularity
|
||||||
|
) {
|
||||||
|
if (granularity && USES_TIME.has(granularity)) {
|
||||||
|
return updateCalendarDateTime(date as CalendarDateTime, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return updateCalendarDate(date as CalendarDate, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultCalendarDateFromDate(
|
||||||
|
value: Date | null | undefined,
|
||||||
|
defaultValue: Date | null | undefined,
|
||||||
|
granularity?: Granularity
|
||||||
|
) {
|
||||||
|
if (value) {
|
||||||
|
return createCalendarDateFromDate(value, granularity)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defaultValue) {
|
||||||
|
return createCalendarDateFromDate(defaultValue, granularity)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
createCalendarDate,
|
||||||
|
createCalendarDateFromDate,
|
||||||
|
getDefaultCalendarDate,
|
||||||
|
getDefaultCalendarDateFromDate,
|
||||||
|
updateCalendarDate,
|
||||||
|
updateCalendarDateFromDate,
|
||||||
|
}
|
||||||
Reference in new issue
Block a user