chore: add datepicker component (#37563)
Added daterpicker component along with other components needed for it like Calendar andTimeField Datepicker  Calendar  Timefield  /ok-to-test tags="@tag.Anvil" <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes - **New Features** - Introduced a `Calendar` component for date selection and display. - Added a `DatePicker` component for selecting dates and times with enhanced error handling. - Launched a `TimeField` component for time input with optional prefix and suffix. - Updated `TextField` component replacing the previous `TextInput` for improved usability. - **Bug Fixes** - Enhanced styling and responsiveness of input components. - **Documentation** - Added Storybook stories for `Calendar`, `DatePicker`, and `TimeField` components to showcase functionalities and configurations. - **Chores** - Refactored imports to utilize the new `TextField` component across various widgets. <!-- end of auto-generated comment: release notes by coderabbit.ai --> <!-- This is an auto-generated comment: Cypress test results --> > [!TIP] > 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉 > Workflow run: <https://github.com/appsmithorg/appsmith/actions/runs/11970103158> > Commit: a1a552cb0bfdc9754341de5db0a6d8b142479083 > <a href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=11970103158&attempt=1" target="_blank">Cypress dashboard</a>. > Tags: `@tag.Anvil` > Spec: > <hr>Fri, 22 Nov 2024 10:01:23 UTC <!-- end of auto-generated comment: Cypress test results -->
This commit is contained in:
parent
afa2324432
commit
d87f7ccd62
|
|
@ -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>
|
||||
<HeadlessCalendarGridHeader>
|
||||
{(day) => <CalendarHeaderCell>{day}</CalendarHeaderCell>}
|
||||
</HeadlessCalendarGridHeader>
|
||||
<HeadlessCalendarGridBody>
|
||||
{(date) => <CalendarCell date={date} />}
|
||||
</HeadlessCalendarGridBody>
|
||||
</HeadlessCalendarGrid>
|
||||
</HeadlessCalendar>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
export { CalendarCell };
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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);
|
||||
|
||||
export { _CalendarHeading as CalendarHeading };
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
export { Calendar } from "./Calendar";
|
||||
export { CalendarCell } from "./CalendarCell";
|
||||
export { CalendarHeading } from "./CalendarHeading";
|
||||
export { CalendarHeaderCell } from "./CalendarHeaderCell";
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from "./src";
|
||||
|
|
@ -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"}
|
||||
className={clsx(inputFieldStyles.field, className)}
|
||||
data-size={size}
|
||||
isDisabled={isDisabled}
|
||||
isRequired={isRequired}
|
||||
{...rest}
|
||||
>
|
||||
{({ state }) => {
|
||||
const root = document.body.querySelector(
|
||||
"[data-theme-provider]",
|
||||
) as HTMLButtonElement;
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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]);
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export { DatePicker } from "./Datepicker";
|
||||
export * from "./types";
|
||||
export { default as dateTimeInputStyles } from "./styles.module.css";
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
.input:is([data-date-input]) {
|
||||
display: flex;
|
||||
gap: calc(var(--inner-spacing-1) / 2);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.input:is([data-date-input]) [data-focused="true"] {
|
||||
background-color: var(--color-bg-accent);
|
||||
color: var(--color-fg-on-accent);
|
||||
box-shadow: 0 0 0 1px var(--color-bd-focus);
|
||||
}
|
||||
|
||||
.popover {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.dialog .timeField {
|
||||
padding: var(--outer-spacing-3);
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import type {
|
||||
DateValue,
|
||||
DatePickerProps as SpectrumDatePickerProps,
|
||||
} from "react-aria-components";
|
||||
import type { SIZES, FieldProps } from "@appsmith/wds";
|
||||
|
||||
export interface DatePickerProps<T extends DateValue>
|
||||
extends Omit<SpectrumDatePickerProps<T>, "slot" | "placeholder">,
|
||||
FieldProps {
|
||||
/** size of the select
|
||||
*
|
||||
* @default medium
|
||||
*/
|
||||
size?: Omit<keyof typeof SIZES, "xSmall" | "large">;
|
||||
/**
|
||||
* className for the popover
|
||||
*/
|
||||
popoverClassName?: string;
|
||||
}
|
||||
|
||||
export type { DateValue };
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
import React from "react";
|
||||
import { objectKeys } from "@appsmith/utils";
|
||||
import { Button, Flex, SIZES } from "@appsmith/wds";
|
||||
import { parseDate } from "@internationalized/date";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import { DatePicker } from "../src";
|
||||
/**
|
||||
* A date picker allows a user to select a date.
|
||||
*/
|
||||
const meta: Meta<typeof DatePicker> = {
|
||||
component: DatePicker,
|
||||
title: "WDS/Widgets/DatePicker",
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof DatePicker>;
|
||||
|
||||
export const Main: Story = {
|
||||
args: {},
|
||||
render: (args) => (
|
||||
<Flex width="sizing-60">
|
||||
<DatePicker {...args} popoverClassName="sb-unstyled" />
|
||||
</Flex>
|
||||
),
|
||||
};
|
||||
|
||||
export const WithDefaultValue: Story = {
|
||||
args: {
|
||||
label: "Default Value",
|
||||
value: parseDate("2023-06-15"),
|
||||
},
|
||||
render: (args) => (
|
||||
<Flex width="sizing-60">
|
||||
<DatePicker {...args} popoverClassName="sb-unstyled" />
|
||||
</Flex>
|
||||
),
|
||||
};
|
||||
|
||||
/**
|
||||
* The component supports two sizes `small` and `medium`. Default size is `medium`.
|
||||
*/
|
||||
export const Sizes: Story = {
|
||||
render: () => (
|
||||
<Flex direction="column" gap="spacing-4" width="sizing-60">
|
||||
{objectKeys(SIZES)
|
||||
.filter((size) => !["xSmall", "large"].includes(size))
|
||||
.map((size) => (
|
||||
<DatePicker key={size} popoverClassName="sb-unstyled" size={size} />
|
||||
))}
|
||||
</Flex>
|
||||
),
|
||||
};
|
||||
|
||||
export const Loading: Story = {
|
||||
args: {
|
||||
isLoading: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
isDisabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const Validation: Story = {
|
||||
render: () => (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
alert("Form submitted");
|
||||
}}
|
||||
>
|
||||
<Flex direction="column" gap="spacing-5" width="sizing-60">
|
||||
<DatePicker
|
||||
isRequired
|
||||
label="Validation"
|
||||
popoverClassName="sb-unstyled"
|
||||
/>
|
||||
<Button type="submit">Submit</Button>
|
||||
</Flex>
|
||||
</form>
|
||||
),
|
||||
};
|
||||
|
||||
export const ContextualHelp: Story = {
|
||||
args: {
|
||||
label: "Date",
|
||||
contextualHelp: "Click to open the date picker and select a date",
|
||||
},
|
||||
};
|
||||
|
||||
export const MaxDate: Story = {
|
||||
args: {
|
||||
label: "Date",
|
||||
maxValue: parseDate("2024-06-15"),
|
||||
},
|
||||
};
|
||||
|
||||
export const MinDate: Story = {
|
||||
args: {
|
||||
label: "Date",
|
||||
minValue: parseDate("2024-06-15"),
|
||||
},
|
||||
};
|
||||
|
||||
export const Granularity: Story = {
|
||||
render: () => (
|
||||
<Flex direction="column" gap="spacing-5" width="sizing-100">
|
||||
<DatePicker
|
||||
granularity="day"
|
||||
label="Day"
|
||||
popoverClassName="sb-unstyled"
|
||||
/>
|
||||
<DatePicker
|
||||
granularity="hour"
|
||||
label="Hour"
|
||||
popoverClassName="sb-unstyled"
|
||||
/>
|
||||
<DatePicker
|
||||
granularity="minute"
|
||||
label="Minute"
|
||||
popoverClassName="sb-unstyled"
|
||||
/>
|
||||
<DatePicker
|
||||
granularity="second"
|
||||
label="Second"
|
||||
popoverClassName="sb-unstyled"
|
||||
/>
|
||||
</Flex>
|
||||
),
|
||||
};
|
||||
|
|
@ -128,7 +128,15 @@
|
|||
* ----------------------------------------------------------------------------
|
||||
*/
|
||||
.inputGroup[data-hovered]
|
||||
.input:not(:is([data-focused], [data-readonly], [data-disabled])) {
|
||||
.input:not(
|
||||
:is(
|
||||
[data-focused],
|
||||
[data-readonly],
|
||||
[data-disabled],
|
||||
[data-focus-within],
|
||||
:has(~ input[data-disabled="true"])
|
||||
)
|
||||
) {
|
||||
background-color: var(--color-bg-neutral-subtle-hover);
|
||||
box-shadow: inset 0 0 0 1px var(--color-bd-on-neutral-subtle-hover);
|
||||
}
|
||||
|
|
@ -175,8 +183,7 @@
|
|||
* ----------------------------------------------------------------------------
|
||||
*/
|
||||
.input[data-disabled],
|
||||
.input[data-disabled] :is(input, textarea),
|
||||
.input[data-disabled] label {
|
||||
.input:has(~ input[data-disabled]) {
|
||||
cursor: not-allowed;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
|
@ -202,7 +209,7 @@
|
|||
* FOCUSSED
|
||||
* ----------------------------------------------------------------------------
|
||||
*/
|
||||
.input[data-focused]:not([data-readonly]) {
|
||||
.input:is([data-focused], [data-focus-within]):not([data-readonly]) {
|
||||
background-color: transparent;
|
||||
box-shadow: 0 0 0 2px var(--color-bd-focus);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
import type { PopoverProps } from "react-aria-components";
|
||||
import { Popover as HeadlessPopover } from "react-aria-components";
|
||||
|
|
@ -5,10 +6,10 @@ import { Popover as HeadlessPopover } from "react-aria-components";
|
|||
import styles from "./styles.module.css";
|
||||
|
||||
export const Popover = (props: PopoverProps) => {
|
||||
const { children, ...rest } = props;
|
||||
const { children, className, ...rest } = props;
|
||||
|
||||
return (
|
||||
<HeadlessPopover {...rest} className={styles.popover}>
|
||||
<HeadlessPopover {...rest} className={clsx(styles.popover, className)}>
|
||||
{children}
|
||||
</HeadlessPopover>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ export interface TextProps {
|
|||
/** Sets the CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the element. Only use as a **last resort**. Use style props instead. */
|
||||
className?: string;
|
||||
/** The children of the component. */
|
||||
children: ReactNode;
|
||||
children?: ReactNode;
|
||||
/** title attribute for the component */
|
||||
title?: string;
|
||||
/** Sets the HTML [id](https://developer.mozilla.org/en-US/docs/Web/API/Element/id) for the element. */
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
export * from "./src";
|
||||
|
|
@ -3,9 +3,9 @@ import React from "react";
|
|||
import { FieldError, FieldLabel, Input, inputFieldStyles } from "@appsmith/wds";
|
||||
import { TextField as HeadlessTextField } from "react-aria-components";
|
||||
|
||||
import type { TextInputProps } from "./types";
|
||||
import type { TextFieldProps } from "./types";
|
||||
|
||||
export function TextInput(props: TextInputProps) {
|
||||
export function TextField(props: TextFieldProps) {
|
||||
const {
|
||||
contextualHelp,
|
||||
errorMessage,
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./TextField";
|
||||
export type { TextFieldProps } from "./types";
|
||||
|
|
@ -2,7 +2,7 @@ import type { ReactNode } from "react";
|
|||
import type { FieldProps, SIZES } from "@appsmith/wds";
|
||||
import type { TextFieldProps as AriaTextFieldProps } from "react-aria-components";
|
||||
|
||||
export interface TextInputProps extends AriaTextFieldProps, FieldProps {
|
||||
export interface TextFieldProps extends AriaTextFieldProps, FieldProps {
|
||||
placeholder?: string;
|
||||
suffix?: ReactNode;
|
||||
prefix?: ReactNode;
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import React from "react";
|
||||
import { Form } from "react-aria-components";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { Flex, Icon, TextInput, Button } from "@appsmith/wds";
|
||||
import { Flex, Icon, TextField, Button } from "@appsmith/wds";
|
||||
|
||||
const meta: Meta<typeof TextInput> = {
|
||||
title: "WDS/Widgets/TextInput",
|
||||
component: TextInput,
|
||||
const meta: Meta<typeof TextField> = {
|
||||
title: "WDS/Widgets/TextField",
|
||||
component: TextField,
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
placeholder: "Write something...",
|
||||
|
|
@ -13,7 +13,7 @@ const meta: Meta<typeof TextInput> = {
|
|||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof TextInput>;
|
||||
type Story = StoryObj<typeof TextField>;
|
||||
|
||||
export const Main: Story = {
|
||||
args: {
|
||||
|
|
@ -36,12 +36,11 @@ export const WithContextualHelp: Story = {
|
|||
};
|
||||
|
||||
export const WithPrefixAndSuffix: Story = {
|
||||
render: (args) => (
|
||||
render: () => (
|
||||
<Flex direction="column" gap="spacing-4">
|
||||
<TextInput {...args} suffix={<Icon name="user" size="medium" />} />
|
||||
<TextInput {...args} prefix={<Icon name="user" size="medium" />} />
|
||||
<TextInput
|
||||
{...args}
|
||||
<TextField suffix={<Icon name="user" size="medium" />} />
|
||||
<TextField prefix={<Icon name="user" size="medium" />} />
|
||||
<TextField
|
||||
prefix={<Icon name="user" size="medium" />}
|
||||
suffix={<Icon name="user" size="medium" />}
|
||||
/>
|
||||
|
|
@ -79,16 +78,14 @@ export const Readonly: Story = {
|
|||
};
|
||||
|
||||
export const Size: Story = {
|
||||
render: (args) => (
|
||||
render: () => (
|
||||
<Flex direction="column" gap="spacing-4">
|
||||
<TextInput
|
||||
{...args}
|
||||
<TextField
|
||||
label="Small"
|
||||
prefix={<Icon name="user" size="medium" />}
|
||||
size="small"
|
||||
/>
|
||||
<TextInput
|
||||
{...args}
|
||||
<TextField
|
||||
label="Medium"
|
||||
prefix={<Icon name="user" size="medium" />}
|
||||
size="medium"
|
||||
|
|
@ -98,11 +95,10 @@ export const Size: Story = {
|
|||
};
|
||||
|
||||
export const Validation: Story = {
|
||||
render: (args) => (
|
||||
render: () => (
|
||||
<Form onSubmit={(e) => e.preventDefault()}>
|
||||
<Flex direction="column" gap="spacing-3" width="sizing-60">
|
||||
<TextInput
|
||||
{...args}
|
||||
<TextField
|
||||
errorMessage="Please enter a valid email address"
|
||||
isRequired
|
||||
label="Email"
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
export * from "./TextInput";
|
||||
export type { TextInputProps } from "./types";
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from "./src";
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
import {
|
||||
FieldError,
|
||||
FieldLabel,
|
||||
inputFieldStyles,
|
||||
textInputStyles,
|
||||
dateTimeInputStyles,
|
||||
} from "@appsmith/wds";
|
||||
import {
|
||||
DateInput,
|
||||
DateSegment,
|
||||
Group,
|
||||
TimeField as HeadlessTimeField,
|
||||
type TimeValue,
|
||||
} from "react-aria-components";
|
||||
import { getTypographyClassName } from "@appsmith/wds-theming";
|
||||
|
||||
import type { TimeFieldProps } from "./types";
|
||||
|
||||
export function TimeField<T extends TimeValue>(props: TimeFieldProps<T>) {
|
||||
const {
|
||||
contextualHelp,
|
||||
errorMessage,
|
||||
isDisabled,
|
||||
isInvalid,
|
||||
isReadOnly,
|
||||
isRequired,
|
||||
label,
|
||||
prefix,
|
||||
size = "medium",
|
||||
suffix,
|
||||
value,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<HeadlessTimeField
|
||||
{...rest}
|
||||
className={clsx(inputFieldStyles.field)}
|
||||
data-field=""
|
||||
isDisabled={isDisabled}
|
||||
isInvalid={isInvalid}
|
||||
isReadOnly={isReadOnly}
|
||||
isRequired={isRequired}
|
||||
value={value}
|
||||
>
|
||||
<FieldLabel
|
||||
contextualHelp={contextualHelp}
|
||||
isDisabled={isDisabled}
|
||||
isRequired={isRequired}
|
||||
>
|
||||
{label}
|
||||
</FieldLabel>
|
||||
<Group className={textInputStyles.inputGroup}>
|
||||
<DateInput
|
||||
className={clsx(
|
||||
textInputStyles.input,
|
||||
dateTimeInputStyles.input,
|
||||
getTypographyClassName("body"),
|
||||
)}
|
||||
data-date-input
|
||||
data-size={Boolean(size) ? size : undefined}
|
||||
>
|
||||
{(segment) => <DateSegment segment={segment} />}
|
||||
</DateInput>
|
||||
{Boolean(prefix) && <span data-input-prefix>{prefix}</span>}
|
||||
{Boolean(suffix) && <span data-input-suffix>{suffix}</span>}
|
||||
</Group>
|
||||
<FieldError>{errorMessage}</FieldError>
|
||||
</HeadlessTimeField>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export { TimeField } from "./TimeField";
|
||||
export type { TimeFieldProps } from "./types";
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import type { ReactNode } from "react";
|
||||
import type { FieldProps, SIZES } from "@appsmith/wds";
|
||||
import type {
|
||||
TimeFieldProps as AriaTimeFieldProps,
|
||||
TimeValue,
|
||||
} from "react-aria-components";
|
||||
|
||||
export interface TimeFieldProps<T extends TimeValue>
|
||||
extends AriaTimeFieldProps<T>,
|
||||
FieldProps {
|
||||
suffix?: ReactNode;
|
||||
prefix?: ReactNode;
|
||||
size?: Omit<keyof typeof SIZES, "xSmall">;
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { TimeField } from "../src";
|
||||
|
||||
import { Time } from "@internationalized/date";
|
||||
|
||||
const meta: Meta<typeof TimeField> = {
|
||||
title: "WDS/Widgets/TimeField",
|
||||
component: TimeField,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"A time input component that allows users to enter and select time values.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof TimeField>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const WithLabel: Story = {
|
||||
args: {
|
||||
value: new Time(14, 15),
|
||||
label: "With Label",
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
label: "Disabled",
|
||||
value: new Time(15, 30),
|
||||
isDisabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithError: Story = {
|
||||
args: {
|
||||
isInvalid: true,
|
||||
value: new Time(9, 45),
|
||||
errorMessage: "Please enter a valid time",
|
||||
},
|
||||
};
|
||||
|
||||
export const Required: Story = {
|
||||
args: {
|
||||
label: "Required",
|
||||
isRequired: true,
|
||||
},
|
||||
};
|
||||
|
|
@ -21,7 +21,7 @@ export * from "./components/ContextualHelp";
|
|||
export * from "./components/Link";
|
||||
export * from "./components/Popover";
|
||||
export * from "./components/FieldError";
|
||||
export * from "./components/TextInput";
|
||||
export * from "./components/TextField";
|
||||
export * from "./components/FieldLabel";
|
||||
export * from "./components/Input";
|
||||
export * from "./components/Field";
|
||||
|
|
@ -32,6 +32,9 @@ export * from "./components/MenuItem";
|
|||
export * from "./components/Markdown";
|
||||
export * from "./components/Sidebar";
|
||||
export * from "./components/Sheet";
|
||||
export * from "./components/Calendar";
|
||||
export * from "./components/Datepicker";
|
||||
export * from "./components/TimeField";
|
||||
|
||||
export * from "./utils";
|
||||
export * from "./hooks";
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import {
|
|||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalContent,
|
||||
TextInput,
|
||||
TextField,
|
||||
ComboBox,
|
||||
Radio,
|
||||
ListBoxItem,
|
||||
|
|
@ -139,7 +139,7 @@ export const ComplexForm = () => {
|
|||
<TextArea label="Your comment" />
|
||||
</Flex>
|
||||
<Flex gap="spacing-2">
|
||||
<TextInput />
|
||||
<TextField />
|
||||
<ComboBox>
|
||||
{[
|
||||
{
|
||||
|
|
@ -167,7 +167,7 @@ export const ComplexForm = () => {
|
|||
<Button>Ok</Button>
|
||||
</Flex>
|
||||
<Flex gap="spacing-2">
|
||||
<TextInput size="small" />
|
||||
<TextField size="small" />
|
||||
<ComboBox
|
||||
items={[
|
||||
{
|
||||
|
|
|
|||
|
|
@ -11,17 +11,17 @@
|
|||
font-family: var(--font-family) !important;
|
||||
}
|
||||
|
||||
th, td,
|
||||
th:not(.sb-unstyled th), td:not(.sb-unstyled td),
|
||||
.css-s230ta {
|
||||
border-color: var(--color-bd) !important;
|
||||
}
|
||||
|
||||
td,
|
||||
td:not(.sb-unstyled td),
|
||||
.css-s230ta {
|
||||
background-color: var(--color-bg-elevation-1) !important;
|
||||
}
|
||||
|
||||
code:not(:has(span)),
|
||||
code:not(:has(span)):not(.sb-unstyled code),
|
||||
.css-o1d7ko {
|
||||
background-color: var(--color-bg-accent-subtle) !important;
|
||||
color: var(--color-fg-on-accent-subtle) !important;
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
}
|
||||
|
||||
/** Note: adding this so that all links are styled the same way excepts the one that are in the story */
|
||||
a:not(.docs-story a) {
|
||||
a:not(.docs-story a):not(.sb-unstyled a) {
|
||||
color: var(--color-fg-accent) !important;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from "react";
|
||||
import { Text, TextInput } from "@appsmith/wds";
|
||||
import { Text, TextField } from "@appsmith/wds";
|
||||
import type { CurrencyInputComponentProps } from "./types";
|
||||
import { CurrencyTypeOptions } from "constants/Currency";
|
||||
import { useDebouncedValue } from "@mantine/hooks";
|
||||
|
|
@ -24,7 +24,7 @@ export function CurrencyInputComponent(props: CurrencyInputComponentProps) {
|
|||
const [errorMessage] = useDebouncedValue(props.errorMessage, DEBOUNCE_TIME);
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
<TextField
|
||||
autoComplete={props.autoComplete}
|
||||
autoFocus={props.autoFocus}
|
||||
contextualHelp={props.tooltip}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from "react";
|
||||
import { isNil } from "lodash";
|
||||
import { TextInput } from "@appsmith/wds";
|
||||
import { TextField } from "@appsmith/wds";
|
||||
import { Icon, TextArea } from "@appsmith/wds";
|
||||
import { useDebouncedValue } from "@mantine/hooks";
|
||||
import { INPUT_TYPES } from "modules/ui-builder/ui/wds/WDSBaseInputWidget";
|
||||
|
|
@ -46,7 +46,7 @@ function InputComponent(props: InputComponentProps) {
|
|||
})();
|
||||
|
||||
const ElementType: React.ElementType =
|
||||
props.inputType === INPUT_TYPES.MULTI_LINE_TEXT ? TextArea : TextInput;
|
||||
props.inputType === INPUT_TYPES.MULTI_LINE_TEXT ? TextArea : TextField;
|
||||
|
||||
const autoComplete = (() => {
|
||||
if (
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from "react";
|
||||
import { ISDCodeOptions } from "constants/ISDCodes_v2";
|
||||
import { Text, TextInput } from "@appsmith/wds";
|
||||
import { Text, TextField } from "@appsmith/wds";
|
||||
|
||||
import type { PhoneInputComponentProps } from "./types";
|
||||
|
||||
|
|
@ -16,7 +16,7 @@ export function PhoneInputComponent(props: PhoneInputComponentProps) {
|
|||
);
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
<TextField
|
||||
autoComplete={props.autoComplete}
|
||||
autoFocus={props.autoFocus}
|
||||
contextualHelp={props.tooltip}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Keys } from "@blueprintjs/core";
|
||||
import { TextInput } from "@appsmith/wds";
|
||||
import { TextField } from "@appsmith/wds";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { EventType } from "constants/AppsmithActionConstants/ActionConstants";
|
||||
|
||||
|
|
@ -46,7 +46,7 @@ function PageNumberInputComponent(props: {
|
|||
);
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
<TextField
|
||||
className="t--table-widget-page-input"
|
||||
excludeFromTabOrder={props.excludeFromTabOrder}
|
||||
isDisabled={props.disabled}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from "react";
|
||||
import { TextInput } from "@appsmith/wds";
|
||||
import { TextField } from "@appsmith/wds";
|
||||
|
||||
export interface SearchProps {
|
||||
isVisibleSearch?: boolean;
|
||||
|
|
@ -12,7 +12,7 @@ export const Search = (props: SearchProps) => {
|
|||
const { excludeFromTabOrder, isVisibleSearch, onSearch, searchKey } = props;
|
||||
|
||||
return isVisibleSearch ? (
|
||||
<TextInput
|
||||
<TextField
|
||||
excludeFromTabOrder={excludeFromTabOrder}
|
||||
onChange={onSearch}
|
||||
placeholder="Search..."
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user