diff --git a/examples/rsp-next-ts-17/components/ReorderableListView.tsx b/examples/rsp-next-ts-17/components/ReorderableListView.tsx index 721f875e1c6..5c7d9b58d93 100644 --- a/examples/rsp-next-ts-17/components/ReorderableListView.tsx +++ b/examples/rsp-next-ts-17/components/ReorderableListView.tsx @@ -22,7 +22,7 @@ export default function ReorderableListView() { // Setup the drag types and associated info for each dragged item. return { "custom-app-type-reorder": JSON.stringify(item), - "text/plain": item.name, + "text/plain": item?.name ?? '', }; }); }, diff --git a/examples/rsp-next-ts/components/ReorderableListView.tsx b/examples/rsp-next-ts/components/ReorderableListView.tsx index 721f875e1c6..5c7d9b58d93 100644 --- a/examples/rsp-next-ts/components/ReorderableListView.tsx +++ b/examples/rsp-next-ts/components/ReorderableListView.tsx @@ -22,7 +22,7 @@ export default function ReorderableListView() { // Setup the drag types and associated info for each dragged item. return { "custom-app-type-reorder": JSON.stringify(item), - "text/plain": item.name, + "text/plain": item?.name ?? '', }; }); }, diff --git a/package.json b/package.json index 2ad731030af..2038982ef6c 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ }, "packageManager": "yarn@4.2.2", "scripts": { - "check-types": "tsc && tsc-strict", + "check-types": "tsc", "install-16": "node scripts/react-16-install-prep.mjs && yarn add react@^16.8.0 react-dom@^16.8.0 @testing-library/react@^12 @testing-library/react-hooks@^8 @testing-library/dom@^8 react-test-renderer@^16.9.0 && node scripts/oldReactSupport.mjs", "install-17": "node scripts/react-17-install-prep.mjs && yarn add react@^17 react-dom@^17 @testing-library/react@^12 @testing-library/react-hooks@^8 @testing-library/dom@^8 react-test-renderer@^16.9.0 && node scripts/oldReactSupport.mjs", "install-19": "node scripts/react-19-install-prep.mjs && yarn add react@next react-dom@next", @@ -208,7 +208,6 @@ "tempy": "^0.5.0", "typescript": "^5.5.0", "typescript-eslint": "^8.9.0", - "typescript-strict-plugin": "^2.0.0", "verdaccio": "^5.13.0", "walk-object": "^4.0.0", "wsrun": "^5.0.0", diff --git a/packages/@internationalized/message/src/MessageDictionary.ts b/packages/@internationalized/message/src/MessageDictionary.ts index 7986222e5fc..9296f39b57d 100644 --- a/packages/@internationalized/message/src/MessageDictionary.ts +++ b/packages/@internationalized/message/src/MessageDictionary.ts @@ -76,9 +76,7 @@ function getStringsForLocale(locale: string, strings: LocalizedStrings, defaultL } function getLanguage(locale: string) { - // @ts-ignore if (Intl.Locale) { - // @ts-ignore return new Intl.Locale(locale).language; } diff --git a/packages/@internationalized/number/src/NumberFormatter.ts b/packages/@internationalized/number/src/NumberFormatter.ts index 9807a5a1af6..570a6ecabe7 100644 --- a/packages/@internationalized/number/src/NumberFormatter.ts +++ b/packages/@internationalized/number/src/NumberFormatter.ts @@ -14,14 +14,12 @@ let formatterCache = new Map(); let supportsSignDisplay = false; try { - // @ts-ignore supportsSignDisplay = (new Intl.NumberFormat('de-DE', {signDisplay: 'exceptZero'})).resolvedOptions().signDisplay === 'exceptZero'; // eslint-disable-next-line no-empty } catch {} let supportsUnit = false; try { - // @ts-ignore supportsUnit = (new Intl.NumberFormat('de-DE', {style: 'unit', unit: 'degree'})).resolvedOptions().style === 'unit'; // eslint-disable-next-line no-empty } catch {} @@ -87,15 +85,12 @@ export class NumberFormatter implements Intl.NumberFormat { /** Formats a number to an array of parts such as separators, digits, punctuation, and more. */ formatToParts(value: number): Intl.NumberFormatPart[] { // TODO: implement signDisplay for formatToParts - // @ts-ignore return this.numberFormatter.formatToParts(value); } /** Formats a number range as a string. */ formatRange(start: number, end: number): string { - // @ts-ignore if (typeof this.numberFormatter.formatRange === 'function') { - // @ts-ignore return this.numberFormatter.formatRange(start, end); } @@ -109,9 +104,7 @@ export class NumberFormatter implements Intl.NumberFormat { /** Formats a number range as an array of parts. */ formatRangeToParts(start: number, end: number): NumberRangeFormatPart[] { - // @ts-ignore if (typeof this.numberFormatter.formatRangeToParts === 'function') { - // @ts-ignore return this.numberFormatter.formatRangeToParts(start, end); } diff --git a/packages/@internationalized/number/src/NumberParser.ts b/packages/@internationalized/number/src/NumberParser.ts index fbf9f934076..6425a63fc4e 100644 --- a/packages/@internationalized/number/src/NumberParser.ts +++ b/packages/@internationalized/number/src/NumberParser.ts @@ -273,7 +273,6 @@ function getSymbols(locale: string, formatter: Intl.NumberFormat, intlOptions: I // Safari does not support the signDisplay option, but our number parser polyfills it. // If no plus sign was returned, but the original options contained signDisplay, default to the '+' character. - // @ts-ignore if (!plusSign && (originalOptions?.signDisplay === 'exceptZero' || originalOptions?.signDisplay === 'always')) { plusSign = '+'; } @@ -305,9 +304,7 @@ function getSymbols(locale: string, formatter: Intl.NumberFormat, intlOptions: I } function replaceAll(str: string, find: string, replace: string) { - // @ts-ignore if (str.replaceAll) { - // @ts-ignore return str.replaceAll(find, replace); } diff --git a/packages/@react-aria/autocomplete/test/useSearchAutocomplete.test.js b/packages/@react-aria/autocomplete/test/useSearchAutocomplete.test.js index b10632ae0c1..1ff0745bb64 100644 --- a/packages/@react-aria/autocomplete/test/useSearchAutocomplete.test.js +++ b/packages/@react-aria/autocomplete/test/useSearchAutocomplete.test.js @@ -11,7 +11,6 @@ */ import {Item} from '@react-stately/collections'; -import {ListLayout} from '@react-stately/layout'; import React from 'react'; import {renderHook} from '@react-spectrum/test-utils-internal'; import {useComboBoxState} from '@react-stately/combobox'; @@ -30,11 +29,6 @@ describe('useSearchAutocomplete', function () { }); let defaultProps = {items: [{id: 1, name: 'one'}], children: (props) => {props.name}}; - let {result} = renderHook(() => useComboBoxState(defaultProps)); - let mockLayout = new ListLayout({ - rowHeight: 40 - }); - mockLayout.collection = result.current.collection; afterEach(() => { jest.clearAllMocks(); @@ -45,8 +39,7 @@ describe('useSearchAutocomplete', function () { label: 'test label', popoverRef: React.createRef(), inputRef: React.createRef(), - listBoxRef: React.createRef(), - layout: mockLayout + listBoxRef: React.createRef() }; let {result} = renderHook(() => useSearchAutocomplete(props, useComboBoxState(defaultProps))); @@ -73,8 +66,7 @@ describe('useSearchAutocomplete', function () { inputRef: { current: document.createElement('input') }, - listBoxRef: React.createRef(), - layout: mockLayout + listBoxRef: React.createRef() }; let {result: state} = renderHook((props) => useComboBoxState(props), {initialProps: props}); diff --git a/packages/@react-aria/combobox/src/useComboBox.ts b/packages/@react-aria/combobox/src/useComboBox.ts index 8505a8c1790..080ca09238e 100644 --- a/packages/@react-aria/combobox/src/useComboBox.ts +++ b/packages/@react-aria/combobox/src/useComboBox.ts @@ -283,7 +283,7 @@ export function useComboBox(props: AriaComboBoxOptions, state: ComboBoxSta let lastSection = useRef(sectionKey); let lastItem = useRef(itemKey); useEffect(() => { - if (isAppleDevice() && focusedItem != null && itemKey !== lastItem.current) { + if (isAppleDevice() && focusedItem != null && itemKey != null && itemKey !== lastItem.current) { let isSelected = state.selectionManager.isSelected(itemKey); let section = sectionKey != null ? state.collection.getItem(sectionKey) : null; let sectionTitle = section?.['aria-label'] || (typeof section?.rendered === 'string' ? section.rendered : '') || ''; diff --git a/packages/@react-aria/combobox/test/useComboBox.test.js b/packages/@react-aria/combobox/test/useComboBox.test.js index c40dc72b2c8..9b83283f355 100644 --- a/packages/@react-aria/combobox/test/useComboBox.test.js +++ b/packages/@react-aria/combobox/test/useComboBox.test.js @@ -12,7 +12,6 @@ import {actHook as act, renderHook} from '@react-spectrum/test-utils-internal'; import {Item} from '@react-stately/collections'; -import {ListLayout} from '@react-stately/layout'; import React from 'react'; import {useComboBox} from '../'; import {useComboBoxState} from '@react-stately/combobox'; @@ -32,11 +31,6 @@ describe('useComboBox', function () { }); let defaultProps = {items: [{id: 1, name: 'one'}], children: (props) => {props.name}}; - let {result} = renderHook(() => useComboBoxState(defaultProps)); - let mockLayout = new ListLayout({ - rowHeight: 40 - }); - mockLayout.collection = result.current.collection; let props = { label: 'test label', @@ -51,8 +45,7 @@ describe('useComboBox', function () { }, listBoxRef: { current: document.createElement('div') - }, - layout: mockLayout + } }; afterEach(() => { diff --git a/packages/@react-aria/datepicker/src/useDateRangePicker.ts b/packages/@react-aria/datepicker/src/useDateRangePicker.ts index a3988ee19d0..8970160b3ad 100644 --- a/packages/@react-aria/datepicker/src/useDateRangePicker.ts +++ b/packages/@react-aria/datepicker/src/useDateRangePicker.ts @@ -14,6 +14,7 @@ import {AriaButtonProps} from '@react-types/button'; import {AriaDatePickerProps, AriaDateRangePickerProps, DateValue} from '@react-types/datepicker'; import {AriaDialogProps} from '@react-types/dialog'; import {createFocusManager} from '@react-aria/focus'; +import {DateRange, RangeCalendarProps} from '@react-types/calendar'; import {DateRangePickerState} from '@react-stately/datepicker'; import {DEFAULT_VALIDATION_RESULT, mergeValidation, privateValidationStateProp} from '@react-stately/form'; import {DOMAttributes, GroupDOMAttributes, KeyboardEvent, RefObject, ValidationResult} from '@react-types/shared'; @@ -21,7 +22,6 @@ import {filterDOMProps, mergeProps, useDescription, useId} from '@react-aria/uti import {focusManagerSymbol, roleSymbol} from './useDateField'; // @ts-ignore import intlMessages from '../intl/*.json'; -import {RangeCalendarProps} from '@react-types/calendar'; import {useDatePickerGroup} from './useDatePickerGroup'; import {useField} from '@react-aria/label'; import {useFocusWithin} from '@react-aria/interactions'; @@ -168,7 +168,7 @@ export function useDateRangePicker(props: AriaDateRangePick startFieldProps: { ...startFieldProps, ...commonFieldProps, - value: state.value?.start, + value: state.value?.start ?? null, onChange: start => state.setDateTime('start', start), autoFocus: props.autoFocus, name: props.startName, @@ -186,7 +186,7 @@ export function useDateRangePicker(props: AriaDateRangePick endFieldProps: { ...endFieldProps, ...commonFieldProps, - value: state.value?.end, + value: state.value?.end ?? null, onChange: end => state.setDateTime('end', end), name: props.endName, [privateValidationStateProp]: { @@ -204,7 +204,7 @@ export function useDateRangePicker(props: AriaDateRangePick errorMessageProps, calendarProps: { autoFocus: true, - value: state.dateRange, + value: state.dateRange?.start && state.dateRange.end ? state.dateRange as DateRange : null, onChange: state.setDateRange, minValue: props.minValue, maxValue: props.maxValue, diff --git a/packages/@react-aria/dnd/src/DragPreview.tsx b/packages/@react-aria/dnd/src/DragPreview.tsx index 3576f3ccccf..1228b85d313 100644 --- a/packages/@react-aria/dnd/src/DragPreview.tsx +++ b/packages/@react-aria/dnd/src/DragPreview.tsx @@ -15,7 +15,7 @@ import {flushSync} from 'react-dom'; import React, {ForwardedRef, JSX, useEffect, useImperativeHandle, useRef, useState} from 'react'; export interface DragPreviewProps { - children: (items: DragItem[]) => JSX.Element + children: (items: DragItem[]) => JSX.Element | null } function DragPreview(props: DragPreviewProps, ref: ForwardedRef) { diff --git a/packages/@react-aria/dnd/src/useDroppableCollection.ts b/packages/@react-aria/dnd/src/useDroppableCollection.ts index b892794eecf..420d924c7c5 100644 --- a/packages/@react-aria/dnd/src/useDroppableCollection.ts +++ b/packages/@react-aria/dnd/src/useDroppableCollection.ts @@ -56,7 +56,7 @@ export interface DroppableCollectionResult { interface DroppingState { collection: Collection>, - focusedKey: Key, + focusedKey: Key | null, selectedKeys: Set, target: DropTarget, draggingKeys: Set, @@ -273,6 +273,7 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state: } } } else if ( + prevFocusedKey != null && state.selectionManager.focusedKey === prevFocusedKey && isInternal && target.type === 'item' && @@ -293,7 +294,7 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state: // Also show the focus ring if the focused key is not selected, e.g. in case of a reorder. state.selectionManager.setFocusedKey(target.key); setInteractionModality('keyboard'); - } else if (!state.selectionManager.isSelected(state.selectionManager.focusedKey)) { + } else if (state.selectionManager.focusedKey != null && !state.selectionManager.isSelected(state.selectionManager.focusedKey)) { setInteractionModality('keyboard'); } @@ -533,7 +534,7 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state: // If the focused key is a cell, get the parent item instead. // For now, we assume that individual cells cannot be dropped on. - let item = localState.state.collection.getItem(key); + let item = key != null ? localState.state.collection.getItem(key) : null; if (item?.type === 'cell') { key = item.parentKey; } diff --git a/packages/@react-aria/dnd/stories/DraggableCollection.tsx b/packages/@react-aria/dnd/stories/DraggableCollection.tsx index 82c142196d7..55d77d0bc4f 100644 --- a/packages/@react-aria/dnd/stories/DraggableCollection.tsx +++ b/packages/@react-aria/dnd/stories/DraggableCollection.tsx @@ -61,7 +61,7 @@ export function DraggableCollectionExample(props) { function DraggableCollection(props) { let ref = React.useRef(null); - let state = useListState(props); + let state = useListState(props); let gridState = useGridState({ selectionMode: 'multiple', collection: React.useMemo(() => new GridCollection({ diff --git a/packages/@react-aria/dnd/stories/dnd.stories.tsx b/packages/@react-aria/dnd/stories/dnd.stories.tsx index 28e4be2aa6d..bb0b5efa785 100644 --- a/packages/@react-aria/dnd/stories/dnd.stories.tsx +++ b/packages/@react-aria/dnd/stories/dnd.stories.tsx @@ -387,7 +387,7 @@ function DraggableCollectionExample(props) { function DraggableCollection(props) { let {isDisabled} = props; let ref = React.useRef(null); - let state = useListState(props); + let state = useListState(props); let gridState = useGridState({ ...props, selectionMode: 'multiple', diff --git a/packages/@react-aria/grid/src/GridKeyboardDelegate.ts b/packages/@react-aria/grid/src/GridKeyboardDelegate.ts index 00e01924a0c..67da4a63b76 100644 --- a/packages/@react-aria/grid/src/GridKeyboardDelegate.ts +++ b/packages/@react-aria/grid/src/GridKeyboardDelegate.ts @@ -33,7 +33,7 @@ export class GridKeyboardDelegate> implements Key protected disabledKeys: Set; protected disabledBehavior: DisabledBehavior; protected direction: Direction; - protected collator: Intl.Collator; + protected collator: Intl.Collator | undefined; protected layoutDelegate: LayoutDelegate; protected focusMode; @@ -43,7 +43,10 @@ export class GridKeyboardDelegate> implements Key this.disabledBehavior = options.disabledBehavior || 'all'; this.direction = options.direction; this.collator = options.collator; - this.layoutDelegate = options.layoutDelegate || (options.layout ? new DeprecatedLayoutDelegate(options.layout) : new DOMLayoutDelegate(options.ref)); + if (!options.layout && !options.ref) { + throw new Error('Either a layout or a ref must be specified.'); + } + this.layoutDelegate = options.layoutDelegate || (options.layout ? new DeprecatedLayoutDelegate(options.layout) : new DOMLayoutDelegate(options.ref!)); this.focusMode = options.focusMode || 'row'; } @@ -66,12 +69,16 @@ export class GridKeyboardDelegate> implements Key while (key != null) { let item = this.collection.getItem(key); + if (!item) { + return null; + } if (!this.isDisabled(item) && (!pred || pred(item))) { return key; } key = this.collection.getKeyBefore(key); } + return null; } protected findNextKey(fromKey?: Key, pred?: (item: Node) => boolean) { @@ -81,23 +88,34 @@ export class GridKeyboardDelegate> implements Key while (key != null) { let item = this.collection.getItem(key); + if (!item) { + return null; + } if (!this.isDisabled(item) && (!pred || pred(item))) { return key; } key = this.collection.getKeyAfter(key); + if (key == null) { + return null; + } } + return null; } - getKeyBelow(key: Key) { + getKeyBelow(fromKey: Key) { + let key: Key | null = fromKey; let startItem = this.collection.getItem(key); if (!startItem) { - return; + return null; } // If focus was on a cell, start searching from the parent row if (this.isCell(startItem)) { - key = startItem.parentKey; + key = startItem.parentKey ?? null; + } + if (key == null) { + return null; } // Find the next item @@ -106,7 +124,10 @@ export class GridKeyboardDelegate> implements Key // If focus was on a cell, focus the cell with the same index in the next row. if (this.isCell(startItem)) { let item = this.collection.getItem(key); - return getNthItem(getChildNodes(item, this.collection), startItem.index).key; + if (!item) { + return null; + } + return getNthItem(getChildNodes(item, this.collection), startItem.index ?? 0)?.key ?? null; } // Otherwise, focus the next row @@ -114,17 +135,22 @@ export class GridKeyboardDelegate> implements Key return key; } } + return null; } - getKeyAbove(key: Key) { + getKeyAbove(fromKey: Key) { + let key: Key | null = fromKey; let startItem = this.collection.getItem(key); if (!startItem) { - return; + return null; } // If focus is on a cell, start searching from the parent row if (this.isCell(startItem)) { - key = startItem.parentKey; + key = startItem.parentKey ?? null; + } + if (key == null) { + return null; } // Find the previous item @@ -133,7 +159,10 @@ export class GridKeyboardDelegate> implements Key // If focus was on a cell, focus the cell with the same index in the previous row. if (this.isCell(startItem)) { let item = this.collection.getItem(key); - return getNthItem(getChildNodes(item, this.collection), startItem.index).key; + if (!item) { + return null; + } + return getNthItem(getChildNodes(item, this.collection), startItem.index ?? 0)?.key || null; } // Otherwise, focus the previous row @@ -141,141 +170,165 @@ export class GridKeyboardDelegate> implements Key return key; } } + return null; } getKeyRightOf(key: Key) { let item = this.collection.getItem(key); if (!item) { - return; + return null; } // If focus is on a row, focus the first child cell. if (this.isRow(item)) { let children = getChildNodes(item, this.collection); - return this.direction === 'rtl' - ? getLastItem(children).key - : getFirstItem(children).key; + return (this.direction === 'rtl' + ? getLastItem(children)?.key + : getFirstItem(children)?.key) ?? null; } // If focus is on a cell, focus the next cell if any, // otherwise focus the parent row. - if (this.isCell(item)) { + if (this.isCell(item) && item.parentKey != null) { let parent = this.collection.getItem(item.parentKey); + if (!parent) { + return null; + } let children = getChildNodes(parent, this.collection); - let next = this.direction === 'rtl' + let next = (this.direction === 'rtl' ? getNthItem(children, item.index - 1) - : getNthItem(children, item.index + 1); + : getNthItem(children, item.index + 1)) ?? null; if (next) { - return next.key; + return next.key ?? null; } // focus row only if focusMode is set to row if (this.focusMode === 'row') { - return item.parentKey; + return item.parentKey ?? null; } - return this.direction === 'rtl' ? this.getFirstKey(key) : this.getLastKey(key); + return (this.direction === 'rtl' ? this.getFirstKey(key) : this.getLastKey(key)) ?? null; } + return null; } getKeyLeftOf(key: Key) { let item = this.collection.getItem(key); if (!item) { - return; + return null; } // If focus is on a row, focus the last child cell. if (this.isRow(item)) { let children = getChildNodes(item, this.collection); - return this.direction === 'rtl' - ? getFirstItem(children).key - : getLastItem(children).key; + return (this.direction === 'rtl' + ? getFirstItem(children)?.key + : getLastItem(children)?.key) ?? null; } // If focus is on a cell, focus the previous cell if any, // otherwise focus the parent row. - if (this.isCell(item)) { + if (this.isCell(item) && item.parentKey != null) { let parent = this.collection.getItem(item.parentKey); + if (!parent) { + return null; + } let children = getChildNodes(parent, this.collection); - let prev = this.direction === 'rtl' + let prev = (this.direction === 'rtl' ? getNthItem(children, item.index + 1) - : getNthItem(children, item.index - 1); + : getNthItem(children, item.index - 1)) ?? null; if (prev) { - return prev.key; + return prev.key ?? null; } // focus row only if focusMode is set to row if (this.focusMode === 'row') { - return item.parentKey; + return item.parentKey ?? null; } - return this.direction === 'rtl' ? this.getLastKey(key) : this.getFirstKey(key); + return (this.direction === 'rtl' ? this.getLastKey(key) : this.getFirstKey(key)) ?? null; } + return null; } - getFirstKey(key?: Key, global?: boolean) { - let item: Node; + getFirstKey(fromKey?: Key, global?: boolean) { + let key: Key | null = fromKey ?? null; + let item: Node | undefined | null; if (key != null) { item = this.collection.getItem(key); if (!item) { - return; + return null; } // If global flag is not set, and a cell is currently focused, // move focus to the first cell in the parent row. - if (this.isCell(item) && !global) { + if (this.isCell(item) && !global && item.parentKey != null) { let parent = this.collection.getItem(item.parentKey); - return getFirstItem(getChildNodes(parent, this.collection)).key; + if (!parent) { + return null; + } + return getFirstItem(getChildNodes(parent, this.collection))?.key ?? null; } } // Find the first row - key = this.findNextKey(null, item => item.type === 'item'); + key = this.findNextKey(undefined, item => item.type === 'item'); // If global flag is set (or if focus mode is cell), focus the first cell in the first row. - if ((key != null && item && this.isCell(item) && global) || this.focusMode === 'cell') { + if (key != null && ((item && this.isCell(item) && global) || this.focusMode === 'cell')) { let item = this.collection.getItem(key); - key = getFirstItem(getChildNodes(item, this.collection)).key; + if (!item) { + return null; + } + key = getFirstItem(getChildNodes(item, this.collection))?.key ?? null; } // Otherwise, focus the row itself. return key; } - getLastKey(key?: Key, global?: boolean) { - let item: Node; + getLastKey(fromKey?: Key, global?: boolean) { + let key: Key | null = fromKey ?? null; + let item: Node | undefined | null; if (key != null) { item = this.collection.getItem(key); if (!item) { - return; + return null; } // If global flag is not set, and a cell is currently focused, // move focus to the last cell in the parent row. - if (this.isCell(item) && !global) { + if (this.isCell(item) && !global && item.parentKey != null) { let parent = this.collection.getItem(item.parentKey); + if (!parent) { + return null; + } let children = getChildNodes(parent, this.collection); - return getLastItem(children).key; + return getLastItem(children)?.key ?? null; } } // Find the last row - key = this.findPreviousKey(null, item => item.type === 'item'); + key = this.findPreviousKey(undefined, item => item.type === 'item'); // If global flag is set (or if focus mode is cell), focus the last cell in the last row. - if ((key != null && item && this.isCell(item) && global) || this.focusMode === 'cell') { + if (key != null && ((item && this.isCell(item) && global) || this.focusMode === 'cell')) { let item = this.collection.getItem(key); + if (!item) { + return null; + } let children = getChildNodes(item, this.collection); - key = getLastItem(children).key; + key = getLastItem(children)?.key ?? null; } // Otherwise, focus the row itself. return key; } - getKeyPageAbove(key: Key) { + getKeyPageAbove(fromKey: Key) { + let key: Key | null = fromKey; let itemRect = this.layoutDelegate.getItemRect(key); if (!itemRect) { return null; @@ -283,15 +336,19 @@ export class GridKeyboardDelegate> implements Key let pageY = Math.max(0, itemRect.y + itemRect.height - this.layoutDelegate.getVisibleRect().height); - while (itemRect && itemRect.y > pageY) { - key = this.getKeyAbove(key); + while (itemRect && itemRect.y > pageY && key != null) { + key = this.getKeyAbove(key) ?? null; + if (key == null) { + break; + } itemRect = this.layoutDelegate.getItemRect(key); } return key; } - getKeyPageBelow(key: Key) { + getKeyPageBelow(fromKey: Key) { + let key: Key | null = fromKey; let itemRect = this.layoutDelegate.getItemRect(key); if (!itemRect) { @@ -316,29 +373,39 @@ export class GridKeyboardDelegate> implements Key } getKeyForSearch(search: string, fromKey?: Key) { + let key: Key | null = fromKey ?? null; if (!this.collator) { return null; } let collection = this.collection; - let key = fromKey ?? this.getFirstKey(); + key = fromKey ?? this.getFirstKey(); + if (key == null) { + return null; + } // If the starting key is a cell, search from its parent row. let startItem = collection.getItem(key); + if (!startItem) { + return null; + } if (startItem.type === 'cell') { - key = startItem.parentKey; + key = startItem.parentKey ?? null; } let hasWrapped = false; while (key != null) { let item = collection.getItem(key); + if (!item) { + return null; + } // check row text value for match if (item.textValue) { let substring = item.textValue.slice(0, search.length); if (this.collator.compare(substring, search) === 0) { if (this.isRow(item) && this.focusMode === 'cell') { - return getFirstItem(getChildNodes(item, this.collection)).key; + return getFirstItem(getChildNodes(item, this.collection))?.key ?? null; } return item.key; diff --git a/packages/@react-aria/grid/src/useGrid.ts b/packages/@react-aria/grid/src/useGrid.ts index cc82eedce40..8cb479fb0a3 100644 --- a/packages/@react-aria/grid/src/useGrid.ts +++ b/packages/@react-aria/grid/src/useGrid.ts @@ -149,7 +149,7 @@ export function useGrid(props: GridProps, state: GridState>(props: GridCellProps } = props; let {direction} = useLocale(); - let {keyboardDelegate, actions: {onCellAction}} = gridMap.get(state); + let {keyboardDelegate, actions: {onCellAction}} = gridMap.get(state)!; // We need to track the key of the item at the time it was last focused so that we force // focus to go to the item when the DOM node is reused for a different item in a virtualizer. - let keyWhenFocused = useRef(null); + let keyWhenFocused = useRef(null); // Handles focusing the cell. If there is a focusable child, // it is focused, otherwise the cell itself is focused. let focus = () => { - let treeWalker = getFocusableTreeWalker(ref.current); - if (focusMode === 'child') { - // If focus is already on a focusable child within the cell, early return so we don't shift focus - if (ref.current.contains(document.activeElement) && ref.current !== document.activeElement) { - return; - } + if (ref.current) { + let treeWalker = getFocusableTreeWalker(ref.current); + if (focusMode === 'child') { + // If focus is already on a focusable child within the cell, early return so we don't shift focus + if (ref.current.contains(document.activeElement) && ref.current !== document.activeElement) { + return; + } - let focusable = state.selectionManager.childFocusStrategy === 'last' - ? last(treeWalker) - : treeWalker.firstChild() as FocusableElement; - if (focusable) { - focusSafely(focusable); - return; + let focusable = state.selectionManager.childFocusStrategy === 'last' + ? last(treeWalker) + : treeWalker.firstChild() as FocusableElement; + if (focusable) { + focusSafely(focusable); + return; + } } - } - if ( - (keyWhenFocused.current != null && node.key !== keyWhenFocused.current) || - !ref.current.contains(document.activeElement) - ) { - focusSafely(ref.current); + if ( + (keyWhenFocused.current != null && node.key !== keyWhenFocused.current) || + !ref.current.contains(document.activeElement) + ) { + focusSafely(ref.current); + } } }; @@ -105,7 +107,7 @@ export function useGridCell>(props: GridCellProps }); let onKeyDownCapture = (e: ReactKeyboardEvent) => { - if (!e.currentTarget.contains(e.target as Element) || state.isKeyboardNavigationDisabled) { + if (!e.currentTarget.contains(e.target as Element) || state.isKeyboardNavigationDisabled || !ref.current || !document.activeElement) { return; } @@ -115,7 +117,7 @@ export function useGridCell>(props: GridCellProps switch (e.key) { case 'ArrowLeft': { // Find the next focusable element within the cell. - let focusable = direction === 'rtl' + let focusable: FocusableElement | null = direction === 'rtl' ? walker.nextNode() as FocusableElement : walker.previousNode() as FocusableElement; @@ -135,12 +137,12 @@ export function useGridCell>(props: GridCellProps // of this one, only one column, and the grid doesn't focus rows, then the next key will be the // same as this one. In that case we need to handle focusing either the cell or the first/last // child, depending on the focus mode. - let prev = keyboardDelegate.getKeyLeftOf(node.key); + let prev = keyboardDelegate.getKeyLeftOf?.(node.key); if (prev !== node.key) { // We prevent the capturing event from reaching children of the cell, e.g. pickers. // We want arrow keys to navigate to the next cell instead. We need to re-dispatch // the event from a higher parent so it still bubbles and gets handled by useSelectableCollection. - ref.current.parentElement.dispatchEvent( + ref.current.parentElement?.dispatchEvent( new KeyboardEvent(e.nativeEvent.type, e.nativeEvent) ); break; @@ -163,7 +165,7 @@ export function useGridCell>(props: GridCellProps break; } case 'ArrowRight': { - let focusable = direction === 'rtl' + let focusable: FocusableElement | null = direction === 'rtl' ? walker.previousNode() as FocusableElement : walker.nextNode() as FocusableElement; @@ -177,12 +179,12 @@ export function useGridCell>(props: GridCellProps focusSafely(focusable); scrollIntoViewport(focusable, {containingElement: getScrollParent(ref.current)}); } else { - let next = keyboardDelegate.getKeyRightOf(node.key); + let next = keyboardDelegate.getKeyRightOf?.(node.key); if (next !== node.key) { // We prevent the capturing event from reaching children of the cell, e.g. pickers. // We want arrow keys to navigate to the next cell instead. We need to re-dispatch // the event from a higher parent so it still bubbles and gets handled by useSelectableCollection. - ref.current.parentElement.dispatchEvent( + ref.current.parentElement?.dispatchEvent( new KeyboardEvent(e.nativeEvent.type, e.nativeEvent) ); break; @@ -212,7 +214,7 @@ export function useGridCell>(props: GridCellProps if (!e.altKey && ref.current.contains(e.target as Element)) { e.stopPropagation(); e.preventDefault(); - ref.current.parentElement.dispatchEvent( + ref.current.parentElement?.dispatchEvent( new KeyboardEvent(e.nativeEvent.type, e.nativeEvent) ); } @@ -266,7 +268,9 @@ export function useGridCell>(props: GridCellProps let tabindex = el.getAttribute('tabindex'); el.removeAttribute('tabindex'); requestAnimationFrame(() => { - el.setAttribute('tabindex', tabindex); + if (tabindex != null) { + el.setAttribute('tabindex', tabindex); + } }); }; } @@ -278,10 +282,10 @@ export function useGridCell>(props: GridCellProps } function last(walker: TreeWalker) { - let next: FocusableElement; - let last: FocusableElement; + let next: FocusableElement | null = null; + let last: FocusableElement | null = null; do { - last = walker.lastChild() as FocusableElement; + last = walker.lastChild() as FocusableElement | null; if (last) { next = last; } diff --git a/packages/@react-aria/grid/src/useGridRow.ts b/packages/@react-aria/grid/src/useGridRow.ts index eaa150c4071..30793497c38 100644 --- a/packages/@react-aria/grid/src/useGridRow.ts +++ b/packages/@react-aria/grid/src/useGridRow.ts @@ -52,8 +52,8 @@ export function useGridRow, S extends GridState actions.onRowAction(node.key) : onAction; + let {actions} = gridMap.get(state)!; + let onRowAction = actions.onRowAction ? () => actions.onRowAction?.(node.key) : onAction; let {itemProps, ...states} = useSelectableItem({ selectionManager: state.selectionManager, key: node.key, diff --git a/packages/@react-aria/grid/src/useGridSelectionAnnouncement.ts b/packages/@react-aria/grid/src/useGridSelectionAnnouncement.ts index fbb1491f6ff..100b93cefc5 100644 --- a/packages/@react-aria/grid/src/useGridSelectionAnnouncement.ts +++ b/packages/@react-aria/grid/src/useGridSelectionAnnouncement.ts @@ -58,7 +58,7 @@ export function useGridSelectionAnnouncement(props: GridSelectionAnnouncement // If adding or removing a single row from the selection, announce the name of that item. let isReplace = state.selectionManager.selectionBehavior === 'replace'; - let messages = []; + let messages: string[] = []; if ((state.selectionManager.selectedKeys.size === 1 && isReplace)) { if (state.collection.getItem(state.selectionManager.selectedKeys.keys().next().value)) { diff --git a/packages/@react-aria/grid/src/useHighlightSelectionDescription.ts b/packages/@react-aria/grid/src/useHighlightSelectionDescription.ts index 08d79fbc748..1d13c402ea9 100644 --- a/packages/@react-aria/grid/src/useHighlightSelectionDescription.ts +++ b/packages/@react-aria/grid/src/useHighlightSelectionDescription.ts @@ -39,7 +39,7 @@ export function useHighlightSelectionDescription(props: HighlightSelectionDescri let selectionMode = props.selectionManager.selectionMode; let selectionBehavior = props.selectionManager.selectionBehavior; - let message = undefined; + let message: string | undefined; if (shouldLongPress) { message = stringFormatter.format('longPressToSelect'); } diff --git a/packages/@react-aria/grid/src/utils.ts b/packages/@react-aria/grid/src/utils.ts index 169d3c168c2..65ffeee0939 100644 --- a/packages/@react-aria/grid/src/utils.ts +++ b/packages/@react-aria/grid/src/utils.ts @@ -17,8 +17,8 @@ import type {Key, KeyboardDelegate} from '@react-types/shared'; interface GridMapShared { keyboardDelegate: KeyboardDelegate, actions: { - onRowAction: (key: Key) => void, - onCellAction: (key: Key) => void + onRowAction?: (key: Key) => void, + onCellAction?: (key: Key) => void } } diff --git a/packages/@react-aria/grid/stories/example.tsx b/packages/@react-aria/grid/stories/example.tsx index e1e84ed3e9a..46c2aa44e47 100644 --- a/packages/@react-aria/grid/stories/example.tsx +++ b/packages/@react-aria/grid/stories/example.tsx @@ -1,3 +1,15 @@ +/* + * Copyright 2021 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + import {GridCollection, useGridState} from '@react-stately/grid'; import {mergeProps} from '@react-aria/utils'; import React from 'react'; @@ -24,7 +36,7 @@ export function Grid(props) { }), [state.collection]) }); - let ref = React.useRef(undefined); + let ref = React.useRef(null); let {gridProps} = useGrid({ 'aria-label': 'Grid', focusMode: gridFocusMode @@ -44,8 +56,8 @@ export function Grid(props) { } function Row({state, item, focusMode}) { - let rowRef = React.useRef(undefined); - let cellRef = React.useRef(undefined); + let rowRef = React.useRef(null); + let cellRef = React.useRef(null); let cellNode = [...item.childNodes][0]; let {rowProps} = useGridRow({node: item}, state, rowRef); let {gridCellProps} = useGridCell({ @@ -64,8 +76,8 @@ function Row({state, item, focusMode}) { }); return ( -
-
+
+
{cellNode.rendered}
diff --git a/packages/@react-aria/gridlist/src/useGridListItem.ts b/packages/@react-aria/gridlist/src/useGridListItem.ts index 898cebb3f60..a6e5a910838 100644 --- a/packages/@react-aria/gridlist/src/useGridListItem.ts +++ b/packages/@react-aria/gridlist/src/useGridListItem.ts @@ -11,7 +11,7 @@ */ import {chain, getScrollParent, mergeProps, scrollIntoViewport, useSlotId, useSyntheticLinkProps} from '@react-aria/utils'; -import {DOMAttributes, FocusableElement, RefObject, Node as RSNode} from '@react-types/shared'; +import {DOMAttributes, FocusableElement, Key, RefObject, Node as RSNode} from '@react-types/shared'; import {focusSafely, getFocusableTreeWalker} from '@react-aria/focus'; import {getLastItem} from '@react-stately/collections'; import {getRowId, listMap} from './utils'; @@ -67,18 +67,19 @@ export function useGridListItem(props: AriaGridListItemOptions, state: ListSt // let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-aria/gridlist'); let {direction} = useLocale(); - let {onAction, linkBehavior, keyboardNavigationBehavior} = listMap.get(state); + let {onAction, linkBehavior, keyboardNavigationBehavior} = listMap.get(state)!; let descriptionId = useSlotId(); // We need to track the key of the item at the time it was last focused so that we force // focus to go to the item when the DOM node is reused for a different item in a virtualizer. - let keyWhenFocused = useRef(null); + let keyWhenFocused = useRef(null); let focus = () => { // Don't shift focus to the row if the active element is a element within the row already // (e.g. clicking on a row button) if ( - (keyWhenFocused.current != null && node.key !== keyWhenFocused.current) || - !ref.current?.contains(document.activeElement) + ref.current !== null && + ((keyWhenFocused.current != null && node.key !== keyWhenFocused.current) || + !ref.current?.contains(document.activeElement)) ) { focusSafely(ref.current); } @@ -90,19 +91,29 @@ export function useGridListItem(props: AriaGridListItemOptions, state: ListSt if (node != null && 'expandedKeys' in state) { // TODO: ideally node.hasChildNodes would be a way to tell if a row has child nodes, but the row's contents make it so that value is always // true... - hasChildRows = [...state.collection.getChildren(node.key)].length > 1; + let children = state.collection.getChildren?.(node.key); + hasChildRows = [...(children ?? [])].length > 1; if (onAction == null && !hasLink && state.selectionManager.selectionMode === 'none' && hasChildRows) { onAction = () => state.toggleKey(node.key); } let isExpanded = hasChildRows ? state.expandedKeys.has(node.key) : undefined; + let setSize = 1; + if (node.level > 0 && node?.parentKey != null) { + let parent = state.collection.getItem(node.parentKey); + if (parent) { + // siblings must exist because our original node exists, same with lastItem + let siblings = state.collection.getChildren?.(parent.key)!; + setSize = getLastItem(siblings)!.index + 1; + } + } else { + setSize = ([...state.collection].filter(row => row.level === 0).at(-1)?.index ?? 0) + 1; + } treeGridRowProps = { 'aria-expanded': isExpanded, 'aria-level': node.level + 1, 'aria-posinset': node?.index + 1, - 'aria-setsize': node.level > 0 ? - (getLastItem(state.collection.getChildren(node?.parentKey))).index + 1 : - [...state.collection].filter(row => row.level === 0).at(-1).index + 1 + 'aria-setsize': setSize }; } @@ -118,7 +129,7 @@ export function useGridListItem(props: AriaGridListItemOptions, state: ListSt }); let onKeyDown = (e: ReactKeyboardEvent) => { - if (!e.currentTarget.contains(e.target as Element)) { + if (!e.currentTarget.contains(e.target as Element) || !ref.current || !document.activeElement) { return; } @@ -206,7 +217,7 @@ export function useGridListItem(props: AriaGridListItemOptions, state: ListSt if (!e.altKey && ref.current.contains(e.target as Element)) { e.stopPropagation(); e.preventDefault(); - ref.current.parentElement.dispatchEvent( + ref.current.parentElement?.dispatchEvent( new KeyboardEvent(e.nativeEvent.type, e.nativeEvent) ); } @@ -287,10 +298,10 @@ export function useGridListItem(props: AriaGridListItemOptions, state: ListSt } function last(walker: TreeWalker) { - let next: FocusableElement; - let last: FocusableElement; + let next: FocusableElement | null = null; + let last: FocusableElement | null = null; do { - last = walker.lastChild() as FocusableElement; + last = walker.lastChild() as FocusableElement | null; if (last) { next = last; } diff --git a/packages/@react-aria/gridlist/src/utils.ts b/packages/@react-aria/gridlist/src/utils.ts index d7ec213c9fd..50c1089bfa0 100644 --- a/packages/@react-aria/gridlist/src/utils.ts +++ b/packages/@react-aria/gridlist/src/utils.ts @@ -15,7 +15,7 @@ import type {ListState} from '@react-stately/list'; interface ListMapShared { id: string, - onAction: (key: Key) => void, + onAction?: (key: Key) => void, linkBehavior?: 'action' | 'selection' | 'override', keyboardNavigationBehavior: 'arrow' | 'tab' } @@ -25,7 +25,7 @@ interface ListMapShared { export const listMap = new WeakMap, ListMapShared>(); export function getRowId(state: ListState, key: Key) { - let {id} = listMap.get(state); + let {id} = listMap.get(state) ?? {}; if (!id) { throw new Error('Unknown list'); } diff --git a/packages/@react-aria/i18n/src/useDefaultLocale.ts b/packages/@react-aria/i18n/src/useDefaultLocale.ts index 30989a1072a..456f68764c7 100644 --- a/packages/@react-aria/i18n/src/useDefaultLocale.ts +++ b/packages/@react-aria/i18n/src/useDefaultLocale.ts @@ -35,7 +35,6 @@ export function getDefaultLocale(): Locale { || 'en-US'; try { - // @ts-ignore Intl.DateTimeFormat.supportedLocalesOf([locale]); } catch { locale = 'en-US'; diff --git a/packages/@react-aria/i18n/src/utils.ts b/packages/@react-aria/i18n/src/utils.ts index 1254f280daa..9531f6e1aa2 100644 --- a/packages/@react-aria/i18n/src/utils.ts +++ b/packages/@react-aria/i18n/src/utils.ts @@ -19,7 +19,6 @@ const RTL_LANGS = new Set(['ae', 'ar', 'arc', 'bcc', 'bqi', 'ckb', 'dv', 'fa', ' */ export function isRTL(localeString: string) { // If the Intl.Locale API is available, use it to get the locale's text direction. - // @ts-ignore if (Intl.Locale) { let locale = new Intl.Locale(localeString).maximize(); diff --git a/packages/@react-aria/menu/src/useMenu.ts b/packages/@react-aria/menu/src/useMenu.ts index b13c2a22f07..37c644b9ba2 100644 --- a/packages/@react-aria/menu/src/useMenu.ts +++ b/packages/@react-aria/menu/src/useMenu.ts @@ -80,7 +80,7 @@ export function useMenu(props: AriaMenuOptions, state: TreeState, ref: onKeyDown: (e) => { // don't clear the menu selected keys if the user is presses escape since escape closes the menu if (e.key !== 'Escape') { - listProps.onKeyDown(e); + listProps.onKeyDown?.(e); } } }) diff --git a/packages/@react-aria/menu/src/useMenuItem.ts b/packages/@react-aria/menu/src/useMenuItem.ts index ac5dd657280..328985b0ac6 100644 --- a/packages/@react-aria/menu/src/useMenuItem.ts +++ b/packages/@react-aria/menu/src/useMenuItem.ts @@ -59,7 +59,7 @@ export interface AriaMenuItemProps extends DOMProps, PressEvents, HoverEvents, K 'aria-label'?: string, /** The unique key for the menu item. */ - key?: Key, + key: Key, /** * Handler that is called when the menu should close after selecting an item. @@ -128,7 +128,7 @@ export function useMenuItem(props: AriaMenuItemProps, state: TreeState, re let isTriggerExpanded = isTrigger && props['aria-expanded'] === 'true'; let isDisabled = props.isDisabled ?? selectionManager.isDisabled(key); let isSelected = props.isSelected ?? selectionManager.isSelected(key); - let data = menuData.get(state); + let data = menuData.get(state)!; let item = state.collection.getItem(key); let onClose = props.onClose || data.onClose; let router = useRouter(); @@ -149,7 +149,7 @@ export function useMenuItem(props: AriaMenuItemProps, state: TreeState, re onAction(key); } - if (e.target instanceof HTMLAnchorElement) { + if (e.target instanceof HTMLAnchorElement && item) { router.open(e.target, e, item.props.href, item.props.routerOptions as RouterOptions); } }; @@ -279,9 +279,9 @@ export function useMenuItem(props: AriaMenuItemProps, state: TreeState, re }); let {focusProps} = useFocus({onBlur, onFocus, onFocusChange}); - let domProps = filterDOMProps(item.props); + let domProps = filterDOMProps(item?.props); delete domProps.id; - let linkProps = useLinkProps(item.props); + let linkProps = useLinkProps(item?.props); return { menuItemProps: { diff --git a/packages/@react-aria/menu/src/useMenuTrigger.ts b/packages/@react-aria/menu/src/useMenuTrigger.ts index 338b4b06fcd..3e22777bb1d 100644 --- a/packages/@react-aria/menu/src/useMenuTrigger.ts +++ b/packages/@react-aria/menu/src/useMenuTrigger.ts @@ -47,7 +47,7 @@ export interface MenuTriggerAria { */ export function useMenuTrigger(props: AriaMenuTriggerProps, state: MenuTriggerState, ref: RefObject): MenuTriggerAria { let { - type = 'menu' as AriaMenuTriggerProps['type'], + type = 'menu', isDisabled, trigger = 'press' } = props; @@ -128,6 +128,7 @@ export function useMenuTrigger(props: AriaMenuTriggerProps, state: MenuTrigge delete triggerProps.onPress; return { + // @ts-ignore - TODO we pass out both DOMAttributes AND AriaButtonProps, but useButton will discard the longPress event handlers, it's only through PressResponder magic that this works for RSP and RAC. it does not work in aria examples menuTriggerProps: { ...triggerProps, ...(trigger === 'press' ? pressProps : longPressProps), diff --git a/packages/@react-aria/menu/src/useSafelyMouseToSubmenu.ts b/packages/@react-aria/menu/src/useSafelyMouseToSubmenu.ts index 751105d7f2e..bc30a1241e9 100644 --- a/packages/@react-aria/menu/src/useSafelyMouseToSubmenu.ts +++ b/packages/@react-aria/menu/src/useSafelyMouseToSubmenu.ts @@ -63,7 +63,7 @@ export function useSafelyMouseToSubmenu(options: SafelyMouseToSubmenuOptions) { let submenu = submenuRef.current; let menu = menuRef.current; - if (isDisabled || !submenu || !isOpen || modality !== 'pointer') { + if (isDisabled || !submenu || !isOpen || modality !== 'pointer' || !menu) { reset(); return; } diff --git a/packages/@react-aria/menu/src/useSubmenuTrigger.ts b/packages/@react-aria/menu/src/useSubmenuTrigger.ts index 63afdb542b1..567a00516f3 100644 --- a/packages/@react-aria/menu/src/useSubmenuTrigger.ts +++ b/packages/@react-aria/menu/src/useSubmenuTrigger.ts @@ -41,7 +41,7 @@ export interface AriaSubmenuTriggerProps { delay?: number } -interface SubmenuTriggerProps extends AriaMenuItemProps { +interface SubmenuTriggerProps extends Omit { /** Whether the submenu trigger is in an expanded state. */ isOpen: boolean } @@ -101,14 +101,14 @@ export function useSubmenuTrigger(props: AriaSubmenuTriggerProps, state: Subm if (direction === 'ltr' && e.currentTarget.contains(e.target as Element)) { e.stopPropagation(); onSubmenuClose(); - ref.current.focus(); + ref.current?.focus(); } break; case 'ArrowRight': if (direction === 'rtl' && e.currentTarget.contains(e.target as Element)) { e.stopPropagation(); onSubmenuClose(); - ref.current.focus(); + ref.current?.focus(); } break; case 'Escape': @@ -124,7 +124,7 @@ export function useSubmenuTrigger(props: AriaSubmenuTriggerProps, state: Subm submenuLevel: state.submenuLevel, ...(type === 'menu' && { onClose: state.closeAll, - autoFocus: state.focusStrategy, + autoFocus: state.focusStrategy ?? undefined, onKeyDown: submenuKeyDown }) }; @@ -205,7 +205,7 @@ export function useSubmenuTrigger(props: AriaSubmenuTriggerProps, state: Subm }; let onBlur = (e) => { - if (state.isOpen && parentMenuRef.current.contains(e.relatedTarget)) { + if (state.isOpen && parentMenuRef.current?.contains(e.relatedTarget)) { onSubmenuClose(); } }; diff --git a/packages/@react-aria/menu/stories/useMenu.stories.tsx b/packages/@react-aria/menu/stories/useMenu.stories.tsx index ca71628b49c..334c0bb323e 100644 --- a/packages/@react-aria/menu/stories/useMenu.stories.tsx +++ b/packages/@react-aria/menu/stories/useMenu.stories.tsx @@ -54,7 +54,7 @@ function MenuButton(props) { let state = useMenuTriggerState(props); // Get props for the menu trigger and menu elements - let ref = React.useRef(undefined); + let ref = React.useRef(null); let {menuTriggerProps, menuProps} = useMenuTrigger({}, state, ref); // Get props for the button based on the trigger props from useMenuTrigger @@ -84,12 +84,12 @@ function MenuPopup(props) { let state = useTreeState({...props, selectionMode: 'none'}); // Get props for the menu element - let ref = React.useRef(undefined); + let ref = React.useRef(null); let {menuProps} = useMenu(props, state, ref); // Handle events that should cause the menu to close, // e.g. blur, clicking outside, or pressing the escape key. - let overlayRef = React.useRef(undefined); + let overlayRef = React.useRef(null); // before useOverlay so this action will get called useInteractOutside({ref: overlayRef, onInteractOutside: action('onInteractOutside')}); let {overlayProps} = useOverlay( @@ -139,7 +139,7 @@ function MenuPopup(props) { function MenuItem({item, state, onAction, onClose}) { // Get props for the menu item element - let ref = React.useRef(undefined); + let ref = React.useRef(null); let {menuItemProps} = useMenuItem( { key: item.key, diff --git a/packages/@react-aria/overlays/src/Overlay.tsx b/packages/@react-aria/overlays/src/Overlay.tsx index 80e2d0fb6ce..49b0965289a 100644 --- a/packages/@react-aria/overlays/src/Overlay.tsx +++ b/packages/@react-aria/overlays/src/Overlay.tsx @@ -39,7 +39,7 @@ export interface OverlayProps { isExiting?: boolean } -export const OverlayContext = React.createContext(null); +export const OverlayContext = React.createContext<{contain: boolean, setContain: React.Dispatch>} | null>(null); /** * A container which renders an overlay such as a popover or modal in a portal, diff --git a/packages/@react-aria/overlays/src/PortalProvider.tsx b/packages/@react-aria/overlays/src/PortalProvider.tsx index c21030f59b4..447cd7a163b 100644 --- a/packages/@react-aria/overlays/src/PortalProvider.tsx +++ b/packages/@react-aria/overlays/src/PortalProvider.tsx @@ -23,7 +23,7 @@ export function UNSTABLE_PortalProvider(props: PortalProviderProps & {children: let {getContainer} = props; let {getContainer: ctxGetContainer} = useUNSTABLE_PortalContext(); return ( - + {props.children} ); diff --git a/packages/@react-aria/overlays/src/ariaHideOutside.ts b/packages/@react-aria/overlays/src/ariaHideOutside.ts index 791aa9b6e8b..31be6884dda 100644 --- a/packages/@react-aria/overlays/src/ariaHideOutside.ts +++ b/packages/@react-aria/overlays/src/ariaHideOutside.ts @@ -13,7 +13,11 @@ // Keeps a ref count of all hidden elements. Added to when hiding an element, and // subtracted from when showing it again. When it reaches zero, aria-hidden is removed. let refCountMap = new WeakMap(); -let observerStack = []; +interface ObserverWrapper { + observe: () => void, + disconnect: () => void +} +let observerStack: Array = []; /** * Hides all elements in the DOM outside the given targets from screen readers using aria-hidden, @@ -40,7 +44,7 @@ export function ariaHideOutside(targets: Element[], root = document.body) { // For that case we want to hide the cells inside as well (https://bugs.webkit.org/show_bug.cgi?id=222623). if ( visibleNodes.has(node) || - (hiddenNodes.has(node.parentElement) && node.parentElement.getAttribute('role') !== 'row') + (node.parentElement && hiddenNodes.has(node.parentElement) && node.parentElement.getAttribute('role') !== 'row') ) { return NodeFilter.FILTER_REJECT; } @@ -133,7 +137,7 @@ export function ariaHideOutside(targets: Element[], root = document.body) { observer.observe(root, {childList: true, subtree: true}); - let observerWrapper = { + let observerWrapper: ObserverWrapper = { observe() { observer.observe(root, {childList: true, subtree: true}); }, @@ -149,6 +153,9 @@ export function ariaHideOutside(targets: Element[], root = document.body) { for (let node of hiddenNodes) { let count = refCountMap.get(node); + if (count == null) { + continue; + } if (count === 1) { node.removeAttribute('aria-hidden'); refCountMap.delete(node); diff --git a/packages/@react-aria/overlays/src/calculatePosition.ts b/packages/@react-aria/overlays/src/calculatePosition.ts index e2c4a361dff..2e929bdbfb7 100644 --- a/packages/@react-aria/overlays/src/calculatePosition.ts +++ b/packages/@react-aria/overlays/src/calculatePosition.ts @@ -64,10 +64,10 @@ interface PositionOpts { type HeightGrowthDirection = 'top' | 'bottom'; export interface PositionResult { - position?: Position, + position: Position, arrowOffsetLeft?: number, arrowOffsetTop?: number, - maxHeight?: number, + maxHeight: number, placement: PlacementAxis } @@ -102,13 +102,12 @@ const TOTAL_SIZE = { const PARSED_PLACEMENT_CACHE = {}; -// @ts-ignore -let visualViewport = typeof document !== 'undefined' && window.visualViewport; +let visualViewport = typeof document !== 'undefined' ? window.visualViewport : null; function getContainerDimensions(containerNode: Element): Dimensions { let width = 0, height = 0, totalWidth = 0, totalHeight = 0, top = 0, left = 0; let scroll: Position = {}; - let isPinchZoomedIn = visualViewport?.scale > 1; + let isPinchZoomedIn = (visualViewport?.scale ?? 1) > 1; if (containerNode.tagName === 'BODY') { let documentElement = document.documentElement; @@ -141,8 +140,8 @@ function getContainerDimensions(containerNode: Element): Dimensions { // before pinch zoom happens scroll.top = 0; scroll.left = 0; - top = visualViewport.pageTop; - left = visualViewport.pageLeft; + top = visualViewport?.pageTop ?? 0; + left = visualViewport?.pageLeft ?? 0; } return {width, height, totalWidth, totalHeight, scroll, top, left}; @@ -174,7 +173,7 @@ function getDelta( padding: number, containerOffsetWithBoundary: Offset ) { - let containerScroll = containerDimensions.scroll[axis]; + let containerScroll = containerDimensions.scroll[axis] ?? 0; // The height/width of the boundary. Matches the axis along which we are adjusting the overlay position let boundarySize = boundaryDimensions[AXIS_SIZE[axis]]; // Calculate the edges of the boundary (accomodating for the boundary padding) and the edges of the overlay. @@ -240,26 +239,26 @@ function computePosition( let position: Position = {}; // button position - position[crossAxis] = childOffset[crossAxis]; + position[crossAxis] = childOffset[crossAxis] ?? 0; if (crossPlacement === 'center') { // + (button size / 2) - (overlay size / 2) // at this point the overlay center should match the button center - position[crossAxis] += (childOffset[crossSize] - overlaySize[crossSize]) / 2; + position[crossAxis]! += ((childOffset[crossSize] ?? 0) - (overlaySize[crossSize] ?? 0)) / 2; } else if (crossPlacement !== crossAxis) { // + (button size) - (overlay size) // at this point the overlay bottom should match the button bottom - position[crossAxis] += (childOffset[crossSize] - overlaySize[crossSize]); + position[crossAxis]! += (childOffset[crossSize] ?? 0) - (overlaySize[crossSize] ?? 0); }/* else { the overlay top should match the button top } */ - position[crossAxis] += crossOffset; + position[crossAxis]! += crossOffset; // overlay top overlapping arrow with button bottom const minPosition = childOffset[crossAxis] - overlaySize[crossSize] + arrowSize + arrowBoundaryOffset; // overlay bottom overlapping arrow with button top const maxPosition = childOffset[crossAxis] + childOffset[crossSize] - arrowSize - arrowBoundaryOffset; - position[crossAxis] = clamp(position[crossAxis], minPosition, maxPosition); + position[crossAxis] = clamp(position[crossAxis]!, minPosition, maxPosition); // Floor these so the position isn't placed on a partial pixel, only whole pixels. Shouldn't matter if it was floored or ceiled, so chose one. if (placement === axis) { @@ -288,19 +287,19 @@ function getMaxHeight( const containerHeight = (isContainerPositioned ? containerOffsetWithBoundary.height : boundaryDimensions[TOTAL_SIZE.height]); // For cases where position is set via "bottom" instead of "top", we need to calculate the true overlay top with respect to the boundary. Reverse calculate this with the same method // used in computePosition. - let overlayTop = position.top != null ? containerOffsetWithBoundary.top + position.top : containerOffsetWithBoundary.top + (containerHeight - position.bottom - overlayHeight); + let overlayTop = position.top != null ? containerOffsetWithBoundary.top + position.top : containerOffsetWithBoundary.top + (containerHeight - (position.bottom ?? 0) - overlayHeight); let maxHeight = heightGrowthDirection !== 'top' ? // We want the distance between the top of the overlay to the bottom of the boundary Math.max(0, - (boundaryDimensions.height + boundaryDimensions.top + boundaryDimensions.scroll.top) // this is the bottom of the boundary + (boundaryDimensions.height + boundaryDimensions.top + (boundaryDimensions.scroll.top ?? 0)) // this is the bottom of the boundary - overlayTop // this is the top of the overlay - - (margins.top + margins.bottom + padding) // save additional space for margin and padding + - ((margins.top ?? 0) + (margins.bottom ?? 0) + padding) // save additional space for margin and padding ) // We want the distance between the bottom of the overlay to the top of the boundary : Math.max(0, (overlayTop + overlayHeight) // this is the bottom of the overlay - - (boundaryDimensions.top + boundaryDimensions.scroll.top) // this is the top of the boundary - - (margins.top + margins.bottom + padding) // save additional space for margin and padding + - (boundaryDimensions.top + (boundaryDimensions.scroll.top ?? 0)) // this is the top of the boundary + - ((margins.top ?? 0) + (margins.bottom ?? 0) + padding) // save additional space for margin and padding ); return Math.min(boundaryDimensions.height - (padding * 2), maxHeight); } @@ -315,10 +314,10 @@ function getAvailableSpace( ) { let {placement, axis, size} = placementInfo; if (placement === axis) { - return Math.max(0, childOffset[axis] - boundaryDimensions[axis] - boundaryDimensions.scroll[axis] + containerOffsetWithBoundary[axis] - margins[axis] - margins[FLIPPED_DIRECTION[axis]] - padding); + return Math.max(0, childOffset[axis] - boundaryDimensions[axis] - (boundaryDimensions.scroll[axis] ?? 0) + containerOffsetWithBoundary[axis] - (margins[axis] ?? 0) - margins[FLIPPED_DIRECTION[axis]] - padding); } - return Math.max(0, boundaryDimensions[size] + boundaryDimensions[axis] + boundaryDimensions.scroll[axis] - containerOffsetWithBoundary[axis] - childOffset[axis] - childOffset[size] - margins[axis] - margins[FLIPPED_DIRECTION[axis]] - padding); + return Math.max(0, boundaryDimensions[size] + boundaryDimensions[axis] + boundaryDimensions.scroll[axis] - containerOffsetWithBoundary[axis] - childOffset[axis] - childOffset[size] - (margins[axis] ?? 0) - margins[FLIPPED_DIRECTION[axis]] - padding); } export function calculatePositionInternal( @@ -389,8 +388,8 @@ export function calculatePositionInternal( } } - let delta = getDelta(crossAxis, position[crossAxis], overlaySize[crossSize], boundaryDimensions, containerDimensions, padding, containerOffsetWithBoundary); - position[crossAxis] += delta; + let delta = getDelta(crossAxis, position[crossAxis]!, overlaySize[crossSize], boundaryDimensions, containerDimensions, padding, containerOffsetWithBoundary); + position[crossAxis]! += delta; let maxHeight = getMaxHeight( position, @@ -410,8 +409,8 @@ export function calculatePositionInternal( overlaySize.height = Math.min(overlaySize.height, maxHeight); position = computePosition(childOffset, boundaryDimensions, overlaySize, placementInfo, normalizedOffset, crossOffset, containerOffsetWithBoundary, isContainerPositioned, arrowSize, arrowBoundaryOffset); - delta = getDelta(crossAxis, position[crossAxis], overlaySize[crossSize], boundaryDimensions, containerDimensions, padding, containerOffsetWithBoundary); - position[crossAxis] += delta; + delta = getDelta(crossAxis, position[crossAxis]!, overlaySize[crossSize], boundaryDimensions, containerDimensions, padding, containerOffsetWithBoundary); + position[crossAxis]! += delta; let arrowPosition: Position = {}; @@ -420,12 +419,12 @@ export function calculatePositionInternal( // childOffset[crossAxis] + .5 * childOffset[crossSize] = absolute position with respect to the trigger's coordinate system that would place the arrow in the center of the trigger // position[crossAxis] - margins[AXIS[crossAxis]] = value use to transform the position to a value with respect to the overlay's coordinate system. A child element's (aka arrow) position absolute's "0" // is positioned after the margin of its parent (aka overlay) so we need to subtract it to get the proper coordinate transform - let preferredArrowPosition = childOffset[crossAxis] + .5 * childOffset[crossSize] - position[crossAxis] - margins[AXIS[crossAxis]]; + let preferredArrowPosition = childOffset[crossAxis] + .5 * childOffset[crossSize] - position[crossAxis]! - margins[AXIS[crossAxis]]; // Min/Max position limits for the arrow with respect to the overlay const arrowMinPosition = arrowSize / 2 + arrowBoundaryOffset; // overlaySize[crossSize] - margins = true size of the overlay - const overlayMargin = AXIS[crossAxis] === 'left' ? margins.left + margins.right : margins.top + margins.bottom; + const overlayMargin = AXIS[crossAxis] === 'left' ? (margins.left ?? 0) + (margins.right ?? 0) : (margins.top ?? 0) + (margins.bottom ?? 0); const arrowMaxPosition = overlaySize[crossSize] - overlayMargin - (arrowSize / 2) - arrowBoundaryOffset; // Min/Max position limits for the arrow with respect to the trigger/overlay anchor element @@ -479,8 +478,8 @@ export function calculatePosition(opts: PositionOpts): PositionResult { let overlaySize: Offset = getOffset(overlayNode); let margins = getMargins(overlayNode); - overlaySize.width += margins.left + margins.right; - overlaySize.height += margins.top + margins.bottom; + overlaySize.width += (margins.left ?? 0) + (margins.right ?? 0); + overlaySize.height += (margins.top ?? 0) + (margins.bottom ?? 0); let scrollSize = getScroll(scrollNode); let boundaryDimensions = getContainerDimensions(boundaryElement); @@ -590,9 +589,7 @@ function isContainingBlock(node: Element): boolean { /transform|perspective/.test(style.willChange) || style.filter !== 'none' || style.contain === 'paint' || - // @ts-ignore ('backdropFilter' in style && style.backdropFilter !== 'none') || - // @ts-ignore ('WebkitBackdropFilter' in style && style.WebkitBackdropFilter !== 'none') ); } diff --git a/packages/@react-aria/overlays/src/useCloseOnScroll.ts b/packages/@react-aria/overlays/src/useCloseOnScroll.ts index d6784f35544..09588b09a5b 100644 --- a/packages/@react-aria/overlays/src/useCloseOnScroll.ts +++ b/packages/@react-aria/overlays/src/useCloseOnScroll.ts @@ -35,7 +35,7 @@ export function useCloseOnScroll(opts: CloseOnScrollOptions) { return; } - let onScroll = (e: MouseEvent) => { + let onScroll = (e: Event) => { // Ignore if scrolling an scrollable region outside the trigger's tree. let target = e.target; // window is not a Node and doesn't have contain, but window contains everything diff --git a/packages/@react-aria/overlays/src/useModal.tsx b/packages/@react-aria/overlays/src/useModal.tsx index 748364d5e81..a51c5ce2a5a 100644 --- a/packages/@react-aria/overlays/src/useModal.tsx +++ b/packages/@react-aria/overlays/src/useModal.tsx @@ -79,7 +79,7 @@ export function useModalProvider(): ModalProviderAria { let context = useContext(Context); return { modalProviderProps: { - 'aria-hidden': context && context.modalCount > 0 ? true : null + 'aria-hidden': context && context.modalCount > 0 ? true : undefined } }; } @@ -123,7 +123,7 @@ export interface OverlayContainerProps extends ModalProviderProps { * nested modal is opened. Only the top-most modal or overlay should * be accessible at once. */ -export function OverlayContainer(props: OverlayContainerProps): React.ReactPortal { +export function OverlayContainer(props: OverlayContainerProps): React.ReactPortal | null { let isSSR = useIsSSR(); let {portalContainer = isSSR ? null : document.body, ...rest} = props; diff --git a/packages/@react-aria/overlays/src/useModalOverlay.ts b/packages/@react-aria/overlays/src/useModalOverlay.ts index de0412ffad2..4f95655a60b 100644 --- a/packages/@react-aria/overlays/src/useModalOverlay.ts +++ b/packages/@react-aria/overlays/src/useModalOverlay.ts @@ -57,7 +57,7 @@ export function useModalOverlay(props: AriaModalOverlayProps, state: OverlayTrig useOverlayFocusContain(); useEffect(() => { - if (state.isOpen) { + if (state.isOpen && ref.current) { return ariaHideOutside([ref.current]); } }, [state.isOpen, ref]); diff --git a/packages/@react-aria/overlays/src/useOverlay.ts b/packages/@react-aria/overlays/src/useOverlay.ts index c5b7dc2432c..2ebeeabad4a 100644 --- a/packages/@react-aria/overlays/src/useOverlay.ts +++ b/packages/@react-aria/overlays/src/useOverlay.ts @@ -120,7 +120,7 @@ export function useOverlay(props: AriaOverlayProps, ref: RefObject({ - position: {}, - arrowOffsetLeft: undefined, - arrowOffsetTop: undefined, - maxHeight: undefined, - placement: undefined - }); + let [position, setPosition] = useState(null); let deps = [ shouldUpdatePosition, @@ -154,17 +147,17 @@ export function useOverlayPosition(props: AriaPositionProps): PositionAria { // changes, the focused element appears to stay in the same position. let anchor: ScrollAnchor | null = null; if (scrollRef.current && scrollRef.current.contains(document.activeElement)) { - let anchorRect = document.activeElement.getBoundingClientRect(); + let anchorRect = document.activeElement?.getBoundingClientRect(); let scrollRect = scrollRef.current.getBoundingClientRect(); // Anchor from the top if the offset is in the top half of the scrollable element, // otherwise anchor from the bottom. anchor = { type: 'top', - offset: anchorRect.top - scrollRect.top + offset: (anchorRect?.top ?? 0) - scrollRect.top }; if (anchor.offset > scrollRect.height / 2) { anchor.type = 'bottom'; - anchor.offset = anchorRect.bottom - scrollRect.bottom; + anchor.offset = (anchorRect?.bottom ?? 0) - scrollRect.bottom; } } @@ -192,6 +185,10 @@ export function useOverlayPosition(props: AriaPositionProps): PositionAria { arrowBoundaryOffset }); + if (!position.position) { + return; + } + // Modify overlay styles directly so positioning happens immediately without the need of a second render // This is so we don't have to delay autoFocus scrolling or delay applying preventScroll for popovers overlay.style.top = ''; @@ -199,11 +196,11 @@ export function useOverlayPosition(props: AriaPositionProps): PositionAria { overlay.style.left = ''; overlay.style.right = ''; - Object.keys(position.position).forEach(key => overlay.style[key] = position.position[key] + 'px'); - overlay.style.maxHeight = position.maxHeight != null ? position.maxHeight + 'px' : undefined; + Object.keys(position.position).forEach(key => overlay.style[key] = (position.position!)[key] + 'px'); + overlay.style.maxHeight = position.maxHeight != null ? position.maxHeight + 'px' : ''; // Restore scroll position relative to anchor element. - if (anchor) { + if (anchor && document.activeElement && scrollRef.current) { let anchorRect = document.activeElement.getBoundingClientRect(); let scrollRect = scrollRef.current.getBoundingClientRect(); let newOffset = anchorRect[anchor.type] - scrollRect[anchor.type]; @@ -268,7 +265,7 @@ export function useOverlayPosition(props: AriaPositionProps): PositionAria { let close = useCallback(() => { if (!isResizing.current) { - onClose(); + onClose?.(); } }, [onClose, isResizing]); @@ -285,17 +282,17 @@ export function useOverlayPosition(props: AriaPositionProps): PositionAria { style: { position: 'absolute', zIndex: 100000, // should match the z-index in ModalTrigger - ...position.position, - maxHeight: position.maxHeight ?? '100vh' + ...position?.position, + maxHeight: position?.maxHeight ?? '100vh' } }, - placement: position.placement, + placement: position?.placement ?? null, arrowProps: { 'aria-hidden': 'true', role: 'presentation', style: { - left: position.arrowOffsetLeft, - top: position.arrowOffsetTop + left: position?.arrowOffsetLeft, + top: position?.arrowOffsetTop } }, updatePosition diff --git a/packages/@react-aria/overlays/src/useOverlayTrigger.ts b/packages/@react-aria/overlays/src/useOverlayTrigger.ts index a5064e7b753..0608b368470 100644 --- a/packages/@react-aria/overlays/src/useOverlayTrigger.ts +++ b/packages/@react-aria/overlays/src/useOverlayTrigger.ts @@ -50,7 +50,7 @@ export function useOverlayTrigger(props: OverlayTriggerProps, state: OverlayTrig // https://www.w3.org/TR/wai-aria-1.1/#aria-haspopup // However, we only add it for menus for now because screen readers often // announce it as a menu even for other values. - let ariaHasPopup = undefined; + let ariaHasPopup: undefined | boolean | 'listbox' = undefined; if (type === 'menu') { ariaHasPopup = true; } else if (type === 'listbox') { @@ -62,7 +62,7 @@ export function useOverlayTrigger(props: OverlayTriggerProps, state: OverlayTrig triggerProps: { 'aria-haspopup': ariaHasPopup, 'aria-expanded': isOpen, - 'aria-controls': isOpen ? overlayId : null, + 'aria-controls': isOpen ? overlayId : undefined, onPress: state.toggle }, overlayProps: { diff --git a/packages/@react-aria/overlays/src/usePopover.ts b/packages/@react-aria/overlays/src/usePopover.ts index dc309a155c6..3577dff3be6 100644 --- a/packages/@react-aria/overlays/src/usePopover.ts +++ b/packages/@react-aria/overlays/src/usePopover.ts @@ -63,7 +63,7 @@ export interface PopoverAria { /** Props to apply to the underlay element, if any. */ underlayProps: DOMAttributes, /** Placement of the popover with respect to the trigger. */ - placement: PlacementAxis + placement: PlacementAxis | null } /** @@ -98,7 +98,7 @@ export function usePopover(props: AriaPopoverProps, state: OverlayTriggerState): targetRef: triggerRef, overlayRef: popoverRef, isOpen: state.isOpen, - onClose: isNonModal ? state.close : null + onClose: isNonModal ? state.close : undefined }); usePreventScroll({ diff --git a/packages/@react-aria/overlays/src/usePreventScroll.ts b/packages/@react-aria/overlays/src/usePreventScroll.ts index ad4f4cdd74e..08948f180c7 100644 --- a/packages/@react-aria/overlays/src/usePreventScroll.ts +++ b/packages/@react-aria/overlays/src/usePreventScroll.ts @@ -17,7 +17,6 @@ interface PreventScrollOptions { isDisabled?: boolean } -// @ts-ignore const visualViewport = typeof document !== 'undefined' && window.visualViewport; // HTML input types that do not cause the software keyboard to appear. @@ -195,7 +194,7 @@ function preventScrollMobileSafari() { } }; - let restoreStyles = null; + let restoreStyles: null | (() => void) = null; let setupStyles = () => { if (restoreStyles) { return; @@ -254,31 +253,35 @@ function setStyle(element: HTMLElement, style: string, value: string) { // Adds an event listener to an element, and returns a function to remove it. function addEvent( - target: EventTarget, + target: Document | Window, event: K, - handler: (this: Document, ev: GlobalEventHandlersEventMap[K]) => any, + handler: (this: Document | Window, ev: GlobalEventHandlersEventMap[K]) => any, options?: boolean | AddEventListenerOptions ) { + // internal function, so it's ok to ignore the difficult to fix type error + // @ts-ignore target.addEventListener(event, handler, options); return () => { + // @ts-ignore target.removeEventListener(event, handler, options); }; } function scrollIntoView(target: Element) { let root = document.scrollingElement || document.documentElement; - while (target && target !== root) { + let nextTarget: Element | null = target; + while (nextTarget && nextTarget !== root) { // Find the parent scrollable element and adjust the scroll position if the target is not already in view. - let scrollable = getScrollParent(target); - if (scrollable !== document.documentElement && scrollable !== document.body && scrollable !== target) { + let scrollable = getScrollParent(nextTarget); + if (scrollable !== document.documentElement && scrollable !== document.body && scrollable !== nextTarget) { let scrollableTop = scrollable.getBoundingClientRect().top; - let targetTop = target.getBoundingClientRect().top; - if (targetTop > scrollableTop + target.clientHeight) { + let targetTop = nextTarget.getBoundingClientRect().top; + if (targetTop > scrollableTop + nextTarget.clientHeight) { scrollable.scrollTop += targetTop - scrollableTop; } } - target = scrollable.parentElement; + nextTarget = scrollable.parentElement; } } diff --git a/packages/@react-aria/overlays/test/calculatePosition.test.ts b/packages/@react-aria/overlays/test/calculatePosition.test.ts index 747495b1314..572a26f768e 100644 --- a/packages/@react-aria/overlays/test/calculatePosition.test.ts +++ b/packages/@react-aria/overlays/test/calculatePosition.test.ts @@ -166,7 +166,7 @@ describe('calculatePosition', function () { }); } - function checkPosition(placement, targetDimension, expected, offset = 0, crossOffset = 0, flip = false, providerOffset = undefined, arrowSize = undefined, arrowBoundaryOffset = undefined) { + function checkPosition(placement, targetDimension, expected, offset = 0, crossOffset = 0, flip?: boolean, providerOffset?: number, arrowSize?: number, arrowBoundaryOffset?: number) { checkPositionCommon( 'Should calculate the correct position', expected, diff --git a/packages/@react-aria/overlays/test/useOverlayPosition.test.tsx b/packages/@react-aria/overlays/test/useOverlayPosition.test.tsx index 378136e510f..4eb382b65d3 100644 --- a/packages/@react-aria/overlays/test/useOverlayPosition.test.tsx +++ b/packages/@react-aria/overlays/test/useOverlayPosition.test.tsx @@ -15,9 +15,9 @@ import React, {useRef} from 'react'; import {useOverlayPosition} from '../'; function Example({triggerTop = 250, ...props}) { - let targetRef = useRef(undefined); - let containerRef = useRef(undefined); - let overlayRef = useRef(undefined); + let targetRef = useRef(null); + let containerRef = useRef(null); + let overlayRef = useRef(null); let {overlayProps, placement, arrowProps} = useOverlayPosition({targetRef, overlayRef, arrowSize: 8, ...props}); let style = {width: 300, height: 200, ...overlayProps.style}; return ( @@ -33,9 +33,11 @@ function Example({triggerTop = 250, ...props}) { ); } -// @ts-ignore +let original = window.HTMLElement.prototype.getBoundingClientRect; HTMLElement.prototype.getBoundingClientRect = function () { + let rect = original.apply(this); return { + ...rect, left: parseInt(this.style.left, 10) || 0, top: parseInt(this.style.top, 10) || 0, right: parseInt(this.style.right, 10) || 0, @@ -213,14 +215,14 @@ describe('useOverlayPosition', function () { }); describe('useOverlayPosition with positioned container', () => { - let stubs = []; + let stubs: jest.SpyInstance[] = []; let realGetBoundingClientRect = window.HTMLElement.prototype.getBoundingClientRect; let realGetComputedStyle = window.getComputedStyle; beforeEach(() => { Object.defineProperty(HTMLElement.prototype, 'clientHeight', {configurable: true, value: 768}); Object.defineProperty(HTMLElement.prototype, 'clientWidth', {configurable: true, value: 500}); stubs.push( - jest.spyOn(window.HTMLElement.prototype, 'offsetParent', 'get').mockImplementation(function () { + jest.spyOn(window.HTMLElement.prototype, 'offsetParent', 'get').mockImplementation(function (this: HTMLElement) { // Make sure container is is the offsetParent of overlay if (this.attributes.getNamedItem('data-testid')?.value === 'overlay') { return document.querySelector('[data-testid="container"]'); @@ -228,10 +230,12 @@ describe('useOverlayPosition with positioned container', () => { return null; } }), - jest.spyOn(window.HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(function () { + jest.spyOn(window.HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(function (this: HTMLElement) { if (this.attributes.getNamedItem('data-testid')?.value === 'container') { // Say, overlay is positioned somewhere + let real = realGetBoundingClientRect.apply(this); return { + ...real, top: 150, left: 0, width: 400, diff --git a/packages/@react-aria/pagination/README.md b/packages/@react-aria/pagination/README.md deleted file mode 100644 index 984706c3e4f..00000000000 --- a/packages/@react-aria/pagination/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# @react-aria/pagination - -This package is part of [react-spectrum](https://github.com/adobe/react-spectrum). See the repo for more details. diff --git a/packages/@react-aria/pagination/index.ts b/packages/@react-aria/pagination/index.ts deleted file mode 100644 index 1210ae1e402..00000000000 --- a/packages/@react-aria/pagination/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright 2020 Adobe. All rights reserved. - * This file is licensed to you under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. You may obtain a copy - * of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS - * OF ANY KIND, either express or implied. See the License for the specific language - * governing permissions and limitations under the License. - */ - -export * from './src'; diff --git a/packages/@react-aria/pagination/intl/ar-AE.json b/packages/@react-aria/pagination/intl/ar-AE.json deleted file mode 100644 index 5428af43e2a..00000000000 --- a/packages/@react-aria/pagination/intl/ar-AE.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "next": "التالي", - "previous": "السابق" -} diff --git a/packages/@react-aria/pagination/intl/bg-BG.json b/packages/@react-aria/pagination/intl/bg-BG.json deleted file mode 100644 index e186d507827..00000000000 --- a/packages/@react-aria/pagination/intl/bg-BG.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "next": "Напред", - "previous": "Назад" -} diff --git a/packages/@react-aria/pagination/intl/cs-CZ.json b/packages/@react-aria/pagination/intl/cs-CZ.json deleted file mode 100644 index bc4542177c9..00000000000 --- a/packages/@react-aria/pagination/intl/cs-CZ.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "next": "Další", - "previous": "Předchozí" -} diff --git a/packages/@react-aria/pagination/intl/da-DK.json b/packages/@react-aria/pagination/intl/da-DK.json deleted file mode 100644 index fb2577085a4..00000000000 --- a/packages/@react-aria/pagination/intl/da-DK.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "next": "Næste", - "previous": "Forrige" -} diff --git a/packages/@react-aria/pagination/intl/de-DE.json b/packages/@react-aria/pagination/intl/de-DE.json deleted file mode 100644 index 1bb4843763a..00000000000 --- a/packages/@react-aria/pagination/intl/de-DE.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "next": "Weiter", - "previous": "Zurück" -} diff --git a/packages/@react-aria/pagination/intl/el-GR.json b/packages/@react-aria/pagination/intl/el-GR.json deleted file mode 100644 index d79270fd27f..00000000000 --- a/packages/@react-aria/pagination/intl/el-GR.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "next": "Επόμενο", - "previous": "Προηγούμενο" -} diff --git a/packages/@react-aria/pagination/intl/en-US.json b/packages/@react-aria/pagination/intl/en-US.json deleted file mode 100644 index c1a997391a9..00000000000 --- a/packages/@react-aria/pagination/intl/en-US.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "previous": "Previous", - "next": "Next" -} diff --git a/packages/@react-aria/pagination/intl/es-ES.json b/packages/@react-aria/pagination/intl/es-ES.json deleted file mode 100644 index 759154d5c10..00000000000 --- a/packages/@react-aria/pagination/intl/es-ES.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "next": "Siguiente", - "previous": "Anterior" -} diff --git a/packages/@react-aria/pagination/intl/et-EE.json b/packages/@react-aria/pagination/intl/et-EE.json deleted file mode 100644 index a641a4b2782..00000000000 --- a/packages/@react-aria/pagination/intl/et-EE.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "next": "Järgmine", - "previous": "Eelmine" -} diff --git a/packages/@react-aria/pagination/intl/fi-FI.json b/packages/@react-aria/pagination/intl/fi-FI.json deleted file mode 100644 index 57830cba817..00000000000 --- a/packages/@react-aria/pagination/intl/fi-FI.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "next": "Seuraava", - "previous": "Edellinen" -} diff --git a/packages/@react-aria/pagination/intl/fr-FR.json b/packages/@react-aria/pagination/intl/fr-FR.json deleted file mode 100644 index 188c470dd7a..00000000000 --- a/packages/@react-aria/pagination/intl/fr-FR.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "next": "Suivant", - "previous": "Précédent" -} diff --git a/packages/@react-aria/pagination/intl/he-IL.json b/packages/@react-aria/pagination/intl/he-IL.json deleted file mode 100644 index 1ad862b8e8e..00000000000 --- a/packages/@react-aria/pagination/intl/he-IL.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "next": "הבא", - "previous": "הקודם" -} diff --git a/packages/@react-aria/pagination/intl/hr-HR.json b/packages/@react-aria/pagination/intl/hr-HR.json deleted file mode 100644 index 3538a44bd27..00000000000 --- a/packages/@react-aria/pagination/intl/hr-HR.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "next": "Sljedeći", - "previous": "Prethodni" -} diff --git a/packages/@react-aria/pagination/intl/hu-HU.json b/packages/@react-aria/pagination/intl/hu-HU.json deleted file mode 100644 index b0f5c8fac21..00000000000 --- a/packages/@react-aria/pagination/intl/hu-HU.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "next": "Következő", - "previous": "Előző" -} diff --git a/packages/@react-aria/pagination/intl/index.js b/packages/@react-aria/pagination/intl/index.js deleted file mode 100644 index ab42abafdb9..00000000000 --- a/packages/@react-aria/pagination/intl/index.js +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2020 Adobe. All rights reserved. - * This file is licensed to you under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. You may obtain a copy - * of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS - * OF ANY KIND, either express or implied. See the License for the specific language - * governing permissions and limitations under the License. - */ - -import csCZ from './cs-CZ.json'; -import daDK from './da-DK.json'; -import deDE from './de-DE.json'; -import enUS from './en-US.json'; -import esES from './es-ES.json'; -import fiFI from './fi-FI.json'; -import frFR from './fr-FR.json'; -import itIT from './it-IT.json'; -import jaJP from './ja-JP.json'; -import koKR from './ko-KR.json'; -import nbNO from './nb-NO.json'; -import nlNL from './nl-NL.json'; -import plPL from './pl-PL.json'; -import ptBR from './pt-BR.json'; -import ruRU from './ru-RU.json'; -import svSE from './sv-SE.json'; -import trTR from './tr-TR.json'; -import zhCN from './zh-CN.json'; -import zhTW from './zh-TW.json'; - -export default { - 'cs-CZ': csCZ, - 'da-DK': daDK, - 'de-DE': deDE, - 'en-US': enUS, - 'es-ES': esES, - 'fi-FI': fiFI, - 'fr-FR': frFR, - 'it-IT': itIT, - 'ja-JP': jaJP, - 'ko-KR': koKR, - 'nb-NO': nbNO, - 'nl-NL': nlNL, - 'pl-PL': plPL, - 'pt-BR': ptBR, - 'ru-RU': ruRU, - 'sv-SE': svSE, - 'tr-TR': trTR, - 'zh-CN': zhCN, - 'zh-TW': zhTW -}; diff --git a/packages/@react-aria/pagination/intl/it-IT.json b/packages/@react-aria/pagination/intl/it-IT.json deleted file mode 100644 index 9996be7a4c4..00000000000 --- a/packages/@react-aria/pagination/intl/it-IT.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "next": "Successivo", - "previous": "Precedente" -} diff --git a/packages/@react-aria/pagination/intl/ja-JP.json b/packages/@react-aria/pagination/intl/ja-JP.json deleted file mode 100644 index 706ce258205..00000000000 --- a/packages/@react-aria/pagination/intl/ja-JP.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "next": "次へ", - "previous": "前へ" -} diff --git a/packages/@react-aria/pagination/intl/ko-KR.json b/packages/@react-aria/pagination/intl/ko-KR.json deleted file mode 100644 index 7f338669cb9..00000000000 --- a/packages/@react-aria/pagination/intl/ko-KR.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "next": "다음", - "previous": "이전" -} diff --git a/packages/@react-aria/pagination/intl/lt-LT.json b/packages/@react-aria/pagination/intl/lt-LT.json deleted file mode 100644 index 3033ce5d642..00000000000 --- a/packages/@react-aria/pagination/intl/lt-LT.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "next": "Paskesnis", - "previous": "Ankstesnis" -} diff --git a/packages/@react-aria/pagination/intl/lv-LV.json b/packages/@react-aria/pagination/intl/lv-LV.json deleted file mode 100644 index 5af5f1dbde7..00000000000 --- a/packages/@react-aria/pagination/intl/lv-LV.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "next": "Tālāk", - "previous": "Atpakaļ" -} diff --git a/packages/@react-aria/pagination/intl/nb-NO.json b/packages/@react-aria/pagination/intl/nb-NO.json deleted file mode 100644 index b9602906dde..00000000000 --- a/packages/@react-aria/pagination/intl/nb-NO.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "next": "Neste", - "previous": "Forrige" -} diff --git a/packages/@react-aria/pagination/intl/nl-NL.json b/packages/@react-aria/pagination/intl/nl-NL.json deleted file mode 100644 index 8757cdd3a29..00000000000 --- a/packages/@react-aria/pagination/intl/nl-NL.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "next": "Volgende", - "previous": "Vorige" -} diff --git a/packages/@react-aria/pagination/intl/pl-PL.json b/packages/@react-aria/pagination/intl/pl-PL.json deleted file mode 100644 index 0b2c6fde95e..00000000000 --- a/packages/@react-aria/pagination/intl/pl-PL.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "next": "Dalej", - "previous": "Wstecz" -} diff --git a/packages/@react-aria/pagination/intl/pt-BR.json b/packages/@react-aria/pagination/intl/pt-BR.json deleted file mode 100644 index 12ac6e9c1eb..00000000000 --- a/packages/@react-aria/pagination/intl/pt-BR.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "next": "Próximo", - "previous": "Anterior" -} diff --git a/packages/@react-aria/pagination/intl/pt-PT.json b/packages/@react-aria/pagination/intl/pt-PT.json deleted file mode 100644 index 12ac6e9c1eb..00000000000 --- a/packages/@react-aria/pagination/intl/pt-PT.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "next": "Próximo", - "previous": "Anterior" -} diff --git a/packages/@react-aria/pagination/intl/ro-RO.json b/packages/@react-aria/pagination/intl/ro-RO.json deleted file mode 100644 index cbdfa7ca388..00000000000 --- a/packages/@react-aria/pagination/intl/ro-RO.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "next": "Următorul", - "previous": "Înainte" -} diff --git a/packages/@react-aria/pagination/intl/ru-RU.json b/packages/@react-aria/pagination/intl/ru-RU.json deleted file mode 100644 index 962a05876a2..00000000000 --- a/packages/@react-aria/pagination/intl/ru-RU.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "next": "Далее", - "previous": "Назад" -} diff --git a/packages/@react-aria/pagination/intl/sk-SK.json b/packages/@react-aria/pagination/intl/sk-SK.json deleted file mode 100644 index 7474f6b6e3f..00000000000 --- a/packages/@react-aria/pagination/intl/sk-SK.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "next": "Nasledujúce", - "previous": "Predchádzajúce" -} diff --git a/packages/@react-aria/pagination/intl/sl-SI.json b/packages/@react-aria/pagination/intl/sl-SI.json deleted file mode 100644 index 25a9d721d21..00000000000 --- a/packages/@react-aria/pagination/intl/sl-SI.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "next": "Naprej", - "previous": "Nazaj" -} diff --git a/packages/@react-aria/pagination/intl/sr-SP.json b/packages/@react-aria/pagination/intl/sr-SP.json deleted file mode 100644 index f25f4c47ff0..00000000000 --- a/packages/@react-aria/pagination/intl/sr-SP.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "next": "Sledeći", - "previous": "Prethodni" -} diff --git a/packages/@react-aria/pagination/intl/sv-SE.json b/packages/@react-aria/pagination/intl/sv-SE.json deleted file mode 100644 index b42a0f0f944..00000000000 --- a/packages/@react-aria/pagination/intl/sv-SE.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "next": "Nästa", - "previous": "Föregående" -} diff --git a/packages/@react-aria/pagination/intl/tr-TR.json b/packages/@react-aria/pagination/intl/tr-TR.json deleted file mode 100644 index c1083c7fa39..00000000000 --- a/packages/@react-aria/pagination/intl/tr-TR.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "next": "Sonraki", - "previous": "Önceki" -} diff --git a/packages/@react-aria/pagination/intl/uk-UA.json b/packages/@react-aria/pagination/intl/uk-UA.json deleted file mode 100644 index 7eb5924cc62..00000000000 --- a/packages/@react-aria/pagination/intl/uk-UA.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "next": "Наступний", - "previous": "Попередній" -} diff --git a/packages/@react-aria/pagination/intl/zh-CN.json b/packages/@react-aria/pagination/intl/zh-CN.json deleted file mode 100644 index 94591beea7f..00000000000 --- a/packages/@react-aria/pagination/intl/zh-CN.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "next": "下一页", - "previous": "上一页" -} diff --git a/packages/@react-aria/pagination/intl/zh-TW.json b/packages/@react-aria/pagination/intl/zh-TW.json deleted file mode 100644 index 1780783d953..00000000000 --- a/packages/@react-aria/pagination/intl/zh-TW.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "next": "下一頁", - "previous": "上一頁" -} diff --git a/packages/@react-aria/pagination/package.json b/packages/@react-aria/pagination/package.json deleted file mode 100644 index 0b650a3d63f..00000000000 --- a/packages/@react-aria/pagination/package.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "name": "@react-aria/pagination", - "version": "3.0.0-alpha.1", - "description": "Spectrum UI components in React", - "license": "Apache-2.0", - "private": true, - "main": "dist/main.js", - "module": "dist/module.js", - "exports": { - "types": "./dist/types.d.ts", - "import": "./dist/import.mjs", - "require": "./dist/main.js" - }, - "types": "dist/types.d.ts", - "source": "src/index.ts", - "files": [ - "dist", - "src" - ], - "sideEffects": false, - "repository": { - "type": "git", - "url": "https://github.com/adobe/react-spectrum" - }, - "dependencies": { - "@react-aria/i18n": "^3.1.0", - "@react-stately/pagination": "3.0.0-alpha.1", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - }, - "publishConfig": { - "access": "public" - } -} diff --git a/packages/@react-aria/pagination/src/index.ts b/packages/@react-aria/pagination/src/index.ts deleted file mode 100644 index 987f15c4a27..00000000000 --- a/packages/@react-aria/pagination/src/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright 2020 Adobe. All rights reserved. - * This file is licensed to you under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. You may obtain a copy - * of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS - * OF ANY KIND, either express or implied. See the License for the specific language - * governing permissions and limitations under the License. - */ -export {usePagination} from './usePagination'; diff --git a/packages/@react-aria/pagination/src/usePagination.ts b/packages/@react-aria/pagination/src/usePagination.ts deleted file mode 100644 index 471f31343fd..00000000000 --- a/packages/@react-aria/pagination/src/usePagination.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2020 Adobe. All rights reserved. - * This file is licensed to you under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. You may obtain a copy - * of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS - * OF ANY KIND, either express or implied. See the License for the specific language - * governing permissions and limitations under the License. - */ - -import intlMessages from '../intl'; -import {PaginationState} from '@react-stately/pagination'; -import {useLocalizedStringFormatter} from '@react-aria/i18n'; - -interface PaginationAriaProps { - value?: any, - onPrevious?: (value: number, ...args: any) => void, - onNext?: (value: number, ...args: any) => void -} - -export function usePagination(props: PaginationAriaProps, state: PaginationState) { - let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-aria/pagination'); - - let onPrevious = () => { - state.onDecrement(); - if (props.onPrevious) { - props.onPrevious(state.ref.current); - } - }; - - let onNext = () => { - state.onIncrement(); - if (props.onNext) { - props.onNext(state.ref.current); - } - }; - - let onKeyDown = (e) => { - switch (e.key) { - case 'ArrowUp': - case 'Up': - state.onIncrement(); - break; - case 'ArrowDown': - case 'Down': - state.onDecrement(); - break; - case 'Enter': - case ' ': - break; - default: - } - }; - - return { - prevButtonProps: { - ...props, - 'aria-label': stringFormatter.format('previous'), - onPress: onPrevious - }, - nextButtonProps: { - ...props, - 'aria-label': stringFormatter.format('next'), - onPress: onNext - }, - textProps: { - ...props, - onKeyDown: onKeyDown - } - }; -} diff --git a/packages/@react-aria/pagination/test/usePagination.test.js b/packages/@react-aria/pagination/test/usePagination.test.js deleted file mode 100644 index bb50c6c7c61..00000000000 --- a/packages/@react-aria/pagination/test/usePagination.test.js +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright 2020 Adobe. All rights reserved. - * This file is licensed to you under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. You may obtain a copy - * of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS - * OF ANY KIND, either express or implied. See the License for the specific language - * governing permissions and limitations under the License. - */ - -import React from 'react'; -import {renderHook} from '@react-spectrum/test-utils-internal'; -import {usePagination} from '../'; - -describe('usePagination tests', function () { - - let setValue = jest.fn(); - let state = {}; - let props = {}; - let preventDefault = jest.fn(); - let onPrevious = jest.fn(); - let onNext = jest.fn(); - let onIncrement = jest.fn(); - let onDecrement = jest.fn(); - let maxValue = 20; - let event = (key) => ({ - key, - preventDefault - }); - - let renderPaginationHook = (props) => { - props.onPrevious = onPrevious; - props.onNext = onNext; - let {result} = renderHook(() => usePagination(props, state)); - return result.current; - }; - - beforeEach(() => { - state.value = 1; - state.ref = {current: 1}; - state.onChange = setValue; - state.onIncrement = onIncrement; - state.onDecrement = onDecrement; - props.onPrevious = onPrevious; - props.onNext = onNext; - }); - - afterEach(() => { - preventDefault.mockClear(); - setValue.mockClear(); - onPrevious.mockClear(); - onNext.mockClear(); - onIncrement.mockClear(); - onDecrement.mockClear(); - state.value = 1; - state.ref = {}; - }); - - it('handles defaults', () => { - let paginationProps = renderPaginationHook({defaultValue: 1}); - expect(typeof paginationProps.prevButtonProps.onPress).toBe('function'); - expect(typeof paginationProps.nextButtonProps.onPress).toBe('function'); - expect(typeof paginationProps.textProps.onKeyDown).toBe('function'); - }); - - it('handles aria props', function () { - let paginationProps = renderPaginationHook({defaultValue: 1}); - expect(paginationProps.prevButtonProps['aria-label']).toEqual('Previous'); - expect(paginationProps.nextButtonProps['aria-label']).toBe('Next'); - }); - - it('handles valid onkeydown', function () { - let paginationProps = renderPaginationHook({defaultValue: 1, maxValue: maxValue}); - paginationProps.textProps.onKeyDown(event('Up')); - expect(state.onIncrement).toHaveBeenCalled(); - }); - - it('handles invalid onkeydown : value <= 1', function () { - let paginationProps = renderPaginationHook({defaultValue: 1, maxValue: maxValue}); - paginationProps.textProps.onKeyDown(event('Down')); - expect(state.onDecrement).toHaveBeenCalled(); - }); - - it('handles invalid onkeydown : value is a character', function () { - let paginationProps = renderPaginationHook({defaultValue: 1, maxValue: maxValue}); - paginationProps.textProps.onKeyDown(event('a')); - expect(state.onChange).not.toHaveBeenCalled(); - }); - - it('handles valid previous', function () { - state.value = 2; - let paginationProps = renderPaginationHook({defaultValue: 1, maxValue: maxValue}); - paginationProps.prevButtonProps.onPress(event('click')); - expect(state.onDecrement).toHaveBeenCalled(); - expect(props.onPrevious).toHaveBeenCalledWith(state.ref.current); - }); - - it('handles invalid previous', function () { - let paginationProps = renderPaginationHook({defaultValue: 1, maxValue: maxValue}); - paginationProps.prevButtonProps.onPress(event('click')); - expect(state.onDecrement).toHaveBeenCalled(); - expect(props.onPrevious).toHaveBeenCalledWith(state.ref.current); - }); - - it('handles valid next', function () { - let paginationProps = renderPaginationHook({defaultValue: 1, maxValue: maxValue}); - paginationProps.nextButtonProps.onPress(event('click')); - expect(state.onIncrement).toHaveBeenCalled(); - expect(props.onNext).toHaveBeenCalledWith(state.ref.current); - }); - - it('handles invalid next', function () { - state.value = maxValue; - let paginationProps = renderPaginationHook({defaultValue: 1, maxValue: maxValue}); - paginationProps.nextButtonProps.onPress(event('click')); - expect(state.onIncrement).toHaveBeenCalled(); - expect(props.onNext).toHaveBeenCalledWith(state.ref.current); - }); -}); diff --git a/packages/@react-aria/select/src/HiddenSelect.tsx b/packages/@react-aria/select/src/HiddenSelect.tsx index f2641c58e4d..f0715e01b6a 100644 --- a/packages/@react-aria/select/src/HiddenSelect.tsx +++ b/packages/@react-aria/select/src/HiddenSelect.tsx @@ -72,7 +72,7 @@ export function useHiddenSelect(props: AriaHiddenSelectOptions, state: Select useFormReset(props.selectRef, state.selectedKey, state.setSelectedKey); useFormValidation({ validationBehavior, - focus: () => triggerRef.current.focus() + focus: () => triggerRef.current?.focus() }, state, props.selectRef); // In Safari, the } @@ -186,7 +188,7 @@ const ComboBoxBase = React.forwardRef(function ComboBoxBase(pr {...listBoxProps} ref={listBoxRef} disallowEmptySelection - autoFocus={state.focusStrategy} + autoFocus={state.focusStrategy ?? undefined} shouldSelectOnPressUp focusOnPointerEnter layout={layout} @@ -215,7 +217,7 @@ interface ComboBoxInputProps extends SpectrumComboBoxProps { isOpen?: boolean } -const ComboBoxInput = React.forwardRef(function ComboBoxInput(props: ComboBoxInputProps, ref: RefObject) { +const ComboBoxInput = React.forwardRef(function ComboBoxInput(props: ComboBoxInputProps, ref: ForwardedRef) { let { isQuiet, isDisabled, @@ -233,7 +235,7 @@ const ComboBoxInput = React.forwardRef(function ComboBoxInput(props: ComboBoxInp } = props; let {hoverProps, isHovered} = useHover({}); let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/combobox'); - let timeout = useRef(null); + let timeout = useRef | null>(null); let [showLoading, setShowLoading] = useState(false); let loadingCircle = ( @@ -272,7 +274,9 @@ const ComboBoxInput = React.forwardRef(function ComboBoxInput(props: ComboBoxInp } else if (!isLoading) { // If loading is no longer happening, clear any timers and hide the loading circle setShowLoading(false); - clearTimeout(timeout.current); + if (timeout.current) { + clearTimeout(timeout.current); + } timeout.current = null; } @@ -281,7 +285,9 @@ const ComboBoxInput = React.forwardRef(function ComboBoxInput(props: ComboBoxInp useEffect(() => { return () => { - clearTimeout(timeout.current); + if (timeout.current) { + clearTimeout(timeout.current); + } timeout.current = null; }; }, []); @@ -337,7 +343,7 @@ const ComboBoxInput = React.forwardRef(function ComboBoxInput(props: ComboBoxInp // loading circle should only be displayed if menu is open, if menuTrigger is "manual", or first time load (to stop circle from showing up when user selects an option) // TODO: add special case for completionMode: complete as well isLoading={showLoading && (isOpen || menuTrigger === 'manual' || loadingState === 'loading')} - loadingIndicator={loadingState != null && loadingCircle} + loadingIndicator={loadingState != null ? loadingCircle : undefined} disableFocusRing /> (props: SpectrumComboBoxProps, ref: FocusableRef) { +export const MobileComboBox = React.forwardRef(function MobileComboBox(props: SpectrumComboBoxProps, ref: FocusableRef) { props = useProviderProps(props); let { @@ -73,17 +73,17 @@ export const MobileComboBox = React.forwardRef(function MobileComboBox(undefined); + let buttonRef = useRef(null); let domRef = useFocusableRef(ref, buttonRef); let {triggerProps, overlayProps} = useOverlayTrigger({type: 'listbox'}, state, buttonRef); let inputRef = useRef(null); useFormValidation({ ...props, - focus: () => buttonRef.current.focus() + focus: () => buttonRef.current?.focus() }, state, inputRef); let {isInvalid, validationErrors, validationDetails} = state.displayValidation; - let validationState = props.validationState || (isInvalid ? 'invalid' : null); + let validationState = props.validationState || (isInvalid ? 'invalid' : undefined); let errorMessage = props.errorMessage ?? validationErrors.join(' '); let {labelProps, fieldProps, descriptionProps, errorMessageProps} = useField({ @@ -96,7 +96,7 @@ export const MobileComboBox = React.forwardRef(function MobileComboBox { if (!props.isDisabled) { - buttonRef.current.focus(); + buttonRef.current?.focus(); setInteractionModality('keyboard'); } }; @@ -104,7 +104,7 @@ export const MobileComboBox = React.forwardRef(function MobileComboBox = { type: 'hidden', name, - value: formValue === 'text' ? state.inputValue : state.selectedKey + value: formValue === 'text' ? state.inputValue : String(state.selectedKey) }; if (validationBehavior === 'native') { @@ -117,7 +117,7 @@ export const MobileComboBox = React.forwardRef(function MobileComboBox {}; } - useFormReset(inputRef, inputProps.value, formValue === 'text' ? state.setInputValue : state.setSelectedKey); + useFormReset(inputRef, String(inputProps.value ?? ''), formValue === 'text' ? state.setInputValue : state.setSelectedKey); return ( <> @@ -166,7 +166,7 @@ interface ComboBoxButtonProps extends AriaButtonProps { className?: string } -const ComboBoxButton = React.forwardRef(function ComboBoxButton(props: ComboBoxButtonProps, ref: RefObject) { +export const ComboBoxButton = React.forwardRef(function ComboBoxButton(props: ComboBoxButtonProps, ref: ForwardedRef) { let { isQuiet, isDisabled, @@ -194,6 +194,7 @@ const ComboBoxButton = React.forwardRef(function ComboBoxButton(props: ComboBoxB ) }); + let objRef = useObjectRef(ref); let {hoverProps, isHovered} = useHover({}); let {buttonProps, isPressed} = useButton({ ...props, @@ -204,7 +205,7 @@ const ComboBoxButton = React.forwardRef(function ComboBoxButton(props: ComboBoxB validationState === 'invalid' ? invalidId : null ].filter(Boolean).join(' '), elementType: 'div' - }, ref); + }, objRef); return ( (} + ref={objRef} style={{...style, outline: 'none'}} className={ classNames( @@ -307,8 +308,8 @@ const ComboBoxButton = React.forwardRef(function ComboBoxButton(props: ComboBoxB ); }); -interface ComboBoxTrayProps extends SpectrumComboBoxProps { - state: ComboBoxState, +interface ComboBoxTrayProps extends SpectrumComboBoxProps { + state: ComboBoxState, overlayProps: HTMLAttributes, loadingIndicator?: ReactElement, onClose: () => void @@ -327,12 +328,12 @@ function ComboBoxTray(props: ComboBoxTrayProps) { onClose } = props; - let timeout = useRef(null); + let timeout = useRef | null>(null); let [showLoading, setShowLoading] = useState(false); - let inputRef = useRef(undefined); - let buttonRef = useRef>(undefined); - let popoverRef = useRef(undefined); - let listBoxRef = useRef(undefined); + let inputRef = useRef(null); + let buttonRef = useRef>(null); + let popoverRef = useRef(null); + let listBoxRef = useRef(null); let isLoading = loadingState === 'loading' || loadingState === 'loadingMore'; let layout = useListBoxLayout(); let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/combobox'); @@ -353,7 +354,9 @@ function ComboBoxTray(props: ComboBoxTrayProps) { ); React.useEffect(() => { - focusSafely(inputRef.current); + if (inputRef.current) { + focusSafely(inputRef.current); + } }, []); React.useEffect(() => { @@ -388,7 +391,7 @@ function ComboBoxTray(props: ComboBoxTrayProps) { excludeFromTabOrder onPress={() => { state.setInputValue(''); - inputRef.current.focus(); + inputRef.current?.focus(); }} UNSAFE_className={ classNames( @@ -431,7 +434,7 @@ function ComboBoxTray(props: ComboBoxTrayProps) { return; } - popoverRef.current.focus(); + popoverRef.current?.focus(); }, [inputRef, popoverRef, isTouchDown]); let inputValue = inputProps.value; @@ -454,7 +457,9 @@ function ComboBoxTray(props: ComboBoxTrayProps) { } else if (loadingState !== 'filtering') { // If loading is no longer happening, clear any timers and hide the loading circle setShowLoading(false); - clearTimeout(timeout.current); + if (timeout.current) { + clearTimeout(timeout.current); + } timeout.current = null; } @@ -464,9 +469,9 @@ function ComboBoxTray(props: ComboBoxTrayProps) { let onKeyDown = (e) => { // Close virtual keyboard if user hits Enter w/o any focused options if (e.key === 'Enter' && state.selectionManager.focusedKey == null) { - popoverRef.current.focus(); + popoverRef.current?.focus(); } else { - inputProps.onKeyDown(e); + inputProps.onKeyDown?.(e); } }; @@ -489,11 +494,11 @@ function ComboBoxTray(props: ComboBoxTrayProps) { inputRef={inputRef} isDisabled={isDisabled} isLoading={showLoading && loadingState === 'filtering'} - loadingIndicator={loadingState != null && loadingCircle} + loadingIndicator={loadingState != null ? loadingCircle : undefined} validationState={validationState} labelAlign="start" labelPosition="top" - wrapperChildren={(state.inputValue !== '' || loadingState === 'filtering' || validationState != null) && !props.isReadOnly && clearButton} + wrapperChildren={(state.inputValue !== '' || loadingState === 'filtering' || validationState != null) && !props.isReadOnly ? clearButton : undefined} UNSAFE_className={ classNames( searchStyles, diff --git a/packages/@react-spectrum/combobox/stories/ComboBox.stories.tsx b/packages/@react-spectrum/combobox/stories/ComboBox.stories.tsx index 69437d2e7f4..7d46f8504c5 100644 --- a/packages/@react-spectrum/combobox/stories/ComboBox.stories.tsx +++ b/packages/@react-spectrum/combobox/stories/ComboBox.stories.tsx @@ -54,7 +54,7 @@ let withSection = [ let lotsOfSections: any[] = []; for (let i = 0; i < 50; i++) { - let children = []; + let children: {name: string}[] = []; for (let j = 0; j < 50; j++) { children.push({name: `Section ${i}, Item ${j}`}); } @@ -605,12 +605,12 @@ function AsyncLoadingExampleControlledKey(props) { let onSelectionChange = (key) => { let itemText = list.getItem(key)?.name; list.setSelectedKeys(new Set([key])); - list.setFilterText(itemText); + list.setFilterText(itemText ?? ''); }; let onInputChange = (value) => { if (value === '') { - list.setSelectedKeys(new Set([null])); + list.setSelectedKeys(new Set()); } list.setFilterText(value); }; @@ -673,12 +673,12 @@ function AsyncLoadingExampleControlledKeyWithReset(props) { let onSelectionChange = (key) => { let itemText = list.getItem(key)?.name; list.setSelectedKeys(new Set([key])); - list.setFilterText(itemText); + list.setFilterText(itemText ?? ''); }; let onInputChange = (value) => { if (value === '') { - list.setSelectedKeys(new Set([null])); + list.setSelectedKeys(new Set()); } list.setFilterText(value); }; diff --git a/packages/@react-spectrum/datepicker/src/DateField.tsx b/packages/@react-spectrum/datepicker/src/DateField.tsx index 06b3c35117b..de30373ce53 100644 --- a/packages/@react-spectrum/datepicker/src/DateField.tsx +++ b/packages/@react-spectrum/datepicker/src/DateField.tsx @@ -45,8 +45,8 @@ function DateField(props: SpectrumDateFieldProps, ref: F createCalendar }); - let fieldRef = useRef(null); - let inputRef = useRef(null); + let fieldRef = useRef(null); + let inputRef = useRef(null); let {labelProps, fieldProps, inputProps, descriptionProps, errorMessageProps, isInvalid, validationErrors, validationDetails} = useDateField({ ...props, inputRef @@ -56,7 +56,7 @@ function DateField(props: SpectrumDateFieldProps, ref: F // The format help text is unnecessary for screen reader users because each segment already has a label. let description = useFormatHelpText(props); if (description && !props.description) { - descriptionProps.id = null; + descriptionProps.id = undefined; } let validationState = state.validationState || (isInvalid ? 'invalid' : null); @@ -72,7 +72,7 @@ function DateField(props: SpectrumDateFieldProps, ref: F labelProps={labelProps} descriptionProps={descriptionProps} errorMessageProps={errorMessageProps} - validationState={validationState} + validationState={validationState ?? undefined} isInvalid={isInvalid} validationErrors={validationErrors} validationDetails={validationDetails} diff --git a/packages/@react-spectrum/datepicker/src/DatePicker.tsx b/packages/@react-spectrum/datepicker/src/DatePicker.tsx index b298e62203b..a81334f1424 100644 --- a/packages/@react-spectrum/datepicker/src/DatePicker.tsx +++ b/packages/@react-spectrum/datepicker/src/DatePicker.tsx @@ -50,7 +50,7 @@ function DatePicker(props: SpectrumDatePickerProps, ref: pageBehavior } = props; let {hoverProps, isHovered} = useHover({isDisabled}); - let targetRef = useRef(undefined); + let targetRef = useRef(null); let state = useDatePickerState({ ...props, shouldCloseOnSelect: () => !state.hasTime @@ -99,10 +99,10 @@ function DatePicker(props: SpectrumDatePickerProps, ref: // The format help text is unnecessary for screen reader users because each segment already has a label. let description = useFormatHelpText(props); if (description && !props.description) { - descriptionProps.id = null; + descriptionProps.id = undefined; } - let placeholder: DateValue = placeholderValue; + let placeholder: DateValue | null | undefined = placeholderValue; let timePlaceholder = placeholder && 'hour' in placeholder ? placeholder : null; let timeMinValue = props.minValue && 'hour' in props.minValue ? props.minValue : null; let timeMaxValue = props.maxValue && 'hour' in props.maxValue ? props.maxValue : null; diff --git a/packages/@react-spectrum/datepicker/src/DatePickerField.tsx b/packages/@react-spectrum/datepicker/src/DatePickerField.tsx index b87dfad5d42..210bfd21324 100644 --- a/packages/@react-spectrum/datepicker/src/DatePickerField.tsx +++ b/packages/@react-spectrum/datepicker/src/DatePickerField.tsx @@ -33,7 +33,7 @@ export function DatePickerField(props: DatePickerFieldProps isRequired, inputClassName } = props; - let ref = useRef(undefined); + let ref = useRef(null); let {locale} = useLocale(); let state = useDateFieldState({ ...props, @@ -41,7 +41,7 @@ export function DatePickerField(props: DatePickerFieldProps createCalendar }); - let inputRef = useRef(undefined); + let inputRef = useRef(null); let {fieldProps, inputProps} = useDateField({...props, inputRef}, state, ref); return ( diff --git a/packages/@react-spectrum/datepicker/src/DatePickerSegment.tsx b/packages/@react-spectrum/datepicker/src/DatePickerSegment.tsx index 36147357f9e..82f43c90e0f 100644 --- a/packages/@react-spectrum/datepicker/src/DatePickerSegment.tsx +++ b/packages/@react-spectrum/datepicker/src/DatePickerSegment.tsx @@ -50,7 +50,7 @@ function LiteralSegment({segment}: LiteralSegmentProps) { } function EditableSegment({segment, state}: DatePickerSegmentProps) { - let ref = useRef(undefined); + let ref = useRef(null); let {segmentProps} = useDateSegment(segment, state, ref); return ( diff --git a/packages/@react-spectrum/datepicker/src/DateRangePicker.tsx b/packages/@react-spectrum/datepicker/src/DateRangePicker.tsx index 11143195d19..9b8dc42ba2f 100644 --- a/packages/@react-spectrum/datepicker/src/DateRangePicker.tsx +++ b/packages/@react-spectrum/datepicker/src/DateRangePicker.tsx @@ -50,7 +50,7 @@ function DateRangePicker(props: SpectrumDateRangePickerProp pageBehavior } = props; let {hoverProps, isHovered} = useHover({isDisabled}); - let targetRef = useRef(undefined); + let targetRef = useRef(null); let state = useDateRangePickerState({ ...props, shouldCloseOnSelect: () => !state.hasTime @@ -99,10 +99,10 @@ function DateRangePicker(props: SpectrumDateRangePickerProp // The format help text is unnecessary for screen reader users because each segment already has a label. let description = useFormatHelpText(props); if (description && !props.description) { - descriptionProps.id = null; + descriptionProps.id = undefined; } - let placeholder: DateValue = placeholderValue; + let placeholder: DateValue | null | undefined = placeholderValue; let timePlaceholder = placeholder && 'hour' in placeholder ? placeholder : null; let timeMinValue = props.minValue && 'hour' in props.minValue ? props.minValue : null; let timeMaxValue = props.maxValue && 'hour' in props.maxValue ? props.maxValue : null; diff --git a/packages/@react-spectrum/datepicker/src/Input.tsx b/packages/@react-spectrum/datepicker/src/Input.tsx index 3d97c5870dc..fc44e36da54 100644 --- a/packages/@react-spectrum/datepicker/src/Input.tsx +++ b/packages/@react-spectrum/datepicker/src/Input.tsx @@ -15,12 +15,12 @@ import Checkmark from '@spectrum-icons/ui/CheckmarkMedium'; import {classNames, useValueEffect} from '@react-spectrum/utils'; import datepickerStyles from './styles.css'; import {mergeProps, mergeRefs, useEvent, useLayoutEffect, useResizeObserver} from '@react-aria/utils'; -import React, {useCallback, useRef} from 'react'; +import React, {ReactElement, useCallback, useRef} from 'react'; import textfieldStyles from '@adobe/spectrum-css-temp/components/textfield/vars.css'; import {useFocusRing} from '@react-aria/focus'; function Input(props, ref) { - let inputRef = useRef(null); + let inputRef = useRef(null); let { isDisabled, isQuiet, @@ -38,7 +38,7 @@ function Input(props, ref) { // not cause a layout shift. let [reservePadding, setReservePadding] = useValueEffect(false); let onResize = useCallback(() => setReservePadding(function *(reservePadding) { - if (inputRef.current) { + if (inputRef.current && inputRef.current.parentElement) { if (reservePadding) { // Try to collapse padding if the content is clipped. if (inputRef.current.scrollWidth > inputRef.current.offsetWidth) { @@ -114,7 +114,7 @@ function Input(props, ref) { 'spectrum-Textfield-validationIcon' ); - let validationIcon = null; + let validationIcon: ReactElement | null = null; if (validationState === 'invalid' && !isDisabled) { validationIcon = ; } else if (validationState === 'valid' && !isDisabled) { diff --git a/packages/@react-spectrum/datepicker/src/TimeField.tsx b/packages/@react-spectrum/datepicker/src/TimeField.tsx index 5d2471ea857..428f4363451 100644 --- a/packages/@react-spectrum/datepicker/src/TimeField.tsx +++ b/packages/@react-spectrum/datepicker/src/TimeField.tsx @@ -43,8 +43,8 @@ function TimeField(props: SpectrumTimeFieldProps, ref: F locale }); - let fieldRef = useRef(null); - let inputRef = useRef(null); + let fieldRef = useRef(null); + let inputRef = useRef(null); let {labelProps, fieldProps, inputProps, descriptionProps, errorMessageProps, isInvalid, validationErrors, validationDetails} = useTimeField({ ...props, inputRef @@ -62,7 +62,7 @@ function TimeField(props: SpectrumTimeFieldProps, ref: F labelProps={labelProps} descriptionProps={descriptionProps} errorMessageProps={errorMessageProps} - validationState={validationState} + validationState={validationState ?? undefined} isInvalid={isInvalid} validationErrors={validationErrors} validationDetails={validationDetails} diff --git a/packages/@react-spectrum/datepicker/src/utils.ts b/packages/@react-spectrum/datepicker/src/utils.ts index 715213ac0b7..9e9fad08211 100644 --- a/packages/@react-spectrum/datepicker/src/utils.ts +++ b/packages/@react-spectrum/datepicker/src/utils.ts @@ -42,7 +42,7 @@ export function useFormatHelpText(props: Pick, 'desc } export function useVisibleMonths(maxVisibleMonths: number) { - let {scale} = useProvider(); + let {scale} = useProvider()!; let [visibleMonths, setVisibleMonths] = useState(getVisibleMonths(scale)); useLayoutEffect(() => { let onResize = () => setVisibleMonths(getVisibleMonths(scale)); @@ -68,7 +68,7 @@ function getVisibleMonths(scale) { } export function useFocusManagerRef(ref: FocusableRef) { - let domRef = useRef(undefined); + let domRef = useRef(null); useImperativeHandle(ref, () => ({ ...createDOMRef(domRef), focus() { diff --git a/packages/@react-spectrum/datepicker/stories/DateField.stories.tsx b/packages/@react-spectrum/datepicker/stories/DateField.stories.tsx index a822f1104d0..59a91141b06 100644 --- a/packages/@react-spectrum/datepicker/stories/DateField.stories.tsx +++ b/packages/@react-spectrum/datepicker/stories/DateField.stories.tsx @@ -314,12 +314,12 @@ function Example(props) { let {locale: defaultLocale} = useLocale(); let pref = preferences.find(p => p.locale === locale); - let preferredCalendars = React.useMemo(() => pref ? pref.ordering.split(' ').map(p => calendars.find(c => c.key === p)).filter(Boolean) : [calendars[0]], [pref]); + let preferredCalendars = React.useMemo(() => pref ? pref.ordering.split(' ').map(p => calendars.find(c => c.key === p)).filter(v => v != null) : [calendars[0]], [pref]); let otherCalendars = React.useMemo(() => calendars.filter(c => !preferredCalendars.some(p => p.key === c.key)), [preferredCalendars]); let updateLocale = locale => { setLocale(locale); - let pref = preferences.find(p => p.locale === locale); + let pref = preferences.find(p => p.locale === locale)!; setCalendar(pref.ordering.split(' ')[0]); }; diff --git a/packages/@react-spectrum/datepicker/stories/DatePicker.stories.tsx b/packages/@react-spectrum/datepicker/stories/DatePicker.stories.tsx index c26c70ac3ef..58740157c8c 100644 --- a/packages/@react-spectrum/datepicker/stories/DatePicker.stories.tsx +++ b/packages/@react-spectrum/datepicker/stories/DatePicker.stories.tsx @@ -332,12 +332,12 @@ function Example(props) { let {locale: defaultLocale} = useLocale(); let pref = preferences.find(p => p.locale === locale); - let preferredCalendars = React.useMemo(() => pref ? pref.ordering.split(' ').map(p => calendars.find(c => c.key === p)).filter(Boolean) : [calendars[0]], [pref]); + let preferredCalendars = React.useMemo(() => pref ? pref.ordering.split(' ').map(p => calendars.find(c => c.key === p)).filter(v => v != null) : [calendars[0]], [pref]); let otherCalendars = React.useMemo(() => calendars.filter(c => !preferredCalendars.some(p => p.key === c.key)), [preferredCalendars]); let updateLocale = locale => { setLocale(locale); - let pref = preferences.find(p => p.locale === locale); + let pref = preferences.find(p => p.locale === locale)!; setCalendar(pref.ordering.split(' ')[0]); }; diff --git a/packages/@react-spectrum/datepicker/stories/DateRangePicker.stories.tsx b/packages/@react-spectrum/datepicker/stories/DateRangePicker.stories.tsx index 37d1c0d8d81..db97cd19936 100644 --- a/packages/@react-spectrum/datepicker/stories/DateRangePicker.stories.tsx +++ b/packages/@react-spectrum/datepicker/stories/DateRangePicker.stories.tsx @@ -14,6 +14,7 @@ import {action} from '@storybook/addon-actions'; import {ActionButton} from '@react-spectrum/button'; import {CalendarDate, getLocalTimeZone, isWeekend, parseDate, today, toZoned} from '@internationalized/date'; import {chain} from '@react-aria/utils'; +import {DateRange} from '@react-types/datepicker'; import {DateRangePicker} from '../'; import {DateValue} from '@react-types/calendar'; import {Flex} from '@react-spectrum/layout'; @@ -129,7 +130,7 @@ MinDate201011MaxDate202011.story = { export const IsDateUnavailable = () => { const disabledRanges = [[today(getLocalTimeZone()), today(getLocalTimeZone()).add({weeks: 1})], [today(getLocalTimeZone()).add({weeks: 2}), today(getLocalTimeZone()).add({weeks: 3})]]; - let [value, setValue] = React.useState(null); + 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) => disabledRanges.some((interval) => date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0); return render({ @@ -238,12 +239,12 @@ function Example(props) { let {locale: defaultLocale} = useLocale(); let pref = preferences.find(p => p.locale === locale); - let preferredCalendars = React.useMemo(() => pref ? pref.ordering.split(' ').map(p => calendars.find(c => c.key === p)).filter(Boolean) : [calendars[0]], [pref]); + let preferredCalendars = React.useMemo(() => pref ? pref.ordering.split(' ').map(p => calendars.find(c => c.key === p)).filter(v => v != null) : [calendars[0]], [pref]); let otherCalendars = React.useMemo(() => calendars.filter(c => !preferredCalendars.some(p => p.key === c.key)), [preferredCalendars]); let updateLocale = locale => { setLocale(locale); - let pref = preferences.find(p => p.locale === locale); + let pref = preferences.find(p => p.locale === locale)!; setCalendar(pref.ordering.split(' ')[0]); }; @@ -270,7 +271,7 @@ function Example(props) { } function ControlledExample(props) { - let [value, setValue] = React.useState(null); + let [value, setValue] = React.useState(null); return ( diff --git a/packages/@react-spectrum/dnd/src/useDragAndDrop.ts b/packages/@react-spectrum/dnd/src/useDragAndDrop.ts index 6ce00d057c5..73f62ef32bb 100644 --- a/packages/@react-spectrum/dnd/src/useDragAndDrop.ts +++ b/packages/@react-spectrum/dnd/src/useDragAndDrop.ts @@ -90,8 +90,8 @@ export function useDragAndDrop(options: DragAndDropOptions): DragAndDropHooks { let hooks = {} as DragHooks & DropHooks & {isVirtualDragging?: () => boolean, renderPreview?: (keys: Set, draggedKey: Key) => JSX.Element}; if (isDraggable) { - hooks.useDraggableCollectionState = function useDraggableCollectionStateOverride(props: DraggableCollectionStateOptions) { - return useDraggableCollectionState({...props, ...options}); + hooks.useDraggableCollectionState = function useDraggableCollectionStateOverride(props: DraggableCollectionStateOpts) { + return useDraggableCollectionState({...props, ...options, getItems: options.getItems!}); }; hooks.useDraggableCollection = useDraggableCollection; hooks.useDraggableItem = useDraggableItem; diff --git a/packages/@react-spectrum/form/stories/Form.stories.tsx b/packages/@react-spectrum/form/stories/Form.stories.tsx index 46d902b2ede..131f5592b4f 100644 --- a/packages/@react-spectrum/form/stories/Form.stories.tsx +++ b/packages/@react-spectrum/form/stories/Form.stories.tsx @@ -484,7 +484,7 @@ function FormWithControls(props: any = {}) { let [favoritePet, setFavoritePet] = useState('cats'); let [favoriteColor, setFavoriteColor] = useState('green' as Key); let [howIFeel, setHowIFeel] = useState('I feel good, o I feel so good!'); - let [birthday, setBirthday] = useState(new CalendarDate(1732, 2, 22)); + let [birthday, setBirthday] = useState(new CalendarDate(1732, 2, 22)); let [money, setMoney] = useState(50); let [superSpeed, setSuperSpeed] = useState(true); diff --git a/packages/@react-spectrum/icon/src/Icon.tsx b/packages/@react-spectrum/icon/src/Icon.tsx index 88474217bfc..6eefcbb42fd 100644 --- a/packages/@react-spectrum/icon/src/Icon.tsx +++ b/packages/@react-spectrum/icon/src/Icon.tsx @@ -12,10 +12,10 @@ import {AriaLabelingProps, DOMProps, IconColorValue, StyleProps} from '@react-types/shared'; import {baseStyleProps, classNames, StyleHandlers, useSlotProps, useStyleProps} from '@react-spectrum/utils'; +import {Context} from '@react-spectrum/provider'; import {filterDOMProps} from '@react-aria/utils'; -import React, {ReactElement} from 'react'; +import React, {ReactElement, useContext} from 'react'; import styles from '@adobe/spectrum-css-temp/components/icon/vars.css'; -import {useProvider} from '@react-spectrum/provider'; export interface IconProps extends DOMProps, AriaLabelingProps, StyleProps { /** @@ -70,7 +70,7 @@ export function Icon(props: IconProps) { } = props; let {styleProps} = useStyleProps(otherProps, iconStyleProps); - let provider = useProvider(); + let provider = useContext(Context); let scale = 'M'; if (provider !== null) { scale = provider.scale === 'large' ? 'L' : 'M'; diff --git a/packages/@react-spectrum/icon/src/UIIcon.tsx b/packages/@react-spectrum/icon/src/UIIcon.tsx index e6d0b59487d..667c1eead0d 100644 --- a/packages/@react-spectrum/icon/src/UIIcon.tsx +++ b/packages/@react-spectrum/icon/src/UIIcon.tsx @@ -12,10 +12,10 @@ import {AriaLabelingProps, DOMProps, StyleProps} from '@react-types/shared'; import {classNames, useSlotProps, useStyleProps} from '@react-spectrum/utils'; +import {Context} from '@react-spectrum/provider'; import {filterDOMProps} from '@react-aria/utils'; -import React, {ReactElement} from 'react'; +import React, {ReactElement, useContext} from 'react'; import styles from '@adobe/spectrum-css-temp/components/icon/vars.css'; -import {useProvider} from '@react-spectrum/provider'; export interface UIIconProps extends DOMProps, AriaLabelingProps, StyleProps { children: ReactElement, @@ -38,7 +38,7 @@ export function UIIcon(props: UIIconProps) { } = props; let {styleProps} = useStyleProps(otherProps); - let provider = useProvider(); + let provider = useContext(Context); let scale = 'M'; if (provider !== null) { scale = provider.scale === 'large' ? 'L' : 'M'; diff --git a/packages/@react-spectrum/layout/chromatic/Flex.stories.tsx b/packages/@react-spectrum/layout/chromatic/Flex.stories.tsx index 2b4386f94b2..6fe16d710e5 100644 --- a/packages/@react-spectrum/layout/chromatic/Flex.stories.tsx +++ b/packages/@react-spectrum/layout/chromatic/Flex.stories.tsx @@ -10,6 +10,7 @@ * governing permissions and limitations under the License. */ +import {BackgroundColorValue, Responsive} from '@react-types/shared'; import {Flex} from '@react-spectrum/layout'; import React from 'react'; import {View} from '@react-spectrum/view'; @@ -28,10 +29,10 @@ let baseColors = [ 'green', 'blue' ]; -let colors = []; +let colors: Array | undefined> = []; for (let color of baseColors) { for (let i = 4; i <= 7; i++) { - colors.push(`${color}-${i}00`); + colors.push(`${color}-${i}00` as Responsive); } } @@ -71,7 +72,7 @@ export const WrappingWithGap = () => ( {colors.map((color) => ( - + ))} diff --git a/packages/@react-spectrum/layout/chromatic/Grid.stories.tsx b/packages/@react-spectrum/layout/chromatic/Grid.stories.tsx index d39fbf7b49b..ce1aee081fe 100644 --- a/packages/@react-spectrum/layout/chromatic/Grid.stories.tsx +++ b/packages/@react-spectrum/layout/chromatic/Grid.stories.tsx @@ -10,6 +10,7 @@ * governing permissions and limitations under the License. */ +import {BackgroundColorValue, Responsive} from '@react-types/shared'; import {Grid, repeat} from '@react-spectrum/layout'; import React from 'react'; import {View} from '@react-spectrum/view'; @@ -28,10 +29,10 @@ let baseColors = [ 'green', 'blue' ]; -let colors = []; +let colors: Array | undefined> = []; for (let color of baseColors) { for (let i = 4; i <= 7; i++) { - colors.push(`${color}-${i}00`); + colors.push(`${color}-${i}00` as Responsive); } } @@ -78,7 +79,7 @@ export const ImplicitGrid = () => ( width="80%" gap="size-100"> {colors.map((color) => ( - + ))} ); diff --git a/packages/@react-spectrum/layout/src/Grid.tsx b/packages/@react-spectrum/layout/src/Grid.tsx index b6cdd13b6ce..497c64473b2 100644 --- a/packages/@react-spectrum/layout/src/Grid.tsx +++ b/packages/@react-spectrum/layout/src/Grid.tsx @@ -46,7 +46,9 @@ function Grid(props: GridProps, ref: DOMRef) { ...otherProps } = props; let {styleProps} = useStyleProps(otherProps, gridStyleProps); - styleProps.style.display = 'grid'; // inline-grid? + if (styleProps.style) { + styleProps.style.display = 'grid'; // inline-grid? + } let domRef = useDOMRef(ref); return ( diff --git a/packages/@react-spectrum/layout/stories/Flex.stories.tsx b/packages/@react-spectrum/layout/stories/Flex.stories.tsx index 7825b503deb..5c2316cabf5 100644 --- a/packages/@react-spectrum/layout/stories/Flex.stories.tsx +++ b/packages/@react-spectrum/layout/stories/Flex.stories.tsx @@ -10,6 +10,7 @@ * governing permissions and limitations under the License. */ +import {BackgroundColorValue, Responsive as TResponsive} from '@react-types/shared'; import {Flex} from '@react-spectrum/layout'; import React from 'react'; import {View} from '@react-spectrum/view'; @@ -28,10 +29,10 @@ let baseColors = [ 'green', 'blue' ]; -let colors = []; +let colors: Array | undefined> = []; for (let color of baseColors) { for (let i = 4; i <= 7; i++) { - colors.push(`${color}-${i}00`); + colors.push(`${color}-${i}00` as TResponsive); } } @@ -67,7 +68,7 @@ export const WrappingWithGap = () => ( {colors.map((color) => ( - + ))} diff --git a/packages/@react-spectrum/layout/stories/Grid.stories.tsx b/packages/@react-spectrum/layout/stories/Grid.stories.tsx index fc6d3a8e2f8..4488255682a 100644 --- a/packages/@react-spectrum/layout/stories/Grid.stories.tsx +++ b/packages/@react-spectrum/layout/stories/Grid.stories.tsx @@ -10,6 +10,7 @@ * governing permissions and limitations under the License. */ +import {BackgroundColorValue, Responsive as TResponsive} from '@react-types/shared'; import {Grid, repeat} from '@react-spectrum/layout'; import React from 'react'; import {View} from '@react-spectrum/view'; @@ -28,10 +29,10 @@ let baseColors = [ 'green', 'blue' ]; -let colors = []; +let colors: Array | undefined> = []; for (let color of baseColors) { for (let i = 4; i <= 7; i++) { - colors.push(`${color}-${i}00`); + colors.push(`${color}-${i}00` as TResponsive); } } @@ -74,7 +75,7 @@ export const ImplicitGrid = () => ( width="80%" gap="size-100"> {colors.map((color) => ( - + ))} ); @@ -95,7 +96,7 @@ export const Responsive = () => ( width="80%" gap={{base: 'size-100', M: 'size-250', L: 'size-350'}}> {colors.map((color) => ( - + ))} ); diff --git a/packages/@react-spectrum/list/src/InsertionIndicator.tsx b/packages/@react-spectrum/list/src/InsertionIndicator.tsx index 4bb068e8cda..f5dde8de1c7 100644 --- a/packages/@react-spectrum/list/src/InsertionIndicator.tsx +++ b/packages/@react-spectrum/list/src/InsertionIndicator.tsx @@ -11,14 +11,14 @@ interface InsertionIndicatorProps { } export default function InsertionIndicator(props: InsertionIndicatorProps) { - let {dropState, dragAndDropHooks} = useContext(ListViewContext); + let {dropState, dragAndDropHooks} = useContext(ListViewContext)!; const {target, isPresentationOnly} = props; - let ref = useRef(undefined); - let {dropIndicatorProps} = dragAndDropHooks.useDropIndicator(props, dropState, ref); + let ref = useRef(null); + let {dropIndicatorProps} = dragAndDropHooks!.useDropIndicator!(props, dropState!, ref); let {visuallyHiddenProps} = useVisuallyHidden(); - let isDropTarget = dropState.isDropTarget(target); + let isDropTarget = dropState!.isDropTarget(target); if (!isDropTarget && dropIndicatorProps['aria-hidden']) { return null; diff --git a/packages/@react-spectrum/list/src/ListView.tsx b/packages/@react-spectrum/list/src/ListView.tsx index e9aa0040cb1..d56f2a0997f 100644 --- a/packages/@react-spectrum/list/src/ListView.tsx +++ b/packages/@react-spectrum/list/src/ListView.tsx @@ -64,18 +64,18 @@ export interface SpectrumListViewProps extends Omit, 'ke interface ListViewContextValue { state: ListState, - dragState: DraggableCollectionState, - dropState: DroppableCollectionState, - dragAndDropHooks: DragAndDropHooks['dragAndDropHooks'], - onAction:(key: Key) => void, + dragState: DraggableCollectionState | null, + dropState: DroppableCollectionState | null, + dragAndDropHooks?: DragAndDropHooks['dragAndDropHooks'], + onAction?: (key: Key) => void, isListDraggable: boolean, isListDroppable: boolean, layout: ListViewLayout, - loadingState: LoadingState, + loadingState?: LoadingState, renderEmptyState?: () => JSX.Element } -export const ListViewContext = React.createContext>(null); +export const ListViewContext = React.createContext | null>(null); const ROW_HEIGHTS = { compact: { @@ -96,7 +96,7 @@ function useListLayout(state: ListState, density: SpectrumListViewProps let {scale} = useProvider(); let layout = useMemo(() => new ListViewLayout({ - estimatedRowHeight: ROW_HEIGHTS[density][scale] + estimatedRowHeight: ROW_HEIGHTS[density || 'regular'][scale] }) // eslint-disable-next-line react-hooks/exhaustive-deps , [scale, density, overflowMode]); @@ -138,15 +138,15 @@ function ListView(props: SpectrumListViewProps, ref: DOMRef let isLoading = loadingState === 'loading' || loadingState === 'loadingMore'; let {styleProps} = useStyleProps(props); - let dragState: DraggableCollectionState; + let dragState: DraggableCollectionState | null = null; let preview = useRef(null); - if (isListDraggable) { - dragState = dragAndDropHooks.useDraggableCollectionState({ + if (isListDraggable && dragAndDropHooks) { + dragState = dragAndDropHooks.useDraggableCollectionState!({ collection, selectionManager, preview }); - dragAndDropHooks.useDraggableCollection({}, dragState, domRef); + dragAndDropHooks.useDraggableCollection!({}, dragState, domRef); } let layout = useListLayout( state, @@ -155,18 +155,18 @@ function ListView(props: SpectrumListViewProps, ref: DOMRef ); let DragPreview = dragAndDropHooks?.DragPreview; - let dropState: DroppableCollectionState; - let droppableCollection: DroppableCollectionResult; - let isRootDropTarget: boolean; - if (isListDroppable) { - dropState = dragAndDropHooks.useDroppableCollectionState({ + let dropState: DroppableCollectionState | null = null; + let droppableCollection: DroppableCollectionResult | null = null; + let isRootDropTarget = false; + if (isListDroppable && dragAndDropHooks) { + dropState = dragAndDropHooks.useDroppableCollectionState!({ collection, selectionManager }); - droppableCollection = dragAndDropHooks.useDroppableCollection({ + droppableCollection = dragAndDropHooks.useDroppableCollection!({ keyboardDelegate: new ListKeyboardDelegate({ collection, - disabledKeys: dragState?.draggingKeys.size ? null : selectionManager.disabledKeys, + disabledKeys: dragState?.draggingKeys.size ? undefined : selectionManager.disabledKeys, ref: domRef, layoutDelegate: layout }), @@ -216,7 +216,7 @@ function ListView(props: SpectrumListViewProps, ref: DOMRef (props: SpectrumListViewProps, ref: DOMRef layout={layout} layoutOptions={useMemo(() => ({isLoading}), [isLoading])} collection={collection}> - {useCallback((type, item) => { + {useCallback((type, item: Node) => { if (type === 'item') { return ; } else if (type === 'loader') { @@ -259,15 +259,21 @@ function ListView(props: SpectrumListViewProps, ref: DOMRef - {DragPreview && isListDraggable && + {DragPreview && isListDraggable && dragAndDropHooks && dragState && {() => { + if (dragState.draggedKey == null) { + return null; + } if (dragAndDropHooks.renderPreview) { return dragAndDropHooks.renderPreview(dragState.draggingKeys, dragState.draggedKey); } let item = state.collection.getItem(dragState.draggedKey); + if (!item) { + return null; + } let itemCount = dragState.draggingKeys.size; - let itemHeight = layout.getLayoutInfo(dragState.draggedKey).rect.height; + let itemHeight = layout.getLayoutInfo(dragState.draggedKey)?.rect.height ?? 0; return ; }} @@ -277,7 +283,7 @@ function ListView(props: SpectrumListViewProps, ref: DOMRef } function Item({item}: {item: Node}) { - let {isListDroppable, state, onAction} = useContext(ListViewContext); + let {isListDroppable, state, onAction} = useContext(ListViewContext)!; return ( <> {isListDroppable && state.collection.getKeyBefore(item.key) == null && @@ -300,7 +306,7 @@ function Item({item}: {item: Node}) { } function LoadingView() { - let {state} = useContext(ListViewContext); + let {state} = useContext(ListViewContext)!; let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/list'); return ( @@ -312,7 +318,7 @@ function LoadingView() { } function EmptyState() { - let {renderEmptyState} = useContext(ListViewContext); + let {renderEmptyState} = useContext(ListViewContext)!; let emptyState = renderEmptyState ? renderEmptyState() : null; if (emptyState == null) { return null; @@ -326,7 +332,7 @@ function EmptyState() { } function CenteredWrapper({children}) { - let {state} = useContext(ListViewContext); + let {state} = useContext(ListViewContext)!; return (
(props: ListViewItemProps) { layout, dragAndDropHooks, loadingState - } = useContext(ListViewContext); + } = useContext(ListViewContext)!; let {direction} = useLocale(); - let rowRef = useRef(undefined); - let checkboxWrapperRef = useRef(undefined); + let rowRef = useRef(null); + let checkboxWrapperRef = useRef(null); let { isFocusVisible: isFocusVisibleWithin, focusProps: focusWithinProps @@ -74,32 +74,30 @@ export function ListViewItem(props: ListViewItemProps) { isVirtualized: true, shouldSelectOnPressUp: isListDraggable }, state, rowRef); - let isDroppable = isListDroppable && !isDisabled; let {hoverProps, isHovered} = useHover({isDisabled: !allowsSelection && !hasAction}); let {checkboxProps} = useGridListSelectionCheckbox({key: item.key}, state); let hasDescription = useHasChild(`.${listStyles['react-spectrum-ListViewItem-description']}`, rowRef); - let draggableItem: DraggableItemResult; - if (isListDraggable) { + let draggableItem: DraggableItemResult | null = null; + if (isListDraggable && dragAndDropHooks && dragState) { - draggableItem = dragAndDropHooks.useDraggableItem({key: item.key, hasDragButton: true}, dragState); + draggableItem = dragAndDropHooks.useDraggableItem!({key: item.key, hasDragButton: true}, dragState); if (isDisabled) { draggableItem = null; } } - let droppableItem: DroppableItemResult; - let isDropTarget: boolean; - let dropIndicator: DropIndicatorAria; - let dropIndicatorRef = useRef(undefined); - if (isListDroppable) { + let isDropTarget = false; + let dropIndicator: DropIndicatorAria | null = null; + let dropIndicatorRef = useRef(null); + if (isListDroppable && dragAndDropHooks && dropState) { let target = {type: 'item', key: item.key, dropPosition: 'on'} as DropTarget; isDropTarget = dropState.isDropTarget(target); - dropIndicator = dragAndDropHooks.useDropIndicator({target}, dropState, dropIndicatorRef); + dropIndicator = dragAndDropHooks.useDropIndicator!({target}, dropState, dropIndicatorRef); } - let dragButtonRef = React.useRef(undefined); + let dragButtonRef = React.useRef(null); let {buttonProps} = useButton({ ...draggableItem?.dragButtonProps, elementType: 'div' @@ -138,18 +136,19 @@ export function ListViewItem(props: ListViewItemProps) { let showCheckbox = state.selectionManager.selectionMode !== 'none' && state.selectionManager.selectionBehavior === 'toggle'; let {visuallyHiddenProps} = useVisuallyHidden(); - let dropProps = isDroppable ? droppableItem?.dropProps : {'aria-hidden': droppableItem?.dropProps['aria-hidden']}; const mergedProps = mergeProps( rowProps, draggableItem?.dragProps, - dropProps, hoverProps, focusWithinProps, - focusProps, - // Remove tab index from list row if performing a screenreader drag. This prevents TalkBack from focusing the row, - // allowing for single swipe navigation between row drop indicator - dragAndDropHooks?.isVirtualDragging() && {tabIndex: null} + focusProps ); + + // Remove tab index from list row if performing a screenreader drag. This prevents TalkBack from focusing the row, + // allowing for single swipe navigation between row drop indicator + if (dragAndDropHooks?.isVirtualDragging?.()) { + mergedProps.tabIndex = undefined; + } let isFirstRow = item.prevKey == null; let isLastRow = item.nextKey == null; @@ -158,15 +157,15 @@ export function ListViewItem(props: ListViewItemProps) { // with bottom border let isFlushWithContainerBottom = false; if (isLastRow && loadingState !== 'loadingMore') { - if (layout.getContentSize()?.height >= layout.virtualizer?.visibleRect.height) { + if (layout.getContentSize()?.height >= (layout.virtualizer?.visibleRect.height ?? 0)) { isFlushWithContainerBottom = true; } } // previous item isn't selected // and the previous item isn't focused or, if it is focused, then if focus globally isn't visible or just focus isn't in the listview - let roundTops = (!state.selectionManager.isSelected(item.prevKey) + let roundTops = (!(item.prevKey != null && state.selectionManager.isSelected(item.prevKey)) && (state.selectionManager.focusedKey !== item.prevKey || !(isGlobalFocusVisible() && state.selectionManager.isFocused))); - let roundBottoms = (!state.selectionManager.isSelected(item.nextKey) + let roundBottoms = (!(item.nextKey != null && state.selectionManager.isSelected(item.nextKey)) && (state.selectionManager.focusedKey !== item.nextKey || !(isGlobalFocusVisible() && state.selectionManager.isFocused))); let content = typeof item.rendered === 'string' ? {item.rendered} : item.rendered; @@ -204,9 +203,9 @@ export function ListViewItem(props: ListViewItemProps) { 'is-hovered': isHovered, 'is-selected': isSelected, 'is-disabled': isDisabled, - 'is-prev-selected': state.selectionManager.isSelected(item.prevKey), - 'is-next-selected': state.selectionManager.isSelected(item.nextKey), - 'react-spectrum-ListViewItem--highlightSelection': state.selectionManager.selectionBehavior === 'replace' && (isSelected || state.selectionManager.isSelected(item.nextKey)), + 'is-prev-selected': item.prevKey != null && state.selectionManager.isSelected(item.prevKey), + 'is-next-selected': item.nextKey != null && state.selectionManager.isSelected(item.nextKey), + 'react-spectrum-ListViewItem--highlightSelection': state.selectionManager.selectionBehavior === 'replace' && (isSelected || (item.nextKey != null && state.selectionManager.isSelected(item.nextKey))), 'react-spectrum-ListViewItem--dropTarget': !!isDropTarget, 'react-spectrum-ListViewItem--firstRow': isFirstRow, 'react-spectrum-ListViewItem--lastRow': isLastRow, diff --git a/packages/@react-spectrum/list/src/ListViewLayout.ts b/packages/@react-spectrum/list/src/ListViewLayout.ts index 8f8d251eefb..5fd2e72b6a6 100644 --- a/packages/@react-spectrum/list/src/ListViewLayout.ts +++ b/packages/@react-spectrum/list/src/ListViewLayout.ts @@ -30,7 +30,7 @@ export class ListViewLayout extends ListLayout { let y = this.contentSize.height; if (this.isLoading) { - let rect = new Rect(0, y, this.virtualizer.visibleRect.width, nodes.length === 0 ? this.virtualizer.visibleRect.height : this.estimatedRowHeight); + let rect = new Rect(0, y, this.virtualizer!.visibleRect.width, nodes.length === 0 ? this.virtualizer!.visibleRect.height : this.estimatedRowHeight ?? 48); let loader = new LayoutInfo('loader', 'loader', rect); let node = { layoutInfo: loader, @@ -42,7 +42,7 @@ export class ListViewLayout extends ListLayout { } if (nodes.length === 0) { - let rect = new Rect(0, y, this.virtualizer.visibleRect.width, this.virtualizer.visibleRect.height); + let rect = new Rect(0, y, this.virtualizer!.visibleRect.width, this.virtualizer!.visibleRect.height); let placeholder = new LayoutInfo('placeholder', 'placeholder', rect); let node = { layoutInfo: placeholder, diff --git a/packages/@react-spectrum/list/src/RootDropIndicator.tsx b/packages/@react-spectrum/list/src/RootDropIndicator.tsx index e2a64cd60b7..b4b09e03c07 100644 --- a/packages/@react-spectrum/list/src/RootDropIndicator.tsx +++ b/packages/@react-spectrum/list/src/RootDropIndicator.tsx @@ -3,12 +3,12 @@ import React, {useContext, useRef} from 'react'; import {useVisuallyHidden} from '@react-aria/visually-hidden'; export default function RootDropIndicator() { - let {dropState, dragAndDropHooks} = useContext(ListViewContext); - let ref = useRef(undefined); - let {dropIndicatorProps} = dragAndDropHooks.useDropIndicator({ + let {dropState, dragAndDropHooks} = useContext(ListViewContext)!; + let ref = useRef(null); + let {dropIndicatorProps} = dragAndDropHooks!.useDropIndicator!({ target: {type: 'root'} - }, dropState, ref); - let isDropTarget = dropState.isDropTarget({type: 'root'}); + }, dropState!, ref); + let isDropTarget = dropState!.isDropTarget({type: 'root'}); let {visuallyHiddenProps} = useVisuallyHidden(); if (!isDropTarget && dropIndicatorProps['aria-hidden']) { diff --git a/packages/@react-spectrum/list/stories/ListView.stories.tsx b/packages/@react-spectrum/list/stories/ListView.stories.tsx index 51fc5eea01d..6af4bf2fa6e 100644 --- a/packages/@react-spectrum/list/stories/ListView.stories.tsx +++ b/packages/@react-spectrum/list/stories/ListView.stories.tsx @@ -396,11 +396,11 @@ function ActionBarExample(props?) { let i = 0; function EmptyTest() { - const [items, setItems] = useState([]); + const [items, setItems] = useState<{key: number, name: string}[]>([]); const [divProps, setDivProps] = useState({}); useEffect(() => { - let newItems = []; + let newItems: typeof items = []; for (i = 0; i < 20; i++) { newItems.push({key: i, name: `Item ${i}`}); } @@ -418,7 +418,7 @@ function EmptyTest() {
- + { item => ( @@ -508,7 +508,7 @@ function Demo(props) { ); } -const manyItems = []; +const manyItems: {key: number, name: string}[] = []; for (let i = 0; i < 500; i++) {manyItems.push({key: i, name: `item ${i}`});} function DisplayNoneComponent(args) { @@ -517,7 +517,7 @@ function DisplayNoneComponent(args) { return ( <> -
+
{(item: any) => ( diff --git a/packages/@react-spectrum/list/stories/ListViewDnD.stories.tsx b/packages/@react-spectrum/list/stories/ListViewDnD.stories.tsx index 3734e0698af..0b9bcf38f42 100644 --- a/packages/@react-spectrum/list/stories/ListViewDnD.stories.tsx +++ b/packages/@react-spectrum/list/stories/ListViewDnD.stories.tsx @@ -101,7 +101,7 @@ export const DragWithinScroll: ListViewStory = { name: 'Drag within list scrolling (Reorder)' }; -let manyItems = []; +let manyItems: {id: string, type: string, textValue: string}[] = []; for (let i = 0; i < 100; i++) { manyItems.push({id: 'item' + i, type: 'item', textValue: 'Item ' + i}); } diff --git a/packages/@react-spectrum/list/stories/ListViewDnDExamples.tsx b/packages/@react-spectrum/list/stories/ListViewDnDExamples.tsx index a23a3c81df4..653af3d77a6 100644 --- a/packages/@react-spectrum/list/stories/ListViewDnDExamples.tsx +++ b/packages/@react-spectrum/list/stories/ListViewDnDExamples.tsx @@ -120,10 +120,10 @@ export function ReorderExample(props) { async onDrop(e) { onDrop(e); if (e.target.type !== 'root' && e.target.dropPosition !== 'on') { - let keys = []; + let keys: Key[] = []; for (let item of e.items) { if (item.kind === 'text') { - let key; + let key: Key; if (item.types.has(dragType)) { key = JSON.parse(await item.getText(dragType)); keys.push(key); @@ -180,7 +180,7 @@ export function DragIntoItemExample(props) { let list = useListData({ initialItems: [ - {id: '0', type: 'folder', textValue: 'Folder 1', childNodes: []}, + {id: '0', type: 'folder', textValue: 'Folder 1', childNodes: [] as any[]}, {id: '1', type: 'item', textValue: 'One'}, {id: '2', type: 'item', textValue: 'Two'}, {id: '3', type: 'item', textValue: 'Three'}, @@ -197,9 +197,9 @@ export function DragIntoItemExample(props) { let dragType = React.useMemo(() => `keys-${Math.random().toString(36).slice(2)}`, []); let onMove = (keys: Key[], target: ItemDropTarget) => { - let folderItem = list.getItem(target.key); + let folderItem = list.getItem(target.key)!; let draggedItems = keys.map((key) => list.getItem(key)); - list.update(target.key, {...folderItem, childNodes: [...folderItem.childNodes, ...draggedItems]}); + list.update(target.key, {...folderItem, childNodes: [...(folderItem.childNodes || []), ...draggedItems]}); list.remove(...keys); }; @@ -222,10 +222,10 @@ export function DragIntoItemExample(props) { onDrop: async e => { onDropAction(e); if (e.target.type !== 'root' && e.target.dropPosition === 'on') { - let keys = []; + let keys: Key[] = []; for (let item of e.items) { if (item.kind === 'text') { - let key; + let key: Key; if (item.types.has(dragType)) { key = JSON.parse(await item.getText(dragType)); keys.push(key); @@ -243,7 +243,7 @@ export function DragIntoItemExample(props) { } }, getDropOperation(target) { - if (target.type === 'root' || target.dropPosition !== 'on' || !list.getItem(target.key).childNodes || disabledKeys.includes(target.key)) { + if (target.type === 'root' || target.dropPosition !== 'on' || !list.getItem(target.key)!.childNodes || disabledKeys.includes(target.key)) { return 'cancel'; } @@ -333,10 +333,10 @@ export function DragBetweenListsExample(props) { onDrop: async e => { onDropAction(e); if (e.target.type !== 'root' && e.target.dropPosition !== 'on') { - let keys = []; + let keys: Key[] = []; for (let item of e.items) { if (item.kind === 'text') { - let key; + let key: Key; if (item.types.has(dragType)) { key = JSON.parse(await item.getText(dragType)); keys.push(key); @@ -445,10 +445,10 @@ export function DragBetweenListsRootOnlyExample(props) { onDrop: async e => { onDropAction(e); if (e.target.type === 'root') { - let keys = []; + let keys: Key[] = []; for (let item of e.items) { if (item.kind === 'text') { - let key; + let key: Key; if (item.types.has('list2')) { key = JSON.parse(await item.getText('list2')); keys.push(key); @@ -491,10 +491,10 @@ export function DragBetweenListsRootOnlyExample(props) { onDrop: async e => { onDropAction(e); if (e.target.type === 'root') { - let keys = []; + let keys: Key[] = []; for (let item of e.items) { if (item.kind === 'text') { - let key; + let key: Key; if (item.types.has('list1')) { key = JSON.parse(await item.getText('list1')); keys.push(key); diff --git a/packages/@react-spectrum/list/stories/ListViewDnDUtil.stories.tsx b/packages/@react-spectrum/list/stories/ListViewDnDUtil.stories.tsx index 6da108bb537..3be13f1b0cc 100644 --- a/packages/@react-spectrum/list/stories/ListViewDnDUtil.stories.tsx +++ b/packages/@react-spectrum/list/stories/ListViewDnDUtil.stories.tsx @@ -74,7 +74,7 @@ export const DragWithin: ListViewStory = { name: 'Drag within list (Reorder}' }; -let manyItems = []; +let manyItems: {identifier: string, type: string, name: string}[] = []; for (let i = 0; i < 100; i++) { manyItems.push({identifier: 'item' + i, type: 'item', name: 'Item ' + i}); } diff --git a/packages/@react-spectrum/list/stories/ListViewDnDUtilExamples.tsx b/packages/@react-spectrum/list/stories/ListViewDnDUtilExamples.tsx index 0ecf61a4c33..50f93f58fd9 100644 --- a/packages/@react-spectrum/list/stories/ListViewDnDUtilExamples.tsx +++ b/packages/@react-spectrum/list/stories/ListViewDnDUtilExamples.tsx @@ -9,8 +9,8 @@ import {useDragAndDrop} from '@react-spectrum/dnd'; import {useListData} from '@react-stately/data'; let itemProcessor = async (items, acceptedDragTypes) => { - let processedItems = []; - let text; + let processedItems: any[] = []; + let text = ''; for (let item of items) { for (let type of acceptedDragTypes) { if (item.kind === 'text' && item.types.has(type)) { @@ -32,14 +32,14 @@ let itemProcessor = async (items, acceptedDragTypes) => { let folderList1 = [ {identifier: '1', type: 'file', name: 'Adobe Photoshop'}, {identifier: '2', type: 'file', name: 'Adobe XD'}, - {identifier: '3', type: 'folder', name: 'Documents', childNodes: []}, + {identifier: '3', type: 'folder', name: 'Documents', childNodes: [] as any[]}, {identifier: '4', type: 'file', name: 'Adobe InDesign'}, {identifier: '5', type: 'folder', name: 'Utilities', childNodes: []}, {identifier: '6', type: 'file', name: 'Adobe AfterEffects'} ]; let folderList2 = [ - {identifier: '7', type: 'folder', name: 'Pictures', childNodes: []}, + {identifier: '7', type: 'folder', name: 'Pictures', childNodes: [] as any[]}, {identifier: '8', type: 'file', name: 'Adobe Fresco'}, {identifier: '9', type: 'folder', name: 'Apps', childNodes: []}, {identifier: '10', type: 'file', name: 'Adobe Illustrator'}, @@ -58,7 +58,7 @@ export function DragExampleUtilHandlers(props) { let acceptedDragTypes = ['file', 'folder', 'text/plain']; let {dragAndDropHooks} = useDragAndDrop({ getItems: (keys) => [...keys].map(key => { - let item = list.getItem(key); + let item = list.getItem(key)!; return { [`${item.type}`]: JSON.stringify(item), 'text/plain': JSON.stringify(item) @@ -102,7 +102,7 @@ export function ReorderExampleUtilHandlers(props) { let acceptedDragTypes = ['file', 'folder', 'text/plain']; let {dragAndDropHooks} = useDragAndDrop({ getItems: (keys) => [...keys].map(key => { - let item = list.getItem(key); + let item = list.getItem(key)!; return { [`${item.type}`]: JSON.stringify(item), 'text/plain': JSON.stringify(item) @@ -116,10 +116,10 @@ export function ReorderExampleUtilHandlers(props) { } = e; action('onReorder')(e); - let itemsToCopy = []; + let itemsToCopy: typeof folderList1 = []; if (dropOperation === 'copy') { for (let key of keys) { - let item = {...list.getItem(key)}; + let item: typeof folderList1[0] = {...list.getItem(key)!}; item.identifier = Math.random().toString(36).slice(2); itemsToCopy.push(item); } @@ -177,7 +177,7 @@ export function ItemDropExampleUtilHandlers(props) { let acceptedDragTypes = ['file', 'folder', 'text/plain']; let {dragAndDropHooks} = useDragAndDrop({ getItems: (keys) => [...keys].map(key => { - let item = list.getItem(key); + let item = list.getItem(key)!; return { [`${item.type}`]: JSON.stringify(item), 'text/plain': JSON.stringify(item) @@ -195,7 +195,7 @@ export function ItemDropExampleUtilHandlers(props) { let processedItems = await itemProcessor(items, acceptedDragTypes); let targetItem = list.getItem(target.key); if (targetItem?.childNodes != null) { - list.update(target.key, {...targetItem, childNodes: [...targetItem.childNodes, ...processedItems]}); + list.update(target.key, {...targetItem, childNodes: [...(targetItem.childNodes || []), ...processedItems]}); if (isInternal && dropOperation === 'move') { let keysToRemove = processedItems.map(item => item.identifier); list.remove(...keysToRemove); @@ -245,7 +245,7 @@ export function RootDropExampleUtilHandlers(props) { let acceptedDragTypes = ['file', 'folder', 'text/plain']; let {dragAndDropHooks: list1Hooks} = useDragAndDrop({ getItems: (keys) => [...keys].map(key => { - let item = list1.getItem(key); + let item = list1.getItem(key)!; return { [`${item.type}`]: JSON.stringify(item), 'text/plain': JSON.stringify(item) @@ -336,7 +336,7 @@ export function InsertExampleUtilHandlers(props) { let acceptedDragTypes = ['file', 'folder', 'text/plain']; let {dragAndDropHooks: list1Hooks} = useDragAndDrop({ getItems: (keys) => [...keys].map(key => { - let item = list1.getItem(key); + let item = list1.getItem(key)!; return { [`${item.type}`]: JSON.stringify(item), 'text/plain': JSON.stringify(item) @@ -517,7 +517,7 @@ export function DragBetweenListsComplex(props) { // List 1 should allow on item drops and external drops, but disallow reordering/internal drops let {dragAndDropHooks: dragAndDropHooksList1} = useDragAndDrop({ getItems: (keys) => [...keys].map(key => { - let item = list1.getItem(key); + let item = list1.getItem(key)!; return { [`${item.type}`]: JSON.stringify(item), 'text/plain': JSON.stringify(item) @@ -552,8 +552,8 @@ export function DragBetweenListsComplex(props) { } = e; action('onItemDropList1')(e); let processedItems = await itemProcessor(items, acceptedDragTypes); - let targetItem = list1.getItem(target.key); - list1.update(target.key, {...targetItem, childNodes: [...targetItem.childNodes, ...processedItems]}); + let targetItem = list1.getItem(target.key)!; + list1.update(target.key, {...targetItem, childNodes: [...(targetItem.childNodes || []), ...processedItems]}); if (isInternal && dropOperation === 'move') { // TODO test this, perhaps it would be easier to also pass the draggedKeys to onItemDrop instead? @@ -575,14 +575,14 @@ export function DragBetweenListsComplex(props) { } }, getAllowedDropOperations: () => ['move', 'copy'], - shouldAcceptItemDrop: (target) => !!list1.getItem(target.key).childNodes, + shouldAcceptItemDrop: (target) => !!list1.getItem(target.key)!.childNodes, ...firstListDnDOptions }); // List 2 should allow reordering, on folder drops, and on root drops let {dragAndDropHooks: dragAndDropHooksList2} = useDragAndDrop({ getItems: (keys) => [...keys].map(key => { - let item = list2.getItem(key); + let item = list2.getItem(key)!; let dragItem = {}; let itemString = JSON.stringify(item); dragItem[`${item.type}`] = itemString; @@ -614,10 +614,10 @@ export function DragBetweenListsComplex(props) { } = e; action('onReorderList2')(e); - let itemsToCopy = []; + let itemsToCopy: typeof folderList2 = []; if (dropOperation === 'copy') { for (let key of keys) { - let item = {...list2.getItem(key)}; + let item: typeof folderList2[0] = {...list2.getItem(key)!}; item.identifier = Math.random().toString(36).slice(2); itemsToCopy.push(item); } @@ -651,8 +651,8 @@ export function DragBetweenListsComplex(props) { } = e; action('onItemDropList2')(e); let processedItems = await itemProcessor(items, acceptedDragTypes); - let targetItem = list2.getItem(target.key); - list2.update(target.key, {...targetItem, childNodes: [...targetItem.childNodes, ...processedItems]}); + let targetItem = list2.getItem(target.key)!; + list2.update(target.key, {...targetItem, childNodes: [...(targetItem.childNodes || []), ...processedItems]}); if (isInternal && dropOperation === 'move') { let keysToRemove = processedItems.map(item => item.identifier); @@ -668,12 +668,12 @@ export function DragBetweenListsComplex(props) { } = e; action('onDragEndList2')(e); if (dropOperation === 'move' && !isInternal) { - let keysToRemove = [...keys].filter(key => list2.getItem(key).type !== 'unique_type'); + let keysToRemove = [...keys].filter(key => list2.getItem(key)!.type !== 'unique_type'); list2.remove(...keysToRemove); } }, getAllowedDropOperations: () => ['move', 'copy'], - shouldAcceptItemDrop: (target) => !!list2.getItem(target.key).childNodes, + shouldAcceptItemDrop: (target) => !!list2.getItem(target.key)!.childNodes, ...secondListDnDOptions }); @@ -738,7 +738,7 @@ export function DragBetweenListsOverride(props) { let list2 = useListData({ initialItems: [ - {identifier: '7', type: 'folder', name: 'Pictures', childNodes: []}, + {identifier: '7', type: 'folder', name: 'Pictures', childNodes: [] as any[]}, {identifier: '8', type: 'file', name: 'Adobe Fresco'}, {identifier: '9', type: 'folder', name: 'Apps', childNodes: []}, {identifier: '10', type: 'file', name: 'Adobe Illustrator'}, @@ -750,7 +750,7 @@ export function DragBetweenListsOverride(props) { let {dragAndDropHooks: dragAndDropHooksList1} = useDragAndDrop({ getItems: (keys) => [...keys].map(key => { - let item = list1.getItem(key); + let item = list1.getItem(key)!; let dragType = `list-1-adobe-${item.type}`; return { [`${dragType}`]: JSON.stringify(item), @@ -778,8 +778,8 @@ export function DragBetweenListsOverride(props) { let { items } = e; - let itemsToAdd = []; - let text; + let itemsToAdd: typeof folderList2 = []; + let text = ''; for (let item of items) { if (item.kind === 'text') { if (item.types.size === 1 && item.types.has('text/plain')) { @@ -800,7 +800,7 @@ export function DragBetweenListsOverride(props) { onReorder: () => action('onReorder'), onRootDrop: () => action('onRootDrop'), onItemDrop: () => action('onItemDrop'), - shouldAcceptItemDrop: (target) => !!list2.getItem(target.key).childNodes, + shouldAcceptItemDrop: (target) => !!list2.getItem(target.key)!.childNodes, acceptedDragTypes: 'all' }); diff --git a/packages/@react-spectrum/listbox/src/ListBoxBase.tsx b/packages/@react-spectrum/listbox/src/ListBoxBase.tsx index a48a42d3b2f..93dbbc45898 100644 --- a/packages/@react-spectrum/listbox/src/ListBoxBase.tsx +++ b/packages/@react-spectrum/listbox/src/ListBoxBase.tsx @@ -21,9 +21,9 @@ import {ListBoxLayout} from './ListBoxLayout'; import {ListBoxOption} from './ListBoxOption'; import {ListBoxSection} from './ListBoxSection'; import {ListState} from '@react-stately/list'; -import {mergeProps} from '@react-aria/utils'; +import {mergeProps, useObjectRef} from '@react-aria/utils'; import {ProgressCircle} from '@react-spectrum/progress'; -import React, {HTMLAttributes, ReactElement, ReactNode, useCallback, useContext, useMemo} from 'react'; +import React, {ForwardedRef, HTMLAttributes, ReactElement, ReactNode, useCallback, useContext, useMemo} from 'react'; import {ReusableView} from '@react-stately/virtualizer'; import styles from '@adobe/spectrum-css-temp/components/menu/vars.css'; import {useLocalizedStringFormatter} from '@react-aria/i18n'; @@ -63,27 +63,28 @@ export function useListBoxLayout(): ListBoxLayout { } /** @private */ -function ListBoxBase(props: ListBoxBaseProps, ref: RefObject) { - let {layout, state, shouldFocusOnHover, shouldUseVirtualFocus, domProps = {}, isLoading, showLoadingSpinner = isLoading, onScroll, renderEmptyState} = props; +function ListBoxBase(props: ListBoxBaseProps, ref: ForwardedRef) { + let {layout, state, shouldFocusOnHover = false, shouldUseVirtualFocus = false, domProps = {}, isLoading, showLoadingSpinner = isLoading, onScroll, renderEmptyState} = props; + let objectRef = useObjectRef(ref); let {listBoxProps} = useListBox({ ...props, layoutDelegate: layout, isVirtualized: true - }, state, ref); + }, state, objectRef); let {styleProps} = useStyleProps(props); // This overrides collection view's renderWrapper to support hierarchy of items in sections. // The header is extracted from the children so it can receive ARIA labeling properties. - type View = ReusableView, ReactElement>; - let renderWrapper = useCallback((parent: View, reusableView: View, children: View[], renderChildren: (views: View[]) => ReactElement[]) => { + type View = ReusableView, ReactNode>; + let renderWrapper = useCallback((parent: View | null, reusableView: View, children: View[], renderChildren: (views: View[]) => ReactElement[]) => { if (reusableView.viewType === 'section') { return ( c.viewType === 'header')?.layoutInfo}> + headerLayoutInfo={children.find(c => c.viewType === 'header')?.layoutInfo ?? null}> {renderChildren(children.filter(c => c.viewType === 'item'))} ); @@ -92,7 +93,7 @@ function ListBoxBase(props: ListBoxBaseProps, ref: RefObject {reusableView.rendered} @@ -109,7 +110,7 @@ function ListBoxBase(props: ListBoxBaseProps, ref: RefObject(props: ListBoxBaseProps, ref: RefObject - {useCallback((type, item: Node) => { + {useCallback((type, item: Node): ReactNode => { if (type === 'item') { return ; } else if (type === 'loader') { return ; } else if (type === 'placeholder') { return ; + } else { + return null; } }, [])} @@ -150,7 +153,7 @@ const _ListBoxBase = React.forwardRef(ListBoxBase) as (props: ListBoxBaseProp export {_ListBoxBase as ListBoxBase}; function LoadingState() { - let {state} = useContext(ListBoxContext); + let {state} = useContext(ListBoxContext)!; let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/listbox'); return ( // aria-selected isn't needed here since this option is not selectable. @@ -166,7 +169,7 @@ function LoadingState() { } function EmptyState() { - let {renderEmptyState} = useContext(ListBoxContext); + let {renderEmptyState} = useContext(ListBoxContext)!; let emptyState = renderEmptyState ? renderEmptyState() : null; if (emptyState == null) { return null; diff --git a/packages/@react-spectrum/listbox/src/ListBoxContext.ts b/packages/@react-spectrum/listbox/src/ListBoxContext.ts index a1424f80a9d..346d222f2fe 100644 --- a/packages/@react-spectrum/listbox/src/ListBoxContext.ts +++ b/packages/@react-spectrum/listbox/src/ListBoxContext.ts @@ -20,4 +20,4 @@ interface ListBoxContextValue { shouldUseVirtualFocus: boolean } -export const ListBoxContext = React.createContext(null); +export const ListBoxContext = React.createContext(null); diff --git a/packages/@react-spectrum/listbox/src/ListBoxLayout.ts b/packages/@react-spectrum/listbox/src/ListBoxLayout.ts index 8f9652c4ea4..f0f2c09b872 100644 --- a/packages/@react-spectrum/listbox/src/ListBoxLayout.ts +++ b/packages/@react-spectrum/listbox/src/ListBoxLayout.ts @@ -32,7 +32,7 @@ export class ListBoxLayout extends ListLayout { let y = this.contentSize.height; if (this.isLoading) { - let rect = new Rect(0, y, this.virtualizer.visibleRect.width, 40); + let rect = new Rect(0, y, this.virtualizer!.visibleRect.width, 40); let loader = new LayoutInfo('loader', 'loader', rect); let node = { layoutInfo: loader, @@ -44,7 +44,7 @@ export class ListBoxLayout extends ListLayout { } if (nodes.length === 0) { - let rect = new Rect(0, y, this.virtualizer.visibleRect.width, this.placeholderHeight ?? this.virtualizer.visibleRect.height); + let rect = new Rect(0, y, this.virtualizer!.visibleRect.width, this.placeholderHeight ?? this.virtualizer!.visibleRect.height); let placeholder = new LayoutInfo('placeholder', 'placeholder', rect); let node = { layoutInfo: placeholder, @@ -67,6 +67,7 @@ export class ListBoxLayout extends ListLayout { parentKey: node.key, value: null, level: node.level, + index: node.index, hasChildNodes: false, childNodes: [], rendered: node.rendered, @@ -81,7 +82,7 @@ export class ListBoxLayout extends ListLayout { y += header.layoutInfo.rect.height; let section = super.buildSection(node, x, y); - section.children.unshift(header); + section.children!.unshift(header); return section; } } diff --git a/packages/@react-spectrum/listbox/src/ListBoxOption.tsx b/packages/@react-spectrum/listbox/src/ListBoxOption.tsx index a8404ddfa5c..1635b985f1d 100644 --- a/packages/@react-spectrum/listbox/src/ListBoxOption.tsx +++ b/packages/@react-spectrum/listbox/src/ListBoxOption.tsx @@ -36,7 +36,7 @@ export function ListBoxOption(props: OptionProps) { key } = item; let ElementType: React.ElementType = item.props.href ? 'a' : 'div'; - let {state, shouldFocusOnHover, shouldUseVirtualFocus} = useContext(ListBoxContext); + let {state, shouldFocusOnHover, shouldUseVirtualFocus} = useContext(ListBoxContext)!; let ref = useRef(undefined); let {optionProps, labelProps, descriptionProps, isSelected, isDisabled, isFocused} = useOption( diff --git a/packages/@react-spectrum/listbox/src/ListBoxSection.tsx b/packages/@react-spectrum/listbox/src/ListBoxSection.tsx index 49bdd39020e..2daaf0c319f 100644 --- a/packages/@react-spectrum/listbox/src/ListBoxSection.tsx +++ b/packages/@react-spectrum/listbox/src/ListBoxSection.tsx @@ -20,8 +20,9 @@ import styles from '@adobe/spectrum-css-temp/components/menu/vars.css'; import {useListBoxSection} from '@react-aria/listbox'; import {useLocale} from '@react-aria/i18n'; -interface ListBoxSectionProps extends Omit { - headerLayoutInfo: LayoutInfo, +interface ListBoxSectionProps extends Omit { + layoutInfo: LayoutInfo, + headerLayoutInfo: LayoutInfo | null, item: Node, children?: ReactNode } @@ -34,7 +35,7 @@ export function ListBoxSection(props: ListBoxSectionProps) { 'aria-label': item['aria-label'] }); - let headerRef = useRef(undefined); + let headerRef = useRef(null); useVirtualizerItem({ layoutInfo: headerLayoutInfo, virtualizer, @@ -42,7 +43,7 @@ export function ListBoxSection(props: ListBoxSectionProps) { }); let {direction} = useLocale(); - let {state} = useContext(ListBoxContext); + let {state} = useContext(ListBoxContext)!; return ( diff --git a/packages/@react-spectrum/listbox/stories/ListBox.stories.tsx b/packages/@react-spectrum/listbox/stories/ListBox.stories.tsx index cdce02bda7f..cc8e70ad895 100644 --- a/packages/@react-spectrum/listbox/stories/ListBox.stories.tsx +++ b/packages/@react-spectrum/listbox/stories/ListBox.stories.tsx @@ -91,9 +91,9 @@ let itemsWithFalsyId = [ ]} ]; -let lotsOfSections: any[] = []; +let lotsOfSections: {name: string, children: {name: string}[]}[] = []; for (let i = 0; i < 50; i++) { - let children = []; + let children: {name: string}[] = []; for (let j = 0; j < 50; j++) { children.push({name: `Section ${i}, Item ${j}`}); } @@ -736,7 +736,7 @@ export const WithSemanticElementsGenerativeMultipleSelection = { export const IsLoading = { render: () => ( - + {(item) => {item.name}} ), @@ -939,18 +939,19 @@ export function FocusExample(args = {}) { let tree = useTreeData({ initialItems: withSection, getKey: (item) => item.name, - getChildren: (item:{name:string, children?:{name:string, children?:{name:string}[]}[]}) => item.children + getChildren: (item: {name:string, children?:{name:string, children?:{name:string}[]}[]}) => item.children ?? [] }); - let [dialog, setDialog] = useState(null); + let [dialog, setDialog] = useState<{action: Key} | null>(null); let ref = useRef(null); return ( setDialog({action})}> - {(tree.selectedKeys.size > 0) && - + {(tree.selectedKeys.size > 0) + ? + : null }
@@ -970,11 +971,11 @@ export function FocusExample(args = {}) { }} selectionMode="multiple" {...args}> - {item => item.children.length && ( + {item => item?.children?.length ? (
{item => {item.value.name}}
- )} + ) : null} }
@@ -1107,7 +1108,7 @@ export function WithTreeData() { } }}> {node => ( -
+
{node => {node.value.name}}
)} diff --git a/packages/@react-spectrum/listbox/test/ListBox.test.js b/packages/@react-spectrum/listbox/test/ListBox.test.js index 8b6c974b455..2a7c07ca6bb 100644 --- a/packages/@react-spectrum/listbox/test/ListBox.test.js +++ b/packages/@react-spectrum/listbox/test/ListBox.test.js @@ -936,7 +936,6 @@ describe('ListBox', function () { let maxHeight = 300; jest.spyOn(window.HTMLElement.prototype, 'clientHeight', 'get').mockImplementation(() => maxHeight); offsetHeight = jest.spyOn(window.HTMLElement.prototype, 'scrollHeight', 'get').mockImplementation(function () { - // @ts-ignore if (this.getAttribute('role') === 'listbox') { // First load should match the clientHeight since the number of items doesn't exceed the listbox height return 300; @@ -959,7 +958,6 @@ describe('ListBox', function () { }).mockImplementationOnce(() => { onLoadMore(); offsetHeight = jest.spyOn(window.HTMLElement.prototype, 'scrollHeight', 'get').mockImplementation(function () { - // @ts-ignore if (this.getAttribute('role') === 'listbox') { // Second load we need to update the height of the scrollable body otherwise we will keep calling loading more return 600; diff --git a/packages/@react-spectrum/menu/chromatic/Submenu.stories.tsx b/packages/@react-spectrum/menu/chromatic/Submenu.stories.tsx index f9075438c4c..3803ad0380e 100644 --- a/packages/@react-spectrum/menu/chromatic/Submenu.stories.tsx +++ b/packages/@react-spectrum/menu/chromatic/Submenu.stories.tsx @@ -108,7 +108,7 @@ let dynamicRenderItem = (item, Icon) => ( ); let dynamicRenderFuncSections = (item: ItemNode) => { - let Icon = iconMap[item.icon]; + let Icon = iconMap[item.icon!]; if (item.children) { if (item.isSection) { let key = item.name ?? item.textValue; diff --git a/packages/@react-spectrum/menu/src/ContextualHelpTrigger.tsx b/packages/@react-spectrum/menu/src/ContextualHelpTrigger.tsx index b4db668dedb..3d740ebdedf 100644 --- a/packages/@react-spectrum/menu/src/ContextualHelpTrigger.tsx +++ b/packages/@react-spectrum/menu/src/ContextualHelpTrigger.tsx @@ -11,10 +11,10 @@ */ import {classNames, SlotProvider, unwrapDOMRef, useIsMobileDevice} from '@react-spectrum/utils'; +import {DOMRefValue, ItemProps, Key} from '@react-types/shared'; import {FocusScope} from '@react-aria/focus'; import {getInteractionModality} from '@react-aria/interactions'; import helpStyles from '@adobe/spectrum-css-temp/components/contextualhelp/vars.css'; -import {ItemProps, Key} from '@react-types/shared'; import {Popover} from '@react-spectrum/overlays'; import React, {JSX, ReactElement, useEffect, useRef, useState} from 'react'; import ReactDOM from 'react-dom'; @@ -41,9 +41,9 @@ function ContextualHelpTrigger(props: InternalMenuDialogTriggerProps): ReactElem let {isUnavailable = false, targetKey} = props; let triggerRef = useRef(null); - let popoverRef = useRef(null); - let {popoverContainer, trayContainerRef, rootMenuTriggerState, menu: parentMenuRef, state} = useMenuStateContext(); - let submenuTriggerState = useSubmenuTriggerState({triggerKey: targetKey}, {...rootMenuTriggerState, ...state}); + let popoverRef = useRef | null>(null); + let {popoverContainer, trayContainerRef, rootMenuTriggerState, menu: parentMenuRef, state} = useMenuStateContext()!; + let submenuTriggerState = useSubmenuTriggerState({triggerKey: targetKey}, {...rootMenuTriggerState!, ...state}); let submenuRef = unwrapDOMRef(popoverRef); let {submenuTriggerProps, popoverProps} = useSubmenuTrigger({ parentMenuRef, @@ -85,7 +85,7 @@ function ContextualHelpTrigger(props: InternalMenuDialogTriggerProps): ReactElem let [, content] = props.children as [ReactElement, ReactElement]; let onBlurWithin = (e) => { - if (e.relatedTarget && popoverRef.current && (!popoverRef?.current?.UNSAFE_getDOMNode()?.contains(e.relatedTarget) && !(e.relatedTarget === triggerRef.current && getInteractionModality() === 'pointer'))) { + if (e.relatedTarget && popoverRef.current && (!popoverRef.current.UNSAFE_getDOMNode()?.contains(e.relatedTarget) && !(e.relatedTarget === triggerRef.current && getInteractionModality() === 'pointer'))) { if (submenuTriggerState.isOpen) { submenuTriggerState.close(); } @@ -143,7 +143,7 @@ function ContextualHelpTrigger(props: InternalMenuDialogTriggerProps): ReactElem UNSAFE_className={classNames(styles, 'spectrum-Submenu-popover')} onDismissButtonPress={onDismissButtonPress} onBlurWithin={onBlurWithin} - container={popoverContainer} + container={popoverContainer!} state={submenuTriggerState} ref={popoverRef} triggerRef={triggerRef} @@ -189,5 +189,5 @@ ContextualHelpTrigger.getCollectionNode = function* getCollectionNode(props: }; }; -let _Item = ContextualHelpTrigger as (props: SpectrumMenuDialogTriggerProps) => JSX.Element; +let _Item = ContextualHelpTrigger as unknown as (props: SpectrumMenuDialogTriggerProps) => JSX.Element; export {_Item as ContextualHelpTrigger}; diff --git a/packages/@react-spectrum/menu/src/Menu.tsx b/packages/@react-spectrum/menu/src/Menu.tsx index 092cc03c247..8864f9a09c5 100644 --- a/packages/@react-spectrum/menu/src/Menu.tsx +++ b/packages/@react-spectrum/menu/src/Menu.tsx @@ -41,15 +41,15 @@ function Menu(props: SpectrumMenuProps, ref: DOMRef(null); + let trayContainerRef = useRef(null); let state = useTreeState(completeProps); let submenuRef = useRef(null); let {menuProps} = useMenu(completeProps, state, domRef); let {styleProps} = useStyleProps(completeProps); useSyncRef(contextProps, domRef); let [leftOffset, setLeftOffset] = useState({left: 0}); - let prevPopoverContainer = useRef(null); + let prevPopoverContainer = useRef(null); useEffect(() => { if (popoverContainer && prevPopoverContainer.current !== popoverContainer && leftOffset.left === 0) { prevPopoverContainer.current = popoverContainer; @@ -59,12 +59,17 @@ function Menu(props: SpectrumMenuProps, ref: DOMRef { - if (!popoverContainer?.contains(e.target) && !trayContainerRef.current?.contains(e.target)) { - rootMenuTriggerState.close(); + if (!popoverContainer?.contains(e.target as Node) && !trayContainerRef.current?.contains(e.target as Node)) { + rootMenuTriggerState?.close(); } }, isDisabled: isSubmenu || !hasOpenSubmenu @@ -141,7 +146,7 @@ export function TrayHeaderWrapper(props) { } }, [hasOpenSubmenu, isMobile]); - let timeoutRef = useRef(null); + let timeoutRef = useRef | null>(null); let handleBackButtonPress = () => { setTraySubmenuAnimation('spectrum-TraySubmenu-exit'); timeoutRef.current = setTimeout(() => { @@ -159,7 +164,7 @@ export function TrayHeaderWrapper(props) { // When opening submenu in tray, focus the first item in the submenu after animation completes // This fixes an issue with iOS VO where the closed submenu was getting focus - let focusTimeoutRef = useRef(null); + let focusTimeoutRef = useRef | null>(null); useEffect(() => { if (isMobile && isSubmenu && !hasOpenSubmenu && traySubmenuAnimation === 'spectrum-TraySubmenu-enter') { focusTimeoutRef.current = setTimeout(() => { diff --git a/packages/@react-spectrum/menu/src/MenuItem.tsx b/packages/@react-spectrum/menu/src/MenuItem.tsx index 872b015e80e..f867dc54600 100644 --- a/packages/@react-spectrum/menu/src/MenuItem.tsx +++ b/packages/@react-spectrum/menu/src/MenuItem.tsx @@ -60,7 +60,7 @@ export function MenuItem(props: MenuItemProps) { let ElementType: React.ElementType = item.props.href ? 'a' : 'div'; if (isSubmenuTrigger) { - isUnavailable = submenuTriggerContext.isUnavailable; + isUnavailable = submenuTriggerContext!.isUnavailable; } let isDisabled = state.disabledKeys.has(key); diff --git a/packages/@react-spectrum/menu/src/MenuSection.tsx b/packages/@react-spectrum/menu/src/MenuSection.tsx index 73418c73b02..cee9902bb80 100644 --- a/packages/@react-spectrum/menu/src/MenuSection.tsx +++ b/packages/@react-spectrum/menu/src/MenuSection.tsx @@ -40,7 +40,8 @@ export function MenuSection(props: MenuSectionProps) { let firstSectionKey = state.collection.getFirstKey(); let lastSectionKey = [...state.collection].filter(node => node.type === 'section').at(-1)?.key; let sectionIsFirst = firstSectionKey === item.key && state.collection.getFirstKey() === firstSectionKey; - let sectionIsLast = lastSectionKey === item.key && state.collection.getItem(state.collection.getLastKey()).parentKey === lastSectionKey; + let lastKey = state.collection.getLastKey(); + let sectionIsLast = lastSectionKey === item.key && lastKey != null && state.collection.getItem(lastKey)!.parentKey === lastSectionKey; return ( diff --git a/packages/@react-spectrum/menu/src/MenuTrigger.tsx b/packages/@react-spectrum/menu/src/MenuTrigger.tsx index 4d5b3c2104c..a80e90fcae3 100644 --- a/packages/@react-spectrum/menu/src/MenuTrigger.tsx +++ b/packages/@react-spectrum/menu/src/MenuTrigger.tsx @@ -23,10 +23,10 @@ import {useMenuTrigger} from '@react-aria/menu'; import {useMenuTriggerState} from '@react-stately/menu'; function MenuTrigger(props: SpectrumMenuTriggerProps, ref: DOMRef) { - let triggerRef = useRef(undefined); + let triggerRef = useRef(null); let domRef = useDOMRef(ref); let menuTriggerRef = domRef || triggerRef; - let menuRef = useRef(undefined); + let menuRef = useRef(null); let { children, align = 'start', diff --git a/packages/@react-spectrum/menu/src/SubmenuTrigger.tsx b/packages/@react-spectrum/menu/src/SubmenuTrigger.tsx index 94f5bdaf35b..902870bc922 100644 --- a/packages/@react-spectrum/menu/src/SubmenuTrigger.tsx +++ b/packages/@react-spectrum/menu/src/SubmenuTrigger.tsx @@ -33,15 +33,15 @@ interface SubmenuTriggerProps { export interface SpectrumSubmenuTriggerProps extends Omit {} function SubmenuTrigger(props: SubmenuTriggerProps) { - let triggerRef = useRef(undefined); + let triggerRef = useRef(null); let { children, targetKey } = props; let [menuTrigger, menu] = React.Children.toArray(children); - let {popoverContainer, trayContainerRef, menu: parentMenuRef, submenu: menuRef, rootMenuTriggerState} = useMenuStateContext(); - let submenuTriggerState = useSubmenuTriggerState({triggerKey: targetKey}, rootMenuTriggerState); + let {popoverContainer, trayContainerRef, menu: parentMenuRef, submenu: menuRef, rootMenuTriggerState} = useMenuStateContext()!; + let submenuTriggerState = useSubmenuTriggerState({triggerKey: targetKey}, rootMenuTriggerState!); let {submenuTriggerProps, submenuProps, popoverProps} = useSubmenuTrigger({ parentMenuRef, submenuRef: menuRef @@ -59,12 +59,12 @@ function SubmenuTrigger(props: SubmenuTriggerProps) { switch (e.key) { case 'ArrowLeft': if (direction === 'ltr') { - triggerRef.current.focus(); + triggerRef.current?.focus(); } break; case 'ArrowRight': if (direction === 'rtl') { - triggerRef.current.focus(); + triggerRef.current?.focus(); } break; } @@ -90,7 +90,7 @@ function SubmenuTrigger(props: SubmenuTriggerProps) { {...popoverProps} onDismissButtonPress={onDismissButtonPress} UNSAFE_className={classNames(styles, 'spectrum-Submenu-popover')} - container={popoverContainer} + container={popoverContainer!} containerPadding={0} enableBothDismissButtons UNSAFE_style={{clipPath: 'unset', overflow: 'visible', borderWidth: '0px'}} @@ -150,5 +150,5 @@ SubmenuTrigger.getCollectionNode = function* (props: SpectrumSubmenuTriggerProps }; }; -let _SubmenuTrigger = SubmenuTrigger as (props: SpectrumSubmenuTriggerProps) => JSX.Element; +let _SubmenuTrigger = SubmenuTrigger as unknown as (props: SpectrumSubmenuTriggerProps) => JSX.Element; export {_SubmenuTrigger as SubmenuTrigger}; diff --git a/packages/@react-spectrum/menu/src/context.ts b/packages/@react-spectrum/menu/src/context.ts index e6b092f7b32..94d1de8e256 100644 --- a/packages/@react-spectrum/menu/src/context.ts +++ b/packages/@react-spectrum/menu/src/context.ts @@ -11,7 +11,7 @@ */ import {DOMProps, FocusStrategy, HoverEvents, KeyboardEvents, PressEvents, RefObject} from '@react-types/shared'; -import React, {HTMLAttributes, MutableRefObject, useContext} from 'react'; +import React, {HTMLAttributes, useContext} from 'react'; import {RootMenuTriggerState} from '@react-stately/menu'; import {TreeState} from '@react-stately/tree'; @@ -20,7 +20,7 @@ export interface MenuContextValue extends Omit, 'aut closeOnSelect?: boolean, shouldFocusWrap?: boolean, autoFocus?: boolean | FocusStrategy, - ref?: MutableRefObject, + ref?: RefObject, state?: RootMenuTriggerState, onBackButtonPress?: () => void, submenuLevel?: number @@ -34,7 +34,7 @@ export function useMenuContext(): MenuContextValue { export interface SubmenuTriggerContextValue extends DOMProps, Pick, Pick, Pick { isUnavailable?: boolean, - triggerRef?: MutableRefObject, + triggerRef?: RefObject, 'aria-expanded'?: boolean | 'true' | 'false', 'aria-controls'?: string, 'aria-haspopup'?: 'dialog' | 'menu', @@ -43,21 +43,21 @@ export interface SubmenuTriggerContextValue extends DOMProps, Pick(undefined); -export function useSubmenuTriggerContext(): SubmenuTriggerContextValue { +export function useSubmenuTriggerContext() { return useContext(SubmenuTriggerContext); } export interface MenuStateContextValue { - state?: TreeState, - popoverContainer?: HTMLElement, - trayContainerRef?: RefObject, - menu?: RefObject, - submenu?: RefObject, + state: TreeState, + popoverContainer: HTMLElement | null, + trayContainerRef: RefObject, + menu: RefObject, + submenu: RefObject, rootMenuTriggerState?: RootMenuTriggerState } -export const MenuStateContext = React.createContext>(undefined); +export const MenuStateContext = React.createContext | undefined>(undefined); -export function useMenuStateContext(): MenuStateContextValue { +export function useMenuStateContext() { return useContext(MenuStateContext); } diff --git a/packages/@react-spectrum/menu/stories/Submenu.stories.tsx b/packages/@react-spectrum/menu/stories/Submenu.stories.tsx index 5b0fc90527f..8e5c496360b 100644 --- a/packages/@react-spectrum/menu/stories/Submenu.stories.tsx +++ b/packages/@react-spectrum/menu/stories/Submenu.stories.tsx @@ -235,7 +235,7 @@ let dynamicRenderItem = (item, Icon) => ( ); let dynamicRenderFuncSections = (item: ItemNode) => { - let Icon = iconMap[item.icon]; + let Icon = iconMap[item.icon!]; if (item.children) { if (item.isSection) { let key = item.name ?? item.textValue; diff --git a/packages/@react-spectrum/menu/test/SubMenuTrigger.test.tsx b/packages/@react-spectrum/menu/test/SubMenuTrigger.test.tsx index 8a119bef6b8..050adc174f3 100644 --- a/packages/@react-spectrum/menu/test/SubMenuTrigger.test.tsx +++ b/packages/@react-spectrum/menu/test/SubMenuTrigger.test.tsx @@ -802,7 +802,7 @@ describe('Submenu', function () { expect(trayDialog).toBeTruthy(); let backButton = within(trayDialog).getByRole('button'); expect(backButton).toHaveAttribute('aria-label', `Return to ${submenuTrigger1.textContent}`); - let menuHeader = within(trayDialog).getAllByText(submenuTrigger1.textContent)[0]; + let menuHeader = within(trayDialog).getAllByText(submenuTrigger1.textContent!)[0]; expect(menuHeader).toBeVisible(); expect(menuHeader.tagName).toBe('H1'); let submenuTrigger2 = submenu1Items[2]; @@ -825,7 +825,7 @@ describe('Submenu', function () { trayDialog = within(tray).getByRole('dialog'); backButton = within(trayDialog).getByRole('button'); expect(backButton).toHaveAttribute('aria-label', `Return to ${submenuTrigger2.textContent}`); - menuHeader = within(tray).getAllByText(submenuTrigger2.textContent)[0]; + menuHeader = within(tray).getAllByText(submenuTrigger2.textContent!)[0]; expect(menuHeader).toBeVisible(); expect(menuHeader.tagName).toBe('H1'); }); diff --git a/packages/@react-spectrum/overlays/src/Modal.tsx b/packages/@react-spectrum/overlays/src/Modal.tsx index 02b8c7f25b1..fe3a3abde21 100644 --- a/packages/@react-spectrum/overlays/src/Modal.tsx +++ b/packages/@react-spectrum/overlays/src/Modal.tsx @@ -18,9 +18,9 @@ import {Overlay} from './Overlay'; import {OverlayProps} from '@react-types/overlays'; import {OverlayTriggerState} from '@react-stately/overlays'; import overrideStyles from './overlays.css'; -import React, {forwardRef, MutableRefObject, ReactNode, useRef} from 'react'; +import React, {ForwardedRef, forwardRef, ReactNode, useRef} from 'react'; import {Underlay} from './Underlay'; -import {useViewportSize} from '@react-aria/utils'; +import {useObjectRef, useViewportSize} from '@react-aria/utils'; interface ModalProps extends AriaModalOverlayProps, StyleProps, Omit { children: ReactNode, @@ -30,7 +30,8 @@ interface ModalProps extends AriaModalOverlayProps, StyleProps, Omit + wrapperRef: RefObject, + children: ReactNode } function Modal(props: ModalProps, ref: DOMRef) { @@ -52,11 +53,12 @@ let typeMap = { fullscreenTakeover: 'fullscreenTakeover' }; -let ModalWrapper = forwardRef(function (props: ModalWrapperProps, ref: RefObject) { +let ModalWrapper = forwardRef(function (props: ModalWrapperProps, ref: ForwardedRef) { let {type, children, state, isOpen, wrapperRef} = props; - let typeVariant = typeMap[type]; + let typeVariant = type != null ? typeMap[type] : undefined; let {styleProps} = useStyleProps(props); - let {modalProps, underlayProps} = useModalOverlay(props, state, ref); + let objRef = useObjectRef(ref); + let {modalProps, underlayProps} = useModalOverlay(props, state, objRef); let wrapperClassName = classNames( modalStyles, @@ -96,7 +98,7 @@ let ModalWrapper = forwardRef(function (props: ModalWrapperProps, ref: RefObject
{children} diff --git a/packages/@react-spectrum/overlays/src/Popover.tsx b/packages/@react-spectrum/overlays/src/Popover.tsx index 79456c1aff2..59d16f4ac5b 100644 --- a/packages/@react-spectrum/overlays/src/Popover.tsx +++ b/packages/@react-spectrum/overlays/src/Popover.tsx @@ -14,11 +14,11 @@ import {AriaPopoverProps, DismissButton, PopoverAria, usePopover} from '@react-a import {classNames, useDOMRef, useStyleProps} from '@react-spectrum/utils'; import {DOMRef, RefObject, StyleProps} from '@react-types/shared'; import {FocusWithinProps, useFocusWithin} from '@react-aria/interactions'; -import {mergeProps, useLayoutEffect} from '@react-aria/utils'; +import {mergeProps, useLayoutEffect, useObjectRef} from '@react-aria/utils'; import {Overlay} from './Overlay'; import {OverlayTriggerState} from '@react-stately/overlays'; import overrideStyles from './overlays.css'; -import React, {forwardRef, MutableRefObject, ReactNode, useRef, useState} from 'react'; +import React, {ForwardedRef, forwardRef, ReactNode, useRef, useState} from 'react'; import styles from '@adobe/spectrum-css-temp/components/popover/vars.css'; import {Underlay} from './Underlay'; @@ -41,7 +41,7 @@ interface PopoverProps extends Omit + wrapperRef: RefObject } interface ArrowProps { @@ -85,7 +85,7 @@ function Popover(props: PopoverProps, ref: DOMRef) { ); } -const PopoverWrapper = forwardRef((props: PopoverWrapperProps, ref: RefObject) => { +const PopoverWrapper = forwardRef((props: PopoverWrapperProps, ref: ForwardedRef) => { let { children, isOpen, @@ -97,9 +97,10 @@ const PopoverWrapper = forwardRef((props: PopoverWrapperProps, ref: RefObject state.close() } = props; let {styleProps} = useStyleProps(props); + let objRef = useObjectRef(ref); let {size, borderWidth, arrowRef} = useArrowSize(); - const borderRadius = usePopoverBorderRadius(ref); + const borderRadius = usePopoverBorderRadius(objRef); let borderDiagonal = borderWidth * Math.SQRT2; let primary = size + borderDiagonal; let secondary = primary * 2; @@ -110,8 +111,8 @@ const PopoverWrapper = forwardRef((props: PopoverWrapperProps, ref: RefObject { children: ReactNode, @@ -30,7 +30,7 @@ interface TrayProps extends AriaModalOverlayProps, StyleProps, Omit + wrapperRef: RefObject } function Tray(props: TrayProps, ref: DOMRef) { @@ -47,7 +47,7 @@ function Tray(props: TrayProps, ref: DOMRef) { ); } -let TrayWrapper = forwardRef(function (props: TrayWrapperProps, ref: RefObject) { +let TrayWrapper = forwardRef(function (props: TrayWrapperProps, ref: ForwardedRef) { let { children, isOpen, @@ -56,11 +56,12 @@ let TrayWrapper = forwardRef(function (props: TrayWrapperProps, ref: RefObject {children} diff --git a/packages/@react-spectrum/pagination/README.md b/packages/@react-spectrum/pagination/README.md deleted file mode 100644 index 6b70e85e059..00000000000 --- a/packages/@react-spectrum/pagination/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# @react-spectrum/pagination - -This package is part of [react-spectrum](https://github.com/adobe/react-spectrum). See the repo for more details. diff --git a/packages/@react-spectrum/pagination/index.ts b/packages/@react-spectrum/pagination/index.ts deleted file mode 100644 index 1210ae1e402..00000000000 --- a/packages/@react-spectrum/pagination/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright 2020 Adobe. All rights reserved. - * This file is licensed to you under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. You may obtain a copy - * of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS - * OF ANY KIND, either express or implied. See the License for the specific language - * governing permissions and limitations under the License. - */ - -export * from './src'; diff --git a/packages/@react-spectrum/pagination/intl/ar-AE.json b/packages/@react-spectrum/pagination/intl/ar-AE.json deleted file mode 100644 index cfcc0eadff4..00000000000 --- a/packages/@react-spectrum/pagination/intl/ar-AE.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "page_count": "من {n, plural, one{# صفحة} other{# من الصفحات}}" -} diff --git a/packages/@react-spectrum/pagination/intl/bg-BG.json b/packages/@react-spectrum/pagination/intl/bg-BG.json deleted file mode 100644 index 92827108299..00000000000 --- a/packages/@react-spectrum/pagination/intl/bg-BG.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "page_count": "на {n, plural, one{# страница} other{# страници}}" -} diff --git a/packages/@react-spectrum/pagination/intl/cs-CZ.json b/packages/@react-spectrum/pagination/intl/cs-CZ.json deleted file mode 100644 index 23ba4acf599..00000000000 --- a/packages/@react-spectrum/pagination/intl/cs-CZ.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "page_count": "z {n, plural, one{# stránky} other{# stránek}}" -} diff --git a/packages/@react-spectrum/pagination/intl/da-DK.json b/packages/@react-spectrum/pagination/intl/da-DK.json deleted file mode 100644 index b18b0f46e3f..00000000000 --- a/packages/@react-spectrum/pagination/intl/da-DK.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "page_count": "af {n, plural, one{# side} other{# sider}}" -} diff --git a/packages/@react-spectrum/pagination/intl/de-DE.json b/packages/@react-spectrum/pagination/intl/de-DE.json deleted file mode 100644 index 35b350f4fda..00000000000 --- a/packages/@react-spectrum/pagination/intl/de-DE.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "page_count": "von {n, plural, one{# Seite} other{# Seiten}}" -} diff --git a/packages/@react-spectrum/pagination/intl/el-GR.json b/packages/@react-spectrum/pagination/intl/el-GR.json deleted file mode 100644 index a8eb1eaa124..00000000000 --- a/packages/@react-spectrum/pagination/intl/el-GR.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "page_count": "από {n, plural, one{# σελίδα} other{# σελίδες}}" -} diff --git a/packages/@react-spectrum/pagination/intl/en-US.json b/packages/@react-spectrum/pagination/intl/en-US.json deleted file mode 100644 index 01412cf3d11..00000000000 --- a/packages/@react-spectrum/pagination/intl/en-US.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "page_count": "of {n, plural, one{# page} other{# pages}}" -} diff --git a/packages/@react-spectrum/pagination/intl/es-ES.json b/packages/@react-spectrum/pagination/intl/es-ES.json deleted file mode 100644 index e92f6b4e96d..00000000000 --- a/packages/@react-spectrum/pagination/intl/es-ES.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "page_count": "de {n, plural, one{# página} other{# páginas}}" -} diff --git a/packages/@react-spectrum/pagination/intl/et-EE.json b/packages/@react-spectrum/pagination/intl/et-EE.json deleted file mode 100644 index 14ba6ba4321..00000000000 --- a/packages/@react-spectrum/pagination/intl/et-EE.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "page_count": "/ {n, plural, one{# lk} other{# lk}}" -} diff --git a/packages/@react-spectrum/pagination/intl/fi-FI.json b/packages/@react-spectrum/pagination/intl/fi-FI.json deleted file mode 100644 index 9b43ed3aeea..00000000000 --- a/packages/@react-spectrum/pagination/intl/fi-FI.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "page_count": "/ {n, plural, one{# sivu} other{# sivua}}" -} diff --git a/packages/@react-spectrum/pagination/intl/fr-FR.json b/packages/@react-spectrum/pagination/intl/fr-FR.json deleted file mode 100644 index dc3d2259d58..00000000000 --- a/packages/@react-spectrum/pagination/intl/fr-FR.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "page_count": "sur {n, plural, one{# page} other{# pages}}" -} diff --git a/packages/@react-spectrum/pagination/intl/he-IL.json b/packages/@react-spectrum/pagination/intl/he-IL.json deleted file mode 100644 index df85f8c2fff..00000000000 --- a/packages/@react-spectrum/pagination/intl/he-IL.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "page_count": "מתוך {n, plural, one{# דף} other{# דפים}}" -} diff --git a/packages/@react-spectrum/pagination/intl/hr-HR.json b/packages/@react-spectrum/pagination/intl/hr-HR.json deleted file mode 100644 index 1d04a6e9497..00000000000 --- a/packages/@react-spectrum/pagination/intl/hr-HR.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "page_count": "od {n, plural, one{# stranice} other{# stranica}}" -} diff --git a/packages/@react-spectrum/pagination/intl/hu-HU.json b/packages/@react-spectrum/pagination/intl/hu-HU.json deleted file mode 100644 index 0bf0c4d6987..00000000000 --- a/packages/@react-spectrum/pagination/intl/hu-HU.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "page_count": "/ {n, plural, one{# oldal} other{# oldalak}}" -} diff --git a/packages/@react-spectrum/pagination/intl/it-IT.json b/packages/@react-spectrum/pagination/intl/it-IT.json deleted file mode 100644 index 6181af5caca..00000000000 --- a/packages/@react-spectrum/pagination/intl/it-IT.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "page_count": "di {n, plural, one{# pagina} other{# pagine}}" -} diff --git a/packages/@react-spectrum/pagination/intl/ja-JP.json b/packages/@react-spectrum/pagination/intl/ja-JP.json deleted file mode 100644 index 80b494d444b..00000000000 --- a/packages/@react-spectrum/pagination/intl/ja-JP.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "page_count": "/ {n, plural, one{# ページ} other{# ページ}}" -} diff --git a/packages/@react-spectrum/pagination/intl/ko-KR.json b/packages/@react-spectrum/pagination/intl/ko-KR.json deleted file mode 100644 index 2b27dca6596..00000000000 --- a/packages/@react-spectrum/pagination/intl/ko-KR.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "page_count": "/ {n, plural, one{# 페이지} other{# 페이지}}" -} diff --git a/packages/@react-spectrum/pagination/intl/lt-LT.json b/packages/@react-spectrum/pagination/intl/lt-LT.json deleted file mode 100644 index e5f73140e09..00000000000 --- a/packages/@react-spectrum/pagination/intl/lt-LT.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "page_count": "iš {n, plural, one{# psl.} other{# psl.}}" -} diff --git a/packages/@react-spectrum/pagination/intl/lv-LV.json b/packages/@react-spectrum/pagination/intl/lv-LV.json deleted file mode 100644 index 854762cf431..00000000000 --- a/packages/@react-spectrum/pagination/intl/lv-LV.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "page_count": "no {n, plural, one{# lpp.} other{# lpp.}}" -} diff --git a/packages/@react-spectrum/pagination/intl/nb-NO.json b/packages/@react-spectrum/pagination/intl/nb-NO.json deleted file mode 100644 index 54b5f1fc4e5..00000000000 --- a/packages/@react-spectrum/pagination/intl/nb-NO.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "page_count": "av {n, plural, one{# side} other{# sider}}" -} diff --git a/packages/@react-spectrum/pagination/intl/nl-NL.json b/packages/@react-spectrum/pagination/intl/nl-NL.json deleted file mode 100644 index f1287ddeb39..00000000000 --- a/packages/@react-spectrum/pagination/intl/nl-NL.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "page_count": "van {n, plural, one{# pagina} other{# pagina's}}" -} diff --git a/packages/@react-spectrum/pagination/intl/pl-PL.json b/packages/@react-spectrum/pagination/intl/pl-PL.json deleted file mode 100644 index de6fb4af28a..00000000000 --- a/packages/@react-spectrum/pagination/intl/pl-PL.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "page_count": "z {n, plural, one{# strony} other{# stron}}" -} diff --git a/packages/@react-spectrum/pagination/intl/pt-BR.json b/packages/@react-spectrum/pagination/intl/pt-BR.json deleted file mode 100644 index e92f6b4e96d..00000000000 --- a/packages/@react-spectrum/pagination/intl/pt-BR.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "page_count": "de {n, plural, one{# página} other{# páginas}}" -} diff --git a/packages/@react-spectrum/pagination/intl/pt-PT.json b/packages/@react-spectrum/pagination/intl/pt-PT.json deleted file mode 100644 index e92f6b4e96d..00000000000 --- a/packages/@react-spectrum/pagination/intl/pt-PT.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "page_count": "de {n, plural, one{# página} other{# páginas}}" -} diff --git a/packages/@react-spectrum/pagination/intl/ro-RO.json b/packages/@react-spectrum/pagination/intl/ro-RO.json deleted file mode 100644 index fdade6f520b..00000000000 --- a/packages/@react-spectrum/pagination/intl/ro-RO.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "page_count": "din {n, plural, one{# pagină} other{# pagini}}" -} diff --git a/packages/@react-spectrum/pagination/intl/ru-RU.json b/packages/@react-spectrum/pagination/intl/ru-RU.json deleted file mode 100644 index 15e1a854993..00000000000 --- a/packages/@react-spectrum/pagination/intl/ru-RU.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "page_count": "из {n, plural, one{# стр.} other{# стр.}}" -} diff --git a/packages/@react-spectrum/pagination/intl/sk-SK.json b/packages/@react-spectrum/pagination/intl/sk-SK.json deleted file mode 100644 index 16828028139..00000000000 --- a/packages/@react-spectrum/pagination/intl/sk-SK.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "page_count": "z {n, plural, one{# strany} other{# strán}}" -} diff --git a/packages/@react-spectrum/pagination/intl/sl-SI.json b/packages/@react-spectrum/pagination/intl/sl-SI.json deleted file mode 100644 index 08cfce5c032..00000000000 --- a/packages/@react-spectrum/pagination/intl/sl-SI.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "page_count": "od {n, plural, one{# stran} other{# strani}}" -} diff --git a/packages/@react-spectrum/pagination/intl/sr-SP.json b/packages/@react-spectrum/pagination/intl/sr-SP.json deleted file mode 100644 index bde5332fc7f..00000000000 --- a/packages/@react-spectrum/pagination/intl/sr-SP.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "page_count": "od {n, plural, one{# strana} other{# strana}}" -} diff --git a/packages/@react-spectrum/pagination/intl/sv-SE.json b/packages/@react-spectrum/pagination/intl/sv-SE.json deleted file mode 100644 index 7fa0fa4faf4..00000000000 --- a/packages/@react-spectrum/pagination/intl/sv-SE.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "page_count": "av {n, plural, one{# sida} other{# sidor}}" -} diff --git a/packages/@react-spectrum/pagination/intl/tr-TR.json b/packages/@react-spectrum/pagination/intl/tr-TR.json deleted file mode 100644 index 9557e5d3ccc..00000000000 --- a/packages/@react-spectrum/pagination/intl/tr-TR.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "page_count": "/ {n, plural, one{# sayfa} other{# sayfa}}" -} diff --git a/packages/@react-spectrum/pagination/intl/uk-UA.json b/packages/@react-spectrum/pagination/intl/uk-UA.json deleted file mode 100644 index a5713af7eb3..00000000000 --- a/packages/@react-spectrum/pagination/intl/uk-UA.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "page_count": "із {n, plural, one{# сторінки} other{# стор.}}" -} diff --git a/packages/@react-spectrum/pagination/intl/zh-CN.json b/packages/@react-spectrum/pagination/intl/zh-CN.json deleted file mode 100644 index ed92dcce046..00000000000 --- a/packages/@react-spectrum/pagination/intl/zh-CN.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "page_count": "共 {n, plural, one{# 页} other{# 页}}" -} diff --git a/packages/@react-spectrum/pagination/intl/zh-TW.json b/packages/@react-spectrum/pagination/intl/zh-TW.json deleted file mode 100644 index c39b8687345..00000000000 --- a/packages/@react-spectrum/pagination/intl/zh-TW.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "page_count": "共 {n, plural, one{# 頁} other{# 頁}}" -} diff --git a/packages/@react-spectrum/pagination/package.json b/packages/@react-spectrum/pagination/package.json deleted file mode 100644 index 03b528ce7a8..00000000000 --- a/packages/@react-spectrum/pagination/package.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "name": "@react-spectrum/pagination", - "version": "3.0.0-alpha.1", - "description": "Spectrum UI components in React", - "license": "Apache-2.0", - "private": true, - "main": "dist/main.js", - "module": "dist/module.js", - "exports": { - "types": "./dist/types.d.ts", - "import": "./dist/import.mjs", - "require": "./dist/main.js" - }, - "types": "dist/types.d.ts", - "source": "src/index.ts", - "files": [ - "dist", - "src" - ], - "sideEffects": [ - "*.css" - ], - "targets": { - "main": { - "includeNodeModules": [ - "@adobe/spectrum-css-temp" - ] - }, - "module": { - "includeNodeModules": [ - "@adobe/spectrum-css-temp" - ] - } - }, - "repository": { - "type": "git", - "url": "https://github.com/adobe/react-spectrum" - }, - "dependencies": { - "@react-aria/i18n": "^3.1.0", - "@react-aria/pagination": "3.0.0-alpha.1", - "@react-spectrum/button": "^3.1.0", - "@react-spectrum/textfield": "^3.1.0", - "@react-spectrum/utils": "^3.1.0", - "@react-stately/pagination": "3.0.0-alpha.1", - "@react-types/pagination": "3.0.0-alpha.1", - "@spectrum-icons/ui": "^3.1.0", - "@swc/helpers": "^0.5.0" - }, - "devDependencies": { - "@adobe/spectrum-css-temp": "3.0.0-alpha.1" - }, - "peerDependencies": { - "@react-spectrum/provider": "^3.0.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - }, - "publishConfig": { - "access": "public" - } -} diff --git a/packages/@react-spectrum/pagination/src/Pagination.tsx b/packages/@react-spectrum/pagination/src/Pagination.tsx deleted file mode 100644 index 45faaf43a60..00000000000 --- a/packages/@react-spectrum/pagination/src/Pagination.tsx +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2020 Adobe. All rights reserved. - * This file is licensed to you under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. You may obtain a copy - * of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS - * OF ANY KIND, either express or implied. See the License for the specific language - * governing permissions and limitations under the License. - */ - -import {ActionButton} from '@react-spectrum/button'; -import ChevronLeftMedium from '@spectrum-icons/ui/ChevronLeftMedium'; -import ChevronRightMedium from '@spectrum-icons/ui/ChevronRightMedium'; -import {classNames} from '@react-spectrum/utils'; -// @ts-ignore -import intlMessages from '../intl/*.json'; -import {PaginationBase} from '@react-types/pagination'; -import React from 'react'; -import styles from '@adobe/spectrum-css-temp/components/pagination/vars.css'; -import {TextField} from '@react-spectrum/textfield'; -import typographyStyles from '@adobe/spectrum-css-temp/components/typography/vars.css'; -import {useLocale, useLocalizedStringFormatter} from '@react-aria/i18n'; -import {usePagination} from '@react-aria/pagination'; -import {usePaginationState} from '@react-stately/pagination'; -import {useProviderProps} from '@react-spectrum/provider'; - -export function PaginationInput(props: PaginationBase) { - props = Object.assign({}, {defaultValue: 1}, props); - props = useProviderProps(props); - let state = usePaginationState(props); - let {prevButtonProps, nextButtonProps, textProps} = usePagination(props, state); - let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/pagination'); - let {direction} = useLocale(); - const {maxValue} = props; - - return ( - - ); -} diff --git a/packages/@react-spectrum/pagination/src/index.ts b/packages/@react-spectrum/pagination/src/index.ts deleted file mode 100644 index e45a076c969..00000000000 --- a/packages/@react-spectrum/pagination/src/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright 2020 Adobe. All rights reserved. - * This file is licensed to you under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. You may obtain a copy - * of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS - * OF ANY KIND, either express or implied. See the License for the specific language - * governing permissions and limitations under the License. - */ - -/// - -export {PaginationInput} from './Pagination'; diff --git a/packages/@react-spectrum/pagination/stories/pagination.stories.js b/packages/@react-spectrum/pagination/stories/pagination.stories.js deleted file mode 100644 index 86415fc1e56..00000000000 --- a/packages/@react-spectrum/pagination/stories/pagination.stories.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2020 Adobe. All rights reserved. - * This file is licensed to you under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. You may obtain a copy - * of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS - * OF ANY KIND, either express or implied. See the License for the specific language - * governing permissions and limitations under the License. - */ - -import {action} from '@storybook/addon-actions'; -import {PaginationInput} from '../'; -import React from 'react'; - -export default { - title: 'PaginationInput' -}; - -export const Default = () => render({maxValue: '10', onChange: action('onChange')}); -export const Controlled = () => - render({maxValue: '50', value: '2', onChange: action('onChange')}); - -Controlled.story = { - name: 'controlled' -}; - -function render(props = {}) { - return ; -} diff --git a/packages/@react-spectrum/picker/src/Picker.tsx b/packages/@react-spectrum/picker/src/Picker.tsx index 1efdd979e3d..06bbd50cc14 100644 --- a/packages/@react-spectrum/picker/src/Picker.tsx +++ b/packages/@react-spectrum/picker/src/Picker.tsx @@ -64,10 +64,10 @@ function Picker(props: SpectrumPickerProps, ref: DOMRef>(undefined); - let triggerRef = useRef>(undefined); + let popoverRef = useRef>(null); + let triggerRef = useRef>(null); let unwrappedTriggerRef = useUnwrapDOMRef(triggerRef); - let listboxRef = useRef(undefined); + let listboxRef = useRef(null); let isLoadingInitial = props.isLoading && state.collection.size === 0; let isLoadingMore = props.isLoading && state.collection.size > 0; @@ -105,7 +105,7 @@ function Picker(props: SpectrumPickerProps, ref: DOMRef(undefined); let {scale} = useProvider(); let onResize = useCallback(() => { @@ -133,7 +133,7 @@ function Picker(props: SpectrumPickerProps, ref: DOMRef ( <>
Test label
- + {(item: any) => {item.name}} diff --git a/packages/@react-spectrum/provider/chromatic/Provider.stories.tsx b/packages/@react-spectrum/provider/chromatic/Provider.stories.tsx index d629c764105..a5f20b61434 100644 --- a/packages/@react-spectrum/provider/chromatic/Provider.stories.tsx +++ b/packages/@react-spectrum/provider/chromatic/Provider.stories.tsx @@ -114,7 +114,7 @@ const ResponsiveStyleTemplate = () => ( const CustomResponsivStylePropsTemplate = () => { let Breakpoint = () => { - let {matchedBreakpoints} = useBreakpoint(); + let {matchedBreakpoints} = useBreakpoint()!; let breakpoint = matchedBreakpoints[0]; let width = {base: 'size-1600', XS: 'size-2000', S: 'size-2400', M: 'size-3000', L: 'size-3400', XL: 'size-4600', XXL: 'size-6000'}; return ( @@ -141,7 +141,7 @@ const CustomResponsivStylePropsTemplate = () => { const BreakpointOmittedTemplate = () => { let Breakpoint = () => { - let {matchedBreakpoints} = useBreakpoint(); + let {matchedBreakpoints} = useBreakpoint()!; let breakpoint = matchedBreakpoints[0]; let width = {base: 'size-1600', S: 'size-2400', L: 'size-3400'}; return ( diff --git a/packages/@react-spectrum/provider/src/Provider.tsx b/packages/@react-spectrum/provider/src/Provider.tsx index d54665a955e..f86a7f64c07 100644 --- a/packages/@react-spectrum/provider/src/Provider.tsx +++ b/packages/@react-spectrum/provider/src/Provider.tsx @@ -23,7 +23,7 @@ import {DOMRef} from '@react-types/shared'; import {filterDOMProps, RouterProvider} from '@react-aria/utils'; import {I18nProvider, useLocale} from '@react-aria/i18n'; import {ModalProvider, useModalProvider} from '@react-aria/overlays'; -import {ProviderContext, ProviderProps} from '@react-types/provider'; +import {ProviderProps} from '@react-types/provider'; import React, {useContext, useEffect, useRef} from 'react'; import styles from '@adobe/spectrum-css-temp/components/page/vars.css'; import typographyStyles from '@adobe/spectrum-css-temp/components/typography/index.css'; @@ -34,7 +34,7 @@ import {version} from '../package.json'; const DEFAULT_BREAKPOINTS = {S: 640, M: 768, L: 1024, XL: 1280, XXL: 1536}; function Provider(props: ProviderProps, ref: DOMRef) { - let prevContext = useProvider(); + let prevContext = useContext(Context); let prevColorScheme = prevContext && prevContext.colorScheme; let prevBreakpoints = prevContext && prevContext.breakpoints; let { @@ -45,17 +45,17 @@ function Provider(props: ProviderProps, ref: DOMRef) { throw new Error('theme not found, the parent provider must have a theme provided'); } // Hooks must always be called. - let autoColorScheme = useColorScheme(theme, defaultColorScheme); + let autoColorScheme = useColorScheme(theme, defaultColorScheme || 'light'); let autoScale = useScale(theme); let {locale: prevLocale} = useLocale(); // if the new theme doesn't support the prevColorScheme, we must resort to the auto - let usePrevColorScheme = !!theme[prevColorScheme]; + let usePrevColorScheme = prevColorScheme ? !!theme[prevColorScheme] : false; // importance of color scheme props > parent > auto:(OS > default > omitted) let { colorScheme = usePrevColorScheme ? prevColorScheme : autoColorScheme, scale = prevContext ? prevContext.scale : autoScale, - locale = prevContext ? prevLocale : null, + locale = prevContext ? prevLocale : undefined, breakpoints = prevContext ? prevBreakpoints : DEFAULT_BREAKPOINTS, children, isQuiet, @@ -83,7 +83,7 @@ function Provider(props: ProviderProps, ref: DOMRef) { validationState }; - let matchedBreakpoints = useMatchedBreakpoints(breakpoints); + let matchedBreakpoints = useMatchedBreakpoints(breakpoints!); let filteredProps = {}; Object.entries(currentProps).forEach(([key, value]) => value !== undefined && (filteredProps[key] = value)); @@ -94,7 +94,7 @@ function Provider(props: ProviderProps, ref: DOMRef) { let contents = children; let domProps = filterDOMProps(otherProps); let {styleProps} = useStyleProps(otherProps, undefined, {matchedBreakpoints}); - if (!prevContext || props.locale || theme !== prevContext.theme || colorScheme !== prevContext.colorScheme || scale !== prevContext.scale || Object.keys(domProps).length > 0 || otherProps.UNSAFE_className || Object.keys(styleProps.style).length > 0) { + if (!prevContext || props.locale || theme !== prevContext.theme || colorScheme !== prevContext.colorScheme || scale !== prevContext.scale || Object.keys(domProps).length > 0 || otherProps.UNSAFE_className || (styleProps.style && Object.keys(styleProps.style).length > 0)) { contents = ( {contents} @@ -138,15 +138,15 @@ const ProviderWrapper = React.forwardRef(function ProviderWrapper(props: Provide let {styleProps} = useStyleProps(otherProps); let domRef = useDOMRef(ref); - let themeKey = Object.keys(theme[colorScheme])[0]; - let scaleKey = Object.keys(theme[scale])[0]; + let themeKey = Object.keys(theme[colorScheme]!)[0]; + let scaleKey = Object.keys(theme[scale]!)[0]; let className = clsx( styleProps.className, styles['spectrum'], typographyStyles['spectrum'], - Object.values(theme[colorScheme]), - Object.values(theme[scale]), + Object.values(theme[colorScheme]!), + Object.values(theme[scale]!), theme.global ? Object.values(theme.global) : null, { 'react-spectrum-provider': shouldKeepSpectrumClassNames, @@ -166,7 +166,7 @@ const ProviderWrapper = React.forwardRef(function ProviderWrapper(props: Provide let hasWarned = useRef(false); useEffect(() => { if (direction && domRef.current) { - let closestDir = domRef.current.parentElement.closest('[dir]'); + let closestDir = domRef.current?.parentElement?.closest('[dir]'); let dir = closestDir && closestDir.getAttribute('dir'); if (dir && dir !== direction && !hasWarned.current) { console.warn(`Language directions cannot be nested. ${direction} inside ${dir}.`); @@ -195,12 +195,16 @@ const ProviderWrapper = React.forwardRef(function ProviderWrapper(props: Provide * Returns the various settings and styles applied by the nearest parent Provider. * Properties explicitly set by the nearest parent Provider override those provided by preceeding Providers. */ -export function useProvider(): ProviderContext { - return useContext(Context); +export function useProvider() { + let context = useContext(Context); + if (!context) { + throw new Error('No root provider found.'); + } + return context; } export function useProviderProps(props: T) : T { - let context = useProvider(); + let context = useContext(Context); if (!context) { return props; } diff --git a/packages/@react-spectrum/provider/src/index.ts b/packages/@react-spectrum/provider/src/index.ts index 31a2ed296bc..a0cd3581c0d 100644 --- a/packages/@react-spectrum/provider/src/index.ts +++ b/packages/@react-spectrum/provider/src/index.ts @@ -13,5 +13,6 @@ /// export {Provider, useProvider, useProviderProps} from './Provider'; +export {Context} from './context'; export type {ProviderContext} from '@react-types/provider'; export type {ProviderProps} from '@react-types/provider'; diff --git a/packages/@react-spectrum/provider/stories/Provider.stories.tsx b/packages/@react-spectrum/provider/stories/Provider.stories.tsx index 535949dce79..931b9e0b014 100644 --- a/packages/@react-spectrum/provider/stories/Provider.stories.tsx +++ b/packages/@react-spectrum/provider/stories/Provider.stories.tsx @@ -144,7 +144,7 @@ ResponsiveStyleProps.story = { export const CustomResponsiveStyleProps = () => { let Breakpoint = () => { - let {matchedBreakpoints} = useBreakpoint(); + let {matchedBreakpoints} = useBreakpoint()!; let breakpoint = matchedBreakpoints[0]; let width = { base: 'size-1600', @@ -177,7 +177,7 @@ CustomResponsiveStyleProps.story = { export const BreakpointOmitted = () => { let Breakpoint = () => { - let {matchedBreakpoints} = useBreakpoint(); + let {matchedBreakpoints} = useBreakpoint()!; let breakpoint = matchedBreakpoints[0]; let width = {base: 'size-1600', S: 'size-2400', L: 'size-3400'}; return ( diff --git a/packages/@react-spectrum/provider/test/Provider.test.tsx b/packages/@react-spectrum/provider/test/Provider.test.tsx index 5c7fbca3d87..ca5fee8ef3f 100644 --- a/packages/@react-spectrum/provider/test/Provider.test.tsx +++ b/packages/@react-spectrum/provider/test/Provider.test.tsx @@ -14,12 +14,13 @@ import {act, fireEvent, pointerMap, render} from '@react-spectrum/test-utils-int import {ActionButton, Button} from '@react-spectrum/button'; import {Checkbox} from '@react-spectrum/checkbox'; import MatchMediaMock from 'jest-matchmedia-mock'; + import {Provider} from '../'; -// eslint-disable-next-line rulesdir/useLayoutEffectRule -import React, {useLayoutEffect, useRef} from 'react'; +import React, {useRef} from 'react'; import {Switch} from '@react-spectrum/switch'; import {TextField} from '@react-spectrum/textfield'; import {useBreakpoint} from '@react-spectrum/utils'; +import {useLayoutEffect} from '@react-aria/utils'; import userEvent from '@testing-library/user-event'; let theme = { @@ -254,9 +255,9 @@ describe('Provider', () => { it('only renders once for multiple resizes in the same range', function () { function Component(props) { - let {matchedBreakpoints} = useBreakpoint(); + let {matchedBreakpoints} = useBreakpoint()!; let {onBreakpointChange, ...otherProps} = props; - let prevBreakpoint = useRef(null); + let prevBreakpoint = useRef(null); let breakpoint = matchedBreakpoints[0]; useLayoutEffect(() => { if (!Object.is(prevBreakpoint.current, breakpoint)) { diff --git a/packages/@react-spectrum/s2/chromatic/CheckboxGroup.stories.tsx b/packages/@react-spectrum/s2/chromatic/CheckboxGroup.stories.tsx index 5bd900e1753..d914ce16b1f 100644 --- a/packages/@react-spectrum/s2/chromatic/CheckboxGroup.stories.tsx +++ b/packages/@react-spectrum/s2/chromatic/CheckboxGroup.stories.tsx @@ -160,7 +160,6 @@ export const ContextualHelpStories = { containerStyle: style({display: 'grid', gridTemplateColumns: 'repeat(5, minmax(0, 250px))', gridAutoFlow: 'row', alignItems: 'center', justifyItems: 'start', gap: 24, width: '[100vw]'}), orientation: 'horizontal', contextualHelp: ( - // @ts-ignore What is a segment? Segments identify who your visitors are, what devices and services they use, where they navigated from, and much more. diff --git a/packages/@react-spectrum/s2/chromatic/ColorSlider.stories.tsx b/packages/@react-spectrum/s2/chromatic/ColorSlider.stories.tsx index 373634aab3c..e8a2728c24e 100644 --- a/packages/@react-spectrum/s2/chromatic/ColorSlider.stories.tsx +++ b/packages/@react-spectrum/s2/chromatic/ColorSlider.stories.tsx @@ -31,7 +31,6 @@ let states = [ {isDisabled: true}, {label: [null, 'custom label']}, {contextualHelp: ( - // @ts-ignore What is a segment? Segments identify who your visitors are, what devices and services they use, where they navigated from, and much more. diff --git a/packages/@react-spectrum/s2/chromatic/utils.tsx b/packages/@react-spectrum/s2/chromatic/utils.tsx index 645a8ecebc3..e0ffca20578 100644 --- a/packages/@react-spectrum/s2/chromatic/utils.tsx +++ b/packages/@react-spectrum/s2/chromatic/utils.tsx @@ -79,11 +79,9 @@ export function shortName(key, value) { export function generateComboChunks(opts: {states: Array, exclude?: (merged: any) => boolean, numChunks: number}) { let {states, exclude, numChunks} = opts; let combos = generatePowerset(states, exclude); - let chunks = []; + let chunks: any[] = []; let chunkSize = Math.ceil(combos.length / numChunks); for (let i = 0; i < numChunks; i++) { - // Not exactly sure why it is complaining about type never - // @ts-ignore chunks.push(combos.slice(i * chunkSize, (i + 1) * chunkSize)); } diff --git a/packages/@react-spectrum/s2/src/ActionMenu.tsx b/packages/@react-spectrum/s2/src/ActionMenu.tsx index 4e9437db079..d322f325075 100644 --- a/packages/@react-spectrum/s2/src/ActionMenu.tsx +++ b/packages/@react-spectrum/s2/src/ActionMenu.tsx @@ -65,7 +65,6 @@ function ActionMenu(props: ActionMenuProps, ref: FocusableR disabledKeys={props.disabledKeys} onAction={props.onAction} size={props.menuSize}> - {/* @ts-ignore TODO: fix type, right now this component is the same as Menu */} {props.children} diff --git a/packages/@react-spectrum/s2/src/CardView.tsx b/packages/@react-spectrum/s2/src/CardView.tsx index d7c9b11d43c..d128d73eb06 100644 --- a/packages/@react-spectrum/s2/src/CardView.tsx +++ b/packages/@react-spectrum/s2/src/CardView.tsx @@ -74,7 +74,7 @@ class FlexibleGridLayout extends Layout, GridLayoutOpt minSpace = new Size(18, 18), maxColumns = Infinity } = invalidationContext.layoutOptions || {}; - let visibleWidth = this.virtualizer.visibleRect.width; + let visibleWidth = this.virtualizer!.visibleRect.width; // The max item width is always the entire viewport. // If the max item height is infinity, scale in proportion to the max width. @@ -102,8 +102,8 @@ class FlexibleGridLayout extends Layout, GridLayoutOpt // Compute the horizontal spacing and content height let horizontalSpacing = Math.floor((visibleWidth - numColumns * itemWidth) / (numColumns + 1)); - let rows = Math.ceil(this.virtualizer.collection.size / numColumns); - let iterator = this.virtualizer.collection[Symbol.iterator](); + let rows = Math.ceil(this.virtualizer!.collection.size / numColumns); + let iterator = this.virtualizer!.collection[Symbol.iterator](); let y = rows > 0 ? minSpace.height : 0; let newLayoutInfos = new Map(); let skeleton: Node | null = null; @@ -151,13 +151,13 @@ class FlexibleGridLayout extends Layout, GridLayoutOpt y += maxHeight + minSpace.height; // Keep adding skeleton rows until we fill the viewport - if (skeleton && row === rows - 1 && y < this.virtualizer.visibleRect.height) { + if (skeleton && row === rows - 1 && y < this.virtualizer!.visibleRect.height) { rows++; } } this.layoutInfos = newLayoutInfos; - this.contentSize = new Size(this.virtualizer.visibleRect.width, y); + this.contentSize = new Size(this.virtualizer!.visibleRect.width, y); } getLayoutInfo(key: Key): LayoutInfo { @@ -171,7 +171,7 @@ class FlexibleGridLayout extends Layout, GridLayoutOpt getVisibleLayoutInfos(rect: Rect): LayoutInfo[] { let layoutInfos: LayoutInfo[] = []; for (let layoutInfo of this.layoutInfos.values()) { - if (layoutInfo.rect.intersects(rect) || this.virtualizer.isPersistedKey(layoutInfo.key)) { + if (layoutInfo.rect.intersects(rect) || this.virtualizer!.isPersistedKey(layoutInfo.key)) { layoutInfos.push(layoutInfo); } } @@ -218,7 +218,7 @@ class WaterfallLayout extends Layout, GridLayoutOption minSpace = new Size(18, 18), maxColumns = Infinity } = invalidationContext.layoutOptions || {}; - let visibleWidth = this.virtualizer.visibleRect.width; + let visibleWidth = this.virtualizer!.visibleRect.width; // The max item width is always the entire viewport. // If the max item height is infinity, scale in proportion to the max width. @@ -277,13 +277,13 @@ class WaterfallLayout extends Layout, GridLayoutOption }; let skeletonCount = 0; - for (let node of this.virtualizer.collection) { + for (let node of this.virtualizer!.collection) { if (node.type === 'skeleton') { // Add skeleton cards until every column has at least one, and we fill the viewport. let startingHeights = [...columnHeights]; while ( !columnHeights.every((h, i) => h !== startingHeights[i]) || - Math.min(...columnHeights) < this.virtualizer.visibleRect.height + Math.min(...columnHeights) < this.virtualizer!.visibleRect.height ) { let key = `${node.key}-${skeletonCount++}`; let content = this.layoutInfos.get(key)?.content || {...node}; @@ -297,7 +297,7 @@ class WaterfallLayout extends Layout, GridLayoutOption // Reset all columns to the maximum for the next section let maxHeight = Math.max(...columnHeights); - this.contentSize = new Size(this.virtualizer.visibleRect.width, maxHeight); + this.contentSize = new Size(this.virtualizer!.visibleRect.width, maxHeight); this.layoutInfos = newLayoutInfos; this.numColumns = numColumns; } @@ -313,7 +313,7 @@ class WaterfallLayout extends Layout, GridLayoutOption getVisibleLayoutInfos(rect: Rect): LayoutInfo[] { let layoutInfos: LayoutInfo[] = []; for (let layoutInfo of this.layoutInfos.values()) { - if (layoutInfo.rect.intersects(rect) || this.virtualizer.isPersistedKey(layoutInfo.key)) { + if (layoutInfo.rect.intersects(rect) || this.virtualizer!.isPersistedKey(layoutInfo.key)) { layoutInfos.push(layoutInfo); } } @@ -344,7 +344,7 @@ class WaterfallLayout extends Layout, GridLayoutOption return null; } - let rect = new Rect(layoutInfo.rect.maxX, layoutInfo.rect.y, this.virtualizer.visibleRect.maxX - layoutInfo.rect.maxX, layoutInfo.rect.height); + let rect = new Rect(layoutInfo.rect.maxX, layoutInfo.rect.y, this.virtualizer!.visibleRect.maxX - layoutInfo.rect.maxX, layoutInfo.rect.height); let layoutInfos = this.getVisibleLayoutInfos(rect); let bestKey: Key | null = null; let bestDistance = Infinity; diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index 91f64e0b3ec..a8fa569bfe9 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -199,7 +199,7 @@ export class S2TableLayout extends UNSTABLE_TableLayout { // TableLayout's buildCollection always sets the body width to the max width between the header width, but // we want the body to be sticky and only as wide as the table so it is always in view if loading/empty if (children?.length === 0) { - layoutInfo.rect.width = this.virtualizer.visibleRect.width - 80; + layoutInfo.rect.width = this.virtualizer!.visibleRect.width - 80; } return [ @@ -212,7 +212,7 @@ export class S2TableLayout extends UNSTABLE_TableLayout { let layoutNode = super.buildLoader(node, x, y); let {layoutInfo} = layoutNode; layoutInfo.allowOverflow = true; - layoutInfo.rect.width = this.virtualizer.visibleRect.width; + layoutInfo.rect.width = this.virtualizer!.visibleRect.width; layoutInfo.isSticky = true; return layoutNode; } @@ -224,7 +224,7 @@ export class S2TableLayout extends UNSTABLE_TableLayout { layoutInfo.allowOverflow = true; // If loading or empty, we'll want the body to be sticky and centered if (children?.length === 0) { - layoutInfo.rect = new Rect(40, 40, this.virtualizer.visibleRect.width - 80, this.virtualizer.visibleRect.height - 80); + layoutInfo.rect = new Rect(40, 40, this.virtualizer!.visibleRect.width - 80, this.virtualizer!.visibleRect.height - 80); layoutInfo.isSticky = true; } diff --git a/packages/@react-spectrum/s2/src/useSpectrumContextProps.ts b/packages/@react-spectrum/s2/src/useSpectrumContextProps.ts index 1a87722b44b..69c0daaac14 100644 --- a/packages/@react-spectrum/s2/src/useSpectrumContextProps.ts +++ b/packages/@react-spectrum/s2/src/useSpectrumContextProps.ts @@ -19,7 +19,6 @@ import {RefObject} from '@react-types/shared'; export function useSpectrumContextProps(props: T & SlotProps, ref: ForwardedRef, context: Context>): [T, RefObject] { let ctx = useSlottedContext(context, props.slot) || {}; - // @ts-ignore - TS says "Type 'unique symbol' cannot be used as an index type." but not sure why. let {ref: contextRef, ...contextProps} = ctx as any; let mergedRef = useObjectRef(useMemo(() => mergeRefs(ref, contextRef), [ref, contextRef])); let mergedProps = mergeProps(contextProps, props) as unknown as T; diff --git a/packages/@react-spectrum/s2/style/spectrum-theme.ts b/packages/@react-spectrum/s2/style/spectrum-theme.ts index eec5f20d5b0..347120e82b3 100644 --- a/packages/@react-spectrum/s2/style/spectrum-theme.ts +++ b/packages/@react-spectrum/s2/style/spectrum-theme.ts @@ -311,7 +311,6 @@ let gridTrack = (value: GridTrack) => { }; let gridTrackSize = (value: GridTrackSize) => { - // @ts-ignore return value in baseSpacing ? baseSpacing[value] : value; }; diff --git a/packages/@react-spectrum/s2/style/style-macro.ts b/packages/@react-spectrum/s2/style/style-macro.ts index cc311db6d8a..d7cde4447f8 100644 --- a/packages/@react-spectrum/s2/style/style-macro.ts +++ b/packages/@react-spectrum/s2/style/style-macro.ts @@ -32,7 +32,6 @@ export function createMappedProperty(fn: (value: string, pro return {default: [fn(v[0], property), v[1]]}; } - // @ts-ignore let val = Array.isArray(values) ? value : values[String(value)]; return mapConditionalValue(val, value => { return [fn(value, property), valueMap.get(value)!]; @@ -78,7 +77,6 @@ export function createColorProperty(colors: PropertyValueMap { let css = opacity ? `rgb(from ${value} r g b / ${opacity}%)` : value; diff --git a/packages/@react-spectrum/slider/src/Slider.tsx b/packages/@react-spectrum/slider/src/Slider.tsx index 7c16db71ebb..6edcd71bd39 100644 --- a/packages/@react-spectrum/slider/src/Slider.tsx +++ b/packages/@react-spectrum/slider/src/Slider.tsx @@ -13,7 +13,7 @@ import {clamp} from '@react-aria/utils'; import {classNames} from '@react-spectrum/utils'; import {FocusableRef} from '@react-types/shared'; -import React from 'react'; +import React, {ReactNode} from 'react'; import {SliderBase, SliderBaseChildArguments, SliderBaseProps} from './SliderBase'; import {SliderThumb} from './SliderThumb'; import {SpectrumSliderProps} from '@react-types/slider'; @@ -80,7 +80,7 @@ function Slider(props: SpectrumSliderProps, ref: FocusableRef) { }} /> ); - let filledTrack = null; + let filledTrack: ReactNode = null; if (isFilled && fillOffset != null) { let width = state.getThumbPercent(0) - state.getValuePercent(fillOffset); let isRightOfOffset = width > 0; diff --git a/packages/@react-spectrum/slider/src/SliderBase.tsx b/packages/@react-spectrum/slider/src/SliderBase.tsx index 824a66fbcbd..f5264a3c2e7 100644 --- a/packages/@react-spectrum/slider/src/SliderBase.tsx +++ b/packages/@react-spectrum/slider/src/SliderBase.tsx @@ -57,12 +57,10 @@ function SliderBase(props: SliderBaseProps, ref: FocusableRef) { if (!('signDisplay' in formatOptions)) { formatOptions = { ...formatOptions, - // @ts-ignore signDisplay: 'exceptZero' }; } } else { - // @ts-ignore formatOptions = {signDisplay: 'exceptZero'}; } } @@ -74,7 +72,7 @@ function SliderBase(props: SliderBaseProps, ref: FocusableRef) { minValue, maxValue }); - let trackRef = useRef(undefined); + let trackRef = useRef(null); let { groupProps, trackProps, @@ -82,11 +80,11 @@ function SliderBase(props: SliderBaseProps, ref: FocusableRef) { outputProps } = useSlider(props, state, trackRef); - let inputRef = useRef(undefined); + let inputRef = useRef(null); let domRef = useFocusableRef(ref, inputRef); let displayValue = ''; - let maxLabelLength = undefined; + let maxLabelLength: number | null = null; if (typeof getValueLabel === 'function') { displayValue = getValueLabel(state.values); @@ -142,7 +140,7 @@ function SliderBase(props: SliderBaseProps, ref: FocusableRef) { + style={maxLabelLength != null ? {width: `${maxLabelLength}ch`, minWidth: `${maxLabelLength}ch`} : undefined}> {displayValue} ); diff --git a/packages/@react-spectrum/slider/src/SliderThumb.tsx b/packages/@react-spectrum/slider/src/SliderThumb.tsx index 8c2a8b0df17..52d5aa746f7 100644 --- a/packages/@react-spectrum/slider/src/SliderThumb.tsx +++ b/packages/@react-spectrum/slider/src/SliderThumb.tsx @@ -33,7 +33,7 @@ export function SliderThumb(props: SliderThumbProps) { inputRef, state } = props; - let backupRef = useRef(undefined); + let backupRef = useRef(null); inputRef = inputRef || backupRef; let {thumbProps, inputProps, isDragging, isFocused} = useSliderThumb({ diff --git a/packages/@react-spectrum/slider/stories/RangeSlider.stories.tsx b/packages/@react-spectrum/slider/stories/RangeSlider.stories.tsx index 6d7b9576a1f..a54e782f35f 100644 --- a/packages/@react-spectrum/slider/stories/RangeSlider.stories.tsx +++ b/packages/@react-spectrum/slider/stories/RangeSlider.stories.tsx @@ -77,7 +77,7 @@ FormatOptionsPercent.story = { name: 'formatOptions percent' }; -export const FormatOptionsCentimeter = (args) => // @ts-ignore TODO why is "unit" even missing? How well is it supported? +export const FormatOptionsCentimeter = (args) => render({...args, maxValue: 1000, formatOptions: {style: 'unit', unit: 'centimeter'}}); FormatOptionsCentimeter.story = { diff --git a/packages/@react-spectrum/slider/stories/Slider.stories.tsx b/packages/@react-spectrum/slider/stories/Slider.stories.tsx index cde0944b306..2cef002818e 100644 --- a/packages/@react-spectrum/slider/stories/Slider.stories.tsx +++ b/packages/@react-spectrum/slider/stories/Slider.stories.tsx @@ -95,7 +95,7 @@ FormatOptionsPercent.story = { name: 'formatOptions percent' }; -export const FormatOptionsCentimeter = (args) => // @ts-ignore TODO why is "unit" even missing? How well is it supported? +export const FormatOptionsCentimeter = (args) => render({...args, maxValue: 1000, formatOptions: {style: 'unit', unit: 'centimeter'}}); FormatOptionsCentimeter.story = { diff --git a/packages/@react-spectrum/slider/test/RangeSlider.test.tsx b/packages/@react-spectrum/slider/test/RangeSlider.test.tsx index 0df0c401dde..95c7096ba0a 100644 --- a/packages/@react-spectrum/slider/test/RangeSlider.test.tsx +++ b/packages/@react-spectrum/slider/test/RangeSlider.test.tsx @@ -39,7 +39,7 @@ describe('RangeSlider', function () { let {getAllByRole, getByRole} = render(); let group = getByRole('group'); - let labelId = group.getAttribute('aria-labelledby'); + let labelId = group.getAttribute('aria-labelledby')!; let [leftSlider, rightSlider] = getAllByRole('slider'); expect(leftSlider.getAttribute('aria-label')).toBe('Minimum'); expect(rightSlider.getAttribute('aria-label')).toBe('Maximum'); @@ -131,7 +131,7 @@ describe('RangeSlider', function () { }); it('can be controlled', function () { - let setValues = []; + let setValues: any[] = []; function Test() { let [value, _setValue] = useState({start: 20, end: 40}); @@ -327,12 +327,15 @@ describe('RangeSlider', function () { describe('mouse interactions', () => { beforeAll(() => { - // @ts-ignore - jest.spyOn(window.HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(() => ({top: 0, left: 0, width: 100, height: 100})); + let originalGetBoundingClientRect = window.HTMLElement.prototype.getBoundingClientRect; + jest.spyOn(window.HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(function (this: HTMLElement) { + let rect = originalGetBoundingClientRect.call(this); + return {...rect, top: 0, left: 0, width: 100, height: 100}; + }); }); + let oldMouseEvent = MouseEvent; beforeAll(() => { - let oldMouseEvent = MouseEvent; // @ts-ignore global.MouseEvent = class FakeMouseEvent extends MouseEvent { _init: {pageX: number, pageY: number}; @@ -347,12 +350,9 @@ describe('RangeSlider', function () { return this._init.pageY; } }; - // @ts-ignore - global.MouseEvent.oldMouseEvent = oldMouseEvent; }); afterAll(() => { - // @ts-ignore - global.MouseEvent = global.MouseEvent.oldMouseEvent; + global.MouseEvent = oldMouseEvent; }); it('can click and drag handle', () => { @@ -365,7 +365,7 @@ describe('RangeSlider', function () { ); let [sliderLeft, sliderRight] = getAllByRole('slider'); - let [thumbLeft, thumbRight] = [sliderLeft.parentElement.parentElement, sliderRight.parentElement.parentElement]; + let [thumbLeft, thumbRight] = [sliderLeft.parentElement!.parentElement!, sliderRight.parentElement!.parentElement!]; fireEvent.mouseDown(thumbLeft, {clientX: 20, pageX: 20}); expect(onChangeSpy).not.toHaveBeenCalled(); @@ -411,7 +411,7 @@ describe('RangeSlider', function () { ); let [sliderLeft, sliderRight] = getAllByRole('slider'); - let [thumbLeft, thumbRight] = [sliderLeft.parentElement.parentElement, sliderRight.parentElement.parentElement]; + let [thumbLeft, thumbRight] = [sliderLeft.parentElement!.parentElement!, sliderRight.parentElement!.parentElement!]; fireEvent.mouseDown(thumbLeft, {clientX: 20, pageX: 20}); expect(onChangeSpy).not.toHaveBeenCalled(); @@ -450,10 +450,9 @@ describe('RangeSlider', function () { ); let [sliderLeft, sliderRight] = getAllByRole('slider'); - let [thumbLeft, thumbRight] = [sliderLeft.parentElement.parentElement, sliderRight.parentElement.parentElement]; + let [thumbLeft, thumbRight] = [sliderLeft.parentElement!.parentElement!, sliderRight.parentElement!.parentElement!]; - // @ts-ignore - let [leftTrack, middleTrack, rightTrack] = [...thumbLeft.parentElement.children].filter(c => c !== thumbLeft && c !== thumbRight); + let [leftTrack, middleTrack, rightTrack] = [...thumbLeft.parentElement!.children].filter(c => c !== thumbLeft && c !== thumbRight); // left track fireEvent.mouseDown(leftTrack, {clientX: 20, pageX: 20}); @@ -502,10 +501,9 @@ describe('RangeSlider', function () { ); let [sliderLeft, sliderRight] = getAllByRole('slider'); - let [thumbLeft, thumbRight] = [sliderLeft.parentElement.parentElement, sliderRight.parentElement.parentElement]; + let [thumbLeft, thumbRight] = [sliderLeft.parentElement!.parentElement!, sliderRight.parentElement!.parentElement!]; - // @ts-ignore - let [leftTrack, middleTrack, rightTrack] = [...thumbLeft.parentElement.children].filter(c => c !== thumbLeft && c !== thumbRight); + let [leftTrack, middleTrack, rightTrack] = [...thumbLeft.parentElement!.children].filter(c => c !== thumbLeft && c !== thumbRight); // left track fireEvent.mouseDown(leftTrack, {clientX: 20, pageX: 20}); diff --git a/packages/@react-spectrum/slider/test/Slider.test.tsx b/packages/@react-spectrum/slider/test/Slider.test.tsx index a64f6d50b35..c5bcba26e04 100644 --- a/packages/@react-spectrum/slider/test/Slider.test.tsx +++ b/packages/@react-spectrum/slider/test/Slider.test.tsx @@ -41,7 +41,7 @@ describe('Slider', function () { let {getByRole} = render(); let group = getByRole('group'); - let labelId = group.getAttribute('aria-labelledby'); + let labelId = group.getAttribute('aria-labelledby')!; let slider = getByRole('slider'); expect(slider.getAttribute('aria-labelledby')).toBe(labelId); expect(slider).toHaveAttribute('aria-valuetext', '0'); @@ -145,7 +145,7 @@ describe('Slider', function () { }); it('can be controlled', function () { - let setValues = []; + let setValues: any[] = []; function Test() { let [value, _setValue] = useState(50); @@ -393,8 +393,11 @@ describe('Slider', function () { describe('mouse interactions', () => { beforeAll(() => { - // @ts-ignore - jest.spyOn(window.HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(() => ({top: 0, left: 0, width: 100, height: 100})); + let originalGetBoundingClientRect = window.HTMLElement.prototype.getBoundingClientRect; + jest.spyOn(window.HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(function (this: HTMLElement) { + let rect = originalGetBoundingClientRect.call(this); + return {...rect, top: 0, left: 0, width: 100, height: 100}; + }); }); installMouseEvent(); @@ -409,7 +412,7 @@ describe('Slider', function () { ); let slider = getByRole('slider'); - let thumb = slider.parentElement; + let thumb = slider.parentElement!; fireEvent.mouseDown(thumb, {clientX: 50, pageX: 50}); expect(onChangeSpy).not.toHaveBeenCalled(); expect(document.activeElement).toBe(slider); @@ -438,7 +441,7 @@ describe('Slider', function () { ); let slider = getByRole('slider'); - let thumb = slider.parentElement; + let thumb = slider.parentElement!; fireEvent.mouseDown(thumb, {clientX: 50, pageX: 50}); expect(onChangeSpy).not.toHaveBeenCalled(); expect(document.activeElement).not.toBe(slider); @@ -458,9 +461,8 @@ describe('Slider', function () { ); let slider = getByRole('slider'); - let thumb = slider.parentElement.parentElement; - // @ts-ignore - let [leftTrack, rightTrack] = [...thumb.parentElement.children].filter(c => c !== thumb); + let thumb = slider.parentElement!.parentElement!; + let [leftTrack, rightTrack] = [...thumb.parentElement!.children].filter(c => c !== thumb); // left track fireEvent.mouseDown(leftTrack, {clientX: 20, pageX: 20}); @@ -491,9 +493,8 @@ describe('Slider', function () { ); let slider = getByRole('slider'); - let thumb = slider.parentElement.parentElement; - // @ts-ignore - let [leftTrack, rightTrack] = [...thumb.parentElement.children].filter(c => c !== thumb); + let thumb = slider.parentElement!.parentElement!; + let [leftTrack, rightTrack] = [...thumb.parentElement!.children].filter(c => c !== thumb); // left track fireEvent.mouseDown(leftTrack, {clientX: 20, pageX: 20}); @@ -540,9 +541,8 @@ describe('Slider', function () { ); let slider = getByRole('slider'); - let thumb = slider.parentElement.parentElement; - // @ts-ignore - let [, rightTrack] = [...thumb.parentElement.children].filter(c => c !== thumb); + let thumb = slider.parentElement!.parentElement!; + let [, rightTrack] = [...thumb.parentElement!.children].filter(c => c !== thumb); fireEvent.touchStart(thumb, {changedTouches: [{identifier: 1, clientX: 50, pageX: 50}]}); expect(onChangeSpy).toHaveBeenCalledTimes(0); diff --git a/packages/@react-spectrum/style-macro-s1/src/spectrum-theme.ts b/packages/@react-spectrum/style-macro-s1/src/spectrum-theme.ts index ff5dccdee28..dee532d6564 100644 --- a/packages/@react-spectrum/style-macro-s1/src/spectrum-theme.ts +++ b/packages/@react-spectrum/style-macro-s1/src/spectrum-theme.ts @@ -404,7 +404,6 @@ let gridTrack = (value: GridTrack) => { }; let gridTrackSize = (value: GridTrackSize) => { - // @ts-ignore return value in baseSpacing ? baseSpacing[value] : value; }; diff --git a/packages/@react-spectrum/style-macro-s1/src/style-macro.ts b/packages/@react-spectrum/style-macro-s1/src/style-macro.ts index d72252cf335..85d6c7d8fbe 100644 --- a/packages/@react-spectrum/style-macro-s1/src/style-macro.ts +++ b/packages/@react-spectrum/style-macro-s1/src/style-macro.ts @@ -30,7 +30,6 @@ export function createMappedProperty(fn: (value: string, pro return {default: [fn(v[0], property), v[1]]}; } - // @ts-ignore let val = Array.isArray(values) ? value : values[String(value)]; return mapConditionalValue(val, value => { return [fn(value, property), valueMap.get(value)!]; @@ -48,7 +47,6 @@ export function createColorProperty(colors: PropertyValueMap { let css = opacity ? `rgb(from ${value} r g b / ${opacity}%)` : value; diff --git a/packages/@react-spectrum/table/src/InsertionIndicator.tsx b/packages/@react-spectrum/table/src/InsertionIndicator.tsx index e68a8d9ba98..39e40d5e271 100644 --- a/packages/@react-spectrum/table/src/InsertionIndicator.tsx +++ b/packages/@react-spectrum/table/src/InsertionIndicator.tsx @@ -26,11 +26,11 @@ export function InsertionIndicator(props: InsertionIndicatorProps) { let {dropState, dragAndDropHooks} = useTableContext(); const {target, rowProps} = props; - let ref = useRef(undefined); - let {dropIndicatorProps} = dragAndDropHooks.useDropIndicator(props, dropState, ref); + let ref = useRef(null); + let {dropIndicatorProps} = dragAndDropHooks!.useDropIndicator!(props, dropState!, ref); let {visuallyHiddenProps} = useVisuallyHidden(); - let isDropTarget = dropState.isDropTarget(target); + let isDropTarget = dropState!.isDropTarget(target); if (!isDropTarget && dropIndicatorProps['aria-hidden']) { return null; @@ -40,8 +40,8 @@ export function InsertionIndicator(props: InsertionIndicatorProps) {
diff --git a/packages/@react-spectrum/table/src/Resizer.tsx b/packages/@react-spectrum/table/src/Resizer.tsx index 3bc619cf97b..6b7c2dbd737 100644 --- a/packages/@react-spectrum/table/src/Resizer.tsx +++ b/packages/@react-spectrum/table/src/Resizer.tsx @@ -7,9 +7,9 @@ import {FocusRing} from '@react-aria/focus'; import {GridNode} from '@react-types/grid'; // @ts-ignore import intlMessages from '../intl/*.json'; -import {isWebKit, mergeProps} from '@react-aria/utils'; +import {isWebKit, mergeProps, useObjectRef} from '@react-aria/utils'; import {Key, RefObject} from '@react-types/shared'; -import React, {createContext, useContext, useEffect, useState} from 'react'; +import React, {createContext, ForwardedRef, useContext, useEffect, useState} from 'react'; import ReactDOM from 'react-dom'; import styles from '@adobe/spectrum-css-temp/components/table/vars.css'; import {TableColumnResizeState} from '@react-stately/table'; @@ -35,9 +35,9 @@ interface ResizerProps { column: GridNode, showResizer: boolean, triggerRef: RefObject, - onResizeStart: (widths: Map) => void, - onResize: (widths: Map) => void, - onResizeEnd: (widths: Map) => void + onResizeStart?: (widths: Map) => void, + onResize?: (widths: Map) => void, + onResizeEnd?: (widths: Map) => void } const CURSORS = { @@ -48,8 +48,9 @@ const CURSORS = { export const ResizeStateContext = createContext | null>(null); -function Resizer(props: ResizerProps, ref: RefObject) { +function Resizer(props: ResizerProps, ref: ForwardedRef) { let {column, showResizer} = props; + let objectRef = useObjectRef(ref); let {isEmpty, onFocusedResizer} = useTableContext(); let layout = useContext(ResizeStateContext)!; // Virtualizer re-renders, but these components are all cached @@ -83,7 +84,7 @@ function Resizer(props: ResizerProps, ref: RefObject= layout.getColumnWidth(column.key); let isWResizable = layout.getColumnMaxWidth(column.key) <= layout.getColumnWidth(column.key); @@ -113,7 +114,7 @@ function Resizer(props: ResizerProps, ref: RefObject
diff --git a/packages/@react-spectrum/table/src/RootDropIndicator.tsx b/packages/@react-spectrum/table/src/RootDropIndicator.tsx index 71eef9c056e..6c626999f0c 100644 --- a/packages/@react-spectrum/table/src/RootDropIndicator.tsx +++ b/packages/@react-spectrum/table/src/RootDropIndicator.tsx @@ -16,11 +16,11 @@ import {useVisuallyHidden} from '@react-aria/visually-hidden'; export function RootDropIndicator() { let {dropState, dragAndDropHooks, state} = useTableContext(); - let ref = useRef(undefined); - let {dropIndicatorProps} = dragAndDropHooks.useDropIndicator({ + let ref = useRef(null); + let {dropIndicatorProps} = dragAndDropHooks!.useDropIndicator!({ target: {type: 'root'} - }, dropState, ref); - let isDropTarget = dropState.isDropTarget({type: 'root'}); + }, dropState!, ref); + let isDropTarget = dropState!.isDropTarget({type: 'root'}); let {visuallyHiddenProps} = useVisuallyHidden(); if (!isDropTarget && dropIndicatorProps['aria-hidden']) { diff --git a/packages/@react-spectrum/table/src/TableViewBase.tsx b/packages/@react-spectrum/table/src/TableViewBase.tsx index 93babd7e1af..e788248cc5a 100644 --- a/packages/@react-spectrum/table/src/TableViewBase.tsx +++ b/packages/@react-spectrum/table/src/TableViewBase.tsx @@ -26,9 +26,9 @@ import {ColumnSize, SpectrumColumnProps, TableCollection} from '@react-types/tab import {DOMRef, DropTarget, FocusableElement, FocusableRef, Key, RefObject} from '@react-types/shared'; import type {DragAndDropHooks} from '@react-spectrum/dnd'; import type {DraggableCollectionState, DroppableCollectionState} from '@react-stately/dnd'; -import type {DraggableItemResult, DropIndicatorAria, DroppableCollectionResult, DroppableItemResult} from '@react-aria/dnd'; +import type {DraggableItemResult, DropIndicatorAria, DroppableCollectionResult} from '@react-aria/dnd'; import {FocusRing, FocusScope, useFocusRing} from '@react-aria/focus'; -import {getInteractionModality, isFocusVisible, useHover, usePress} from '@react-aria/interactions'; +import {getInteractionModality, HoverProps, isFocusVisible, useHover, usePress} from '@react-aria/interactions'; import {GridNode} from '@react-types/grid'; import {InsertionIndicator} from './InsertionIndicator'; // @ts-ignore @@ -108,9 +108,9 @@ const LEVEL_OFFSET_WIDTH = { export interface TableContextValue { state: TableState | TreeGridState, - dragState: DraggableCollectionState, - dropState: DroppableCollectionState, - dragAndDropHooks: DragAndDropHooks['dragAndDropHooks'], + dragState: DraggableCollectionState | null, + dropState: DroppableCollectionState | null, + dragAndDropHooks?: DragAndDropHooks['dragAndDropHooks'], isTableDraggable: boolean, isTableDroppable: boolean, layout: TableViewLayout, @@ -119,20 +119,20 @@ export interface TableContextValue { setIsInResizeMode: (val: boolean) => void, isEmpty: boolean, onFocusedResizer: () => void, - onResizeStart: (widths: Map) => void, - onResize: (widths: Map) => void, - onResizeEnd: (widths: Map) => void, + onResizeStart?: (widths: Map) => void, + onResize?: (widths: Map) => void, + onResizeEnd?: (widths: Map) => void, headerMenuOpen: boolean, setHeaderMenuOpen: (val: boolean) => void, renderEmptyState?: () => ReactElement } -export const TableContext = React.createContext>(null); +export const TableContext = React.createContext | null>(null); export function useTableContext() { - return useContext(TableContext); + return useContext(TableContext)!; } -export const VirtualizerContext = React.createContext(null); +export const VirtualizerContext = React.createContext<{width: number, key: Key | null} | null>(null); export function useVirtualizerContext() { return useContext(VirtualizerContext); } @@ -181,51 +181,51 @@ function TableViewBase(props: TableBaseProps, ref: DOMRef(undefined); - let bodyRef = useRef(undefined); + let headerRef = useRef(null); + let bodyRef = useRef(null); let density = props.density || 'regular'; - let layout = useMemo(() => new TableViewLayout({ + let layout = useMemo(() => new TableViewLayout({ // If props.rowHeight is auto, then use estimated heights based on scale, otherwise use fixed heights. rowHeight: props.overflowMode === 'wrap' - ? null + ? undefined : ROW_HEIGHTS[density][scale], estimatedRowHeight: props.overflowMode === 'wrap' ? ROW_HEIGHTS[density][scale] - : null, + : undefined, headingHeight: props.overflowMode === 'wrap' - ? null + ? undefined : DEFAULT_HEADER_HEIGHT[scale], estimatedHeadingHeight: props.overflowMode === 'wrap' ? DEFAULT_HEADER_HEIGHT[scale] - : null + : undefined }), // don't recompute when state.collection changes, only used for initial value - + [props.overflowMode, scale, density] ); - let dragState: DraggableCollectionState; + let dragState: DraggableCollectionState | null = null; let preview = useRef(null); - if (isTableDraggable) { - dragState = dragAndDropHooks.useDraggableCollectionState({ + if (isTableDraggable && dragAndDropHooks) { + dragState = dragAndDropHooks.useDraggableCollectionState!({ collection: state.collection, selectionManager: state.selectionManager, preview }); - dragAndDropHooks.useDraggableCollection({}, dragState, domRef); + dragAndDropHooks.useDraggableCollection!({}, dragState, domRef); } let DragPreview = dragAndDropHooks?.DragPreview; - let dropState: DroppableCollectionState; - let droppableCollection: DroppableCollectionResult; - let isRootDropTarget: boolean; - if (isTableDroppable) { - dropState = dragAndDropHooks.useDroppableCollectionState({ + let dropState: DroppableCollectionState | null = null; + let droppableCollection: DroppableCollectionResult | null = null; + let isRootDropTarget = false; + if (isTableDroppable && dragAndDropHooks) { + dropState = dragAndDropHooks.useDroppableCollectionState!({ collection: state.collection, selectionManager: state.selectionManager }); - droppableCollection = dragAndDropHooks.useDroppableCollection({ + droppableCollection = dragAndDropHooks.useDroppableCollection!({ keyboardDelegate: new ListKeyboardDelegate({ collection: state.collection, disabledKeys: state.selectionManager.disabledKeys, @@ -249,13 +249,13 @@ function TableViewBase(props: TableBaseProps, ref: DOMRef ReactElement[]) => { + let renderWrapper = useCallback((parent: View | null, reusableView: View, children: View[], renderChildren: (views: View[]) => ReactElement[]): ReactElement => { if (reusableView.viewType === 'rowgroup') { return ( (props: TableBaseProps, ref: DOMRef + layoutInfo={reusableView.layoutInfo!} + parent={parent?.layoutInfo ?? null}> {renderChildren(children)} ); @@ -280,9 +280,9 @@ function TableViewBase(props: TableBaseProps, ref: DOMRef + item={reusableView.content!} + layoutInfo={reusableView.layoutInfo!} + parent={parent?.layoutInfo ?? null}> {renderChildren(children)} ); @@ -293,9 +293,9 @@ function TableViewBase(props: TableBaseProps, ref: DOMRef + layoutInfo={reusableView.layoutInfo!} + parent={parent?.layoutInfo ?? null} + item={reusableView.content!}> {renderChildren(children)} ); @@ -304,9 +304,9 @@ function TableViewBase(props: TableBaseProps, ref: DOMRef + parent={parent!}> {reusableView.rendered} ); @@ -337,7 +337,7 @@ function TableViewBase(props: TableBaseProps, ref: DOMRef 1 ? item.colspan : null} /> + aria-colspan={item.colspan != null && item.colspan > 1 ? item.colspan : undefined} /> ); case 'column': if (item.props.isSelectionCell) { @@ -371,6 +371,7 @@ function TableViewBase(props: TableBaseProps, ref: DOMRef; } } + return null; }, []); let [isVerticalScrollbarVisible, setVerticalScollbarVisible] = useState(false); @@ -390,7 +391,9 @@ function TableViewBase(props: TableBaseProps, ref: DOMRef { - bodyRef.current.scrollLeft = headerRef.current.scrollLeft; + if (bodyRef.current && headerRef.current) { + bodyRef.current.scrollLeft = headerRef.current.scrollLeft; + } }; let onResizeStart = useCallback((widths) => { @@ -419,12 +422,15 @@ function TableViewBase(props: TableBaseProps, ref: DOMRef(props: TableBaseProps, ref: DOMRef - {DragPreview && isTableDraggable && + {DragPreview && isTableDraggable && dragAndDropHooks && dragState && {() => { + if (dragState.draggedKey == null) { + return null; + } if (dragAndDropHooks.renderPreview) { return dragAndDropHooks.renderPreview(dragState.draggingKeys, dragState.draggedKey); } let itemCount = dragState.draggingKeys.size; - let maxWidth = bodyRef.current.getBoundingClientRect().width; + let maxWidth = bodyRef.current!.getBoundingClientRect().width; let height = ROW_HEIGHTS[density][scale]; - let itemText = state.collection.getTextValue(dragState.draggedKey); + let itemText = state.collection.getTextValue!(dragState.draggedKey); return ; }} @@ -505,16 +514,16 @@ interface TableVirtualizerProps extends HTMLAttributes { layout: TableViewLayout, collection: TableCollection, persistedKeys: Set | null, - renderView: (type: string, content: GridNode) => ReactElement, - renderWrapper?: ( + renderView: (type: string, content: GridNode) => ReactElement | null, + renderWrapper: ( parent: View | null, reusableView: View, children: View[], renderChildren: (views: View[]) => ReactElement[] - ) => ReactElement, - domRef: RefObject, - bodyRef: RefObject, - headerRef: RefObject, + ) => ReactElement | null, + domRef: RefObject, + bodyRef: RefObject, + headerRef: RefObject, onVisibleRectChange: (rect: Rect) => void, isFocusVisible: boolean, isVirtualDragging: boolean, @@ -565,8 +574,10 @@ function TableVirtualizer(props: TableVirtualizerProps) { collection, renderView, onVisibleRectChange(rect) { - bodyRef.current.scrollTop = rect.y; - setScrollLeft(bodyRef.current, direction, rect.x); + if (bodyRef.current) { + bodyRef.current.scrollTop = rect.y; + setScrollLeft(bodyRef.current, direction, rect.x); + } }, persistedKeys, layoutOptions: useMemo(() => ({ @@ -589,7 +600,7 @@ function TableVirtualizer(props: TableVirtualizerProps) { // only that it changes in a resize, and when that happens, we want to sync the body to the // header scroll position useEffect(() => { - if (getInteractionModality() === 'keyboard' && headerRef.current.contains(document.activeElement)) { + if (getInteractionModality() === 'keyboard' && headerRef.current?.contains(document.activeElement) && bodyRef.current) { scrollIntoView(headerRef.current, document.activeElement as HTMLElement); scrollIntoViewport(document.activeElement, {containingElement: domRef.current}); bodyRef.current.scrollLeft = headerRef.current.scrollLeft; @@ -600,10 +611,12 @@ function TableVirtualizer(props: TableVirtualizerProps) { // Sync the scroll position from the table body to the header container. let onScroll = useCallback(() => { - headerRef.current.scrollLeft = bodyRef.current.scrollLeft; + if (headerRef.current && bodyRef.current) { + headerRef.current.scrollLeft = bodyRef.current.scrollLeft; + } }, [bodyRef, headerRef]); - let resizerPosition = columnResizeState.resizingColumn != null ? layout.getLayoutInfo(columnResizeState.resizingColumn).rect.maxX - 2 : 0; + let resizerPosition = columnResizeState.resizingColumn != null ? layout.getLayoutInfo(columnResizeState.resizingColumn)!.rect.maxX - 2 : 0; let resizerAtEdge = resizerPosition > Math.max(state.virtualizer.contentSize.width, state.virtualizer.visibleRect.width) - 3; // this should be fine, every movement of the resizer causes a rerender @@ -617,10 +630,10 @@ function TableVirtualizer(props: TableVirtualizerProps) { width: resizingColumnWidth, key: columnResizeState.resizingColumn }), [resizingColumnWidth, columnResizeState.resizingColumn]); - let mergedProps = mergeProps( - otherProps, - isVirtualDragging && {tabIndex: null} - ); + + if (isVirtualDragging) { + otherProps.tabIndex = undefined; + } let firstColumn = collection.columns[0]; let scrollPadding = 0; @@ -634,7 +647,7 @@ function TableVirtualizer(props: TableVirtualizerProps) {
(props: TableVirtualizerProps) { // Using tabIndex={-1} prevents the ScrollView from being tabbable, and using role="rowgroup" // here and role="presentation" on the table body content fixes the table structure. role="rowgroup" - tabIndex={isVirtualDragging ? null : -1} + tabIndex={isVirtualDragging ? undefined : -1} style={{ flex: 1, scrollPaddingInlineStart: scrollPadding @@ -697,7 +710,7 @@ function TableVirtualizer(props: TableVirtualizerProps) { ); } -function renderChildren(parent: View | null, views: View[], renderWrapper: TableVirtualizerProps['renderWrapper']) { +function renderChildren(parent: View | null, views: View[], renderWrapper: NonNullable['renderWrapper']>) { return views.map(view => { return renderWrapper( parent, @@ -717,7 +730,7 @@ function useStyle(layoutInfo: LayoutInfo, parent: LayoutInfo | null) { return style; } -function TableHeader({children, layoutInfo, parent, ...otherProps}) { +function TableHeader({children, layoutInfo, parent, ...otherProps}: {children: ReactNode, layoutInfo: LayoutInfo, parent: LayoutInfo | null}) { let {rowGroupProps} = useTableRowGroup(); let style = useStyle(layoutInfo, parent); @@ -845,7 +858,7 @@ function ResizableTableColumnHeader(props) { headerMenuOpen, setHeaderMenuOpen } = useTableContext(); - let columnResizeState = useContext(ResizeStateContext); + let columnResizeState = useContext(ResizeStateContext)!; let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/table'); let {pressProps, isPressed} = usePress({isDisabled: isEmpty}); let {columnHeaderProps} = useTableColumnHeader({ @@ -878,20 +891,21 @@ function ResizableTableColumnHeader(props) { }; let allowsSorting = column.props?.allowsSorting; let items = useMemo(() => { - let options = [ - allowsSorting ? { + let options: {label: string, id: string}[] = []; + if (allowsSorting) { + options.push({ label: stringFormatter.format('sortAscending'), id: 'sort-asc' - } : undefined, - allowsSorting ? { + }); + options.push({ label: stringFormatter.format('sortDescending'), id: 'sort-desc' - } : undefined, - { - label: stringFormatter.format('resizeColumn'), - id: 'resize' - } - ]; + }); + } + options.push({ + label: stringFormatter.format('resizeColumn'), + id: 'resize' + }); return options; // eslint-disable-next-line react-hooks/exhaustive-deps }, [allowsSorting]); @@ -992,7 +1006,7 @@ function ResizableTableColumnHeader(props) { } function TableSelectAllCell({column}) { - let ref = useRef(undefined); + let ref = useRef(null); let {state} = useTableContext(); let isSingleSelectionMode = state.selectionManager.selectionMode === 'single'; let {columnHeaderProps} = useTableColumnHeader({ @@ -1039,7 +1053,7 @@ function TableSelectAllCell({column}) { } function TableDragHeaderCell({column}) { - let ref = useRef(undefined); + let ref = useRef(null); let {state} = useTableContext(); let {columnHeaderProps} = useTableColumnHeader({ node: column, @@ -1069,9 +1083,9 @@ function TableDragHeaderCell({column}) { ); } -function TableRowGroup({children, layoutInfo, parent, ...otherProps}) { +function TableRowGroup({children, layoutInfo, parent, ...otherProps}: {children: ReactNode, layoutInfo: LayoutInfo, parent: LayoutInfo | null, role: string}) { let {rowGroupProps} = useTableRowGroup(); - let {isTableDroppable} = useContext(TableContext); + let {isTableDroppable} = useContext(TableContext)!; let style = useStyle(layoutInfo, parent); return ( @@ -1108,18 +1122,18 @@ function DragButton() { interface TableRowContextValue { dragButtonProps: React.HTMLAttributes, - dragButtonRef: React.MutableRefObject, + dragButtonRef: React.RefObject, isFocusVisibleWithin: boolean } -const TableRowContext = React.createContext(null); +const TableRowContext = React.createContext(null); export function useTableRowContext() { - return useContext(TableRowContext); + return useContext(TableRowContext)!; } -function TableRow({item, children, layoutInfo, parent, ...otherProps}) { - let ref = useRef(undefined); +function TableRow({item, children, layoutInfo, parent, ...otherProps}: {item: GridNode, children: ReactNode, layoutInfo: LayoutInfo, parent: LayoutInfo | null}) { + let ref = useRef(null); let {state, layout, dragAndDropHooks, isTableDraggable, isTableDroppable, dragState, dropState} = useTableContext(); let isSelected = state.selectionManager.isSelected(item.key); let {rowProps, hasAction, allowsSelection} = useTableRow({ @@ -1130,7 +1144,6 @@ function TableRow({item, children, layoutInfo, parent, ...otherProps}) { let isDisabled = state.selectionManager.isDisabled(item.key); let isInteractive = !isDisabled && (hasAction || allowsSelection || isTableDraggable); - let isDroppable = isTableDroppable && !isDisabled; let {pressProps, isPressed} = usePress({isDisabled: !isInteractive}); // The row should show the focus background style when any cell inside it is focused. @@ -1147,31 +1160,29 @@ function TableRow({item, children, layoutInfo, parent, ...otherProps}) { // border corners of the last row when selected. let isFlushWithContainerBottom = false; if (isLastRow) { - if (layout.getContentSize()?.height >= layout.virtualizer?.visibleRect.height) { + if (layout.getContentSize()?.height >= (layout.virtualizer?.visibleRect.height ?? 0)) { isFlushWithContainerBottom = true; } } - let draggableItem: DraggableItemResult; - if (isTableDraggable) { - - draggableItem = dragAndDropHooks.useDraggableItem({key: item.key, hasDragButton: true}, dragState); + let draggableItem: DraggableItemResult | null = null; + if (isTableDraggable && dragAndDropHooks && dragState) { + draggableItem = dragAndDropHooks.useDraggableItem!({key: item.key, hasDragButton: true}, dragState); if (isDisabled) { draggableItem = null; } } - let droppableItem: DroppableItemResult; - let isDropTarget: boolean; - let dropIndicator: DropIndicatorAria; - let dropIndicatorRef = useRef(undefined); - if (isTableDroppable) { + let isDropTarget = false; + let dropIndicator: DropIndicatorAria | null = null; + let dropIndicatorRef = useRef(null); + if (isTableDroppable && dragAndDropHooks && dropState) { let target = {type: 'item', key: item.key, dropPosition: 'on'} as DropTarget; isDropTarget = dropState.isDropTarget(target); - - dropIndicator = dragAndDropHooks.useDropIndicator({target}, dropState, dropIndicatorRef); + + dropIndicator = dragAndDropHooks.useDropIndicator!({target}, dropState, dropIndicatorRef); } - let dragButtonRef = React.useRef(undefined); + let dragButtonRef = React.useRef(null); let {buttonProps: dragButtonProps} = useButton({ ...draggableItem?.dragButtonProps, elementType: 'div' @@ -1190,10 +1201,9 @@ function TableRow({item, children, layoutInfo, parent, ...otherProps}) { draggableItem?.dragProps, // Remove tab index from list row if performing a screenreader drag. This prevents TalkBack from focusing the row, // allowing for single swipe navigation between row drop indicator - dragAndDropHooks?.isVirtualDragging() && {tabIndex: null} + dragAndDropHooks?.isVirtualDragging?.() ? {tabIndex: null} : null ) as HTMLAttributes & DOMAttributes; - let dropProps = isDroppable ? droppableItem?.dropProps : {'aria-hidden': droppableItem?.dropProps['aria-hidden']}; let {visuallyHiddenProps} = useVisuallyHidden(); return ( @@ -1212,7 +1222,7 @@ function TableRow({item, children, layoutInfo, parent, ...otherProps}) {
}
, children: ReactNode, layoutInfo: LayoutInfo, parent: LayoutInfo | null} & HoverProps) { let {state, headerMenuOpen} = useTableContext(); - let ref = useRef(undefined); + let ref = useRef(null); let {rowProps} = useTableHeaderRow({node: item, isVirtualized: true}, state, ref); let {hoverProps} = useHover({...props, isDisabled: headerMenuOpen}); let style = useStyle(layoutInfo, parent); @@ -1265,7 +1275,7 @@ function TableHeaderRow({item, children, layoutInfo, parent, ...props}) { } function TableDragCell({cell}) { - let ref = useRef(undefined); + let ref = useRef(null); let {state, isTableDraggable} = useTableContext(); let isDisabled = state.selectionManager.isDisabled(cell.parentKey); let {gridCellProps} = useTableCell({ @@ -1300,7 +1310,7 @@ function TableDragCell({cell}) { } function TableCheckboxCell({cell}) { - let ref = useRef(undefined); + let ref = useRef(null); let {state} = useTableContext(); // The TableCheckbox should always render its disabled status if the row is disabled, regardless of disabledBehavior, // but the cell itself should not render its disabled styles if disabledBehavior="selection" because the row might have actions on it. @@ -1348,7 +1358,7 @@ function TableCell({cell}) { let {scale} = useProvider(); let {state} = useTableContext(); let isExpandableTable = 'expandedKeys' in state; - let ref = useRef(undefined); + let ref = useRef(null); let columnProps = cell.column.props as SpectrumColumnProps; let isDisabled = state.selectionManager.isDisabled(cell.parentKey); let {gridCellProps} = useTableCell({ @@ -1412,11 +1422,11 @@ function TableCell({cell}) { ); } -function TableCellWrapper({layoutInfo, virtualizer, parent, children}) { - let {isTableDroppable, dropState} = useContext(TableContext); - let isDropTarget: boolean; - let isRootDroptarget: boolean; - if (isTableDroppable) { +function TableCellWrapper({layoutInfo, virtualizer, parent, children}: {layoutInfo: LayoutInfo, virtualizer: any, parent: ReusableView, children: ReactNode}) { + let {isTableDroppable, dropState} = useContext(TableContext)!; + let isDropTarget = false; + let isRootDroptarget = false; + if (isTableDroppable && dropState) { if (parent.content) { isDropTarget = dropState.isDropTarget({type: 'item', dropPosition: 'on', key: parent.content.key}); } @@ -1450,7 +1460,7 @@ function ExpandableRowChevron({cell}) { // TODO: move some/all of the chevron button setup into a separate hook? let {direction} = useLocale(); let {state} = useTableContext(); - let expandButtonRef = useRef(undefined); + let expandButtonRef = useRef(null); let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/table'); let isExpanded; @@ -1493,7 +1503,7 @@ function ExpandableRowChevron({cell}) { } function LoadingState() { - let {state} = useContext(TableContext); + let {state} = useContext(TableContext)!; let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/table'); return ( @@ -1505,7 +1515,7 @@ function LoadingState() { } function EmptyState() { - let {renderEmptyState} = useContext(TableContext); + let {renderEmptyState} = useContext(TableContext)!; let emptyState = renderEmptyState ? renderEmptyState() : null; if (emptyState == null) { return null; @@ -1523,7 +1533,7 @@ function CenteredWrapper({children}) { let rowProps; if ('expandedKeys' in state) { - let topLevelRowCount = [...state.keyMap.get(state.collection.body.key).childNodes].length; + let topLevelRowCount = [...state.collection.body.childNodes].length; rowProps = { 'aria-level': 1, 'aria-posinset': topLevelRowCount + 1, diff --git a/packages/@react-spectrum/table/src/TableViewLayout.ts b/packages/@react-spectrum/table/src/TableViewLayout.ts index 400ce240c27..a52f9359e71 100644 --- a/packages/@react-spectrum/table/src/TableViewLayout.ts +++ b/packages/@react-spectrum/table/src/TableViewLayout.ts @@ -13,12 +13,14 @@ import {DropTarget} from '@react-types/shared'; import {GridNode} from '@react-types/grid'; import {LayoutInfo, Rect} from '@react-stately/virtualizer'; import {LayoutNode, TableLayout} from '@react-stately/layout'; +import {TableCollection} from '@react-stately/table'; export class TableViewLayout extends TableLayout { private isLoading: boolean = false; protected buildCollection(): LayoutNode[] { - let loadingState = this.collection.body.props.loadingState; + let collection = this.virtualizer!.collection as TableCollection; + let loadingState = collection.body.props.loadingState; this.isLoading = loadingState === 'loading' || loadingState === 'loadingMore'; return super.buildCollection(); } @@ -32,11 +34,15 @@ export class TableViewLayout extends TableLayout { protected buildBody(): LayoutNode { let node = super.buildBody(0); let {children, layoutInfo} = node; + if (!children) { + throw new Error('Missing children in LayoutInfo'); + } + let width = node.layoutInfo.rect.width; if (this.isLoading) { // Add some margin around the loader to ensure that scrollbars don't flicker in and out. - let rect = new Rect(40, children.length === 0 ? 40 : layoutInfo.rect.maxY, (width || this.virtualizer.visibleRect.width) - 80, children.length === 0 ? this.virtualizer.visibleRect.height - 80 : 60); + let rect = new Rect(40, children.length === 0 ? 40 : layoutInfo.rect.maxY, (width || this.virtualizer!.visibleRect.width) - 80, children.length === 0 ? this.virtualizer!.visibleRect.height - 80 : 60); let loader = new LayoutInfo('loader', 'loader', rect); loader.parentKey = layoutInfo.key; loader.isSticky = children.length === 0; @@ -49,7 +55,7 @@ export class TableViewLayout extends TableLayout { layoutInfo.rect.height = loader.rect.maxY; width = Math.max(width, rect.width); } else if (children.length === 0) { - let rect = new Rect(40, 40, this.virtualizer.visibleRect.width - 80, this.virtualizer.visibleRect.height - 80); + let rect = new Rect(40, 40, this.virtualizer!.visibleRect.width - 80, this.virtualizer!.visibleRect.height - 80); let empty = new LayoutInfo('empty', 'empty', rect); empty.parentKey = layoutInfo.key; empty.isSticky = true; @@ -87,9 +93,9 @@ export class TableViewLayout extends TableLayout { return node.props?.isDragButtonCell || node.props?.isSelectionCell; } - getDropTargetFromPoint(x: number, y: number, isValidDropTarget: (target: DropTarget) => boolean): DropTarget { + getDropTargetFromPoint(x: number, y: number, isValidDropTarget: (target: DropTarget) => boolean): DropTarget | null { // Offset for height of header row - y -= this.virtualizer.layout.getVisibleLayoutInfos(new Rect(x, y, 1, 1)).find(info => info.type === 'headerrow')?.rect.height; + y -= this.getVisibleLayoutInfos(new Rect(x, y, 1, 1)).find(info => info.type === 'headerrow')?.rect.height ?? 0; return super.getDropTargetFromPoint(x, y, isValidDropTarget); } } diff --git a/packages/@react-spectrum/table/stories/CRUDExample.tsx b/packages/@react-spectrum/table/stories/CRUDExample.tsx index 3853d860806..8fa0b32e694 100644 --- a/packages/@react-spectrum/table/stories/CRUDExample.tsx +++ b/packages/@react-spectrum/table/stories/CRUDExample.tsx @@ -24,6 +24,7 @@ import {Form} from '@react-spectrum/form'; import {Heading} from '@react-spectrum/text'; import {IllustratedMessage} from '@react-spectrum/illustratedmessage'; import {Item, Menu, MenuTrigger} from '@react-spectrum/menu'; +import {Key} from '@react-types/shared'; import More from '@spectrum-icons/workflow/More'; import NoSearchResults from '@spectrum-icons/illustrations/NoSearchResults'; import React, {useState} from 'react'; @@ -38,7 +39,7 @@ export function CRUDExample(props) { ] }); - let [dialog, setDialog] = useState(null); + let [dialog, setDialog] = useState<{action: Key, item?: any} | null>(null); let createItem = (item) => { list.prepend({...item, id: Date.now()}); }; @@ -49,8 +50,9 @@ export function CRUDExample(props) { setDialog({action})}> - {selectedCount > 0 && - + {selectedCount > 0 + ? + : null } ) => void, [name: string]: any}) { let {columns = defaultColumns, rows = defaultRows, onResize, ...otherProps} = props; - let [widths, _setWidths] = useState>(() => new Map(columns.filter(col => col.width).map((col) => [col.uid as Key, col.width]))); + let [widths, _setWidths] = useState>(() => new Map(columns.filter(col => col.width).map((col) => [col.uid as Key, col.width ?? null]))); let setWidths = useCallback((vals: Map) => { let controlledKeys = new Set(columns.filter(col => col.width).map((col) => col.uid as Key)); diff --git a/packages/@react-spectrum/table/stories/Table.stories.tsx b/packages/@react-spectrum/table/stories/Table.stories.tsx index 8e7353b7ffb..7f87af4acc1 100644 --- a/packages/@react-spectrum/table/stories/Table.stories.tsx +++ b/packages/@react-spectrum/table/stories/Table.stories.tsx @@ -2070,13 +2070,13 @@ const allItems = [ function LoadingTable() { const [items, setItems] = useState(allItems); - const [loadingState, setLoadingState] = useState(undefined); + const [loadingState, setLoadingState] = useState('idle'); const onSortChange = () => { setItems([]); setLoadingState('loading'); setTimeout(() => { setItems(items.length > 1 ? [...items.slice(0, 1)] : []); - setLoadingState(undefined); + setLoadingState('idle'); }, 1000); }; diff --git a/packages/@react-spectrum/table/stories/TableDnD.stories.tsx b/packages/@react-spectrum/table/stories/TableDnD.stories.tsx index 4ff47820ac7..19d6b27c74b 100644 --- a/packages/@react-spectrum/table/stories/TableDnD.stories.tsx +++ b/packages/@react-spectrum/table/stories/TableDnD.stories.tsx @@ -91,7 +91,7 @@ export const DragWithinTable: TableStory = { name: 'Drag within table (Reorder)' }; -let manyItems = []; +let manyItems: typeof items = []; for (let i = 0; i < 100; i++) { manyItems.push({...items[i % 10], id: `${i}`}); } diff --git a/packages/@react-spectrum/table/stories/TableDnDExamples.tsx b/packages/@react-spectrum/table/stories/TableDnDExamples.tsx index 5cf41de71b9..668ad84463a 100644 --- a/packages/@react-spectrum/table/stories/TableDnDExamples.tsx +++ b/packages/@react-spectrum/table/stories/TableDnDExamples.tsx @@ -47,7 +47,7 @@ let getAllowedDropOperationsAction = action('getAllowedDropOperationsAction'); export function DragExample(props?) { let {tableViewProps, dragHookOptions} = props; let getItems = (keys) => [...keys].map(key => { - let item = items.find(item => item.id === key); + let item = items.find(item => item.id === key)!; return { 'text/plain': `${item.first_name} ${item.last_name}` }; @@ -81,7 +81,7 @@ export function DragExample(props?) { export function DragWithoutRowHeaderExample(props?) { let {tableViewProps, dragHookOptions} = props; let getItems = (keys) => [...keys].map(key => { - let item = items.find(item => item.id === key); + let item = items.find(item => item.id === key)!; return { 'text/plain': `${item.first_name} ${item.last_name}` }; @@ -149,10 +149,10 @@ export function ReorderExample(props) { async onDrop(e) { onDrop(e); if (e.target.type !== 'root' && e.target.dropPosition !== 'on') { - let keys = []; + let keys: Key[] = []; for (let item of e.items) { if (item.kind === 'text') { - let key; + let key: Key; if (item.types.has(dragType)) { key = JSON.parse(await item.getText(dragType)); keys.push(key); @@ -212,15 +212,15 @@ export function DragOntoRowExample(props) { let list = useListData({ initialItems: [ - {id: '0', type: 'folder', name: 'Folder 1', childNodes: []}, + {id: '0', type: 'folder', name: 'Folder 1', childNodes: [] as any[]}, {id: '1', type: 'item', name: 'One'}, {id: '2', type: 'item', name: 'Two'}, {id: '3', type: 'item', name: 'Three'}, {id: '4', type: 'item', name: 'Four'}, {id: '5', type: 'item', name: 'Five'}, {id: '6', type: 'item', name: 'Six'}, - {id: '7', type: 'folder', name: 'Folder (disabled)', childNodes: []}, - {id: '8', type: 'folder', name: 'Folder 2', childNodes: []} + {id: '7', type: 'folder', name: 'Folder (disabled)', childNodes: [] as any[]}, + {id: '8', type: 'folder', name: 'Folder 2', childNodes: [] as any[]} ] }); let disabledKeys: Key[] = ['2', '7']; @@ -229,9 +229,9 @@ export function DragOntoRowExample(props) { let dragType = React.useMemo(() => `keys-${Math.random().toString(36).slice(2)}`, []); let onMove = (keys: Key[], target: ItemDropTarget) => { - let folderItem = list.getItem(target.key); + let folderItem = list.getItem(target.key)!; let draggedItems = keys.map((key) => list.getItem(key)); - list.update(target.key, {...folderItem, childNodes: [...folderItem.childNodes, ...draggedItems]}); + list.update(target.key, {...folderItem, childNodes: [...(folderItem.childNodes || []), ...draggedItems]}); list.remove(...keys); }; @@ -254,10 +254,10 @@ export function DragOntoRowExample(props) { onDrop: async e => { onDropAction(e); if (e.target.type !== 'root' && e.target.dropPosition === 'on') { - let keys = []; + let keys: Key[] = []; for (let item of e.items) { if (item.kind === 'text') { - let key; + let key: Key; if (item.types.has(dragType)) { key = JSON.parse(await item.getText(dragType)); keys.push(key); @@ -295,7 +295,7 @@ export function DragOntoRowExample(props) { {item.type} {item.name} - {item.type === 'folder' ? `${item.childNodes.length} dropped items` : '-'} + {item.type === 'folder' ? `${item.childNodes?.length} dropped items` : '-'} )} @@ -386,10 +386,10 @@ export function DragBetweenTablesExample(props) { onDrop: async e => { onDropAction(e); if (e.target.type !== 'root' && e.target.dropPosition !== 'on') { - let keys = []; + let keys: Key[] = []; for (let item of e.items) { if (item.kind === 'text') { - let key; + let key: Key; if (item.types.has(dragType)) { key = JSON.parse(await item.getText(dragType)); keys.push(key); @@ -494,10 +494,10 @@ export function DragBetweenTablesRootOnlyExample(props) { onDrop: async e => { onDropAction(e); if (e.target.type === 'root') { - let keys = []; + let keys: Key[] = []; for (let item of e.items) { if (item.kind === 'text') { - let key; + let key: Key; if (item.types.has('list2')) { key = JSON.parse(await item.getText('list2')); keys.push(key); @@ -540,10 +540,10 @@ export function DragBetweenTablesRootOnlyExample(props) { onDrop: async e => { onDropAction(e); if (e.target.type === 'root') { - let keys = []; + let keys: Key[] = []; for (let item of e.items) { if (item.kind === 'text') { - let key; + let key: Key; if (item.types.has('list1')) { key = JSON.parse(await item.getText('list1')); keys.push(key); diff --git a/packages/@react-spectrum/table/stories/TableDnDUtilExamples.tsx b/packages/@react-spectrum/table/stories/TableDnDUtilExamples.tsx index 2dfc13d0b85..62957cc62aa 100644 --- a/packages/@react-spectrum/table/stories/TableDnDUtilExamples.tsx +++ b/packages/@react-spectrum/table/stories/TableDnDUtilExamples.tsx @@ -10,8 +10,8 @@ import {useListData} from '@react-stately/data'; let onSelectionChange = action('onSelectionChange'); let itemProcessor = async (items, acceptedDragTypes) => { - let processedItems = []; - let text; + let processedItems: any[] = []; + let text = ''; for (let item of items) { for (let type of acceptedDragTypes) { if (item.kind === 'text' && item.types.has(type)) { @@ -33,16 +33,16 @@ let itemProcessor = async (items, acceptedDragTypes) => { let folderList1 = [ {identifier: '1', type: 'file', name: 'Adobe Photoshop'}, {identifier: '2', type: 'file', name: 'Adobe XD'}, - {identifier: '3', type: 'folder', name: 'Documents', childNodes: []}, + {identifier: '3', type: 'folder', name: 'Documents', childNodes: [] as any[]}, {identifier: '4', type: 'file', name: 'Adobe InDesign'}, - {identifier: '5', type: 'folder', name: 'Utilities', childNodes: []}, + {identifier: '5', type: 'folder', name: 'Utilities', childNodes: [] as any[]}, {identifier: '6', type: 'file', name: 'Adobe AfterEffects'} ]; let folderList2 = [ - {identifier: '7', type: 'folder', name: 'Pictures', childNodes: []}, + {identifier: '7', type: 'folder', name: 'Pictures', childNodes: [] as any[]}, {identifier: '8', type: 'file', name: 'Adobe Fresco'}, - {identifier: '9', type: 'folder', name: 'Apps', childNodes: []}, + {identifier: '9', type: 'folder', name: 'Apps', childNodes: [] as any[]}, {identifier: '10', type: 'file', name: 'Adobe Illustrator'}, {identifier: '11', type: 'file', name: 'Adobe Lightroom'}, {identifier: '12', type: 'file', name: 'Adobe Dreamweaver'}, @@ -65,7 +65,7 @@ export function DragExampleUtilHandlers(props) { let acceptedDragTypes = ['file', 'folder', 'text/plain']; let {dragAndDropHooks} = useDragAndDrop({ getItems: (keys) => [...keys].map(key => { - let item = list.getItem(key); + let item = list.getItem(key)!; return { [`${item.type}`]: JSON.stringify(item), 'text/plain': JSON.stringify(item) @@ -102,7 +102,7 @@ export function ReorderExampleUtilHandlers(props) { let acceptedDragTypes = ['file', 'folder', 'text/plain']; let {dragAndDropHooks} = useDragAndDrop({ getItems: (keys) => [...keys].map(key => { - let item = list.getItem(key); + let item = list.getItem(key)!; return { [`${item.type}`]: JSON.stringify(item), 'text/plain': JSON.stringify(item) @@ -116,10 +116,10 @@ export function ReorderExampleUtilHandlers(props) { } = e; action('onReorder')(e); - let itemsToCopy = []; + let itemsToCopy: typeof folderList1 = []; if (dropOperation === 'copy') { for (let key of keys) { - let item = {...list.getItem(key)}; + let item: typeof folderList1[0] = {...list.getItem(key)!}; item.identifier = Math.random().toString(36).slice(2); itemsToCopy.push(item); } @@ -170,7 +170,7 @@ export function ItemDropExampleUtilHandlers(props) { let acceptedDragTypes = ['file', 'folder', 'text/plain']; let {dragAndDropHooks} = useDragAndDrop({ getItems: (keys) => [...keys].map(key => { - let item = list.getItem(key); + let item = list.getItem(key)!; return { [`${item.type}`]: JSON.stringify(item), 'text/plain': JSON.stringify(item) @@ -230,7 +230,7 @@ export function RootDropExampleUtilHandlers(props) { let acceptedDragTypes = ['file', 'folder', 'text/plain']; let {dragAndDropHooks: table1Hooks} = useDragAndDrop({ getItems: (keys) => [...keys].map(key => { - let item = list1.getItem(key); + let item = list1.getItem(key)!; return { [`${item.type}`]: JSON.stringify(item), 'text/plain': JSON.stringify(item) @@ -311,7 +311,7 @@ export function InsertExampleUtilHandlers(props) { let acceptedDragTypes = ['file', 'folder', 'text/plain']; let {dragAndDropHooks: table1Hooks} = useDragAndDrop({ getItems: (keys) => [...keys].map(key => { - let item = list1.getItem(key); + let item = list1.getItem(key)!; return { [`${item.type}`]: JSON.stringify(item), 'text/plain': JSON.stringify(item) @@ -472,7 +472,7 @@ export function DragBetweenTablesComplex(props) { // table 1 should allow on item drops and external drops, but disallow reordering/internal drops let {dragAndDropHooks: dragAndDropHooksTable1} = useDragAndDrop({ getItems: (keys) => [...keys].map(key => { - let item = list1.getItem(key); + let item = list1.getItem(key)!; return { [`${item.type}`]: JSON.stringify(item), 'text/plain': JSON.stringify(item) @@ -507,8 +507,8 @@ export function DragBetweenTablesComplex(props) { } = e; action('onItemDropTable1')(e); let processedItems = await itemProcessor(items, acceptedDragTypes); - let targetItem = list1.getItem(target.key); - list1.update(target.key, {...targetItem, childNodes: [...targetItem.childNodes, ...processedItems]}); + let targetItem = list1.getItem(target.key)!; + list1.update(target.key, {...targetItem, childNodes: [...(targetItem.childNodes || []), ...processedItems]}); if (isInternal && dropOperation === 'move') { // TODO test this, perhaps it would be easier to also pass the draggedKeys to onItemDrop instead? @@ -537,7 +537,7 @@ export function DragBetweenTablesComplex(props) { // table 2 should allow reordering, on folder drops, and on root drops let {dragAndDropHooks: dragAndDropHooksTable2} = useDragAndDrop({ getItems: (keys) => [...keys].map(key => { - let item = list2.getItem(key); + let item = list2.getItem(key)!; let dragItem = {}; let itemString = JSON.stringify(item); dragItem[`${item.type}`] = itemString; @@ -569,10 +569,10 @@ export function DragBetweenTablesComplex(props) { } = e; action('onReorderTable2')(e); - let itemsToCopy = []; + let itemsToCopy: typeof folderList1 = []; if (dropOperation === 'copy') { for (let key of keys) { - let item = {...list2.getItem(key)}; + let item: typeof folderList1[0] = {...list2.getItem(key)!}; item.identifier = Math.random().toString(36).slice(2); itemsToCopy.push(item); } @@ -606,8 +606,8 @@ export function DragBetweenTablesComplex(props) { } = e; action('onItemDropTable2')(e); let processedItems = await itemProcessor(items, acceptedDragTypes); - let targetItem = list2.getItem(target.key); - list2.update(target.key, {...targetItem, childNodes: [...targetItem.childNodes, ...processedItems]}); + let targetItem = list2.getItem(target.key)!; + list2.update(target.key, {...targetItem, childNodes: [...(targetItem.childNodes || []), ...processedItems]}); if (isInternal && dropOperation === 'move') { let keysToRemove = processedItems.map(item => item.identifier); @@ -623,7 +623,7 @@ export function DragBetweenTablesComplex(props) { } = e; action('onDragEndTable2')(e); if (dropOperation === 'move' && !isInternal) { - let keysToRemove = [...keys].filter(key => list2.getItem(key).type !== 'unique_type'); + let keysToRemove = [...keys].filter(key => list2.getItem(key)!.type !== 'unique_type'); list2.remove(...keysToRemove); } }, @@ -681,9 +681,9 @@ export function DragBetweenTablesOverride(props) { let list2 = useListData({ initialItems: [ - {identifier: '7', type: 'folder', name: 'Pictures', childNodes: []}, + {identifier: '7', type: 'folder', name: 'Pictures', childNodes: [] as any}, {identifier: '8', type: 'file', name: 'Adobe Fresco'}, - {identifier: '9', type: 'folder', name: 'Apps', childNodes: []}, + {identifier: '9', type: 'folder', name: 'Apps', childNodes: [] as any}, {identifier: '10', type: 'file', name: 'Adobe Illustrator'}, {identifier: '11', type: 'file', name: 'Adobe Lightroom'}, {identifier: '12', type: 'file', name: 'Adobe Dreamweaver'} @@ -693,7 +693,7 @@ export function DragBetweenTablesOverride(props) { let {dragAndDropHooks: dragAndDropHooksTable1} = useDragAndDrop({ getItems: (keys) => [...keys].map(key => { - let item = list1.getItem(key); + let item = list1.getItem(key)!; let dragType = `table-1-adobe-${item.type}`; return { [`${dragType}`]: JSON.stringify(item), @@ -721,7 +721,7 @@ export function DragBetweenTablesOverride(props) { let { items } = e; - let itemsToAdd = []; + let itemsToAdd: typeof folderList1 = []; let text; for (let item of items) { if (item.kind === 'text') { diff --git a/packages/@react-spectrum/table/stories/TreeGridTable.stories.tsx b/packages/@react-spectrum/table/stories/TreeGridTable.stories.tsx index 3fe4a6b8c28..ce4d3a3554d 100644 --- a/packages/@react-spectrum/table/stories/TreeGridTable.stories.tsx +++ b/packages/@react-spectrum/table/stories/TreeGridTable.stories.tsx @@ -162,7 +162,7 @@ export const UserSetRowHeader: TableStory = { } }; -let manyRows = []; +let manyRows: Record[] = []; function generateRow(lvlIndex, lvlLimit, rowIndex) { let row = {key: `Row ${rowIndex} Lvl ${lvlIndex}`}; for (let col of columns) { diff --git a/packages/@react-spectrum/table/test/TableSizing.test.tsx b/packages/@react-spectrum/table/test/TableSizing.test.tsx index 375a10f0624..22440fb3020 100644 --- a/packages/@react-spectrum/table/test/TableSizing.test.tsx +++ b/packages/@react-spectrum/table/test/TableSizing.test.tsx @@ -55,7 +55,7 @@ let items = [ ]; -let manyItems = []; +let manyItems: {id: number, foo: string, bar: string, baz: string}[] = []; for (let i = 1; i <= 100; i++) { manyItems.push({id: i, foo: 'Foo ' + i, bar: 'Bar ' + i, baz: 'Baz ' + i}); } @@ -227,9 +227,9 @@ describe('TableViewSizing', function () { it('should support variable row heights with overflowMode="wrap"', function () { let scrollHeight = jest.spyOn(window.HTMLElement.prototype, 'scrollHeight', 'get') - .mockImplementation(function () { + .mockImplementation(function (this: HTMLElement) { let row = this.closest('[role=row]'); - return row && row.textContent.includes('Foo 1') ? 64 : 48; + return row && row.textContent?.includes('Foo 1') ? 64 : 48; }); let tree = renderTable({overflowMode: 'wrap'}); @@ -257,7 +257,7 @@ describe('TableViewSizing', function () { it('should support variable column header heights with overflowMode="wrap"', function () { let scrollHeight = jest.spyOn(window.HTMLElement.prototype, 'scrollHeight', 'get') - .mockImplementation(function () { + .mockImplementation(function (this: HTMLElement) { return this.textContent === 'Tier Two Header B' ? 48 : 34; }); @@ -1178,8 +1178,8 @@ describe('TableViewSizing', function () { ); await user.tab(); - fireEvent.keyDown(document.activeElement, {key: 'ArrowUp'}); - fireEvent.keyUp(document.activeElement, {key: 'ArrowUp'}); + fireEvent.keyDown(document.activeElement!, {key: 'ArrowUp'}); + fireEvent.keyUp(document.activeElement!, {key: 'ArrowUp'}); let header = tree.getAllByRole('columnheader')[0]; let resizableHeader = within(header).getByRole('button'); @@ -1194,11 +1194,11 @@ describe('TableViewSizing', function () { expect((row.childNodes[2] as HTMLElement).style.width).toBe('200px'); } - fireEvent.keyDown(document.activeElement, {key: 'Enter'}); - fireEvent.keyUp(document.activeElement, {key: 'Enter'}); + fireEvent.keyDown(document.activeElement!, {key: 'Enter'}); + fireEvent.keyUp(document.activeElement!, {key: 'Enter'}); - fireEvent.keyDown(document.activeElement, {key: 'Enter'}); - fireEvent.keyUp(document.activeElement, {key: 'Enter'}); + fireEvent.keyDown(document.activeElement!, {key: 'Enter'}); + fireEvent.keyUp(document.activeElement!, {key: 'Enter'}); act(() => {jest.runAllTimers();}); act(() => {jest.runAllTimers();}); @@ -1206,10 +1206,10 @@ describe('TableViewSizing', function () { expect(document.activeElement).toBe(resizer); - fireEvent.keyDown(document.activeElement, {key: 'ArrowRight'}); - fireEvent.keyUp(document.activeElement, {key: 'ArrowRight'}); - fireEvent.keyDown(document.activeElement, {key: 'ArrowRight'}); - fireEvent.keyUp(document.activeElement, {key: 'ArrowRight'}); + fireEvent.keyDown(document.activeElement!, {key: 'ArrowRight'}); + fireEvent.keyUp(document.activeElement!, {key: 'ArrowRight'}); + fireEvent.keyDown(document.activeElement!, {key: 'ArrowRight'}); + fireEvent.keyUp(document.activeElement!, {key: 'ArrowRight'}); expect(resizer).toHaveAttribute('value', '620'); for (let row of rows) { @@ -1218,10 +1218,10 @@ describe('TableViewSizing', function () { expect((row.childNodes[2] as HTMLElement).style.width).toBe('190px'); } - fireEvent.keyDown(document.activeElement, {key: 'ArrowLeft'}); - fireEvent.keyUp(document.activeElement, {key: 'ArrowLeft'}); - fireEvent.keyDown(document.activeElement, {key: 'ArrowLeft'}); - fireEvent.keyUp(document.activeElement, {key: 'ArrowLeft'}); + fireEvent.keyDown(document.activeElement!, {key: 'ArrowLeft'}); + fireEvent.keyUp(document.activeElement!, {key: 'ArrowLeft'}); + fireEvent.keyDown(document.activeElement!, {key: 'ArrowLeft'}); + fireEvent.keyUp(document.activeElement!, {key: 'ArrowLeft'}); expect(resizer).toHaveAttribute('value', '600'); for (let row of rows) { @@ -1230,8 +1230,8 @@ describe('TableViewSizing', function () { expect((row.childNodes[2] as HTMLElement).style.width).toBe('200px'); } - fireEvent.keyDown(document.activeElement, {key: 'Escape'}); - fireEvent.keyUp(document.activeElement, {key: 'Escape'}); + fireEvent.keyDown(document.activeElement!, {key: 'Escape'}); + fireEvent.keyUp(document.activeElement!, {key: 'Escape'}); expect(onResizeEnd).toHaveBeenCalledTimes(1); expect(onResizeEnd).toHaveBeenCalledWith(new Map([['foo', 600], ['bar', '1fr'], ['baz', '1fr']])); @@ -1260,8 +1260,8 @@ describe('TableViewSizing', function () { ); await user.tab(); - fireEvent.keyDown(document.activeElement, {key: 'ArrowUp'}); - fireEvent.keyUp(document.activeElement, {key: 'ArrowUp'}); + fireEvent.keyDown(document.activeElement!, {key: 'ArrowUp'}); + fireEvent.keyUp(document.activeElement!, {key: 'ArrowUp'}); let header = tree.getAllByRole('columnheader')[0]; let resizableHeader = within(header).getByRole('button'); @@ -1269,11 +1269,11 @@ describe('TableViewSizing', function () { expect(tree.queryByRole('slider')).toBeNull(); - fireEvent.keyDown(document.activeElement, {key: 'Enter'}); - fireEvent.keyUp(document.activeElement, {key: 'Enter'}); + fireEvent.keyDown(document.activeElement!, {key: 'Enter'}); + fireEvent.keyUp(document.activeElement!, {key: 'Enter'}); - fireEvent.keyDown(document.activeElement, {key: 'Enter'}); - fireEvent.keyUp(document.activeElement, {key: 'Enter'}); + fireEvent.keyDown(document.activeElement!, {key: 'Enter'}); + fireEvent.keyUp(document.activeElement!, {key: 'Enter'}); act(() => {jest.runAllTimers();}); act(() => {jest.runAllTimers();}); @@ -1281,8 +1281,8 @@ describe('TableViewSizing', function () { expect(document.activeElement).toBe(resizer); - fireEvent.keyDown(document.activeElement, {key: 'Enter'}); - fireEvent.keyUp(document.activeElement, {key: 'Enter'}); + fireEvent.keyDown(document.activeElement!, {key: 'Enter'}); + fireEvent.keyUp(document.activeElement!, {key: 'Enter'}); expect(onResizeEnd).toHaveBeenCalledTimes(1); expect(onResizeEnd).toHaveBeenCalledWith(new Map([['foo', 600], ['bar', '1fr'], ['baz', '1fr']])); expect(document.activeElement).toBe(resizableHeader); @@ -1310,19 +1310,19 @@ describe('TableViewSizing', function () { ); await user.tab(); - fireEvent.keyDown(document.activeElement, {key: 'ArrowUp'}); - fireEvent.keyUp(document.activeElement, {key: 'ArrowUp'}); + fireEvent.keyDown(document.activeElement!, {key: 'ArrowUp'}); + fireEvent.keyUp(document.activeElement!, {key: 'ArrowUp'}); let header = tree.getAllByRole('columnheader')[0]; let resizableHeader = within(header).getByRole('button'); expect(document.activeElement).toBe(resizableHeader); expect(tree.queryByRole('slider')).toBeNull(); - fireEvent.keyDown(document.activeElement, {key: 'Enter'}); - fireEvent.keyUp(document.activeElement, {key: 'Enter'}); + fireEvent.keyDown(document.activeElement!, {key: 'Enter'}); + fireEvent.keyUp(document.activeElement!, {key: 'Enter'}); - fireEvent.keyDown(document.activeElement, {key: 'Enter'}); - fireEvent.keyUp(document.activeElement, {key: 'Enter'}); + fireEvent.keyDown(document.activeElement!, {key: 'Enter'}); + fireEvent.keyUp(document.activeElement!, {key: 'Enter'}); act(() => {jest.runAllTimers();}); act(() => {jest.runAllTimers();}); @@ -1360,8 +1360,8 @@ describe('TableViewSizing', function () { ); await user.tab(); - fireEvent.keyDown(document.activeElement, {key: 'ArrowUp'}); - fireEvent.keyUp(document.activeElement, {key: 'ArrowUp'}); + fireEvent.keyDown(document.activeElement!, {key: 'ArrowUp'}); + fireEvent.keyUp(document.activeElement!, {key: 'ArrowUp'}); let header = tree.getAllByRole('columnheader')[0]; let resizableHeader = within(header).getByRole('button'); @@ -1369,11 +1369,11 @@ describe('TableViewSizing', function () { expect(tree.queryByRole('slider')).toBeNull(); - fireEvent.keyDown(document.activeElement, {key: 'Enter'}); - fireEvent.keyUp(document.activeElement, {key: 'Enter'}); + fireEvent.keyDown(document.activeElement!, {key: 'Enter'}); + fireEvent.keyUp(document.activeElement!, {key: 'Enter'}); - fireEvent.keyDown(document.activeElement, {key: 'Enter'}); - fireEvent.keyUp(document.activeElement, {key: 'Enter'}); + fireEvent.keyDown(document.activeElement!, {key: 'Enter'}); + fireEvent.keyUp(document.activeElement!, {key: 'Enter'}); act(() => {jest.runAllTimers();}); act(() => {jest.runAllTimers();}); @@ -1408,7 +1408,7 @@ describe('TableViewSizing', function () { let headers = tree.getAllByRole('columnheader'); for (let colheader of headers) { - if (parseInt(colheader.getAttribute('aria-colspan'), 10) > 1) { + if (parseInt(colheader.getAttribute('aria-colspan')!, 10) > 1) { expect(within(colheader).queryByRole('button')).toBeFalsy(); } } diff --git a/packages/@react-spectrum/table/test/TreeGridTable.test.tsx b/packages/@react-spectrum/table/test/TreeGridTable.test.tsx index 3c175047a92..d5fc7af728d 100644 --- a/packages/@react-spectrum/table/test/TreeGridTable.test.tsx +++ b/packages/@react-spectrum/table/test/TreeGridTable.test.tsx @@ -56,8 +56,8 @@ let getCell = (tree, text) => { let focusCell = (tree, text) => act(() => getCell(tree, text).focus()); let moveFocus = (key, opts = {}) => { - fireEvent.keyDown(document.activeElement, {key, ...opts}); - fireEvent.keyUp(document.activeElement, {key, ...opts}); + fireEvent.keyDown(document.activeElement!, {key, ...opts}); + fireEvent.keyUp(document.activeElement!, {key, ...opts}); }; let render = (children, scale = 'medium' as Scale, locale = 'en-US') => { @@ -808,7 +808,7 @@ describe('TableView with expandable rows', function () { describe('scrolling', function () { it('should scroll to a cell when it is focused', function () { - let treegrid = render(); + let treegrid = render(); let body = (treegrid.getByRole('treegrid').childNodes[1] as HTMLElement); focusCell(treegrid, 'Row 9, Lvl 1, Foo'); @@ -816,7 +816,7 @@ describe('TableView with expandable rows', function () { }); it('should scroll to a nested row cell when it is focused off screen', function () { - let treegrid = render(); + let treegrid = render(); let body = (treegrid.getByRole('treegrid').childNodes[1] as HTMLElement); let cell = getCell(treegrid, 'Row 1, Lvl 3, Foo'); act(() => cell.focus()); @@ -890,7 +890,7 @@ describe('TableView with expandable rows', function () { describe('row selection', function () { describe('with pointer', function () { it('should select a row when clicking on the chevron cell', async function () { - let treegrid = render(); + let treegrid = render(); let rowgroups = treegrid.getAllByRole('rowgroup'); let rows = within(rowgroups[1]).getAllByRole('row'); let chevronCell = getCell(treegrid, 'Row 1, Lvl 1, Foo'); @@ -911,7 +911,7 @@ describe('TableView with expandable rows', function () { }); it('should select a nested row on click', async function () { - let treegrid = render(); + let treegrid = render(); let rowgroups = treegrid.getAllByRole('rowgroup'); let rows = within(rowgroups[1]).getAllByRole('row'); let cell = getCell(treegrid, 'Row 1, Lvl 3, Foo'); @@ -927,7 +927,7 @@ describe('TableView with expandable rows', function () { describe('with keyboard', function () { it('should select a nested row by pressing the Enter key on a row', function () { - let treegrid = render(); + let treegrid = render(); let rowgroups = treegrid.getAllByRole('rowgroup'); let rows = within(rowgroups[1]).getAllByRole('row'); @@ -941,7 +941,7 @@ describe('TableView with expandable rows', function () { }); it('should select a nested row by pressing the Space key on a row', function () { - let treegrid = render(); + let treegrid = render(); let rowgroups = treegrid.getAllByRole('rowgroup'); let rows = within(rowgroups[1]).getAllByRole('row'); @@ -955,7 +955,7 @@ describe('TableView with expandable rows', function () { }); it('should select a row by pressing the Enter key on a chevron cell', function () { - let treegrid = render(); + let treegrid = render(); let rowgroups = treegrid.getAllByRole('rowgroup'); let rows = within(rowgroups[1]).getAllByRole('row'); let cell = getCell(treegrid, 'Row 1, Lvl 1, Foo'); @@ -970,7 +970,7 @@ describe('TableView with expandable rows', function () { }); it('should select a row by pressing the Space key on a chevron cell', function () { - let treegrid = render(); + let treegrid = render(); let rowgroups = treegrid.getAllByRole('rowgroup'); let rows = within(rowgroups[1]).getAllByRole('row'); let cell = getCell(treegrid, 'Row 1, Lvl 1, Foo'); @@ -986,7 +986,7 @@ describe('TableView with expandable rows', function () { }); it('should select nested rows if select all checkbox is pressed', async function () { - let treegrid = render(); + let treegrid = render(); let checkbox = treegrid.getByLabelText('Select All'); let rowgroups = treegrid.getAllByRole('rowgroup'); let rows = within(rowgroups[1]).getAllByRole('row'); @@ -996,7 +996,7 @@ describe('TableView with expandable rows', function () { }); it('should not allow selection of disabled nested rows', async function () { - let treegrid = render(); + let treegrid = render(); let rowgroups = treegrid.getAllByRole('rowgroup'); let rows = within(rowgroups[1]).getAllByRole('row'); let cell = getCell(treegrid, 'Row 1, Lvl 2, Foo'); @@ -1016,7 +1016,7 @@ describe('TableView with expandable rows', function () { describe('range selection', function () { describe('with pointer', function () { it('should support selecting a range from a top level row to a nested row', async function () { - let treegrid = render(); + let treegrid = render(); let rowgroups = treegrid.getAllByRole('rowgroup'); let rows = within(rowgroups[1]).getAllByRole('row'); @@ -1033,7 +1033,7 @@ describe('TableView with expandable rows', function () { }); it('should support selecting a range from a nested row to a top level row', async function () { - let treegrid = render(); + let treegrid = render(); let rowgroups = treegrid.getAllByRole('rowgroup'); let rows = within(rowgroups[1]).getAllByRole('row'); @@ -1050,7 +1050,7 @@ describe('TableView with expandable rows', function () { }); it('should support selecting a range from a top level row to a descendent child row', async function () { - let treegrid = render(); + let treegrid = render(); let rowgroups = treegrid.getAllByRole('rowgroup'); let rows = within(rowgroups[1]).getAllByRole('row'); @@ -1067,7 +1067,7 @@ describe('TableView with expandable rows', function () { }); it('should support selecting a range from a nested child row to its top level row ancestor', async function () { - let treegrid = render(); + let treegrid = render(); let rowgroups = treegrid.getAllByRole('rowgroup'); let rows = within(rowgroups[1]).getAllByRole('row'); @@ -1084,7 +1084,7 @@ describe('TableView with expandable rows', function () { }); it('should not include disabled rows', async function () { - let treegrid = render(); + let treegrid = render(); let rowgroups = treegrid.getAllByRole('rowgroup'); let rows = within(rowgroups[1]).getAllByRole('row'); @@ -1104,7 +1104,7 @@ describe('TableView with expandable rows', function () { describe('with keyboard', function () { it('should extend a selection with Shift + ArrowDown through nested keys', function () { - let treegrid = render(); + let treegrid = render(); let rowgroups = treegrid.getAllByRole('rowgroup'); let rows = within(rowgroups[1]).getAllByRole('row'); @@ -1112,24 +1112,24 @@ describe('TableView with expandable rows', function () { pressWithKeyboard(getCell(treegrid, 'Row 1, Lvl 1, Foo')); onSelectionChange.mockReset(); - fireEvent.keyDown(document.activeElement, {key: 'ArrowDown', shiftKey: true}); - fireEvent.keyUp(document.activeElement, {key: 'ArrowDown', shiftKey: true}); + fireEvent.keyDown(document.activeElement!, {key: 'ArrowDown', shiftKey: true}); + fireEvent.keyUp(document.activeElement!, {key: 'ArrowDown', shiftKey: true}); act(() => jest.runAllTimers()); checkSelection(onSelectionChange, [ 'Row 1 Lvl 1', 'Row 1 Lvl 2' ]); onSelectionChange.mockReset(); - fireEvent.keyDown(document.activeElement, {key: 'ArrowDown', shiftKey: true}); - fireEvent.keyUp(document.activeElement, {key: 'ArrowDown', shiftKey: true}); + fireEvent.keyDown(document.activeElement!, {key: 'ArrowDown', shiftKey: true}); + fireEvent.keyUp(document.activeElement!, {key: 'ArrowDown', shiftKey: true}); act(() => jest.runAllTimers()); checkSelection(onSelectionChange, [ 'Row 1 Lvl 1', 'Row 1 Lvl 2', 'Row 1 Lvl 3' ]); onSelectionChange.mockReset(); - fireEvent.keyDown(document.activeElement, {key: 'ArrowDown', shiftKey: true}); - fireEvent.keyUp(document.activeElement, {key: 'ArrowDown', shiftKey: true}); + fireEvent.keyDown(document.activeElement!, {key: 'ArrowDown', shiftKey: true}); + fireEvent.keyUp(document.activeElement!, {key: 'ArrowDown', shiftKey: true}); act(() => jest.runAllTimers()); checkSelection(onSelectionChange, [ 'Row 1 Lvl 1', 'Row 1 Lvl 2', 'Row 1 Lvl 3', 'Row 2 Lvl 1' @@ -1140,7 +1140,7 @@ describe('TableView with expandable rows', function () { }); it('should extend a selection with Shift + ArrowUp through nested keys', function () { - let treegrid = render(); + let treegrid = render(); let rowgroups = treegrid.getAllByRole('rowgroup'); let rows = within(rowgroups[1]).getAllByRole('row'); @@ -1148,24 +1148,24 @@ describe('TableView with expandable rows', function () { pressWithKeyboard(getCell(treegrid, 'Row 2, Lvl 1, Foo')); onSelectionChange.mockReset(); - fireEvent.keyDown(document.activeElement, {key: 'ArrowUp', shiftKey: true}); - fireEvent.keyUp(document.activeElement, {key: 'ArrowUp', shiftKey: true}); + fireEvent.keyDown(document.activeElement!, {key: 'ArrowUp', shiftKey: true}); + fireEvent.keyUp(document.activeElement!, {key: 'ArrowUp', shiftKey: true}); act(() => jest.runAllTimers()); checkSelection(onSelectionChange, [ 'Row 2 Lvl 1', 'Row 1 Lvl 3' ]); onSelectionChange.mockReset(); - fireEvent.keyDown(document.activeElement, {key: 'ArrowUp', shiftKey: true}); - fireEvent.keyUp(document.activeElement, {key: 'ArrowUp', shiftKey: true}); + fireEvent.keyDown(document.activeElement!, {key: 'ArrowUp', shiftKey: true}); + fireEvent.keyUp(document.activeElement!, {key: 'ArrowUp', shiftKey: true}); act(() => jest.runAllTimers()); checkSelection(onSelectionChange, [ 'Row 2 Lvl 1', 'Row 1 Lvl 3', 'Row 1 Lvl 2' ]); onSelectionChange.mockReset(); - fireEvent.keyDown(document.activeElement, {key: 'ArrowUp', shiftKey: true}); - fireEvent.keyUp(document.activeElement, {key: 'ArrowUp', shiftKey: true}); + fireEvent.keyDown(document.activeElement!, {key: 'ArrowUp', shiftKey: true}); + fireEvent.keyUp(document.activeElement!, {key: 'ArrowUp', shiftKey: true}); act(() => jest.runAllTimers()); checkSelection(onSelectionChange, [ 'Row 2 Lvl 1', 'Row 1 Lvl 3', 'Row 1 Lvl 2', 'Row 1 Lvl 1' @@ -1176,7 +1176,7 @@ describe('TableView with expandable rows', function () { }); it('should extend a selection with Ctrl + Shift + Home', function () { - let treegrid = render(); + let treegrid = render(); let rowgroups = treegrid.getAllByRole('rowgroup'); let rows = within(rowgroups[1]).getAllByRole('row'); @@ -1184,8 +1184,8 @@ describe('TableView with expandable rows', function () { pressWithKeyboard(getCell(treegrid, 'Row 3, Lvl 1, Foo')); onSelectionChange.mockReset(); - fireEvent.keyDown(document.activeElement, {key: 'Home', shiftKey: true, ctrlKey: true}); - fireEvent.keyUp(document.activeElement, {key: 'Home', shiftKey: true, ctrlKey: true}); + fireEvent.keyDown(document.activeElement!, {key: 'Home', shiftKey: true, ctrlKey: true}); + fireEvent.keyUp(document.activeElement!, {key: 'Home', shiftKey: true, ctrlKey: true}); act(() => jest.runAllTimers()); checkSelection(onSelectionChange, [ @@ -1197,7 +1197,7 @@ describe('TableView with expandable rows', function () { }); it('should extend a selection with Ctrl + Shift + End', function () { - let treegrid = render(); + let treegrid = render(); let rowgroups = treegrid.getAllByRole('rowgroup'); let rows = within(rowgroups[1]).getAllByRole('row'); @@ -1205,15 +1205,15 @@ describe('TableView with expandable rows', function () { pressWithKeyboard(getCell(treegrid, 'Row 3, Lvl 1, Foo')); onSelectionChange.mockReset(); - fireEvent.keyDown(document.activeElement, {key: 'End', shiftKey: true, ctrlKey: true}); - fireEvent.keyUp(document.activeElement, {key: 'End', shiftKey: true, ctrlKey: true}); + fireEvent.keyDown(document.activeElement!, {key: 'End', shiftKey: true, ctrlKey: true}); + fireEvent.keyUp(document.activeElement!, {key: 'End', shiftKey: true, ctrlKey: true}); act(() => jest.runAllTimers()); checkRowSelection(rows.slice(6), true); }); it('should extend a selection with Shift + PageDown', function () { - let treegrid = render(); + let treegrid = render(); let rowgroups = treegrid.getAllByRole('rowgroup'); let rows = within(rowgroups[1]).getAllByRole('row'); @@ -1221,8 +1221,8 @@ describe('TableView with expandable rows', function () { pressWithKeyboard(getCell(treegrid, 'Row 3, Lvl 1, Foo')); onSelectionChange.mockReset(); - fireEvent.keyDown(document.activeElement, {key: 'PageDown', shiftKey: true}); - fireEvent.keyUp(document.activeElement, {key: 'PageDown', shiftKey: true}); + fireEvent.keyDown(document.activeElement!, {key: 'PageDown', shiftKey: true}); + fireEvent.keyUp(document.activeElement!, {key: 'PageDown', shiftKey: true}); act(() => jest.runAllTimers()); checkSelection(onSelectionChange, [ @@ -1235,7 +1235,7 @@ describe('TableView with expandable rows', function () { }); it('should extend a selection with Shift + PageUp', function () { - let treegrid = render(); + let treegrid = render(); let rowgroups = treegrid.getAllByRole('rowgroup'); let rows = within(rowgroups[1]).getAllByRole('row'); @@ -1243,8 +1243,8 @@ describe('TableView with expandable rows', function () { pressWithKeyboard(getCell(treegrid, 'Row 3, Lvl 1, Foo')); onSelectionChange.mockReset(); - fireEvent.keyDown(document.activeElement, {key: 'PageUp', shiftKey: true}); - fireEvent.keyUp(document.activeElement, {key: 'PageUp', shiftKey: true}); + fireEvent.keyDown(document.activeElement!, {key: 'PageUp', shiftKey: true}); + fireEvent.keyUp(document.activeElement!, {key: 'PageUp', shiftKey: true}); act(() => jest.runAllTimers()); checkSelection(onSelectionChange, [ @@ -1253,7 +1253,7 @@ describe('TableView with expandable rows', function () { }); it('should not include disabled rows', function () { - let treegrid = render(); + let treegrid = render(); let rowgroups = treegrid.getAllByRole('rowgroup'); let rows = within(rowgroups[1]).getAllByRole('row'); @@ -1261,11 +1261,11 @@ describe('TableView with expandable rows', function () { pressWithKeyboard(getCell(treegrid, 'Row 1, Lvl 1, Foo')); onSelectionChange.mockReset(); - fireEvent.keyDown(document.activeElement, {key: 'ArrowDown', shiftKey: true}); - fireEvent.keyUp(document.activeElement, {key: 'ArrowDown', shiftKey: true}); + fireEvent.keyDown(document.activeElement!, {key: 'ArrowDown', shiftKey: true}); + fireEvent.keyUp(document.activeElement!, {key: 'ArrowDown', shiftKey: true}); act(() => jest.runAllTimers()); - fireEvent.keyDown(document.activeElement, {key: 'ArrowDown', shiftKey: true}); - fireEvent.keyUp(document.activeElement, {key: 'ArrowDown', shiftKey: true}); + fireEvent.keyDown(document.activeElement!, {key: 'ArrowDown', shiftKey: true}); + fireEvent.keyUp(document.activeElement!, {key: 'ArrowDown', shiftKey: true}); act(() => jest.runAllTimers()); checkSelection(onSelectionChange, [ 'Row 1 Lvl 1', 'Row 1 Lvl 3' @@ -1286,7 +1286,7 @@ describe('TableView with expandable rows', function () { ${'mouse'} ${'touch'} `('should trigger onAction when clicking nested rows with $Name', async ({Name}) => { - let treegrid = render(); + let treegrid = render(); let rowgroups = treegrid.getAllByRole('rowgroup'); let rows = within(rowgroups[1]).getAllByRole('row'); let cell = getCell(treegrid, 'Row 1, Lvl 3, Foo'); @@ -1315,7 +1315,7 @@ describe('TableView with expandable rows', function () { }); it('should trigger onAction when pressing Enter', function () { - let treegrid = render(); + let treegrid = render(); let rowgroups = treegrid.getAllByRole('rowgroup'); let rows = within(rowgroups[1]).getAllByRole('row'); let cell = getCell(treegrid, 'Row 1, Lvl 3, Foo'); @@ -1342,7 +1342,7 @@ describe('TableView with expandable rows', function () { installPointerEvent(); it('should toggle selection with mouse', function () { - let treegrid = render(); + let treegrid = render(); expect(treegrid.queryByLabelText('Select All')).toBeNull(); let rowgroups = treegrid.getAllByRole('rowgroup'); @@ -1371,7 +1371,7 @@ describe('TableView with expandable rows', function () { }); it('should toggle selection with touch', function () { - let treegrid = render(); + let treegrid = render(); expect(treegrid.queryByLabelText('Select All')).toBeNull(); let rowgroups = treegrid.getAllByRole('rowgroup'); @@ -1399,7 +1399,7 @@ describe('TableView with expandable rows', function () { }); it('should support long press to enter selection mode on touch', async function () { - let treegrid = render(); + let treegrid = render(); await user.click(document.body); let rowgroups = treegrid.getAllByRole('rowgroup'); let rows = within(rowgroups[1]).getAllByRole('row'); @@ -1439,7 +1439,7 @@ describe('TableView with expandable rows', function () { }); it('should support double click to perform onAction with mouse', async function () { - let treegrid = render(); + let treegrid = render(); expect(treegrid.queryByLabelText('Select All')).toBeNull(); let rowgroups = treegrid.getAllByRole('rowgroup'); @@ -1455,7 +1455,6 @@ describe('TableView with expandable rows', function () { checkSelection(onSelectionChange, ['Row 1 Lvl 3']); expect(onAction).not.toHaveBeenCalled(); onSelectionChange.mockReset(); - // @ts-ignore await user.dblClick(cell); act(() => jest.runAllTimers()); expect(announce).toHaveBeenCalledTimes(1); @@ -1465,7 +1464,7 @@ describe('TableView with expandable rows', function () { }); it('should support single tap to perform onAction with touch', function () { - let treegrid = render(); + let treegrid = render(); expect(treegrid.queryByLabelText('Select All')).toBeNull(); let cell = getCell(treegrid, 'Row 1, Lvl 3, Foo'); diff --git a/packages/@react-spectrum/tag/src/TagGroup.tsx b/packages/@react-spectrum/tag/src/TagGroup.tsx index ad34d300a1e..da6ac127018 100644 --- a/packages/@react-spectrum/tag/src/TagGroup.tsx +++ b/packages/@react-spectrum/tag/src/TagGroup.tsx @@ -62,7 +62,7 @@ function TagGroup(props: SpectrumTagGroupProps, ref: DOMRef renderEmptyState = () => stringFormatter.format('noTags') } = props; let domRef = useDOMRef(ref); - let containerRef = useRef(null); + let containerRef = useRef(null); let tagsRef = useRef(null); let {direction} = useLocale(); let {scale} = useProvider(); @@ -85,7 +85,7 @@ function TagGroup(props: SpectrumTagGroupProps, ref: DOMRef delete props.onAction; let {gridProps, labelProps, descriptionProps, errorMessageProps} = useTagGroup({...props, keyboardDelegate}, state, tagsRef); let actionsId = useId(); - let actionsRef = useRef(null); + let actionsRef = useRef(null); let updateVisibleTagCount = useCallback(() => { if (maxRows && maxRows > 0) { @@ -124,7 +124,7 @@ function TagGroup(props: SpectrumTagGroupProps, ref: DOMRef // Remove tags until there is space for the collapse button and action button (if present) on the last row. let buttons = [...currActionsRef.children]; - if (maxRows && buttons.length > 0 && rowCount >= maxRows) { + if (maxRows && buttons.length > 0 && rowCount >= maxRows && currContainerRef.parentElement) { let buttonsWidth = buttons.reduce((acc, curr) => acc += curr.getBoundingClientRect().width, 0); buttonsWidth += TAG_STYLES[scale].margin * 2 * buttons.length; let end = direction === 'ltr' ? 'right' : 'left'; @@ -134,7 +134,7 @@ function TagGroup(props: SpectrumTagGroupProps, ref: DOMRef let availableWidth = containerEnd - lastTagEnd; while (availableWidth < buttonsWidth && index > 0) { - availableWidth += tagWidths.pop(); + availableWidth += tagWidths.pop()!; index--; } } diff --git a/packages/@react-spectrum/toast/src/ToastContainer.tsx b/packages/@react-spectrum/toast/src/ToastContainer.tsx index 788e9d19081..e5af9d47814 100644 --- a/packages/@react-spectrum/toast/src/ToastContainer.tsx +++ b/packages/@react-spectrum/toast/src/ToastContainer.tsx @@ -77,14 +77,14 @@ function useActiveToastContainer() { * A ToastContainer renders the queued toasts in an application. It should be placed * at the root of the app. */ -export function ToastContainer(props: SpectrumToastContainerProps): ReactElement { +export function ToastContainer(props: SpectrumToastContainerProps): ReactElement | null { // Track all toast provider instances in a set. // Only the first one will actually render. // We use a ref to do this, since it will have a stable identity // over the lifetime of the component. - let ref = useRef(undefined); + let ref = useRef(null); + - useEffect(() => { toastProviders.add(ref); triggerSubscriptions(); @@ -144,7 +144,7 @@ function addToast(children: string, variant: SpectrumToastValue['variant'], opti let shouldContinue = window.dispatchEvent(event); if (!shouldContinue) { - return; + return () => {}; } } @@ -160,7 +160,7 @@ function addToast(children: string, variant: SpectrumToastValue['variant'], opti // Minimum time of 5s from https://spectrum.adobe.com/page/toast/#Auto-dismissible // Actionable toasts cannot be auto dismissed. That would fail WCAG SC 2.2.1. // It is debatable whether non-actionable toasts would also fail. - let timeout = options.timeout && !options.onAction ? Math.max(options.timeout, 5000) : null; + let timeout = options.timeout && !options.onAction ? Math.max(options.timeout, 5000) : undefined; let queue = getGlobalToastQueue(); let key = queue.add(value, {timeout, onClose: options.onClose}); return () => queue.close(key); diff --git a/packages/@react-spectrum/toast/src/Toaster.tsx b/packages/@react-spectrum/toast/src/Toaster.tsx index e5ea827337b..8be59a78644 100644 --- a/packages/@react-spectrum/toast/src/Toaster.tsx +++ b/packages/@react-spectrum/toast/src/Toaster.tsx @@ -34,7 +34,7 @@ export function Toaster(props: ToastContainerProps): ReactElement { state } = props; - let ref = useRef(undefined); + let ref = useRef(null); let {regionProps} = useToastRegion(props, state, ref); let {focusProps, isFocusVisible} = useFocusRing(); let {getContainer} = useUNSTABLE_PortalContext(); diff --git a/packages/@react-spectrum/toast/stories/Toast.stories.tsx b/packages/@react-spectrum/toast/stories/Toast.stories.tsx index 64516082447..625eb6b9522 100644 --- a/packages/@react-spectrum/toast/stories/Toast.stories.tsx +++ b/packages/@react-spectrum/toast/stories/Toast.stories.tsx @@ -212,7 +212,7 @@ function RenderProvider(options: SpectrumToastOptions) { } function ToastToggle(options: SpectrumToastOptions) { - let [close, setClose] = useState(null); + let [close, setClose] = useState<(() => void) | null>(null); return (