Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 56 additions & 31 deletions packages/@react-spectrum/s2/src/Calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,36 +72,59 @@ export interface CalendarProps<T extends DateValue>

export const CalendarContext = createContext<ContextValue<Partial<CalendarProps<any>>, HTMLDivElement>>(null);

const calendarStyles = style({
const calendarStyles = style<{isMultiMonth?: boolean}>({
display: 'flex',
containerType: {
default: 'inline-size',
isMultiMonth: 'unset'
},
flexDirection: 'column',
gap: 24,
width: 'fit',
disableTapHighlight: true
disableTapHighlight: true,
'--cell-gap': {
type: 'paddingStart',
value: 4
},
'--cell-max-width': {
type: 'width',
value: 32
},
'--cell-responsive-size': {
type: 'width',
value: {
default: '[min(var(--cell-max-width), (100cqw - (var(--cell-gap) * 12)) / 7)]',
isMultiMonth: '--cell-max-width'
}
},
width: {
default: 'calc(7 * var(--cell-max-width) + var(--cell-gap) * 12)',
isMultiMonth: 'fit'
},
maxWidth: {
default: 'full',
isMultiMonth: 'unset'
}
}, getAllowedOverrides());

const headerStyles = style({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
width: 'full'
justifyContent: 'space-between'
});

const headingStyles = style({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
margin: 0,
width: 'full'
flexGrow: 1
});

const titleStyles = style({
font: 'title-lg',
textAlign: 'center',
flexGrow: 1,
flexShrink: 0,
flexBasis: '0%',
minWidth: 0
flexShrink: 0
});

const headerCellStyles = style({
Expand All @@ -121,10 +144,7 @@ const headerCellStyles = style({

const cellStyles = style({
outlineStyle: 'none',
'--cell-gap': {
type: 'paddingStart',
value: 4
},
boxSizing: 'content-box',
paddingStart: {
default: 4,
isFirstChild: 0
Expand All @@ -142,15 +162,16 @@ const cellStyles = style({
isLastWeek: 0
},
position: 'relative',
width: 32,
height: 32,
display: {
default: 'flex',
isOutsideMonth: 'none'
},
alignItems: 'center',
justifyContent: 'center',
disableTapHighlight: true
disableTapHighlight: true,
width: '--cell-responsive-size',
aspectRatio: 'square',
height: 'auto'
});

const cellInnerStyles = style<CalendarCellRenderProps & {selectionMode: 'single' | 'range'}>({
Expand All @@ -174,7 +195,7 @@ const cellInnerStyles = style<CalendarCellRenderProps & {selectionMode: 'single'
font: 'body-sm',
cursor: 'default',
width: 'full',
height: 32,
height: 'full',
borderRadius: 'full',
display: 'flex',
alignItems: 'center',
Expand Down Expand Up @@ -291,7 +312,7 @@ const cellInnerStyles = style<CalendarCellRenderProps & {selectionMode: 'single'

const todayStyles = style({
position: 'absolute',
bottom: 4,
bottom: '12.5%',
left: '50%',
transform: 'translateX(-50%)',
width: 4,
Expand Down Expand Up @@ -420,13 +441,14 @@ export const Calendar = /*#__PURE__*/ (forwardRef as forwardRefType)(function Ca
...otherProps
} = props;
let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/s2');
let isMultiMonth = visibleMonths > 1;
return (
<AriaCalendar
{...otherProps}
ref={ref}
visibleDuration={{months: visibleMonths}}
style={UNSAFE_style}
className={(UNSAFE_className || '') + calendarStyles(null, styles)}>
className={(UNSAFE_className || '') + calendarStyles({isMultiMonth}, styles)}>
{({isInvalid, isDisabled}) => {
return (
<>
Expand All @@ -435,11 +457,7 @@ export const Calendar = /*#__PURE__*/ (forwardRef as forwardRefType)(function Ca
[HeaderContext, null],
[HeadingContext, null]
]}>
<Header styles={headerStyles}>
<CalendarButton slot="previous"><ChevronLeftIcon /></CalendarButton>
<CalendarHeading />
<CalendarButton slot="next"><ChevronRightIcon /></CalendarButton>
</Header>
<CalendarHeader />
</Provider>
<div
className={style({
Expand All @@ -450,7 +468,7 @@ export const Calendar = /*#__PURE__*/ (forwardRef as forwardRefType)(function Ca
alignItems: 'start'
})}>
{Array.from({length: visibleMonths}).map((_, i) => (
<CalendarGrid months={i} key={i} />
<CalendarGrid key={i} months={i} />
))}
</div>
{isInvalid && (
Expand All @@ -465,6 +483,16 @@ export const Calendar = /*#__PURE__*/ (forwardRef as forwardRefType)(function Ca
);
});

export const CalendarHeader = (): ReactElement => {
return (
<Header styles={headerStyles}>
<CalendarButton slot="previous"><ChevronLeftIcon /></CalendarButton>
<CalendarHeading />
<CalendarButton slot="next"><ChevronRightIcon /></CalendarButton>
</Header>
);
};

export const CalendarGrid = (props: Omit<AriaCalendarGridProps, 'children'> & PropsWithChildren & {months: number}): ReactElement => {
let rangeCalendarProps = useSlottedContext(RangeCalendarContext);
let calendarProps = useSlottedContext(AriaCalendarContext);
Expand Down Expand Up @@ -497,7 +525,7 @@ export const CalendarGrid = (props: Omit<AriaCalendarGridProps, 'children'> & Pr

// 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 => {
const CalendarHeading = (): ReactElement => {
let calendarStateContext = useContext(CalendarStateContext);
let rangeCalendarStateContext = useContext(RangeCalendarStateContext);
let {visibleRange, timeZone} = calendarStateContext ?? rangeCalendarStateContext ?? {};
Expand Down Expand Up @@ -648,11 +676,8 @@ const CalendarCellInner = (props: Omit<CalendarCellProps, 'children'> & {isRange
<div
className={style({
position: 'relative',
width: 32,
'--cell-width': {
type: 'width',
value: '[self(width)]'
}
width: 'full',
height: 'full'
})}>
<div
ref={ref}
Expand Down
62 changes: 37 additions & 25 deletions packages/@react-spectrum/s2/src/RangeCalendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,12 @@ import {
RangeCalendarProps as AriaRangeCalendarProps,
DateValue
} from 'react-aria-components/RangeCalendar';
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 {CalendarGrid, CalendarHeader} from './Calendar';
import {ContextValue, Provider} from 'react-aria-components/slots';
import {createContext, ForwardedRef, forwardRef, ReactNode} from 'react';
import {createContext, CSSProperties, ForwardedRef, forwardRef, ReactNode} from 'react';
import {forwardRefType, GlobalDOMAttributes} from '@react-types/shared';
import {getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'};
import {Header, HeaderContext, HeadingContext} from './Content';
import {HeaderContext, HeadingContext} from './Content';
import {helpTextStyles} from './Field';
// @ts-ignore
import intlMessages from '../intl/*.json';
Expand All @@ -31,7 +29,6 @@ import {Text} from 'react-aria-components/Text';
import {useLocalizedStringFormatter} from 'react-aria/useLocalizedStringFormatter';
import {useSpectrumContextProps} from './useSpectrumContextProps';


export interface RangeCalendarProps<T extends DateValue>
extends Omit<AriaRangeCalendarProps<T>, 'visibleDuration' | 'style' | 'className' | 'render' | 'children' | 'styles' | keyof GlobalDOMAttributes>,
StyleProps {
Expand All @@ -48,21 +45,40 @@ export interface RangeCalendarProps<T extends DateValue>

export const RangeCalendarContext = createContext<ContextValue<Partial<RangeCalendarProps<any>>, HTMLDivElement>>(null);


const calendarStyles = style({
const calendarStyles = style<{isMultiMonth?: boolean}>({
display: 'flex',
containerType: {
default: 'inline-size',
isMultiMonth: 'unset'
},
flexDirection: 'column',
gap: 24,
width: 'fit'
disableTapHighlight: true,
'--cell-gap': {
type: 'paddingStart',
value: 4
},
'--cell-max-width': {
type: 'width',
value: 32
},
'--cell-responsive-size': {
type: 'width',
value: {
default: '[min(var(--cell-max-width), (100cqw - (var(--cell-gap) * 12)) / 7)]',
isMultiMonth: '--cell-max-width'
}
},
width: {
default: 'calc(7 * var(--cell-max-width) + var(--cell-gap) * 12)',
isMultiMonth: 'fit'
},
maxWidth: {
default: 'full',
isMultiMonth: 'unset'
}
}, getAllowedOverrides());

const headerStyles = style({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
width: 'full'
});

/**
* RangeCalendars display a grid of days in one or more months and allow users to select a contiguous range of dates.
*/
Expand All @@ -78,13 +94,14 @@ export const RangeCalendar = /*#__PURE__*/ (forwardRef as forwardRefType)(functi
} = props;
let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/s2');

let isMultiMonth = visibleMonths > 1;
return (
<AriaRangeCalendar
{...otherProps}
ref={ref}
visibleDuration={{months: visibleMonths}}
style={UNSAFE_style}
className={(UNSAFE_className || '') + calendarStyles(null, styles)}>
style={{...UNSAFE_style, '--num-calendars': visibleMonths} as CSSProperties}
className={(UNSAFE_className || '') + calendarStyles({isMultiMonth}, styles)}>
{({isInvalid, isDisabled}) => {
return (
<>
Expand All @@ -93,22 +110,17 @@ export const RangeCalendar = /*#__PURE__*/ (forwardRef as forwardRefType)(functi
[HeaderContext, null],
[HeadingContext, null]
]}>
<Header styles={headerStyles}>
<CalendarButton slot="previous"><ChevronLeftIcon /></CalendarButton>
<CalendarHeading />
<CalendarButton slot="next"><ChevronRightIcon /></CalendarButton>
</Header>
<CalendarHeader />
</Provider>
<div
className={style({
display: 'flex',
flexDirection: 'row',
gap: 24,
width: 'full',
alignItems: 'start'
})}>
{Array.from({length: visibleMonths}).map((_, i) => (
<CalendarGrid months={i} key={i} />
<CalendarGrid key={i} months={i} />
))}
</div>
{isInvalid && (
Expand Down
8 changes: 1 addition & 7 deletions packages/dev/s2-docs/pages/react-aria/Virtualizer.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ export default Layout;

import docs from 'docs:react-aria-components';
import {GroupedPropTable} from '../../src/PropTable';
import {InlineAlert, Heading, Content} from '@react-spectrum/s2';

export const tags = ['windowing', 'list', 'grid', 'infinite'];
export const description = 'Renders a scrollable collection of data using customizable layouts.';
Expand Down Expand Up @@ -42,11 +41,6 @@ for (let i = 0; i < 5000; i++) {

Virtualizer uses <TypeLink links={docs.links} type={docs.exports.Layout} /> objects to determine the position and size of each item, and provide the list of currently visible items. When using a Virtualizer, all items are positioned by the `Layout`, and CSS layout properties such as flexbox and grid do not apply.

<InlineAlert variant="notice" maxWidth={600}>
<Heading>Virtualized components must have a defined size</Heading>
<Content>This may be an explicit CSS `width` and `height`, or an implicit size (e.g. percentage or `flex`) bounded by an ancestor element. Without a bounded size, all items will be rendered to the DOM, negating the performance benefits of virtualized scrolling.</Content>
</InlineAlert>

### List

`ListLayout` places items along its orientation. Rows can be fixed or variable in size. When using a variable size, set the `estimatedRowSize` to a reasonable guess for how tall or wide the rows will be on average. This allows the size of the scrollbar to be calculated.
Expand Down Expand Up @@ -322,7 +316,7 @@ for (let i = 0; imageOptions.length < 500; i++) {
items={imageOptions}
style={{display: 'block', padding: 0, height: 250}}>
{(item) => (
<ListBoxItem textValue={item.title} aria-label={item.title} style={{height: '100%', padding: 12, overflow: 'hidden', boxSizing: 'border-box', border: '2px solid var(--border-color)', borderRadius: 8}}>
<ListBoxItem textValue={item.title} aria-label={item.title} style={{width: Math.round(200 * Number(item.aspectRatio) + 24), height: '100%', padding: 12, overflow: 'hidden', boxSizing: 'border-box', border: '2px solid var(--border-color)', borderRadius: 8}}>
<img src={item.image} alt="" style={{maxHeight: 200, aspectRatio: item.aspectRatio, borderRadius: 4}} />
</ListBoxItem>
)}
Expand Down
6 changes: 4 additions & 2 deletions packages/react-aria-components/stories/ListBox.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,8 @@ function generateRandomString(minLength: number, maxLength: number): string {

function VirtualizedListBoxRender(args): JSX.Element {
let {variableHeight, isLoading, orientation} = args;
let estimatedRowHeight = orientation === 'horizontal' ? 117 : 25;
let estimatedHeadingHeight = orientation === 'horizontal' ? 63 : 26;
let heightProperty = orientation === 'horizontal' ? 'width' : 'height';
let widthProperty = orientation === 'horizontal' ? 'height' : 'width';
let sections: {id: string, name: string, children: {id: string, name: string}[]}[] = [];
Expand All @@ -369,8 +371,8 @@ function VirtualizedListBoxRender(args): JSX.Element {
<Virtualizer
layout={new ListLayout({
orientation,
estimatedRowHeight: 25,
estimatedHeadingHeight: 26,
estimatedRowHeight,
estimatedHeadingHeight,
loaderHeight: 30
})}>
<ListBox orientation={orientation} className={styles.menu} style={{[heightProperty]: 400, [widthProperty]: 200}} aria-label="virtualized listbox">
Expand Down
Loading