Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TS Strict Calendar #6076

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/@internationalized/date/src/queries.ts
Expand Up @@ -238,7 +238,7 @@ export function getWeeksInMonth(date: DateValue, locale: string): number {
}

/** Returns the lesser of the two provider dates. */
export function minDate<A extends DateValue, B extends DateValue>(a: A, b: B): A | B {
export function minDate<A extends DateValue, B extends DateValue>(a?: A, b?: B): A | B | undefined {
if (a && b) {
return a.compare(b) <= 0 ? a : b;
}
Expand All @@ -247,7 +247,7 @@ export function minDate<A extends DateValue, B extends DateValue>(a: A, b: B): A
}

/** Returns the greater of the two provider dates. */
export function maxDate<A extends DateValue, B extends DateValue>(a: A, b: B): A | B {
export function maxDate<A extends DateValue, B extends DateValue>(a?: A, b?: B): A | B | undefined {
if (a && b) {
return a.compare(b) >= 0 ? a : b;
}
Expand Down
4 changes: 2 additions & 2 deletions packages/@react-aria/textfield/src/useTextField.ts
Expand Up @@ -81,7 +81,7 @@ export interface AriaTextFieldOptions<T extends TextFieldIntrinsicElements> exte
*/
inputElementType?: T,
/**
* Controls whether inputted text is automatically capitalized and, if so, in what manner.
* Controls whether inputted text is automatically capitalized and, if so, in what manner.
* See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/autocapitalize).
*/
autoCapitalize?: 'off' | 'none' | 'on' | 'sentences' | 'words' | 'characters'
Expand Down Expand Up @@ -166,7 +166,7 @@ export function useTextField<T extends TextFieldIntrinsicElements = DefaultEleme
labelProps,
inputProps: mergeProps(
domProps,
inputElementType === 'input' && inputOnlyProps,
inputElementType === 'input' ? inputOnlyProps : undefined,
{
disabled: isDisabled,
readOnly: isReadOnly,
Expand Down
2 changes: 1 addition & 1 deletion packages/@react-spectrum/calendar/src/Calendar.tsx
Expand Up @@ -34,7 +34,7 @@ function Calendar<T extends DateValue>(props: SpectrumCalendarProps<T>, ref: Foc
createCalendar
});

let domRef = useRef();
let domRef = useRef(null);
useImperativeHandle(ref, () => ({
...createDOMRef(domRef),
focus() {
Expand Down
6 changes: 3 additions & 3 deletions packages/@react-spectrum/calendar/src/CalendarBase.tsx
Expand Up @@ -22,7 +22,7 @@ import {DOMProps, StyleProps} from '@react-types/shared';
import {HelpText} from '@react-spectrum/label';
// @ts-ignore
import intlMessages from '../intl/*.json';
import React, {HTMLAttributes, RefObject} from 'react';
import React, {HTMLAttributes, JSX, RefObject} from 'react';
import styles from '@adobe/spectrum-css-temp/components/calendar/vars.css';
import {useDateFormatter, useLocale, useLocalizedStringFormatter} from '@react-aria/i18n';
import {VisuallyHidden} from '@react-aria/visually-hidden';
Expand Down Expand Up @@ -59,8 +59,8 @@ export function CalendarBase<T extends CalendarState | RangeCalendarState>(props
timeZone: state.timeZone
});

let titles = [];
let calendars = [];
let titles: JSX.Element[] = [];
let calendars: JSX.Element[] = [];
for (let i = 0; i < visibleMonths; i++) {
let d = currentMonth.add({months: i});
titles.push(
Expand Down
2 changes: 1 addition & 1 deletion packages/@react-spectrum/calendar/src/CalendarCell.tsx
Expand Up @@ -27,7 +27,7 @@ interface CalendarCellProps extends AriaCalendarCellProps {
}

export function CalendarCell({state, currentMonth, ...props}: CalendarCellProps) {
let ref = useRef<HTMLElement>();
let ref = useRef<HTMLElement>(null);
let {
cellProps,
buttonProps,
Expand Down
2 changes: 1 addition & 1 deletion packages/@react-spectrum/calendar/src/RangeCalendar.tsx
Expand Up @@ -34,7 +34,7 @@ function RangeCalendar<T extends DateValue>(props: SpectrumRangeCalendarProps<T>
createCalendar
});

let domRef = useRef();
let domRef = useRef(null);
useImperativeHandle(ref, () => ({
...createDOMRef(domRef),
focus() {
Expand Down
12 changes: 7 additions & 5 deletions packages/@react-spectrum/calendar/stories/Calendar.stories.tsx
Expand Up @@ -202,14 +202,16 @@ function Example(props) {
let [calendar, setCalendar] = React.useState<Key>(calendars[0].key);
let {locale: defaultLocale} = useLocale();

let pref = preferences.find(p => p.locale === locale);
let pref = preferences.find(p => p.locale === locale)!;
let preferredCalendars = React.useMemo(() => pref ? pref.ordering.split(' ').map(p => calendars.find(c => c.key === p)).filter(Boolean) : [calendars[0]], [pref]);
let otherCalendars = React.useMemo(() => calendars.filter(c => !preferredCalendars.some(p => p.key === c.key)), [preferredCalendars]);
let otherCalendars = React.useMemo(() => calendars.filter(c => !preferredCalendars.some(p => p!.key === c.key)), [preferredCalendars]);

let updateLocale = locale => {
setLocale(locale);
let pref = preferences.find(p => p.locale === locale);
setCalendar(pref.ordering.split(' ')[0]);
if (pref) {
setCalendar(pref.ordering.split(' ')[0]);
}
};

return (
Expand All @@ -220,14 +222,14 @@ function Example(props) {
</Picker>
<Picker label="Calendar" selectedKey={calendar} onSelectionChange={setCalendar}>
<Section title="Preferred" items={preferredCalendars}>
{item => <Item>{item.name}</Item>}
{item => <Item>{item!.name}</Item>}
</Section>
<Section title="Other" items={otherCalendars}>
{item => <Item>{item.name}</Item>}
</Section>
</Picker>
</Flex>
<Provider locale={(locale || defaultLocale) + (calendar && calendar !== preferredCalendars[0].key ? '-u-ca-' + calendar : '')}>
<Provider locale={(locale || defaultLocale) + (calendar && calendar !== preferredCalendars![0]!.key ? '-u-ca-' + calendar : '')}>
<View maxWidth="100vw" padding="size-10" overflow="auto">
<Calendar {...props} />
</View>
Expand Down
6 changes: 3 additions & 3 deletions packages/@react-stately/calendar/src/types.ts
Expand Up @@ -108,17 +108,17 @@ export interface CalendarState extends CalendarStateBase {

export interface RangeCalendarState extends CalendarStateBase {
/** The currently selected date range. */
readonly value: RangeValue<DateValue>,
readonly value: RangeValue<DateValue> | null,
/** Sets the currently selected date range. */
setValue(value: RangeValue<DateValue>): void,
setValue(value: RangeValue<DateValue> | null): void,
/** Highlights the given date during selection, e.g. by hovering or dragging. */
highlightDate(date: CalendarDate): void,
/** The current anchor date that the user clicked on to begin range selection. */
readonly anchorDate: CalendarDate | null,
/** Sets the anchor date that the user clicked on to begin range selection. */
setAnchorDate(date: CalendarDate | null): void,
/** The currently highlighted date range. */
readonly highlightedRange: RangeValue<CalendarDate>,
readonly highlightedRange: RangeValue<CalendarDate> | null,
/** Whether the user is currently dragging over the calendar. */
readonly isDragging: boolean,
/** Sets whether the user is dragging over the calendar. */
Expand Down
36 changes: 18 additions & 18 deletions packages/@react-stately/calendar/src/useCalendarState.ts
Expand Up @@ -27,13 +27,13 @@ import {
toCalendarDate,
today
} from '@internationalized/date';
import {CalendarProps, DateValue} from '@react-types/calendar';
import {CalendarProps, DateValue, MappedDateValue} from '@react-types/calendar';
import {CalendarState} from './types';
import {useControlledState} from '@react-stately/utils';
import {useMemo, useState} from 'react';
import {ValidationState} from '@react-types/shared';

export interface CalendarStateOptions<T extends DateValue = DateValue> extends CalendarProps<T> {
export interface CalendarStateOptions<T extends DateValue> extends CalendarProps<T> {
/** The locale to display and edit the value according to. */
locale: string,
/**
Expand All @@ -51,7 +51,6 @@ export interface CalendarStateOptions<T extends DateValue = DateValue> extends C
/** Determines how to align the initial selection relative to the visible date range. */
selectionAlignment?: 'start' | 'center' | 'end'
}

/**
* Provides state management for a calendar component.
* A calendar displays one or more date grids and allows users to select a single date.
Expand All @@ -71,7 +70,7 @@ export function useCalendarState<T extends DateValue = DateValue>(props: Calenda
} = props;
let calendar = useMemo(() => createCalendar(resolvedOptions.calendar), [createCalendar, resolvedOptions.calendar]);

let [value, setControlledValue] = useControlledState<DateValue>(props.value, props.defaultValue, props.onChange);
let [value, setControlledValue] = useControlledState<DateValue | null, MappedDateValue<T>>(props.value!, props.defaultValue ?? null!, props.onChange);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not certain we can fix useControlledState without overload syntax. This is the only way I've seen to prevent that syntax from propagating to every hook and component that uses useControlledState, but I'm open to other ideas.

I'm also unable to reproduce the issue in ts playground https://tinyurl.com/2p98znas
I don't know if that means I've over engineered it or have missed some setting

let calendarDateValue = useMemo(() => value ? toCalendar(toCalendarDate(value), calendar) : null, [value, calendar]);
let timeZone = useMemo(() => value && 'timeZone' in value ? value.timeZone : resolvedOptions.timeZone, [value, resolvedOptions.timeZone]);
let focusedCalendarDate = useMemo(() => (
Expand Down Expand Up @@ -138,25 +137,26 @@ export function useCalendarState<T extends DateValue = DateValue>(props: Calenda

function setValue(newValue: CalendarDate | null) {
if (!props.isDisabled && !props.isReadOnly) {
if (newValue === null) {
let localValue = newValue;
if (localValue === null) {
setControlledValue(null);
return;
}
newValue = constrainValue(newValue, minValue, maxValue);
newValue = previousAvailableDate(newValue, startDate, isDateUnavailable);
if (!newValue) {
localValue = constrainValue(localValue, minValue, maxValue);
localValue = previousAvailableDate(localValue, startDate, isDateUnavailable);
if (!localValue) {
return;
}

// The display calendar should not have any effect on the emitted value.
// Emit dates in the same calendar as the original value, if any, otherwise gregorian.
newValue = toCalendar(newValue, value?.calendar || new GregorianCalendar());
localValue = toCalendar(localValue, value?.calendar || new GregorianCalendar());

// Preserve time if the input value had one.
if (value && 'hour' in value) {
setControlledValue(value.set(newValue));
setControlledValue(value.set(localValue));
} else {
setControlledValue(newValue);
setControlledValue(localValue);
}
}
}
Expand Down Expand Up @@ -184,8 +184,8 @@ export function useCalendarState<T extends DateValue = DateValue>(props: Calenda
}, [pageBehavior, visibleDuration]);

return {
isDisabled: props.isDisabled,
isReadOnly: props.isReadOnly,
isDisabled: props.isDisabled ?? false,
isReadOnly: props.isReadOnly ?? false,
value: calendarDateValue,
setValue,
visibleRange: {
Expand Down Expand Up @@ -308,25 +308,25 @@ export function useCalendarState<T extends DateValue = DateValue>(props: Calenda
return isFocused && focusedDate && isSameDay(date, focusedDate);
},
isCellDisabled(date) {
return props.isDisabled || date.compare(startDate) < 0 || date.compare(endDate) > 0 || this.isInvalid(date, minValue, maxValue);
return props.isDisabled || date.compare(startDate) < 0 || date.compare(endDate) > 0 || this.isInvalid(date);
},
isCellUnavailable(date) {
return props.isDateUnavailable && props.isDateUnavailable(date);
return props.isDateUnavailable ? props.isDateUnavailable(date) : false;
},
isPreviousVisibleRangeInvalid() {
let prev = startDate.subtract({days: 1});
return isSameDay(prev, startDate) || this.isInvalid(prev, minValue, maxValue);
return isSameDay(prev, startDate) || this.isInvalid(prev);
},
isNextVisibleRangeInvalid() {
// Adding may return the same date if we reached the end of time
// according to the calendar system (e.g. 9999-12-31).
let next = endDate.add({days: 1});
return isSameDay(next, endDate) || this.isInvalid(next, minValue, maxValue);
return isSameDay(next, endDate) || this.isInvalid(next);
},
getDatesInWeek(weekIndex, from = startDate) {
// let date = startOfWeek(from, locale);
let date = from.add({weeks: weekIndex});
let dates = [];
let dates: (CalendarDate | null)[] = [];

date = startOfWeek(date, locale);

Expand Down
51 changes: 27 additions & 24 deletions packages/@react-stately/calendar/src/useRangeCalendarState.ts
Expand Up @@ -13,7 +13,7 @@
import {alignCenter, constrainValue, isInvalid, previousAvailableDate} from './utils';
import {Calendar, CalendarDate, DateDuration, GregorianCalendar, isEqualDay, maxDate, minDate, toCalendar, toCalendarDate} from '@internationalized/date';
import {CalendarState, RangeCalendarState} from './types';
import {DateRange, DateValue, RangeCalendarProps} from '@react-types/calendar';
import {DateValue, RangeCalendarProps} from '@react-types/calendar';
import {RangeValue, ValidationState} from '@react-types/shared';
import {useCalendarState} from './useCalendarState';
import {useControlledState} from '@react-stately/utils';
Expand Down Expand Up @@ -42,13 +42,13 @@ export interface RangeCalendarStateOptions<T extends DateValue = DateValue> exte
*/
export function useRangeCalendarState<T extends DateValue = DateValue>(props: RangeCalendarStateOptions<T>): RangeCalendarState {
let {value: valueProp, defaultValue, onChange, createCalendar, locale, visibleDuration = {months: 1}, minValue, maxValue, ...calendarProps} = props;
let [value, setValue] = useControlledState<DateRange>(
valueProp,
defaultValue || null,
let [value, setValue] = useControlledState<RangeValue<T> | null, RangeValue<T>>(
valueProp!,
defaultValue || null!,
onChange
);

let [anchorDate, setAnchorDateState] = useState(null);
let [anchorDate, setAnchorDateState] = useState<CalendarDate | null>(null);
let alignment: 'center' | 'start' = 'center';
if (value && value.start && value.end) {
let start = alignCenter(toCalendarDate(value.start), visibleDuration, locale, minValue, maxValue);
Expand All @@ -60,8 +60,8 @@ export function useRangeCalendarState<T extends DateValue = DateValue>(props: Ra
}

// Available range must be stored in a ref so we have access to the updated version immediately in `isInvalid`.
let availableRangeRef = useRef<RangeValue<DateValue>>(null);
let [availableRange, setAvailableRange] = useState<RangeValue<DateValue>>(null);
let availableRangeRef = useRef<Partial<RangeValue<DateValue>> | null>(null);
let [availableRange, setAvailableRange] = useState<Partial<RangeValue<DateValue>>|null>(null);
let min = useMemo(() => maxDate(minValue, availableRange?.start), [minValue, availableRange]);
let max = useMemo(() => minDate(maxValue, availableRange?.end), [maxValue, availableRange]);

Expand All @@ -78,9 +78,11 @@ export function useRangeCalendarState<T extends DateValue = DateValue>(props: Ra

let updateAvailableRange = (date) => {
if (date && props.isDateUnavailable && !props.allowsNonContiguousRanges) {
const nextAvailableStartDate = nextUnavailableDate(date, calendar, -1);
const nextAvailableEndDate = nextUnavailableDate(date, calendar, 1);
availableRangeRef.current = {
start: nextUnavailableDate(date, calendar, -1),
end: nextUnavailableDate(date, calendar, 1)
start: nextAvailableStartDate,
end: nextAvailableEndDate
};
setAvailableRange(availableRangeRef.current);
} else {
Expand All @@ -96,7 +98,7 @@ export function useRangeCalendarState<T extends DateValue = DateValue>(props: Ra
setLastVisibleRange(calendar.visibleRange);
}

let setAnchorDate = (date: CalendarDate) => {
let setAnchorDate = (date: CalendarDate | null) => {
if (date) {
setAnchorDateState(date);
updateAvailableRange(date);
Expand All @@ -112,20 +114,22 @@ export function useRangeCalendarState<T extends DateValue = DateValue>(props: Ra
return;
}

date = constrainValue(date, min, max);
date = previousAvailableDate(date, calendar.visibleRange.start, props.isDateUnavailable);
if (!date) {
const constrainedDate = constrainValue(date, min, max);
const previousAvailableConstrainedDate = previousAvailableDate(constrainedDate, calendar.visibleRange.start, props.isDateUnavailable);
if (!previousAvailableConstrainedDate) {
return;
}

if (!anchorDate) {
setAnchorDate(date);
setAnchorDate(previousAvailableConstrainedDate);
} else {
let range = makeRange(anchorDate, date);
setValue({
start: convertValue(range.start, value?.start),
end: convertValue(range.end, value?.end)
});
let range = makeRange(anchorDate, previousAvailableConstrainedDate);
if (range) {
setValue({
start: convertValue(range.start, value?.start) as T,
end: convertValue(range.end, value?.end) as T
});
}
setAnchorDate(null);
}
};
Expand Down Expand Up @@ -167,7 +171,7 @@ export function useRangeCalendarState<T extends DateValue = DateValue>(props: Ra
}
},
isSelected(date) {
return highlightedRange && date.compare(highlightedRange.start) >= 0 && date.compare(highlightedRange.end) <= 0 && !calendar.isCellDisabled(date) && !calendar.isCellUnavailable(date);
return Boolean(highlightedRange && date.compare(highlightedRange.start) >= 0 && date.compare(highlightedRange.end) <= 0 && !calendar.isCellDisabled(date) && !calendar.isCellUnavailable(date));
},
isInvalid(date) {
return calendar.isInvalid(date) || isInvalid(date, availableRangeRef.current?.start, availableRangeRef.current?.end);
Expand All @@ -177,7 +181,7 @@ export function useRangeCalendarState<T extends DateValue = DateValue>(props: Ra
};
}

function makeRange(start: DateValue, end: DateValue): RangeValue<CalendarDate> {
function makeRange(start: DateValue, end: DateValue): RangeValue<CalendarDate> | null {
if (!start || !end) {
return null;
}
Expand All @@ -189,7 +193,7 @@ function makeRange(start: DateValue, end: DateValue): RangeValue<CalendarDate> {
return {start: toCalendarDate(start), end: toCalendarDate(end)};
}

function convertValue(newValue: CalendarDate, oldValue: DateValue) {
function convertValue(newValue: CalendarDate, oldValue?: DateValue): DateValue {
// The display calendar should not have any effect on the emitted value.
// Emit dates in the same calendar as the original value, if any, otherwise gregorian.
newValue = toCalendar(newValue, oldValue?.calendar || new GregorianCalendar());
Expand All @@ -202,7 +206,7 @@ function convertValue(newValue: CalendarDate, oldValue: DateValue) {
return newValue;
}

function nextUnavailableDate(anchorDate: CalendarDate, state: CalendarState, dir: number) {
function nextUnavailableDate(anchorDate: CalendarDate, state: CalendarState, dir: number): CalendarDate | undefined {
let nextDate = anchorDate.add({days: dir});
while (
(dir < 0 ? nextDate.compare(state.visibleRange.start) >= 0 : nextDate.compare(state.visibleRange.end) <= 0) &&
Expand All @@ -215,5 +219,4 @@ function nextUnavailableDate(anchorDate: CalendarDate, state: CalendarState, dir
return nextDate.add({days: -dir});
}

return null;
}