Merge pull request #33909 from backstage/cursor/add-date-range-picker
feat(ui): add DateRangePicker component
This commit is contained in:
@@ -47,6 +47,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/version-bridge": "workspace:^",
|
||||
"@internationalized/date": "^3.12.0",
|
||||
"@remixicon/react": "^4.6.0",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"clsx": "^2.1.1",
|
||||
|
||||
@@ -14,6 +14,8 @@ import type { ComponentProps } from 'react';
|
||||
import type { ComponentPropsWithoutRef } from 'react';
|
||||
import type { ComponentPropsWithRef } from 'react';
|
||||
import type { CSSProperties } from 'react';
|
||||
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';
|
||||
import type { DisclosureGroupProps } from 'react-aria-components';
|
||||
import type { DisclosurePanelProps } from 'react-aria-components';
|
||||
@@ -1103,6 +1105,45 @@ export interface CursorResponse<T> {
|
||||
totalCount?: number;
|
||||
}
|
||||
|
||||
// @public
|
||||
export const DateRangePicker: ForwardRefExoticComponent<
|
||||
DateRangePickerProps & RefAttributes<HTMLDivElement>
|
||||
>;
|
||||
|
||||
// @public
|
||||
export const DateRangePickerDefinition: {
|
||||
readonly styles: {
|
||||
readonly [key: string]: string;
|
||||
};
|
||||
readonly classNames: {
|
||||
readonly root: 'bui-DateRangePicker';
|
||||
};
|
||||
readonly propDefs: {
|
||||
readonly size: {
|
||||
readonly dataAttribute: true;
|
||||
readonly default: 'small';
|
||||
};
|
||||
readonly className: {};
|
||||
readonly label: {};
|
||||
readonly description: {};
|
||||
readonly secondaryLabel: {};
|
||||
};
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type DateRangePickerOwnProps = {
|
||||
size?: 'small' | 'medium' | Partial<Record<Breakpoint, 'small' | 'medium'>>;
|
||||
className?: string;
|
||||
label?: FieldLabelProps['label'];
|
||||
description?: FieldLabelProps['description'];
|
||||
secondaryLabel?: FieldLabelProps['secondaryLabel'];
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export interface DateRangePickerProps
|
||||
extends Omit<DateRangePickerProps_2<DateValue>, 'className' | 'children'>,
|
||||
DateRangePickerOwnProps {}
|
||||
|
||||
// @public
|
||||
export const Dialog: ForwardRefExoticComponent<
|
||||
DialogProps & RefAttributes<HTMLDivElement>
|
||||
|
||||
@@ -0,0 +1,406 @@
|
||||
/*
|
||||
* 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-DateRangePicker {
|
||||
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-DateRangePickerGroup {
|
||||
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: 280px;
|
||||
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: 290px;
|
||||
padding-inline-end: var(--bui-space-1);
|
||||
|
||||
& .bui-DateRangePickerButton {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Date fields wrapper — scrollable row of inputs + separator
|
||||
============================================================ */
|
||||
|
||||
.bui-DateRangePickerDateFields {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
overflow-x: auto;
|
||||
overflow-y: clip;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Date inputs
|
||||
============================================================ */
|
||||
|
||||
.bui-DateRangePickerDateInput {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
width: unset;
|
||||
min-width: unset;
|
||||
padding: unset;
|
||||
border: unset;
|
||||
box-shadow: none;
|
||||
background: none;
|
||||
height: auto;
|
||||
|
||||
&[slot='end'] {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Date segments (month, day, year literals and placeholders)
|
||||
============================================================ */
|
||||
|
||||
.bui-DateRangePickerSegment {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Separator between start and end
|
||||
============================================================ */
|
||||
|
||||
.bui-DateRangePickerSeparator {
|
||||
color: var(--bui-fg-secondary);
|
||||
font-size: var(--bui-font-size-3);
|
||||
flex-shrink: 0;
|
||||
user-select: none;
|
||||
padding: 0 var(--bui-space-1);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Calendar trigger button
|
||||
============================================================ */
|
||||
|
||||
.bui-DateRangePickerButton {
|
||||
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-DateRangePickerCalendar {
|
||||
width: fit-content;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.bui-DateRangePickerCalendarHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--bui-space-3);
|
||||
}
|
||||
|
||||
.bui-DateRangePickerCalendarHeading {
|
||||
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-DateRangePickerCalendarNavButton {
|
||||
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-DateRangePickerCalendarGrid {
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0 2px;
|
||||
}
|
||||
|
||||
.bui-DateRangePickerCalendarHeaderCell {
|
||||
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
|
||||
|
||||
Technique: isolation: isolate creates a stacking context so
|
||||
that ::before (range fill, z-index: -2) and ::after (circle,
|
||||
z-index: -1) paint behind the cell's text content while still
|
||||
rendering on top of the page background.
|
||||
============================================================ */
|
||||
|
||||
.bui-DateRangePickerCalendarCell {
|
||||
isolation: isolate;
|
||||
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);
|
||||
|
||||
&[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);
|
||||
border-radius: var(--bui-radius-full);
|
||||
}
|
||||
|
||||
&[data-focus-visible] {
|
||||
outline: 2px solid var(--bui-ring);
|
||||
outline-offset: -2px;
|
||||
border-radius: var(--bui-radius-full);
|
||||
}
|
||||
|
||||
/* Today marker */
|
||||
&[data-today]:not([data-selected]):not([data-selection-start]):not(
|
||||
[data-selection-end]
|
||||
) {
|
||||
font-weight: var(--bui-font-weight-bold);
|
||||
}
|
||||
|
||||
/* ── Range fill via ::before ─────────────────────────────── */
|
||||
|
||||
&[data-selected]::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: color-mix(in srgb, var(--bui-bg-solid) 15%, transparent);
|
||||
z-index: -2;
|
||||
}
|
||||
|
||||
/* Start cell: fill only the right half to connect forward */
|
||||
&[data-selection-start]::before {
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
/* End cell: fill only the left half to connect backward */
|
||||
&[data-selection-end]::before {
|
||||
right: 50%;
|
||||
}
|
||||
|
||||
/* Same-day selection: no range fill needed */
|
||||
&[data-selection-start][data-selection-end]::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ── Solid circle for start / end via ::after ────────────── */
|
||||
|
||||
&[data-selection-start]::after,
|
||||
&[data-selection-end]::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--bui-bg-solid);
|
||||
border-radius: var(--bui-radius-full);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* Text color on top of the solid circle */
|
||||
&[data-selection-start],
|
||||
&[data-selection-end] {
|
||||
color: var(--bui-fg-solid);
|
||||
}
|
||||
}
|
||||
|
||||
/* Round the left edge of ::before when the cell is the first in its row
|
||||
(our class is on an inner element inside <td>, hence td:first-child > *) */
|
||||
.bui-DateRangePickerCalendarCell[data-selected]:not(
|
||||
[data-selection-start]
|
||||
):is(td:first-child > *, [aria-disabled] + td > *)::before {
|
||||
border-start-start-radius: var(--bui-radius-full);
|
||||
border-end-start-radius: var(--bui-radius-full);
|
||||
}
|
||||
|
||||
/* Round the right edge of ::before when the cell is the last in its row */
|
||||
.bui-DateRangePickerCalendarCell[data-selected]:not([data-selection-end]):is(
|
||||
td:last-child > *
|
||||
)::before {
|
||||
border-start-end-radius: var(--bui-radius-full);
|
||||
border-end-end-radius: var(--bui-radius-full);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* 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 { DateRangePicker } from './DateRangePicker';
|
||||
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/DateRangePicker',
|
||||
component: DateRangePicker,
|
||||
args: {
|
||||
style: { width: 360 },
|
||||
},
|
||||
});
|
||||
|
||||
export const Default = meta.story({
|
||||
args: {},
|
||||
});
|
||||
|
||||
export const WithLabel = meta.story({
|
||||
args: {
|
||||
label: 'Date range',
|
||||
},
|
||||
});
|
||||
|
||||
export const WithDescription = meta.story({
|
||||
args: {
|
||||
label: 'Date range',
|
||||
description: 'Select a start and end date for your event.',
|
||||
},
|
||||
});
|
||||
|
||||
export const WithDefaultValue = meta.story({
|
||||
args: {
|
||||
label: 'Booking period',
|
||||
defaultValue: {
|
||||
start: parseDate('2025-02-03'),
|
||||
end: parseDate('2025-02-14'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const Sizes = meta.story({
|
||||
args: {
|
||||
label: 'Date range',
|
||||
},
|
||||
render: args => (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '1rem',
|
||||
width: 360,
|
||||
}}
|
||||
>
|
||||
<DateRangePicker {...args} size="small" label="Small" />
|
||||
<DateRangePicker {...args} size="medium" label="Medium" />
|
||||
</div>
|
||||
),
|
||||
});
|
||||
|
||||
export const Required = meta.story({
|
||||
args: {
|
||||
label: 'Trip dates',
|
||||
isRequired: true,
|
||||
},
|
||||
render: args => (
|
||||
<Form
|
||||
onSubmit={e => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '1rem',
|
||||
width: 360,
|
||||
}}
|
||||
>
|
||||
<DateRangePicker {...args} />
|
||||
<Button type="submit">Submit</Button>
|
||||
</Form>
|
||||
),
|
||||
});
|
||||
|
||||
export const Disabled = meta.story({
|
||||
args: {
|
||||
label: 'Date range',
|
||||
isDisabled: true,
|
||||
defaultValue: {
|
||||
start: parseDate('2025-03-01'),
|
||||
end: parseDate('2025-03-15'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const Invalid = meta.story({
|
||||
args: {
|
||||
label: 'Date range',
|
||||
isInvalid: true,
|
||||
errorMessage: 'The selected range is not available.',
|
||||
defaultValue: {
|
||||
start: parseDate('2025-04-01'),
|
||||
end: parseDate('2025-04-10'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const WithMinMaxValue = meta.story({
|
||||
args: {
|
||||
label: 'Date range',
|
||||
description: 'You can only select dates within the next 30 days.',
|
||||
minValue: today(getLocalTimeZone()),
|
||||
maxValue: today(getLocalTimeZone()).add({ days: 30 }),
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Weekends are marked unavailable. Because `allowsNonContiguousRanges` is not
|
||||
* set (defaults to false), the picker prevents the user from selecting any
|
||||
* range that spans across an unavailable date — the selection snaps to avoid
|
||||
* crossing a weekend.
|
||||
*/
|
||||
export const WithUnavailableDates = meta.story({
|
||||
render: args => {
|
||||
const { locale } = useLocale();
|
||||
return (
|
||||
<DateRangePicker
|
||||
{...args}
|
||||
label="Working days only"
|
||||
description="Weekends are unavailable. You cannot select a range that spans across them."
|
||||
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 { DateRangePicker as AriaDateRangePicker } from 'react-aria-components';
|
||||
import { FieldLabel } from '../FieldLabel';
|
||||
import { FieldError } from '../FieldError';
|
||||
import { Popover } from '../Popover';
|
||||
import { DateRangePickerGroup } from './DateRangePickerGroup';
|
||||
import { DateRangePickerCalendar } from './DateRangePickerCalendar';
|
||||
import { useDefinition } from '../../hooks/useDefinition';
|
||||
import { DateRangePickerDefinition } from './definition';
|
||||
import type { DateRangePickerProps } from './types';
|
||||
|
||||
/**
|
||||
* A date range picker that combines two date fields and a calendar popover,
|
||||
* allowing users to enter or select a date range with full keyboard and
|
||||
* screen reader accessibility.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const DateRangePicker = forwardRef<HTMLDivElement, DateRangePickerProps>(
|
||||
(props, ref) => {
|
||||
const { ownProps, restProps, dataAttributes } = useDefinition(
|
||||
DateRangePickerDefinition,
|
||||
props,
|
||||
);
|
||||
|
||||
const { classes, label, description, secondaryLabel } = ownProps;
|
||||
|
||||
const ariaLabel = restProps['aria-label'];
|
||||
const ariaLabelledBy = restProps['aria-labelledby'];
|
||||
|
||||
useEffect(() => {
|
||||
if (!label && !ariaLabel && !ariaLabelledBy) {
|
||||
console.warn(
|
||||
'DateRangePicker requires either a visible label, aria-label, or aria-labelledby for accessibility',
|
||||
);
|
||||
}
|
||||
}, [label, ariaLabel, ariaLabelledBy]);
|
||||
|
||||
const secondaryLabelText =
|
||||
secondaryLabel || (restProps.isRequired ? 'Required' : null);
|
||||
|
||||
return (
|
||||
<AriaDateRangePicker
|
||||
className={classes.root}
|
||||
{...dataAttributes}
|
||||
{...restProps}
|
||||
ref={ref}
|
||||
>
|
||||
<FieldLabel
|
||||
label={label}
|
||||
secondaryLabel={secondaryLabelText}
|
||||
description={description}
|
||||
descriptionSlot="description"
|
||||
/>
|
||||
<DateRangePickerGroup dataSize={dataAttributes['data-size']} />
|
||||
<FieldError />
|
||||
<Popover hideArrow>
|
||||
<DateRangePickerCalendar />
|
||||
</Popover>
|
||||
</AriaDateRangePicker>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
DateRangePicker.displayName = 'DateRangePicker';
|
||||
@@ -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 {
|
||||
RangeCalendar,
|
||||
CalendarGrid,
|
||||
CalendarGridHeader,
|
||||
CalendarHeaderCell,
|
||||
CalendarGridBody,
|
||||
CalendarCell,
|
||||
Heading,
|
||||
Button,
|
||||
} from 'react-aria-components';
|
||||
import { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react';
|
||||
import { useDefinition } from '../../hooks/useDefinition';
|
||||
import { DateRangePickerCalendarDefinition } from './definition';
|
||||
|
||||
/**
|
||||
* Calendar popover content for DateRangePicker — renders the RangeCalendar
|
||||
* with navigation and a full calendar grid.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export const DateRangePickerCalendar = () => {
|
||||
const { ownProps } = useDefinition(DateRangePickerCalendarDefinition, {});
|
||||
const { classes } = ownProps;
|
||||
|
||||
return (
|
||||
<RangeCalendar 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>
|
||||
</RangeCalendar>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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 { DateRangePickerGroupDefinition } from './definition';
|
||||
|
||||
/**
|
||||
* Custom field group for DateRangePicker — renders two DateInput fields,
|
||||
* a separator, and a calendar trigger button.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export const DateRangePickerGroup = ({ dataSize }: { dataSize?: string }) => {
|
||||
const { ownProps, dataAttributes } = useDefinition(
|
||||
DateRangePickerGroupDefinition,
|
||||
{},
|
||||
);
|
||||
const { classes } = ownProps;
|
||||
|
||||
return (
|
||||
<Group
|
||||
className={classes.root}
|
||||
{...dataAttributes}
|
||||
{...(dataSize ? { 'data-size': dataSize } : {})}
|
||||
>
|
||||
<div className={classes.dateFields}>
|
||||
<DateInput slot="start" className={classes.dateInput}>
|
||||
{segment => (
|
||||
<DateSegment segment={segment} className={classes.segment} />
|
||||
)}
|
||||
</DateInput>
|
||||
<span aria-hidden="true" className={classes.separator}>
|
||||
–
|
||||
</span>
|
||||
<DateInput slot="end" className={classes.dateInput}>
|
||||
{segment => (
|
||||
<DateSegment segment={segment} className={classes.segment} />
|
||||
)}
|
||||
</DateInput>
|
||||
</div>
|
||||
<Button className={classes.button} aria-label="Open calendar">
|
||||
<RiCalendarLine size={16} aria-hidden="true" />
|
||||
</Button>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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 { DateRangePickerOwnProps } from './types';
|
||||
import styles from './DateRangePicker.module.css';
|
||||
|
||||
/**
|
||||
* Component definition for DateRangePicker
|
||||
* @public
|
||||
*/
|
||||
export const DateRangePickerDefinition =
|
||||
defineComponent<DateRangePickerOwnProps>()({
|
||||
styles,
|
||||
classNames: {
|
||||
root: 'bui-DateRangePicker',
|
||||
},
|
||||
propDefs: {
|
||||
size: { dataAttribute: true, default: 'small' },
|
||||
className: {},
|
||||
label: {},
|
||||
description: {},
|
||||
secondaryLabel: {},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Component definition for DateRangePickerGroup
|
||||
* @internal
|
||||
*/
|
||||
export const DateRangePickerGroupDefinition = defineComponent<
|
||||
Record<string, never>
|
||||
>()({
|
||||
styles,
|
||||
classNames: {
|
||||
root: 'bui-DateRangePickerGroup',
|
||||
dateFields: 'bui-DateRangePickerDateFields',
|
||||
dateInput: 'bui-DateRangePickerDateInput',
|
||||
segment: 'bui-DateRangePickerSegment',
|
||||
separator: 'bui-DateRangePickerSeparator',
|
||||
button: 'bui-DateRangePickerButton',
|
||||
},
|
||||
bg: 'consumer',
|
||||
propDefs: {},
|
||||
});
|
||||
|
||||
/**
|
||||
* Component definition for DateRangePickerCalendar
|
||||
* @internal
|
||||
*/
|
||||
export const DateRangePickerCalendarDefinition = defineComponent<
|
||||
Record<string, never>
|
||||
>()({
|
||||
styles,
|
||||
classNames: {
|
||||
root: 'bui-DateRangePickerCalendar',
|
||||
header: 'bui-DateRangePickerCalendarHeader',
|
||||
heading: 'bui-DateRangePickerCalendarHeading',
|
||||
navButton: 'bui-DateRangePickerCalendarNavButton',
|
||||
grid: 'bui-DateRangePickerCalendarGrid',
|
||||
gridHeader: 'bui-DateRangePickerCalendarGridHeader',
|
||||
headerCell: 'bui-DateRangePickerCalendarHeaderCell',
|
||||
gridBody: 'bui-DateRangePickerCalendarGridBody',
|
||||
cell: 'bui-DateRangePickerCalendarCell',
|
||||
},
|
||||
propDefs: {},
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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 './DateRangePicker';
|
||||
export * from './types';
|
||||
export { DateRangePickerDefinition } 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 { DateRangePickerProps as AriaDateRangePickerProps } from 'react-aria-components';
|
||||
import type { DateValue } from '@internationalized/date';
|
||||
import type { Breakpoint } from '../../types';
|
||||
import type { FieldLabelProps } from '../FieldLabel/types';
|
||||
|
||||
/** @public */
|
||||
export type DateRangePickerOwnProps = {
|
||||
/**
|
||||
* The size of the date range 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 DateRangePickerProps
|
||||
extends Omit<AriaDateRangePickerProps<DateValue>, 'className' | 'children'>,
|
||||
DateRangePickerOwnProps {}
|
||||
@@ -36,6 +36,7 @@ export { CardDefinition } from './components/Card/definition';
|
||||
export { CheckboxDefinition } from './components/Checkbox/definition';
|
||||
export { CheckboxGroupDefinition } from './components/CheckboxGroup/definition';
|
||||
export { ContainerDefinition } from './components/Container/definition';
|
||||
export { DateRangePickerDefinition } from './components/DateRangePicker/definition';
|
||||
export { DialogDefinition } from './components/Dialog/definition';
|
||||
export { FieldErrorDefinition } from './components/FieldError/definition';
|
||||
export { FieldLabelDefinition } from './components/FieldLabel/definition';
|
||||
|
||||
@@ -34,6 +34,7 @@ export * from './components/Avatar';
|
||||
export * from './components/Badge';
|
||||
export * from './components/Button';
|
||||
export * from './components/Card';
|
||||
export * from './components/DateRangePicker';
|
||||
export * from './components/Dialog';
|
||||
export * from './components/FieldLabel';
|
||||
export * from './components/PluginHeader';
|
||||
|
||||
Reference in New Issue
Block a user