From c143634ea0795b3748ef26f2580fbb0eb6f41070 Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Thu, 19 Jun 2025 15:51:18 +1000 Subject: [PATCH 01/33] feat: S2 DateField/DatePicker/Calendar --- packages/@react-spectrum/s2/src/Calendar.tsx | 7 +- packages/@react-spectrum/s2/src/Content.tsx | 3 +- packages/@react-spectrum/s2/src/DateField.tsx | 138 ++++++++-- .../@react-spectrum/s2/src/DatePicker.tsx | 246 +++++++++++++++--- packages/@react-spectrum/s2/src/Field.tsx | 14 +- .../@react-spectrum/s2/src/NumberField.tsx | 1 + packages/@react-spectrum/s2/src/index.ts | 5 + .../s2/stories/DateField.stories.tsx | 85 ++++++ .../s2/stories/DatePicker.stories.tsx | 85 ++++++ .../__snapshots__/calendar.test.ts.snap | 2 +- .../__snapshots__/datefield.test.ts.snap | 2 +- .../__snapshots__/datepicker.test.ts.snap | 2 +- 12 files changed, 522 insertions(+), 68 deletions(-) create mode 100644 packages/@react-spectrum/s2/stories/DateField.stories.tsx create mode 100644 packages/@react-spectrum/s2/stories/DatePicker.stories.tsx diff --git a/packages/@react-spectrum/s2/src/Calendar.tsx b/packages/@react-spectrum/s2/src/Calendar.tsx index 7d6f5e07794..a3649883e75 100644 --- a/packages/@react-spectrum/s2/src/Calendar.tsx +++ b/packages/@react-spectrum/s2/src/Calendar.tsx @@ -16,11 +16,12 @@ import { Button, CalendarCell, CalendarGrid, + ContextValue, DateValue, Heading, Text } from 'react-aria-components'; -import {ReactNode} from 'react'; +import {Context, createContext, ReactNode} from 'react'; export interface CalendarProps @@ -28,6 +29,10 @@ export interface CalendarProps errorMessage?: string } +export const CalendarContext: + Context>, HTMLDivElement>> = + createContext>, HTMLDivElement>>(null); + export function Calendar( {errorMessage, ...props}: CalendarProps ): ReactNode { diff --git a/packages/@react-spectrum/s2/src/Content.tsx b/packages/@react-spectrum/s2/src/Content.tsx index 7c64e6fe7d1..bc7395724de 100644 --- a/packages/@react-spectrum/s2/src/Content.tsx +++ b/packages/@react-spectrum/s2/src/Content.tsx @@ -27,7 +27,8 @@ interface ContentProps extends UnsafeStyles, SlotProps { id?: string } -interface HeadingProps extends ContentProps { +interface HeadingProps extends Omit { + children?: ReactNode, level?: number } diff --git a/packages/@react-spectrum/s2/src/DateField.tsx b/packages/@react-spectrum/s2/src/DateField.tsx index 48609aca2fa..7f42647629c 100644 --- a/packages/@react-spectrum/s2/src/DateField.tsx +++ b/packages/@react-spectrum/s2/src/DateField.tsx @@ -13,33 +13,133 @@ import { DateField as AriaDateField, DateFieldProps as AriaDateFieldProps, + ContextValue, DateInput, DateSegment, DateValue, - FieldError, - Label, - Text + FormContext } from 'react-aria-components'; -import {HelpTextProps} from '@react-types/shared'; -import {ReactNode} from 'react'; +import {Context, createContext, forwardRef, ReactElement, Ref, RefAttributes, useContext} from 'react'; +import {field, fieldInput, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; +import {FieldErrorIcon, FieldGroup, FieldLabel, HelpText} from './Field'; +import {forwardRefType, HelpTextProps, SpectrumLabelableProps} from '@react-types/shared'; +import {style} from '../style' with {type: 'macro'}; +import {useSpectrumContextProps} from './useSpectrumContextProps'; -export interface DateFieldProps - extends AriaDateFieldProps, HelpTextProps { - label?: ReactNode +export interface DateFieldProps extends + Omit, 'children' | 'className' | 'style'>, + StyleProps, + SpectrumLabelableProps, + HelpTextProps { + /** + * The size of the DateField. + * + * @default 'M' + */ + size?: 'S' | 'M' | 'L' | 'XL' } -export function DateField( - {label, description, errorMessage, ...props}: DateFieldProps -): ReactNode { +export const DateFieldContext: + Context>, HTMLDivElement>> = + createContext>, HTMLDivElement>>(null); + +const segmentContainer = style({ + flexGrow: 1 +}); + +const dateInput = style({ + outlineStyle: 'none', + caretColor: 'transparent', + backgroundColor: { + default: 'transparent', + isFocused: 'blue-900' + }, + color: { + isFocused: 'white' + }, + borderRadius: '[2px]', + paddingX: 2 +}); + +const iconStyles = style({ + display: 'flex', + alignItems: 'center', + justifyContent: 'end' +}); + +export const DateField: + (props: DateFieldProps & RefAttributes) => ReactElement | null = +/*#__PURE__*/ (forwardRef as forwardRefType)(function DateField( + props: DateFieldProps, ref: Ref +): ReactElement { + [props, ref] = useSpectrumContextProps(props, ref, DateFieldContext); + let { + label, + contextualHelp, + description: descriptionMessage, + errorMessage, + isRequired, + size = 'M', + labelPosition = 'top', + necessityIndicator, + labelAlign = 'start', + UNSAFE_style, + UNSAFE_className, + styles, + ...dateFieldProps + } = props; + let formContext = useContext(FormContext); + return ( - - - - {(segment) => } - - {description && {description}} - {errorMessage} + + {({isDisabled, isInvalid}) => { + return ( + <> + + {label} + + + + + {(segment) => } + + {isInvalid &&
} +
+ + {errorMessage} + + + ); + }}
); -} +}); diff --git a/packages/@react-spectrum/s2/src/DatePicker.tsx b/packages/@react-spectrum/s2/src/DatePicker.tsx index 9332aecb4d7..5217dd0e3e4 100644 --- a/packages/@react-spectrum/s2/src/DatePicker.tsx +++ b/packages/@react-spectrum/s2/src/DatePicker.tsx @@ -14,56 +14,220 @@ import { DatePicker as AriaDatePicker, DatePickerProps as AriaDatePickerProps, Button, - Calendar, + ButtonRenderProps, CalendarCell, CalendarGrid, + ContextValue, DateInput, DateSegment, DateValue, - Dialog, - FieldError, - Group, - Heading, - Label, - Popover, - Text + FormContext, + Provider } from 'react-aria-components'; -import {HelpTextProps} from '@react-types/shared'; -import {ReactNode} from 'react'; +import {baseColor, focusRing, fontRelative, iconStyle, style} from '../style' with {type: 'macro'}; +import {Calendar, Heading, IconContext, Popover} from '../'; +import CalendarIcon from '../s2wf-icons/S2_Icon_Calendar_20_N.svg'; +import {centerBaseline} from './CenterBaseline'; +import {Context, createContext, forwardRef, ReactElement, Ref, RefAttributes, useContext, useRef, useState} from 'react'; +import {controlBorderRadius, field, fieldInput, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; +import {FieldErrorIcon, FieldGroup, FieldLabel, HelpText} from './Field'; +import {forwardRefType, HelpTextProps, SpectrumLabelableProps} from '@react-types/shared'; +import {pressScale} from './pressScale'; +import {useSpectrumContextProps} from './useSpectrumContextProps'; -export interface DatePickerProps - extends AriaDatePickerProps, HelpTextProps { - label?: ReactNode + +export interface DatePickerProps extends + Omit, 'children' | 'className' | 'style'>, + StyleProps, + SpectrumLabelableProps, + HelpTextProps { + /** + * The size of the DateField. + * + * @default 'M' + */ + size?: 'S' | 'M' | 'L' | 'XL' } -export function DatePicker( - {label, description, errorMessage, ...props}: DatePickerProps -): ReactNode { +export const DatePickerContext: + Context>, HTMLDivElement>> = + createContext>, HTMLDivElement>>(null); + +const segmentContainer = style({ + flexGrow: 1 +}); + +const dateInput = style({ + outlineStyle: 'none', + caretColor: 'transparent', + backgroundColor: { + default: 'transparent', + isFocused: 'blue-900' + }, + color: { + isFocused: 'white' + }, + borderRadius: '[2px]', + paddingX: 2 +}); + +const iconStyles = style({ + flexGrow: 1, + display: 'flex', + alignItems: 'center', + justifyContent: 'end' +}); + +const inputButton = style({ + ...focusRing(), + ...controlBorderRadius('sm'), + display: 'flex', + textAlign: 'center', + borderStyle: 'none', + alignItems: 'center', + justifyContent: 'center', + size: { + size: { + S: 16, + M: 20, + L: 24, + XL: 32 + } + }, + marginStart: 'text-to-control', + aspectRatio: 'square', + flexShrink: 0, + transition: { + default: 'default', + forcedColors: 'none' + }, + backgroundColor: { + default: baseColor('gray-100'), + isOpen: 'gray-200', + isDisabled: 'disabled', + forcedColors: { + default: 'ButtonText', + isHovered: 'Highlight', + isOpen: 'Highlight', + isDisabled: 'GrayText' + } + }, + color: { + default: baseColor('neutral'), + isDisabled: 'disabled', + forcedColors: 'ButtonFace' + } +}); + +export const DatePicker: + (props: DatePickerProps & RefAttributes) => ReactElement | null = +/*#__PURE__*/ (forwardRef as forwardRefType)(function DatePicker( + props: DatePickerProps, ref: Ref +): ReactElement { + [props, ref] = useSpectrumContextProps(props, ref, DatePickerContext); + let { + label, + contextualHelp, + description: descriptionMessage, + errorMessage, + isRequired, + size = 'M', + labelPosition = 'top', + necessityIndicator, + labelAlign = 'start', + UNSAFE_style, + UNSAFE_className, + styles, + ...dateFieldProps + } = props; + let formContext = useContext(FormContext); + let buttonRef = useRef(null); + let [buttonHasFocus, setButtonHasFocus] = useState(false); + return ( - - - - - {(segment) => } - - - - {description && {description}} - {errorMessage} - - - -
- - - -
- - {(date) => } - -
-
-
+ + {({isDisabled, isInvalid, isOpen}) => { + return ( + <> + + {label} + + + + + {(segment) => } + + {isInvalid &&
} + +
+ + +
+ + + +
+ + {(date) => } + +
+
+ + {errorMessage} + + + ); + }}
); -} +}); + diff --git a/packages/@react-spectrum/s2/src/Field.tsx b/packages/@react-spectrum/s2/src/Field.tsx index d4bbe520d38..d79d828bd0b 100644 --- a/packages/@react-spectrum/s2/src/Field.tsx +++ b/packages/@react-spectrum/s2/src/Field.tsx @@ -22,6 +22,7 @@ import {ForwardedRef, forwardRef, ReactNode} from 'react'; import {IconContext} from './Icon'; // @ts-ignore import intlMessages from '../intl/*.json'; +import {isFocusVisible} from '@react-aria/interactions'; import {mergeStyles} from '../style/runtime'; import {StyleString} from '../style/types'; import {useDOMRef} from '@react-spectrum/utils'; @@ -152,7 +153,8 @@ export const FieldLabel = forwardRef(function FieldLabel(props: FieldLabelProps, interface FieldGroupProps extends Omit, UnsafeStyles { size?: 'S' | 'M' | 'L' | 'XL', children: ReactNode, - styles?: StyleString + styles?: StyleString, + shouldTurnOffFocusRing?: boolean } const fieldGroupStyles = style({ @@ -187,10 +189,11 @@ const fieldGroupStyles = style({ }); export const FieldGroup = forwardRef(function FieldGroup(props: FieldGroupProps, ref: ForwardedRef) { + let {shouldTurnOffFocusRing, ...otherProps} = props; return ( { // Forward focus to input element when clicking on a non-interactive child (e.g. icon or padding) if (e.pointerType === 'mouse' && !(e.target as Element).closest('button,input,textarea')) { @@ -206,7 +209,12 @@ export const FieldGroup = forwardRef(function FieldGroup(props: FieldGroupProps, }} style={props.UNSAFE_style} className={renderProps => (props.UNSAFE_className || '') + ' ' + centerBaselineBefore + mergeStyles( - fieldGroupStyles({...renderProps, size: props.size || 'M'}), + fieldGroupStyles({ + ...renderProps, + isFocusWithin: shouldTurnOffFocusRing ? false : renderProps.isFocusWithin, + isFocusVisible: shouldTurnOffFocusRing ? false : renderProps.isFocusVisible, + size: props.size || 'M' + }), props.styles )} /> ); diff --git a/packages/@react-spectrum/s2/src/NumberField.tsx b/packages/@react-spectrum/s2/src/NumberField.tsx index 63fab753df9..95b5dc1ceef 100644 --- a/packages/@react-spectrum/s2/src/NumberField.tsx +++ b/packages/@react-spectrum/s2/src/NumberField.tsx @@ -176,6 +176,7 @@ export const NumberField = forwardRef(function NumberField(props: NumberFieldPro return ( = { + component: DateField, + parameters: { + layout: 'centered' + }, + tags: ['autodocs'], + argTypes: { + ...categorizeArgTypes('Events', ['onChange']), + label: {control: {type: 'text'}}, + description: {control: {type: 'text'}}, + errorMessage: {control: {type: 'text'}}, + contextualHelp: {table: {disable: true}} + }, + title: 'DateField' +}; + +export default meta; +type Story = StoryObj; + +export const Example: Story = {}; + +export const Validation: Story = { + render: (args) => ( +
+ + + + ), + args: { + label: 'Birthday', + isRequired: true + } +}; + +export const CustomWidth: Story = { + render: (args) => ( + + ), + args: { + label: 'Birthday' + } +}; + +export const ContextualHelpExample: Story = { + render: (args) => ( + + Quantity + + + Enter a date, any date. May I recommend today? + + +
+ Learn more about what happened on this date. +
+ + } /> + ), + args: { + label: 'On this day' + } +}; diff --git a/packages/@react-spectrum/s2/stories/DatePicker.stories.tsx b/packages/@react-spectrum/s2/stories/DatePicker.stories.tsx new file mode 100644 index 00000000000..8b2e684e465 --- /dev/null +++ b/packages/@react-spectrum/s2/stories/DatePicker.stories.tsx @@ -0,0 +1,85 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you 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 REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {Button, Content, ContextualHelp, DatePicker, Footer, Form, Heading, Link, Text} from '../src'; +import {categorizeArgTypes} from './utils'; +import type {Meta, StoryObj} from '@storybook/react'; +import {style} from '../style' with {type: 'macro'}; + +const meta: Meta = { + component: DatePicker, + parameters: { + layout: 'centered' + }, + tags: ['autodocs'], + argTypes: { + ...categorizeArgTypes('Events', ['onChange']), + label: {control: {type: 'text'}}, + description: {control: {type: 'text'}}, + errorMessage: {control: {type: 'text'}}, + contextualHelp: {table: {disable: true}} + }, + title: 'DatePicker' +}; + +export default meta; +type Story = StoryObj; + +export const Example: Story = {}; + +export const Validation: Story = { + render: (args) => ( +
+ + + + ), + args: { + label: 'Birthday', + isRequired: true + } +}; + +export const CustomWidth: Story = { + render: (args) => ( + + ), + args: { + label: 'Birthday' + } +}; + +export const ContextualHelpExample: Story = { + render: (args) => ( + + Quantity + + + Enter a date, any date. May I recommend today? + + +
+ Learn more about what happened on this date. +
+ + } /> + ), + args: { + label: 'On this day' + } +}; diff --git a/packages/dev/codemods/src/s1-to-s2/__tests__/__snapshots__/calendar.test.ts.snap b/packages/dev/codemods/src/s1-to-s2/__tests__/__snapshots__/calendar.test.ts.snap index 76e6c926918..692c1437dfa 100644 --- a/packages/dev/codemods/src/s1-to-s2/__tests__/__snapshots__/calendar.test.ts.snap +++ b/packages/dev/codemods/src/s1-to-s2/__tests__/__snapshots__/calendar.test.ts.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Does nothing 1`] = ` -"import {Calendar} from '@adobe/react-spectrum'; +"import { Calendar } from "@react-spectrum/s2";
diff --git a/packages/dev/codemods/src/s1-to-s2/__tests__/__snapshots__/datefield.test.ts.snap b/packages/dev/codemods/src/s1-to-s2/__tests__/__snapshots__/datefield.test.ts.snap index 35cb5e31b97..f2e5b13430e 100644 --- a/packages/dev/codemods/src/s1-to-s2/__tests__/__snapshots__/datefield.test.ts.snap +++ b/packages/dev/codemods/src/s1-to-s2/__tests__/__snapshots__/datefield.test.ts.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Does nothing 1`] = ` -"import {DateField} from '@adobe/react-spectrum'; +"import { DateField } from "@react-spectrum/s2";
diff --git a/packages/dev/codemods/src/s1-to-s2/__tests__/__snapshots__/datepicker.test.ts.snap b/packages/dev/codemods/src/s1-to-s2/__tests__/__snapshots__/datepicker.test.ts.snap index 1b813ace880..5d59549159a 100644 --- a/packages/dev/codemods/src/s1-to-s2/__tests__/__snapshots__/datepicker.test.ts.snap +++ b/packages/dev/codemods/src/s1-to-s2/__tests__/__snapshots__/datepicker.test.ts.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Does nothing 1`] = ` -"import {DatePicker} from '@adobe/react-spectrum'; +"import { DatePicker } from "@react-spectrum/s2";
From 3b4f66931816a120d5858d5b336f52e71e1dd0e9 Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Thu, 19 Jun 2025 15:51:57 +1000 Subject: [PATCH 02/33] fix lint --- packages/@react-spectrum/s2/src/DatePicker.tsx | 3 +-- packages/@react-spectrum/s2/src/Field.tsx | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/@react-spectrum/s2/src/DatePicker.tsx b/packages/@react-spectrum/s2/src/DatePicker.tsx index 5217dd0e3e4..9bb15cd0fff 100644 --- a/packages/@react-spectrum/s2/src/DatePicker.tsx +++ b/packages/@react-spectrum/s2/src/DatePicker.tsx @@ -24,10 +24,9 @@ import { FormContext, Provider } from 'react-aria-components'; -import {baseColor, focusRing, fontRelative, iconStyle, style} from '../style' with {type: 'macro'}; +import {baseColor, focusRing, fontRelative, style} from '../style' with {type: 'macro'}; import {Calendar, Heading, IconContext, Popover} from '../'; import CalendarIcon from '../s2wf-icons/S2_Icon_Calendar_20_N.svg'; -import {centerBaseline} from './CenterBaseline'; import {Context, createContext, forwardRef, ReactElement, Ref, RefAttributes, useContext, useRef, useState} from 'react'; import {controlBorderRadius, field, fieldInput, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; import {FieldErrorIcon, FieldGroup, FieldLabel, HelpText} from './Field'; diff --git a/packages/@react-spectrum/s2/src/Field.tsx b/packages/@react-spectrum/s2/src/Field.tsx index d79d828bd0b..3c915ad0b3f 100644 --- a/packages/@react-spectrum/s2/src/Field.tsx +++ b/packages/@react-spectrum/s2/src/Field.tsx @@ -22,7 +22,6 @@ import {ForwardedRef, forwardRef, ReactNode} from 'react'; import {IconContext} from './Icon'; // @ts-ignore import intlMessages from '../intl/*.json'; -import {isFocusVisible} from '@react-aria/interactions'; import {mergeStyles} from '../style/runtime'; import {StyleString} from '../style/types'; import {useDOMRef} from '@react-spectrum/utils'; From 6401de3fcf44ab793b9cb2d59aee18904489851d Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Thu, 19 Jun 2025 16:21:38 +1000 Subject: [PATCH 03/33] start calendar --- packages/@react-spectrum/s2/src/Calendar.tsx | 84 +++++++++++++++++-- .../@react-spectrum/s2/src/DatePicker.tsx | 8 +- .../s2/stories/Calendar.stories.tsx | 36 ++++++++ 3 files changed, 116 insertions(+), 12 deletions(-) create mode 100644 packages/@react-spectrum/s2/stories/Calendar.stories.tsx diff --git a/packages/@react-spectrum/s2/src/Calendar.tsx b/packages/@react-spectrum/s2/src/Calendar.tsx index a3649883e75..09b69506bfa 100644 --- a/packages/@react-spectrum/s2/src/Calendar.tsx +++ b/packages/@react-spectrum/s2/src/Calendar.tsx @@ -14,14 +14,24 @@ import { Calendar as AriaCalendar, CalendarProps as AriaCalendarProps, Button, + ButtonProps, CalendarCell, CalendarGrid, + CalendarGridBody, + CalendarGridHeader, + CalendarHeaderCell, ContextValue, DateValue, - Heading, Text } from 'react-aria-components'; -import {Context, createContext, ReactNode} from 'react'; +import {ActionButton} from './'; +import {Context, createContext, ReactNode, useRef} from 'react'; +import {style} from '../style' with {type: 'macro'}; +import { Header, Heading } from './Content'; +import ChevronRightIcon from '../s2wf-icons/S2_Icon_ChevronRight_20_N.svg'; +import ChevronLeftIcon from '../s2wf-icons/S2_Icon_ChevronLeft_20_N.svg'; +import { pressScale } from '..'; + export interface CalendarProps @@ -33,20 +43,78 @@ export const CalendarContext: Context>, HTMLDivElement>> = createContext>, HTMLDivElement>>(null); +const headerStyles = style({ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + width: 'full' +}); + +const titleStyles = style({ + font: 'title-lg', + textAlign: 'center', + flexGrow: 1 +}); + +const headerCellStyles = style({ + font: 'title-sm', + textAlign: 'center', + flexGrow: 1 +}); + +const cellStyles = style({ + outlineStyle: 'none', + font: 'body-sm', + width: 24, + height: 24, + margin: 2, + borderRadius: 'full', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + backgroundColor: { + isSelected: 'blue-600' + } +}); + + export function Calendar( {errorMessage, ...props}: CalendarProps ): ReactNode { return ( -
- - - -
+
+ + + +
- {(date) => } + + {(day) => ( + + {day} + + )} + + + {(date) => ( + + )} + {errorMessage && {errorMessage}}
); } + +const CalendarButton = (props: Omit & {children: ReactNode}) => { + return ( + + {props.children} + + ); +}; diff --git a/packages/@react-spectrum/s2/src/DatePicker.tsx b/packages/@react-spectrum/s2/src/DatePicker.tsx index 9bb15cd0fff..45f5bb4e734 100644 --- a/packages/@react-spectrum/s2/src/DatePicker.tsx +++ b/packages/@react-spectrum/s2/src/DatePicker.tsx @@ -25,7 +25,7 @@ import { Provider } from 'react-aria-components'; import {baseColor, focusRing, fontRelative, style} from '../style' with {type: 'macro'}; -import {Calendar, Heading, IconContext, Popover} from '../'; +import {Calendar, Heading, IconContext} from '../'; import CalendarIcon from '../s2wf-icons/S2_Icon_Calendar_20_N.svg'; import {Context, createContext, forwardRef, ReactElement, Ref, RefAttributes, useContext, useRef, useState} from 'react'; import {controlBorderRadius, field, fieldInput, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; @@ -33,6 +33,7 @@ import {FieldErrorIcon, FieldGroup, FieldLabel, HelpText} from './Field'; import {forwardRefType, HelpTextProps, SpectrumLabelableProps} from '@react-types/shared'; import {pressScale} from './pressScale'; import {useSpectrumContextProps} from './useSpectrumContextProps'; +import {PopoverBase} from './Popover'; export interface DatePickerProps extends @@ -167,7 +168,6 @@ export const DatePicker: contextualHelp={contextualHelp}> {label} - - +
@@ -215,7 +215,7 @@ export const DatePicker: {(date) => } - + = { + component: Calendar, + parameters: { + layout: 'centered' + }, + tags: ['autodocs'], + argTypes: { + ...categorizeArgTypes('Events', ['onChange']), + label: {control: {type: 'text'}}, + description: {control: {type: 'text'}}, + errorMessage: {control: {type: 'text'}}, + contextualHelp: {table: {disable: true}} + }, + title: 'Calendar' +}; + +export default meta; +type Story = StoryObj; + +export const Example: Story = {}; From 7bbdd476774598e9d19c3f3e91992136a8459a68 Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Fri, 20 Jun 2025 15:19:15 +1000 Subject: [PATCH 04/33] Style calendar and fix some datepicker styles --- packages/@react-aria/calendar/src/index.ts | 1 + packages/@react-spectrum/s2/package.json | 1 + packages/@react-spectrum/s2/src/Calendar.tsx | 212 ++++++++++++++---- .../@react-spectrum/s2/src/DatePicker.tsx | 22 +- packages/@react-spectrum/s2/src/index.ts | 1 + .../s2/stories/Calendar.stories.tsx | 57 ++++- .../react-aria-components/src/Calendar.tsx | 15 +- yarn.lock | 1 + 8 files changed, 252 insertions(+), 58 deletions(-) diff --git a/packages/@react-aria/calendar/src/index.ts b/packages/@react-aria/calendar/src/index.ts index 1676e603a42..f9da6b5f40c 100644 --- a/packages/@react-aria/calendar/src/index.ts +++ b/packages/@react-aria/calendar/src/index.ts @@ -14,6 +14,7 @@ export {useCalendar} from './useCalendar'; export {useRangeCalendar} from './useRangeCalendar'; export {useCalendarGrid} from './useCalendarGrid'; export {useCalendarCell} from './useCalendarCell'; +export {getEraFormat} from './utils'; export type {AriaCalendarProps, AriaRangeCalendarProps, CalendarProps, DateValue, RangeCalendarProps} from '@react-types/calendar'; export type {CalendarAria} from './useCalendarBase'; diff --git a/packages/@react-spectrum/s2/package.json b/packages/@react-spectrum/s2/package.json index 01b53e081b6..3b583f7619c 100644 --- a/packages/@react-spectrum/s2/package.json +++ b/packages/@react-spectrum/s2/package.json @@ -150,6 +150,7 @@ }, "dependencies": { "@internationalized/number": "^3.6.3", + "@react-aria/calendar": "^3.8.3", "@react-aria/collections": "3.0.0-rc.3", "@react-aria/focus": "^3.20.5", "@react-aria/i18n": "^3.12.10", diff --git a/packages/@react-spectrum/s2/src/Calendar.tsx b/packages/@react-spectrum/s2/src/Calendar.tsx index 09b69506bfa..31376bb79b3 100644 --- a/packages/@react-spectrum/s2/src/Calendar.tsx +++ b/packages/@react-spectrum/s2/src/Calendar.tsx @@ -10,39 +10,49 @@ * governing permissions and limitations under the License. */ +import {ActionButton, Header, Heading} from './'; import { Calendar as AriaCalendar, CalendarProps as AriaCalendarProps, - Button, ButtonProps, CalendarCell, CalendarGrid, CalendarGridBody, CalendarGridHeader, CalendarHeaderCell, + CalendarStateContext, ContextValue, DateValue, Text } from 'react-aria-components'; -import {ActionButton} from './'; -import {Context, createContext, ReactNode, useRef} from 'react'; -import {style} from '../style' with {type: 'macro'}; -import { Header, Heading } from './Content'; -import ChevronRightIcon from '../s2wf-icons/S2_Icon_ChevronRight_20_N.svg'; +import {baseColor, focusRing, lightDark, style} from '../style' with {type: 'macro'}; import ChevronLeftIcon from '../s2wf-icons/S2_Icon_ChevronLeft_20_N.svg'; -import { pressScale } from '..'; - +import ChevronRightIcon from '../s2wf-icons/S2_Icon_ChevronRight_20_N.svg'; +import {Context, createContext, ForwardedRef, forwardRef, Fragment, ReactElement, ReactNode, RefAttributes, useContext, useMemo} from 'react'; +import {forwardRefType} from '@react-types/shared'; +import {getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; +import {getEraFormat} from '@react-aria/calendar'; +import {useDateFormatter} from '@react-aria/i18n'; export interface CalendarProps - extends AriaCalendarProps { - errorMessage?: string + extends Omit, 'visibleDuration' | 'style' | 'className' | 'styles'>, + StyleProps { + errorMessage?: string, + visibleMonths?: number } export const CalendarContext: Context>, HTMLDivElement>> = createContext>, HTMLDivElement>>(null); +const calendarStyles = style({ + display: 'flex', + flexDirection: 'column', + gap: 24, + width: 'full' +}, getAllowedOverrides()); + const headerStyles = style({ display: 'flex', alignItems: 'center', @@ -50,64 +60,190 @@ const headerStyles = style({ width: 'full' }); +const headingStyles = style({ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + margin: 0, + width: 'full' +}); + const titleStyles = style({ font: 'title-lg', textAlign: 'center', - flexGrow: 1 + flexGrow: 1, + flexShrink: 0, + flexBasis: '0%', + minWidth: 0 }); const headerCellStyles = style({ font: 'title-sm', + cursor: 'default', textAlign: 'center', flexGrow: 1 }); const cellStyles = style({ - outlineStyle: 'none', + ...focusRing(), + position: 'relative', font: 'body-sm', - width: 24, - height: 24, + cursor: 'default', + width: 32, + height: 32, margin: 2, borderRadius: 'full', - display: 'flex', + display: { + default: 'flex', + isOutsideMonth: 'none' + }, alignItems: 'center', justifyContent: 'center', backgroundColor: { - isSelected: 'blue-600' + isToday: { + default: baseColor('gray-300'), + isDisabled: 'disabled' + }, + isSelected: { + default: lightDark('accent-900', 'accent-700'), + isHovered: lightDark('accent-1000', 'accent-600'), + isPressed: lightDark('accent-1000', 'accent-600'), + isFocusVisible: lightDark('accent-1000', 'accent-600') + } + }, + color: { + isSelected: 'white', + isDisabled: 'disabled' } }); +const unavailableStyles = style({ + position: 'absolute', + top: 'calc(50% - 1px)', + left: 'calc(25% - 1px)', + right: 'calc(25% - 1px)', + height: 2, + transform: 'rotate(-16deg)', + borderRadius: 'full', + backgroundColor: '[currentColor]' +}); + -export function Calendar( - {errorMessage, ...props}: CalendarProps -): ReactNode { +export const Calendar: + (props: CalendarProps & RefAttributes) => ReactElement | null = +/*#__PURE__*/ (forwardRef as forwardRefType)(function Calendar(props: CalendarProps, ref: ForwardedRef) { + let { + visibleMonths = 1, + errorMessage, + UNSAFE_style, + UNSAFE_className, + styles, + ...otherProps + } = props; return ( - +
- +
- - - {(day) => ( - - {day} - - )} - - - {(date) => ( - - )} - - +
+ {Array.from({length: visibleMonths}).map((_, i) => ( + + + {(day) => ( + + {day} + + )} + + + {(date) => ( + + {({isUnavailable, formattedDate}) => ( + <> +
+ {formattedDate} +
+ {isUnavailable &&
} + + )} + + )} + + + ))} +
{errorMessage && {errorMessage}} ); -} +}); + +// Ordinarily the heading is a formatted date range, ie January 2025 - February 2025. +// However, we want to show each month individually. +const CalendarHeading = () => { + let {visibleRange, timeZone} = useContext(CalendarStateContext) ?? {}; + let era: any = getEraFormat(visibleRange?.start) || getEraFormat(visibleRange?.end); + let monthFormatter = useDateFormatter({ + month: 'long', + year: 'numeric', + era, + calendar: visibleRange?.start.calendar.identifier, + timeZone + }); + let months = useMemo(() => { + if (!visibleRange) { + return []; + } + let months: string[] = []; + for (let i = visibleRange.start; i.compare(visibleRange.end) <= 0; i = i.add({months: 1})) { + // TODO: account for the first week possibly overlapping, like with a custom 454 calendar. + // there has to be a better way to do this... + if (i.month === visibleRange.start.month) { + i = i.add({weeks: 1}); + } + months.push(monthFormatter.format(i.toDate(timeZone!))); + } + return months; + }, [visibleRange, monthFormatter, timeZone]); + + return ( + + {months.map((month, i) => { + if (i === 0) { + return ( + +
{month}
+
+ ); + } else { + return ( + + {/* Spacers to account for Next/Previous buttons and gap, spelled out to show the math */} +
+
+
+
{month}
+ + ); + } + })} + + ); +}; const CalendarButton = (props: Omit & {children: ReactNode}) => { return ( diff --git a/packages/@react-spectrum/s2/src/DatePicker.tsx b/packages/@react-spectrum/s2/src/DatePicker.tsx index 45f5bb4e734..76159013893 100644 --- a/packages/@react-spectrum/s2/src/DatePicker.tsx +++ b/packages/@react-spectrum/s2/src/DatePicker.tsx @@ -15,8 +15,6 @@ import { DatePickerProps as AriaDatePickerProps, Button, ButtonRenderProps, - CalendarCell, - CalendarGrid, ContextValue, DateInput, DateSegment, @@ -25,15 +23,15 @@ import { Provider } from 'react-aria-components'; import {baseColor, focusRing, fontRelative, style} from '../style' with {type: 'macro'}; -import {Calendar, Heading, IconContext} from '../'; +import {Calendar, IconContext} from '../'; import CalendarIcon from '../s2wf-icons/S2_Icon_Calendar_20_N.svg'; import {Context, createContext, forwardRef, ReactElement, Ref, RefAttributes, useContext, useRef, useState} from 'react'; import {controlBorderRadius, field, fieldInput, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; import {FieldErrorIcon, FieldGroup, FieldLabel, HelpText} from './Field'; import {forwardRefType, HelpTextProps, SpectrumLabelableProps} from '@react-types/shared'; +import {PopoverBase} from './Popover'; import {pressScale} from './pressScale'; import {useSpectrumContextProps} from './useSpectrumContextProps'; -import {PopoverBase} from './Popover'; export interface DatePickerProps extends @@ -81,6 +79,7 @@ const iconStyles = style({ const inputButton = style({ ...focusRing(), ...controlBorderRadius('sm'), + cursor: 'default', display: 'flex', textAlign: 'center', borderStyle: 'none', @@ -204,17 +203,10 @@ export const DatePicker: - - -
- - - -
- - {(date) => } - -
+ + = { component: Calendar, @@ -25,7 +30,13 @@ const meta: Meta = { label: {control: {type: 'text'}}, description: {control: {type: 'text'}}, errorMessage: {control: {type: 'text'}}, - contextualHelp: {table: {disable: true}} + contextualHelp: {table: {disable: true}}, + visibleMonths: { + control: { + type: 'select' + }, + options: [1, 2, 3] + } }, title: 'Calendar' }; @@ -34,3 +45,45 @@ export default meta; type Story = StoryObj; export const Example: Story = {}; + +export const DateUnavailable: Story = { + args: { + isDateUnavailable: (date: DateValue) => { + const disabledIntervals = [[today(getLocalTimeZone()).subtract({days: 13}), today(getLocalTimeZone()), today(getLocalTimeZone()).add({weeks: 1})], [today(getLocalTimeZone()).add({weeks: 2}), today(getLocalTimeZone()).add({weeks: 3})]]; + return disabledIntervals.some((interval) => date.compare(interval[0]) > 0 && date.compare(interval[1]) < 0); + } + } +}; + +export const MinValue: Story = { + args: { + minValue: today(getLocalTimeZone()).add({days: 1}) + } +}; + +function ControlledFocus(props: CalendarProps): ReactElement { + const defaultFocusedDate = props.focusedValue ?? new CalendarDate(2019, 6, 5); + let [focusedDate, setFocusedDate] = useState(defaultFocusedDate); + return ( +
+ setFocusedDate(defaultFocusedDate)}>Reset focused date + +
+ ); +} + +function CustomCalendar(props: CalendarProps): ReactElement { + return ( + new Custom454Calendar()} focusedValue={new CalendarDate(2023, 2, 5)} /> + ); +} + +export const Custom454Example: Story = { + render: (args) => +}; diff --git a/packages/react-aria-components/src/Calendar.tsx b/packages/react-aria-components/src/Calendar.tsx index dd4f683a29a..341a8bcaafe 100644 --- a/packages/react-aria-components/src/Calendar.tsx +++ b/packages/react-aria-components/src/Calendar.tsx @@ -24,7 +24,7 @@ import { VisuallyHidden } from 'react-aria'; import {ButtonContext} from './Button'; -import {CalendarDate, CalendarIdentifier, createCalendar, DateDuration, endOfMonth, Calendar as ICalendar, isSameDay, isSameMonth} from '@internationalized/date'; +import {CalendarDate, CalendarIdentifier, createCalendar, DateDuration, endOfMonth, Calendar as ICalendar, isSameDay, isSameMonth, isToday} from '@internationalized/date'; import {CalendarState, RangeCalendarState, useCalendarState, useRangeCalendarState} from 'react-stately'; import {ContextValue, DOMProps, Provider, RenderProps, SlotProps, StyleProps, useContextProps, useRenderProps, useSlottedContext} from './utils'; import {DOMAttributes, FocusableElement, forwardRefType, HoverEvents} from '@react-types/shared'; @@ -315,7 +315,12 @@ export interface CalendarCellRenderProps { * Whether the cell is part of an invalid selection. * @selector [data-invalid] */ - isInvalid: boolean + isInvalid: boolean, + /** + * Whether the cell is today. + * @selector [data-today] + */ + isToday: boolean } export interface CalendarGridProps extends StyleProps { @@ -497,6 +502,8 @@ export const CalendarCell = /*#__PURE__*/ (forwardRef as forwardRefType)(functio let state = calendarState ?? rangeCalendarState!; let {startDate: currentMonth} = useContext(InternalCalendarGridContext) ?? {startDate: state.visibleRange.start}; let isOutsideMonth = !isSameMonth(currentMonth, date); + // TODO: check api with team, this seemed useful though + let istoday = isToday(date, state.timeZone); let buttonRef = useRef(null); let {cellProps, buttonProps, ...states} = useCalendarCell( @@ -526,6 +533,7 @@ export const CalendarCell = /*#__PURE__*/ (forwardRef as forwardRefType)(functio isFocusVisible, isSelectionStart, isSelectionEnd, + isToday: istoday, ...states } }); @@ -542,7 +550,8 @@ export const CalendarCell = /*#__PURE__*/ (forwardRef as forwardRefType)(functio 'data-selected': states.isSelected || undefined, 'data-selection-start': isSelectionStart || undefined, 'data-selection-end': isSelectionEnd || undefined, - 'data-invalid': states.isInvalid || undefined + 'data-invalid': states.isInvalid || undefined, + 'data-today': istoday || undefined }; return ( diff --git a/yarn.lock b/yarn.lock index 843e4e26f02..48a28a30c67 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7976,6 +7976,7 @@ __metadata: "@adobe/spectrum-tokens": "npm:^13.10.0" "@internationalized/number": "npm:^3.6.3" "@parcel/macros": "npm:^2.14.0" + "@react-aria/calendar": "npm:^3.8.3" "@react-aria/collections": "npm:3.0.0-rc.3" "@react-aria/focus": "npm:^3.20.5" "@react-aria/i18n": "npm:^3.12.10" From 0716d724614f8233afffb61033dabe10367b2f21 Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Mon, 23 Jun 2025 11:35:20 +1000 Subject: [PATCH 05/33] changing ring size --- packages/@react-spectrum/s2/src/Calendar.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/@react-spectrum/s2/src/Calendar.tsx b/packages/@react-spectrum/s2/src/Calendar.tsx index 31376bb79b3..aef6612aea3 100644 --- a/packages/@react-spectrum/s2/src/Calendar.tsx +++ b/packages/@react-spectrum/s2/src/Calendar.tsx @@ -86,6 +86,11 @@ const headerCellStyles = style({ const cellStyles = style({ ...focusRing(), + outlineOffset: { + default: -2, + isToday: 2, + isSelected: 2 + }, position: 'relative', font: 'body-sm', cursor: 'default', From b67342f7a29cff4ab3e96209c6d72cb7cd6417a7 Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Mon, 23 Jun 2025 11:40:43 +1000 Subject: [PATCH 06/33] fix hover --- packages/@react-spectrum/s2/src/Calendar.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/@react-spectrum/s2/src/Calendar.tsx b/packages/@react-spectrum/s2/src/Calendar.tsx index aef6612aea3..a8bf5716d2e 100644 --- a/packages/@react-spectrum/s2/src/Calendar.tsx +++ b/packages/@react-spectrum/s2/src/Calendar.tsx @@ -105,6 +105,8 @@ const cellStyles = style({ alignItems: 'center', justifyContent: 'center', backgroundColor: { + default: 'transparent', + isHovered: 'gray-100', isToday: { default: baseColor('gray-300'), isDisabled: 'disabled' From beb5265cc1e3152ba0fc2a25fb1705f3af45cbe3 Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Mon, 23 Jun 2025 12:02:45 +1000 Subject: [PATCH 07/33] revert outline size change --- packages/@react-spectrum/s2/src/Calendar.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/@react-spectrum/s2/src/Calendar.tsx b/packages/@react-spectrum/s2/src/Calendar.tsx index a8bf5716d2e..42fc867bf93 100644 --- a/packages/@react-spectrum/s2/src/Calendar.tsx +++ b/packages/@react-spectrum/s2/src/Calendar.tsx @@ -86,11 +86,6 @@ const headerCellStyles = style({ const cellStyles = style({ ...focusRing(), - outlineOffset: { - default: -2, - isToday: 2, - isSelected: 2 - }, position: 'relative', font: 'body-sm', cursor: 'default', From fb693ab51a7f66186e6702c3977aefa421cb7e73 Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Mon, 23 Jun 2025 17:08:48 +1000 Subject: [PATCH 08/33] add range calendar --- packages/@react-spectrum/s2/package.json | 1 + packages/@react-spectrum/s2/src/Calendar.tsx | 2 +- .../@react-spectrum/s2/src/RangeCalendar.tsx | 325 +++++++++++++++++- packages/@react-spectrum/s2/src/index.ts | 2 + .../s2/stories/RangeCalendar.stories.tsx | 89 +++++ .../react-aria-components/src/Calendar.tsx | 30 +- 6 files changed, 424 insertions(+), 25 deletions(-) create mode 100644 packages/@react-spectrum/s2/stories/RangeCalendar.stories.tsx diff --git a/packages/@react-spectrum/s2/package.json b/packages/@react-spectrum/s2/package.json index 3b583f7619c..1da0a4b03af 100644 --- a/packages/@react-spectrum/s2/package.json +++ b/packages/@react-spectrum/s2/package.json @@ -149,6 +149,7 @@ "jest": "^29.5.0" }, "dependencies": { + "@internationalized/date": "^3.8.2", "@internationalized/number": "^3.6.3", "@react-aria/calendar": "^3.8.3", "@react-aria/collections": "3.0.0-rc.3", diff --git a/packages/@react-spectrum/s2/src/Calendar.tsx b/packages/@react-spectrum/s2/src/Calendar.tsx index 42fc867bf93..f2066ce5ccb 100644 --- a/packages/@react-spectrum/s2/src/Calendar.tsx +++ b/packages/@react-spectrum/s2/src/Calendar.tsx @@ -180,7 +180,7 @@ export const Calendar:
{formattedDate}
- {isUnavailable &&
} + {isUnavailable &&
} )} diff --git a/packages/@react-spectrum/s2/src/RangeCalendar.tsx b/packages/@react-spectrum/s2/src/RangeCalendar.tsx index 030696b7781..8ede2a9ea1d 100644 --- a/packages/@react-spectrum/s2/src/RangeCalendar.tsx +++ b/packages/@react-spectrum/s2/src/RangeCalendar.tsx @@ -10,38 +10,325 @@ * governing permissions and limitations under the License. */ +import {ActionButton, Header, Heading} from './'; import { + CalendarCell as AriaCalendarCell, RangeCalendar as AriaRangeCalendar, RangeCalendarProps as AriaRangeCalendarProps, - Button, - CalendarCell, + ButtonProps, + CalendarCellProps, + CalendarCellRenderProps, CalendarGrid, + CalendarGridBody, + CalendarGridHeader, + CalendarHeaderCell, + ContextValue, DateValue, - Heading, + RangeCalendarStateContext, Text } from 'react-aria-components'; -import {ReactNode} from 'react'; +import {baseColor, focusRing, lightDark, style} from '../style' with {type: 'macro'}; +import ChevronLeftIcon from '../s2wf-icons/S2_Icon_ChevronLeft_20_N.svg'; +import ChevronRightIcon from '../s2wf-icons/S2_Icon_ChevronRight_20_N.svg'; +import {Context, createContext, CSSProperties, ForwardedRef, forwardRef, Fragment, ReactElement, ReactNode, RefAttributes, useContext, useMemo} from 'react'; +import {forwardRefType} from '@react-types/shared'; +import {getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; +import {getEraFormat} from '@react-aria/calendar'; +import {useDateFormatter} from '@react-aria/i18n'; export interface RangeCalendarProps - extends AriaRangeCalendarProps { - errorMessage?: string + extends Omit, 'visibleDuration' | 'style' | 'className' | 'styles'>, + StyleProps { + errorMessage?: string, + visibleMonths?: number } -export function RangeCalendar( - {errorMessage, ...props}: RangeCalendarProps -): ReactNode { +export const RangeCalendarContext: + Context>, HTMLDivElement>> = + createContext>, HTMLDivElement>>(null); + + +const calendarStyles = style({ + display: 'flex', + flexDirection: 'column', + gap: 24, + width: 'full' +}, getAllowedOverrides()); + +const headerStyles = style({ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + width: 'full' +}); + +const headingStyles = style({ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + margin: 0, + width: 'full' +}); + +const titleStyles = style({ + font: 'title-lg', + textAlign: 'center', + flexGrow: 1, + flexShrink: 0, + flexBasis: '0%', + minWidth: 0 +}); + +const headerCellStyles = style({ + font: 'title-sm', + cursor: 'default', + textAlign: 'center', + flexGrow: 1 +}); + +const cellStyles = style({ + paddingX: 0, + paddingY: 2 +}); + +const cellInnerWrapperStyles = style({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: 'full', + boxSizing: 'border-box', + borderStartRadius: { + isSelectionStart: 'full' + }, + borderEndRadius: { + isSelectionEnd: 'full' + }, + outlineStyle: 'none' +}); + +const innerCellStyles = style({ + ...focusRing(), + position: 'relative', + font: 'body-sm', + cursor: 'default', + width: 32, + '--cell-width': { + type: 'width', + value: '[self(width)]' // keep in sync with innerCellStyles.width + }, + height: 32, + borderRadius: 'full', + display: { + default: 'flex', + isOutsideMonth: 'none' + }, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: { + default: 'transparent', + isHovered: 'gray-100', + isToday: { + default: baseColor('gray-300'), + isDisabled: 'disabled' + }, + isSelectionStart: { + default: lightDark('accent-900', 'accent-700'), + isHovered: lightDark('accent-1000', 'accent-600'), + isPressed: lightDark('accent-1000', 'accent-600'), + isFocusVisible: lightDark('accent-1000', 'accent-600') + }, + isSelectionEnd: { + default: lightDark('accent-900', 'accent-700'), + isHovered: lightDark('accent-1000', 'accent-600'), + isPressed: lightDark('accent-1000', 'accent-600'), + isFocusVisible: lightDark('accent-1000', 'accent-600') + } + }, + color: { + isSelected: { + isSelectionStart: 'white', + isSelectionEnd: 'white' + }, + isDisabled: 'disabled' + } +}); + +const unavailableStyles = style({ + position: 'absolute', + top: 'calc(50% - 1px)', + left: 'calc(25% - 1px)', + right: 'calc(25% - 1px)', + height: 2, + transform: 'rotate(-16deg)', + borderRadius: 'full', + backgroundColor: '[currentColor]' +}); + +const selectionSpanStyles = style({ + position: 'absolute', + zIndex: -1, + top: 0, + insetStart: 'calc(-1 * var(--selection-span) * var(--cell-width))', + insetEnd: 0, + bottom: 0, + borderWidth: 2, + borderStyle: 'dashed', + borderColor: 'blue-800', // focus-indicator-color + borderStartRadius: 'full', + borderEndRadius: 'full', + backgroundColor: 'blue-subtle' +}); + +export const RangeCalendar: + (props: RangeCalendarProps & RefAttributes) => ReactElement | null = +/*#__PURE__*/ (forwardRef as forwardRefType)(function RangeCalendar(props: RangeCalendarProps, ref: ForwardedRef) { + let { + visibleMonths = 1, + errorMessage, + UNSAFE_style, + UNSAFE_className, + styles, + ...otherProps + } = props; + return ( - -
- - - -
- - {(date) => } - + +
+ + + +
+
+ {Array.from({length: visibleMonths}).map((_, i) => { + return ( + + + {(day) => ( + + {day} + + )} + + + {(date, weekIndex) => { + return ( + + ); + }} + + + ); + })} +
{errorMessage && {errorMessage}}
); -} +}); + +// Ordinarily the heading is a formatted date range, ie January 2025 - February 2025. +// However, we want to show each month individually. +const CalendarHeading = () => { + let {visibleRange, timeZone} = useContext(RangeCalendarStateContext) ?? {}; + let era: any = getEraFormat(visibleRange?.start) || getEraFormat(visibleRange?.end); + let monthFormatter = useDateFormatter({ + month: 'long', + year: 'numeric', + era, + calendar: visibleRange?.start.calendar.identifier, + timeZone + }); + let months = useMemo(() => { + if (!visibleRange) { + return []; + } + let months: string[] = []; + for (let i = visibleRange.start; i.compare(visibleRange.end) <= 0; i = i.add({months: 1})) { + // TODO: account for the first week possibly overlapping, like with a custom 454 calendar. + // there has to be a better way to do this... + if (i.month === visibleRange.start.month) { + i = i.add({weeks: 1}); + } + months.push(monthFormatter.format(i.toDate(timeZone!))); + } + return months; + }, [visibleRange, monthFormatter, timeZone]); + + return ( + + {months.map((month, i) => { + if (i === 0) { + return ( + +
{month}
+
+ ); + } else { + return ( + + {/* Spacers to account for Next/Previous buttons and gap, spelled out to show the math */} +
+
+
+
{month}
+ + ); + } + })} + + ); +}; + +const CalendarButton = (props: Omit & {children: ReactNode}) => { + return ( + + {props.children} + + ); +}; + +const CalendarCell = (props: Omit & {weekIndex: number}) => { + let state = useContext(RangeCalendarStateContext)!; + let {getDatesInWeek} = state; + let {weekIndex} = props; + let datesInWeek = getDatesInWeek(weekIndex); + + return ( + + {(renderProps) => { + let {isUnavailable, formattedDate} = renderProps; + let isBackgroundStyleApplied = false; + let firstSelectedInWeek = datesInWeek.findIndex(date => date && state.isSelected(date)); + let indexOfCurrentDate = datesInWeek.findIndex(date => date && date.compare(props.date) === 0); + if (renderProps.isSelected && firstSelectedInWeek !== -1 && indexOfCurrentDate !== -1) { + isBackgroundStyleApplied = true; + } + return ( +
+
+ {formattedDate} +
+ {isUnavailable &&
} + {isBackgroundStyleApplied &&
} +
+ ); + }} + + ); +}; diff --git a/packages/@react-spectrum/s2/src/index.ts b/packages/@react-spectrum/s2/src/index.ts index 66649b3ab88..07b95010483 100644 --- a/packages/@react-spectrum/s2/src/index.ts +++ b/packages/@react-spectrum/s2/src/index.ts @@ -65,6 +65,7 @@ export {ProgressCircle, ProgressCircleContext} from './ProgressCircle'; export {Provider} from './Provider'; export {Radio} from './Radio'; export {RadioGroup, RadioGroupContext} from './RadioGroup'; +export {RangeCalendar, RangeCalendarContext} from './RangeCalendar'; export {RangeSlider, RangeSliderContext} from './RangeSlider'; export {SearchField, SearchFieldContext} from './SearchField'; export {SegmentedControl, SegmentedControlItem, SegmentedControlContext} from './SegmentedControl'; @@ -143,6 +144,7 @@ export type {RadioGroupProps} from './RadioGroup'; export type {SearchFieldProps} from './SearchField'; export type {SegmentedControlProps, SegmentedControlItemProps} from './SegmentedControl'; export type {SliderProps} from './Slider'; +export type {RangeCalendarProps} from './RangeCalendar'; export type {RangeSliderProps} from './RangeSlider'; export type {SkeletonProps} from './Skeleton'; export type {SkeletonCollectionProps} from './SkeletonCollection'; diff --git a/packages/@react-spectrum/s2/stories/RangeCalendar.stories.tsx b/packages/@react-spectrum/s2/stories/RangeCalendar.stories.tsx new file mode 100644 index 00000000000..8690e83fe82 --- /dev/null +++ b/packages/@react-spectrum/s2/stories/RangeCalendar.stories.tsx @@ -0,0 +1,89 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you 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 REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {ActionButton, RangeCalendar, RangeCalendarProps} from '../src'; +import {CalendarDate, getLocalTimeZone, today} from '@internationalized/date'; +import {categorizeArgTypes} from './utils'; +import {Custom454Calendar} from '../../../@internationalized/date/tests/customCalendarImpl'; +import {DateValue} from 'react-aria'; +import type {Meta, StoryObj} from '@storybook/react'; +import {ReactElement, useState} from 'react'; +import {style} from '../style' with {type: 'macro'}; + +const meta: Meta = { + component: RangeCalendar, + parameters: { + layout: 'centered' + }, + tags: ['autodocs'], + argTypes: { + ...categorizeArgTypes('Events', ['onChange']), + label: {control: {type: 'text'}}, + description: {control: {type: 'text'}}, + errorMessage: {control: {type: 'text'}}, + contextualHelp: {table: {disable: true}}, + visibleMonths: { + control: { + type: 'select' + }, + options: [1, 2, 3] + } + }, + title: 'RangeCalendar' +}; + +export default meta; +type Story = StoryObj; + +export const Example: Story = {}; + +export const DateUnavailable: Story = { + args: { + isDateUnavailable: (date: DateValue) => { + const disabledIntervals = [[today(getLocalTimeZone()).subtract({days: 13}), today(getLocalTimeZone()), today(getLocalTimeZone()).add({weeks: 1})], [today(getLocalTimeZone()).add({weeks: 2}), today(getLocalTimeZone()).add({weeks: 3})]]; + return disabledIntervals.some((interval) => date.compare(interval[0]) > 0 && date.compare(interval[1]) < 0); + } + } +}; + +export const MinValue: Story = { + args: { + minValue: today(getLocalTimeZone()).add({days: 1}) + } +}; + +function ControlledFocus(props: RangeCalendarProps): ReactElement { + const defaultFocusedDate = props.focusedValue ?? new CalendarDate(2019, 6, 5); + let [focusedDate, setFocusedDate] = useState(defaultFocusedDate); + return ( +
+ setFocusedDate(defaultFocusedDate)}>Reset focused date + +
+ ); +} + +function CustomCalendar(props: RangeCalendarProps): ReactElement { + return ( + new Custom454Calendar()} focusedValue={new CalendarDate(2023, 2, 5)} /> + ); +} + +export const Custom454Example: Story = { + render: (args) => +}; diff --git a/packages/react-aria-components/src/Calendar.tsx b/packages/react-aria-components/src/Calendar.tsx index 341a8bcaafe..e928add1467 100644 --- a/packages/react-aria-components/src/Calendar.tsx +++ b/packages/react-aria-components/src/Calendar.tsx @@ -30,7 +30,7 @@ import {ContextValue, DOMProps, Provider, RenderProps, SlotProps, StyleProps, us import {DOMAttributes, FocusableElement, forwardRefType, HoverEvents} from '@react-types/shared'; import {filterDOMProps} from '@react-aria/utils'; import {HeadingContext} from './RSPContexts'; -import React, {createContext, ForwardedRef, forwardRef, ReactElement, useContext, useRef} from 'react'; +import React, {createContext, CSSProperties, ForwardedRef, forwardRef, ReactElement, useContext, useRef} from 'react'; import {TextContext} from './Text'; export interface CalendarRenderProps { @@ -453,7 +453,7 @@ export {CalendarHeaderCellForwardRef as CalendarHeaderCell}; export interface CalendarGridBodyProps extends StyleProps { /** A function to render a `` for a given date. */ - children: (date: CalendarDate) => ReactElement + children: (date: CalendarDate, weekIndex: number, dayIndex: number) => ReactElement } function CalendarGridBody(props: CalendarGridBodyProps, ref: ForwardedRef) { @@ -473,7 +473,7 @@ function CalendarGridBody(props: CalendarGridBodyProps, ref: ForwardedRef {state.getDatesInWeek(weekIndex, startDate).map((date, i) => ( date - ? React.cloneElement(children(date), {key: i}) + ? React.cloneElement(children(date, weekIndex, i), {key: i}) : ))} @@ -490,7 +490,11 @@ export {CalendarGridBodyForwardRef as CalendarGridBody}; export interface CalendarCellProps extends RenderProps, HoverEvents { /** The date to render in the cell. */ - date: CalendarDate + date: CalendarDate, + /** The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the element. A function may be provided to compute the class based on component state. */ + cellClassName?: string | ((values: CalendarCellRenderProps & {defaultClassName: string | undefined}) => string), + /** The inline [style](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/style) for the element. A function may be provided to compute the style based on component state. */ + cellStyle?: CSSProperties | ((values: CalendarCellRenderProps & {defaultStyle: CSSProperties}) => CSSProperties | undefined) } /** @@ -538,6 +542,22 @@ export const CalendarCell = /*#__PURE__*/ (forwardRef as forwardRefType)(functio } }); + let cellRenderProps = useRenderProps({ + className: otherProps.cellClassName, + style: otherProps.cellStyle, + defaultClassName: 'react-aria-CalendarCellWrapper', + values: { + date, + isHovered, + isOutsideMonth, + isFocusVisible, + isSelectionStart, + isSelectionEnd, + isToday: istoday, + ...states + } + }); + let dataAttrs = { 'data-focused': states.isFocused || undefined, 'data-hovered': isHovered || undefined, @@ -555,7 +575,7 @@ export const CalendarCell = /*#__PURE__*/ (forwardRef as forwardRefType)(functio }; return ( - +
); From 1ab469979d1d606842e21700c8b72653fee94187 Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Tue, 24 Jun 2025 09:00:44 +1000 Subject: [PATCH 09/33] fix yarn lock --- yarn.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/yarn.lock b/yarn.lock index 48a28a30c67..b3695205d67 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7974,6 +7974,7 @@ __metadata: resolution: "@react-spectrum/s2@workspace:packages/@react-spectrum/s2" dependencies: "@adobe/spectrum-tokens": "npm:^13.10.0" + "@internationalized/date": "npm:^3.8.2" "@internationalized/number": "npm:^3.6.3" "@parcel/macros": "npm:^2.14.0" "@react-aria/calendar": "npm:^3.8.3" From f51db9ce1414aa712a7176391fa06b40638618ab Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Tue, 24 Jun 2025 12:34:57 +1000 Subject: [PATCH 10/33] fix cell styles for different size ring and gap --- packages/@react-spectrum/s2/src/Calendar.tsx | 20 +++++++++++++++++-- .../@react-spectrum/s2/src/RangeCalendar.tsx | 15 +++++++++++--- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/packages/@react-spectrum/s2/src/Calendar.tsx b/packages/@react-spectrum/s2/src/Calendar.tsx index f2066ce5ccb..a920f8f4e2b 100644 --- a/packages/@react-spectrum/s2/src/Calendar.tsx +++ b/packages/@react-spectrum/s2/src/Calendar.tsx @@ -16,6 +16,7 @@ import { CalendarProps as AriaCalendarProps, ButtonProps, CalendarCell, + CalendarCellRenderProps, CalendarGrid, CalendarGridBody, CalendarGridHeader, @@ -84,8 +85,22 @@ const headerCellStyles = style({ flexGrow: 1 }); -const cellStyles = style({ +const cellStyles = style({ + paddingX: 4, + '--cell-gap': { + type: 'paddingStart', + value: 4 + }, + paddingY: 2 +}); + +const cellInnerStyles = style({ ...focusRing(), + outlineOffset: { + default: -2, + isToday: 2, + isSelected: 2 + }, position: 'relative', font: 'body-sm', cursor: 'default', @@ -174,7 +189,8 @@ export const Calendar: {(date) => ( + cellClassName={cellStyles} + className={cellInnerStyles}> {({isUnavailable, formattedDate}) => ( <>
diff --git a/packages/@react-spectrum/s2/src/RangeCalendar.tsx b/packages/@react-spectrum/s2/src/RangeCalendar.tsx index 8ede2a9ea1d..7b15c3a0a61 100644 --- a/packages/@react-spectrum/s2/src/RangeCalendar.tsx +++ b/packages/@react-spectrum/s2/src/RangeCalendar.tsx @@ -88,7 +88,11 @@ const headerCellStyles = style({ }); const cellStyles = style({ - paddingX: 0, + paddingX: 4, + '--cell-gap': { + type: 'paddingStart', + value: 4 + }, paddingY: 2 }); @@ -109,13 +113,18 @@ const cellInnerWrapperStyles = style({ const innerCellStyles = style({ ...focusRing(), + outlineOffset: { + default: -2, + isToday: 2, + isSelected: 2 + }, position: 'relative', font: 'body-sm', cursor: 'default', width: 32, '--cell-width': { type: 'width', - value: '[self(width)]' // keep in sync with innerCellStyles.width + value: '[self(width)]' }, height: 32, borderRadius: 'full', @@ -169,7 +178,7 @@ const selectionSpanStyles = style({ position: 'absolute', zIndex: -1, top: 0, - insetStart: 'calc(-1 * var(--selection-span) * var(--cell-width))', + insetStart: 'calc(-1 * var(--selection-span) * (var(--cell-width) + var(--cell-gap) + var(--cell-gap)))', insetEnd: 0, bottom: 0, borderWidth: 2, From c8e2ef28dcc00f12d1c31256b1740dfed71b2aba Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Tue, 24 Jun 2025 12:43:49 +1000 Subject: [PATCH 11/33] add spectrum context --- packages/@react-spectrum/s2/src/Calendar.tsx | 2 ++ packages/@react-spectrum/s2/src/RangeCalendar.tsx | 2 ++ 2 files changed, 4 insertions(+) diff --git a/packages/@react-spectrum/s2/src/Calendar.tsx b/packages/@react-spectrum/s2/src/Calendar.tsx index a920f8f4e2b..3dafce79df8 100644 --- a/packages/@react-spectrum/s2/src/Calendar.tsx +++ b/packages/@react-spectrum/s2/src/Calendar.tsx @@ -34,6 +34,7 @@ import {forwardRefType} from '@react-types/shared'; import {getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; import {getEraFormat} from '@react-aria/calendar'; import {useDateFormatter} from '@react-aria/i18n'; +import {useSpectrumContextProps} from './useSpectrumContextProps'; export interface CalendarProps @@ -149,6 +150,7 @@ const unavailableStyles = style({ export const Calendar: (props: CalendarProps & RefAttributes) => ReactElement | null = /*#__PURE__*/ (forwardRef as forwardRefType)(function Calendar(props: CalendarProps, ref: ForwardedRef) { + [props, ref] = useSpectrumContextProps(props, ref, CalendarContext); let { visibleMonths = 1, errorMessage, diff --git a/packages/@react-spectrum/s2/src/RangeCalendar.tsx b/packages/@react-spectrum/s2/src/RangeCalendar.tsx index 7b15c3a0a61..6148b8b3e86 100644 --- a/packages/@react-spectrum/s2/src/RangeCalendar.tsx +++ b/packages/@react-spectrum/s2/src/RangeCalendar.tsx @@ -35,6 +35,7 @@ import {forwardRefType} from '@react-types/shared'; import {getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; import {getEraFormat} from '@react-aria/calendar'; import {useDateFormatter} from '@react-aria/i18n'; +import {useSpectrumContextProps} from './useSpectrumContextProps'; export interface RangeCalendarProps @@ -192,6 +193,7 @@ const selectionSpanStyles = style({ export const RangeCalendar: (props: RangeCalendarProps & RefAttributes) => ReactElement | null = /*#__PURE__*/ (forwardRef as forwardRefType)(function RangeCalendar(props: RangeCalendarProps, ref: ForwardedRef) { + [props, ref] = useSpectrumContextProps(props, ref, RangeCalendarContext); let { visibleMonths = 1, errorMessage, From eb5c4ffc323940a4bcedee0e22209bea512fc2d2 Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Tue, 24 Jun 2025 14:14:30 +1000 Subject: [PATCH 12/33] Add DateRangePicker --- packages/@react-spectrum/s2/src/Calendar.tsx | 2 +- packages/@react-spectrum/s2/src/DateField.tsx | 2 +- .../@react-spectrum/s2/src/DatePicker.tsx | 2 +- .../s2/src/DateRangePicker.tsx | 260 ++++++++++++++---- .../@react-spectrum/s2/src/RangeCalendar.tsx | 2 +- packages/@react-spectrum/s2/src/index.ts | 6 +- .../s2/stories/DateRangePicker.stories.tsx | 85 ++++++ 7 files changed, 306 insertions(+), 53 deletions(-) create mode 100644 packages/@react-spectrum/s2/stories/DateRangePicker.stories.tsx diff --git a/packages/@react-spectrum/s2/src/Calendar.tsx b/packages/@react-spectrum/s2/src/Calendar.tsx index 3dafce79df8..bb6caaf8f08 100644 --- a/packages/@react-spectrum/s2/src/Calendar.tsx +++ b/packages/@react-spectrum/s2/src/Calendar.tsx @@ -165,7 +165,7 @@ export const Calendar: ref={ref} visibleDuration={{months: visibleMonths}} style={UNSAFE_style} - className={UNSAFE_className + calendarStyles(null, styles)}> + className={(UNSAFE_className || '') + calendarStyles(null, styles)}>
diff --git a/packages/@react-spectrum/s2/src/DateField.tsx b/packages/@react-spectrum/s2/src/DateField.tsx index 7f42647629c..5ac21111749 100644 --- a/packages/@react-spectrum/s2/src/DateField.tsx +++ b/packages/@react-spectrum/s2/src/DateField.tsx @@ -97,7 +97,7 @@ export const DateField: isRequired={isRequired} {...dateFieldProps} style={UNSAFE_style} - className={UNSAFE_className + style(field(), getAllowedOverrides())({ + className={(UNSAFE_className || '') + style(field(), getAllowedOverrides())({ isInForm: !!formContext, labelPosition, size diff --git a/packages/@react-spectrum/s2/src/DatePicker.tsx b/packages/@react-spectrum/s2/src/DatePicker.tsx index 76159013893..d189b24b0b9 100644 --- a/packages/@react-spectrum/s2/src/DatePicker.tsx +++ b/packages/@react-spectrum/s2/src/DatePicker.tsx @@ -149,7 +149,7 @@ export const DatePicker: isRequired={isRequired} {...dateFieldProps} style={UNSAFE_style} - className={UNSAFE_className + style(field(), getAllowedOverrides())({ + className={(UNSAFE_className || '') + style(field(), getAllowedOverrides())({ isInForm: !!formContext, labelPosition, size diff --git a/packages/@react-spectrum/s2/src/DateRangePicker.tsx b/packages/@react-spectrum/s2/src/DateRangePicker.tsx index b1fffd39cd2..ea604de87b4 100644 --- a/packages/@react-spectrum/s2/src/DateRangePicker.tsx +++ b/packages/@react-spectrum/s2/src/DateRangePicker.tsx @@ -14,60 +14,226 @@ import { DateRangePicker as AriaDateRangePicker, DateRangePickerProps as AriaDateRangePickerProps, Button, - CalendarCell, - CalendarGrid, + ButtonRenderProps, + ContextValue, DateInput, DateSegment, DateValue, - Dialog, - FieldError, - Group, - Heading, - Label, - Popover, - RangeCalendar, - Text + FormContext, + Provider } from 'react-aria-components'; -import {HelpTextProps} from '@react-types/shared'; -import {ReactNode} from 'react'; +import {baseColor, focusRing, fontRelative, style} from '../style' with {type: 'macro'}; +import CalendarIcon from '../s2wf-icons/S2_Icon_Calendar_20_N.svg'; +import {Context, createContext, forwardRef, ReactElement, Ref, RefAttributes, useContext, useRef, useState} from 'react'; +import {controlBorderRadius, field, fieldInput, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; +import {FieldErrorIcon, FieldGroup, FieldLabel, HelpText} from './Field'; +import {forwardRefType, HelpTextProps, SpectrumLabelableProps} from '@react-types/shared'; +import {IconContext, RangeCalendar} from '../'; +import {PopoverBase} from './Popover'; +import {pressScale} from './pressScale'; +import {useSpectrumContextProps} from './useSpectrumContextProps'; -export interface DateRangePickerProps - extends AriaDateRangePickerProps, HelpTextProps { - label?: ReactNode + +export interface DateRangePickerProps extends + Omit, 'children' | 'className' | 'style'>, + StyleProps, + SpectrumLabelableProps, + HelpTextProps { + /** + * The size of the DateField. + * + * @default 'M' + */ + size?: 'S' | 'M' | 'L' | 'XL' } -export function DateRangePicker( - {label, description, errorMessage, ...props}: DateRangePickerProps -): ReactNode { +export const DateRangePickerContext: + Context>, HTMLDivElement>> = + createContext>, HTMLDivElement>>(null); + +const segmentContainer = style({ + flexGrow: 0, + flexShrink: 0 +}); + +const dateInput = style({ + outlineStyle: 'none', + caretColor: 'transparent', + backgroundColor: { + default: 'transparent', + isFocused: 'blue-900' + }, + color: { + isFocused: 'white' + }, + borderRadius: '[2px]', + paddingX: 2 +}); + +const iconStyles = style({ + flexGrow: 1, + display: 'flex', + alignItems: 'center', + justifyContent: 'end' +}); + +const inputButton = style({ + ...focusRing(), + ...controlBorderRadius('sm'), + cursor: 'default', + display: 'flex', + textAlign: 'center', + borderStyle: 'none', + alignItems: 'center', + justifyContent: 'center', + size: { + size: { + S: 16, + M: 20, + L: 24, + XL: 32 + } + }, + marginStart: 'text-to-control', + aspectRatio: 'square', + transition: { + default: 'default', + forcedColors: 'none' + }, + backgroundColor: { + default: baseColor('gray-100'), + isOpen: 'gray-200', + isDisabled: 'disabled', + forcedColors: { + default: 'ButtonText', + isHovered: 'Highlight', + isOpen: 'Highlight', + isDisabled: 'GrayText' + } + }, + color: { + default: baseColor('neutral'), + isDisabled: 'disabled', + forcedColors: 'ButtonFace' + } +}); + +export const DateRangePicker: + (props: DateRangePickerProps & RefAttributes) => ReactElement | null = +/*#__PURE__*/ (forwardRef as forwardRefType)(function DateRangePicker( + props: DateRangePickerProps, ref: Ref +): ReactElement { + [props, ref] = useSpectrumContextProps(props, ref, DateRangePickerContext); + let { + label, + contextualHelp, + description: descriptionMessage, + errorMessage, + isRequired, + size = 'M', + labelPosition = 'top', + necessityIndicator, + labelAlign = 'start', + UNSAFE_style, + UNSAFE_className, + styles, + ...dateFieldProps + } = props; + let formContext = useContext(FormContext); + let buttonRef = useRef(null); + let [buttonHasFocus, setButtonHasFocus] = useState(false); + return ( - - - - - {(segment) => } - - - - {(segment) => } - - - - {description && {description}} - {errorMessage} - - - -
- - - -
- - {(date) => } - -
-
-
+ + {({isDisabled, isInvalid, isOpen}) => { + return ( + <> + + {label} + + + + {(segment) => } + + + + {(segment) => } + + {isInvalid &&
} +
+ +
+
+ + + + + {errorMessage} + + + ); + }}
); -} +}); + diff --git a/packages/@react-spectrum/s2/src/RangeCalendar.tsx b/packages/@react-spectrum/s2/src/RangeCalendar.tsx index 6148b8b3e86..326f2afa430 100644 --- a/packages/@react-spectrum/s2/src/RangeCalendar.tsx +++ b/packages/@react-spectrum/s2/src/RangeCalendar.tsx @@ -209,7 +209,7 @@ export const RangeCalendar: ref={ref} visibleDuration={{months: visibleMonths}} style={UNSAFE_style} - className={UNSAFE_className + calendarStyles(null, styles)}> + className={(UNSAFE_className || '') + calendarStyles(null, styles)}>
diff --git a/packages/@react-spectrum/s2/src/index.ts b/packages/@react-spectrum/s2/src/index.ts index 8ff2a8411f0..00bcb7e80a9 100644 --- a/packages/@react-spectrum/s2/src/index.ts +++ b/packages/@react-spectrum/s2/src/index.ts @@ -38,8 +38,9 @@ export {ColorSwatchPicker, ColorSwatchPickerContext} from './ColorSwatchPicker'; export {ColorWheel, ColorWheelContext} from './ColorWheel'; export {ComboBox, ComboBoxItem, ComboBoxSection, ComboBoxContext} from './ComboBox'; export {ContextualHelp, ContextualHelpContext} from './ContextualHelp'; -export {DateField} from './DateField'; -export {DatePicker} from './DatePicker'; +export {DateField, DateFieldContext} from './DateField'; +export {DatePicker, DatePickerContext} from './DatePicker'; +export {DateRangePicker, DateRangePickerContext} from './DateRangePicker'; export {DisclosureHeader, Disclosure, DisclosurePanel, DisclosureContext, DisclosureTitle} from './Disclosure'; export {Heading, HeadingContext, Header, HeaderContext, Content, ContentContext, Footer, FooterContext, Text, TextContext, Keyboard, KeyboardContext} from './Content'; export {Dialog} from './Dialog'; @@ -119,6 +120,7 @@ export type {ColorWheelProps} from './ColorWheel'; export type {ComboBoxProps, ComboBoxItemProps, ComboBoxSectionProps} from './ComboBox'; export type {DateFieldProps} from './DateField'; export type {DatePickerProps} from './DatePicker'; +export type {DateRangePickerProps} from './DateRangePicker'; export type {DialogProps} from './Dialog'; export type {CustomDialogProps} from './CustomDialog'; export type {FullscreenDialogProps} from './FullscreenDialog'; diff --git a/packages/@react-spectrum/s2/stories/DateRangePicker.stories.tsx b/packages/@react-spectrum/s2/stories/DateRangePicker.stories.tsx new file mode 100644 index 00000000000..e5266153158 --- /dev/null +++ b/packages/@react-spectrum/s2/stories/DateRangePicker.stories.tsx @@ -0,0 +1,85 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you 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 REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {Button, Content, ContextualHelp, DateRangePicker, Footer, Form, Heading, Link, Text} from '../src'; +import {categorizeArgTypes} from './utils'; +import type {Meta, StoryObj} from '@storybook/react'; +import {style} from '../style' with {type: 'macro'}; + +const meta: Meta = { + component: DateRangePicker, + parameters: { + layout: 'centered' + }, + tags: ['autodocs'], + argTypes: { + ...categorizeArgTypes('Events', ['onChange']), + label: {control: {type: 'text'}}, + description: {control: {type: 'text'}}, + errorMessage: {control: {type: 'text'}}, + contextualHelp: {table: {disable: true}} + }, + title: 'DateRangePicker' +}; + +export default meta; +type Story = StoryObj; + +export const Example: Story = {}; + +export const Validation: Story = { + render: (args) => ( +
+ + + + ), + args: { + label: 'Birthday', + isRequired: true + } +}; + +export const CustomWidth: Story = { + render: (args) => ( + + ), + args: { + label: 'Birthday' + } +}; + +export const ContextualHelpExample: Story = { + render: (args) => ( + + Quantity + + + Enter a date, any date. May I recommend today? + + +
+ Learn more about what happened on this date. +
+ + } /> + ), + args: { + label: 'On this day' + } +}; From aa71a8360532175f17022b801db4454cf621ccd6 Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Tue, 24 Jun 2025 14:15:27 +1000 Subject: [PATCH 13/33] add comment --- packages/@react-spectrum/s2/src/DateRangePicker.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/@react-spectrum/s2/src/DateRangePicker.tsx b/packages/@react-spectrum/s2/src/DateRangePicker.tsx index ea604de87b4..959122865d0 100644 --- a/packages/@react-spectrum/s2/src/DateRangePicker.tsx +++ b/packages/@react-spectrum/s2/src/DateRangePicker.tsx @@ -143,6 +143,7 @@ export const DateRangePicker: let buttonRef = useRef(null); let [buttonHasFocus, setButtonHasFocus] = useState(false); + // TODO: fix width return ( Date: Tue, 24 Jun 2025 15:44:50 +1000 Subject: [PATCH 14/33] fix test --- .../__tests__/__snapshots__/daterangepicker.test.ts.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dev/codemods/src/s1-to-s2/__tests__/__snapshots__/daterangepicker.test.ts.snap b/packages/dev/codemods/src/s1-to-s2/__tests__/__snapshots__/daterangepicker.test.ts.snap index 66d0c389672..7162dc296c1 100644 --- a/packages/dev/codemods/src/s1-to-s2/__tests__/__snapshots__/daterangepicker.test.ts.snap +++ b/packages/dev/codemods/src/s1-to-s2/__tests__/__snapshots__/daterangepicker.test.ts.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Does nothing 1`] = ` -"import {DateRangePicker} from '@adobe/react-spectrum'; +"import { DateRangePicker } from "@react-spectrum/s2";
From 0cbd1071c7db8e35135df96bac34026119cd4476 Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Tue, 24 Jun 2025 16:47:11 +1000 Subject: [PATCH 15/33] Add TimeField and aria labels --- packages/@react-spectrum/s2/src/TimeField.tsx | 140 +++++++++++++++--- packages/@react-spectrum/s2/src/index.ts | 2 + .../s2/stories/Calendar.stories.tsx | 17 ++- .../s2/stories/DatePicker.stories.tsx | 6 +- .../s2/stories/RangeCalendar.stories.tsx | 17 ++- .../s2/stories/TimeField.stories.tsx | 89 +++++++++++ 6 files changed, 243 insertions(+), 28 deletions(-) create mode 100644 packages/@react-spectrum/s2/stories/TimeField.stories.tsx diff --git a/packages/@react-spectrum/s2/src/TimeField.tsx b/packages/@react-spectrum/s2/src/TimeField.tsx index fc058f7e65c..48fd93028bf 100644 --- a/packages/@react-spectrum/s2/src/TimeField.tsx +++ b/packages/@react-spectrum/s2/src/TimeField.tsx @@ -13,32 +13,134 @@ import { TimeField as AriaTimeField, TimeFieldProps as AriaTimeFieldProps, + ContextValue, DateInput, DateSegment, - FieldError, - Label, - Text, + FormContext, TimeValue } from 'react-aria-components'; -import {HelpTextProps} from '@react-types/shared'; -import {ReactNode} from 'react'; +import {Context, createContext, forwardRef, ReactElement, Ref, RefAttributes, useContext} from 'react'; +import {field, fieldInput, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; +import {FieldErrorIcon, FieldGroup, FieldLabel, HelpText} from './Field'; +import {forwardRefType, HelpTextProps, SpectrumLabelableProps} from '@react-types/shared'; +import {style} from '../style' with {type: 'macro'}; +import {useSpectrumContextProps} from './useSpectrumContextProps'; -export interface TimeFieldProps - extends AriaTimeFieldProps, HelpTextProps { - label?: ReactNode + +export interface TimeFieldProps extends + Omit, 'children' | 'className' | 'style'>, + StyleProps, + SpectrumLabelableProps, + HelpTextProps { + /** + * The size of the TimeField. + * + * @default 'M' + */ + size?: 'S' | 'M' | 'L' | 'XL' } -export function TimeField( - {label, description, errorMessage, ...props}: TimeFieldProps -): ReactNode { +export const TimeFieldContext: + Context>, HTMLDivElement>> = + createContext>, HTMLDivElement>>(null); + +const segmentContainer = style({ + flexGrow: 1 +}); + +// TODO: Figure out field width +const timeInput = style({ + outlineStyle: 'none', + caretColor: 'transparent', + backgroundColor: { + default: 'transparent', + isFocused: 'blue-900' + }, + color: { + isFocused: 'white' + }, + borderRadius: '[2px]', + paddingX: 2 +}); + +const iconStyles = style({ + display: 'flex', + alignItems: 'center', + justifyContent: 'end' +}); + +export const TimeField: + (props: TimeFieldProps & RefAttributes) => ReactElement | null = +/*#__PURE__*/ (forwardRef as forwardRefType)(function TimeField( + props: TimeFieldProps, ref: Ref +): ReactElement { + [props, ref] = useSpectrumContextProps(props, ref, TimeFieldContext); + let { + label, + contextualHelp, + description: descriptionMessage, + errorMessage, + isRequired, + size = 'M', + labelPosition = 'top', + necessityIndicator, + labelAlign = 'start', + UNSAFE_style, + UNSAFE_className, + styles, + ...timeFieldProps + } = props; + let formContext = useContext(FormContext); + return ( - - - - {(segment) => } - - {description && {description}} - {errorMessage} + + {({isDisabled, isInvalid}) => { + return ( + <> + + {label} + + + + + {(segment) => } + + {isInvalid &&
} +
+ + {errorMessage} + + + ); + }}
); -} +}); diff --git a/packages/@react-spectrum/s2/src/index.ts b/packages/@react-spectrum/s2/src/index.ts index 00bcb7e80a9..e58969bb7a3 100644 --- a/packages/@react-spectrum/s2/src/index.ts +++ b/packages/@react-spectrum/s2/src/index.ts @@ -81,6 +81,7 @@ export {TableView, TableHeader, TableBody, Row, Cell, Column, TableContext} from export {Tabs, TabList, Tab, TabPanel, TabsContext} from './Tabs'; export {TagGroup, Tag, TagGroupContext} from './TagGroup'; export {TextArea, TextField, TextAreaContext, TextFieldContext} from './TextField'; +export {TimeField, TimeFieldContext} from './TimeField'; export {ToastContainer as UNSTABLE_ToastContainer, ToastQueue as UNSTABLE_ToastQueue} from './Toast'; export {ToggleButton, ToggleButtonContext} from './ToggleButton'; export {ToggleButtonGroup, ToggleButtonGroupContext} from './ToggleButtonGroup'; @@ -158,6 +159,7 @@ export type {TableViewProps, TableHeaderProps, TableBodyProps, RowProps, CellPro export type {TabsProps, TabProps, TabListProps, TabPanelProps} from './Tabs'; export type {TagGroupProps, TagProps} from './TagGroup'; export type {TextFieldProps, TextAreaProps} from './TextField'; +export type {TimeFieldProps} from './TimeField'; export type {ToastOptions, ToastContainerProps} from './Toast'; export type {ToggleButtonProps} from './ToggleButton'; export type {ToggleButtonGroupProps} from './ToggleButtonGroup'; diff --git a/packages/@react-spectrum/s2/stories/Calendar.stories.tsx b/packages/@react-spectrum/s2/stories/Calendar.stories.tsx index 8c09b2982d3..4192906d75d 100644 --- a/packages/@react-spectrum/s2/stories/Calendar.stories.tsx +++ b/packages/@react-spectrum/s2/stories/Calendar.stories.tsx @@ -44,20 +44,26 @@ const meta: Meta = { export default meta; type Story = StoryObj; -export const Example: Story = {}; +export const Example: Story = { + args: { + 'aria-label': 'Birthday' + } +}; export const DateUnavailable: Story = { args: { isDateUnavailable: (date: DateValue) => { const disabledIntervals = [[today(getLocalTimeZone()).subtract({days: 13}), today(getLocalTimeZone()), today(getLocalTimeZone()).add({weeks: 1})], [today(getLocalTimeZone()).add({weeks: 2}), today(getLocalTimeZone()).add({weeks: 3})]]; return disabledIntervals.some((interval) => date.compare(interval[0]) > 0 && date.compare(interval[1]) < 0); - } + }, + 'aria-label': 'Birthday' } }; export const MinValue: Story = { args: { - minValue: today(getLocalTimeZone()).add({days: 1}) + minValue: today(getLocalTimeZone()).add({days: 1}), + 'aria-label': 'Birthday' } }; @@ -85,5 +91,8 @@ function CustomCalendar(props: CalendarProps): ReactElement { } export const Custom454Example: Story = { - render: (args) => + render: (args) => , + args: { + 'aria-label': 'Birthday' + } }; diff --git a/packages/@react-spectrum/s2/stories/DatePicker.stories.tsx b/packages/@react-spectrum/s2/stories/DatePicker.stories.tsx index 8b2e684e465..3779043c2c0 100644 --- a/packages/@react-spectrum/s2/stories/DatePicker.stories.tsx +++ b/packages/@react-spectrum/s2/stories/DatePicker.stories.tsx @@ -34,7 +34,11 @@ const meta: Meta = { export default meta; type Story = StoryObj; -export const Example: Story = {}; +export const Example: Story = { + args: { + 'aria-label': 'Birthday' + } +}; export const Validation: Story = { render: (args) => ( diff --git a/packages/@react-spectrum/s2/stories/RangeCalendar.stories.tsx b/packages/@react-spectrum/s2/stories/RangeCalendar.stories.tsx index 8690e83fe82..e3370462303 100644 --- a/packages/@react-spectrum/s2/stories/RangeCalendar.stories.tsx +++ b/packages/@react-spectrum/s2/stories/RangeCalendar.stories.tsx @@ -44,20 +44,26 @@ const meta: Meta = { export default meta; type Story = StoryObj; -export const Example: Story = {}; +export const Example: Story = { + args: { + 'aria-label': 'Reservation' + } +}; export const DateUnavailable: Story = { args: { isDateUnavailable: (date: DateValue) => { const disabledIntervals = [[today(getLocalTimeZone()).subtract({days: 13}), today(getLocalTimeZone()), today(getLocalTimeZone()).add({weeks: 1})], [today(getLocalTimeZone()).add({weeks: 2}), today(getLocalTimeZone()).add({weeks: 3})]]; return disabledIntervals.some((interval) => date.compare(interval[0]) > 0 && date.compare(interval[1]) < 0); - } + }, + 'aria-label': 'Reservation' } }; export const MinValue: Story = { args: { - minValue: today(getLocalTimeZone()).add({days: 1}) + minValue: today(getLocalTimeZone()).add({days: 1}), + 'aria-label': 'Reservation' } }; @@ -85,5 +91,8 @@ function CustomCalendar(props: RangeCalendarProps): ReactElement { } export const Custom454Example: Story = { - render: (args) => + render: (args) => , + args: { + 'aria-label': 'Reservation' + } }; diff --git a/packages/@react-spectrum/s2/stories/TimeField.stories.tsx b/packages/@react-spectrum/s2/stories/TimeField.stories.tsx new file mode 100644 index 00000000000..369ac8caa9e --- /dev/null +++ b/packages/@react-spectrum/s2/stories/TimeField.stories.tsx @@ -0,0 +1,89 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you 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 REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {Button, Content, ContextualHelp, Footer, Form, Heading, Link, Text, TimeField} from '../src'; +import {categorizeArgTypes} from './utils'; +import type {Meta, StoryObj} from '@storybook/react'; +import {style} from '../style' with {type: 'macro'}; + +const meta: Meta = { + component: TimeField, + parameters: { + layout: 'centered' + }, + tags: ['autodocs'], + argTypes: { + ...categorizeArgTypes('Events', ['onChange']), + label: {control: {type: 'text'}}, + description: {control: {type: 'text'}}, + errorMessage: {control: {type: 'text'}}, + contextualHelp: {table: {disable: true}} + }, + title: 'TimeField' +}; + +export default meta; +type Story = StoryObj; + +export const Example: Story = { + args: { + 'aria-label': 'Launch time' + } +}; + +export const Validation: Story = { + render: (args) => ( +
+ + + + ), + args: { + label: 'Launch time', + isRequired: true + } +}; + +export const CustomWidth: Story = { + render: (args) => ( + + ), + args: { + label: 'Launch time' + } +}; + +export const ContextualHelpExample: Story = { + render: (args) => ( + + Quantity + + + Enter a date, any date. May I recommend today? + + +
+ Learn more about what happened on this date. +
+ + } /> + ), + args: { + label: 'On this day' + } +}; From 559c1bcb014fd2adcc3b6f39ee6ee87ed736614b Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Thu, 26 Jun 2025 11:35:07 +1000 Subject: [PATCH 16/33] add press scaling --- packages/@react-spectrum/s2/src/Calendar.tsx | 42 ++++++----- .../@react-spectrum/s2/src/RangeCalendar.tsx | 72 ++++++++++++------- 2 files changed, 71 insertions(+), 43 deletions(-) diff --git a/packages/@react-spectrum/s2/src/Calendar.tsx b/packages/@react-spectrum/s2/src/Calendar.tsx index bb6caaf8f08..2a072c2af70 100644 --- a/packages/@react-spectrum/s2/src/Calendar.tsx +++ b/packages/@react-spectrum/s2/src/Calendar.tsx @@ -10,12 +10,13 @@ * governing permissions and limitations under the License. */ -import {ActionButton, Header, Heading} from './'; +import {ActionButton, Header, Heading, pressScale} from './'; import { Calendar as AriaCalendar, + CalendarCell as AriaCalendarCell, CalendarProps as AriaCalendarProps, ButtonProps, - CalendarCell, + CalendarCellProps, CalendarCellRenderProps, CalendarGrid, CalendarGridBody, @@ -29,7 +30,7 @@ import { import {baseColor, focusRing, lightDark, style} from '../style' with {type: 'macro'}; import ChevronLeftIcon from '../s2wf-icons/S2_Icon_ChevronLeft_20_N.svg'; import ChevronRightIcon from '../s2wf-icons/S2_Icon_ChevronRight_20_N.svg'; -import {Context, createContext, ForwardedRef, forwardRef, Fragment, ReactElement, ReactNode, RefAttributes, useContext, useMemo} from 'react'; +import {Context, createContext, ForwardedRef, forwardRef, Fragment, ReactElement, ReactNode, RefAttributes, useContext, useMemo, useRef} from 'react'; import {forwardRefType} from '@react-types/shared'; import {getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; import {getEraFormat} from '@react-aria/calendar'; @@ -189,19 +190,7 @@ export const Calendar: {(date) => ( - - {({isUnavailable, formattedDate}) => ( - <> -
- {formattedDate} -
- {isUnavailable &&
} - - )} - + )} @@ -274,3 +263,24 @@ const CalendarButton = (props: Omit & {children: ReactN ); }; + +const CalendarCell = (props: Omit) => { + let ref = useRef(null); + return ( + + {({isUnavailable, formattedDate}) => ( + <> +
+ {formattedDate} +
+ {isUnavailable &&
} + + )} + + ); +}; diff --git a/packages/@react-spectrum/s2/src/RangeCalendar.tsx b/packages/@react-spectrum/s2/src/RangeCalendar.tsx index 326f2afa430..7d384edcad4 100644 --- a/packages/@react-spectrum/s2/src/RangeCalendar.tsx +++ b/packages/@react-spectrum/s2/src/RangeCalendar.tsx @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import {ActionButton, Header, Heading} from './'; +import {ActionButton, Header, Heading, pressScale} from './'; import { CalendarCell as AriaCalendarCell, RangeCalendar as AriaRangeCalendar, @@ -30,7 +30,7 @@ import { import {baseColor, focusRing, lightDark, style} from '../style' with {type: 'macro'}; import ChevronLeftIcon from '../s2wf-icons/S2_Icon_ChevronLeft_20_N.svg'; import ChevronRightIcon from '../s2wf-icons/S2_Icon_ChevronRight_20_N.svg'; -import {Context, createContext, CSSProperties, ForwardedRef, forwardRef, Fragment, ReactElement, ReactNode, RefAttributes, useContext, useMemo} from 'react'; +import {Context, createContext, CSSProperties, ForwardedRef, forwardRef, Fragment, ReactElement, ReactNode, RefAttributes, useContext, useMemo, useRef} from 'react'; import {forwardRefType} from '@react-types/shared'; import {getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; import {getEraFormat} from '@react-aria/calendar'; @@ -122,11 +122,6 @@ const innerCellStyles = style({ position: 'relative', font: 'body-sm', cursor: 'default', - width: 32, - '--cell-width': { - type: 'width', - value: '[self(width)]' - }, height: 32, borderRadius: 'full', display: { @@ -312,10 +307,6 @@ const CalendarButton = (props: Omit & {children: ReactN }; const CalendarCell = (props: Omit & {weekIndex: number}) => { - let state = useContext(RangeCalendarStateContext)!; - let {getDatesInWeek} = state; - let {weekIndex} = props; - let datesInWeek = getDatesInWeek(weekIndex); return ( & {weekIndex: n className={cellInnerWrapperStyles} cellClassName={cellStyles}> {(renderProps) => { - let {isUnavailable, formattedDate} = renderProps; - let isBackgroundStyleApplied = false; - let firstSelectedInWeek = datesInWeek.findIndex(date => date && state.isSelected(date)); - let indexOfCurrentDate = datesInWeek.findIndex(date => date && date.compare(props.date) === 0); - if (renderProps.isSelected && firstSelectedInWeek !== -1 && indexOfCurrentDate !== -1) { - isBackgroundStyleApplied = true; - } - return ( -
-
- {formattedDate} -
- {isUnavailable &&
} - {isBackgroundStyleApplied &&
} -
- ); + return ; }} ); }; + +const CalendarCellInner = (props: Omit & {weekIndex: number, renderProps: CalendarCellRenderProps, date: DateValue}) => { + let state = useContext(RangeCalendarStateContext)!; + let {getDatesInWeek} = state; + let {weekIndex, renderProps} = props; + let ref = useRef(null); + let {isUnavailable, formattedDate} = renderProps; + let datesInWeek = getDatesInWeek(weekIndex); + let firstSelectedInWeek = datesInWeek.findIndex(date => date && state.isSelected(date)); + let indexOfCurrentDate = datesInWeek.findIndex(date => date && date.compare(props.date) === 0); + + let isBackgroundStyleApplied = ( + renderProps.isSelected + && firstSelectedInWeek !== -1 + && indexOfCurrentDate !== -1 + && (state.isSelected(props.date.subtract({days: 1})) + || state.isSelected(props.date.add({days: 1})) + )); + + return ( +
+
+
+ {formattedDate} +
+ {isUnavailable &&
} +
+ {isBackgroundStyleApplied &&
} +
+ ); +}; \ No newline at end of file From 90332c7d6e5d171ef306aa440904c8b39cd57f5d Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Thu, 26 Jun 2025 15:19:17 +1000 Subject: [PATCH 17/33] fix non-contiguous ranges styles --- .../@react-spectrum/s2/src/RangeCalendar.tsx | 28 +++++++++------- .../s2/stories/RangeCalendar.stories.tsx | 32 +++++++++++++++---- 2 files changed, 42 insertions(+), 18 deletions(-) diff --git a/packages/@react-spectrum/s2/src/RangeCalendar.tsx b/packages/@react-spectrum/s2/src/RangeCalendar.tsx index 7d384edcad4..4dc2b4fe18b 100644 --- a/packages/@react-spectrum/s2/src/RangeCalendar.tsx +++ b/packages/@react-spectrum/s2/src/RangeCalendar.tsx @@ -228,9 +228,9 @@ export const RangeCalendar: )} - {(date, weekIndex) => { + {(date, weekIndex, dayIndex) => { return ( - + ); }} @@ -306,7 +306,7 @@ const CalendarButton = (props: Omit & {children: ReactN ); }; -const CalendarCell = (props: Omit & {weekIndex: number}) => { +const CalendarCell = (props: Omit & {weekIndex: number, dayIndex: number}) => { return ( & {weekIndex: n ); }; -const CalendarCellInner = (props: Omit & {weekIndex: number, renderProps: CalendarCellRenderProps, date: DateValue}) => { +const CalendarCellInner = (props: Omit & {weekIndex: number, dayIndex: number, renderProps: CalendarCellRenderProps, date: DateValue}) => { let state = useContext(RangeCalendarStateContext)!; let {getDatesInWeek} = state; - let {weekIndex, renderProps} = props; + let {weekIndex, dayIndex, renderProps} = props; let ref = useRef(null); let {isUnavailable, formattedDate} = renderProps; let datesInWeek = getDatesInWeek(weekIndex); - let firstSelectedInWeek = datesInWeek.findIndex(date => date && state.isSelected(date)); - let indexOfCurrentDate = datesInWeek.findIndex(date => date && date.compare(props.date) === 0); + + // Starting from the current day, find the first day before it in the current week that is not selected. + // Then, the span of selected days is the current day minus the first unselected day. + let firstUnselectedInRangeInWeek = datesInWeek.slice(0, dayIndex + 1).reverse().findIndex((date, i) => date && i > 0 && !state.isSelected(date)); + let selectionSpan = -1; + if (firstUnselectedInRangeInWeek > -1 && renderProps.isSelected) { + selectionSpan = firstUnselectedInRangeInWeek - 1; + } else if (renderProps.isSelected) { + selectionSpan = dayIndex; + } let isBackgroundStyleApplied = ( renderProps.isSelected - && firstSelectedInWeek !== -1 - && indexOfCurrentDate !== -1 && (state.isSelected(props.date.subtract({days: 1})) || state.isSelected(props.date.add({days: 1})) )); @@ -357,7 +363,7 @@ const CalendarCellInner = (props: Omit & {weekInd
{isUnavailable &&
}
- {isBackgroundStyleApplied &&
} + {isBackgroundStyleApplied &&
}
); -}; \ No newline at end of file +}; diff --git a/packages/@react-spectrum/s2/stories/RangeCalendar.stories.tsx b/packages/@react-spectrum/s2/stories/RangeCalendar.stories.tsx index e3370462303..dc1578b4718 100644 --- a/packages/@react-spectrum/s2/stories/RangeCalendar.stories.tsx +++ b/packages/@react-spectrum/s2/stories/RangeCalendar.stories.tsx @@ -11,15 +11,16 @@ */ import {ActionButton, RangeCalendar, RangeCalendarProps} from '../src'; -import {CalendarDate, getLocalTimeZone, today} from '@internationalized/date'; +import {CalendarDate, getLocalTimeZone, isWeekend, today} from '@internationalized/date'; import {categorizeArgTypes} from './utils'; import {Custom454Calendar} from '../../../@internationalized/date/tests/customCalendarImpl'; import {DateValue} from 'react-aria'; import type {Meta, StoryObj} from '@storybook/react'; import {ReactElement, useState} from 'react'; import {style} from '../style' with {type: 'macro'}; +import { useLocale } from '@react-aria/i18n'; -const meta: Meta = { +const meta: Meta> = { component: RangeCalendar, parameters: { layout: 'centered' @@ -27,15 +28,17 @@ const meta: Meta = { tags: ['autodocs'], argTypes: { ...categorizeArgTypes('Events', ['onChange']), - label: {control: {type: 'text'}}, - description: {control: {type: 'text'}}, - errorMessage: {control: {type: 'text'}}, - contextualHelp: {table: {disable: true}}, visibleMonths: { control: { type: 'select' }, options: [1, 2, 3] + }, + firstDayOfWeek: { + control: { + type: 'select' + }, + options: ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] } }, title: 'RangeCalendar' @@ -56,7 +59,22 @@ export const DateUnavailable: Story = { const disabledIntervals = [[today(getLocalTimeZone()).subtract({days: 13}), today(getLocalTimeZone()), today(getLocalTimeZone()).add({weeks: 1})], [today(getLocalTimeZone()).add({weeks: 2}), today(getLocalTimeZone()).add({weeks: 3})]]; return disabledIntervals.some((interval) => date.compare(interval[0]) > 0 && date.compare(interval[1]) < 0); }, - 'aria-label': 'Reservation' + 'aria-label': 'Reservation', + allowsNonContiguousRanges: true + } +}; + +export const WeekendsUnavailable: Story = { + render: function UnavailableWeekendsRender(args) { + let {locale} = useLocale(); + + return ( + isWeekend(date, locale)} /> + ); + }, + args: { + 'aria-label': 'Reservation', + allowsNonContiguousRanges: true } }; From 6fbf92212f9adbdd1fe3705787845ef4b902c2a6 Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Thu, 26 Jun 2025 15:24:32 +1000 Subject: [PATCH 18/33] fix lint --- packages/@react-spectrum/s2/stories/RangeCalendar.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@react-spectrum/s2/stories/RangeCalendar.stories.tsx b/packages/@react-spectrum/s2/stories/RangeCalendar.stories.tsx index dc1578b4718..d08bb7cfcd7 100644 --- a/packages/@react-spectrum/s2/stories/RangeCalendar.stories.tsx +++ b/packages/@react-spectrum/s2/stories/RangeCalendar.stories.tsx @@ -18,7 +18,7 @@ import {DateValue} from 'react-aria'; import type {Meta, StoryObj} from '@storybook/react'; import {ReactElement, useState} from 'react'; import {style} from '../style' with {type: 'macro'}; -import { useLocale } from '@react-aria/i18n'; +import {useLocale} from '@react-aria/i18n'; const meta: Meta> = { component: RangeCalendar, From a82e0478c37e2657b7ee1cf03907481b62d87094 Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Fri, 27 Jun 2025 08:25:51 +1000 Subject: [PATCH 19/33] remove explicit modules --- packages/@react-spectrum/s2/src/Calendar.tsx | 10 +++------- packages/@react-spectrum/s2/src/DateField.tsx | 10 +++------- packages/@react-spectrum/s2/src/DatePicker.tsx | 10 +++------- packages/@react-spectrum/s2/src/DateRangePicker.tsx | 10 +++------- packages/@react-spectrum/s2/src/RangeCalendar.tsx | 10 +++------- packages/@react-spectrum/s2/src/TimeField.tsx | 10 +++------- 6 files changed, 18 insertions(+), 42 deletions(-) diff --git a/packages/@react-spectrum/s2/src/Calendar.tsx b/packages/@react-spectrum/s2/src/Calendar.tsx index 2a072c2af70..43fe29aee76 100644 --- a/packages/@react-spectrum/s2/src/Calendar.tsx +++ b/packages/@react-spectrum/s2/src/Calendar.tsx @@ -30,7 +30,7 @@ import { import {baseColor, focusRing, lightDark, style} from '../style' with {type: 'macro'}; import ChevronLeftIcon from '../s2wf-icons/S2_Icon_ChevronLeft_20_N.svg'; import ChevronRightIcon from '../s2wf-icons/S2_Icon_ChevronRight_20_N.svg'; -import {Context, createContext, ForwardedRef, forwardRef, Fragment, ReactElement, ReactNode, RefAttributes, useContext, useMemo, useRef} from 'react'; +import {createContext, ForwardedRef, forwardRef, Fragment, ReactNode, useContext, useMemo, useRef} from 'react'; import {forwardRefType} from '@react-types/shared'; import {getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; import {getEraFormat} from '@react-aria/calendar'; @@ -45,9 +45,7 @@ export interface CalendarProps visibleMonths?: number } -export const CalendarContext: - Context>, HTMLDivElement>> = - createContext>, HTMLDivElement>>(null); +export const CalendarContext = createContext>, HTMLDivElement>>(null); const calendarStyles = style({ display: 'flex', @@ -148,9 +146,7 @@ const unavailableStyles = style({ }); -export const Calendar: - (props: CalendarProps & RefAttributes) => ReactElement | null = -/*#__PURE__*/ (forwardRef as forwardRefType)(function Calendar(props: CalendarProps, ref: ForwardedRef) { +export const Calendar = /*#__PURE__*/ (forwardRef as forwardRefType)(function Calendar(props: CalendarProps, ref: ForwardedRef) { [props, ref] = useSpectrumContextProps(props, ref, CalendarContext); let { visibleMonths = 1, diff --git a/packages/@react-spectrum/s2/src/DateField.tsx b/packages/@react-spectrum/s2/src/DateField.tsx index 5ac21111749..c61dc4165cc 100644 --- a/packages/@react-spectrum/s2/src/DateField.tsx +++ b/packages/@react-spectrum/s2/src/DateField.tsx @@ -19,7 +19,7 @@ import { DateValue, FormContext } from 'react-aria-components'; -import {Context, createContext, forwardRef, ReactElement, Ref, RefAttributes, useContext} from 'react'; +import {createContext, forwardRef, ReactElement, Ref, useContext} from 'react'; import {field, fieldInput, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; import {FieldErrorIcon, FieldGroup, FieldLabel, HelpText} from './Field'; import {forwardRefType, HelpTextProps, SpectrumLabelableProps} from '@react-types/shared'; @@ -40,9 +40,7 @@ export interface DateFieldProps extends size?: 'S' | 'M' | 'L' | 'XL' } -export const DateFieldContext: - Context>, HTMLDivElement>> = - createContext>, HTMLDivElement>>(null); +export const DateFieldContext = createContext>, HTMLDivElement>>(null); const segmentContainer = style({ flexGrow: 1 @@ -68,9 +66,7 @@ const iconStyles = style({ justifyContent: 'end' }); -export const DateField: - (props: DateFieldProps & RefAttributes) => ReactElement | null = -/*#__PURE__*/ (forwardRef as forwardRefType)(function DateField( +export const DateField = /*#__PURE__*/ (forwardRef as forwardRefType)(function DateField( props: DateFieldProps, ref: Ref ): ReactElement { [props, ref] = useSpectrumContextProps(props, ref, DateFieldContext); diff --git a/packages/@react-spectrum/s2/src/DatePicker.tsx b/packages/@react-spectrum/s2/src/DatePicker.tsx index d189b24b0b9..b5d7079fea3 100644 --- a/packages/@react-spectrum/s2/src/DatePicker.tsx +++ b/packages/@react-spectrum/s2/src/DatePicker.tsx @@ -25,8 +25,8 @@ import { import {baseColor, focusRing, fontRelative, style} from '../style' with {type: 'macro'}; import {Calendar, IconContext} from '../'; import CalendarIcon from '../s2wf-icons/S2_Icon_Calendar_20_N.svg'; -import {Context, createContext, forwardRef, ReactElement, Ref, RefAttributes, useContext, useRef, useState} from 'react'; import {controlBorderRadius, field, fieldInput, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; +import {createContext, forwardRef, ReactElement, Ref, useContext, useRef, useState} from 'react'; import {FieldErrorIcon, FieldGroup, FieldLabel, HelpText} from './Field'; import {forwardRefType, HelpTextProps, SpectrumLabelableProps} from '@react-types/shared'; import {PopoverBase} from './Popover'; @@ -47,9 +47,7 @@ export interface DatePickerProps extends size?: 'S' | 'M' | 'L' | 'XL' } -export const DatePickerContext: - Context>, HTMLDivElement>> = - createContext>, HTMLDivElement>>(null); +export const DatePickerContext = createContext>, HTMLDivElement>>(null); const segmentContainer = style({ flexGrow: 1 @@ -118,9 +116,7 @@ const inputButton = style(props: DatePickerProps & RefAttributes) => ReactElement | null = -/*#__PURE__*/ (forwardRef as forwardRefType)(function DatePicker( +export const DatePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(function DatePicker( props: DatePickerProps, ref: Ref ): ReactElement { [props, ref] = useSpectrumContextProps(props, ref, DatePickerContext); diff --git a/packages/@react-spectrum/s2/src/DateRangePicker.tsx b/packages/@react-spectrum/s2/src/DateRangePicker.tsx index 959122865d0..a5ac4caf308 100644 --- a/packages/@react-spectrum/s2/src/DateRangePicker.tsx +++ b/packages/@react-spectrum/s2/src/DateRangePicker.tsx @@ -24,8 +24,8 @@ import { } from 'react-aria-components'; import {baseColor, focusRing, fontRelative, style} from '../style' with {type: 'macro'}; import CalendarIcon from '../s2wf-icons/S2_Icon_Calendar_20_N.svg'; -import {Context, createContext, forwardRef, ReactElement, Ref, RefAttributes, useContext, useRef, useState} from 'react'; import {controlBorderRadius, field, fieldInput, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; +import {createContext, forwardRef, ReactElement, Ref, useContext, useRef, useState} from 'react'; import {FieldErrorIcon, FieldGroup, FieldLabel, HelpText} from './Field'; import {forwardRefType, HelpTextProps, SpectrumLabelableProps} from '@react-types/shared'; import {IconContext, RangeCalendar} from '../'; @@ -47,9 +47,7 @@ export interface DateRangePickerProps extends size?: 'S' | 'M' | 'L' | 'XL' } -export const DateRangePickerContext: - Context>, HTMLDivElement>> = - createContext>, HTMLDivElement>>(null); +export const DateRangePickerContext = createContext>, HTMLDivElement>>(null); const segmentContainer = style({ flexGrow: 0, @@ -118,9 +116,7 @@ const inputButton = style(props: DateRangePickerProps & RefAttributes) => ReactElement | null = -/*#__PURE__*/ (forwardRef as forwardRefType)(function DateRangePicker( +export const DateRangePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(function DateRangePicker( props: DateRangePickerProps, ref: Ref ): ReactElement { [props, ref] = useSpectrumContextProps(props, ref, DateRangePickerContext); diff --git a/packages/@react-spectrum/s2/src/RangeCalendar.tsx b/packages/@react-spectrum/s2/src/RangeCalendar.tsx index 4dc2b4fe18b..555192889b8 100644 --- a/packages/@react-spectrum/s2/src/RangeCalendar.tsx +++ b/packages/@react-spectrum/s2/src/RangeCalendar.tsx @@ -30,7 +30,7 @@ import { import {baseColor, focusRing, lightDark, style} from '../style' with {type: 'macro'}; import ChevronLeftIcon from '../s2wf-icons/S2_Icon_ChevronLeft_20_N.svg'; import ChevronRightIcon from '../s2wf-icons/S2_Icon_ChevronRight_20_N.svg'; -import {Context, createContext, CSSProperties, ForwardedRef, forwardRef, Fragment, ReactElement, ReactNode, RefAttributes, useContext, useMemo, useRef} from 'react'; +import {createContext, CSSProperties, ForwardedRef, forwardRef, Fragment, ReactNode, useContext, useMemo, useRef} from 'react'; import {forwardRefType} from '@react-types/shared'; import {getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; import {getEraFormat} from '@react-aria/calendar'; @@ -45,9 +45,7 @@ export interface RangeCalendarProps visibleMonths?: number } -export const RangeCalendarContext: - Context>, HTMLDivElement>> = - createContext>, HTMLDivElement>>(null); +export const RangeCalendarContext = createContext>, HTMLDivElement>>(null); const calendarStyles = style({ @@ -185,9 +183,7 @@ const selectionSpanStyles = style({ backgroundColor: 'blue-subtle' }); -export const RangeCalendar: - (props: RangeCalendarProps & RefAttributes) => ReactElement | null = -/*#__PURE__*/ (forwardRef as forwardRefType)(function RangeCalendar(props: RangeCalendarProps, ref: ForwardedRef) { +export const RangeCalendar = /*#__PURE__*/ (forwardRef as forwardRefType)(function RangeCalendar(props: RangeCalendarProps, ref: ForwardedRef) { [props, ref] = useSpectrumContextProps(props, ref, RangeCalendarContext); let { visibleMonths = 1, diff --git a/packages/@react-spectrum/s2/src/TimeField.tsx b/packages/@react-spectrum/s2/src/TimeField.tsx index 48fd93028bf..0a3d63be8f0 100644 --- a/packages/@react-spectrum/s2/src/TimeField.tsx +++ b/packages/@react-spectrum/s2/src/TimeField.tsx @@ -19,7 +19,7 @@ import { FormContext, TimeValue } from 'react-aria-components'; -import {Context, createContext, forwardRef, ReactElement, Ref, RefAttributes, useContext} from 'react'; +import {createContext, forwardRef, ReactElement, Ref, useContext} from 'react'; import {field, fieldInput, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; import {FieldErrorIcon, FieldGroup, FieldLabel, HelpText} from './Field'; import {forwardRefType, HelpTextProps, SpectrumLabelableProps} from '@react-types/shared'; @@ -40,9 +40,7 @@ export interface TimeFieldProps extends size?: 'S' | 'M' | 'L' | 'XL' } -export const TimeFieldContext: - Context>, HTMLDivElement>> = - createContext>, HTMLDivElement>>(null); +export const TimeFieldContext = createContext>, HTMLDivElement>>(null); const segmentContainer = style({ flexGrow: 1 @@ -69,9 +67,7 @@ const iconStyles = style({ justifyContent: 'end' }); -export const TimeField: - (props: TimeFieldProps & RefAttributes) => ReactElement | null = -/*#__PURE__*/ (forwardRef as forwardRefType)(function TimeField( +export const TimeField = /*#__PURE__*/ (forwardRef as forwardRefType)(function TimeField( props: TimeFieldProps, ref: Ref ): ReactElement { [props, ref] = useSpectrumContextProps(props, ref, TimeFieldContext); From 356148a204bb04bd94a828ea6e5932dda402e28f Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Fri, 27 Jun 2025 16:03:01 +1000 Subject: [PATCH 20/33] Add time fields to date pickers --- packages/@react-spectrum/s2/intl/en-US.json | 5 +- packages/@react-spectrum/s2/src/Calendar.tsx | 102 ++++++++++------ .../@react-spectrum/s2/src/DatePicker.tsx | 44 ++++++- .../s2/src/DateRangePicker.tsx | 81 ++++++++++--- .../@react-spectrum/s2/src/RangeCalendar.tsx | 110 ++++++++++++------ 5 files changed, 248 insertions(+), 94 deletions(-) diff --git a/packages/@react-spectrum/s2/intl/en-US.json b/packages/@react-spectrum/s2/intl/en-US.json index 6f12fab4668..20d6593b516 100644 --- a/packages/@react-spectrum/s2/intl/en-US.json +++ b/packages/@react-spectrum/s2/intl/en-US.json @@ -35,5 +35,8 @@ "breadcrumbs.more": "More items", "toast.clearAll": "Clear all", "toast.collapse": "Collapse", - "toast.showAll": "Show all" + "toast.showAll": "Show all", + "rangeCalendar.startTime": "Start time", + "rangeCalendar.endTime": "End time", + "calendar.time": "Time" } diff --git a/packages/@react-spectrum/s2/src/Calendar.tsx b/packages/@react-spectrum/s2/src/Calendar.tsx index 43fe29aee76..ab8094f04b7 100644 --- a/packages/@react-spectrum/s2/src/Calendar.tsx +++ b/packages/@react-spectrum/s2/src/Calendar.tsx @@ -30,9 +30,9 @@ import { import {baseColor, focusRing, lightDark, style} from '../style' with {type: 'macro'}; import ChevronLeftIcon from '../s2wf-icons/S2_Icon_ChevronLeft_20_N.svg'; import ChevronRightIcon from '../s2wf-icons/S2_Icon_ChevronRight_20_N.svg'; +import {controlFont, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; import {createContext, ForwardedRef, forwardRef, Fragment, ReactNode, useContext, useMemo, useRef} from 'react'; -import {forwardRefType} from '@react-types/shared'; -import {getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; +import {forwardRefType, ValidationResult} from '@react-types/shared'; import {getEraFormat} from '@react-aria/calendar'; import {useDateFormatter} from '@react-aria/i18n'; import {useSpectrumContextProps} from './useSpectrumContextProps'; @@ -41,7 +41,7 @@ import {useSpectrumContextProps} from './useSpectrumContextProps'; export interface CalendarProps extends Omit, 'visibleDuration' | 'style' | 'className' | 'styles'>, StyleProps { - errorMessage?: string, + errorMessage?: ReactNode | ((v: ValidationResult) => ReactNode), visibleMonths?: number } @@ -51,7 +51,7 @@ const calendarStyles = style({ display: 'flex', flexDirection: 'column', gap: 24, - width: 'full' + width: 'fit' }, getAllowedOverrides()); const headerStyles = style({ @@ -145,6 +145,29 @@ const unavailableStyles = style({ backgroundColor: '[currentColor]' }); +export const helpTextStyles = style({ + gridArea: 'helptext', + display: 'flex', + alignItems: 'baseline', + gap: 'text-to-visual', + font: controlFont(), + color: { + default: 'neutral-subdued', + isInvalid: 'negative', + isDisabled: 'disabled' + }, + '--iconPrimary': { + type: 'fill', + value: 'currentColor' + }, + contain: 'inline-size', + paddingTop: '--field-gap', + cursor: { + default: 'text', + isDisabled: 'default' + } +}); + export const Calendar = /*#__PURE__*/ (forwardRef as forwardRefType)(function Calendar(props: CalendarProps, ref: ForwardedRef) { [props, ref] = useSpectrumContextProps(props, ref, CalendarContext); @@ -163,36 +186,47 @@ export const Calendar = /*#__PURE__*/ (forwardRef as forwardRefType)(function Ca visibleDuration={{months: visibleMonths}} style={UNSAFE_style} className={(UNSAFE_className || '') + calendarStyles(null, styles)}> -
- - - -
-
- {Array.from({length: visibleMonths}).map((_, i) => ( - - - {(day) => ( - - {day} - - )} - - - {(date) => ( - - )} - - - ))} -
- {errorMessage && {errorMessage}} + {({isInvalid, isDisabled}) => { + return ( + <> +
+ + + +
+
+ {Array.from({length: visibleMonths}).map((_, i) => ( + + + {(day) => ( + + {day} + + )} + + + {(date) => ( + + )} + + + ))} +
+ {errorMessage && ( + + {/* @ts-ignore */} + {errorMessage} + + )} + + ); + }} ); }); diff --git a/packages/@react-spectrum/s2/src/DatePicker.tsx b/packages/@react-spectrum/s2/src/DatePicker.tsx index b5d7079fea3..9ed83f4ac37 100644 --- a/packages/@react-spectrum/s2/src/DatePicker.tsx +++ b/packages/@react-spectrum/s2/src/DatePicker.tsx @@ -20,17 +20,21 @@ import { DateSegment, DateValue, FormContext, - Provider + Provider, + TimeValue } from 'react-aria-components'; import {baseColor, focusRing, fontRelative, style} from '../style' with {type: 'macro'}; -import {Calendar, IconContext} from '../'; +import {Calendar, IconContext, TimeField} from '../'; import CalendarIcon from '../s2wf-icons/S2_Icon_Calendar_20_N.svg'; import {controlBorderRadius, field, fieldInput, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; import {createContext, forwardRef, ReactElement, Ref, useContext, useRef, useState} from 'react'; import {FieldErrorIcon, FieldGroup, FieldLabel, HelpText} from './Field'; import {forwardRefType, HelpTextProps, SpectrumLabelableProps} from '@react-types/shared'; +// @ts-ignore +import intlMessages from '../intl/*.json'; import {PopoverBase} from './Popover'; import {pressScale} from './pressScale'; +import {useLocalizedStringFormatter} from '@react-aria/i18n'; import {useSpectrumContextProps} from './useSpectrumContextProps'; @@ -50,7 +54,9 @@ export interface DatePickerProps extends export const DatePickerContext = createContext>, HTMLDivElement>>(null); const segmentContainer = style({ - flexGrow: 1 + flexGrow: 1, + flexShrink: 1, + overflow: 'hidden' }); const dateInput = style({ @@ -120,6 +126,7 @@ export const DatePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(function props: DatePickerProps, ref: Ref ): ReactElement { [props, ref] = useSpectrumContextProps(props, ref, DatePickerContext); + let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/s2'); let { label, contextualHelp, @@ -133,6 +140,7 @@ export const DatePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(function UNSAFE_style, UNSAFE_className, styles, + placeholderValue, ...dateFieldProps } = props; let formContext = useContext(FormContext); @@ -150,7 +158,13 @@ export const DatePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(function labelPosition, size }, styles)}> - {({isDisabled, isInvalid, isOpen}) => { + {({isDisabled, isInvalid, isOpen, state}) => { + let placeholder: DateValue | undefined = placeholderValue ?? undefined; + let timePlaceholder = placeholder && 'hour' in placeholder ? placeholder : undefined; + let timeMinValue = props.minValue && 'hour' in props.minValue ? props.minValue : undefined; + let timeMaxValue = props.maxValue && 'hour' in props.maxValue ? props.maxValue : undefined; + let timeGranularity = state.granularity === 'hour' || state.granularity === 'minute' || state.granularity === 'second' ? state.granularity : undefined; + let showTimeField = !!timeGranularity; return ( <> {(segment) => } @@ -202,7 +218,23 @@ export const DatePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(function - +
+ + {showTimeField && ( + state.setTimeValue(v as TimeValue)} + placeholderValue={timePlaceholder} + granularity={timeGranularity} + minValue={timeMinValue} + maxValue={timeMaxValue} + hourCycle={props.hourCycle} + hideTimeZone={props.hideTimeZone} /> + )} +
extends export const DateRangePickerContext = createContext>, HTMLDivElement>>(null); const segmentContainer = style({ + flexGrow: 0, + flexShrink: 1, + overflow: 'hidden', + textWrap: 'nowrap', + display: 'flex', + flexWrap: 'nowrap' +}); +const segment = style({ flexGrow: 0, flexShrink: 0 }); @@ -120,6 +131,7 @@ export const DateRangePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(func props: DateRangePickerProps, ref: Ref ): ReactElement { [props, ref] = useSpectrumContextProps(props, ref, DateRangePickerContext); + let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/s2'); let { label, contextualHelp, @@ -133,13 +145,14 @@ export const DateRangePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(func UNSAFE_style, UNSAFE_className, styles, + placeholderValue, ...dateFieldProps } = props; let formContext = useContext(FormContext); let buttonRef = useRef(null); let [buttonHasFocus, setButtonHasFocus] = useState(false); - // TODO: fix width + // TODO: fix width? default min width? return ( - {({isDisabled, isInvalid, isOpen}) => { + {({isDisabled, isInvalid, isOpen, state}) => { + let placeholder: DateValue | undefined = placeholderValue || undefined; + let timePlaceholder = placeholder && 'hour' in placeholder ? placeholder : undefined; + let timeMinValue = props.minValue && 'hour' in props.minValue ? props.minValue : undefined; + let timeMaxValue = props.maxValue && 'hour' in props.maxValue ? props.maxValue : undefined; + let timeGranularity = state.granularity === 'hour' + || state.granularity === 'minute' + || state.granularity === 'second' + ? state.granularity : undefined; + let showTimeField = !!timeGranularity; return ( <> - - {(segment) => } - - - - {(segment) => } - +
+ + {(segment) => } + + + + {(segment) => } + +
{isInvalid &&
}
- +
+ + {showTimeField && ( +
+ state.setTime('start', v)} + placeholderValue={timePlaceholder} + granularity={timeGranularity} + minValue={timeMinValue} + maxValue={timeMaxValue} + hourCycle={props.hourCycle} + hideTimeZone={props.hideTimeZone} /> + state.setTime('end', v)} + placeholderValue={timePlaceholder} + granularity={timeGranularity} + minValue={timeMinValue} + maxValue={timeMaxValue} + hourCycle={props.hourCycle} + hideTimeZone={props.hideTimeZone} /> +
+ )} +
extends Omit, 'visibleDuration' | 'style' | 'className' | 'styles'>, StyleProps { - errorMessage?: string, + errorMessage?: ReactNode | ((v: ValidationResult) => ReactNode), visibleMonths?: number } @@ -52,7 +52,7 @@ const calendarStyles = style({ display: 'flex', flexDirection: 'column', gap: 24, - width: 'full' + width: 'fit' }, getAllowedOverrides()); const headerStyles = style({ @@ -183,6 +183,29 @@ const selectionSpanStyles = style({ backgroundColor: 'blue-subtle' }); +export const helpTextStyles = style({ + gridArea: 'helptext', + display: 'flex', + alignItems: 'baseline', + gap: 'text-to-visual', + font: controlFont(), + color: { + default: 'neutral-subdued', + isInvalid: 'negative', + isDisabled: 'disabled' + }, + '--iconPrimary': { + type: 'fill', + value: 'currentColor' + }, + contain: 'inline-size', + paddingTop: '--field-gap', + cursor: { + default: 'text', + isDisabled: 'default' + } +}); + export const RangeCalendar = /*#__PURE__*/ (forwardRef as forwardRefType)(function RangeCalendar(props: RangeCalendarProps, ref: ForwardedRef) { [props, ref] = useSpectrumContextProps(props, ref, RangeCalendarContext); let { @@ -201,40 +224,51 @@ export const RangeCalendar = /*#__PURE__*/ (forwardRef as forwardRefType)(functi visibleDuration={{months: visibleMonths}} style={UNSAFE_style} className={(UNSAFE_className || '') + calendarStyles(null, styles)}> -
- - - -
-
- {Array.from({length: visibleMonths}).map((_, i) => { - return ( - - - {(day) => ( - - {day} - - )} - - - {(date, weekIndex, dayIndex) => { - return ( - - ); - }} - - - ); - })} -
- {errorMessage && {errorMessage}} + {({isInvalid, isDisabled}) => { + return ( + <> +
+ + + +
+
+ {Array.from({length: visibleMonths}).map((_, i) => { + return ( + + + {(day) => ( + + {day} + + )} + + + {(date, weekIndex, dayIndex) => { + return ( + + ); + }} + + + ); + })} +
+ {errorMessage && ( + + {/* @ts-ignore */} + {errorMessage} + + )} + + ); + }} ); }); From b0f6cac1a613d96c632c48bbda1b97f913b084f8 Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Fri, 27 Jun 2025 16:16:13 +1000 Subject: [PATCH 21/33] fix buttons in date picker popover --- packages/@react-spectrum/s2/src/ActionButton.tsx | 2 +- packages/@react-spectrum/s2/stories/Calendar.stories.tsx | 3 --- .../@react-spectrum/s2/stories/RangeCalendar.stories.tsx | 6 ------ packages/react-aria-components/src/DatePicker.tsx | 2 +- 4 files changed, 2 insertions(+), 11 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ActionButton.tsx b/packages/@react-spectrum/s2/src/ActionButton.tsx index 9f817c9827c..c960e03ce71 100644 --- a/packages/@react-spectrum/s2/src/ActionButton.tsx +++ b/packages/@react-spectrum/s2/src/ActionButton.tsx @@ -266,7 +266,7 @@ export const ActionButton = forwardRef(function ActionButton(props: ActionButton isDisabled = props.isDisabled } = ctx || {}; - + console.log('props', props.isQuiet); return ( = { tags: ['autodocs'], argTypes: { ...categorizeArgTypes('Events', ['onChange']), - label: {control: {type: 'text'}}, - description: {control: {type: 'text'}}, errorMessage: {control: {type: 'text'}}, - contextualHelp: {table: {disable: true}}, visibleMonths: { control: { type: 'select' diff --git a/packages/@react-spectrum/s2/stories/RangeCalendar.stories.tsx b/packages/@react-spectrum/s2/stories/RangeCalendar.stories.tsx index d08bb7cfcd7..e522792e29f 100644 --- a/packages/@react-spectrum/s2/stories/RangeCalendar.stories.tsx +++ b/packages/@react-spectrum/s2/stories/RangeCalendar.stories.tsx @@ -33,12 +33,6 @@ const meta: Meta> = { type: 'select' }, options: [1, 2, 3] - }, - firstDayOfWeek: { - control: { - type: 'select' - }, - options: ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] } }, title: 'RangeCalendar' diff --git a/packages/react-aria-components/src/DatePicker.tsx b/packages/react-aria-components/src/DatePicker.tsx index 55e03a122bf..e239d29c3a0 100644 --- a/packages/react-aria-components/src/DatePicker.tsx +++ b/packages/react-aria-components/src/DatePicker.tsx @@ -73,7 +73,7 @@ export const DatePickerStateContext = createContext(null export const DateRangePickerStateContext = createContext(null); // Contexts to clear inside the popover. -const CLEAR_CONTEXTS = [GroupContext, ButtonContext, LabelContext, TextContext]; +const CLEAR_CONTEXTS = [GroupContext, ButtonContext, LabelContext, TextContext, OverlayTriggerStateContext]; /** * A date picker combines a DateField and a Calendar popover to allow users to enter or select a date and time value. From eb9685ad641b356c770a315f35f885fea67859eb Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Fri, 27 Jun 2025 16:17:23 +1000 Subject: [PATCH 22/33] remove console log --- packages/@react-spectrum/s2/src/ActionButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@react-spectrum/s2/src/ActionButton.tsx b/packages/@react-spectrum/s2/src/ActionButton.tsx index c960e03ce71..9f817c9827c 100644 --- a/packages/@react-spectrum/s2/src/ActionButton.tsx +++ b/packages/@react-spectrum/s2/src/ActionButton.tsx @@ -266,7 +266,7 @@ export const ActionButton = forwardRef(function ActionButton(props: ActionButton isDisabled = props.isDisabled } = ctx || {}; - console.log('props', props.isQuiet); + return ( Date: Fri, 27 Jun 2025 16:25:28 +1000 Subject: [PATCH 23/33] fix storybook intermittent crash from implicit actions, scrolling --- packages/@react-spectrum/s2/src/DatePicker.tsx | 2 +- packages/@react-spectrum/s2/src/DateRangePicker.tsx | 2 +- packages/@react-spectrum/s2/stories/DatePicker.stories.tsx | 5 +++++ .../@react-spectrum/s2/stories/DateRangePicker.stories.tsx | 7 ++++++- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/@react-spectrum/s2/src/DatePicker.tsx b/packages/@react-spectrum/s2/src/DatePicker.tsx index 9ed83f4ac37..91671efb899 100644 --- a/packages/@react-spectrum/s2/src/DatePicker.tsx +++ b/packages/@react-spectrum/s2/src/DatePicker.tsx @@ -217,7 +217,7 @@ export const DatePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(function + styles={style({paddingX: 16, paddingY: 32, overflow: 'auto'})}>
{showTimeField && ( diff --git a/packages/@react-spectrum/s2/src/DateRangePicker.tsx b/packages/@react-spectrum/s2/src/DateRangePicker.tsx index ac58bb9353a..2c58d57cdce 100644 --- a/packages/@react-spectrum/s2/src/DateRangePicker.tsx +++ b/packages/@react-spectrum/s2/src/DateRangePicker.tsx @@ -240,7 +240,7 @@ export const DateRangePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(func + styles={style({paddingX: 16, paddingY: 32, overflow: 'auto'})}>
{showTimeField && ( diff --git a/packages/@react-spectrum/s2/stories/DatePicker.stories.tsx b/packages/@react-spectrum/s2/stories/DatePicker.stories.tsx index 3779043c2c0..5c8bc71f04d 100644 --- a/packages/@react-spectrum/s2/stories/DatePicker.stories.tsx +++ b/packages/@react-spectrum/s2/stories/DatePicker.stories.tsx @@ -12,6 +12,7 @@ import {Button, Content, ContextualHelp, DatePicker, Footer, Form, Heading, Link, Text} from '../src'; import {categorizeArgTypes} from './utils'; +import {fn} from '@storybook/test'; import type {Meta, StoryObj} from '@storybook/react'; import {style} from '../style' with {type: 'macro'}; @@ -28,6 +29,10 @@ const meta: Meta = { errorMessage: {control: {type: 'text'}}, contextualHelp: {table: {disable: true}} }, + args: { + onOpenChange: fn(), + onChange: fn() + }, title: 'DatePicker' }; diff --git a/packages/@react-spectrum/s2/stories/DateRangePicker.stories.tsx b/packages/@react-spectrum/s2/stories/DateRangePicker.stories.tsx index e5266153158..8757558f0ad 100644 --- a/packages/@react-spectrum/s2/stories/DateRangePicker.stories.tsx +++ b/packages/@react-spectrum/s2/stories/DateRangePicker.stories.tsx @@ -12,6 +12,7 @@ import {Button, Content, ContextualHelp, DateRangePicker, Footer, Form, Heading, Link, Text} from '../src'; import {categorizeArgTypes} from './utils'; +import {fn} from '@storybook/test'; import type {Meta, StoryObj} from '@storybook/react'; import {style} from '../style' with {type: 'macro'}; @@ -22,12 +23,16 @@ const meta: Meta = { }, tags: ['autodocs'], argTypes: { - ...categorizeArgTypes('Events', ['onChange']), + ...categorizeArgTypes('Events', ['onChange', 'onOpenChange']), label: {control: {type: 'text'}}, description: {control: {type: 'text'}}, errorMessage: {control: {type: 'text'}}, contextualHelp: {table: {disable: true}} }, + args: { + onOpenChange: fn(), + onChange: fn() + }, title: 'DateRangePicker' }; From 5ee804b4891d0f3b22fd39606f47c17b82fb31ce Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Fri, 27 Jun 2025 16:35:22 +1000 Subject: [PATCH 24/33] fix accessibility violation --- .../@react-spectrum/s2/src/DatePicker.tsx | 23 +++++++++++++++---- .../s2/src/DateRangePicker.tsx | 20 ++++++++++++---- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/packages/@react-spectrum/s2/src/DatePicker.tsx b/packages/@react-spectrum/s2/src/DatePicker.tsx index 91671efb899..9a44736661f 100644 --- a/packages/@react-spectrum/s2/src/DatePicker.tsx +++ b/packages/@react-spectrum/s2/src/DatePicker.tsx @@ -21,7 +21,8 @@ import { DateValue, FormContext, Provider, - TimeValue + TimeValue, + DialogContext } from 'react-aria-components'; import {baseColor, focusRing, fontRelative, style} from '../style' with {type: 'macro'}; import {Calendar, IconContext, TimeField} from '../'; @@ -215,9 +216,7 @@ export const DatePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(function - +
{showTimeField && ( @@ -235,7 +234,7 @@ export const DatePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(function hideTimeZone={props.hideTimeZone} /> )}
-
+ + {props.children} + + ); +} + diff --git a/packages/@react-spectrum/s2/src/DateRangePicker.tsx b/packages/@react-spectrum/s2/src/DateRangePicker.tsx index 2c58d57cdce..f2e55d1b901 100644 --- a/packages/@react-spectrum/s2/src/DateRangePicker.tsx +++ b/packages/@react-spectrum/s2/src/DateRangePicker.tsx @@ -19,6 +19,7 @@ import { DateInput, DateSegment, DateValue, + DialogContext, FormContext, Provider } from 'react-aria-components'; @@ -238,9 +239,7 @@ export const DateRangePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(func
- +
{showTimeField && ( @@ -270,7 +269,7 @@ export const DateRangePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(func
)}
-
+ + {props.children} + + ); +} From f0658775ce72ba2cf678e540fb861d633004a48b Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Mon, 30 Jun 2025 19:25:39 +1000 Subject: [PATCH 25/33] review changes and deduplicating code --- packages/@react-spectrum/s2/src/Calendar.tsx | 206 ++++++++++--- packages/@react-spectrum/s2/src/DateField.tsx | 17 +- .../@react-spectrum/s2/src/DatePicker.tsx | 133 +++++---- .../s2/src/DateRangePicker.tsx | 185 +++--------- .../@react-spectrum/s2/src/RangeCalendar.tsx | 277 +----------------- .../s2/stories/DateField.stories.tsx | 6 +- .../react-aria-components/src/Calendar.tsx | 30 +- 7 files changed, 312 insertions(+), 542 deletions(-) diff --git a/packages/@react-spectrum/s2/src/Calendar.tsx b/packages/@react-spectrum/s2/src/Calendar.tsx index ab8094f04b7..d90c726c069 100644 --- a/packages/@react-spectrum/s2/src/Calendar.tsx +++ b/packages/@react-spectrum/s2/src/Calendar.tsx @@ -14,26 +14,28 @@ import {ActionButton, Header, Heading, pressScale} from './'; import { Calendar as AriaCalendar, CalendarCell as AriaCalendarCell, + CalendarGrid as AriaCalendarGrid, + CalendarHeaderCell as AriaCalendarHeaderCell, CalendarProps as AriaCalendarProps, ButtonProps, CalendarCellProps, CalendarCellRenderProps, - CalendarGrid, CalendarGridBody, CalendarGridHeader, - CalendarHeaderCell, + CalendarHeaderCellProps, CalendarStateContext, ContextValue, DateValue, + RangeCalendarStateContext, Text } from 'react-aria-components'; import {baseColor, focusRing, lightDark, style} from '../style' with {type: 'macro'}; import ChevronLeftIcon from '../s2wf-icons/S2_Icon_ChevronLeft_20_N.svg'; import ChevronRightIcon from '../s2wf-icons/S2_Icon_ChevronRight_20_N.svg'; import {controlFont, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; -import {createContext, ForwardedRef, forwardRef, Fragment, ReactNode, useContext, useMemo, useRef} from 'react'; import {forwardRefType, ValidationResult} from '@react-types/shared'; import {getEraFormat} from '@react-aria/calendar'; +import React, {createContext, CSSProperties, ForwardedRef, forwardRef, Fragment, PropsWithChildren, ReactElement, ReactNode, useContext, useMemo, useRef} from 'react'; import {useDateFormatter} from '@react-aria/i18n'; import {useSpectrumContextProps} from './useSpectrumContextProps'; @@ -42,6 +44,10 @@ export interface CalendarProps extends Omit, 'visibleDuration' | 'style' | 'className' | 'styles'>, StyleProps { errorMessage?: ReactNode | ((v: ValidationResult) => ReactNode), + /** + * The number of months to display at once. + * @default 1 + */ visibleMonths?: number } @@ -82,20 +88,50 @@ const headerCellStyles = style({ font: 'title-sm', cursor: 'default', textAlign: 'center', - flexGrow: 1 + paddingStart: { + default: 4, + ':first-child': 0 + }, + paddingEnd: { + default: 4, + ':last-child': 0 + }, + paddingBottom: 12 }); -const cellStyles = style({ - paddingX: 4, +const cellStyles = style({ + outlineStyle: 'none', '--cell-gap': { type: 'paddingStart', value: 4 }, - paddingY: 2 + paddingStart: { + default: 4, + isFirstChild: 0 + }, + paddingEnd: { + default: 4, + isLastChild: 0 + }, + paddingTop: { + default: 2, + isFirstWeek: 0 + }, + paddingBottom: 2, + position: 'relative', + width: 32, + height: 32, + display: { + default: 'flex', + isOutsideMonth: 'none' + }, + alignItems: 'center', + justifyContent: 'center' }); const cellInnerStyles = style({ ...focusRing(), + transition: 'default', outlineOffset: { default: -2, isToday: 2, @@ -104,32 +140,53 @@ const cellInnerStyles = style({ position: 'relative', font: 'body-sm', cursor: 'default', - width: 32, + width: 'full', height: 32, - margin: 2, borderRadius: 'full', - display: { - default: 'flex', - isOutsideMonth: 'none' - }, + display: 'flex', alignItems: 'center', justifyContent: 'center', backgroundColor: { default: 'transparent', isHovered: 'gray-100', + isDisabled: 'transparent', isToday: { default: baseColor('gray-300'), isDisabled: 'disabled' }, isSelected: { + selectionMode: { + single: { + default: lightDark('accent-900', 'accent-700'), + isHovered: lightDark('accent-1000', 'accent-600'), + isPressed: lightDark('accent-1000', 'accent-600'), + isFocusVisible: lightDark('accent-1000', 'accent-600'), + isDisabled: 'transparent' + }, + range: { + isHovered: 'blue-500' + } + } + }, + isSelectionStart: { default: lightDark('accent-900', 'accent-700'), isHovered: lightDark('accent-1000', 'accent-600'), isPressed: lightDark('accent-1000', 'accent-600'), isFocusVisible: lightDark('accent-1000', 'accent-600') - } + }, + isSelectionEnd: { + default: lightDark('accent-900', 'accent-700'), + isHovered: lightDark('accent-1000', 'accent-600'), + isPressed: lightDark('accent-1000', 'accent-600'), + isFocusVisible: lightDark('accent-1000', 'accent-600') + }, + isUnavailable: 'transparent' }, color: { + default: 'neutral', isSelected: 'white', + isSelectionStart: 'white', + isSelectionEnd: 'white', isDisabled: 'disabled' } }); @@ -145,6 +202,21 @@ const unavailableStyles = style({ backgroundColor: '[currentColor]' }); +const selectionSpanStyles = style({ + position: 'absolute', + zIndex: -1, + top: 0, + insetStart: 'calc(-1 * var(--selection-span) * (var(--cell-width) + var(--cell-gap) + var(--cell-gap)))', + insetEnd: 0, + bottom: 0, + borderWidth: 2, + borderStyle: 'dashed', + borderColor: 'blue-800', // focus-indicator-color + borderStartRadius: 'full', + borderEndRadius: 'full', + backgroundColor: 'blue-subtle' +}); + export const helpTextStyles = style({ gridArea: 'helptext', display: 'flex', @@ -202,20 +274,20 @@ export const Calendar = /*#__PURE__*/ (forwardRef as forwardRefType)(function Ca width: 'full' })}> {Array.from({length: visibleMonths}).map((_, i) => ( - + {(day) => ( - + {day} )} - {(date) => ( - + {(date, weekIndex, dayIndex) => ( + )} - + ))}
{errorMessage && ( @@ -233,8 +305,10 @@ export const Calendar = /*#__PURE__*/ (forwardRef as forwardRefType)(function Ca // Ordinarily the heading is a formatted date range, ie January 2025 - February 2025. // However, we want to show each month individually. -const CalendarHeading = () => { - let {visibleRange, timeZone} = useContext(CalendarStateContext) ?? {}; +export const CalendarHeading = (): ReactElement => { + let calendarStateContext = useContext(CalendarStateContext); + let rangeCalendarStateContext = useContext(RangeCalendarStateContext); + let {visibleRange, timeZone} = calendarStateContext ?? rangeCalendarStateContext ?? {}; let era: any = getEraFormat(visibleRange?.start) || getEraFormat(visibleRange?.end); let monthFormatter = useDateFormatter({ month: 'long', @@ -272,9 +346,9 @@ const CalendarHeading = () => { return ( {/* Spacers to account for Next/Previous buttons and gap, spelled out to show the math */} -
-
-
+
+
+
{month}
); @@ -284,7 +358,7 @@ const CalendarHeading = () => { ); }; -const CalendarButton = (props: Omit & {children: ReactNode}) => { +export const CalendarButton = (props: Omit & {children: ReactNode}): ReactElement => { return ( & {children: ReactN ); }; -const CalendarCell = (props: Omit) => { - let ref = useRef(null); +export const CalendarHeaderCell = (props: Omit & PropsWithChildren): ReactElement => { + return ( + + {props.children} + + ); +}; + +export const CalendarCell = (props: Omit & {dayIndex: number, weekIndex: number}): ReactElement => { + let isFirstWeek = props.weekIndex === 0; + let isFirstChild = props.dayIndex === 0; + let isLastChild = props.dayIndex === 6; return ( - {({isUnavailable, formattedDate}) => ( - <> -
- {formattedDate} -
- {isUnavailable &&
} - - )} + className={(renderProps) => cellStyles({...renderProps, isFirstChild, isLastChild, isFirstWeek})}> + {(renderProps) => } ); }; + +const CalendarCellInner = (props: Omit & {weekIndex: number, dayIndex: number, renderProps?: CalendarCellRenderProps, date: DateValue}): ReactElement => { + let calendarStateContext = useContext(CalendarStateContext); + let rangeCalendarStateContext = useContext(RangeCalendarStateContext); + let state = (calendarStateContext ?? rangeCalendarStateContext)!; + + let {getDatesInWeek} = state; + let {weekIndex, dayIndex, date, renderProps} = props; + let ref = useRef(null); + let {isUnavailable, formattedDate, isSelected} = renderProps!; + let datesInWeek = getDatesInWeek(weekIndex); + + // Starting from the current day, find the first day before it in the current week that is not selected. + // Then, the span of selected days is the current day minus the first unselected day. + let firstUnselectedInRangeInWeek = datesInWeek.slice(0, dayIndex + 1).reverse().findIndex((date, i) => date && i > 0 && !state.isSelected(date)); + let selectionSpan = -1; + if (firstUnselectedInRangeInWeek > -1 && isSelected) { + selectionSpan = firstUnselectedInRangeInWeek - 1; + } else if (isSelected) { + selectionSpan = dayIndex; + } + + + let isRangeSelection = !!rangeCalendarStateContext; + let isBackgroundStyleApplied = ( + isSelected + && isRangeSelection + && (state.isSelected(date.subtract({days: 1})) + || state.isSelected(date.add({days: 1})) + )); + + return ( +
+
+
+ {formattedDate} +
+ {isUnavailable &&
} +
+ {isBackgroundStyleApplied &&
} +
+ ); +}; diff --git a/packages/@react-spectrum/s2/src/DateField.tsx b/packages/@react-spectrum/s2/src/DateField.tsx index c61dc4165cc..e219e4dc18e 100644 --- a/packages/@react-spectrum/s2/src/DateField.tsx +++ b/packages/@react-spectrum/s2/src/DateField.tsx @@ -13,9 +13,9 @@ import { DateField as AriaDateField, DateFieldProps as AriaDateFieldProps, + DateSegment as AriaDateSegment, ContextValue, DateInput, - DateSegment, DateValue, FormContext } from 'react-aria-components'; @@ -23,6 +23,7 @@ import {createContext, forwardRef, ReactElement, Ref, useContext} from 'react'; import {field, fieldInput, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; import {FieldErrorIcon, FieldGroup, FieldLabel, HelpText} from './Field'; import {forwardRefType, HelpTextProps, SpectrumLabelableProps} from '@react-types/shared'; +import {DateSegment as IDateSegment} from 'react-stately'; import {style} from '../style' with {type: 'macro'}; import {useSpectrumContextProps} from './useSpectrumContextProps'; @@ -46,7 +47,7 @@ const segmentContainer = style({ flexGrow: 1 }); -const dateInput = style({ +const dateSegment = style({ outlineStyle: 'none', caretColor: 'transparent', backgroundColor: { @@ -57,7 +58,11 @@ const dateInput = style({ isFocused: 'white' }, borderRadius: '[2px]', - paddingX: 2 + paddingX: { + default: 2, + isPunctuation: 0 + }, + paddingY: 2 }); const iconStyles = style({ @@ -122,7 +127,7 @@ export const DateField = /*#__PURE__*/ (forwardRef as forwardRefType)(function D paddingX: 'edge-to-text' })({size})}> - {(segment) => } + {(segment) => } {isInvalid &&
} @@ -139,3 +144,7 @@ export const DateField = /*#__PURE__*/ (forwardRef as forwardRefType)(function D ); }); + +export function DateSegment(props: {segment: IDateSegment}): ReactElement { + return dateSegment({...renderProps, isPunctuation: props.segment.type === 'literal'})} {...props} />; +} diff --git a/packages/@react-spectrum/s2/src/DatePicker.tsx b/packages/@react-spectrum/s2/src/DatePicker.tsx index 9a44736661f..f1e7858a8cf 100644 --- a/packages/@react-spectrum/s2/src/DatePicker.tsx +++ b/packages/@react-spectrum/s2/src/DatePicker.tsx @@ -17,18 +17,18 @@ import { ButtonRenderProps, ContextValue, DateInput, - DateSegment, DateValue, + DialogContext, FormContext, Provider, - TimeValue, - DialogContext + TimeValue } from 'react-aria-components'; import {baseColor, focusRing, fontRelative, style} from '../style' with {type: 'macro'}; -import {Calendar, IconContext, TimeField} from '../'; +import {Calendar, CalendarProps, IconContext, TimeField} from '../'; import CalendarIcon from '../s2wf-icons/S2_Icon_Calendar_20_N.svg'; import {controlBorderRadius, field, fieldInput, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; -import {createContext, forwardRef, ReactElement, Ref, useContext, useRef, useState} from 'react'; +import {createContext, forwardRef, PropsWithChildren, ReactElement, Ref, useContext, useRef, useState} from 'react'; +import {DateSegment} from './DateField'; import {FieldErrorIcon, FieldGroup, FieldLabel, HelpText} from './Field'; import {forwardRefType, HelpTextProps, SpectrumLabelableProps} from '@react-types/shared'; // @ts-ignore @@ -41,6 +41,7 @@ import {useSpectrumContextProps} from './useSpectrumContextProps'; export interface DatePickerProps extends Omit, 'children' | 'className' | 'style'>, + Pick, 'visibleMonths' | 'createCalendar'>, StyleProps, SpectrumLabelableProps, HelpTextProps { @@ -60,20 +61,6 @@ const segmentContainer = style({ overflow: 'hidden' }); -const dateInput = style({ - outlineStyle: 'none', - caretColor: 'transparent', - backgroundColor: { - default: 'transparent', - isFocused: 'blue-900' - }, - color: { - isFocused: 'white' - }, - borderRadius: '[2px]', - paddingX: 2 -}); - const iconStyles = style({ flexGrow: 1, display: 'flex', @@ -84,6 +71,7 @@ const iconStyles = style({ const inputButton = style({ ...focusRing(), ...controlBorderRadius('sm'), + font: 'ui', cursor: 'default', display: 'flex', textAlign: 'center', @@ -101,10 +89,7 @@ const inputButton = style(null); let [buttonHasFocus, setButtonHasFocus] = useState(false); return ( @@ -191,50 +177,28 @@ export const DatePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(function paddingEnd: 4 })({size})}> - {(segment) => } + {(segment) => } {isInvalid &&
} - + - -
- - {showTimeField && ( - state.setTimeValue(v as TimeValue)} - placeholderValue={timePlaceholder} - granularity={timeGranularity} - minValue={timeMinValue} - maxValue={timeMaxValue} - hourCycle={props.hourCycle} - hideTimeZone={props.hideTimeZone} /> - )} -
-
+ + + {showTimeField && ( + state.setTimeValue(v as TimeValue)} + placeholderValue={timePlaceholder} + granularity={timeGranularity} + minValue={timeMinValue} + maxValue={timeMaxValue} + hourCycle={props.hourCycle} + hideTimeZone={props.hideTimeZone} /> + )} + + styles={style({ + paddingX: 16, + paddingY: 32, + overflow: 'auto', + display: 'flex', + flexDirection: 'column', + gap: 16 + })}> {props.children} ); } + +export function CalendarButton(props: {isOpen: boolean, size: 'S' | 'M' | 'L' | 'XL', setButtonHasFocus: (hasFocus: boolean) => void}): ReactElement { + let buttonRef = useRef(null); + let {isOpen, size, setButtonHasFocus} = props; + return ( + + ); +} diff --git a/packages/@react-spectrum/s2/src/DateRangePicker.tsx b/packages/@react-spectrum/s2/src/DateRangePicker.tsx index f2e55d1b901..b4c6dc5e368 100644 --- a/packages/@react-spectrum/s2/src/DateRangePicker.tsx +++ b/packages/@react-spectrum/s2/src/DateRangePicker.tsx @@ -13,27 +13,21 @@ import { DateRangePicker as AriaDateRangePicker, DateRangePickerProps as AriaDateRangePickerProps, - Button, - ButtonRenderProps, ContextValue, DateInput, - DateSegment, DateValue, - DialogContext, - FormContext, - Provider + FormContext } from 'react-aria-components'; -import {baseColor, focusRing, fontRelative, style} from '../style' with {type: 'macro'}; -import CalendarIcon from '../s2wf-icons/S2_Icon_Calendar_20_N.svg'; -import {controlBorderRadius, field, fieldInput, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; -import {createContext, forwardRef, ReactElement, Ref, useContext, useRef, useState} from 'react'; +import {CalendarButton, CalendarPopover} from './DatePicker'; +import {createContext, forwardRef, ReactElement, Ref, useContext, useState} from 'react'; +import {DateSegment} from './DateField'; +import {field, fieldInput, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; import {FieldErrorIcon, FieldGroup, FieldLabel, HelpText} from './Field'; import {forwardRefType, HelpTextProps, SpectrumLabelableProps} from '@react-types/shared'; -import {IconContext, RangeCalendar, TimeField} from '../'; // @ts-ignore import intlMessages from '../intl/*.json'; -import {PopoverBase} from './Popover'; -import {pressScale} from './pressScale'; +import {RangeCalendar, TimeField} from '../'; +import {style} from '../style' with {type: 'macro'}; import {useLocalizedStringFormatter} from '@react-aria/i18n'; import {useSpectrumContextProps} from './useSpectrumContextProps'; @@ -48,7 +42,8 @@ export interface DateRangePickerProps extends * * @default 'M' */ - size?: 'S' | 'M' | 'L' | 'XL' + size?: 'S' | 'M' | 'L' | 'XL', + visibleMonths?: number } export const DateRangePickerContext = createContext>, HTMLDivElement>>(null); @@ -61,25 +56,11 @@ const segmentContainer = style({ display: 'flex', flexWrap: 'nowrap' }); -const segment = style({ +const input = style({ flexGrow: 0, flexShrink: 0 }); -const dateInput = style({ - outlineStyle: 'none', - caretColor: 'transparent', - backgroundColor: { - default: 'transparent', - isFocused: 'blue-900' - }, - color: { - isFocused: 'white' - }, - borderRadius: '[2px]', - paddingX: 2 -}); - const iconStyles = style({ flexGrow: 1, display: 'flex', @@ -87,47 +68,6 @@ const iconStyles = style({ justifyContent: 'end' }); -const inputButton = style({ - ...focusRing(), - ...controlBorderRadius('sm'), - cursor: 'default', - display: 'flex', - textAlign: 'center', - borderStyle: 'none', - alignItems: 'center', - justifyContent: 'center', - size: { - size: { - S: 16, - M: 20, - L: 24, - XL: 32 - } - }, - marginStart: 'text-to-control', - aspectRatio: 'square', - transition: { - default: 'default', - forcedColors: 'none' - }, - backgroundColor: { - default: baseColor('gray-100'), - isOpen: 'gray-200', - isDisabled: 'disabled', - forcedColors: { - default: 'ButtonText', - isHovered: 'Highlight', - isOpen: 'Highlight', - isDisabled: 'GrayText' - } - }, - color: { - default: baseColor('neutral'), - isDisabled: 'disabled', - forcedColors: 'ButtonFace' - } -}); - export const DateRangePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(function DateRangePicker( props: DateRangePickerProps, ref: Ref ): ReactElement { @@ -150,7 +90,6 @@ export const DateRangePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(func ...dateFieldProps } = props; let formContext = useContext(FormContext); - let buttonRef = useRef(null); let [buttonHasFocus, setButtonHasFocus] = useState(false); // TODO: fix width? default min width? @@ -200,12 +139,12 @@ export const DateRangePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(func paddingEnd: 4 })({size})}>
- - {(segment) => } + + {(segment) => } - - {(segment) => } + + {(segment) => }
{isInvalid &&
} @@ -216,60 +155,38 @@ export const DateRangePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(func display: 'flex', justifyContent: 'end' })}> - +
- -
- - {showTimeField && ( -
- state.setTime('start', v)} - placeholderValue={timePlaceholder} - granularity={timeGranularity} - minValue={timeMinValue} - maxValue={timeMaxValue} - hourCycle={props.hourCycle} - hideTimeZone={props.hideTimeZone} /> - state.setTime('end', v)} - placeholderValue={timePlaceholder} - granularity={timeGranularity} - minValue={timeMinValue} - maxValue={timeMaxValue} - hourCycle={props.hourCycle} - hideTimeZone={props.hideTimeZone} /> -
- )} -
-
+ + + {showTimeField && ( +
+ state.setTime('start', v)} + placeholderValue={timePlaceholder} + granularity={timeGranularity} + minValue={timeMinValue} + maxValue={timeMaxValue} + hourCycle={props.hourCycle} + hideTimeZone={props.hideTimeZone} /> + state.setTime('end', v)} + placeholderValue={timePlaceholder} + granularity={timeGranularity} + minValue={timeMinValue} + maxValue={timeMaxValue} + hourCycle={props.hourCycle} + hideTimeZone={props.hideTimeZone} /> +
+ )} +
); }); - -function Popover(props) { - // We don't have a dialog anymore, so we don't consume DialogContext. Have to place the - // id and aria label on something otherwise we get a violation. - let dialogProps = useContext(DialogContext) as any; - return ( - - {props.children} - - ); -} diff --git a/packages/@react-spectrum/s2/src/RangeCalendar.tsx b/packages/@react-spectrum/s2/src/RangeCalendar.tsx index 0b6aac54a62..eace8c0be4e 100644 --- a/packages/@react-spectrum/s2/src/RangeCalendar.tsx +++ b/packages/@react-spectrum/s2/src/RangeCalendar.tsx @@ -10,31 +10,24 @@ * governing permissions and limitations under the License. */ -import {ActionButton, Header, Heading, pressScale} from './'; import { - CalendarCell as AriaCalendarCell, + CalendarGrid as AriaCalendarGrid, RangeCalendar as AriaRangeCalendar, RangeCalendarProps as AriaRangeCalendarProps, - ButtonProps, - CalendarCellProps, - CalendarCellRenderProps, - CalendarGrid, CalendarGridBody, CalendarGridHeader, - CalendarHeaderCell, ContextValue, DateValue, - RangeCalendarStateContext, Text } from 'react-aria-components'; -import {baseColor, focusRing, lightDark, style} from '../style' with {type: 'macro'}; +import {CalendarButton, CalendarCell, CalendarHeaderCell, CalendarHeading} from './Calendar'; import ChevronLeftIcon from '../s2wf-icons/S2_Icon_ChevronLeft_20_N.svg'; import ChevronRightIcon from '../s2wf-icons/S2_Icon_ChevronRight_20_N.svg'; import {controlFont, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; -import {createContext, CSSProperties, ForwardedRef, forwardRef, Fragment, ReactNode, useContext, useMemo, useRef} from 'react'; +import {createContext, ForwardedRef, forwardRef, ReactNode} from 'react'; import {forwardRefType, ValidationResult} from '@react-types/shared'; -import {getEraFormat} from '@react-aria/calendar'; -import {useDateFormatter} from '@react-aria/i18n'; +import {Header} from './'; +import {style} from '../style' with {type: 'macro'}; import {useSpectrumContextProps} from './useSpectrumContextProps'; @@ -62,127 +55,6 @@ const headerStyles = style({ width: 'full' }); -const headingStyles = style({ - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - margin: 0, - width: 'full' -}); - -const titleStyles = style({ - font: 'title-lg', - textAlign: 'center', - flexGrow: 1, - flexShrink: 0, - flexBasis: '0%', - minWidth: 0 -}); - -const headerCellStyles = style({ - font: 'title-sm', - cursor: 'default', - textAlign: 'center', - flexGrow: 1 -}); - -const cellStyles = style({ - paddingX: 4, - '--cell-gap': { - type: 'paddingStart', - value: 4 - }, - paddingY: 2 -}); - -const cellInnerWrapperStyles = style({ - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - width: 'full', - boxSizing: 'border-box', - borderStartRadius: { - isSelectionStart: 'full' - }, - borderEndRadius: { - isSelectionEnd: 'full' - }, - outlineStyle: 'none' -}); - -const innerCellStyles = style({ - ...focusRing(), - outlineOffset: { - default: -2, - isToday: 2, - isSelected: 2 - }, - position: 'relative', - font: 'body-sm', - cursor: 'default', - height: 32, - borderRadius: 'full', - display: { - default: 'flex', - isOutsideMonth: 'none' - }, - alignItems: 'center', - justifyContent: 'center', - backgroundColor: { - default: 'transparent', - isHovered: 'gray-100', - isToday: { - default: baseColor('gray-300'), - isDisabled: 'disabled' - }, - isSelectionStart: { - default: lightDark('accent-900', 'accent-700'), - isHovered: lightDark('accent-1000', 'accent-600'), - isPressed: lightDark('accent-1000', 'accent-600'), - isFocusVisible: lightDark('accent-1000', 'accent-600') - }, - isSelectionEnd: { - default: lightDark('accent-900', 'accent-700'), - isHovered: lightDark('accent-1000', 'accent-600'), - isPressed: lightDark('accent-1000', 'accent-600'), - isFocusVisible: lightDark('accent-1000', 'accent-600') - } - }, - color: { - isSelected: { - isSelectionStart: 'white', - isSelectionEnd: 'white' - }, - isDisabled: 'disabled' - } -}); - -const unavailableStyles = style({ - position: 'absolute', - top: 'calc(50% - 1px)', - left: 'calc(25% - 1px)', - right: 'calc(25% - 1px)', - height: 2, - transform: 'rotate(-16deg)', - borderRadius: 'full', - backgroundColor: '[currentColor]' -}); - -const selectionSpanStyles = style({ - position: 'absolute', - zIndex: -1, - top: 0, - insetStart: 'calc(-1 * var(--selection-span) * (var(--cell-width) + var(--cell-gap) + var(--cell-gap)))', - insetEnd: 0, - bottom: 0, - borderWidth: 2, - borderStyle: 'dashed', - borderColor: 'blue-800', // focus-indicator-color - borderStartRadius: 'full', - borderEndRadius: 'full', - backgroundColor: 'blue-subtle' -}); - export const helpTextStyles = style({ gridArea: 'helptext', display: 'flex', @@ -241,22 +113,20 @@ export const RangeCalendar = /*#__PURE__*/ (forwardRef as forwardRefType)(functi })}> {Array.from({length: visibleMonths}).map((_, i) => { return ( - + {(day) => ( - + {day} )} - {(date, weekIndex, dayIndex) => { - return ( - - ); - }} + {(date, weekIndex, dayIndex) => ( + + )} - + ); })}
@@ -272,128 +142,3 @@ export const RangeCalendar = /*#__PURE__*/ (forwardRef as forwardRefType)(functi ); }); - -// Ordinarily the heading is a formatted date range, ie January 2025 - February 2025. -// However, we want to show each month individually. -const CalendarHeading = () => { - let {visibleRange, timeZone} = useContext(RangeCalendarStateContext) ?? {}; - let era: any = getEraFormat(visibleRange?.start) || getEraFormat(visibleRange?.end); - let monthFormatter = useDateFormatter({ - month: 'long', - year: 'numeric', - era, - calendar: visibleRange?.start.calendar.identifier, - timeZone - }); - let months = useMemo(() => { - if (!visibleRange) { - return []; - } - let months: string[] = []; - for (let i = visibleRange.start; i.compare(visibleRange.end) <= 0; i = i.add({months: 1})) { - // TODO: account for the first week possibly overlapping, like with a custom 454 calendar. - // there has to be a better way to do this... - if (i.month === visibleRange.start.month) { - i = i.add({weeks: 1}); - } - months.push(monthFormatter.format(i.toDate(timeZone!))); - } - return months; - }, [visibleRange, monthFormatter, timeZone]); - - return ( - - {months.map((month, i) => { - if (i === 0) { - return ( - -
{month}
-
- ); - } else { - return ( - - {/* Spacers to account for Next/Previous buttons and gap, spelled out to show the math */} -
-
-
-
{month}
- - ); - } - })} - - ); -}; - -const CalendarButton = (props: Omit & {children: ReactNode}) => { - return ( - - {props.children} - - ); -}; - -const CalendarCell = (props: Omit & {weekIndex: number, dayIndex: number}) => { - - return ( - - {(renderProps) => { - return ; - }} - - ); -}; - -const CalendarCellInner = (props: Omit & {weekIndex: number, dayIndex: number, renderProps: CalendarCellRenderProps, date: DateValue}) => { - let state = useContext(RangeCalendarStateContext)!; - let {getDatesInWeek} = state; - let {weekIndex, dayIndex, renderProps} = props; - let ref = useRef(null); - let {isUnavailable, formattedDate} = renderProps; - let datesInWeek = getDatesInWeek(weekIndex); - - // Starting from the current day, find the first day before it in the current week that is not selected. - // Then, the span of selected days is the current day minus the first unselected day. - let firstUnselectedInRangeInWeek = datesInWeek.slice(0, dayIndex + 1).reverse().findIndex((date, i) => date && i > 0 && !state.isSelected(date)); - let selectionSpan = -1; - if (firstUnselectedInRangeInWeek > -1 && renderProps.isSelected) { - selectionSpan = firstUnselectedInRangeInWeek - 1; - } else if (renderProps.isSelected) { - selectionSpan = dayIndex; - } - - let isBackgroundStyleApplied = ( - renderProps.isSelected - && (state.isSelected(props.date.subtract({days: 1})) - || state.isSelected(props.date.add({days: 1})) - )); - - return ( -
-
-
- {formattedDate} -
- {isUnavailable &&
} -
- {isBackgroundStyleApplied &&
} -
- ); -}; diff --git a/packages/@react-spectrum/s2/stories/DateField.stories.tsx b/packages/@react-spectrum/s2/stories/DateField.stories.tsx index 326e3cec1cb..cd2f2f77000 100644 --- a/packages/@react-spectrum/s2/stories/DateField.stories.tsx +++ b/packages/@react-spectrum/s2/stories/DateField.stories.tsx @@ -34,7 +34,11 @@ const meta: Meta = { export default meta; type Story = StoryObj; -export const Example: Story = {}; +export const Example: Story = { + args: { + 'aria-label': 'Birthday' + } +}; export const Validation: Story = { render: (args) => ( diff --git a/packages/react-aria-components/src/Calendar.tsx b/packages/react-aria-components/src/Calendar.tsx index e928add1467..982e145ea93 100644 --- a/packages/react-aria-components/src/Calendar.tsx +++ b/packages/react-aria-components/src/Calendar.tsx @@ -30,7 +30,7 @@ import {ContextValue, DOMProps, Provider, RenderProps, SlotProps, StyleProps, us import {DOMAttributes, FocusableElement, forwardRefType, HoverEvents} from '@react-types/shared'; import {filterDOMProps} from '@react-aria/utils'; import {HeadingContext} from './RSPContexts'; -import React, {createContext, CSSProperties, ForwardedRef, forwardRef, ReactElement, useContext, useRef} from 'react'; +import React, {AllHTMLAttributes, createContext, ForwardedRef, forwardRef, ReactElement, useContext, useRef} from 'react'; import {TextContext} from './Text'; export interface CalendarRenderProps { @@ -323,7 +323,7 @@ export interface CalendarCellRenderProps { isToday: boolean } -export interface CalendarGridProps extends StyleProps { +export interface CalendarGridProps extends StyleProps, Omit, 'children'> { /** * Either a function to render calendar cells for each date in the month, * or children containing a ``` and `` @@ -382,6 +382,8 @@ export const CalendarGrid = /*#__PURE__*/ (forwardRef as forwardRefType)(functio @@ -490,11 +492,7 @@ export {CalendarGridBodyForwardRef as CalendarGridBody}; export interface CalendarCellProps extends RenderProps, HoverEvents { /** The date to render in the cell. */ - date: CalendarDate, - /** The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the element. A function may be provided to compute the class based on component state. */ - cellClassName?: string | ((values: CalendarCellRenderProps & {defaultClassName: string | undefined}) => string), - /** The inline [style](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/style) for the element. A function may be provided to compute the style based on component state. */ - cellStyle?: CSSProperties | ((values: CalendarCellRenderProps & {defaultStyle: CSSProperties}) => CSSProperties | undefined) + date: CalendarDate } /** @@ -542,22 +540,6 @@ export const CalendarCell = /*#__PURE__*/ (forwardRef as forwardRefType)(functio } }); - let cellRenderProps = useRenderProps({ - className: otherProps.cellClassName, - style: otherProps.cellStyle, - defaultClassName: 'react-aria-CalendarCellWrapper', - values: { - date, - isHovered, - isOutsideMonth, - isFocusVisible, - isSelectionStart, - isSelectionEnd, - isToday: istoday, - ...states - } - }); - let dataAttrs = { 'data-focused': states.isFocused || undefined, 'data-hovered': isHovered || undefined, @@ -575,7 +557,7 @@ export const CalendarCell = /*#__PURE__*/ (forwardRef as forwardRefType)(functio }; return ( - ); From d17b7fd40fc159b8a9a1060c439b4591cea7f1e4 Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Mon, 30 Jun 2025 19:32:58 +1000 Subject: [PATCH 26/33] make outline in range smaller --- packages/@react-spectrum/s2/src/Calendar.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/@react-spectrum/s2/src/Calendar.tsx b/packages/@react-spectrum/s2/src/Calendar.tsx index d90c726c069..985999423f7 100644 --- a/packages/@react-spectrum/s2/src/Calendar.tsx +++ b/packages/@react-spectrum/s2/src/Calendar.tsx @@ -135,7 +135,14 @@ const cellInnerStyles = style({ outlineOffset: { default: -2, isToday: 2, - isSelected: 2 + isSelected: { + selectionMode: { + single: 2, + range: -2 + } + }, + isSelectionStart: 2, + isSelectionEnd: 2 }, position: 'relative', font: 'body-sm', From be9560f665bf6a9a21aaf423d9ab75cd2cfd06ad Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Mon, 30 Jun 2025 19:37:11 +1000 Subject: [PATCH 27/33] Label instead of aria label the stories by default --- .../s2/stories/DatePicker.stories.tsx | 7 ++++++- .../s2/stories/DateRangePicker.stories.tsx | 16 +++++++++++++--- .../s2/stories/TimeField.stories.tsx | 6 ++++++ 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/packages/@react-spectrum/s2/stories/DatePicker.stories.tsx b/packages/@react-spectrum/s2/stories/DatePicker.stories.tsx index 5c8bc71f04d..c5828774283 100644 --- a/packages/@react-spectrum/s2/stories/DatePicker.stories.tsx +++ b/packages/@react-spectrum/s2/stories/DatePicker.stories.tsx @@ -41,10 +41,15 @@ type Story = StoryObj; export const Example: Story = { args: { - 'aria-label': 'Birthday' + label: 'Birthday' } }; +export const AriaLabel: Story = { + args: { + 'aria-label': 'Birthday' + } +}; export const Validation: Story = { render: (args) => (
diff --git a/packages/@react-spectrum/s2/stories/DateRangePicker.stories.tsx b/packages/@react-spectrum/s2/stories/DateRangePicker.stories.tsx index 8757558f0ad..f12cf3f4022 100644 --- a/packages/@react-spectrum/s2/stories/DateRangePicker.stories.tsx +++ b/packages/@react-spectrum/s2/stories/DateRangePicker.stories.tsx @@ -39,7 +39,17 @@ const meta: Meta = { export default meta; type Story = StoryObj; -export const Example: Story = {}; +export const Example: Story = { + args: { + label: 'Reservation dates' + } +}; + +export const AriaLabel: Story = { + args: { + 'aria-label': 'Reservation dates' + } +}; export const Validation: Story = { render: (args) => ( @@ -49,7 +59,7 @@ export const Validation: Story = { ), args: { - label: 'Birthday', + label: 'Reservation dates', isRequired: true } }; @@ -59,7 +69,7 @@ export const CustomWidth: Story = { ), args: { - label: 'Birthday' + label: 'Reservation dates' } }; diff --git a/packages/@react-spectrum/s2/stories/TimeField.stories.tsx b/packages/@react-spectrum/s2/stories/TimeField.stories.tsx index 369ac8caa9e..6880b6beb46 100644 --- a/packages/@react-spectrum/s2/stories/TimeField.stories.tsx +++ b/packages/@react-spectrum/s2/stories/TimeField.stories.tsx @@ -35,6 +35,12 @@ export default meta; type Story = StoryObj; export const Example: Story = { + args: { + label: 'Launch time' + } +}; + +export const AriaLabel: Story = { args: { 'aria-label': 'Launch time' } From 34501702b8e0ec10b4d0514f4f9016ad2e14de18 Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Tue, 1 Jul 2025 11:19:33 +1000 Subject: [PATCH 28/33] reduce duplication, fix styles --- packages/@react-spectrum/s2/src/DateField.tsx | 28 ++++++--- .../@react-spectrum/s2/src/DatePicker.tsx | 57 ++++++++++--------- .../s2/src/DateRangePicker.tsx | 30 +++------- packages/@react-spectrum/s2/src/TimeField.tsx | 36 ++---------- 4 files changed, 61 insertions(+), 90 deletions(-) diff --git a/packages/@react-spectrum/s2/src/DateField.tsx b/packages/@react-spectrum/s2/src/DateField.tsx index e219e4dc18e..e3a4becc6bf 100644 --- a/packages/@react-spectrum/s2/src/DateField.tsx +++ b/packages/@react-spectrum/s2/src/DateField.tsx @@ -13,9 +13,10 @@ import { DateField as AriaDateField, DateFieldProps as AriaDateFieldProps, + DateInput as AriaDateInput, DateSegment as AriaDateSegment, ContextValue, - DateInput, + DateInputProps, DateValue, FormContext } from 'react-aria-components'; @@ -23,7 +24,6 @@ import {createContext, forwardRef, ReactElement, Ref, useContext} from 'react'; import {field, fieldInput, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; import {FieldErrorIcon, FieldGroup, FieldLabel, HelpText} from './Field'; import {forwardRefType, HelpTextProps, SpectrumLabelableProps} from '@react-types/shared'; -import {DateSegment as IDateSegment} from 'react-stately'; import {style} from '../style' with {type: 'macro'}; import {useSpectrumContextProps} from './useSpectrumContextProps'; @@ -52,7 +52,7 @@ const dateSegment = style({ caretColor: 'transparent', backgroundColor: { default: 'transparent', - isFocused: 'blue-900' + isFocused: 'blue-800' }, color: { isFocused: 'white' @@ -126,10 +126,8 @@ export const DateField = /*#__PURE__*/ (forwardRef as forwardRefType)(function D ...fieldInput(), paddingX: 'edge-to-text' })({size})}> - - {(segment) => } - - {isInvalid &&
} + + dateSegment({...renderProps, isPunctuation: props.segment.type === 'literal'})} {...props} />; +export function DateInput(props: Omit): ReactElement { + return ( + + {(segment) => ( + dateSegment({...renderProps, isPunctuation: segment.type === 'literal'})} /> + )} + + ); +} + +export function InvalidIndicator(props: {isInvalid: boolean, isDisabled: boolean}): ReactElement | null { + return props.isInvalid ?
: null; } diff --git a/packages/@react-spectrum/s2/src/DatePicker.tsx b/packages/@react-spectrum/s2/src/DatePicker.tsx index f1e7858a8cf..22ae4052924 100644 --- a/packages/@react-spectrum/s2/src/DatePicker.tsx +++ b/packages/@react-spectrum/s2/src/DatePicker.tsx @@ -16,20 +16,19 @@ import { Button, ButtonRenderProps, ContextValue, - DateInput, DateValue, DialogContext, FormContext, Provider, TimeValue } from 'react-aria-components'; -import {baseColor, focusRing, fontRelative, style} from '../style' with {type: 'macro'}; +import {baseColor, focusRing, fontRelative, space, style} from '../style' with {type: 'macro'}; import {Calendar, CalendarProps, IconContext, TimeField} from '../'; import CalendarIcon from '../s2wf-icons/S2_Icon_Calendar_20_N.svg'; import {controlBorderRadius, field, fieldInput, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; import {createContext, forwardRef, PropsWithChildren, ReactElement, Ref, useContext, useRef, useState} from 'react'; -import {DateSegment} from './DateField'; -import {FieldErrorIcon, FieldGroup, FieldLabel, HelpText} from './Field'; +import {DateInput, InvalidIndicator} from './DateField'; +import {FieldGroup, FieldLabel, HelpText} from './Field'; import {forwardRefType, HelpTextProps, SpectrumLabelableProps} from '@react-types/shared'; // @ts-ignore import intlMessages from '../intl/*.json'; @@ -55,30 +54,24 @@ export interface DatePickerProps extends export const DatePickerContext = createContext>, HTMLDivElement>>(null); -const segmentContainer = style({ - flexGrow: 1, - flexShrink: 1, - overflow: 'hidden' -}); - -const iconStyles = style({ - flexGrow: 1, - display: 'flex', - alignItems: 'center', - justifyContent: 'end' -}); - const inputButton = style({ ...focusRing(), ...controlBorderRadius('sm'), - font: 'ui', + font: { + size: { + S: 'ui-sm', + M: 'ui', + L: 'ui-lg', + XL: 'ui-xl' + } + }, cursor: 'default', display: 'flex', textAlign: 'center', borderStyle: 'none', alignItems: 'center', justifyContent: 'center', - size: { + width: { size: { S: 16, M: 20, @@ -86,6 +79,7 @@ const inputButton = style - - {(segment) => } - - {isInvalid &&
} + + @@ -245,7 +244,7 @@ export function CalendarButton(props: {isOpen: boolean, size: 'S' | 'M' | 'L' | // @ts-ignore isPressed={false} onFocusChange={setButtonHasFocus} - style={renderProps => pressScale(buttonRef)(renderProps)} + style={pressScale(buttonRef)} className={renderProps => inputButton({ ...renderProps, size, @@ -254,7 +253,13 @@ export function CalendarButton(props: {isOpen: boolean, size: 'S' | 'M' | 'L' | diff --git a/packages/@react-spectrum/s2/src/DateRangePicker.tsx b/packages/@react-spectrum/s2/src/DateRangePicker.tsx index b4c6dc5e368..8c9bd4f1034 100644 --- a/packages/@react-spectrum/s2/src/DateRangePicker.tsx +++ b/packages/@react-spectrum/s2/src/DateRangePicker.tsx @@ -14,15 +14,14 @@ import { DateRangePicker as AriaDateRangePicker, DateRangePickerProps as AriaDateRangePickerProps, ContextValue, - DateInput, DateValue, FormContext } from 'react-aria-components'; import {CalendarButton, CalendarPopover} from './DatePicker'; import {createContext, forwardRef, ReactElement, Ref, useContext, useState} from 'react'; -import {DateSegment} from './DateField'; +import {DateInput, InvalidIndicator} from './DateField'; import {field, fieldInput, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; -import {FieldErrorIcon, FieldGroup, FieldLabel, HelpText} from './Field'; +import {FieldGroup, FieldLabel, HelpText} from './Field'; import {forwardRefType, HelpTextProps, SpectrumLabelableProps} from '@react-types/shared'; // @ts-ignore import intlMessages from '../intl/*.json'; @@ -48,7 +47,7 @@ export interface DateRangePickerProps extends export const DateRangePickerContext = createContext>, HTMLDivElement>>(null); -const segmentContainer = style({ +const segmentsContainer = style({ flexGrow: 0, flexShrink: 1, overflow: 'hidden', @@ -56,17 +55,6 @@ const segmentContainer = style({ display: 'flex', flexWrap: 'nowrap' }); -const input = style({ - flexGrow: 0, - flexShrink: 0 -}); - -const iconStyles = style({ - flexGrow: 1, - display: 'flex', - alignItems: 'center', - justifyContent: 'end' -}); export const DateRangePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(function DateRangePicker( props: DateRangePickerProps, ref: Ref @@ -138,16 +126,12 @@ export const DateRangePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(func paddingStart: 'edge-to-text', paddingEnd: 4 })({size})}> -
- - {(segment) => } - +
+ - - {(segment) => } - +
- {isInvalid &&
} +
extends export const TimeFieldContext = createContext>, HTMLDivElement>>(null); -const segmentContainer = style({ - flexGrow: 1 -}); - -// TODO: Figure out field width -const timeInput = style({ - outlineStyle: 'none', - caretColor: 'transparent', - backgroundColor: { - default: 'transparent', - isFocused: 'blue-900' - }, - color: { - isFocused: 'white' - }, - borderRadius: '[2px]', - paddingX: 2 -}); - -const iconStyles = style({ - display: 'flex', - alignItems: 'center', - justifyContent: 'end' -}); - export const TimeField = /*#__PURE__*/ (forwardRef as forwardRefType)(function TimeField( props: TimeFieldProps, ref: Ref ): ReactElement { @@ -122,10 +96,8 @@ export const TimeField = /*#__PURE__*/ (forwardRef as forwardRefType)(function T ...fieldInput(), paddingX: 'edge-to-text' })({size})}> - - {(segment) => } - - {isInvalid &&
} + + Date: Tue, 1 Jul 2025 13:15:37 +1000 Subject: [PATCH 29/33] remove added api for day/week index and add storybook decorators for calendar systems --- packages/@react-spectrum/s2/src/Calendar.tsx | 99 ++++++++++++++++--- .../@react-spectrum/s2/src/RangeCalendar.tsx | 4 +- .../s2/stories/Calendar.stories.tsx | 11 ++- .../s2/stories/RangeCalendar.stories.tsx | 11 ++- packages/@react-spectrum/s2/stories/utils.tsx | 38 ++++++- .../react-aria-components/src/Calendar.tsx | 4 +- 6 files changed, 141 insertions(+), 26 deletions(-) diff --git a/packages/@react-spectrum/s2/src/Calendar.tsx b/packages/@react-spectrum/s2/src/Calendar.tsx index 985999423f7..4fe7658abca 100644 --- a/packages/@react-spectrum/s2/src/Calendar.tsx +++ b/packages/@react-spectrum/s2/src/Calendar.tsx @@ -23,9 +23,11 @@ import { CalendarGridBody, CalendarGridHeader, CalendarHeaderCellProps, + CalendarState, CalendarStateContext, ContextValue, DateValue, + RangeCalendarState, RangeCalendarStateContext, Text } from 'react-aria-components'; @@ -36,8 +38,13 @@ import {controlFont, getAllowedOverrides, StyleProps} from './style-utils' with import {forwardRefType, ValidationResult} from '@react-types/shared'; import {getEraFormat} from '@react-aria/calendar'; import React, {createContext, CSSProperties, ForwardedRef, forwardRef, Fragment, PropsWithChildren, ReactElement, ReactNode, useContext, useMemo, useRef} from 'react'; -import {useDateFormatter} from '@react-aria/i18n'; +import {useDateFormatter, useLocale} from '@react-aria/i18n'; import {useSpectrumContextProps} from './useSpectrumContextProps'; +import { + CalendarDate, + getDayOfWeek, + startOfMonth +} from '@internationalized/date'; export interface CalendarProps @@ -290,8 +297,8 @@ export const Calendar = /*#__PURE__*/ (forwardRef as forwardRefType)(function Ca )} - {(date, weekIndex, dayIndex) => ( - + {(date) => ( + )} @@ -383,26 +390,32 @@ export const CalendarHeaderCell = (props: Omit & {dayIndex: number, weekIndex: number}): ReactElement => { - let isFirstWeek = props.weekIndex === 0; - let isFirstChild = props.dayIndex === 0; - let isLastChild = props.dayIndex === 6; +export const CalendarCell = (props: Omit & {firstDayOfWeek: 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat' | undefined}): ReactElement => { + let {locale} = useLocale(); + let defaultFirstDayOfWeek = useDefaultFirstDayOfWeek(locale); + let firstDayOfWeek = props.firstDayOfWeek ?? defaultFirstDayOfWeek; + // Calculate the day and week index based on the date. + let {dayIndex, weekIndex} = useWeekAndDayIndices(props.date, locale, firstDayOfWeek); + + let isFirstWeek = weekIndex === 0; + let isFirstChild = dayIndex === 0; + let isLastChild = dayIndex === 6; + + let calendarStateContext = useContext(CalendarStateContext); + let rangeCalendarStateContext = useContext(RangeCalendarStateContext); + let state = (calendarStateContext ?? rangeCalendarStateContext)!; return ( cellStyles({...renderProps, isFirstChild, isLastChild, isFirstWeek})}> - {(renderProps) => } + {(renderProps) => } ); }; -const CalendarCellInner = (props: Omit & {weekIndex: number, dayIndex: number, renderProps?: CalendarCellRenderProps, date: DateValue}): ReactElement => { - let calendarStateContext = useContext(CalendarStateContext); - let rangeCalendarStateContext = useContext(RangeCalendarStateContext); - let state = (calendarStateContext ?? rangeCalendarStateContext)!; - +const CalendarCellInner = (props: Omit & {isRangeSelection: boolean, state: CalendarState | RangeCalendarState, weekIndex: number, dayIndex: number, renderProps?: CalendarCellRenderProps, date: DateValue}): ReactElement => { + let {weekIndex, dayIndex, date, renderProps, state, isRangeSelection} = props; let {getDatesInWeek} = state; - let {weekIndex, dayIndex, date, renderProps} = props; let ref = useRef(null); let {isUnavailable, formattedDate, isSelected} = renderProps!; let datesInWeek = getDatesInWeek(weekIndex); @@ -417,8 +430,6 @@ const CalendarCellInner = (props: Omit & {weekInd selectionSpan = dayIndex; } - - let isRangeSelection = !!rangeCalendarStateContext; let isBackgroundStyleApplied = ( isSelected && isRangeSelection @@ -449,3 +460,59 @@ const CalendarCellInner = (props: Omit & {weekInd
); }; + +type DayOfWeek = 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat'; + +/** + * Calculate the week index (0-based) and day index (0-based) for a given date within a month in a calendar. + * @param date - The date to calculate indices for. + * @param locale - The locale string (e.g., 'en-US', 'fr-FR', 'hi-IN-u-ca-indian'). + * @param firstDayOfWeek - Optional override for the first day of the week ('sun', 'mon', 'tue', etc.). + * @returns Object with weekIndex and dayIndex. + */ +function useWeekAndDayIndices( + date: CalendarDate, + locale: string, + firstDayOfWeek?: DayOfWeek +) { + let {dayIndex, weekIndex} = useMemo(() => { + // Get the day index within the week (0-6) + const dayIndex = getDayOfWeek(date, locale, firstDayOfWeek); + + const monthStart = startOfMonth(date); + + // Calculate the week index by finding which week this date falls into + // within the month's calendar grid + const monthStartDayOfWeek = getDayOfWeek(monthStart, locale, firstDayOfWeek); + const dayOfMonth = date.day; + + const weekIndex = Math.floor((dayOfMonth + monthStartDayOfWeek - 1) / 7); + + return { + weekIndex, + dayIndex + }; + }, [date, locale, firstDayOfWeek]); + + return {dayIndex, weekIndex}; +} + +function useDefaultFirstDayOfWeek(locale: string): DayOfWeek { + let day = useMemo(() => { + // Create a known date (e.g., January 1, 2024, which was a Monday) + const knownDate = new CalendarDate(2024, 1, 1); + + // Get the day of week for this date without specifying firstDayOfWeek + // This will use the locale's default + const dayOfWeek = getDayOfWeek(knownDate, locale); + + // Since we know this date was a Monday (day 1 in our system), + // we can calculate the first day of the week + const firstDayNumber = (1 - dayOfWeek + 7) % 7; + + const dayMap = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'] as const; + return dayMap[firstDayNumber]; + }, [locale]); + + return day; +} diff --git a/packages/@react-spectrum/s2/src/RangeCalendar.tsx b/packages/@react-spectrum/s2/src/RangeCalendar.tsx index eace8c0be4e..b019fbd79a0 100644 --- a/packages/@react-spectrum/s2/src/RangeCalendar.tsx +++ b/packages/@react-spectrum/s2/src/RangeCalendar.tsx @@ -122,8 +122,8 @@ export const RangeCalendar = /*#__PURE__*/ (forwardRef as forwardRefType)(functi )} - {(date, weekIndex, dayIndex) => ( - + {(date) => ( + )} diff --git a/packages/@react-spectrum/s2/stories/Calendar.stories.tsx b/packages/@react-spectrum/s2/stories/Calendar.stories.tsx index f220137a008..00643dbc7cf 100644 --- a/packages/@react-spectrum/s2/stories/Calendar.stories.tsx +++ b/packages/@react-spectrum/s2/stories/Calendar.stories.tsx @@ -12,7 +12,7 @@ import {ActionButton, Calendar, CalendarProps} from '../src'; import {CalendarDate, getLocalTimeZone, today} from '@internationalized/date'; -import {categorizeArgTypes} from './utils'; +import {CalendarSwitcher, categorizeArgTypes} from './utils'; import {Custom454Calendar} from '../../../@internationalized/date/tests/customCalendarImpl'; import {DateValue} from 'react-aria'; import type {Meta, StoryObj} from '@storybook/react'; @@ -35,7 +35,14 @@ const meta: Meta = { options: [1, 2, 3] } }, - title: 'Calendar' + title: 'Calendar', + decorators: [ + (Story) => ( + + + + ) + ] }; export default meta; diff --git a/packages/@react-spectrum/s2/stories/RangeCalendar.stories.tsx b/packages/@react-spectrum/s2/stories/RangeCalendar.stories.tsx index e522792e29f..cd6aab12004 100644 --- a/packages/@react-spectrum/s2/stories/RangeCalendar.stories.tsx +++ b/packages/@react-spectrum/s2/stories/RangeCalendar.stories.tsx @@ -12,7 +12,7 @@ import {ActionButton, RangeCalendar, RangeCalendarProps} from '../src'; import {CalendarDate, getLocalTimeZone, isWeekend, today} from '@internationalized/date'; -import {categorizeArgTypes} from './utils'; +import {CalendarSwitcher, categorizeArgTypes} from './utils'; import {Custom454Calendar} from '../../../@internationalized/date/tests/customCalendarImpl'; import {DateValue} from 'react-aria'; import type {Meta, StoryObj} from '@storybook/react'; @@ -35,7 +35,14 @@ const meta: Meta> = { options: [1, 2, 3] } }, - title: 'RangeCalendar' + title: 'RangeCalendar', + decorators: [ + (Story) => ( + + + + ) + ] }; export default meta; diff --git a/packages/@react-spectrum/s2/stories/utils.tsx b/packages/@react-spectrum/s2/stories/utils.tsx index 9a166a56d8e..1d5b3df1821 100644 --- a/packages/@react-spectrum/s2/stories/utils.tsx +++ b/packages/@react-spectrum/s2/stories/utils.tsx @@ -11,8 +11,10 @@ */ -import {ReactNode, useState} from 'react'; +import {PropsWithChildren, ReactElement, ReactNode, useState} from 'react'; import {style} from '../style' with {type: 'macro'}; +import { Key, useLocale } from 'react-aria'; +import { Picker, PickerItem, Provider } from '../src'; type StaticColor = 'black' | 'white' | 'auto' | undefined; @@ -39,7 +41,7 @@ export function StaticColorProvider(props: {children: ReactNode, staticColor?: S {props.children}
{props.staticColor === 'auto' && ( -
From 27c4230a48f705ba691844fd4bf34c28145b404f Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Tue, 1 Jul 2025 15:11:55 +1000 Subject: [PATCH 30/33] field widths, share more --- packages/@react-spectrum/s2/src/Calendar.tsx | 10 +-- packages/@react-spectrum/s2/src/DateField.tsx | 20 ++++- .../@react-spectrum/s2/src/DatePicker.tsx | 39 ++++++---- .../s2/src/DateRangePicker.tsx | 27 +++---- packages/@react-spectrum/s2/src/TimeField.tsx | 6 +- .../s2/stories/DateField.stories.tsx | 17 +++- .../s2/stories/DatePicker.stories.tsx | 19 ++++- .../s2/stories/DateRangePicker.stories.tsx | 19 ++++- .../s2/stories/TimeField.stories.tsx | 11 ++- packages/@react-spectrum/s2/stories/utils.tsx | 77 ++++++++++++++++--- 10 files changed, 180 insertions(+), 65 deletions(-) diff --git a/packages/@react-spectrum/s2/src/Calendar.tsx b/packages/@react-spectrum/s2/src/Calendar.tsx index 4fe7658abca..6e977798620 100644 --- a/packages/@react-spectrum/s2/src/Calendar.tsx +++ b/packages/@react-spectrum/s2/src/Calendar.tsx @@ -32,6 +32,11 @@ import { Text } from 'react-aria-components'; import {baseColor, focusRing, lightDark, style} from '../style' with {type: 'macro'}; +import { + CalendarDate, + getDayOfWeek, + startOfMonth +} from '@internationalized/date'; import ChevronLeftIcon from '../s2wf-icons/S2_Icon_ChevronLeft_20_N.svg'; import ChevronRightIcon from '../s2wf-icons/S2_Icon_ChevronRight_20_N.svg'; import {controlFont, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; @@ -40,11 +45,6 @@ import {getEraFormat} from '@react-aria/calendar'; import React, {createContext, CSSProperties, ForwardedRef, forwardRef, Fragment, PropsWithChildren, ReactElement, ReactNode, useContext, useMemo, useRef} from 'react'; import {useDateFormatter, useLocale} from '@react-aria/i18n'; import {useSpectrumContextProps} from './useSpectrumContextProps'; -import { - CalendarDate, - getDayOfWeek, - startOfMonth -} from '@internationalized/date'; export interface CalendarProps diff --git a/packages/@react-spectrum/s2/src/DateField.tsx b/packages/@react-spectrum/s2/src/DateField.tsx index e3a4becc6bf..b2ce70a4579 100644 --- a/packages/@react-spectrum/s2/src/DateField.tsx +++ b/packages/@react-spectrum/s2/src/DateField.tsx @@ -20,7 +20,7 @@ import { DateValue, FormContext } from 'react-aria-components'; -import {createContext, forwardRef, ReactElement, Ref, useContext} from 'react'; +import {createContext, forwardRef, PropsWithChildren, ReactElement, Ref, useContext} from 'react'; import {field, fieldInput, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; import {FieldErrorIcon, FieldGroup, FieldLabel, HelpText} from './Field'; import {forwardRefType, HelpTextProps, SpectrumLabelableProps} from '@react-types/shared'; @@ -44,7 +44,13 @@ export interface DateFieldProps extends export const DateFieldContext = createContext>, HTMLDivElement>>(null); const segmentContainer = style({ - flexGrow: 1 + flexGrow: 1, + flexShrink: 1, + minWidth: 0, + height: 'full', + overflow: 'hidden', + display: 'flex', + alignItems: 'center' }); const dateSegment = style({ @@ -126,7 +132,9 @@ export const DateField = /*#__PURE__*/ (forwardRef as forwardRefType)(function D ...fieldInput(), paddingX: 'edge-to-text' })({size})}> - + + + {props.children}; +} + export function DateInput(props: Omit): ReactElement { return ( - + {(segment) => ( ( props: DatePickerProps, ref: Ref ): ReactElement { @@ -177,25 +184,29 @@ export const DatePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(function } } })({size})}> - + + + {showTimeField && ( - state.setTimeValue(v as TimeValue)} - placeholderValue={timePlaceholder} - granularity={timeGranularity} - minValue={timeMinValue} - maxValue={timeMaxValue} - hourCycle={props.hourCycle} - hideTimeZone={props.hideTimeZone} /> +
+ state.setTimeValue(v as TimeValue)} + placeholderValue={timePlaceholder} + granularity={timeGranularity} + minValue={timeMinValue} + maxValue={timeMaxValue} + hourCycle={props.hourCycle} + hideTimeZone={props.hideTimeZone} /> +
)}
extends export const DateRangePickerContext = createContext>, HTMLDivElement>>(null); -const segmentsContainer = style({ - flexGrow: 0, - flexShrink: 1, - overflow: 'hidden', - textWrap: 'nowrap', - display: 'flex', - flexWrap: 'nowrap' -}); - export const DateRangePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(function DateRangePicker( props: DateRangePickerProps, ref: Ref ): ReactElement { @@ -75,12 +66,12 @@ export const DateRangePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(func UNSAFE_className, styles, placeholderValue, + visibleMonths = 1, ...dateFieldProps } = props; let formContext = useContext(FormContext); let [buttonHasFocus, setButtonHasFocus] = useState(false); - // TODO: fix width? default min width? return ( -
+ -
+
- + {showTimeField && ( -
+
state.setTime('start', v)} @@ -158,7 +149,7 @@ export const DateRangePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(func hourCycle={props.hourCycle} hideTimeZone={props.hideTimeZone} /> state.setTime('end', v)} diff --git a/packages/@react-spectrum/s2/src/TimeField.tsx b/packages/@react-spectrum/s2/src/TimeField.tsx index 69ca222c200..31b6e7797a3 100644 --- a/packages/@react-spectrum/s2/src/TimeField.tsx +++ b/packages/@react-spectrum/s2/src/TimeField.tsx @@ -18,7 +18,7 @@ import { TimeValue } from 'react-aria-components'; import {createContext, forwardRef, ReactElement, Ref, useContext} from 'react'; -import {DateInput, InvalidIndicator} from './DateField'; +import {DateInput, DateInputContainer, InvalidIndicator} from './DateField'; import {field, fieldInput, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; import {FieldGroup, FieldLabel, HelpText} from './Field'; import {forwardRefType, HelpTextProps, SpectrumLabelableProps} from '@react-types/shared'; @@ -96,7 +96,9 @@ export const TimeField = /*#__PURE__*/ (forwardRef as forwardRefType)(function T ...fieldInput(), paddingX: 'edge-to-text' })({size})}> - + + + = { errorMessage: {control: {type: 'text'}}, contextualHelp: {table: {disable: true}} }, - title: 'DateField' + title: 'DateField', + decorators: [ + (Story) => ( + + + + ) + ] }; export default meta; type Story = StoryObj; export const Example: Story = { + args: { + label: 'Birthday' + } +}; + +export const AriaLabel: Story = { args: { 'aria-label': 'Birthday' } diff --git a/packages/@react-spectrum/s2/stories/DatePicker.stories.tsx b/packages/@react-spectrum/s2/stories/DatePicker.stories.tsx index c5828774283..6d9faab4b4d 100644 --- a/packages/@react-spectrum/s2/stories/DatePicker.stories.tsx +++ b/packages/@react-spectrum/s2/stories/DatePicker.stories.tsx @@ -11,7 +11,7 @@ */ import {Button, Content, ContextualHelp, DatePicker, Footer, Form, Heading, Link, Text} from '../src'; -import {categorizeArgTypes} from './utils'; +import {CalendarSwitcher, categorizeArgTypes} from './utils'; import {fn} from '@storybook/test'; import type {Meta, StoryObj} from '@storybook/react'; import {style} from '../style' with {type: 'macro'}; @@ -27,13 +27,26 @@ const meta: Meta = { label: {control: {type: 'text'}}, description: {control: {type: 'text'}}, errorMessage: {control: {type: 'text'}}, - contextualHelp: {table: {disable: true}} + contextualHelp: {table: {disable: true}}, + visibleMonths: { + control: { + type: 'select' + }, + options: [1, 2, 3] + } }, args: { onOpenChange: fn(), onChange: fn() }, - title: 'DatePicker' + title: 'DatePicker', + decorators: [ + (Story) => ( + + + + ) + ] }; export default meta; diff --git a/packages/@react-spectrum/s2/stories/DateRangePicker.stories.tsx b/packages/@react-spectrum/s2/stories/DateRangePicker.stories.tsx index f12cf3f4022..4e025518f41 100644 --- a/packages/@react-spectrum/s2/stories/DateRangePicker.stories.tsx +++ b/packages/@react-spectrum/s2/stories/DateRangePicker.stories.tsx @@ -11,7 +11,7 @@ */ import {Button, Content, ContextualHelp, DateRangePicker, Footer, Form, Heading, Link, Text} from '../src'; -import {categorizeArgTypes} from './utils'; +import {CalendarSwitcher, categorizeArgTypes} from './utils'; import {fn} from '@storybook/test'; import type {Meta, StoryObj} from '@storybook/react'; import {style} from '../style' with {type: 'macro'}; @@ -27,13 +27,26 @@ const meta: Meta = { label: {control: {type: 'text'}}, description: {control: {type: 'text'}}, errorMessage: {control: {type: 'text'}}, - contextualHelp: {table: {disable: true}} + contextualHelp: {table: {disable: true}}, + visibleMonths: { + control: { + type: 'select' + }, + options: [1, 2, 3] + } }, args: { onOpenChange: fn(), onChange: fn() }, - title: 'DateRangePicker' + title: 'DateRangePicker', + decorators: [ + (Story) => ( + + + + ) + ] }; export default meta; diff --git a/packages/@react-spectrum/s2/stories/TimeField.stories.tsx b/packages/@react-spectrum/s2/stories/TimeField.stories.tsx index 6880b6beb46..5aa597409bb 100644 --- a/packages/@react-spectrum/s2/stories/TimeField.stories.tsx +++ b/packages/@react-spectrum/s2/stories/TimeField.stories.tsx @@ -11,7 +11,7 @@ */ import {Button, Content, ContextualHelp, Footer, Form, Heading, Link, Text, TimeField} from '../src'; -import {categorizeArgTypes} from './utils'; +import {CalendarSwitcher, categorizeArgTypes} from './utils'; import type {Meta, StoryObj} from '@storybook/react'; import {style} from '../style' with {type: 'macro'}; @@ -28,7 +28,14 @@ const meta: Meta = { errorMessage: {control: {type: 'text'}}, contextualHelp: {table: {disable: true}} }, - title: 'TimeField' + title: 'TimeField', + decorators: [ + (Story) => ( + + + + ) + ] }; export default meta; diff --git a/packages/@react-spectrum/s2/stories/utils.tsx b/packages/@react-spectrum/s2/stories/utils.tsx index 1d5b3df1821..706d8fe7e69 100644 --- a/packages/@react-spectrum/s2/stories/utils.tsx +++ b/packages/@react-spectrum/s2/stories/utils.tsx @@ -11,10 +11,10 @@ */ -import {PropsWithChildren, ReactElement, ReactNode, useState} from 'react'; +import {Collection, Header, Heading, Picker, PickerItem, PickerSection, Provider} from '../src'; +import {Key, useLocale} from 'react-aria'; +import {PropsWithChildren, ReactElement, ReactNode, useMemo, useState} from 'react'; import {style} from '../style' with {type: 'macro'}; -import { Key, useLocale } from 'react-aria'; -import { Picker, PickerItem, Provider } from '../src'; type StaticColor = 'black' | 'white' | 'auto' | undefined; @@ -73,7 +73,28 @@ export function categorizeArgTypes(category: string, args: string[]): any { }, {}); } -const calendars = [ +// https://github.com/unicode-org/cldr/blob/22af90ae3bb04263f651323ce3d9a71747a75ffb/common/supplemental/supplementalData.xml#L4649-L4664 +const preferences = [ + {locale: '', label: 'Default', ordering: 'gregory'}, + {label: 'Arabic (Algeria)', locale: 'ar-DZ', territories: 'DJ DZ EH ER IQ JO KM LB LY MA MR OM PS SD SY TD TN YE', ordering: 'gregory islamic islamic-civil islamic-tbla'}, + {label: 'Arabic (United Arab Emirates)', locale: 'ar-AE', territories: 'AE BH KW QA', ordering: 'gregory islamic-umalqura islamic islamic-civil islamic-tbla'}, + {label: 'Arabic (Egypt)', locale: 'AR-EG', territories: 'EG', ordering: 'gregory coptic islamic islamic-civil islamic-tbla'}, + {label: 'Arabic (Saudi Arabia)', locale: 'ar-SA', territories: 'SA', ordering: 'islamic-umalqura gregory islamic islamic-rgsa'}, + {label: 'Farsi (Afghanistan)', locale: 'fa-AF', territories: 'AF IR', ordering: 'persian gregory islamic islamic-civil islamic-tbla'}, + // {territories: 'CN CX HK MO SG', ordering: 'gregory chinese'}, + {label: 'Amharic (Ethiopia)', locale: 'am-ET', territories: 'ET', ordering: 'gregory ethiopic ethioaa'}, + {label: 'Hebrew (Israel)', locale: 'he-IL', territories: 'IL', ordering: 'gregory hebrew islamic islamic-civil islamic-tbla'}, + {label: 'Hindi (India)', locale: 'hi-IN', territories: 'IN', ordering: 'gregory indian'}, + {label: 'Japanese (Japan)', locale: 'ja-JP', territories: 'JP', ordering: 'gregory japanese'}, + // {territories: 'KR', ordering: 'gregory dangi'}, + {label: 'Thai (Thailand)', locale: 'th-TH', territories: 'TH', ordering: 'buddhist gregory'}, + {label: 'Chinese (Taiwan)', locale: 'zh-TW', territories: 'TW', ordering: 'gregory roc chinese'} +]; +type Calendar = { + id: string, + name: string +}; +const calendars: Calendar[] = [ {id: 'gregory', name: 'Gregorian'}, {id: 'japanese', name: 'Japanese'}, {id: 'buddhist', name: 'Buddhist'}, @@ -90,15 +111,47 @@ const calendars = [ ]; export function CalendarSwitcher(props: PropsWithChildren): ReactElement { - let {locale} = useLocale(); - let [calendarLocale, setCalendarLocale] = useState(null); - calendarLocale ??= calendars[0].id; + let [locale, setLocale] = useState(''); + let [calendar, setCalendar] = useState(calendars[0].id); + let {locale: defaultLocale} = useLocale(); + + let pref = preferences.find(p => p.locale === locale)!; + let preferredCalendars: Calendar[] = useMemo(() => pref ? pref.ordering.split(' ').map(p => calendars.find(c => c.id === p)).filter(c => c !== undefined) : [calendars[0]], [pref]); + let otherCalendars: Calendar[] = useMemo(() => calendars.filter(c => !preferredCalendars.some(p => p!.id === c.id)), [preferredCalendars]); + + let updateLocale = locale => { + setLocale(locale); + let pref = preferences.find(p => p.locale === locale); + if (pref) { + setCalendar(pref.ordering.split(' ')[0]); + } + }; return ( -
- - {item => {item.name}} - - +
+
+ + {item => {item.label}} + + + +
+ Preferred +
+ + {item => {item!.name}} + +
+ +
+ Other +
+ + {item => {item.name}} + +
+
+
+ {props.children}
From 33628a08500eca983d62ef161763c56dc619373d Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Tue, 1 Jul 2025 17:30:56 +1000 Subject: [PATCH 31/33] Add hcm support --- packages/@react-spectrum/s2/src/Calendar.tsx | 67 +++++++++++++++---- packages/@react-spectrum/s2/src/DateField.tsx | 20 ++++-- .../@react-spectrum/s2/src/DatePicker.tsx | 5 +- .../s2/style/spectrum-theme.ts | 3 +- 4 files changed, 75 insertions(+), 20 deletions(-) diff --git a/packages/@react-spectrum/s2/src/Calendar.tsx b/packages/@react-spectrum/s2/src/Calendar.tsx index 6e977798620..a0656962c5f 100644 --- a/packages/@react-spectrum/s2/src/Calendar.tsx +++ b/packages/@react-spectrum/s2/src/Calendar.tsx @@ -136,9 +136,12 @@ const cellStyles = style({ justifyContent: 'center' }); -const cellInnerStyles = style({ +const cellInnerStyles = style({ ...focusRing(), - transition: 'default', + transition: { + default: 'default', + forcedColors: 'none' + }, outlineOffset: { default: -2, isToday: 2, @@ -160,6 +163,7 @@ const cellInnerStyles = style({ display: 'flex', alignItems: 'center', justifyContent: 'center', + forcedColorAdjust: 'none', backgroundColor: { default: 'transparent', isHovered: 'gray-100', @@ -194,14 +198,38 @@ const cellInnerStyles = style({ isPressed: lightDark('accent-1000', 'accent-600'), isFocusVisible: lightDark('accent-1000', 'accent-600') }, - isUnavailable: 'transparent' + isUnavailable: 'transparent', + forcedColors: { + default: 'transparent', + isToday: 'ButtonFace', + isHovered: 'Highlight', + isSelected: { + selectionMode: { + single: 'Highlight', + range: { + isHovered: 'Highlight' + } + } + }, + isSelectionStart: 'Highlight', + isSelectionEnd: 'Highlight', + isUnavailable: 'transparent' + } }, color: { default: 'neutral', isSelected: 'white', isSelectionStart: 'white', isSelectionEnd: 'white', - isDisabled: 'disabled' + isDisabled: 'disabled', + forcedColors: { + default: 'ButtonText', + isToday: 'ButtonFace', + isSelected: 'HighlightText', + isSelectionStart: 'HighlightText', + isSelectionEnd: 'HighlightText', + isDisabled: 'GrayText' + } } }); @@ -225,10 +253,21 @@ const selectionSpanStyles = style({ bottom: 0, borderWidth: 2, borderStyle: 'dashed', - borderColor: 'blue-800', // focus-indicator-color + borderColor: { + default: 'blue-800', // focus-indicator-color + forcedColors: { + default: 'ButtonText' + } + }, borderStartRadius: 'full', borderEndRadius: 'full', - backgroundColor: 'blue-subtle' + backgroundColor: { + default: 'blue-subtle', + forcedColors: { + default: 'Highlight' + } + }, + forcedColorAdjust: 'none' }); export const helpTextStyles = style({ @@ -397,24 +436,25 @@ export const CalendarCell = (props: Omit & {first // Calculate the day and week index based on the date. let {dayIndex, weekIndex} = useWeekAndDayIndices(props.date, locale, firstDayOfWeek); - let isFirstWeek = weekIndex === 0; - let isFirstChild = dayIndex === 0; - let isLastChild = dayIndex === 6; - let calendarStateContext = useContext(CalendarStateContext); let rangeCalendarStateContext = useContext(RangeCalendarStateContext); let state = (calendarStateContext ?? rangeCalendarStateContext)!; + + let isFirstWeek = weekIndex === 0; + let isFirstChild = dayIndex === 0; + let isLastChild = dayIndex === 6; + let isNextDaySelected = state.isSelected(props.date.add({days: 1})); return ( cellStyles({...renderProps, isFirstChild, isLastChild, isFirstWeek})}> - {(renderProps) => } + {(renderProps) => } ); }; -const CalendarCellInner = (props: Omit & {isRangeSelection: boolean, state: CalendarState | RangeCalendarState, weekIndex: number, dayIndex: number, renderProps?: CalendarCellRenderProps, date: DateValue}): ReactElement => { - let {weekIndex, dayIndex, date, renderProps, state, isRangeSelection} = props; +const CalendarCellInner = (props: Omit & {isNextDaySelected: boolean, isLastChild: boolean, isRangeSelection: boolean, state: CalendarState | RangeCalendarState, weekIndex: number, dayIndex: number, renderProps?: CalendarCellRenderProps, date: DateValue}): ReactElement => { + let {weekIndex, dayIndex, date, renderProps, state, isRangeSelection, isNextDaySelected, isLastChild} = props; let {getDatesInWeek} = state; let ref = useRef(null); let {isUnavailable, formattedDate, isSelected} = renderProps!; @@ -432,6 +472,7 @@ const CalendarCellInner = (props: Omit & {isRange let isBackgroundStyleApplied = ( isSelected + && (isLastChild || !isNextDaySelected) && isRangeSelection && (state.isSelected(date.subtract({days: 1})) || state.isSelected(date.add({days: 1})) diff --git a/packages/@react-spectrum/s2/src/DateField.tsx b/packages/@react-spectrum/s2/src/DateField.tsx index b2ce70a4579..6894799a457 100644 --- a/packages/@react-spectrum/s2/src/DateField.tsx +++ b/packages/@react-spectrum/s2/src/DateField.tsx @@ -15,10 +15,12 @@ import { DateFieldProps as AriaDateFieldProps, DateInput as AriaDateInput, DateSegment as AriaDateSegment, + DateSegmentProps, ContextValue, DateInputProps, DateValue, - FormContext + FormContext, + DateSegmentRenderProps } from 'react-aria-components'; import {createContext, forwardRef, PropsWithChildren, ReactElement, Ref, useContext} from 'react'; import {field, fieldInput, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; @@ -53,22 +55,30 @@ const segmentContainer = style({ alignItems: 'center' }); -const dateSegment = style({ +const dateSegment = style({ outlineStyle: 'none', caretColor: 'transparent', backgroundColor: { default: 'transparent', - isFocused: 'blue-800' + isFocused: 'blue-800', + forcedColors: { + default: 'transparent', + isFocused: 'Highlight' + } }, color: { - isFocused: 'white' + isFocused: 'white', + forcedColors: { + isFocused: 'HighlightText' + } }, borderRadius: '[2px]', paddingX: { default: 2, isPunctuation: 0 }, - paddingY: 2 + paddingY: 2, + forcedColorAdjust: 'none' }); const iconStyles = style({ diff --git a/packages/@react-spectrum/s2/src/DatePicker.tsx b/packages/@react-spectrum/s2/src/DatePicker.tsx index 0070fed5a9d..29bd236a3ff 100644 --- a/packages/@react-spectrum/s2/src/DatePicker.tsx +++ b/packages/@react-spectrum/s2/src/DatePicker.tsx @@ -83,7 +83,10 @@ const inputButton = style('zIndex'), // eslint-disable-next-line @typescript-eslint/no-unused-vars disableTapHighlight: new ArbitraryProperty('-webkit-tap-highlight-color', (_value: true) => 'rgba(0,0,0,0)'), - unicodeBidi: ['normal', 'embed', 'bidi-override', 'isolate', 'isolate-override', 'plaintext'] as const + unicodeBidi: ['normal', 'embed', 'bidi-override', 'isolate', 'isolate-override', 'plaintext'] as const, + caretColor: ['auto', 'transparent'] as const }, shorthands: { padding: ['paddingTop', 'paddingBottom', 'paddingStart', 'paddingEnd'] as const, From fad7550519dc2449623c5a4e4ca129f3b34c43fe Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Tue, 1 Jul 2025 20:05:05 +1000 Subject: [PATCH 32/33] add chromatic stories and fix bugs --- .../s2/chromatic/Calendar.stories.tsx | 152 ++++++++++++++++++ .../s2/chromatic/DateField.stories.tsx | 62 +++++++ .../s2/chromatic/DatePicker.stories.tsx | 111 +++++++++++++ .../s2/chromatic/DateRangePicker.stories.tsx | 122 ++++++++++++++ .../s2/chromatic/RangeCalendar.stories.tsx | 89 ++++++++++ .../s2/chromatic/TimeField.stories.tsx | 61 +++++++ packages/@react-spectrum/s2/src/Calendar.tsx | 77 ++++++--- packages/@react-spectrum/s2/src/DateField.tsx | 5 +- .../@react-spectrum/s2/src/RangeCalendar.tsx | 26 +-- 9 files changed, 657 insertions(+), 48 deletions(-) create mode 100644 packages/@react-spectrum/s2/chromatic/Calendar.stories.tsx create mode 100644 packages/@react-spectrum/s2/chromatic/DateField.stories.tsx create mode 100644 packages/@react-spectrum/s2/chromatic/DatePicker.stories.tsx create mode 100644 packages/@react-spectrum/s2/chromatic/DateRangePicker.stories.tsx create mode 100644 packages/@react-spectrum/s2/chromatic/RangeCalendar.stories.tsx create mode 100644 packages/@react-spectrum/s2/chromatic/TimeField.stories.tsx diff --git a/packages/@react-spectrum/s2/chromatic/Calendar.stories.tsx b/packages/@react-spectrum/s2/chromatic/Calendar.stories.tsx new file mode 100644 index 00000000000..6a49de66027 --- /dev/null +++ b/packages/@react-spectrum/s2/chromatic/Calendar.stories.tsx @@ -0,0 +1,152 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you 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 REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {Calendar} from '../src'; +import {CalendarDate} from '@internationalized/date'; +import {Custom454Calendar} from '../../../@internationalized/date/tests/customCalendarImpl'; +import {DateValue} from 'react-aria'; +import type {Meta, StoryObj} from '@storybook/react'; +import {screen, userEvent, within} from '@storybook/test'; + +const meta: Meta = { + component: Calendar, + parameters: { + chromaticProvider: {disableAnimations: true} + }, + title: 'S2 Chromatic/Calendar' +}; + +export default meta; + +type Story = StoryObj; + +const date = new CalendarDate(2022, 2, 3); + +export const Default: Story = { + args: { + defaultFocusedValue: date + } +}; + +export const MultiMonth: Story = { + args: { + defaultFocusedValue: date, + visibleMonths: 3 + } +}; + +export const UnavailableDays: Story = { + args: { + defaultFocusedValue: date, + minValue: new CalendarDate(2022, 2, 2), + maxValue: new CalendarDate(2022, 2, 20), + isDateUnavailable: (date: DateValue) => { + return date.day >= 15 && date.day <= 18; + }, + isInvalid: true, + errorMessage: 'Invalid date' + } +}; + +export const CustomCalendar: Story = { + args: { + defaultFocusedValue: date, + createCalendar: () => new Custom454Calendar() + }, + parameters: { + chromaticProvider: { + // only works for en-US? + locales: ['en-US'] + } + } +}; + +export const DefaultHover: Story = { + args: { + defaultFocusedValue: date + }, + play: async () => { + let grid = screen.getByRole('grid'); + let cell = within(grid).getAllByRole('button')[7]; + await userEvent.hover(cell); + }, + parameters: { + chromaticProvider: { + colorSchemes: ['light'], + backgrounds: ['base'], + locales: ['en-US'], + disableAnimations: true + } + } +}; + +export const DefaultClick: Story = { + args: { + defaultFocusedValue: date + }, + play: async () => { + let grid = screen.getByRole('grid'); + let cell = within(grid).getAllByRole('button')[7]; + await userEvent.hover(cell); + await userEvent.click(cell); + await userEvent.unhover(cell); + }, + parameters: { + chromaticProvider: { + colorSchemes: ['light'], + backgrounds: ['base'], + locales: ['en-US'], + disableAnimations: true + } + } +}; + +export const DefaultKeyboardFocus: Story = { + args: { + defaultFocusedValue: date + }, + play: async () => { + await userEvent.tab(); + await userEvent.tab(); + await userEvent.tab(); + await userEvent.keyboard('{ArrowDown}'); + }, + parameters: { + chromaticProvider: { + colorSchemes: ['light'], + backgrounds: ['base'], + locales: ['en-US'], + disableAnimations: true + } + } +}; + +export const DefaultKeyboardSelected: Story = { + args: { + defaultFocusedValue: date + }, + play: async () => { + await userEvent.tab(); + await userEvent.tab(); + await userEvent.tab(); + await userEvent.keyboard('{ArrowDown}'); + await userEvent.keyboard('{Enter}'); + }, + parameters: { + chromaticProvider: { + colorSchemes: ['light'], + backgrounds: ['base'], + locales: ['en-US'], + disableAnimations: true + } + } +}; diff --git a/packages/@react-spectrum/s2/chromatic/DateField.stories.tsx b/packages/@react-spectrum/s2/chromatic/DateField.stories.tsx new file mode 100644 index 00000000000..bdae39164f9 --- /dev/null +++ b/packages/@react-spectrum/s2/chromatic/DateField.stories.tsx @@ -0,0 +1,62 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you 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 REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {CalendarDate} from '@internationalized/date'; +import {DateField} from '../'; +import type {Meta, StoryObj} from '@storybook/react'; +import {userEvent} from '@storybook/test'; + +const meta: Meta = { + component: DateField, + parameters: { + chromaticProvider: {disableAnimations: true} + }, + title: 'S2 Chromatic/DateField' +}; + +export default meta; + +type Story = StoryObj; + +const date = new CalendarDate(2022, 2, 3); + +export const Default: Story = { + args: { + label: 'Date of birth' + } +}; + +export const WithValue: Story = { + args: { + label: 'Date of birth', + value: date + } +}; + +export const Focused: Story = { + args: { + label: 'Date of birth', + value: date + }, + play: async () => { + await userEvent.tab(); + await userEvent.keyboard('{ArrowRight}'); + }, + parameters: { + chromaticProvider: { + colorSchemes: ['light'], + backgrounds: ['base'], + locales: ['en-US'], + disableAnimations: true + } + } +}; diff --git a/packages/@react-spectrum/s2/chromatic/DatePicker.stories.tsx b/packages/@react-spectrum/s2/chromatic/DatePicker.stories.tsx new file mode 100644 index 00000000000..3569333ccee --- /dev/null +++ b/packages/@react-spectrum/s2/chromatic/DatePicker.stories.tsx @@ -0,0 +1,111 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you 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 REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {CalendarDate, CalendarDateTime} from '@internationalized/date'; +import {DatePicker} from '../'; +import type {Meta, StoryObj} from '@storybook/react'; +import {userEvent} from '@storybook/test'; + +const meta: Meta = { + component: DatePicker, + parameters: { + chromaticProvider: {disableAnimations: true} + }, + title: 'S2 Chromatic/DatePicker' +}; + +export default meta; + +type Story = StoryObj; + +const date = new CalendarDate(2022, 2, 3); + +export const Default: Story = { + args: { + label: 'Date of birth' + } +}; + +export const WithValue: Story = { + args: { + label: 'Date of birth', + value: date + } +}; + +export const Focused: Story = { + args: { + label: 'Date of birth', + value: date + }, + play: async () => { + await userEvent.tab(); + await userEvent.keyboard('{ArrowRight}'); + }, + parameters: { + chromaticProvider: { + colorSchemes: ['light'], + backgrounds: ['base'], + locales: ['en-US'], + disableAnimations: true + } + } +}; + +export const OpenPicker: Story = { + args: { + label: 'Date of birth', + value: date + }, + play: async () => { + await userEvent.tab(); + await userEvent.keyboard('{ArrowRight}'); + await userEvent.keyboard('{ArrowRight}'); + await userEvent.keyboard('{ArrowRight}'); + await userEvent.keyboard('{Enter}'); + }, + parameters: { + chromaticProvider: { + colorSchemes: ['light'], + backgrounds: ['base'], + locales: ['en-US'], + disableAnimations: true + } + } +}; + +export const OpenPickerWithTime: Story = { + args: { + label: 'Date of birth', + value: new CalendarDateTime(2022, 2, 3, 12, 0, 0), + granularity: 'second' + }, + play: async () => { + await userEvent.tab(); + await userEvent.keyboard('{ArrowRight}'); + await userEvent.keyboard('{ArrowRight}'); + await userEvent.keyboard('{ArrowRight}'); + await userEvent.keyboard('{ArrowRight}'); + await userEvent.keyboard('{ArrowRight}'); + await userEvent.keyboard('{ArrowRight}'); + await userEvent.keyboard('{ArrowRight}'); + await userEvent.keyboard('{Enter}'); + }, + parameters: { + chromaticProvider: { + colorSchemes: ['light'], + backgrounds: ['base'], + locales: ['en-US'], + disableAnimations: true + } + } +}; diff --git a/packages/@react-spectrum/s2/chromatic/DateRangePicker.stories.tsx b/packages/@react-spectrum/s2/chromatic/DateRangePicker.stories.tsx new file mode 100644 index 00000000000..db68daecacc --- /dev/null +++ b/packages/@react-spectrum/s2/chromatic/DateRangePicker.stories.tsx @@ -0,0 +1,122 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you 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 REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {CalendarDate, CalendarDateTime} from '@internationalized/date'; +import {DateRangePicker} from '../'; +import type {Meta, StoryObj} from '@storybook/react'; +import {userEvent} from '@storybook/test'; + +const meta: Meta = { + component: DateRangePicker, + parameters: { + chromaticProvider: {disableAnimations: true} + }, + title: 'S2 Chromatic/DateRangePicker' +}; + +export default meta; + +type Story = StoryObj; + +const startDate = new CalendarDate(2022, 2, 3); +const endDate = new CalendarDate(2022, 2, 10); + +export const Default: Story = { + args: { + label: 'Date of birth' + } +}; + +export const WithValue: Story = { + args: { + label: 'Date of birth', + value: {start: startDate, end: endDate} + } +}; + +export const Focused: Story = { + args: { + label: 'Date of birth', + value: {start: startDate, end: endDate} + }, + play: async () => { + await userEvent.tab(); + await userEvent.keyboard('{ArrowRight}'); + }, + parameters: { + chromaticProvider: { + colorSchemes: ['light'], + backgrounds: ['base'], + locales: ['en-US'], + disableAnimations: true + } + } +}; + +export const OpenPicker: Story = { + args: { + label: 'Date of birth', + value: {start: startDate, end: endDate} + }, + play: async () => { + await userEvent.tab(); + await userEvent.keyboard('{ArrowRight}'); + await userEvent.keyboard('{ArrowRight}'); + await userEvent.keyboard('{ArrowRight}'); + await userEvent.keyboard('{ArrowRight}'); + await userEvent.keyboard('{ArrowRight}'); + await userEvent.keyboard('{ArrowRight}'); + await userEvent.keyboard('{Enter}'); + }, + parameters: { + chromaticProvider: { + colorSchemes: ['light'], + backgrounds: ['base'], + locales: ['en-US'], + disableAnimations: true + } + } +}; + +export const OpenPickerWithTime: Story = { + args: { + label: 'Date of birth', + value: {start: new CalendarDateTime(2022, 2, 3, 12, 0, 0), end: new CalendarDateTime(2022, 2, 10, 18, 40, 35)}, + granularity: 'second' + }, + play: async () => { + await userEvent.tab(); + await userEvent.keyboard('{ArrowRight}'); + await userEvent.keyboard('{ArrowRight}'); + await userEvent.keyboard('{ArrowRight}'); + await userEvent.keyboard('{ArrowRight}'); + await userEvent.keyboard('{ArrowRight}'); + await userEvent.keyboard('{ArrowRight}'); + await userEvent.keyboard('{ArrowRight}'); + await userEvent.keyboard('{ArrowRight}'); + await userEvent.keyboard('{ArrowRight}'); + await userEvent.keyboard('{ArrowRight}'); + await userEvent.keyboard('{ArrowRight}'); + await userEvent.keyboard('{ArrowRight}'); + await userEvent.keyboard('{ArrowRight}'); + await userEvent.keyboard('{ArrowRight}'); + await userEvent.keyboard('{Enter}'); + }, + parameters: { + chromaticProvider: { + colorSchemes: ['light'], + backgrounds: ['base'], + locales: ['en-US'], + disableAnimations: true + } + } +}; diff --git a/packages/@react-spectrum/s2/chromatic/RangeCalendar.stories.tsx b/packages/@react-spectrum/s2/chromatic/RangeCalendar.stories.tsx new file mode 100644 index 00000000000..d99beb64599 --- /dev/null +++ b/packages/@react-spectrum/s2/chromatic/RangeCalendar.stories.tsx @@ -0,0 +1,89 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you 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 REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {CalendarDate} from '@internationalized/date'; +import {Custom454Calendar} from '../../../@internationalized/date/tests/customCalendarImpl'; +import {DateValue} from 'react-aria'; +import type {Meta, StoryObj} from '@storybook/react'; +import {RangeCalendar} from '../src'; +import {userEvent} from '@storybook/test'; + +const meta: Meta = { + component: RangeCalendar, + parameters: { + chromaticProvider: {disableAnimations: true} + }, + title: 'S2 Chromatic/RangeCalendar' +}; + +export default meta; + +type Story = StoryObj; + +const date = new CalendarDate(2022, 2, 3); + +export const Default: Story = { + args: { + defaultFocusedValue: date + } +}; + +export const MultiMonth: Story = { + args: { + defaultFocusedValue: date, + visibleMonths: 3 + } +}; + +export const CustomCalendar: Story = { + args: { + defaultFocusedValue: date, + createCalendar: () => new Custom454Calendar() + }, + parameters: { + chromaticProvider: { + // only works for en-US? + locales: ['en-US'] + } + } +}; + +export const DefaultSelectRange: Story = { + args: { + defaultFocusedValue: date, + minValue: new CalendarDate(2022, 2, 2), + maxValue: new CalendarDate(2022, 2, 26), + isDateUnavailable: (date: DateValue) => { + return date.day >= 15 && date.day <= 18; + }, + allowsNonContiguousRanges: true + }, + play: async () => { + await userEvent.tab(); + await userEvent.tab(); + await userEvent.tab(); + await userEvent.keyboard('{ArrowRight}'); + await userEvent.keyboard('{Enter}'); + await userEvent.keyboard('{ArrowDown}'); + await userEvent.keyboard('{ArrowRight}'); + await userEvent.keyboard('{ArrowDown}'); + await userEvent.keyboard('{Enter}'); + }, + parameters: { + chromaticProvider: { + colorSchemes: ['light'], + backgrounds: ['base'], + locales: ['en-US'], + disableAnimations: true + } + } +}; diff --git a/packages/@react-spectrum/s2/chromatic/TimeField.stories.tsx b/packages/@react-spectrum/s2/chromatic/TimeField.stories.tsx new file mode 100644 index 00000000000..668d897006a --- /dev/null +++ b/packages/@react-spectrum/s2/chromatic/TimeField.stories.tsx @@ -0,0 +1,61 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you 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 REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import type {Meta, StoryObj} from '@storybook/react'; +import {Time} from '@internationalized/date'; +import {TimeField} from '../'; +import {userEvent} from '@storybook/test'; + +const meta: Meta = { + component: TimeField, + parameters: { + chromaticProvider: {disableAnimations: true} + }, + title: 'S2 Chromatic/TimeField' +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + label: 'Water plants reminder' + } +}; + +export const WithValue: Story = { + args: { + label: 'Water plants reminder', + value: new Time(2, 15, 56), + granularity: 'second' + } +}; + +export const Focused: Story = { + args: { + label: 'Water plants reminder', + value: new Time(18, 13) + }, + play: async () => { + await userEvent.tab(); + await userEvent.keyboard('{ArrowRight}'); + }, + parameters: { + chromaticProvider: { + colorSchemes: ['light'], + backgrounds: ['base'], + locales: ['en-US'], + disableAnimations: true + } + } +}; diff --git a/packages/@react-spectrum/s2/src/Calendar.tsx b/packages/@react-spectrum/s2/src/Calendar.tsx index a0656962c5f..c869e47882a 100644 --- a/packages/@react-spectrum/s2/src/Calendar.tsx +++ b/packages/@react-spectrum/s2/src/Calendar.tsx @@ -31,6 +31,7 @@ import { RangeCalendarStateContext, Text } from 'react-aria-components'; +import {AriaCalendarGridProps, getEraFormat} from '@react-aria/calendar'; import {baseColor, focusRing, lightDark, style} from '../style' with {type: 'macro'}; import { CalendarDate, @@ -41,7 +42,6 @@ import ChevronLeftIcon from '../s2wf-icons/S2_Icon_ChevronLeft_20_N.svg'; import ChevronRightIcon from '../s2wf-icons/S2_Icon_ChevronRight_20_N.svg'; import {controlFont, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; import {forwardRefType, ValidationResult} from '@react-types/shared'; -import {getEraFormat} from '@react-aria/calendar'; import React, {createContext, CSSProperties, ForwardedRef, forwardRef, Fragment, PropsWithChildren, ReactElement, ReactNode, useContext, useMemo, useRef} from 'react'; import {useDateFormatter, useLocale} from '@react-aria/i18n'; import {useSpectrumContextProps} from './useSpectrumContextProps'; @@ -218,7 +218,12 @@ const cellInnerStyles = style {Array.from({length: visibleMonths}).map((_, i) => ( - - - {(day) => ( - - {day} - - )} - - - {(date) => ( - - )} - - + ))}
{errorMessage && ( @@ -356,6 +348,33 @@ export const Calendar = /*#__PURE__*/ (forwardRef as forwardRefType)(function Ca ); }); +export const CalendarGrid = (props: Omit & PropsWithChildren & {months: number}): ReactElement => { + // use isolation to start a new stacking context so that we can use zIndex -1 for the selection span. + return ( + + + {(day) => ( + + {day} + + )} + + + {(date) => ( + + )} + + + ); +}; + // Ordinarily the heading is a formatted date range, ie January 2025 - February 2025. // However, we want to show each month individually. export const CalendarHeading = (): ReactElement => { @@ -412,16 +431,28 @@ export const CalendarHeading = (): ReactElement => { }; export const CalendarButton = (props: Omit & {children: ReactNode}): ReactElement => { + let {direction} = useLocale(); return ( - - {props.children} - +
+ + {props.children} + +
); }; -export const CalendarHeaderCell = (props: Omit & PropsWithChildren): ReactElement => { +const CalendarHeaderCell = (props: Omit & PropsWithChildren): ReactElement => { return ( {props.children} @@ -429,7 +460,7 @@ export const CalendarHeaderCell = (props: Omit & {firstDayOfWeek: 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat' | undefined}): ReactElement => { +const CalendarCell = (props: Omit & {firstDayOfWeek: 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat' | undefined}): ReactElement => { let {locale} = useLocale(); let defaultFirstDayOfWeek = useDefaultFirstDayOfWeek(locale); let firstDayOfWeek = props.firstDayOfWeek ?? defaultFirstDayOfWeek; diff --git a/packages/@react-spectrum/s2/src/DateField.tsx b/packages/@react-spectrum/s2/src/DateField.tsx index 6894799a457..7db48ebf41e 100644 --- a/packages/@react-spectrum/s2/src/DateField.tsx +++ b/packages/@react-spectrum/s2/src/DateField.tsx @@ -15,12 +15,11 @@ import { DateFieldProps as AriaDateFieldProps, DateInput as AriaDateInput, DateSegment as AriaDateSegment, - DateSegmentProps, ContextValue, DateInputProps, + DateSegmentRenderProps, DateValue, - FormContext, - DateSegmentRenderProps + FormContext } from 'react-aria-components'; import {createContext, forwardRef, PropsWithChildren, ReactElement, Ref, useContext} from 'react'; import {field, fieldInput, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; diff --git a/packages/@react-spectrum/s2/src/RangeCalendar.tsx b/packages/@react-spectrum/s2/src/RangeCalendar.tsx index b019fbd79a0..42a92bc483e 100644 --- a/packages/@react-spectrum/s2/src/RangeCalendar.tsx +++ b/packages/@react-spectrum/s2/src/RangeCalendar.tsx @@ -11,16 +11,13 @@ */ import { - CalendarGrid as AriaCalendarGrid, RangeCalendar as AriaRangeCalendar, RangeCalendarProps as AriaRangeCalendarProps, - CalendarGridBody, - CalendarGridHeader, ContextValue, DateValue, Text } from 'react-aria-components'; -import {CalendarButton, CalendarCell, CalendarHeaderCell, CalendarHeading} from './Calendar'; +import {CalendarButton, CalendarGrid, CalendarHeading} from './Calendar'; import ChevronLeftIcon from '../s2wf-icons/S2_Icon_ChevronLeft_20_N.svg'; import ChevronRightIcon from '../s2wf-icons/S2_Icon_ChevronRight_20_N.svg'; import {controlFont, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; @@ -111,24 +108,9 @@ export const RangeCalendar = /*#__PURE__*/ (forwardRef as forwardRefType)(functi gap: 24, width: 'full' })}> - {Array.from({length: visibleMonths}).map((_, i) => { - return ( - - - {(day) => ( - - {day} - - )} - - - {(date) => ( - - )} - - - ); - })} + {Array.from({length: visibleMonths}).map((_, i) => ( + + ))}
{errorMessage && ( From 6ee3dc6e12360ff272762e76f5835a8c0f67b9df Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Tue, 1 Jul 2025 20:15:35 +1000 Subject: [PATCH 33/33] remove problematic story --- .../s2/chromatic/Calendar.stories.tsx | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/packages/@react-spectrum/s2/chromatic/Calendar.stories.tsx b/packages/@react-spectrum/s2/chromatic/Calendar.stories.tsx index 6a49de66027..c082bef108f 100644 --- a/packages/@react-spectrum/s2/chromatic/Calendar.stories.tsx +++ b/packages/@react-spectrum/s2/chromatic/Calendar.stories.tsx @@ -89,27 +89,6 @@ export const DefaultHover: Story = { } }; -export const DefaultClick: Story = { - args: { - defaultFocusedValue: date - }, - play: async () => { - let grid = screen.getByRole('grid'); - let cell = within(grid).getAllByRole('button')[7]; - await userEvent.hover(cell); - await userEvent.click(cell); - await userEvent.unhover(cell); - }, - parameters: { - chromaticProvider: { - colorSchemes: ['light'], - backgrounds: ['base'], - locales: ['en-US'], - disableAnimations: true - } - } -}; - export const DefaultKeyboardFocus: Story = { args: { defaultFocusedValue: date
+
))}