feat(ui): add DatePicker component (#34184)

* feat(ui): add DatePicker component

Add a single-date picker built on React Aria's DatePicker, mirroring the
existing DateRangePicker implementation. Includes the date field with
segmented input, calendar popover, BUI design tokens, bg consumer
pattern, and full keyboard/screen reader accessibility.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Erik Hughes <erikh@spotify.com>

* fix(ui): address DatePicker PR feedback

- Remove unused dataAttributes spread from DatePickerGroup
- Mark DatePickerGroupDefinition and DatePickerCalendarDefinition as
  public so CSS class name changes appear in API reports
- Add Affected components line to changeset

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Erik Hughes <erikh@spotify.com>

* fix(ui): restore dataAttributes spread in DatePickerGroup

The bg: 'consumer' config on DatePickerGroupDefinition emits
data-on-bg attributes via useDefinition. Without spreading
dataAttributes onto <Group>, the CSS [data-on-bg] selectors
never match and background auto-increment doesn't work.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Erik Hughes <erikh@spotify.com>

---------

Signed-off-by: Erik Hughes <erikh@spotify.com>
Co-authored-by: Erik Hughes <erikh@spotify.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik Hughes
2026-05-12 10:46:46 +02:00
committed by GitHub
parent dbeb7aab3e
commit d726bcd842
17 changed files with 1126 additions and 0 deletions
+7
View File
@@ -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
@@ -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 <DatePicker label="Date" style={{ maxWidth: '220px' }} />;
};
export const Sizes = () => {
return (
<Flex
direction="column"
gap="4"
style={{ width: '100%', maxWidth: '280px' }}
>
<DatePicker label="Small" size="small" />
<DatePicker label="Medium" size="medium" />
</Flex>
);
};
export const WithDefaultValue = () => {
return (
<DatePicker
label="Booking date"
defaultValue={parseDate('2025-02-03')}
style={{ maxWidth: '280px' }}
/>
);
};
export const Disabled = () => {
return <DatePicker label="Date" isDisabled style={{ maxWidth: '280px' }} />;
};
@@ -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',
};
<PageTitle
title="DatePicker"
description="A date picker that combines a date field and a calendar popover for selecting a date."
/>
<Snippet
align="center"
py={4}
preview={<WithLabel />}
code={withLabelSnippet}
/>
## Usage
<CodeBlock code={datePickerUsageSnippet} />
## API reference
<PropsTable data={datePickerPropDefs} />
<ReactAriaLink component="DatePicker" href={reactAriaUrls.datePicker} />
## Examples
### Sizes
<Snippet
align="center"
py={4}
open
preview={<Sizes />}
code={sizesSnippet}
layout="side-by-side"
/>
### With default value
<Snippet
align="center"
py={4}
open
preview={<WithDefaultValue />}
code={withDefaultValueSnippet}
layout="side-by-side"
/>
### Disabled
<Snippet
align="center"
py={4}
open
preview={<Disabled />}
code={disabledSnippet}
layout="side-by-side"
/>
<Theming definition={DatePickerDefinition} />
<ChangelogComponent component="date-picker" />
@@ -0,0 +1,94 @@
import {
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
import { Chip } from '@/components/Chip';
export const datePickerPropDefs: Record<string, PropDef> = {
size: {
type: 'enum',
values: ['small', 'medium'],
default: 'small',
responsive: true,
description: (
<>
Visual size of the picker. Use <Chip>small</Chip> for dense layouts,{' '}
<Chip>medium</Chip> 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 <Chip>Required</Chip>.
</>
),
},
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,
};
@@ -0,0 +1,25 @@
export const datePickerUsageSnippet = `import { DatePicker } from '@backstage/ui';
<DatePicker label="Date" />`;
export const withLabelSnippet = `<DatePicker
label="Date"
description="Select the date of your event."
/>`;
export const sizesSnippet = `<Flex direction="column" gap="4">
<DatePicker label="Small" size="small" />
<DatePicker label="Medium" size="medium" />
</Flex>`;
export const withDefaultValueSnippet = `import { parseDate } from '@internationalized/date';
<DatePicker
label="Booking date"
defaultValue={parseDate('2025-02-03')}
/>`;
export const disabledSnippet = `<DatePicker
label="Date"
isDisabled
/>`;
+4
View File
@@ -58,6 +58,10 @@ export const components: Page[] = [
title: 'Container',
slug: 'container',
},
{
title: 'DatePicker',
slug: 'date-picker',
},
{
title: 'DateRangePicker',
slug: 'date-range-picker',
+74
View File
@@ -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<T> {
totalCount?: number;
}
// @public
export const DatePicker: ForwardRefExoticComponent<
DatePickerProps & RefAttributes<HTMLDivElement>
>;
// @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<Record<Breakpoint, 'small' | 'medium'>>;
className?: string;
label?: FieldLabelProps['label'];
description?: FieldLabelProps['description'];
secondaryLabel?: FieldLabelProps['secondaryLabel'];
};
// @public (undocumented)
export interface DatePickerProps
extends Omit<DatePickerProps_2<DateValue>, 'className' | 'children'>,
DatePickerOwnProps {}
// @public
export const DateRangePicker: ForwardRefExoticComponent<
DateRangePickerProps & RefAttributes<HTMLDivElement>
@@ -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);
}
}
}
@@ -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 => (
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '1rem',
width: 280,
}}
>
<DatePicker {...args} size="small" label="Small" />
<DatePicker {...args} size="medium" label="Medium" />
</div>
),
});
export const Required = meta.story({
args: {
label: 'Trip date',
isRequired: true,
},
render: args => (
<Form
onSubmit={e => {
e.preventDefault();
}}
style={{
display: 'flex',
flexDirection: 'column',
gap: '1rem',
width: 280,
}}
>
<DatePicker {...args} />
<Button type="submit">Submit</Button>
</Form>
),
});
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 (
<DatePicker
{...args}
label="Working days only"
description="Weekends are unavailable."
isDateUnavailable={date => isWeekend(date, locale)}
/>
);
},
});
@@ -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<HTMLDivElement, DatePickerProps>(
(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 (
<AriaDatePicker
className={classes.root}
{...dataAttributes}
{...restProps}
ref={ref}
>
<FieldLabel
label={label}
secondaryLabel={secondaryLabelText}
description={description}
descriptionSlot="description"
/>
<DatePickerGroup dataSize={dataAttributes['data-size']} />
<FieldError />
<Popover hideArrow>
<DatePickerCalendar />
</Popover>
</AriaDatePicker>
);
},
);
DatePicker.displayName = 'DatePicker';
@@ -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 (
<Calendar className={classes.root}>
<header className={classes.header}>
<Button slot="previous" className={classes.navButton}>
<RiArrowLeftSLine size={16} aria-hidden="true" />
</Button>
<Heading className={classes.heading} />
<Button slot="next" className={classes.navButton}>
<RiArrowRightSLine size={16} aria-hidden="true" />
</Button>
</header>
<CalendarGrid className={classes.grid}>
<CalendarGridHeader className={classes.gridHeader}>
{day => (
<CalendarHeaderCell className={classes.headerCell}>
{day}
</CalendarHeaderCell>
)}
</CalendarGridHeader>
<CalendarGridBody className={classes.gridBody}>
{date => <CalendarCell className={classes.cell} date={date} />}
</CalendarGridBody>
</CalendarGrid>
</Calendar>
);
};
@@ -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 (
<Group
className={classes.root}
{...dataAttributes}
{...(dataSize ? { 'data-size': dataSize } : {})}
>
<DateInput className={classes.dateInput}>
{segment => (
<DateSegment segment={segment} className={classes.segment} />
)}
</DateInput>
<Button className={classes.button} aria-label="Open calendar">
<RiCalendarLine size={16} aria-hidden="true" />
</Button>
</Group>
);
};
@@ -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<DatePickerOwnProps>()({
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<string, never>
>()({
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<string, never>
>()({
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: {},
});
@@ -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';
@@ -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<Record<Breakpoint, 'small' | 'medium'>>;
className?: string;
label?: FieldLabelProps['label'];
description?: FieldLabelProps['description'];
secondaryLabel?: FieldLabelProps['secondaryLabel'];
};
/** @public */
export interface DatePickerProps
extends Omit<AriaDatePickerProps<DateValue>, 'className' | 'children'>,
DatePickerOwnProps {}
+5
View File
@@ -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';
+1
View File
@@ -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';