-
Notifications
You must be signed in to change notification settings - Fork 4.5k
chore: add datepicker component #37563
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
83c73c7
1851f71
b9e266e
cf9a392
3c2322e
a276c1c
e067549
a1a552c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| import React from "react"; | ||
| import type { | ||
| DateValue, | ||
| CalendarProps as HeadlessCalendarProps, | ||
| } from "react-aria-components"; | ||
| import { | ||
| CalendarGrid as HeadlessCalendarGrid, | ||
| CalendarGridBody as HeadlessCalendarGridBody, | ||
| CalendarGridHeader as HeadlessCalendarGridHeader, | ||
| Calendar as HeadlessCalendar, | ||
| } from "react-aria-components"; | ||
| import { Flex, IconButton } from "@appsmith/wds"; | ||
|
|
||
| import styles from "./styles.module.css"; | ||
| import { CalendarCell } from "./CalendarCell"; | ||
| import { CalendarHeading } from "./CalendarHeading"; | ||
| import { CalendarHeaderCell } from "./CalendarHeaderCell"; | ||
|
|
||
| type CalendarProps<T extends DateValue> = HeadlessCalendarProps<T>; | ||
|
|
||
| export const Calendar = <T extends DateValue>(props: CalendarProps<T>) => { | ||
| return ( | ||
| <HeadlessCalendar {...props} className={styles.calendar}> | ||
| <Flex alignItems="center" justifyContent="space-between" width="100%"> | ||
| <IconButton icon="chevron-left" slot="previous" variant="ghost" /> | ||
| <CalendarHeading size="subtitle" /> | ||
| <IconButton icon="chevron-right" slot="next" variant="ghost" /> | ||
| </Flex> | ||
| <HeadlessCalendarGrid> | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's the storybook styles that we have applied on
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we override it here? |
||
| <HeadlessCalendarGridHeader> | ||
| {(day) => <CalendarHeaderCell>{day}</CalendarHeaderCell>} | ||
| </HeadlessCalendarGridHeader> | ||
| <HeadlessCalendarGridBody> | ||
| {(date) => <CalendarCell date={date} />} | ||
| </HeadlessCalendarGridBody> | ||
| </HeadlessCalendarGrid> | ||
| </HeadlessCalendar> | ||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import React from "react"; | ||
| import { Text } from "@appsmith/wds"; | ||
| import { | ||
| CalendarCell as HeadlessCalendarCell, | ||
| type CalendarCellProps as HeadlessCalendarCellProps, | ||
| } from "react-aria-components"; | ||
|
|
||
| import styles from "./styles.module.css"; | ||
|
|
||
| export type CalendarCellProps = HeadlessCalendarCellProps & | ||
| React.RefAttributes<HTMLTableCellElement>; | ||
|
|
||
| function CalendarCell(props: CalendarCellProps) { | ||
| const { date } = props; | ||
|
|
||
| return ( | ||
| <HeadlessCalendarCell {...props} className={styles["calendar-cell"]}> | ||
| <Text>{date.day}</Text> | ||
| </HeadlessCalendarCell> | ||
| ); | ||
| } | ||
KelvinOm marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| export { CalendarCell }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import React from "react"; | ||
| import { Text } from "@appsmith/wds"; | ||
| import { CalendarHeaderCell as HeadlessCalendarHeaderCell } from "react-aria-components"; | ||
|
|
||
| import { type CalendarHeaderCellProps as HeadlessCalendarHeaderCellProps } from "react-aria-components"; | ||
|
|
||
| export type CalendarHeaderCellProps = HeadlessCalendarHeaderCellProps & | ||
| React.RefAttributes<HTMLTableCellElement>; | ||
|
|
||
| function CalendarHeaderCell(props: CalendarHeaderCellProps) { | ||
| const { children } = props; | ||
|
|
||
| return ( | ||
| <HeadlessCalendarHeaderCell {...props}> | ||
| <Text color="neutral" fontWeight={700} textAlign="center"> | ||
| {children} | ||
| </Text> | ||
| </HeadlessCalendarHeaderCell> | ||
| ); | ||
| } | ||
|
|
||
| export { CalendarHeaderCell }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import { Text, type TextProps } from "@appsmith/wds"; | ||
| import React, { forwardRef, type ForwardedRef } from "react"; | ||
| import { HeadingContext, useContextProps } from "react-aria-components"; | ||
|
|
||
| function CalendarHeading( | ||
| props: TextProps, | ||
| ref: ForwardedRef<HTMLHeadingElement>, | ||
| ) { | ||
| [props, ref] = useContextProps(props, ref, HeadingContext); | ||
| const { children, ...domProps } = props; | ||
|
|
||
| return ( | ||
| <Text {...domProps} color="neutral" ref={ref}> | ||
| {children} | ||
| </Text> | ||
| ); | ||
| } | ||
|
|
||
| const _CalendarHeading = forwardRef(CalendarHeading); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (nit) You can simplify the export a bit here if you do the same as in our other files.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't get it. Did you mean to use like: const CalendarHeading = forwardRef((
props: TextProps,
ref: ForwardedRef<HTMLHeadingElement>,
) => {
[props, ref] = useContextProps(props, ref, HeadingContext);
const { children, ...domProps } = props;
return (
<Text {...domProps} color="neutral" ref={ref}>
{children}
</Text>
);
});
export { CalendarHeading };
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I mean like we usually do. |
||
|
|
||
| export { _CalendarHeading as CalendarHeading }; | ||
KelvinOm marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| export { Calendar } from "./Calendar"; | ||
| export { CalendarCell } from "./CalendarCell"; | ||
| export { CalendarHeading } from "./CalendarHeading"; | ||
| export { CalendarHeaderCell } from "./CalendarHeaderCell"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| .calendar { | ||
| padding: var(--outer-spacing-3); | ||
| } | ||
|
|
||
| .calendar table { | ||
| display: flex; | ||
| flex-direction: column; | ||
| margin: 0; | ||
| } | ||
|
|
||
| .calendar thead tr { | ||
| display: flex; | ||
| justify-content: space-around; | ||
| padding-block-start: var(--inner-spacing-1); | ||
| } | ||
|
|
||
| .calendar tbody tr { | ||
| display: flex; | ||
| justify-content: space-between; | ||
| } | ||
|
|
||
| .calendar thead th { | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| inline-size: var(--sizing-9); | ||
| block-size: var(--sizing-9); | ||
| } | ||
|
|
||
| .calendar tbody td { | ||
| padding: var(--inner-spacing-1); | ||
| } | ||
|
|
||
| .calendar tbody [role="button"] { | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| inline-size: var(--sizing-9); | ||
| block-size: var(--sizing-9); | ||
| border-radius: var(--border-radius-elevation-3); | ||
| border: var(--border-width-2) solid transparent; | ||
| text-align: center; | ||
| } | ||
|
|
||
| .calendar tbody [role="button"][data-disabled] { | ||
| opacity: var(--opacity-disabled); | ||
| } | ||
|
|
||
| .calendar tbody [role="button"][data-hovered] { | ||
| background-color: var(--color-bg-accent-subtle-hover); | ||
| cursor: pointer; | ||
| } | ||
|
|
||
| .calendar tbody [role="button"][data-pressed] { | ||
| background-color: var(--color-bg-accent-subtle-active); | ||
| } | ||
|
|
||
| .calendar tbody [role="button"][data-selected] { | ||
| background-color: var(--color-bg-accent); | ||
| color: var(--color-fg-on-accent); | ||
| } | ||
|
|
||
| .calendar tbody [role="button"][data-focus-visible] { | ||
| outline: var(--border-width-2) solid var(--color-bd-accent); | ||
| outline-offset: var(--border-width-2); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| import type { Meta, StoryObj } from "@storybook/react"; | ||
| import { Calendar } from "../src"; | ||
| import { today, getLocalTimeZone } from "@internationalized/date"; | ||
|
|
||
| const meta: Meta<typeof Calendar> = { | ||
| component: Calendar, | ||
| title: "WDS/Widgets/Calendar", | ||
| parameters: { | ||
| docs: { | ||
| description: { | ||
| component: "A calendar component for date selection and display.", | ||
| }, | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
| export default meta; | ||
| type Story = StoryObj<typeof Calendar>; | ||
|
|
||
| export const Default: Story = { | ||
| args: { | ||
| defaultValue: today(getLocalTimeZone()), | ||
| }, | ||
| }; | ||
|
|
||
| export const WithMinDate: Story = { | ||
| args: { | ||
| defaultValue: today(getLocalTimeZone()), | ||
| minValue: today(getLocalTimeZone()), | ||
| }, | ||
| }; | ||
|
|
||
| export const WithMaxDate: Story = { | ||
| args: { | ||
| defaultValue: today(getLocalTimeZone()), | ||
| maxValue: today(getLocalTimeZone()).add({ days: 10 }), | ||
| }, | ||
| }; | ||
|
|
||
| export const Disabled: Story = { | ||
| args: { | ||
| defaultValue: today(getLocalTimeZone()), | ||
| isDisabled: true, | ||
| }, | ||
| }; | ||
|
|
||
| export const ReadOnly: Story = { | ||
| args: { | ||
| defaultValue: today(getLocalTimeZone()), | ||
| isReadOnly: true, | ||
| }, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * from "./src"; |
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,112 @@ | ||||||||||||||
| import { | ||||||||||||||
| FieldError, | ||||||||||||||
| FieldLabel, | ||||||||||||||
| Popover, | ||||||||||||||
| Calendar, | ||||||||||||||
| inputFieldStyles, | ||||||||||||||
| TimeField, | ||||||||||||||
| } from "@appsmith/wds"; | ||||||||||||||
| import clsx from "clsx"; | ||||||||||||||
| import React from "react"; | ||||||||||||||
| import { | ||||||||||||||
| Dialog, | ||||||||||||||
| DatePicker as HeadlessDatePicker, | ||||||||||||||
| type TimeValue, | ||||||||||||||
| type DateValue, | ||||||||||||||
| } from "react-aria-components"; | ||||||||||||||
|
|
||||||||||||||
| import type { DatePickerProps } from "./types"; | ||||||||||||||
| import datePickerStyles from "./styles.module.css"; | ||||||||||||||
| import { DatepickerTrigger } from "./DatepickerTrigger"; | ||||||||||||||
|
|
||||||||||||||
| export const DatePicker = <T extends DateValue>(props: DatePickerProps<T>) => { | ||||||||||||||
| const { | ||||||||||||||
| className, | ||||||||||||||
| contextualHelp, | ||||||||||||||
| errorMessage, | ||||||||||||||
| isDisabled, | ||||||||||||||
| isLoading, | ||||||||||||||
| isRequired, | ||||||||||||||
| label, | ||||||||||||||
| placeholderValue, | ||||||||||||||
| popoverClassName, | ||||||||||||||
| size = "medium", | ||||||||||||||
| ...rest | ||||||||||||||
| } = props; | ||||||||||||||
|
|
||||||||||||||
| const placeholder: DateValue | null | undefined = placeholderValue; | ||||||||||||||
| const timePlaceholder = ( | ||||||||||||||
| placeholder && "hour" in placeholder ? placeholder : null | ||||||||||||||
| ) as TimeValue; | ||||||||||||||
| const timeMinValue = ( | ||||||||||||||
| props.minValue && "hour" in props.minValue ? props.minValue : null | ||||||||||||||
| ) as TimeValue; | ||||||||||||||
| const timeMaxValue = ( | ||||||||||||||
| props.maxValue && "hour" in props.maxValue ? props.maxValue : null | ||||||||||||||
| ) as TimeValue; | ||||||||||||||
|
|
||||||||||||||
| return ( | ||||||||||||||
| <HeadlessDatePicker | ||||||||||||||
| aria-label={Boolean(label) ? undefined : "DatePicker"} | ||||||||||||||
KelvinOm marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
| className={clsx(inputFieldStyles.field, className)} | ||||||||||||||
| data-size={size} | ||||||||||||||
| isDisabled={isDisabled} | ||||||||||||||
| isRequired={isRequired} | ||||||||||||||
| {...rest} | ||||||||||||||
| > | ||||||||||||||
| {({ state }) => { | ||||||||||||||
| const root = document.body.querySelector( | ||||||||||||||
| "[data-theme-provider]", | ||||||||||||||
| ) as HTMLButtonElement; | ||||||||||||||
|
Comment on lines
+58
to
+60
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix incorrect type casting of root element The root element with - ) as HTMLButtonElement;
+ ) as HTMLElement;📝 Committable suggestion
Suggested change
|
||||||||||||||
| const timeGranularity = | ||||||||||||||
| state.granularity === "hour" || | ||||||||||||||
| state.granularity === "minute" || | ||||||||||||||
| state.granularity === "second" | ||||||||||||||
| ? state.granularity | ||||||||||||||
| : null; | ||||||||||||||
| const showTimeField = !!timeGranularity; | ||||||||||||||
|
|
||||||||||||||
| return ( | ||||||||||||||
| <> | ||||||||||||||
| <FieldLabel | ||||||||||||||
| contextualHelp={contextualHelp} | ||||||||||||||
| isDisabled={isDisabled} | ||||||||||||||
| isRequired={isRequired} | ||||||||||||||
| > | ||||||||||||||
| {label} | ||||||||||||||
| </FieldLabel> | ||||||||||||||
| <DatepickerTrigger | ||||||||||||||
| isDisabled={isDisabled} | ||||||||||||||
| isLoading={isLoading} | ||||||||||||||
| size={size} | ||||||||||||||
| /> | ||||||||||||||
| <FieldError>{errorMessage}</FieldError> | ||||||||||||||
| <Popover | ||||||||||||||
| UNSTABLE_portalContainer={root} | ||||||||||||||
| className={clsx(datePickerStyles.popover, popoverClassName)} | ||||||||||||||
| > | ||||||||||||||
| <Dialog className={datePickerStyles.dialog}> | ||||||||||||||
| <Calendar /> | ||||||||||||||
| {showTimeField && ( | ||||||||||||||
| <div className={datePickerStyles.timeField}> | ||||||||||||||
| <TimeField | ||||||||||||||
| granularity={timeGranularity} | ||||||||||||||
| hideTimeZone={props.hideTimeZone} | ||||||||||||||
| hourCycle={props.hourCycle} | ||||||||||||||
| label="Time" | ||||||||||||||
| maxValue={timeMaxValue} | ||||||||||||||
| minValue={timeMinValue} | ||||||||||||||
| onChange={state.setTimeValue} | ||||||||||||||
| placeholderValue={timePlaceholder} | ||||||||||||||
| value={state.timeValue} | ||||||||||||||
| /> | ||||||||||||||
| </div> | ||||||||||||||
| )} | ||||||||||||||
| </Dialog> | ||||||||||||||
| </Popover> | ||||||||||||||
| </> | ||||||||||||||
| ); | ||||||||||||||
| }} | ||||||||||||||
| </HeadlessDatePicker> | ||||||||||||||
| ); | ||||||||||||||
| }; | ||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| import clsx from "clsx"; | ||
| import React, { useMemo } from "react"; | ||
| import type { SIZES } from "@appsmith/wds"; | ||
| import { getTypographyClassName } from "@appsmith/wds-theming"; | ||
| import { textInputStyles, Spinner, IconButton } from "@appsmith/wds"; | ||
| import { DateInput, DateSegment, Group } from "react-aria-components"; | ||
|
|
||
| import dateInputStyles from "./styles.module.css"; | ||
|
|
||
| interface DatepickerTriggerProps { | ||
| isLoading?: boolean; | ||
| size?: Omit<keyof typeof SIZES, "xSmall" | "large">; | ||
| isDisabled?: boolean; | ||
| } | ||
|
|
||
| export const DatepickerTrigger = (props: DatepickerTriggerProps) => { | ||
| const { isDisabled, isLoading, size } = props; | ||
|
|
||
| const suffix = useMemo(() => { | ||
| if (Boolean(isLoading)) return <Spinner />; | ||
|
|
||
| return ( | ||
| <IconButton | ||
| color={Boolean(isLoading) ? "neutral" : "accent"} | ||
| icon="calendar-month" | ||
| isDisabled={isDisabled} | ||
| isLoading={isLoading} | ||
| size={size === "medium" ? "small" : "xSmall"} | ||
| variant={Boolean(isLoading) ? "ghost" : "filled"} | ||
| /> | ||
| ); | ||
| }, [isLoading, size, isDisabled]); | ||
KelvinOm marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| return ( | ||
| <Group className={textInputStyles.inputGroup}> | ||
| <DateInput | ||
| className={clsx( | ||
| textInputStyles.input, | ||
| dateInputStyles.input, | ||
| getTypographyClassName("body"), | ||
| )} | ||
| data-date-input | ||
| > | ||
| {(segment) => <DateSegment segment={segment} />} | ||
| </DateInput> | ||
| <span data-input-suffix>{suffix}</span> | ||
| </Group> | ||
| ); | ||
| }; | ||

Uh oh!
There was an error while loading. Please reload this page.