diff --git a/.changeset/ui-date-picker.md b/.changeset/ui-date-picker.md new file mode 100644 index 0000000000..eba3624e99 --- /dev/null +++ b/.changeset/ui-date-picker.md @@ -0,0 +1,7 @@ +--- +'@backstage/ui': patch +--- + +Added new `DatePicker` component — combines a date field and a calendar popover for selecting a date, built on React Aria with full keyboard and screen reader accessibility. Uses BUI design tokens throughout, including auto-incremented backgrounds via the bg consumer pattern. + +**Affected components:** DatePicker diff --git a/docs-ui/src/app/components/date-picker/components.tsx b/docs-ui/src/app/components/date-picker/components.tsx new file mode 100644 index 0000000000..699831c793 --- /dev/null +++ b/docs-ui/src/app/components/date-picker/components.tsx @@ -0,0 +1,36 @@ +'use client'; + +import { DatePicker } from '../../../../../packages/ui/src/components/DatePicker/DatePicker'; +import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex'; +import { parseDate } from '@internationalized/date'; + +export const WithLabel = () => { + return ; +}; + +export const Sizes = () => { + return ( + + + + + ); +}; + +export const WithDefaultValue = () => { + return ( + + ); +}; + +export const Disabled = () => { + return ; +}; diff --git a/docs-ui/src/app/components/date-picker/page.mdx b/docs-ui/src/app/components/date-picker/page.mdx new file mode 100644 index 0000000000..d0e8cfdc11 --- /dev/null +++ b/docs-ui/src/app/components/date-picker/page.mdx @@ -0,0 +1,82 @@ +import { PropsTable } from '@/components/PropsTable'; +import { Snippet } from '@/components/Snippet'; +import { datePickerPropDefs } from './props-definition'; +import { + datePickerUsageSnippet, + withLabelSnippet, + sizesSnippet, + withDefaultValueSnippet, + disabledSnippet, +} from './snippets'; +import { WithLabel, Sizes, WithDefaultValue, Disabled } from './components'; +import { PageTitle } from '@/components/PageTitle'; +import { Theming } from '@/components/Theming'; +import { DatePickerDefinition } from '../../../utils/definitions'; +import { ChangelogComponent } from '@/components/ChangelogComponent'; +import { CodeBlock } from '@/components/CodeBlock'; +import { ReactAriaLink } from '@/components/ReactAriaLink'; + +export const reactAriaUrls = { + datePicker: 'https://react-aria.adobe.com/DatePicker', +}; + + + +} + code={withLabelSnippet} +/> + +## Usage + + + +## API reference + + + + + +## Examples + +### Sizes + +} + code={sizesSnippet} + layout="side-by-side" +/> + +### With default value + +} + code={withDefaultValueSnippet} + layout="side-by-side" +/> + +### Disabled + +} + code={disabledSnippet} + layout="side-by-side" +/> + + + + diff --git a/docs-ui/src/app/components/date-picker/props-definition.tsx b/docs-ui/src/app/components/date-picker/props-definition.tsx new file mode 100644 index 0000000000..ccb5842b17 --- /dev/null +++ b/docs-ui/src/app/components/date-picker/props-definition.tsx @@ -0,0 +1,94 @@ +import { + classNamePropDefs, + stylePropDefs, + type PropDef, +} from '@/utils/propDefs'; +import { Chip } from '@/components/Chip'; + +export const datePickerPropDefs: Record = { + size: { + type: 'enum', + values: ['small', 'medium'], + default: 'small', + responsive: true, + description: ( + <> + Visual size of the picker. Use small for dense layouts,{' '} + medium for prominent fields. + + ), + }, + label: { + type: 'string', + description: 'Visible label displayed above the picker.', + }, + secondaryLabel: { + type: 'string', + description: ( + <> + Secondary text shown next to the label. If not provided and isRequired + is true, displays Required. + + ), + }, + description: { + type: 'string', + description: 'Help text displayed below the label.', + }, + value: { + type: 'enum', + values: ['DateValue'], + description: 'Controlled value of the date.', + }, + defaultValue: { + type: 'enum', + values: ['DateValue'], + description: 'Default value for uncontrolled usage.', + }, + onChange: { + type: 'enum', + values: ['(value: DateValue | null) => void'], + description: 'Handler called when the selected date changes.', + }, + granularity: { + type: 'enum', + values: ['day', 'hour', 'minute', 'second'], + default: 'day', + description: + 'Smallest unit displayed. Defaults to "day" for dates and "minute" for times.', + }, + minValue: { + type: 'enum', + values: ['DateValue'], + description: 'Minimum allowed date. Dates before this are disabled.', + }, + maxValue: { + type: 'enum', + values: ['DateValue'], + description: 'Maximum allowed date. Dates after this are disabled.', + }, + isDateUnavailable: { + type: 'enum', + values: ['(date: DateValue) => boolean'], + description: + 'Callback invoked for each calendar date. Return true to mark a date as unavailable.', + }, + name: { + type: 'string', + description: 'Form field name for the date, submitted as ISO 8601.', + }, + isRequired: { + type: 'boolean', + description: 'Whether the field is required for form submission.', + }, + isDisabled: { + type: 'boolean', + description: 'Whether the picker is disabled.', + }, + isReadOnly: { + type: 'boolean', + description: 'Whether the picker is read-only.', + }, + ...classNamePropDefs, + ...stylePropDefs, +}; diff --git a/docs-ui/src/app/components/date-picker/snippets.ts b/docs-ui/src/app/components/date-picker/snippets.ts new file mode 100644 index 0000000000..9bab51bf45 --- /dev/null +++ b/docs-ui/src/app/components/date-picker/snippets.ts @@ -0,0 +1,25 @@ +export const datePickerUsageSnippet = `import { DatePicker } from '@backstage/ui'; + +`; + +export const withLabelSnippet = ``; + +export const sizesSnippet = ` + + +`; + +export const withDefaultValueSnippet = `import { parseDate } from '@internationalized/date'; + +`; + +export const disabledSnippet = ``; diff --git a/docs-ui/src/utils/data.ts b/docs-ui/src/utils/data.ts index fb7ac148a5..515deacb13 100644 --- a/docs-ui/src/utils/data.ts +++ b/docs-ui/src/utils/data.ts @@ -58,6 +58,10 @@ export const components: Page[] = [ title: 'Container', slug: 'container', }, + { + title: 'DatePicker', + slug: 'date-picker', + }, { title: 'DateRangePicker', slug: 'date-range-picker', diff --git a/packages/ui/report.api.md b/packages/ui/report.api.md index ac6c9f13a2..3fcdb8319e 100644 --- a/packages/ui/report.api.md +++ b/packages/ui/report.api.md @@ -15,6 +15,7 @@ import type { ComponentProps } from 'react'; import type { ComponentPropsWithoutRef } from 'react'; import type { ComponentPropsWithRef } from 'react'; import type { CSSProperties } from 'react'; +import type { DatePickerProps as DatePickerProps_2 } from 'react-aria-components'; import type { DateRangePickerProps as DateRangePickerProps_2 } from 'react-aria-components'; import type { DateValue } from '@internationalized/date'; import type { DialogTriggerProps as DialogTriggerProps_2 } from 'react-aria-components'; @@ -1211,6 +1212,79 @@ export interface CursorResponse { totalCount?: number; } +// @public +export const DatePicker: ForwardRefExoticComponent< + DatePickerProps & RefAttributes +>; + +// @public +export const DatePickerCalendarDefinition: { + readonly styles: { + readonly [key: string]: string; + }; + readonly classNames: { + readonly root: 'bui-DatePickerCalendar'; + readonly header: 'bui-DatePickerCalendarHeader'; + readonly heading: 'bui-DatePickerCalendarHeading'; + readonly navButton: 'bui-DatePickerCalendarNavButton'; + readonly grid: 'bui-DatePickerCalendarGrid'; + readonly gridHeader: 'bui-DatePickerCalendarGridHeader'; + readonly headerCell: 'bui-DatePickerCalendarHeaderCell'; + readonly gridBody: 'bui-DatePickerCalendarGridBody'; + readonly cell: 'bui-DatePickerCalendarCell'; + }; + readonly propDefs: {}; +}; + +// @public +export const DatePickerDefinition: { + readonly styles: { + readonly [key: string]: string; + }; + readonly classNames: { + readonly root: 'bui-DatePicker'; + }; + readonly propDefs: { + readonly size: { + readonly dataAttribute: true; + readonly default: 'small'; + }; + readonly className: {}; + readonly label: {}; + readonly description: {}; + readonly secondaryLabel: {}; + }; +}; + +// @public +export const DatePickerGroupDefinition: { + readonly styles: { + readonly [key: string]: string; + }; + readonly classNames: { + readonly root: 'bui-DatePickerGroup'; + readonly dateInput: 'bui-DatePickerDateInput'; + readonly segment: 'bui-DatePickerSegment'; + readonly button: 'bui-DatePickerButton'; + }; + readonly bg: 'consumer'; + readonly propDefs: {}; +}; + +// @public (undocumented) +export type DatePickerOwnProps = { + size?: 'small' | 'medium' | Partial>; + className?: string; + label?: FieldLabelProps['label']; + description?: FieldLabelProps['description']; + secondaryLabel?: FieldLabelProps['secondaryLabel']; +}; + +// @public (undocumented) +export interface DatePickerProps + extends Omit, 'className' | 'children'>, + DatePickerOwnProps {} + // @public export const DateRangePicker: ForwardRefExoticComponent< DateRangePickerProps & RefAttributes diff --git a/packages/ui/src/components/DatePicker/DatePicker.module.css b/packages/ui/src/components/DatePicker/DatePicker.module.css new file mode 100644 index 0000000000..6fd60dffac --- /dev/null +++ b/packages/ui/src/components/DatePicker/DatePicker.module.css @@ -0,0 +1,317 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@layer tokens, base, components, utilities; + +@layer components { + /* ============================================================ + Root + ============================================================ */ + + .bui-DatePicker { + display: flex; + flex-direction: column; + font-family: var(--bui-font-regular); + width: 100%; + flex-shrink: 0; + } + + /* ============================================================ + Field group — custom container (not reusing any BUI field) + ============================================================ */ + + .bui-DatePickerGroup { + display: flex; + align-items: center; + background-color: var(--bui-bg-neutral-1); + border-radius: var(--bui-radius-2); + padding: 0 var(--bui-space-1) 0 var(--bui-space-3); + width: fit-content; + min-width: 200px; + max-width: 100%; + overflow: clip; + transition: box-shadow 0.2s ease-in-out; + cursor: text; + + /* bg consumer — auto-increment background based on parent context */ + &[data-on-bg='neutral-1'] { + background-color: var(--bui-bg-neutral-2); + } + + &[data-on-bg='neutral-2'] { + background-color: var(--bui-bg-neutral-3); + } + + &[data-on-bg='neutral-3'] { + background-color: var(--bui-bg-neutral-4); + } + + &[data-focus-within] { + outline: none; + box-shadow: inset 0 0 0 1px var(--bui-ring); + } + + &[data-invalid] { + box-shadow: inset 0 0 0 1px var(--bui-border-danger); + } + + &[data-disabled] { + opacity: 0.5; + cursor: not-allowed; + } + + /* Sizes */ + &[data-size='small'] { + height: 2rem; + padding-inline-start: var(--bui-space-2); + } + + &[data-size='medium'] { + height: 2.5rem; + min-width: 210px; + padding-inline-end: var(--bui-space-1); + + & .bui-DatePickerButton { + width: 2rem; + height: 2rem; + } + } + } + + /* ============================================================ + Date input + ============================================================ */ + + .bui-DatePickerDateInput { + flex: 1; + display: inline-flex; + align-items: center; + width: unset; + min-width: unset; + padding: unset; + border: unset; + box-shadow: none; + background: none; + height: auto; + overflow-x: auto; + overflow-y: clip; + scrollbar-width: none; + } + + /* ============================================================ + Date segments (month, day, year literals and placeholders) + ============================================================ */ + + .bui-DatePickerSegment { + display: inline-block; + padding: var(--bui-space-0_5) var(--bui-space-1); + border-radius: var(--bui-radius-1); + font-size: var(--bui-font-size-3); + font-family: var(--bui-font-regular); + font-weight: var(--bui-font-weight-regular); + color: var(--bui-fg-primary); + caret-color: transparent; + outline: none; + font-variant-numeric: tabular-nums; + + &[data-placeholder] { + color: var(--bui-fg-secondary); + } + + &[data-type='literal'] { + color: var(--bui-fg-secondary); + padding: 0; + } + + &[data-focused] { + background-color: var(--bui-bg-solid); + color: var(--bui-fg-solid); + border-radius: var(--bui-radius-1); + + &[data-placeholder] { + color: var(--bui-fg-solid); + } + } + + &[data-disabled] { + color: var(--bui-fg-disabled); + } + } + + /* ============================================================ + Calendar trigger button + ============================================================ */ + + .bui-DatePickerButton { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + width: 1.5rem; + height: 1.5rem; + padding: 0; + background: none; + border: none; + border-radius: var(--bui-radius-2); + color: var(--bui-fg-secondary); + cursor: pointer; + margin-left: auto; + transition: color 0.15s ease-in-out; + outline: none; + + &[data-hovered] { + color: var(--bui-fg-primary); + } + + &[data-focus-visible] { + box-shadow: inset 0 0 0 1px var(--bui-ring); + } + + &[data-pressed] { + color: var(--bui-fg-primary); + } + } + + /* ============================================================ + Calendar (inside Popover) + ============================================================ */ + + .bui-DatePickerCalendar { + width: fit-content; + outline: none; + } + + .bui-DatePickerCalendarHeader { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: var(--bui-space-3); + } + + .bui-DatePickerCalendarHeading { + font-size: var(--bui-font-size-3); + font-weight: var(--bui-font-weight-bold); + font-family: var(--bui-font-regular); + color: var(--bui-fg-primary); + flex: 1; + text-align: center; + } + + .bui-DatePickerCalendarNavButton { + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + width: var(--bui-space-8); + height: var(--bui-space-8); + background: none; + border: none; + border-radius: var(--bui-radius-2); + color: var(--bui-fg-secondary); + cursor: pointer; + transition: background-color 0.15s ease-in-out, color 0.15s ease-in-out; + + &[data-hovered] { + background-color: var(--bui-bg-neutral-2); + color: var(--bui-fg-primary); + } + + &[data-focus-visible] { + outline: 2px solid var(--bui-ring); + outline-offset: 1px; + } + + &[data-pressed] { + background-color: var(--bui-bg-neutral-2); + } + } + + /* ============================================================ + Calendar grid + ============================================================ */ + + .bui-DatePickerCalendarGrid { + width: 100%; + border-collapse: separate; + border-spacing: 0 2px; + } + + .bui-DatePickerCalendarHeaderCell { + font-size: var(--bui-font-size-2); + font-weight: var(--bui-font-weight-regular); + color: var(--bui-fg-secondary); + text-align: center; + padding-bottom: var(--bui-space-2); + width: 40px; + } + + /* ============================================================ + Calendar cells + ============================================================ */ + + .bui-DatePickerCalendarCell { + position: relative; + width: 40px; + height: 40px; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + outline: none; + cursor: default; + font-size: var(--bui-font-size-3); + font-family: var(--bui-font-regular); + color: var(--bui-fg-primary); + border-radius: var(--bui-radius-full); + + &[data-outside-month] { + color: var(--bui-fg-disabled); + } + + &[data-disabled] { + color: var(--bui-fg-disabled); + cursor: not-allowed; + } + + &[data-unavailable] { + color: var(--bui-fg-disabled); + cursor: not-allowed; + text-decoration: line-through; + } + + &[data-hovered]:not([data-disabled]):not([data-unavailable]):not( + [data-selected] + ) { + background-color: var(--bui-bg-neutral-2); + } + + &[data-focus-visible] { + outline: 2px solid var(--bui-ring); + outline-offset: -2px; + } + + /* Today marker */ + &[data-today]:not([data-selected]) { + font-weight: var(--bui-font-weight-bold); + } + + /* Selected day */ + &[data-selected] { + background-color: var(--bui-bg-solid); + color: var(--bui-fg-solid); + } + } +} diff --git a/packages/ui/src/components/DatePicker/DatePicker.stories.tsx b/packages/ui/src/components/DatePicker/DatePicker.stories.tsx new file mode 100644 index 0000000000..5553c094f6 --- /dev/null +++ b/packages/ui/src/components/DatePicker/DatePicker.stories.tsx @@ -0,0 +1,143 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import preview from '../../../../../.storybook/preview'; +import { DatePicker } from './DatePicker'; +import { + parseDate, + today, + getLocalTimeZone, + isWeekend, +} from '@internationalized/date'; +import { useLocale, Form } from 'react-aria-components'; +import { Button } from '../Button'; + +const meta = preview.meta({ + title: 'Backstage UI/DatePicker', + component: DatePicker, + args: { + style: { width: 280 }, + }, +}); + +export const Default = meta.story({ + args: {}, +}); + +export const WithLabel = meta.story({ + args: { + label: 'Date', + }, +}); + +export const WithDescription = meta.story({ + args: { + label: 'Date', + description: 'Select the date of your event.', + }, +}); + +export const WithDefaultValue = meta.story({ + args: { + label: 'Booking date', + defaultValue: parseDate('2025-02-03'), + }, +}); + +export const Sizes = meta.story({ + args: { + label: 'Date', + }, + render: args => ( +
+ + +
+ ), +}); + +export const Required = meta.story({ + args: { + label: 'Trip date', + isRequired: true, + }, + render: args => ( +
{ + e.preventDefault(); + }} + style={{ + display: 'flex', + flexDirection: 'column', + gap: '1rem', + width: 280, + }} + > + + + + ), +}); + +export const Disabled = meta.story({ + args: { + label: 'Date', + isDisabled: true, + defaultValue: parseDate('2025-03-01'), + }, +}); + +export const Invalid = meta.story({ + args: { + label: 'Date', + isInvalid: true, + errorMessage: 'The selected date is not available.', + defaultValue: parseDate('2025-04-01'), + }, +}); + +export const WithMinMaxValue = meta.story({ + args: { + label: 'Date', + description: 'You can only select dates within the next 30 days.', + minValue: today(getLocalTimeZone()), + maxValue: today(getLocalTimeZone()).add({ days: 30 }), + }, +}); + +/** + * Weekends are marked unavailable and cannot be selected. + */ +export const WithUnavailableDates = meta.story({ + render: args => { + const { locale } = useLocale(); + return ( + isWeekend(date, locale)} + /> + ); + }, +}); diff --git a/packages/ui/src/components/DatePicker/DatePicker.tsx b/packages/ui/src/components/DatePicker/DatePicker.tsx new file mode 100644 index 0000000000..e88c819a99 --- /dev/null +++ b/packages/ui/src/components/DatePicker/DatePicker.tsx @@ -0,0 +1,81 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { forwardRef, useEffect } from 'react'; +import { DatePicker as AriaDatePicker } from 'react-aria-components'; +import { FieldLabel } from '../FieldLabel'; +import { FieldError } from '../FieldError'; +import { Popover } from '../Popover'; +import { DatePickerGroup } from './DatePickerGroup'; +import { DatePickerCalendar } from './DatePickerCalendar'; +import { useDefinition } from '../../hooks/useDefinition'; +import { DatePickerDefinition } from './definition'; +import type { DatePickerProps } from './types'; + +/** + * A date picker that combines a date field and a calendar popover, allowing + * users to enter or select a date with full keyboard and screen reader + * accessibility. + * + * @public + */ +export const DatePicker = forwardRef( + (props, ref) => { + const { ownProps, restProps, dataAttributes } = useDefinition( + DatePickerDefinition, + props, + ); + + const { classes, label, description, secondaryLabel } = ownProps; + + const ariaLabel = restProps['aria-label']; + const ariaLabelledBy = restProps['aria-labelledby']; + + useEffect(() => { + if (!label && !ariaLabel && !ariaLabelledBy) { + console.warn( + 'DatePicker requires either a visible label, aria-label, or aria-labelledby for accessibility', + ); + } + }, [label, ariaLabel, ariaLabelledBy]); + + const secondaryLabelText = + secondaryLabel || (restProps.isRequired ? 'Required' : null); + + return ( + + + + + + + + + ); + }, +); + +DatePicker.displayName = 'DatePicker'; diff --git a/packages/ui/src/components/DatePicker/DatePickerCalendar.tsx b/packages/ui/src/components/DatePicker/DatePickerCalendar.tsx new file mode 100644 index 0000000000..4c03bf197b --- /dev/null +++ b/packages/ui/src/components/DatePicker/DatePickerCalendar.tsx @@ -0,0 +1,66 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Calendar, + CalendarGrid, + CalendarGridHeader, + CalendarHeaderCell, + CalendarGridBody, + CalendarCell, + Heading, + Button, +} from 'react-aria-components'; +import { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react'; +import { useDefinition } from '../../hooks/useDefinition'; +import { DatePickerCalendarDefinition } from './definition'; + +/** + * Calendar popover content for DatePicker — renders the Calendar with + * navigation and a full calendar grid. + * + * @internal + */ +export const DatePickerCalendar = () => { + const { ownProps } = useDefinition(DatePickerCalendarDefinition, {}); + const { classes } = ownProps; + + return ( + +
+ + + +
+ + + {day => ( + + {day} + + )} + + + {date => } + + +
+ ); +}; diff --git a/packages/ui/src/components/DatePicker/DatePickerGroup.tsx b/packages/ui/src/components/DatePicker/DatePickerGroup.tsx new file mode 100644 index 0000000000..5e9d4dc76e --- /dev/null +++ b/packages/ui/src/components/DatePicker/DatePickerGroup.tsx @@ -0,0 +1,51 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Group, DateInput, DateSegment, Button } from 'react-aria-components'; +import { RiCalendarLine } from '@remixicon/react'; +import { useDefinition } from '../../hooks/useDefinition'; +import { DatePickerGroupDefinition } from './definition'; + +/** + * Custom field group for DatePicker — renders a single DateInput field + * and a calendar trigger button. + * + * @internal + */ +export const DatePickerGroup = ({ dataSize }: { dataSize?: string }) => { + const { ownProps, dataAttributes } = useDefinition( + DatePickerGroupDefinition, + {}, + ); + const { classes } = ownProps; + + return ( + + + {segment => ( + + )} + + + + ); +}; diff --git a/packages/ui/src/components/DatePicker/definition.ts b/packages/ui/src/components/DatePicker/definition.ts new file mode 100644 index 0000000000..f082355ccf --- /dev/null +++ b/packages/ui/src/components/DatePicker/definition.ts @@ -0,0 +1,77 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { defineComponent } from '../../hooks/useDefinition'; +import type { DatePickerOwnProps } from './types'; +import styles from './DatePicker.module.css'; + +/** + * Component definition for DatePicker + * @public + */ +export const DatePickerDefinition = defineComponent()({ + styles, + classNames: { + root: 'bui-DatePicker', + }, + propDefs: { + size: { dataAttribute: true, default: 'small' }, + className: {}, + label: {}, + description: {}, + secondaryLabel: {}, + }, +}); + +/** + * Component definition for DatePickerGroup + * @public + */ +export const DatePickerGroupDefinition = defineComponent< + Record +>()({ + styles, + classNames: { + root: 'bui-DatePickerGroup', + dateInput: 'bui-DatePickerDateInput', + segment: 'bui-DatePickerSegment', + button: 'bui-DatePickerButton', + }, + bg: 'consumer', + propDefs: {}, +}); + +/** + * Component definition for DatePickerCalendar + * @public + */ +export const DatePickerCalendarDefinition = defineComponent< + Record +>()({ + styles, + classNames: { + root: 'bui-DatePickerCalendar', + header: 'bui-DatePickerCalendarHeader', + heading: 'bui-DatePickerCalendarHeading', + navButton: 'bui-DatePickerCalendarNavButton', + grid: 'bui-DatePickerCalendarGrid', + gridHeader: 'bui-DatePickerCalendarGridHeader', + headerCell: 'bui-DatePickerCalendarHeaderCell', + gridBody: 'bui-DatePickerCalendarGridBody', + cell: 'bui-DatePickerCalendarCell', + }, + propDefs: {}, +}); diff --git a/packages/ui/src/components/DatePicker/index.ts b/packages/ui/src/components/DatePicker/index.ts new file mode 100644 index 0000000000..60d7a8d78a --- /dev/null +++ b/packages/ui/src/components/DatePicker/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './DatePicker'; +export * from './types'; +export { + DatePickerDefinition, + DatePickerGroupDefinition, + DatePickerCalendarDefinition, +} from './definition'; diff --git a/packages/ui/src/components/DatePicker/types.ts b/packages/ui/src/components/DatePicker/types.ts new file mode 100644 index 0000000000..fb98ce407b --- /dev/null +++ b/packages/ui/src/components/DatePicker/types.ts @@ -0,0 +1,40 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { DatePickerProps as AriaDatePickerProps } from 'react-aria-components'; +import type { DateValue } from '@internationalized/date'; +import type { Breakpoint } from '../../types'; +import type { FieldLabelProps } from '../FieldLabel/types'; + +/** @public */ +export type DatePickerOwnProps = { + /** + * The size of the date picker + * @defaultValue 'small' + */ + size?: 'small' | 'medium' | Partial>; + + className?: string; + + label?: FieldLabelProps['label']; + description?: FieldLabelProps['description']; + secondaryLabel?: FieldLabelProps['secondaryLabel']; +}; + +/** @public */ +export interface DatePickerProps + extends Omit, 'className' | 'children'>, + DatePickerOwnProps {} diff --git a/packages/ui/src/definitions.ts b/packages/ui/src/definitions.ts index ed7628f293..cb71f5062e 100644 --- a/packages/ui/src/definitions.ts +++ b/packages/ui/src/definitions.ts @@ -37,6 +37,11 @@ export { CheckboxDefinition } from './components/Checkbox/definition'; export { CheckboxGroupDefinition } from './components/CheckboxGroup/definition'; export { ComboboxDefinition } from './components/Combobox/definition'; export { ContainerDefinition } from './components/Container/definition'; +export { + DatePickerDefinition, + DatePickerGroupDefinition, + DatePickerCalendarDefinition, +} from './components/DatePicker/definition'; export { DateRangePickerDefinition } from './components/DateRangePicker/definition'; export { DialogDefinition } from './components/Dialog/definition'; export { FieldErrorDefinition } from './components/FieldError/definition'; diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 106adb4cd0..bc29073767 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -34,6 +34,7 @@ export * from './components/Avatar'; export * from './components/Badge'; export * from './components/Button'; export * from './components/Card'; +export * from './components/DatePicker'; export * from './components/DateRangePicker'; export * from './components/Dialog'; export * from './components/FieldLabel';