From 664a024bc2d4bd9182c379dcd9e91089d5d96173 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Fri, 17 Mar 2023 12:00:15 -0400 Subject: [PATCH 1/9] React Aria Components TS strict --- packages/@react-aria/tabs/src/index.ts | 2 +- packages/@react-aria/tabs/src/useTabList.ts | 5 +- .../calendar/src/useCalendarState.ts | 4 +- .../calendar/src/useRangeCalendarState.ts | 4 +- .../datepicker/src/useDateFieldState.ts | 4 +- .../datepicker/src/useDateRangePickerState.ts | 4 +- .../datepicker/src/useTimeFieldState.ts | 4 +- .../@react-types/shared/src/collections.d.ts | 12 +-- .../react-aria-components/docs/Button.mdx | 4 +- .../react-aria-components/docs/Calendar.mdx | 8 +- .../docs/CheckboxGroup.mdx | 16 ++-- .../react-aria-components/src/Breadcrumbs.tsx | 4 +- .../react-aria-components/src/Calendar.tsx | 10 +-- .../react-aria-components/src/Checkbox.tsx | 4 +- .../react-aria-components/src/Collection.tsx | 73 ++++++++++--------- .../react-aria-components/src/ComboBox.tsx | 8 +- .../react-aria-components/src/DateField.tsx | 8 +- .../react-aria-components/src/DatePicker.tsx | 10 +-- packages/react-aria-components/src/Dialog.tsx | 4 +- .../react-aria-components/src/GridList.tsx | 8 +- packages/react-aria-components/src/Link.tsx | 6 +- .../react-aria-components/src/ListBox.tsx | 14 ++-- packages/react-aria-components/src/Menu.tsx | 16 ++-- packages/react-aria-components/src/Meter.tsx | 2 +- packages/react-aria-components/src/Modal.tsx | 8 +- .../react-aria-components/src/NumberField.tsx | 2 +- .../src/OverlayArrow.tsx | 4 +- .../react-aria-components/src/Popover.tsx | 9 ++- .../react-aria-components/src/ProgressBar.tsx | 6 +- .../react-aria-components/src/RadioGroup.tsx | 8 +- .../react-aria-components/src/SearchField.tsx | 4 +- packages/react-aria-components/src/Select.tsx | 8 +- packages/react-aria-components/src/Slider.tsx | 12 +-- packages/react-aria-components/src/Table.tsx | 48 ++++++------ packages/react-aria-components/src/Tabs.tsx | 31 +++++--- .../react-aria-components/src/TextField.tsx | 2 +- .../react-aria-components/src/Tooltip.tsx | 14 ++-- packages/react-aria-components/src/utils.tsx | 6 +- packages/react-aria/src/index.ts | 2 +- tsconfig.json | 3 +- 40 files changed, 209 insertions(+), 192 deletions(-) diff --git a/packages/@react-aria/tabs/src/index.ts b/packages/@react-aria/tabs/src/index.ts index 6a75ea01d1a..e36be83acab 100644 --- a/packages/@react-aria/tabs/src/index.ts +++ b/packages/@react-aria/tabs/src/index.ts @@ -16,4 +16,4 @@ export type {AriaTabListProps, AriaTabPanelProps, AriaTabProps} from '@react-typ export type {Orientation} from '@react-types/shared'; export type {TabAria} from './useTab'; export type {TabPanelAria} from './useTabPanel'; -export type {TabListAria} from './useTabList'; +export type {AriaTabListOptions, TabListAria} from './useTabList'; diff --git a/packages/@react-aria/tabs/src/useTabList.ts b/packages/@react-aria/tabs/src/useTabList.ts index b241126985e..e69e0fd51a6 100644 --- a/packages/@react-aria/tabs/src/useTabList.ts +++ b/packages/@react-aria/tabs/src/useTabList.ts @@ -20,17 +20,18 @@ import {TabsKeyboardDelegate} from './TabsKeyboardDelegate'; import {useLocale} from '@react-aria/i18n'; import {useSelectableCollection} from '@react-aria/selection'; +export interface AriaTabListOptions extends Omit, 'children'> {} + export interface TabListAria { /** Props for the tablist container. */ tabListProps: DOMAttributes } - /** * Provides the behavior and accessibility implementation for a tab list. * Tabs organize content into multiple sections and allow users to navigate between them. */ -export function useTabList(props: AriaTabListProps, state: TabListState, ref: RefObject): TabListAria { +export function useTabList(props: AriaTabListOptions, state: TabListState, ref: RefObject): TabListAria { let { orientation = 'horizontal', keyboardActivation = 'automatic' diff --git a/packages/@react-stately/calendar/src/useCalendarState.ts b/packages/@react-stately/calendar/src/useCalendarState.ts index e9024962f02..fe6aab36958 100644 --- a/packages/@react-stately/calendar/src/useCalendarState.ts +++ b/packages/@react-stately/calendar/src/useCalendarState.ts @@ -32,7 +32,7 @@ import {CalendarState} from './types'; import {useControlledState} from '@react-stately/utils'; import {useMemo, useRef, useState} from 'react'; -export interface CalendarStateOptions extends CalendarProps { +export interface CalendarStateOptions extends CalendarProps { /** The locale to display and edit the value according to. */ locale: string, /** @@ -55,7 +55,7 @@ export interface CalendarStateOptions extends CalendarProps { * Provides state management for a calendar component. * A calendar displays one or more date grids and allows users to select a single date. */ -export function useCalendarState(props: CalendarStateOptions): CalendarState { +export function useCalendarState(props: CalendarStateOptions): CalendarState { let defaultFormatter = useMemo(() => new DateFormatter(props.locale), [props.locale]); let resolvedOptions = useMemo(() => defaultFormatter.resolvedOptions(), [defaultFormatter]); let { diff --git a/packages/@react-stately/calendar/src/useRangeCalendarState.ts b/packages/@react-stately/calendar/src/useRangeCalendarState.ts index cb21224246e..2bf7e8bb549 100644 --- a/packages/@react-stately/calendar/src/useRangeCalendarState.ts +++ b/packages/@react-stately/calendar/src/useRangeCalendarState.ts @@ -20,7 +20,7 @@ import {useCalendarState} from './useCalendarState'; import {useControlledState} from '@react-stately/utils'; import {useMemo, useRef, useState} from 'react'; -export interface RangeCalendarStateOptions extends RangeCalendarProps { +export interface RangeCalendarStateOptions extends RangeCalendarProps { /** The locale to display and edit the value according to. */ locale: string, /** @@ -41,7 +41,7 @@ export interface RangeCalendarStateOptions extends RangeCalendarProps * Provides state management for a range calendar component. * A range calendar displays one or more date grids and allows users to select a contiguous range of dates. */ -export function useRangeCalendarState(props: RangeCalendarStateOptions): RangeCalendarState { +export function useRangeCalendarState(props: RangeCalendarStateOptions): RangeCalendarState { let {value: valueProp, defaultValue, onChange, createCalendar, locale, visibleDuration = {months: 1}, minValue, maxValue, ...calendarProps} = props; let [value, setValue] = useControlledState( valueProp, diff --git a/packages/@react-stately/datepicker/src/useDateFieldState.ts b/packages/@react-stately/datepicker/src/useDateFieldState.ts index 97315086ae4..ad63325ca7c 100644 --- a/packages/@react-stately/datepicker/src/useDateFieldState.ts +++ b/packages/@react-stately/datepicker/src/useDateFieldState.ts @@ -115,7 +115,7 @@ const TYPE_MAPPING = { dayperiod: 'dayPeriod' }; -export interface DateFieldStateOptions extends DatePickerProps { +export interface DateFieldStateOptions extends DatePickerProps { /** * The maximum unit to display in the date field. * @default 'year' @@ -137,7 +137,7 @@ export interface DateFieldStateOptions extends DatePickerProps { * A date field allows users to enter and edit date and time values using a keyboard. * Each part of a date value is displayed in an individually editable segment. */ -export function useDateFieldState(props: DateFieldStateOptions): DateFieldState { +export function useDateFieldState(props: DateFieldStateOptions): DateFieldState { let { locale, createCalendar, diff --git a/packages/@react-stately/datepicker/src/useDateRangePickerState.ts b/packages/@react-stately/datepicker/src/useDateRangePickerState.ts index 13333a37072..36a6105935f 100644 --- a/packages/@react-stately/datepicker/src/useDateRangePickerState.ts +++ b/packages/@react-stately/datepicker/src/useDateRangePickerState.ts @@ -18,7 +18,7 @@ import {RangeValue, ValidationState} from '@react-types/shared'; import {useControlledState} from '@react-stately/utils'; import {useState} from 'react'; -export interface DateRangePickerStateOptions extends DateRangePickerProps { +export interface DateRangePickerStateOptions extends DateRangePickerProps { /** * Determines whether the date picker popover should close automatically when a date is selected. * @default true @@ -71,7 +71,7 @@ export interface DateRangePickerState extends OverlayTriggerState { * A date range picker combines two DateFields and a RangeCalendar popover to allow * users to enter or select a date and time range. */ -export function useDateRangePickerState(props: DateRangePickerStateOptions): DateRangePickerState { +export function useDateRangePickerState(props: DateRangePickerStateOptions): DateRangePickerState { let overlayState = useOverlayTriggerState(props); let [controlledValue, setControlledValue] = useControlledState(props.value, props.defaultValue || null, props.onChange); let [placeholderValue, setPlaceholderValue] = useState(() => controlledValue || {start: null, end: null}); diff --git a/packages/@react-stately/datepicker/src/useTimeFieldState.ts b/packages/@react-stately/datepicker/src/useTimeFieldState.ts index 0c73068164f..e547583480c 100644 --- a/packages/@react-stately/datepicker/src/useTimeFieldState.ts +++ b/packages/@react-stately/datepicker/src/useTimeFieldState.ts @@ -16,7 +16,7 @@ import {getLocalTimeZone, GregorianCalendar, Time, toCalendarDateTime, today, to import {useControlledState} from '@react-stately/utils'; import {useMemo} from 'react'; -export interface TimeFieldStateOptions extends TimePickerProps { +export interface TimeFieldStateOptions extends TimePickerProps { /** The locale to display and edit the value according to. */ locale: string } @@ -26,7 +26,7 @@ export interface TimeFieldStateOptions extends TimePickerProps { * A time field allows users to enter and edit time values using a keyboard. * Each part of a time value is displayed in an individually editable segment. */ -export function useTimeFieldState(props: TimeFieldStateOptions): DateFieldState { +export function useTimeFieldState(props: TimeFieldStateOptions): DateFieldState { let { placeholderValue = new Time(), minValue, diff --git a/packages/@react-types/shared/src/collections.d.ts b/packages/@react-types/shared/src/collections.d.ts index 126386e0eb8..a8769e06d71 100644 --- a/packages/@react-types/shared/src/collections.d.ts +++ b/packages/@react-types/shared/src/collections.d.ts @@ -133,10 +133,10 @@ export interface Collection extends Iterable { getKeys(): Iterable, /** Get an item by its key. */ - getItem(key: Key): T, + getItem(key: Key): T | null, /** Get an item by the index of its key. */ - at(idx: number): T, + at(idx: number): T | null, /** Get the key that comes before the given key in the collection. */ getKeyBefore(key: Key): Key | null, @@ -160,7 +160,7 @@ export interface Node { /** A unique key for the node. */ key: Key, /** The object value the node was created from. */ - value: T, + value: T | null, /** The level of depth this node is at in the heirarchy. */ level: number, /** Whether this item has children, even if not loaded yet. */ @@ -181,11 +181,11 @@ export interface Node { /** A function that should be called to wrap the rendered node. */ wrapper?: (element: ReactElement) => ReactElement, /** The key of the parent node. */ - parentKey?: Key, + parentKey?: Key | null, /** The key of the node before this node. */ - prevKey?: Key, + prevKey?: Key | null, /** The key of the node after this node. */ - nextKey?: Key, + nextKey?: Key | null, /** Additional properties specific to a particular node type. */ props?: any, /** @private */ diff --git a/packages/react-aria-components/docs/Button.mdx b/packages/react-aria-components/docs/Button.mdx index a69f0541e48..1ac8d2e476b 100644 --- a/packages/react-aria-components/docs/Button.mdx +++ b/packages/react-aria-components/docs/Button.mdx @@ -191,13 +191,13 @@ Each of these handlers receives a

{pointerType ? `You are pressing the button with a ${pointerType}!` : 'Ready to be pressed.'}

diff --git a/packages/react-aria-components/docs/Calendar.mdx b/packages/react-aria-components/docs/Calendar.mdx index ba96b296379..0e2f66e943d 100644 --- a/packages/react-aria-components/docs/Calendar.mdx +++ b/packages/react-aria-components/docs/Calendar.mdx @@ -470,10 +470,11 @@ Selected dates passed to `onChange` always use the same calendar system as the ` The below example displays a `Calendar` in the Hindi language, using the Indian calendar. Dates emitted from `onChange` are in the Gregorian calendar. ```tsx example +import type {DateValue} from 'react-aria-components'; import {I18nProvider} from '@react-aria/i18n'; function Example() { - let [date, setDate] = React.useState(null); + let [date, setDate] = React.useState(null); return ( @@ -503,6 +504,7 @@ This example includes multiple unavailable date ranges, e.g. dates when no appoi ```tsx example +import type {DateValue} from 'react-aria-components'; import {today, isWeekend} from '@internationalized/date'; import {useLocale} from '@react-aria/i18n'; @@ -515,7 +517,7 @@ function Example() { ]; let {locale} = useLocale(); - let isDateUnavailable = (date) => isWeekend(date, locale) || disabledRanges.some((interval) => date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0); + let isDateUnavailable = (date: DateValue) => isWeekend(date, locale) || disabledRanges.some((interval) => date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0); return } @@ -580,7 +582,7 @@ function Example() { value={date} onChange={setDate} validationState={isInvalid ? 'invalid' : 'valid'} - errorMessage={isInvalid ? 'We are closed on weekends' : null} /> + errorMessage={isInvalid ? 'We are closed on weekends' : undefined} /> ); } ``` diff --git a/packages/react-aria-components/docs/CheckboxGroup.mdx b/packages/react-aria-components/docs/CheckboxGroup.mdx index 0f17f4a9b53..4bfd8b6f9f1 100644 --- a/packages/react-aria-components/docs/CheckboxGroup.mdx +++ b/packages/react-aria-components/docs/CheckboxGroup.mdx @@ -436,10 +436,10 @@ prop to `"invalid"` when no options are selected and removes it otherwise. ```tsx example function Example() { - let [selected, setSelected] = React.useState([]); + let [selected, setSelected] = React.useState([]); return ( - + Lettuce Tomato Onion @@ -460,13 +460,13 @@ indicates that the group is required, not any individual option. In addition, `v ```tsx example function Example() { - let [selected, setSelected] = React.useState([]); + let [selected, setSelected] = React.useState([]); return ( - Terms and conditions - Privacy policy - Cookie policy + Terms and conditions + Privacy policy + Cookie policy ); } @@ -487,8 +487,8 @@ function Example() { onChange={setChecked} value={checked} validationState={isValid ? 'valid' : 'invalid'} - description={isValid ? 'Select your pets.' : null} - errorMessage={isValid ? null : checked.includes('cats') + description={isValid ? 'Select your pets.' : undefined} + errorMessage={isValid ? undefined : checked.includes('cats') ? 'No cats allowed.' : 'Select only dogs and dragons.'} > diff --git a/packages/react-aria-components/src/Breadcrumbs.tsx b/packages/react-aria-components/src/Breadcrumbs.tsx index bb374562126..9c4ff3d768e 100644 --- a/packages/react-aria-components/src/Breadcrumbs.tsx +++ b/packages/react-aria-components/src/Breadcrumbs.tsx @@ -60,9 +60,9 @@ export {_Breadcrumbs as Breadcrumbs}; function BreadcrumbItem({node, isCurrent, isDisabled}) { // Recreating useBreadcrumbItem because we want to use composition instead of having the link builtin. - let headingProps: HTMLAttributes = isCurrent ? {'aria-current': 'page'} : undefined; + let headingProps: HTMLAttributes | null = isCurrent ? {'aria-current': 'page'} : null; let linkProps = { - 'aria-current': isCurrent ? 'page' : undefined, + 'aria-current': isCurrent ? 'page' : null, isDisabled: isDisabled || isCurrent }; diff --git a/packages/react-aria-components/src/Calendar.tsx b/packages/react-aria-components/src/Calendar.tsx index 5adce19c31f..cc617ec2a09 100644 --- a/packages/react-aria-components/src/Calendar.tsx +++ b/packages/react-aria-components/src/Calendar.tsx @@ -38,8 +38,8 @@ export interface RangeCalendarProps extends Omit, HTMLDivElement>>({}); export const RangeCalendarContext = createContext, HTMLDivElement>>({}); -const InternalCalendarContext = createContext(null); -const InternalCalendarGridContext = createContext(null); +const InternalCalendarContext = createContext(null); +const InternalCalendarGridContext = createContext(null); function Calendar(props: CalendarProps, ref: ForwardedRef) { [props, ref] = useContextProps(props, ref, CalendarContext); @@ -257,7 +257,7 @@ export interface CalendarGridProps extends StyleProps { } function CalendarGrid(props: CalendarGridProps, ref: ForwardedRef) { - let state = useContext(InternalCalendarContext); + let state = useContext(InternalCalendarContext)!; let startDate = state.visibleRange.start; if (props.offset) { startDate = startDate.add(props.offset); @@ -313,8 +313,8 @@ export interface CalendarCellProps extends RenderProps } function CalendarCell({date, ...otherProps}: CalendarCellProps, ref: ForwardedRef) { - let state = useContext(InternalCalendarContext); - let currentMonth = useContext(InternalCalendarGridContext); + let state = useContext(InternalCalendarContext)!; + let currentMonth = useContext(InternalCalendarGridContext)!; let objectRef = useObjectRef(ref); let {cellProps, buttonProps, ...states} = useCalendarCell( {date}, diff --git a/packages/react-aria-components/src/Checkbox.tsx b/packages/react-aria-components/src/Checkbox.tsx index ae2a51ab591..e4999320a9c 100644 --- a/packages/react-aria-components/src/Checkbox.tsx +++ b/packages/react-aria-components/src/Checkbox.tsx @@ -87,7 +87,7 @@ export interface CheckboxRenderProps { * Whether the checkbox is valid or invalid. * @selector [data-validation-state="valid | invalid"] */ - validationState: ValidationState, + validationState?: ValidationState, /** * Whether the checkbox is required. * @selector [data-required] @@ -96,7 +96,7 @@ export interface CheckboxRenderProps { } export const CheckboxGroupContext = createContext>(null); -const InternalCheckboxGroupContext = createContext(null); +const InternalCheckboxGroupContext = createContext(null); function CheckboxGroup(props: CheckboxGroupProps, ref: ForwardedRef) { [props, ref] = useContextProps(props, ref, CheckboxGroupContext); diff --git a/packages/react-aria-components/src/Collection.tsx b/packages/react-aria-components/src/Collection.tsx index 5526ef17f48..ca4b115bb82 100644 --- a/packages/react-aria-components/src/Collection.tsx +++ b/packages/react-aria-components/src/Collection.tsx @@ -11,10 +11,11 @@ */ import {CollectionBase} from '@react-types/shared'; import {createPortal} from 'react-dom'; -import {DOMProps, RenderProps, useContextProps} from './utils'; +import {DOMProps, RenderProps} from './utils'; import {Collection as ICollection, Node, SelectionBehavior, SelectionMode, ItemProps as SharedItemProps, SectionProps as SharedSectionProps} from 'react-stately'; -import React, {cloneElement, createContext, Key, ReactElement, ReactNode, ReactPortal, useCallback, useMemo} from 'react'; +import React, {cloneElement, createContext, Key, ReactElement, ReactNode, ReactPortal, useCallback, useContext, useMemo} from 'react'; import {useSyncExternalStore} from 'use-sync-external-store/shim/index.js'; +import { mergeProps } from '@react-aria/utils/dist/module'; // This Collection implementation is perhaps a little unusual. It works by rendering the React tree into a // Portal to a fake DOM implementation. This gives us efficient access to the tree of rendered objects, and @@ -38,12 +39,12 @@ type Mutable = { export class NodeValue implements Node { readonly type: string; readonly key: Key; - readonly value: T; + readonly value: T | null = null; readonly level: number = 0; readonly hasChildNodes: boolean = false; readonly rendered: ReactNode = null; - readonly textValue: string | null = null; - readonly 'aria-label'?: string = null; + readonly textValue: string = ''; + readonly 'aria-label'?: string = undefined; readonly index: number = 0; readonly parentKey: Key | null = null; readonly prevKey: Key | null = null; @@ -85,11 +86,11 @@ export class NodeValue implements Node { * and queues an update with the owner document. */ class BaseNode { - private _firstChild: ElementNode | null; - private _lastChild: ElementNode | null; - private _previousSibling: ElementNode | null; - private _nextSibling: ElementNode | null; - private _parentNode: BaseNode | null; + private _firstChild: ElementNode | null = null; + private _lastChild: ElementNode | null = null; + private _previousSibling: ElementNode | null = null; + private _nextSibling: ElementNode | null = null; + private _parentNode: BaseNode | null = null; ownerDocument: Document; constructor(ownerDocument: Document) { @@ -186,14 +187,14 @@ class BaseNode { if (this.firstChild === referenceNode) { this.firstChild = newNode; - } else { + } else if (referenceNode.previousSibling) { referenceNode.previousSibling.nextSibling = newNode; } referenceNode.previousSibling = newNode; newNode.parentNode = referenceNode.parentNode; - let node = referenceNode; + let node: ElementNode | null = referenceNode; while (node) { node.index++; node = node.nextSibling; @@ -232,7 +233,7 @@ class BaseNode { child.parentNode = null; child.nextSibling = null; child.previousSibling = null; - child.index = null; + child.index = 0; this.ownerDocument.removeNode(child); } @@ -255,7 +256,7 @@ const TYPE_MAP = { export class ElementNode extends BaseNode { nodeType = 8; // COMMENT_NODE (we'd use ELEMENT_NODE but React DevTools will fail to get its dimensions) node: NodeValue; - private _index: number; + private _index: number = 0; constructor(type: string, ownerDocument: Document) { super(ownerDocument); @@ -284,11 +285,11 @@ export class ElementNode extends BaseNode { node.index = this.index; node.level = this.level; node.parentKey = this.parentNode instanceof ElementNode ? this.parentNode.node.key : null; - node.prevKey = this.previousSibling?.node.key; - node.nextKey = this.nextSibling?.node.key; + node.prevKey = this.previousSibling?.node.key ?? null; + node.nextKey = this.nextSibling?.node.key ?? null; node.hasChildNodes = !!this.firstChild; - node.firstChildKey = this.firstChild?.node.key; - node.lastChildKey = this.lastChild?.node.key; + node.firstChildKey = this.firstChild?.node.key ?? null; + node.lastChildKey = this.lastChild?.node.key ?? null; } // Special property that React passes through as an object rather than a string via setAttribute. @@ -347,10 +348,10 @@ export class BaseCollection implements ICollection> { } *[Symbol.iterator]() { - let node: Node = this.keyMap.get(this.firstKey); + let node: Node | undefined = this.firstKey != null ? this.keyMap.get(this.firstKey) : undefined; while (node) { yield node; - node = this.keyMap.get(node.nextKey); + node = node.nextKey != null ? this.keyMap.get(node.nextKey) : undefined; } } @@ -359,10 +360,10 @@ export class BaseCollection implements ICollection> { return { *[Symbol.iterator]() { let parent = keyMap.get(key); - let node = parent && keyMap.get(parent.firstChildKey); + let node = parent?.firstChildKey != null ? keyMap.get(parent.firstChildKey) : null; while (node) { - yield node; - node = keyMap.get(node.nextKey); + yield node as Node; + node = node.nextKey != null ? keyMap.get(node.nextKey) : undefined; } } }; @@ -377,11 +378,11 @@ export class BaseCollection implements ICollection> { if (node.prevKey != null) { node = this.keyMap.get(node.prevKey); - while (node.type !== 'item' && node.lastChildKey != null) { + while (node && node.type !== 'item' && node.lastChildKey != null) { node = this.keyMap.get(node.lastChildKey); } - return node.key; + return node?.key ?? null; } return node.parentKey; @@ -408,6 +409,8 @@ export class BaseCollection implements ICollection> { return null; } } + + return null; } getFirstKey() { @@ -415,16 +418,16 @@ export class BaseCollection implements ICollection> { } getLastKey() { - let node = this.keyMap.get(this.lastKey); + let node = this.lastKey != null ? this.keyMap.get(this.lastKey) : null; while (node?.lastChildKey != null) { node = this.keyMap.get(node.lastChildKey); } - return node?.key; + return node?.key ?? null; } - getItem(key: Key): Node { - return this.keyMap.get(key); + getItem(key: Key): Node | null { + return this.keyMap.get(key) ?? null; } at(): Node { @@ -459,7 +462,7 @@ export class BaseCollection implements ICollection> { this.keyMap.delete(key); } - commit(firstKey: Key, lastKey: Key) { + commit(firstKey: Key | null, lastKey: Key | null) { if (this.frozen) { throw new Error('Cannot commit a frozen collection'); } @@ -484,6 +487,7 @@ export class Document> extends BaseNode { private subscriptions: Set<() => void> = new Set(); constructor(collection: C) { + // @ts-ignore super(null); this.collection = collection; this.collectionMutated = true; @@ -556,7 +560,7 @@ export class Document> extends BaseNode { collection.addNode(element.node); } - collection.commit(this.firstChild?.node.key, this.lastChild?.node.key); + collection.commit(this.firstChild?.node.key ?? null, this.lastChild?.node.key ?? null); this.mutatedNodes.clear(); } @@ -592,7 +596,7 @@ export function useCachedChildren(props: CachedChildrenOptions let cache = useMemo(() => new WeakMap(), []); return useMemo(() => { if (items && typeof children === 'function') { - let res = []; + let res: ReactElement[] = []; for (let item of items) { let rendered = cache.get(item); if (!rendered) { @@ -704,11 +708,12 @@ export function Section(props: SectionProps): JSX.Element { return
{children}
; } -export const CollectionContext = createContext>(null); +export const CollectionContext = createContext | null>(null); export const CollectionRendererContext = createContext['children']>(null); export function Collection(props: CollectionProps): JSX.Element { - [props] = useContextProps(props, null, CollectionContext); + let ctx = useContext(CollectionContext); + props = mergeProps(ctx, props); let renderer = typeof props.children === 'function' ? props.children : null; return ( diff --git a/packages/react-aria-components/src/ComboBox.tsx b/packages/react-aria-components/src/ComboBox.tsx index c9b7eb1d628..9b3610c0d7c 100644 --- a/packages/react-aria-components/src/ComboBox.tsx +++ b/packages/react-aria-components/src/ComboBox.tsx @@ -42,14 +42,14 @@ function ComboBox(props: ComboBoxProps, ref: ForwardedRef(null); let inputRef = useRef(null); - let listBoxRef = useRef(null); - let popoverRef = useRef(null); + let listBoxRef = useRef(null); + let popoverRef = useRef(null); let [labelRef, label] = useSlot(); let { buttonProps, @@ -69,7 +69,7 @@ function ComboBox(props: ComboBoxProps, ref: ForwardedRef(null); let onResize = useCallback(() => { if (inputRef.current) { let buttonRect = buttonRef.current?.getBoundingClientRect(); diff --git a/packages/react-aria-components/src/DateField.tsx b/packages/react-aria-components/src/DateField.tsx index b85c0d3a4a3..e2263977453 100644 --- a/packages/react-aria-components/src/DateField.tsx +++ b/packages/react-aria-components/src/DateField.tsx @@ -39,7 +39,7 @@ function DateField(props: DateFieldProps, ref: Forwarded createCalendar }); - let fieldRef = useRef(); + let fieldRef = useRef(null); let [labelRef, label] = useSlot(); let {labelProps, fieldProps, descriptionProps, errorMessageProps} = useDateField({...props, label}, state, fieldRef); @@ -83,7 +83,7 @@ function TimeField(props: TimeFieldProps, ref: Forwarded locale }); - let fieldRef = useRef(); + let fieldRef = useRef(null); let [labelRef, label] = useSlot(); let {labelProps, fieldProps, descriptionProps, errorMessageProps} = useTimeField({...props, label}, state, fieldRef); @@ -119,7 +119,7 @@ function TimeField(props: TimeFieldProps, ref: Forwarded const _TimeField = forwardRef(TimeField); export {_TimeField as TimeField}; -const InternalDateInputContext = createContext(null); +const InternalDateInputContext = createContext(null); export interface DateInputProps extends SlotProps, StyleProps { children: (segment: IDateSegment) => ReactElement @@ -176,7 +176,7 @@ export interface DateSegmentProps extends RenderProps { } function DateSegment({segment, ...otherProps}: DateSegmentProps, ref: ForwardedRef) { - let state = useContext(InternalDateInputContext); + let state = useContext(InternalDateInputContext)!; let domRef = useObjectRef(ref); let {segmentProps} = useDateSegment(segment, state, domRef); let renderProps = useRenderProps({ diff --git a/packages/react-aria-components/src/DatePicker.tsx b/packages/react-aria-components/src/DatePicker.tsx index 5b768dfcbf7..0c3ea534ba8 100644 --- a/packages/react-aria-components/src/DatePicker.tsx +++ b/packages/react-aria-components/src/DatePicker.tsx @@ -32,7 +32,7 @@ export const DateRangePickerContext = createContext(props: DatePickerProps, ref: ForwardedRef) { [props, ref] = useContextProps(props, ref, DatePickerContext); let state = useDatePickerState(props); - let groupRef = useRef(); + let groupRef = useRef(null); let [labelRef, label] = useSlot(); let { groupProps, @@ -52,7 +52,7 @@ function DatePicker(props: DatePickerProps, ref: Forward createCalendar }); - let fieldRef = useRef(); + let fieldRef = useRef(null); let {fieldProps: dateFieldProps} = useDateField({...fieldProps, label}, fieldState, fieldRef); let renderProps = useRenderProps({ @@ -94,7 +94,7 @@ export {_DatePicker as DatePicker}; function DateRangePicker(props: DateRangePickerProps, ref: ForwardedRef) { [props, ref] = useContextProps(props, ref, DateRangePickerContext); let state = useDateRangePickerState(props); - let groupRef = useRef(); + let groupRef = useRef(null); let [labelRef, label] = useSlot(); let { groupProps, @@ -115,7 +115,7 @@ function DateRangePicker(props: DateRangePickerProps, re createCalendar }); - let startFieldRef = useRef(); + let startFieldRef = useRef(null); let {fieldProps: startDateFieldProps} = useDateField({...startFieldProps, label}, startFieldState, startFieldRef); let endFieldState = useDateFieldState({ @@ -124,7 +124,7 @@ function DateRangePicker(props: DateRangePickerProps, re createCalendar }); - let endFieldRef = useRef(); + let endFieldRef = useRef(null); let {fieldProps: endDateFieldProps} = useDateField({...endFieldProps, label}, endFieldState, endFieldRef); let renderProps = useRenderProps({ diff --git a/packages/react-aria-components/src/Dialog.tsx b/packages/react-aria-components/src/Dialog.tsx index 579bd1f8a14..350f00a2813 100644 --- a/packages/react-aria-components/src/Dialog.tsx +++ b/packages/react-aria-components/src/Dialog.tsx @@ -40,7 +40,7 @@ export const DialogContext = createContext(null); let {triggerProps, overlayProps} = useOverlayTrigger({type: 'dialog'}, state, buttonRef); return ( @@ -64,7 +64,7 @@ function Dialog(props: DialogProps, ref: ForwardedRef) { let children = props.children; if (typeof children === 'function') { children = children({ - close: props.onClose + close: props.onClose || (() => {}) }); } diff --git a/packages/react-aria-components/src/GridList.tsx b/packages/react-aria-components/src/GridList.tsx index 70c3a25cc4d..fe59c5dcc86 100644 --- a/packages/react-aria-components/src/GridList.tsx +++ b/packages/react-aria-components/src/GridList.tsx @@ -24,7 +24,7 @@ export interface GridListProps extends Omit, 'children'> } export const GridListContext = createContext, HTMLUListElement>>(null); -const InternalGridListContext = createContext>(null); +const InternalGridListContext = createContext | null>(null); function GridList(props: GridListProps, ref: ForwardedRef) { [props, ref] = useContextProps(props, ref, GridListContext); @@ -32,7 +32,7 @@ function GridList(props: GridListProps, ref: ForwardedRef(null); let {rowProps, gridCellProps, descriptionProps, ...states} = useGridListItem( {node: item}, state, diff --git a/packages/react-aria-components/src/Link.tsx b/packages/react-aria-components/src/Link.tsx index f4371821fe6..6e6f6561905 100644 --- a/packages/react-aria-components/src/Link.tsx +++ b/packages/react-aria-components/src/Link.tsx @@ -66,7 +66,7 @@ function Link(props: LinkProps, ref: ForwardedRef) { defaultClassName: 'react-aria-Link', values: { isCurrent: !!props['aria-current'], - isDisabled: props.isDisabled, + isDisabled: props.isDisabled || false, isPressed, isHovered, isFocused, @@ -74,7 +74,7 @@ function Link(props: LinkProps, ref: ForwardedRef) { } }); - let element: any = typeof renderProps.children === 'string' + let element: any = typeof renderProps.children === 'string' ? {renderProps.children} : React.Children.only(renderProps.children); @@ -85,7 +85,7 @@ function Link(props: LinkProps, ref: ForwardedRef) { children: element.props.children, 'data-hovered': isHovered || undefined, 'data-pressed': isPressed || undefined, - 'data-focus-visible': isFocusVisible || undefined + 'data-focus-visible': isFocusVisible || undefined }, element.props) }); } diff --git a/packages/react-aria-components/src/ListBox.tsx b/packages/react-aria-components/src/ListBox.tsx index be0e03a6440..0afbc2d66f5 100644 --- a/packages/react-aria-components/src/ListBox.tsx +++ b/packages/react-aria-components/src/ListBox.tsx @@ -31,11 +31,11 @@ interface ListBoxContextValue extends ListBoxProps { interface InternalListBoxContextValue { state: ListState, - shouldFocusOnHover: boolean + shouldFocusOnHover?: boolean } export const ListBoxContext = createContext, HTMLDivElement>>(null); -const InternalListBoxContext = createContext(null); +const InternalListBoxContext = createContext(null); function ListBox(props: ListBoxProps, ref: ForwardedRef) { [props, ref] = useContextProps(props, ref, ListBoxContext); @@ -115,14 +115,14 @@ interface ListBoxSectionProps extends StyleProps { } function ListBoxSection({section, className, style, ...otherProps}: ListBoxSectionProps) { - let {state} = useContext(InternalListBoxContext); + let {state} = useContext(InternalListBoxContext)!; let {headingProps, groupProps} = useListBoxSection({ heading: section.rendered, - 'aria-label': section['aria-label'] + 'aria-label': section['aria-label'] ?? undefined }); let children = useCachedChildren({ - items: state.collection.getChildren(section.key), + items: state.collection.getChildren!(section.key), children: item => { if (item.type !== 'item') { throw new Error('Only items are allowed within a section'); @@ -153,8 +153,8 @@ interface OptionProps { } function Option({item}: OptionProps) { - let ref = useRef(); - let {state, shouldFocusOnHover} = useContext(InternalListBoxContext); + let ref = useRef(null); + let {state, shouldFocusOnHover} = useContext(InternalListBoxContext)!; let {optionProps, labelProps, descriptionProps, ...states} = useOption( {key: item.key}, state, diff --git a/packages/react-aria-components/src/Menu.tsx b/packages/react-aria-components/src/Menu.tsx index abce9ea23d2..cb246b85634 100644 --- a/packages/react-aria-components/src/Menu.tsx +++ b/packages/react-aria-components/src/Menu.tsx @@ -25,7 +25,7 @@ import {Separator, SeparatorContext} from './Separator'; import {TextContext} from './Text'; export const MenuContext = createContext, HTMLDivElement>>(null); -const InternalMenuContext = createContext>(null); +const InternalMenuContext = createContext | null>(null); export interface MenuTriggerProps extends BaseMenuTriggerProps { children?: ReactNode @@ -34,7 +34,7 @@ export interface MenuTriggerProps extends BaseMenuTriggerProps { export function MenuTrigger(props: MenuTriggerProps) { let state = useMenuTriggerState(props); - let ref = useRef(); + let ref = useRef(null); let {menuTriggerProps, menuProps} = useMenuTrigger({ ...props, type: 'menu' @@ -77,7 +77,7 @@ function MenuInner({props, collection, menuRef: ref}: MenuInne let state = useTreeState({ ...props, collection, - children: null + children: undefined }); let {menuProps} = useMenu(props, state, ref); @@ -127,14 +127,14 @@ interface MenuSectionProps extends StyleProps { } function MenuSection({section, className, style, ...otherProps}: MenuSectionProps) { - let state = useContext(InternalMenuContext); + let state = useContext(InternalMenuContext)!; let {headingProps, groupProps} = useMenuSection({ heading: section.rendered, - 'aria-label': section['aria-label'] + 'aria-label': section['aria-label'] ?? undefined }); let children = useCachedChildren({ - items: state.collection.getChildren(section.key), + items: state.collection.getChildren!(section.key), children: item => { if (item.type !== 'item') { throw new Error('Only items are allowed within a section'); @@ -173,8 +173,8 @@ interface MenuItemProps { } function MenuItem({item}: MenuItemProps) { - let state = useContext(InternalMenuContext); - let ref = useRef(); + let state = useContext(InternalMenuContext)!; + let ref = useRef(null); let {menuItemProps, labelProps, descriptionProps, keyboardShortcutProps, ...states} = useMenuItem({key: item.key}, state, ref); let props: ItemProps = item.props; diff --git a/packages/react-aria-components/src/Meter.tsx b/packages/react-aria-components/src/Meter.tsx index 89c971d2e8d..33f5e4c46f3 100644 --- a/packages/react-aria-components/src/Meter.tsx +++ b/packages/react-aria-components/src/Meter.tsx @@ -26,7 +26,7 @@ export interface MeterRenderProps { * A formatted version of the value. * @selector [aria-valuetext] */ - valueText: string + valueText?: string } export const MeterContext = createContext>(null); diff --git a/packages/react-aria-components/src/Modal.tsx b/packages/react-aria-components/src/Modal.tsx index b9120e94857..9c5baebbfd2 100644 --- a/packages/react-aria-components/src/Modal.tsx +++ b/packages/react-aria-components/src/Modal.tsx @@ -29,8 +29,8 @@ interface InternalModalContextValue { isExiting: boolean } -export const ModalContext = createContext(null); -const InternalModalContext = createContext(null); +export const ModalContext = createContext(null); +const InternalModalContext = createContext(null); export interface ModalRenderProps { /** @@ -98,7 +98,7 @@ export const ModalOverlay = forwardRef((props: ModalOverlayProps, ref: Forwarded let state = ctx?.state ?? useOverlayTriggerState(props); let objectRef = useObjectRef(ref); - let modalRef = useRef(null); + let modalRef = useRef(null); let isOverlayExiting = useExitAnimation(objectRef, state.isOpen); let isModalExiting = useExitAnimation(modalRef, state.isOpen); let isExiting = isOverlayExiting || isModalExiting; @@ -159,7 +159,7 @@ interface ModalContentProps extends RenderProps { } function ModalContent(props: ModalContentProps) { - let {modalProps, modalRef, isExiting} = useContext(InternalModalContext); + let {modalProps, modalRef, isExiting} = useContext(InternalModalContext)!; let mergedRefs = useMemo(() => mergeRefs(props.modalRef, modalRef), [props.modalRef, modalRef]); let ref = useObjectRef(mergedRefs); diff --git a/packages/react-aria-components/src/NumberField.tsx b/packages/react-aria-components/src/NumberField.tsx index e9340bc254a..d9f41cd49c3 100644 --- a/packages/react-aria-components/src/NumberField.tsx +++ b/packages/react-aria-components/src/NumberField.tsx @@ -28,7 +28,7 @@ function NumberField(props: NumberFieldProps, ref: ForwardedRef) [props, ref] = useContextProps(props, ref, NumberFieldContext); let {locale} = useLocale(); let state = useNumberFieldState({...props, locale}); - let inputRef = useRef(); + let inputRef = useRef(null); let [labelRef, label] = useSlot(); let { labelProps, diff --git a/packages/react-aria-components/src/OverlayArrow.tsx b/packages/react-aria-components/src/OverlayArrow.tsx index 4cc25b08543..6c9ca1cd867 100644 --- a/packages/react-aria-components/src/OverlayArrow.tsx +++ b/packages/react-aria-components/src/OverlayArrow.tsx @@ -20,7 +20,7 @@ interface OverlayArrowContextValue { placement: PlacementAxis } -export const OverlayArrowContext = createContext(null); +export const OverlayArrowContext = createContext(null); export interface OverlayArrowProps extends Omit, 'className' | 'style' | 'children'>, RenderProps {} @@ -33,7 +33,7 @@ export interface OverlayArrowRenderProps { } function OverlayArrow(props: OverlayArrowProps, ref: ForwardedRef) { - let {arrowProps, placement} = useContext(OverlayArrowContext); + let {arrowProps, placement} = useContext(OverlayArrowContext)!; let style: CSSProperties = { ...arrowProps.style, position: 'absolute', diff --git a/packages/react-aria-components/src/Popover.tsx b/packages/react-aria-components/src/Popover.tsx index 526fb7e2860..13384f0d594 100644 --- a/packages/react-aria-components/src/Popover.tsx +++ b/packages/react-aria-components/src/Popover.tsx @@ -45,15 +45,16 @@ export interface PopoverRenderProps { } interface PopoverContextValue extends PopoverProps { - state?: OverlayTriggerState, - preserveChildren?: boolean + state: OverlayTriggerState, + preserveChildren?: boolean, + triggerRef: RefObject } export const PopoverContext = createContext>(null); function Popover(props: PopoverProps, ref: ForwardedRef) { [props, ref] = useContextProps(props, ref, PopoverContext); - let {preserveChildren, state} = props as PopoverContextValue; + let {preserveChildren, state, triggerRef} = props as PopoverContextValue; let isExiting = useExitAnimation(ref, state.isOpen); if (state && !state.isOpen && !isExiting) { @@ -63,7 +64,7 @@ function Popover(props: PopoverProps, ref: ForwardedRef) { return ( diff --git a/packages/react-aria-components/src/ProgressBar.tsx b/packages/react-aria-components/src/ProgressBar.tsx index eac03bc6b79..e166a111174 100644 --- a/packages/react-aria-components/src/ProgressBar.tsx +++ b/packages/react-aria-components/src/ProgressBar.tsx @@ -21,12 +21,12 @@ export interface ProgressBarRenderProps { /** * The value as a percentage between the minimum and maximum. */ - percentage: number, + percentage?: number, /** * A formatted version of the value. * @selector [aria-valuetext] */ - valueText: string, + valueText?: string, /** * Whether the progress bar is indeterminate. * @selector :not([aria-valuenow]) @@ -42,7 +42,7 @@ function ProgressBar(props: ProgressBarProps, ref: ForwardedRef) value = 0, minValue = 0, maxValue = 100, - isIndeterminate + isIndeterminate = false } = props; let [labelRef, label] = useSlot(); diff --git a/packages/react-aria-components/src/RadioGroup.tsx b/packages/react-aria-components/src/RadioGroup.tsx index a06582327d8..b50adf77421 100644 --- a/packages/react-aria-components/src/RadioGroup.tsx +++ b/packages/react-aria-components/src/RadioGroup.tsx @@ -46,7 +46,7 @@ export interface RadioGroupRenderProps { * The validation state of the radio group. * @selector [aria-invalid] */ - validationState: ValidationState + validationState: ValidationState | null } export interface RadioRenderProps { @@ -89,7 +89,7 @@ export interface RadioRenderProps { * Whether the radio is valid or invalid. * @selector [data-validation-state="valid | invalid"] */ - validationState: ValidationState, + validationState: ValidationState | null, /** * Whether the checkbox is required. * @selector [data-required] @@ -98,7 +98,7 @@ export interface RadioRenderProps { } export const RadioGroupContext = createContext>(null); -let InternalRadioContext = createContext(null); +let InternalRadioContext = createContext(null); function RadioGroup(props: RadioGroupProps, ref: ForwardedRef) { [props, ref] = useContextProps(props, ref, RadioGroupContext); @@ -141,7 +141,7 @@ function RadioGroup(props: RadioGroupProps, ref: ForwardedRef) { } function Radio(props: RadioProps, ref: ForwardedRef) { - let state = React.useContext(InternalRadioContext); + let state = React.useContext(InternalRadioContext)!; let domRef = useObjectRef(ref); let {inputProps, isSelected, isDisabled, isPressed: isPressedKeyboard} = useRadio(props, state, domRef); let {isFocused, isFocusVisible, focusProps} = useFocusRing(); diff --git a/packages/react-aria-components/src/SearchField.tsx b/packages/react-aria-components/src/SearchField.tsx index cbeecd771fb..6857ea07983 100644 --- a/packages/react-aria-components/src/SearchField.tsx +++ b/packages/react-aria-components/src/SearchField.tsx @@ -33,7 +33,7 @@ export const SearchFieldContext = createContext) { [props, ref] = useContextProps(props, ref, SearchFieldContext); - let inputRef = useRef(null); + let inputRef = useRef(null); let [labelRef, label] = useSlot(); let state = useSearchFieldState(props); let {labelProps, inputProps, clearButtonProps, descriptionProps, errorMessageProps} = useSearchField({ @@ -48,7 +48,7 @@ function SearchField(props: SearchFieldProps, ref: ForwardedRef) }); return ( -
, HTMLDivElement>>(null); -const InternalSelectContext = createContext(null); +const InternalSelectContext = createContext(null); function Select(props: SelectProps, ref: ForwardedRef) { [props, ref] = useContextProps(props, ref, SelectContext); @@ -44,7 +44,7 @@ function Select(props: SelectProps, ref: ForwardedRef(props: SelectProps, ref: ForwardedRef(null); let onResize = useCallback(() => { if (buttonRef.current) { setButtonWidth(buttonRef.current.offsetWidth + 'px'); @@ -131,7 +131,7 @@ export interface SelectValueRenderProps { export interface SelectValueProps extends Omit, keyof RenderProps>, RenderProps {} function SelectValue(props: SelectValueProps, ref: ForwardedRef) { - let {state, valueProps} = useContext(InternalSelectContext); + let {state, valueProps} = useContext(InternalSelectContext)!; let renderProps = useRenderProps({ ...props, defaultChildren: state.selectedItem?.rendered || 'Select an item', diff --git a/packages/react-aria-components/src/Slider.tsx b/packages/react-aria-components/src/Slider.tsx index ea33d4d1be1..17a193b4bac 100644 --- a/packages/react-aria-components/src/Slider.tsx +++ b/packages/react-aria-components/src/Slider.tsx @@ -33,7 +33,7 @@ interface SliderContextValue { } export const SliderContext = createContext>(null); -const InternalSliderContext = createContext(null); +const InternalSliderContext = createContext(null); export interface SliderRenderProps { /** @@ -50,7 +50,7 @@ export interface SliderRenderProps { function Slider(props: SliderProps, ref: ForwardedRef) { [props, ref] = useContextProps(props, ref, SliderContext); - let trackRef = useRef(null); + let trackRef = useRef(null); let numberFormatter = useNumberFormatter(props.formatOptions); let state = useSliderState({...props, numberFormatter}); let [labelRef, label] = useSlot(); @@ -93,7 +93,7 @@ export {_Slider as Slider}; export interface SliderOutputProps extends RenderProps {} function SliderOutput({children, style, className}: SliderOutputProps, ref: ForwardedRef) { - let {state, outputProps} = useContext(InternalSliderContext); + let {state, outputProps} = useContext(InternalSliderContext)!; let renderProps = useRenderProps({ className, style, @@ -115,7 +115,7 @@ export {_SliderOutput as SliderOutput}; export interface SliderTrackProps extends RenderProps {} function SliderTrack(props: SliderTrackProps, ref: ForwardedRef) { - let {state, trackProps, trackRef} = useContext(InternalSliderContext); + let {state, trackProps, trackRef} = useContext(InternalSliderContext)!; let domRef = mergeRefs(ref, trackRef); let renderProps = useRenderProps({ ...props, @@ -160,9 +160,9 @@ export interface SliderThumbRenderProps { export interface SliderThumbProps extends AriaSliderThumbProps, RenderProps {} function SliderThumb(props: SliderThumbProps, ref: ForwardedRef) { - let {state, trackRef} = useContext(InternalSliderContext); + let {state, trackRef} = useContext(InternalSliderContext)!; let {index = 0} = props; - let inputRef = useRef(null); + let inputRef = useRef(null); let [labelRef, label] = useSlot(); let {thumbProps, inputProps, labelProps, isDragging, isFocused, isDisabled} = useSliderThumb({ ...props, diff --git a/packages/react-aria-components/src/Table.tsx b/packages/react-aria-components/src/Table.tsx index a2058d41f7a..6edc421cccc 100644 --- a/packages/react-aria-components/src/Table.tsx +++ b/packages/react-aria-components/src/Table.tsx @@ -102,7 +102,7 @@ class TableCollection extends BaseCollection implements ITableCollection extends BaseCollection implements ITableCollection> { @@ -146,7 +146,7 @@ class TableCollection extends BaseCollection implements ITableCollection, HTMLTableElement>>(null); -const InternalTableContext = createContext>(null); +const InternalTableContext = createContext | null>(null); export interface TableProps extends Omit, 'children'>, CollectionProps, StyleProps, SlotProps { /** @@ -172,7 +172,7 @@ function Table(props: TableProps, ref: ForwardedRef(null); +const TableOptionsContext = createContext(null); /** * Returns options from the parent `` component. */ export function useTableOptions(): TableOptionsContextValue { - return useContext(TableOptionsContext); + return useContext(TableOptionsContext)!; } export interface TableHeaderProps { @@ -305,7 +305,7 @@ export interface ColumnRenderProps { * The current sort direction. * @selector [aria-sort="ascending | descending"] */ - sortDirection: SortDirection | null + sortDirection?: SortDirection } export interface ColumnProps extends RenderProps { @@ -428,13 +428,13 @@ function TableRowGroup({type: Element, children, ...otherProps}) { } function TableHeaderRow({item}: {item: GridNode}) { - let ref = useRef(); - let state = useContext(InternalTableContext); + let ref = useRef(null); + let state = useContext(InternalTableContext)!; let {rowProps} = useTableHeaderRow({node: item}, state, ref); let {checkboxProps} = useTableSelectAllCheckbox(state); let cells = useCachedChildren({ - items: state.collection.getChildren(item.key), + items: state.collection.getChildren!(item.key), children: (item) => { switch (item.type) { case 'column': @@ -462,8 +462,8 @@ function TableHeaderRow({item}: {item: GridNode}) { } function TableColumnHeader({column}: {column: GridNode}) { - let ref = useRef(); - let state = useContext(InternalTableContext); + let ref = useRef(null); + let state = useContext(InternalTableContext)!; let {columnHeaderProps} = useTableColumnHeader( {node: column}, state, @@ -482,8 +482,8 @@ function TableColumnHeader({column}: {column: GridNode}) { isFocusVisible, allowsSorting: column.props.allowsSorting, sortDirection: state.sortDescriptor?.column === column.key - ? state.sortDescriptor?.direction - : null + ? state.sortDescriptor.direction + : undefined } }); @@ -500,9 +500,9 @@ function TableColumnHeader({column}: {column: GridNode}) { ); } -function TableRow({item}: {item: GridNode}) { - let ref = useRef(); - let state = useContext(InternalTableContext); +function TableRow({item}: {item: GridNode}) { + let ref = useRef(null); + let state = useContext(InternalTableContext)!; let {rowProps, ...states} = useTableRow( { node: item @@ -536,8 +536,8 @@ function TableRow({item}: {item: GridNode}) { }); let cells = useCachedChildren({ - items: state.collection.getChildren(item.key), - children: (item: Node) => { + items: state.collection.getChildren!(item.key), + children: (item: Node) => { switch (item.type) { case 'cell': return ; @@ -571,8 +571,8 @@ function TableRow({item}: {item: GridNode}) { } function TableCell({cell}: {cell: GridNode}) { - let ref = useRef(); - let state = useContext(InternalTableContext); + let ref = useRef(null); + let state = useContext(InternalTableContext)!; // @ts-ignore cell.column = state.collection.columns[cell.index]; diff --git a/packages/react-aria-components/src/Tabs.tsx b/packages/react-aria-components/src/Tabs.tsx index bf04abeafce..775d6527464 100644 --- a/packages/react-aria-components/src/Tabs.tsx +++ b/packages/react-aria-components/src/Tabs.tsx @@ -14,7 +14,7 @@ import {AriaLabelingProps} from '@react-types/shared'; import {AriaTabListProps, AriaTabPanelProps, mergeProps, Orientation, useFocusRing, useHover, useTab, useTabList, useTabPanel} from 'react-aria'; import {CollectionProps, Item, useCollection} from './Collection'; import {ContextValue, forwardRefType, RenderProps, SlotProps, StyleRenderProps, useContextProps, useRenderProps} from './utils'; -import {Node, useTabListState} from 'react-stately'; +import {Node, TabListState, useTabListState} from 'react-stately'; import React, {createContext, ForwardedRef, forwardRef, Key, useContext, useEffect, useState} from 'react'; import {useObjectRef} from '@react-aria/utils'; @@ -95,13 +95,19 @@ export interface TabPanelRenderProps { isFocusVisible: boolean } +interface InternalTabsContextValue { + state: TabListState | null, + setState: React.Dispatch | null>>, + orientation: Orientation +} + export const TabsContext = createContext>(null); -const InternalTabsContext = createContext(null); +const InternalTabsContext = createContext(null); function Tabs(props: TabsProps, ref: ForwardedRef) { [props, ref] = useContextProps(props, ref, TabsContext); let {orientation = 'horizontal'} = props; - let [state, setState] = useState(null); + let [state, setState] = useState | null>(null); let renderProps = useRenderProps({ ...props, @@ -131,19 +137,18 @@ const _Tabs = forwardRef(Tabs); export {_Tabs as Tabs}; function TabList(props: TabListProps, ref: ForwardedRef) { - let {setState, orientation} = useContext(InternalTabsContext); + let {setState, orientation} = useContext(InternalTabsContext)!; let objectRef = useObjectRef(ref); let {portal, collection} = useCollection(props); let state = useTabListState({ ...props, collection, - children: null + children: undefined }); let {tabListProps} = useTabList({ ...props, - children: null, orientation }, state, objectRef); @@ -193,7 +198,7 @@ export function Tab(props: TabProps): JSX.Element { function TabInner({item, state}) { let {key} = item; - let ref = React.useRef(); + let ref = React.useRef(null); let {tabProps, isSelected, isDisabled, isPressed} = useTab({key}, state, ref); let {focusProps, isFocused, isFocusVisible} = useFocusRing(); let {hoverProps, isHovered} = useHover({ @@ -231,9 +236,11 @@ export interface TabPanelsProps extends Omit, 'disabledKey * The ids of the items within the must match up with a corresponding item inside the . */ export function TabPanels(props: TabPanelsProps) { - const {state} = useContext(InternalTabsContext); + const {state} = useContext(InternalTabsContext)!; let {portal, collection} = useCollection(props); - const selectedItem = collection.getItem(state?.selectedKey); + const selectedItem = state?.selectedKey != null + ? collection.getItem(state!.selectedKey) + : null; return ( <> @@ -251,9 +258,9 @@ export function TabPanel(props: TabPanelProps): JSX.Element { } function SelectedTabPanel({item}: {item: Node}) { - const {state} = useContext(InternalTabsContext); - let ref = React.useRef(); - let {tabPanelProps} = useTabPanel(item.props, state, ref); + const {state} = useContext(InternalTabsContext)!; + let ref = React.useRef(null); + let {tabPanelProps} = useTabPanel(item.props, state!, ref); let {focusProps, isFocused, isFocusVisible} = useFocusRing(); let renderProps = useRenderProps({ diff --git a/packages/react-aria-components/src/TextField.tsx b/packages/react-aria-components/src/TextField.tsx index 2b303b34cf5..fa5875754b2 100644 --- a/packages/react-aria-components/src/TextField.tsx +++ b/packages/react-aria-components/src/TextField.tsx @@ -24,7 +24,7 @@ export const TextFieldContext = createContext) { [props, ref] = useContextProps(props, ref, TextFieldContext); - let inputRef = useRef(null); + let inputRef = useRef(null); let [labelRef, label] = useSlot(); let {labelProps, inputProps, descriptionProps, errorMessageProps} = useTextField({ ...props, diff --git a/packages/react-aria-components/src/Tooltip.tsx b/packages/react-aria-components/src/Tooltip.tsx index 64b416c0fc0..61346709efc 100644 --- a/packages/react-aria-components/src/Tooltip.tsx +++ b/packages/react-aria-components/src/Tooltip.tsx @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import {AriaLabelingProps, DOMAttributes} from '@react-types/shared'; +import {AriaLabelingProps, DOMAttributes, FocusableElement} from '@react-types/shared'; import {FocusableProvider} from '@react-aria/focus'; import {mergeProps, OverlayContainer, PlacementAxis, PositionProps, useOverlayPosition, useTooltip, useTooltipTrigger} from 'react-aria'; import {mergeRefs, useObjectRef} from '@react-aria/utils'; @@ -45,11 +45,11 @@ export interface TooltipRenderProps { interface TooltipContextValue { state: TooltipTriggerState, - triggerRef: RefObject, + triggerRef: RefObject, tooltipProps: DOMAttributes } -const InternalTooltipContext = createContext(null); +const InternalTooltipContext = createContext(null); /** * TooltipTrigger wraps around a trigger element and a Tooltip. It handles opening and closing @@ -58,7 +58,7 @@ const InternalTooltipContext = createContext(null); */ export function TooltipTrigger(props: TooltipTriggerComponentProps) { let state = useTooltipTriggerState(props); - let ref = useRef(); + let ref = useRef(null); let {triggerProps, tooltipProps} = useTooltipTrigger(props, state, ref); return ( @@ -71,7 +71,7 @@ export function TooltipTrigger(props: TooltipTriggerComponentProps) { } function Tooltip(props: TooltipProps, ref: ForwardedRef) { - let {state} = useContext(InternalTooltipContext); + let {state} = useContext(InternalTooltipContext)!; let objectRef = useObjectRef(ref); let isExiting = useExitAnimation(objectRef, state.isOpen); if (!state.isOpen && !isExiting) { @@ -92,9 +92,9 @@ const _Tooltip = forwardRef(Tooltip); export {_Tooltip as Tooltip}; function TooltipInner(props: TooltipProps & {isExiting: boolean, tooltipRef: ForwardedRef}) { - let {state, triggerRef, tooltipProps: triggerTooltipProps} = useContext(InternalTooltipContext); + let {state, triggerRef, tooltipProps: triggerTooltipProps} = useContext(InternalTooltipContext)!; - let overlayRef = useRef(); + let overlayRef = useRef(null); let {overlayProps, arrowProps, placement} = useOverlayPosition({ placement: props.placement || 'top', targetRef: triggerRef, diff --git a/packages/react-aria-components/src/utils.tsx b/packages/react-aria-components/src/utils.tsx index a090c2ccf7f..fe385d56622 100644 --- a/packages/react-aria-components/src/utils.tsx +++ b/packages/react-aria-components/src/utils.tsx @@ -28,7 +28,7 @@ interface SlottedValue { [slotCallbackSymbol]?: (value: T) => void } -export type ContextValue = SlottedValue> | WithRef; +export type ContextValue = SlottedValue> | WithRef | null | undefined; type ProviderValue = [React.Context, T]; type ProviderValues = @@ -117,7 +117,7 @@ export interface SlotProps { export function useContextProps(props: T & SlotProps, ref: React.ForwardedRef, context: React.Context>): [T, React.RefObject] { let ctx = useContext(context) || {}; - if ('slots' in ctx) { + if ('slots' in ctx && ctx.slots) { if (!props.slot) { throw new Error('A slot prop is required'); } @@ -201,7 +201,7 @@ export function useExitAnimation(ref: RefObject, isOpen: boolean) { } function useAnimation(ref: RefObject, isActive: boolean, onEnd: () => void) { - let prevAnimation = useRef(null); + let prevAnimation = useRef(null); if (isActive && ref.current) { prevAnimation.current = window.getComputedStyle(ref.current).animation; } diff --git a/packages/react-aria/src/index.ts b/packages/react-aria/src/index.ts index 5fe5db9864f..f649633f264 100644 --- a/packages/react-aria/src/index.ts +++ b/packages/react-aria/src/index.ts @@ -73,7 +73,7 @@ export type {SSRProviderProps} from '@react-aria/ssr'; export type {AriaSliderProps, AriaSliderThumbProps, AriaSliderThumbOptions, SliderAria, SliderThumbAria} from '@react-aria/slider'; export type {AriaSwitchProps, SwitchAria} from '@react-aria/switch'; export type {AriaTableCellProps, AriaTableColumnHeaderProps, AriaTableProps, AriaTableSelectionCheckboxProps, GridAria, GridRowAria, GridRowProps, TableCellAria, TableColumnHeaderAria, TableHeaderRowAria, TableSelectAllCheckboxAria, TableSelectionCheckboxAria} from '@react-aria/table'; -export type {AriaTabListProps, AriaTabPanelProps, AriaTabProps, TabAria, TabListAria, TabPanelAria} from '@react-aria/tabs'; +export type {AriaTabListProps, AriaTabListOptions, AriaTabPanelProps, AriaTabProps, TabAria, TabListAria, TabPanelAria} from '@react-aria/tabs'; export type {AriaTextFieldOptions, AriaTextFieldProps, TextFieldAria} from '@react-aria/textfield'; export type {AriaTooltipProps, TooltipAria, TooltipTriggerAria, TooltipTriggerProps} from '@react-aria/tooltip'; export type {VisuallyHiddenAria, VisuallyHiddenProps} from '@react-aria/visually-hidden'; diff --git a/tsconfig.json b/tsconfig.json index 061d63f604f..687c66a793c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -59,7 +59,8 @@ "./packages/@react-types/statuslight", "./packages/@react-types/text", "./packages/@react-types/view", - "./packages/@react-types/well" + "./packages/@react-types/well", + "./packages/react-aria-components" ] } ] From c6fada95dfe2b9173d64f132e42d50165aaec768 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Fri, 17 Mar 2023 10:20:45 -0700 Subject: [PATCH 2/9] Allow null in types for DatePicker components #3187 --- packages/@react-types/calendar/src/index.d.ts | 4 ++-- packages/@react-types/datepicker/src/index.d.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/@react-types/calendar/src/index.d.ts b/packages/@react-types/calendar/src/index.d.ts index 84382eefa94..d07946a1581 100644 --- a/packages/@react-types/calendar/src/index.d.ts +++ b/packages/@react-types/calendar/src/index.d.ts @@ -56,8 +56,8 @@ export interface CalendarPropsBase { } export type DateRange = RangeValue; -export interface CalendarProps extends CalendarPropsBase, ValueBase> {} -export interface RangeCalendarProps extends CalendarPropsBase, ValueBase, RangeValue>> { +export interface CalendarProps extends CalendarPropsBase, ValueBase> {} +export interface RangeCalendarProps extends CalendarPropsBase, ValueBase | null, RangeValue>> { /** * When combined with `isDateUnavailable`, determines whether non-contiguous ranges, * i.e. ranges containing unavailable dates, may be selected. diff --git a/packages/@react-types/datepicker/src/index.d.ts b/packages/@react-types/datepicker/src/index.d.ts index 5fbe9cffabe..6d7aa8404d8 100644 --- a/packages/@react-types/datepicker/src/index.d.ts +++ b/packages/@react-types/datepicker/src/index.d.ts @@ -55,17 +55,17 @@ interface DateFieldBase extends InputBase, Validation, Focu } interface AriaDateFieldBaseProps extends DateFieldBase, AriaLabelingProps, DOMProps {} -export interface DateFieldProps extends DateFieldBase, ValueBase> {} +export interface DateFieldProps extends DateFieldBase, ValueBase> {} export interface AriaDateFieldProps extends DateFieldProps, AriaDateFieldBaseProps {} interface DatePickerBase extends DateFieldBase, OverlayTriggerProps {} export interface AriaDatePickerBaseProps extends DatePickerBase, AriaLabelingProps, DOMProps {} -export interface DatePickerProps extends DatePickerBase, ValueBase> {} +export interface DatePickerProps extends DatePickerBase, ValueBase> {} export interface AriaDatePickerProps extends DatePickerProps, AriaDatePickerBaseProps {} export type DateRange = RangeValue; -export interface DateRangePickerProps extends DatePickerBase, ValueBase, RangeValue>> { +export interface DateRangePickerProps extends DatePickerBase, ValueBase | null, RangeValue>> { /** * When combined with `isDateUnavailable`, determines whether non-contiguous ranges, * i.e. ranges containing unavailable dates, may be selected. @@ -112,7 +112,7 @@ type MappedTimeValue = T extends Time ? Time : never; -export interface TimePickerProps extends InputBase, Validation, FocusableProps, LabelableProps, HelpTextProps, ValueBase> { +export interface TimePickerProps extends InputBase, Validation, FocusableProps, LabelableProps, HelpTextProps, ValueBase> { /** Whether to display the time in 12 or 24 hour format. By default, this is determined by the user's locale. */ hourCycle?: 12 | 24, /** From 35957556e5d9c078901980c450bcfd33fa9ea1b6 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Fri, 17 Mar 2023 10:26:50 -0700 Subject: [PATCH 3/9] Make examples more strict --- .../react-aria-components/docs/Breadcrumbs.mdx | 2 +- .../react-aria-components/docs/Calendar.mdx | 2 -- .../react-aria-components/docs/ComboBox.mdx | 6 +++--- .../react-aria-components/docs/DateField.mdx | 6 +++--- .../react-aria-components/docs/DatePicker.mdx | 4 ++-- .../docs/DateRangePicker.mdx | 14 +++++++------- packages/react-aria-components/docs/Link.mdx | 4 ++-- .../react-aria-components/docs/NumberField.mdx | 4 ++-- .../react-aria-components/docs/RadioGroup.mdx | 4 ++-- .../docs/RangeCalendar.mdx | 7 ++++--- packages/react-aria-components/docs/Select.mdx | 4 ++-- packages/react-aria-components/docs/Table.mdx | 10 +++++++--- .../react-aria-components/docs/TimeField.mdx | 4 ++-- .../react-aria-components/src/Breadcrumbs.tsx | 9 ++++++++- .../react-aria-components/src/Collection.tsx | 12 ++++++------ .../react-aria-components/src/DateField.tsx | 2 +- .../react-aria-components/src/GridList.tsx | 2 +- packages/react-aria-components/src/Table.tsx | 18 ++++++++++-------- packages/react-aria-components/src/Tabs.tsx | 2 +- 19 files changed, 64 insertions(+), 52 deletions(-) diff --git a/packages/react-aria-components/docs/Breadcrumbs.mdx b/packages/react-aria-components/docs/Breadcrumbs.mdx index 601f6c5ebbf..bb8ebd2ae81 100644 --- a/packages/react-aria-components/docs/Breadcrumbs.mdx +++ b/packages/react-aria-components/docs/Breadcrumbs.mdx @@ -247,7 +247,7 @@ function Example() { {id: 3, label: 'March 2022 Assets'} ]); - let navigate = (item) => { + let navigate = (item: typeof breadcrumbs[0]) => { setBreadcrumbs(breadcrumbs.slice(0, breadcrumbs.indexOf(item) + 1)); }; diff --git a/packages/react-aria-components/docs/Calendar.mdx b/packages/react-aria-components/docs/Calendar.mdx index 0e2f66e943d..f4e459f06ea 100644 --- a/packages/react-aria-components/docs/Calendar.mdx +++ b/packages/react-aria-components/docs/Calendar.mdx @@ -470,7 +470,6 @@ Selected dates passed to `onChange` always use the same calendar system as the ` The below example displays a `Calendar` in the Hindi language, using the Indian calendar. Dates emitted from `onChange` are in the Gregorian calendar. ```tsx example -import type {DateValue} from 'react-aria-components'; import {I18nProvider} from '@react-aria/i18n'; function Example() { @@ -504,7 +503,6 @@ This example includes multiple unavailable date ranges, e.g. dates when no appoi ```tsx example -import type {DateValue} from 'react-aria-components'; import {today, isWeekend} from '@internationalized/date'; import {useLocale} from '@react-aria/i18n'; diff --git a/packages/react-aria-components/docs/ComboBox.mdx b/packages/react-aria-components/docs/ComboBox.mdx index c3e33c76cc9..184554b7f41 100644 --- a/packages/react-aria-components/docs/ComboBox.mdx +++ b/packages/react-aria-components/docs/ComboBox.mdx @@ -783,7 +783,7 @@ function ControlledComboBox() { // Specify how each of the ComboBox values should change when an // option is selected from the list box - let onSelectionChange = (key) => { + let onSelectionChange = (key: React.Key) => { setFieldState(prevState => { let selectedItem = prevState.items.find(option => option.id === key); return ({ @@ -796,7 +796,7 @@ function ControlledComboBox() { // Specify how each of the ComboBox values should change when the input // field is altered by the user - let onInputChange = (value) => { + let onInputChange = (value: string) => { setFieldState(prevState => ({ inputValue: value, selectedKey: value === '' ? '' : prevState.selectedKey, @@ -805,7 +805,7 @@ function ControlledComboBox() { }; // Show entire list if user opens the menu manually - let onOpenChange = (isOpen, menuTrigger) => { + let onOpenChange = (isOpen: boolean, menuTrigger?: string) => { if (menuTrigger === 'manual' && isOpen) { setFieldState(prevState => ({ inputValue: prevState.inputValue, diff --git a/packages/react-aria-components/docs/DateField.mdx b/packages/react-aria-components/docs/DateField.mdx index a19139b6e96..ee707264944 100644 --- a/packages/react-aria-components/docs/DateField.mdx +++ b/packages/react-aria-components/docs/DateField.mdx @@ -471,7 +471,7 @@ The below example displays a `DateField` in the Hindi language, using the Indian import {I18nProvider} from '@react-aria/i18n'; function Example() { - let [date, setDate] = React.useState(null); + let [date, setDate] = React.useState(null); return ( @@ -517,8 +517,8 @@ function Example() { value={date} onChange={setDate} validationState={isInvalid ? 'invalid' : 'valid'} - description={isInvalid ? null : 'Select a weekday'} - errorMessage={isInvalid ? 'We are closed on weekends' : null} /> + description={isInvalid ? undefined : 'Select a weekday'} + errorMessage={isInvalid ? 'We are closed on weekends' : undefined} /> ); } ``` diff --git a/packages/react-aria-components/docs/DatePicker.mdx b/packages/react-aria-components/docs/DatePicker.mdx index 01a42066d81..75c18e1e92e 100644 --- a/packages/react-aria-components/docs/DatePicker.mdx +++ b/packages/react-aria-components/docs/DatePicker.mdx @@ -845,7 +845,7 @@ The below example displays a `DatePicker` in the Hindi language, using the India import {I18nProvider} from '@react-aria/i18n'; function Example() { - let [date, setDate] = React.useState(null); + let [date, setDate] = React.useState(null); return ( @@ -889,7 +889,7 @@ function Example() { ]; let {locale} = useLocale(); - let isDateUnavailable = (date) => isWeekend(date, locale) || disabledRanges.some((interval) => date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0); + let isDateUnavailable = (date: DateValue) => isWeekend(date, locale) || disabledRanges.some((interval) => date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0); return } diff --git a/packages/react-aria-components/docs/DateRangePicker.mdx b/packages/react-aria-components/docs/DateRangePicker.mdx index e4127068fd1..239ec74eea4 100644 --- a/packages/react-aria-components/docs/DateRangePicker.mdx +++ b/packages/react-aria-components/docs/DateRangePicker.mdx @@ -903,7 +903,7 @@ import type {DateRange} from 'react-aria-components'; import {I18nProvider} from '@react-aria/i18n'; function Example() { - let [range, setRange] = React.useState(null); + let [range, setRange] = React.useState(null); return ( @@ -952,9 +952,9 @@ function Example() { [now.add({days: 23}), now.add({days: 24})], ]; - let isDateUnavailable = (date) => disabledRanges.some((interval) => date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0); - let [value, setValue] = React.useState(null); - let isInvalid = value && disabledRanges.some(interval => value.end.compare(interval[0]) >= 0 && value.start.compare(interval[1]) <= 0); + let isDateUnavailable = (date: DateValue) => disabledRanges.some((interval) => date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0); + let [value, setValue] = React.useState(null); + let isInvalid = disabledRanges.some(interval => value && value.end.compare(interval[0]) >= 0 && value.start.compare(interval[1]) <= 0); return ( + validationState={isInvalid ? 'invalid' : undefined} /> ); } ``` @@ -1007,8 +1007,8 @@ function Example() { value={range} onChange={setRange} validationState={isInvalid ? 'invalid' : 'valid'} - description={isInvalid ? null : 'Select your vacation dates'} - errorMessage={isInvalid ? 'Maximum stay duration is 1 week' : null} /> + description={isInvalid ? undefined : 'Select your vacation dates'} + errorMessage={isInvalid ? 'Maximum stay duration is 1 week' : undefined} /> ); } ``` diff --git a/packages/react-aria-components/docs/Link.mdx b/packages/react-aria-components/docs/Link.mdx index 2a74077f5ea..564de953d43 100644 --- a/packages/react-aria-components/docs/Link.mdx +++ b/packages/react-aria-components/docs/Link.mdx @@ -208,13 +208,13 @@ Each of these handlers receives a setPointerType(e.pointerType)} - onPressEnd={e => setPointerType(null)}> + onPressEnd={() => setPointerType('')}> Press me

{pointerType ? `You are pressing the link with a ${pointerType}!` : 'Ready to be pressed.'}

diff --git a/packages/react-aria-components/docs/NumberField.mdx b/packages/react-aria-components/docs/NumberField.mdx index cd7f7ccc880..7b05840fbfa 100644 --- a/packages/react-aria-components/docs/NumberField.mdx +++ b/packages/react-aria-components/docs/NumberField.mdx @@ -528,8 +528,8 @@ function Example() { value={value} onChange={setValue} label="Positive numbers only" - description={isValid ? 'Enter a positive number.' : null} - errorMessage={isValid ? null : value === 0 ? 'Is zero positive?' : 'Positive numbers are bigger than 0.'} /> + description={isValid ? 'Enter a positive number.' : undefined} + errorMessage={isValid ? undefined : value === 0 ? 'Is zero positive?' : 'Positive numbers are bigger than 0.'} /> ); } ``` diff --git a/packages/react-aria-components/docs/RadioGroup.mdx b/packages/react-aria-components/docs/RadioGroup.mdx index 88977517c31..977c3327780 100644 --- a/packages/react-aria-components/docs/RadioGroup.mdx +++ b/packages/react-aria-components/docs/RadioGroup.mdx @@ -397,8 +397,8 @@ function Example() { aria-label="Favorite pet" onChange={setSelected} validationState={isValid ? 'valid' : 'invalid'} - description={isValid ? 'Please select a pet.' : null} - errorMessage={isValid ? null : + description={isValid ? 'Please select a pet.' : undefined} + errorMessage={isValid ? undefined : selected === 'cats' ? 'No cats allowed.' : 'Please select dogs.' diff --git a/packages/react-aria-components/docs/RangeCalendar.mdx b/packages/react-aria-components/docs/RangeCalendar.mdx index 4f86965c02f..ae96bcf4d38 100644 --- a/packages/react-aria-components/docs/RangeCalendar.mdx +++ b/packages/react-aria-components/docs/RangeCalendar.mdx @@ -505,10 +505,11 @@ Selected dates passed to `onChange` always use the same calendar system as the ` The below example displays a `RangeCalendar` in the Hindi language, using the Indian calendar. Dates emitted from `onChange` are in the Gregorian calendar. ```tsx example +import type {DateRange} from 'react-aria-components'; import {I18nProvider} from '@react-aria/i18n'; function Example() { - let [range, setRange] = React.useState(null); + let [range, setRange] = React.useState(null); return ( @@ -550,7 +551,7 @@ function Example() { [now.add({days: 23}), now.add({days: 24})], ]; - let isDateUnavailable = (date) => disabledRanges.some((interval) => date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0); + let isDateUnavailable = (date: DateValue) => disabledRanges.some((interval) => date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0); return } @@ -633,7 +634,7 @@ function Example() { value={range} onChange={setRange} validationState={isInvalid ? 'invalid' : 'valid'} - errorMessage={isInvalid ? 'Maximum stay duration is 1 week' : null} /> + errorMessage={isInvalid ? 'Maximum stay duration is 1 week' : undefined} /> ); } ``` diff --git a/packages/react-aria-components/docs/Select.mdx b/packages/react-aria-components/docs/Select.mdx index 873163ee903..9312bb03f8a 100644 --- a/packages/react-aria-components/docs/Select.mdx +++ b/packages/react-aria-components/docs/Select.mdx @@ -908,8 +908,8 @@ function Example() { (new Set(props.defaultSelectedKeys || [2])); return (
diff --git a/packages/react-aria-components/docs/TimeField.mdx b/packages/react-aria-components/docs/TimeField.mdx index 65bf6190c8e..bb7c8c1607e 100644 --- a/packages/react-aria-components/docs/TimeField.mdx +++ b/packages/react-aria-components/docs/TimeField.mdx @@ -451,8 +451,8 @@ function Example() { value={time} onChange={setTime} validationState={isInvalid ? 'invalid' : 'valid'} - description={isInvalid ? null : 'Select a meeting time'} - errorMessage={isInvalid ? 'Meetings start every 15 minutes.' : null} /> + description={isInvalid ? undefined : 'Select a meeting time'} + errorMessage={isInvalid ? 'Meetings start every 15 minutes.' : undefined} /> ); } ``` diff --git a/packages/react-aria-components/src/Breadcrumbs.tsx b/packages/react-aria-components/src/Breadcrumbs.tsx index 9c4ff3d768e..5fe3209c668 100644 --- a/packages/react-aria-components/src/Breadcrumbs.tsx +++ b/packages/react-aria-components/src/Breadcrumbs.tsx @@ -15,6 +15,7 @@ import {ContextValue, forwardRefType, Provider, SlotProps, StyleProps, useContex import {filterDOMProps} from '@react-aria/utils'; import {HeadingContext} from './Heading'; import {LinkContext} from './Link'; +import {Node} from 'react-stately'; import React, {createContext, ForwardedRef, forwardRef, HTMLAttributes} from 'react'; export interface BreadcrumbsProps extends Omit, 'disabledKeys'>, Omit, StyleProps, SlotProps { @@ -58,7 +59,13 @@ function Breadcrumbs(props: BreadcrumbsProps, ref: Forwarde const _Breadcrumbs = (forwardRef as forwardRefType)(Breadcrumbs); export {_Breadcrumbs as Breadcrumbs}; -function BreadcrumbItem({node, isCurrent, isDisabled}) { +interface BreadcrumbItemProps { + node: Node, + isCurrent: boolean, + isDisabled?: boolean +} + +function BreadcrumbItem({node, isCurrent, isDisabled}: BreadcrumbItemProps) { // Recreating useBreadcrumbItem because we want to use composition instead of having the link builtin. let headingProps: HTMLAttributes | null = isCurrent ? {'aria-current': 'page'} : null; let linkProps = { diff --git a/packages/react-aria-components/src/Collection.tsx b/packages/react-aria-components/src/Collection.tsx index ca4b115bb82..147f4f20b74 100644 --- a/packages/react-aria-components/src/Collection.tsx +++ b/packages/react-aria-components/src/Collection.tsx @@ -13,9 +13,9 @@ import {CollectionBase} from '@react-types/shared'; import {createPortal} from 'react-dom'; import {DOMProps, RenderProps} from './utils'; import {Collection as ICollection, Node, SelectionBehavior, SelectionMode, ItemProps as SharedItemProps, SectionProps as SharedSectionProps} from 'react-stately'; +import {mergeProps} from 'react-aria'; import React, {cloneElement, createContext, Key, ReactElement, ReactNode, ReactPortal, useCallback, useContext, useMemo} from 'react'; import {useSyncExternalStore} from 'use-sync-external-store/shim/index.js'; -import { mergeProps } from '@react-aria/utils/dist/module'; // This Collection implementation is perhaps a little unusual. It works by rendering the React tree into a // Portal to a fake DOM implementation. This gives us efficient access to the tree of rendered objects, and @@ -272,7 +272,7 @@ export class ElementNode extends BaseNode { this.ownerDocument.dirtyNodes.add(this); } - get level() { + get level(): number { if (this.parentNode instanceof ElementNode) { return this.parentNode.level + (this.node.type === 'item' ? 1 : 0); } @@ -294,7 +294,7 @@ export class ElementNode extends BaseNode { // Special property that React passes through as an object rather than a string via setAttribute. // See below for details. - set multiple(value) { + set multiple(value: any) { let node = this.ownerDocument.getMutableNode(this); node.props = value; node.rendered = value.rendered; @@ -319,8 +319,8 @@ export class ElementNode extends BaseNode { hasAttribute() {} setAttribute(key: string, value: string) { - if (key in this.node) { - let node = this.ownerDocument.getMutableNode(this); + let node = this.ownerDocument.getMutableNode(this); + if (key in node) { node[key] = value; } } @@ -712,7 +712,7 @@ export const CollectionContext = createContext | export const CollectionRendererContext = createContext['children']>(null); export function Collection(props: CollectionProps): JSX.Element { - let ctx = useContext(CollectionContext); + let ctx = useContext(CollectionContext)!; props = mergeProps(ctx, props); let renderer = typeof props.children === 'function' ? props.children : null; return ( diff --git a/packages/react-aria-components/src/DateField.tsx b/packages/react-aria-components/src/DateField.tsx index e2263977453..d300c7795c4 100644 --- a/packages/react-aria-components/src/DateField.tsx +++ b/packages/react-aria-components/src/DateField.tsx @@ -116,7 +116,7 @@ function TimeField(props: TimeFieldProps, ref: Forwarded * A time field allows users to enter and edit time values using a keyboard. * Each part of a time value is displayed in an individually editable segment. */ -const _TimeField = forwardRef(TimeField); +const _TimeField = (forwardRef as forwardRefType)(TimeField); export {_TimeField as TimeField}; const InternalDateInputContext = createContext(null); diff --git a/packages/react-aria-components/src/GridList.tsx b/packages/react-aria-components/src/GridList.tsx index fe59c5dcc86..472c5092752 100644 --- a/packages/react-aria-components/src/GridList.tsx +++ b/packages/react-aria-components/src/GridList.tsx @@ -72,7 +72,7 @@ function GridList(props: GridListProps, ref: ForwardedRef}) { let state = useContext(InternalGridListContext)!; let ref = React.useRef(null); let {rowProps, gridCellProps, descriptionProps, ...states} = useGridListItem( diff --git a/packages/react-aria-components/src/Table.tsx b/packages/react-aria-components/src/Table.tsx index 6edc421cccc..8139ed1f58d 100644 --- a/packages/react-aria-components/src/Table.tsx +++ b/packages/react-aria-components/src/Table.tsx @@ -1,7 +1,7 @@ import {BaseCollection, CollectionContext, CollectionProps, CollectionRendererContext, ItemRenderProps, NodeValue, useCachedChildren, useCollection} from './Collection'; import {buildHeaderRows} from '@react-stately/table'; import {CheckboxContext} from './Checkbox'; -import {ContextValue, forwardRefType, Provider, RenderProps, SlotProps, StyleProps, useContextProps, useRenderProps} from './utils'; +import {ContextValue, Provider, RenderProps, SlotProps, StyleProps, useContextProps, useRenderProps} from './utils'; import {DisabledBehavior, Node, SelectionBehavior, SelectionMode, SortDirection, TableState, useTableState} from 'react-stately'; import {filterDOMProps} from '@react-aria/utils'; import {GridNode} from '@react-types/grid'; @@ -145,10 +145,12 @@ class TableCollection extends BaseCollection implements ITableCollection, HTMLTableElement>>(null); +export const TableContext = createContext>(null); const InternalTableContext = createContext | null>(null); -export interface TableProps extends Omit, 'children'>, CollectionProps, StyleProps, SlotProps { +export interface TableProps extends Omit, 'children'>, StyleProps, SlotProps { + /** The elements that make up the table. Includes the TableHeader, TableBody, Columns, and Rows. */ + children?: ReactNode, /** * How multiple selection should behave in the collection. * @default "toggle" @@ -165,9 +167,9 @@ export interface TableProps extends Omit, 'children'>, Co onCellAction?: (key: Key) => void } -function Table(props: TableProps, ref: ForwardedRef) { +function Table(props: TableProps, ref: ForwardedRef) { [props, ref] = useContextProps(props, ref, TableContext); - let initialCollection = useMemo(() => new TableCollection(), []); + let initialCollection = useMemo(() => new TableCollection(), []); let {portal, collection} = useCollection(props, initialCollection); let state = useTableState({ ...props, @@ -179,7 +181,7 @@ function Table(props: TableProps, ref: ForwardedRef) => { + children: useCallback((item: Node) => { switch (item.type) { case 'headerrow': return ; @@ -191,7 +193,7 @@ function Table(props: TableProps, ref: ForwardedRef) => { + children: useCallback((item: Node) => { switch (item.type) { case 'item': return ; @@ -239,7 +241,7 @@ function Table(props: TableProps, ref: ForwardedRef, state: TabListState}) { let {key} = item; let ref = React.useRef(null); let {tabProps, isSelected, isDisabled, isPressed} = useTab({key}, state, ref); From 3e6b36714c10b55cd9f67f4f12c819286583d944 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Fri, 17 Mar 2023 10:40:59 -0700 Subject: [PATCH 4/9] Fix ComboBox example --- packages/react-aria-components/docs/ComboBox.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-aria-components/docs/ComboBox.mdx b/packages/react-aria-components/docs/ComboBox.mdx index 184554b7f41..430dfcc7da2 100644 --- a/packages/react-aria-components/docs/ComboBox.mdx +++ b/packages/react-aria-components/docs/ComboBox.mdx @@ -772,7 +772,7 @@ function ControlledComboBox() { // Store ComboBox input value, selected option, open state, and items // in a state tracker let [fieldState, setFieldState] = React.useState({ - selectedKey: '', + selectedKey: '' as React.Key, inputValue: '', items: optionList }); From 05dc0165ea9a7bf600541ac98996c7e3d600733f Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Mon, 20 Mar 2023 17:48:35 -0700 Subject: [PATCH 5/9] Strict fixes --- .../@react-aria/selection/src/ListKeyboardDelegate.ts | 8 ++++++++ packages/@react-aria/utils/src/mergeProps.ts | 5 +++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/@react-aria/selection/src/ListKeyboardDelegate.ts b/packages/@react-aria/selection/src/ListKeyboardDelegate.ts index 7a14cc30a16..3f18b048469 100644 --- a/packages/@react-aria/selection/src/ListKeyboardDelegate.ts +++ b/packages/@react-aria/selection/src/ListKeyboardDelegate.ts @@ -36,6 +36,8 @@ export class ListKeyboardDelegate implements KeyboardDelegate { key = this.collection.getKeyAfter(key); } + + return null; } getKeyAbove(key: Key) { @@ -48,6 +50,8 @@ export class ListKeyboardDelegate implements KeyboardDelegate { key = this.collection.getKeyBefore(key); } + + return null; } getFirstKey() { @@ -60,6 +64,8 @@ export class ListKeyboardDelegate implements KeyboardDelegate { key = this.collection.getKeyAfter(key); } + + return null; } getLastKey() { @@ -72,6 +78,8 @@ export class ListKeyboardDelegate implements KeyboardDelegate { key = this.collection.getKeyBefore(key); } + + return null; } private getItem(key: Key): HTMLElement { diff --git a/packages/@react-aria/utils/src/mergeProps.ts b/packages/@react-aria/utils/src/mergeProps.ts index 9fdb5a1c017..e1cd5b03716 100644 --- a/packages/@react-aria/utils/src/mergeProps.ts +++ b/packages/@react-aria/utils/src/mergeProps.ts @@ -19,7 +19,8 @@ interface Props { } // taken from: https://stackoverflow.com/questions/51603250/typescript-3-parameter-list-intersection-type/51604379#51604379 -type TupleTypes = { [P in keyof T]: T[P] } extends { [key: number]: infer V } ? V : never; +type TupleTypes = { [P in keyof T]: T[P] } extends { [key: number]: infer V } ? NullToObject : never; +type NullToObject = T extends (null | undefined) ? {} : T; // eslint-disable-next-line no-undef, @typescript-eslint/no-unused-vars type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never; @@ -30,7 +31,7 @@ type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ( * For all other props, the last prop object overrides all previous ones. * @param args - Multiple sets of props to merge together. */ -export function mergeProps(...args: T): UnionToIntersection> { +export function mergeProps(...args: T): UnionToIntersection> { // Start with a base clone of the first argument. This is a lot faster than starting // with an empty object and adding properties as we go. let result: Props = {...args[0]}; From 66bb4241cd413cbe9446e88153dcb6c0731ae3fc Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Mon, 20 Mar 2023 19:35:56 -0700 Subject: [PATCH 6/9] Add TS type guard functions for drop item types --- packages/@react-aria/dnd/src/index.ts | 1 + packages/@react-aria/dnd/src/utils.ts | 17 ++++++++++++++++- packages/react-aria/src/index.ts | 2 +- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/@react-aria/dnd/src/index.ts b/packages/@react-aria/dnd/src/index.ts index c72126cd021..76e0f8a6735 100644 --- a/packages/@react-aria/dnd/src/index.ts +++ b/packages/@react-aria/dnd/src/index.ts @@ -65,3 +65,4 @@ export {useClipboard} from './useClipboard'; export {DragPreview} from './DragPreview'; export {ListDropTargetDelegate} from './ListDropTargetDelegate'; export {isVirtualDragging} from './DragManager'; +export {isDirectoryDropItem, isFileDropItem, isTextDropItem} from './utils'; diff --git a/packages/@react-aria/dnd/src/utils.ts b/packages/@react-aria/dnd/src/utils.ts index 866602471cc..c46e495dc8a 100644 --- a/packages/@react-aria/dnd/src/utils.ts +++ b/packages/@react-aria/dnd/src/utils.ts @@ -11,7 +11,7 @@ */ import {CUSTOM_DRAG_TYPE, DROP_OPERATION, GENERIC_TYPE, NATIVE_DRAG_TYPES} from './constants'; -import {DirectoryDropItem, DragItem, DropItem, FileDropItem, DragTypes as IDragTypes} from '@react-types/shared'; +import {DirectoryDropItem, DragItem, DropItem, FileDropItem, DragTypes as IDragTypes, TextDropItem} from '@react-types/shared'; import {DroppableCollectionState} from '@react-stately/dnd'; import {getInteractionModality, useInteractionModality} from '@react-aria/interactions'; import {Key, RefObject} from 'react'; @@ -314,6 +314,21 @@ function getEntryFile(entry: FileSystemFileEntry): Promise { return new Promise((resolve, reject) => entry.file(resolve, reject)); } +/** Returns whether a drop item contains text data. */ +export function isTextDropItem(dropItem: DropItem): dropItem is TextDropItem { + return dropItem.kind === 'text'; +} + +/** Returns whether a drop item is a file. */ +export function isFileDropItem(dropItem: DropItem): dropItem is FileDropItem { + return dropItem.kind === 'file'; +} + +/** Returns whether a drop item is a directory. */ +export function isDirectoryDropItem(dropItem: DropItem): dropItem is DirectoryDropItem { + return dropItem.kind === 'directory'; +} + // Global DnD collection state tracker. export interface DnDState { /** A ref for the of the drag items in the current drag session if any. */ diff --git a/packages/react-aria/src/index.ts b/packages/react-aria/src/index.ts index f649633f264..2c0f65f5f45 100644 --- a/packages/react-aria/src/index.ts +++ b/packages/react-aria/src/index.ts @@ -17,7 +17,7 @@ export {useCheckbox, useCheckboxGroup, useCheckboxGroupItem} from '@react-aria/c export {useComboBox} from '@react-aria/combobox'; export {useDateField, useDatePicker, useDateRangePicker, useDateSegment, useTimeField} from '@react-aria/datepicker'; export {useDialog} from '@react-aria/dialog'; -export {useDrag, useDrop, useDraggableCollection, useDroppableCollection, useDroppableItem, useDropIndicator, useDraggableItem, useClipboard, DragPreview, ListDropTargetDelegate, DIRECTORY_DRAG_TYPE} from '@react-aria/dnd'; +export {useDrag, useDrop, useDraggableCollection, useDroppableCollection, useDroppableItem, useDropIndicator, useDraggableItem, useClipboard, DragPreview, ListDropTargetDelegate, DIRECTORY_DRAG_TYPE, isDirectoryDropItem, isFileDropItem, isTextDropItem} from '@react-aria/dnd'; export {FocusRing, FocusScope, useFocusManager, useFocusRing, useFocusable} from '@react-aria/focus'; export {I18nProvider, useCollator, useDateFormatter, useFilter, useLocale, useLocalizedStringFormatter, useMessageFormatter, useNumberFormatter} from '@react-aria/i18n'; export {useFocus, useFocusVisible, useFocusWithin, useHover, useInteractOutside, useKeyboard, useMove, usePress, useLongPress} from '@react-aria/interactions'; From ce672f2d8ffdbb5e29252b0c315b31f7163c3d39 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Mon, 20 Mar 2023 19:41:04 -0700 Subject: [PATCH 7/9] Convert dnd examples to TS --- .../react-aria-components/docs/GridList.mdx | 71 ++++++++++++----- .../react-aria-components/docs/ListBox.mdx | 75 +++++++++++++----- packages/react-aria-components/docs/Table.mdx | 76 +++++++++++++------ 3 files changed, 161 insertions(+), 61 deletions(-) diff --git a/packages/react-aria-components/docs/GridList.mdx b/packages/react-aria-components/docs/GridList.mdx index 3216a666724..de9af8abc36 100644 --- a/packages/react-aria-components/docs/GridList.mdx +++ b/packages/react-aria-components/docs/GridList.mdx @@ -846,7 +846,7 @@ function DraggableGridList() { /*- begin highlight -*/ getItems(keys) { return [...keys].map(key => { - let item = items.get(key as string); + let item = items.get(key as string)!; return { 'text/plain': item.name, 'text/html': `<${item.style}>${item.name}`, @@ -876,8 +876,13 @@ function DraggableGridList() { Dropping on the GridList as a whole can be enabled using the `onRootDrop` event. When a valid drag hovers over the GridList, it receives the `isDropTarget` state and can be styled using the `[data-drop-target]` CSS selector. ```tsx example +interface Item { + id: number, + name: string +} + function Example() { - let [items, setItems] = React.useState([]); + let [items, setItems] = React.useState([]); let { dragAndDropHooks } = useDragAndDrop({ /*- begin highlight -*/ @@ -1091,17 +1096,25 @@ A ([]); let { dragAndDropHooks } = useDragAndDrop({ /*- begin highlight -*/ acceptedDragTypes: ['custom-app-type'], async onRootDrop(e) { let items = await Promise.all( - e.items.map(async (item: TextDropItem) => JSON.parse(await item.getText('custom-app-type'))) + e.items + .filter(isTextDropItem) + .map(async item => JSON.parse(await item.getText('custom-app-type'))) ); setItems(items); } @@ -1110,7 +1123,7 @@ function DroppableGridList() { return ( "Drop items here"}> - {({name, style: Tag = 'span'}) => {name}} + {item => {React.createElement(item.style, null, item.name)}} ); } @@ -1129,17 +1142,23 @@ A ([]); let { dragAndDropHooks } = useDragAndDrop({ /*- begin highlight -*/ acceptedDragTypes: ['image/jpeg', 'image/png'], async onRootDrop(e) { let items = await Promise.all( - e.items.map(async (item: FileDropItem) => ({ + e.items.filter(isFileDropItem).map(async item => ({ id: Math.random(), url: URL.createObjectURL(await item.getFile()), name: item.name @@ -1200,23 +1219,28 @@ The `getEntries` method returns an [async iterable](https://developer.mozilla.or This example accepts directory drops over the whole collection, and renders the contents as items in the list. `DIRECTORY_DRAG_TYPE` is imported from `react-aria-components` and included in the `acceptedDragTypes` prop to limit the accepted items to only directories. ```tsx example -import type {DirectoryDropItem} from 'react-aria-components'; import File from '@spectrum-icons/workflow/FileTxt'; import Folder from '@spectrum-icons/workflow/Folder'; ///- begin highlight -/// -import {DIRECTORY_DRAG_TYPE} from 'react-aria-components'; +import {DIRECTORY_DRAG_TYPE, isDirectoryDropItem} from 'react-aria-components'; ///- end highlight -/// +interface DirItem { + name: string, + kind: string +} + function Example() { - let [files, setFiles] = React.useState([]); + let [files, setFiles] = React.useState([]); let { dragAndDropHooks } = useDragAndDrop({ /*- begin highlight -*/ acceptedDragTypes: [DIRECTORY_DRAG_TYPE], async onRootDrop(e) { // Read entries in directory and update state with relevant info. + let dir = e.items.find(isDirectoryDropItem)!; let files = []; - for await (let entry of (e.items[0] as DirectoryDropItem).getEntries()) { + for await (let entry of dir.getEntries()) { files.push({ name: entry.name, kind: entry.kind @@ -1399,8 +1423,15 @@ The `getDropOperation` function passed to `useDragAndDrop` can be used to provid In the below example, the drop target only supports dropping PNG images. If a PNG is dragged over the target, it will be highlighted and the operating system displays a copy cursor. If another type is dragged over the target, then there is no visual feedback, indicating that a drop is not accepted there. If the user holds a modifier key such as Control while dragging over the drop target in order to change the drop operation, then the drop target does not accept the drop. ```tsx example +///- begin collapse -/// +interface ImageItem { + id: number, + url: string, + name: string +} +///- end collapse -/// function Example() { - let [items, setItems] = React.useState([]); + let [items, setItems] = React.useState([]); let { dragAndDropHooks } = useDragAndDrop({ /*- begin highlight -*/ @@ -1410,7 +1441,7 @@ function Example() { async onRootDrop(e) { ///- begin collapse -/// let items = await Promise.all( - e.items.map(async (item: FileDropItem) => ({ + e.items.filter(isFileDropItem).map(async item => ({ id: Math.random(), url: URL.createObjectURL(await item.getFile()), name: item.name @@ -1468,7 +1499,7 @@ let onItemDrop = async (e) => { This example puts together many of the concepts described above, allowing users to drag items between lists bidirectionally. It also supports reordering items within the same list. When a list is empty, it accepts drops on the whole collection. `getDropOperation` ensures that items are always moved rather than copied, which avoids duplicate items. ```tsx example -import type {TextDropItem} from 'react-aria-components'; +import {isTextDropItem} from 'react-aria-components'; interface FileItem { id: string, @@ -1517,7 +1548,9 @@ function DndGridList(props: DndGridListProps) { // Handle drops between items from other lists. async onInsert(e) { let processedItems = await Promise.all( - e.items.map(async (item: TextDropItem) => JSON.parse(await item.getText('custom-app-type'))) + e.items + .filter(isTextDropItem) + .map(async item => JSON.parse(await item.getText('custom-app-type'))) ); if (e.target.dropPosition === 'before') { list.insertBefore(e.target.key, ...processedItems); @@ -1529,7 +1562,9 @@ function DndGridList(props: DndGridListProps) { // Handle drops on the collection when empty. async onRootDrop(e) { let processedItems = await Promise.all( - e.items.map(async (item: TextDropItem) => JSON.parse(await item.getText('custom-app-type'))) + e.items + .filter(isTextDropItem) + .map(async item => JSON.parse(await item.getText('custom-app-type'))) ); list.append(...processedItems); }, diff --git a/packages/react-aria-components/docs/ListBox.mdx b/packages/react-aria-components/docs/ListBox.mdx index 04ba5ba3bd1..bb1ee14e239 100644 --- a/packages/react-aria-components/docs/ListBox.mdx +++ b/packages/react-aria-components/docs/ListBox.mdx @@ -812,7 +812,7 @@ function DraggableListBox() { /*- begin highlight -*/ getItems(keys) { return [...keys].map(key => { - let item = items.get(key as string); + let item = items.get(key as string)!; return { 'text/plain': item.name, 'text/html': `<${item.style}>${item.name}`, @@ -842,8 +842,13 @@ function DraggableListBox() { Dropping on the ListBox as a whole can be enabled using the `onRootDrop` event. When a valid drag hovers over the ListBox, it receives the `isDropTarget` state and can be styled using the `[data-drop-target]` CSS selector. ```tsx example +interface Item { + id: number, + name: string +} + function Example() { - let [items, setItems] = React.useState([]); + let [items, setItems] = React.useState([]); let { dragAndDropHooks } = useDragAndDrop({ /*- begin highlight -*/ @@ -926,8 +931,6 @@ function Example() { Dropping between items can be enabled using the `onInsert` event. ListBox renders a between items to indicate the insertion position, which can be styled using the `.react-aria-DropIndicator` selector. When it is active, it receives the `[data-drop-target]` state. ```tsx example -import type {TextDropItem} from 'react-aria-components'; - function Example() { let list = useListData({ initialItems: [ @@ -1059,17 +1062,25 @@ A ([]); let { dragAndDropHooks } = useDragAndDrop({ /*- begin highlight -*/ acceptedDragTypes: ['custom-app-type'], async onRootDrop(e) { let items = await Promise.all( - e.items.map(async (item: TextDropItem) => JSON.parse(await item.getText('custom-app-type'))) + e.items + .filter(isTextDropItem) + .map(async item => JSON.parse(await item.getText('custom-app-type'))) ); setItems(items); } @@ -1078,7 +1089,7 @@ function DroppableListBox() { return ( "Drop items here"}> - {({name, style: Tag = 'span'}) => {name}} + {item => {React.createElement(item.style, null, item.name)}} ); } @@ -1097,17 +1108,23 @@ A ([]); let { dragAndDropHooks } = useDragAndDrop({ /*- begin highlight -*/ acceptedDragTypes: ['image/jpeg', 'image/png'], async onRootDrop(e) { let items = await Promise.all( - e.items.map(async (item: FileDropItem) => ({ + e.items.filter(isFileDropItem).map(async item => ({ id: Math.random(), url: URL.createObjectURL(await item.getFile()), name: item.name @@ -1168,23 +1185,28 @@ The `getEntries` method returns an [async iterable](https://developer.mozilla.or This example accepts directory drops over the whole collection, and renders the contents as items in the list. `DIRECTORY_DRAG_TYPE` is imported from `react-aria-components` and included in the `acceptedDragTypes` prop to limit the accepted items to only directories. ```tsx example -import type {DirectoryDropItem} from 'react-aria-components'; import File from '@spectrum-icons/workflow/FileTxt'; import Folder from '@spectrum-icons/workflow/Folder'; ///- begin highlight -/// -import {DIRECTORY_DRAG_TYPE} from 'react-aria-components'; +import {DIRECTORY_DRAG_TYPE, isDirectoryDropItem} from 'react-aria-components'; ///- end highlight -/// +interface DirItem { + name: string, + kind: string +} + function Example() { - let [files, setFiles] = React.useState([]); + let [files, setFiles] = React.useState([]); let { dragAndDropHooks } = useDragAndDrop({ /*- begin highlight -*/ acceptedDragTypes: [DIRECTORY_DRAG_TYPE], async onRootDrop(e) { // Read entries in directory and update state with relevant info. + let dir = e.items.find(isDirectoryDropItem)!; let files = []; - for await (let entry of (e.items[0] as DirectoryDropItem).getEntries()) { + for await (let entry of dir.getEntries()) { files.push({ name: entry.name, kind: entry.kind @@ -1367,8 +1389,15 @@ The `getDropOperation` function passed to `useDragAndDrop` can be used to provid In the below example, the drop target only supports dropping PNG images. If a PNG is dragged over the target, it will be highlighted and the operating system displays a copy cursor. If another type is dragged over the target, then there is no visual feedback, indicating that a drop is not accepted there. If the user holds a modifier key such as Control while dragging over the drop target in order to change the drop operation, then the drop target does not accept the drop. ```tsx example +///- begin collapse -/// +interface ImageItem { + id: number, + url: string, + name: string +} +///- end collapse -/// function Example() { - let [items, setItems] = React.useState([]); + let [items, setItems] = React.useState([]); let { dragAndDropHooks } = useDragAndDrop({ /*- begin highlight -*/ @@ -1378,7 +1407,7 @@ function Example() { async onRootDrop(e) { ///- begin collapse -/// let items = await Promise.all( - e.items.map(async (item: FileDropItem) => ({ + e.items.filter(isFileDropItem).map(async item => ({ id: Math.random(), url: URL.createObjectURL(await item.getFile()), name: item.name @@ -1433,10 +1462,10 @@ let onItemDrop = async (e) => { ### Drag between lists -This example puts together many of the concepts described above, allowing users to drag items between lists bidirectionally. Items are always moved to avoid duplicate items being added to the same list. +This example puts together many of the concepts described above, allowing users to drag items between lists bidirectionally. It also supports reordering items within the same list. When a list is empty, it accepts drops on the whole collection. `getDropOperation` ensures that items are always moved rather than copied, which avoids duplicate items. ```tsx example -import type {TextDropItem} from 'react-aria-components'; +import {isTextDropItem} from 'react-aria-components'; interface FileItem { id: string, @@ -1485,7 +1514,9 @@ function DndListBox(props: DndListBoxProps) { // Handle drops between items from other lists. async onInsert(e) { let processedItems = await Promise.all( - e.items.map(async (item: TextDropItem) => JSON.parse(await item.getText('custom-app-type'))) + e.items + .filter(isTextDropItem) + .map(async item => JSON.parse(await item.getText('custom-app-type'))) ); if (e.target.dropPosition === 'before') { list.insertBefore(e.target.key, ...processedItems); @@ -1497,7 +1528,9 @@ function DndListBox(props: DndListBoxProps) { // Handle drops on the collection when empty. async onRootDrop(e) { let processedItems = await Promise.all( - e.items.map(async (item: TextDropItem) => JSON.parse(await item.getText('custom-app-type'))) + e.items + .filter(isTextDropItem) + .map(async item => JSON.parse(await item.getText('custom-app-type'))) ); list.append(...processedItems); }, diff --git a/packages/react-aria-components/docs/Table.mdx b/packages/react-aria-components/docs/Table.mdx index 3a54a0e0939..46bf3d6ec3c 100644 --- a/packages/react-aria-components/docs/Table.mdx +++ b/packages/react-aria-components/docs/Table.mdx @@ -1227,7 +1227,7 @@ function DraggableTable() { /*- begin highlight -*/ getItems(keys) { return [...keys].map(key => { - let item = items.find(item => item.id === key); + let item = items.find(item => item.id === key)!; return { 'text/plain': `${item.name} – ${item.type}`, 'text/html': `${item.name}${item.type}`, @@ -1258,15 +1258,18 @@ function DraggableTable() { Dropping on the Table as a whole can be enabled using the `onRootDrop` event. When a valid drag hovers over the Table, it receives the `isDropTarget` state and can be styled using the `[data-drop-target]` CSS selector. ```tsx example -import type {TextDropItem} from 'react-aria-components'; +import {isTextDropItem} from 'react-aria-components'; function Example() { - let [items, setItems] = React.useState([]); + let [items, setItems] = React.useState([]); let { dragAndDropHooks } = useDragAndDrop({ /*- begin highlight -*/ async onRootDrop(e) { - let items = await Promise.all(e.items.map(async (item: TextDropItem) => ( + let items = await Promise.all( + e.items + .filter(isTextDropItem) + .map(async item => ( JSON.parse(await item.getText('pokemon')) ))); setItems(items); @@ -1340,7 +1343,7 @@ function Example() { Dropping between items can be enabled using the `onInsert` event. Table renders a between items to indicate the insertion position, which can be styled using the `.react-aria-DropIndicator` selector. When it is active, it receives the `[data-drop-target]` state. ```tsx example -import type {TextDropItem} from 'react-aria-components'; +import {isTextDropItem} from 'react-aria-components'; function Example() { let list = useListData({ @@ -1355,7 +1358,7 @@ function Example() { let { dragAndDropHooks } = useDragAndDrop({ ///- begin highlight -/// async onInsert(e) { - let items = await Promise.all(e.items.map(async (item: TextDropItem) => { + let items = await Promise.all(e.items.filter(isTextDropItem).map(async item => { let {name, type, level} = JSON.parse(await item.getText('pokemon')); return {id: Math.random(), name, type, level}; })); @@ -1401,7 +1404,7 @@ function Example() { // ... ///- begin collapse -/// async onInsert(e) { - let items = await Promise.all(e.items.map(async (item: TextDropItem) => { + let items = await Promise.all(e.items.filter(isTextDropItem).map(async item => { let {name, type, level} = JSON.parse(await item.getText('pokemon')); return {id: Math.random(), name, type, level}; })); @@ -1473,17 +1476,19 @@ A ([]); let { dragAndDropHooks } = useDragAndDrop({ /*- begin highlight -*/ acceptedDragTypes: ['pokemon'], async onRootDrop(e) { let items = await Promise.all( - e.items.map(async (item: TextDropItem) => JSON.parse(await item.getText('pokemon'))) + e.items + .filter(isTextDropItem) + .map(async item => JSON.parse(await item.getText('pokemon'))) ); setItems(items); } @@ -1512,17 +1517,25 @@ A ([]); let { dragAndDropHooks } = useDragAndDrop({ /*- begin highlight -*/ acceptedDragTypes: ['image/jpeg', 'image/png'], async onRootDrop(e) { let items = await Promise.all( - e.items.map(async (item: FileDropItem) => { + e.items.filter(isFileDropItem).map(async item => { let file = await item.getFile(); return { id: Math.random(), @@ -1586,23 +1599,29 @@ The `getEntries` method returns an [async iterable](https://developer.mozilla.or This example accepts directory drops over the whole collection, and renders the contents as items in the list. `DIRECTORY_DRAG_TYPE` is imported from `react-aria-components` and included in the `acceptedDragTypes` prop to limit the accepted items to only directories. ```tsx example -import type {DirectoryDropItem} from 'react-aria-components'; import File from '@spectrum-icons/workflow/FileTxt'; import Folder from '@spectrum-icons/workflow/Folder'; ///- begin highlight -/// -import {DIRECTORY_DRAG_TYPE} from 'react-aria-components'; +import {DIRECTORY_DRAG_TYPE, isDirectoryDropItem} from 'react-aria-components'; ///- end highlight -/// +interface DirItem { + name: string, + kind: string, + type: string +} + function Example() { - let [files, setFiles] = React.useState([]); + let [files, setFiles] = React.useState([]); let { dragAndDropHooks } = useDragAndDrop({ /*- begin highlight -*/ acceptedDragTypes: [DIRECTORY_DRAG_TYPE], async onRootDrop(e) { // Read entries in directory and update state with relevant info. + let dir = e.items.find(isDirectoryDropItem)!; let files = []; - for await (let entry of (e.items[0] as DirectoryDropItem).getEntries()) { + for await (let entry of dir.getEntries()) { files.push({ name: entry.name, kind: entry.kind, @@ -1769,8 +1788,17 @@ The `getDropOperation` function passed to `useDragAndDrop` can be used to provid In the below example, the drop target only supports dropping PNG images. If a PNG is dragged over the target, it will be highlighted and the operating system displays a copy cursor. If another type is dragged over the target, then there is no visual feedback, indicating that a drop is not accepted there. If the user holds a modifier key such as Control while dragging over the drop target in order to change the drop operation, then the drop target does not accept the drop. ```tsx example +///- begin collapse -/// +interface ImageItem { + id: number, + url: string, + name: string, + type: string, + lastModified: number +} +///- end collapse -/// function Example() { - let [items, setItems] = React.useState([]); + let [items, setItems] = React.useState([]); let { dragAndDropHooks } = useDragAndDrop({ /*- begin highlight -*/ @@ -1780,7 +1808,7 @@ function Example() { async onRootDrop(e) { ///- begin collapse -/// let items = await Promise.all( - e.items.map(async (item: FileDropItem) => { + e.items.filter(isFileDropItem).map(async item => { let file = await item.getFile(); return { id: Math.random(), @@ -1853,7 +1881,7 @@ let onItemDrop = async (e) => { This example puts together many of the concepts described above, allowing users to drag items between tables bidirectionally. It also supports reordering items within the same table. When a table is empty, it accepts drops on the whole collection. `getDropOperation` ensures that items are always moved rather than copied, which avoids duplicate items. ```tsx example -import type {TextDropItem} from 'react-aria-components'; +import {isTextDropItem} from 'react-aria-components'; interface FileItem { id: string, @@ -1902,7 +1930,9 @@ function DndTable(props: DndTableProps) { // Handle drops between items from other lists. async onInsert(e) { let processedItems = await Promise.all( - e.items.map(async (item: TextDropItem) => JSON.parse(await item.getText('custom-app-type'))) + e.items + .filter(isTextDropItem) + .map(async item => JSON.parse(await item.getText('custom-app-type'))) ); if (e.target.dropPosition === 'before') { list.insertBefore(e.target.key, ...processedItems); @@ -1914,7 +1944,9 @@ function DndTable(props: DndTableProps) { // Handle drops on the collection when empty. async onRootDrop(e) { let processedItems = await Promise.all( - e.items.map(async (item: TextDropItem) => JSON.parse(await item.getText('custom-app-type'))) + e.items + .filter(isTextDropItem) + .map(async item => JSON.parse(await item.getText('custom-app-type'))) ); list.append(...processedItems); }, From 814d8552f582e8a5bcd2d51b05f7a6eab16fd415 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Mon, 20 Mar 2023 19:49:23 -0700 Subject: [PATCH 8/9] lint --- packages/@react-aria/utils/src/mergeProps.ts | 4 +++- packages/react-aria-components/src/Table.tsx | 2 +- packages/react-aria-components/src/useDragAndDrop.tsx | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/@react-aria/utils/src/mergeProps.ts b/packages/@react-aria/utils/src/mergeProps.ts index e1cd5b03716..5d7444e20f5 100644 --- a/packages/@react-aria/utils/src/mergeProps.ts +++ b/packages/@react-aria/utils/src/mergeProps.ts @@ -18,6 +18,8 @@ interface Props { [key: string]: any } +type PropsArg = Props | null | undefined; + // taken from: https://stackoverflow.com/questions/51603250/typescript-3-parameter-list-intersection-type/51604379#51604379 type TupleTypes = { [P in keyof T]: T[P] } extends { [key: number]: infer V } ? NullToObject : never; type NullToObject = T extends (null | undefined) ? {} : T; @@ -31,7 +33,7 @@ type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ( * For all other props, the last prop object overrides all previous ones. * @param args - Multiple sets of props to merge together. */ -export function mergeProps(...args: T): UnionToIntersection> { +export function mergeProps(...args: T): UnionToIntersection> { // Start with a base clone of the first argument. This is a lot faster than starting // with an empty object and adding properties as we go. let result: Props = {...args[0]}; diff --git a/packages/react-aria-components/src/Table.tsx b/packages/react-aria-components/src/Table.tsx index 92cf42f3302..116c765b14e 100644 --- a/packages/react-aria-components/src/Table.tsx +++ b/packages/react-aria-components/src/Table.tsx @@ -213,7 +213,7 @@ function Table(props: TableProps, ref: ForwardedRef) { } let dragState: DraggableCollectionState | undefined = undefined; - let dropState: DroppableCollectionState | undefined = undefined + let dropState: DroppableCollectionState | undefined = undefined; let droppableCollection: DroppableCollectionResult | undefined = undefined; let isRootDropTarget = false; let dragPreview: JSX.Element | null = null; diff --git a/packages/react-aria-components/src/useDragAndDrop.tsx b/packages/react-aria-components/src/useDragAndDrop.tsx index ecd15e7146e..0066865a139 100644 --- a/packages/react-aria-components/src/useDragAndDrop.tsx +++ b/packages/react-aria-components/src/useDragAndDrop.tsx @@ -31,7 +31,6 @@ import { useDroppableCollection, useDroppableItem } from 'react-aria'; -import React, {createContext, ForwardedRef, forwardRef, Key, ReactNode, RefObject, useContext, useMemo} from 'react'; import {DraggableCollectionProps, DroppableCollectionProps} from '@react-types/shared'; import { DraggableCollectionState, @@ -41,6 +40,7 @@ import { useDraggableCollectionState, useDroppableCollectionState } from 'react-stately'; +import React, {createContext, ForwardedRef, forwardRef, Key, ReactNode, RefObject, useContext, useMemo} from 'react'; import {RenderProps} from './utils'; interface DraggableCollectionStateOpts extends Omit {} From 0707a61dffee95bf175257af7fcda1c97e310478 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Mon, 20 Mar 2023 21:00:47 -0700 Subject: [PATCH 9/9] Fix test --- packages/@react-aria/selection/src/ListKeyboardDelegate.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@react-aria/selection/src/ListKeyboardDelegate.ts b/packages/@react-aria/selection/src/ListKeyboardDelegate.ts index 3f18b048469..8f708c83e6c 100644 --- a/packages/@react-aria/selection/src/ListKeyboardDelegate.ts +++ b/packages/@react-aria/selection/src/ListKeyboardDelegate.ts @@ -97,7 +97,7 @@ export class ListKeyboardDelegate implements KeyboardDelegate { while (item && item.offsetTop > pageY) { key = this.getKeyAbove(key); - item = this.getItem(key); + item = key == null ? null : this.getItem(key); } return key; @@ -114,7 +114,7 @@ export class ListKeyboardDelegate implements KeyboardDelegate { while (item && item.offsetTop < pageY) { key = this.getKeyBelow(key); - item = this.getItem(key); + item = key == null ? null : this.getItem(key); } return key;