Skip to content

Commit 8f3c0ea

Browse files
authored
TS Strict Stately List (#6567)
* TS Strict Stately List
1 parent 69cc445 commit 8f3c0ea

File tree

14 files changed

+123
-84
lines changed

14 files changed

+123
-84
lines changed

packages/@react-aria/tabs/src/useTabPanel.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export interface TabPanelAria {
2727
* Provides the behavior and accessibility implementation for a tab panel. A tab panel is a container for
2828
* the contents of a tab, and is shown when the tab is selected.
2929
*/
30-
export function useTabPanel<T>(props: AriaTabPanelProps, state: TabListState<T>, ref: RefObject<Element | null>): TabPanelAria {
30+
export function useTabPanel<T>(props: AriaTabPanelProps, state: TabListState<T> | null, ref: RefObject<Element | null>): TabPanelAria {
3131
// The tabpanel should have tabIndex=0 when there are no tabbable elements within it.
3232
// Otherwise, tabbing from the focused tab should go directly to the first tabbable element
3333
// within the tabpanel.

packages/@react-aria/tabs/src/utils.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@ import {TabListState} from '@react-stately/tabs';
1515

1616
export const tabsIds = new WeakMap<TabListState<unknown>, string>();
1717

18-
export function generateId<T>(state: TabListState<T>, key: Key, role: string) {
18+
export function generateId<T>(state: TabListState<T> | null, key: Key | null | undefined, role: string) {
19+
if (!state) {
20+
// this case should only happen in the first render before the tabs are registered
21+
return '';
22+
}
1923
if (typeof key === 'string') {
2024
key = key.replace(/\s+/g, '');
2125
}

packages/@react-spectrum/autocomplete/src/SearchAutocomplete.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ function _SearchAutocompleteBase<T extends object>(props: SpectrumSearchAutocomp
172172
{...listBoxProps}
173173
ref={listBoxRef}
174174
disallowEmptySelection
175-
autoFocus={state.focusStrategy}
175+
autoFocus={state.focusStrategy ?? undefined}
176176
shouldSelectOnPressUp
177177
focusOnPointerEnter
178178
layout={layout}

packages/@react-spectrum/tabs/src/Tabs.tsx

Lines changed: 38 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@
1212

1313
import {AriaTabPanelProps, SpectrumTabListProps, SpectrumTabPanelsProps, SpectrumTabsProps} from '@react-types/tabs';
1414
import {classNames, SlotProvider, unwrapDOMRef, useDOMRef, useStyleProps} from '@react-spectrum/utils';
15-
import {DOMProps, DOMRef, Key, Node, Orientation, StyleProps} from '@react-types/shared';
15+
import {DOMProps, DOMRef, DOMRefValue, Key, Node, Orientation, RefObject, StyleProps} from '@react-types/shared';
1616
import {filterDOMProps, mergeProps, useId, useLayoutEffect, useResizeObserver} from '@react-aria/utils';
1717
import {FocusRing} from '@react-aria/focus';
1818
import {Item, Picker} from '@react-spectrum/picker';
1919
import {ListCollection} from '@react-stately/list';
2020
import React, {
21-
MutableRefObject,
21+
CSSProperties,
22+
HTMLAttributes,
2223
ReactElement,
2324
ReactNode,
2425
useCallback,
@@ -40,22 +41,20 @@ import {useTab, useTabList, useTabPanel} from '@react-aria/tabs';
4041
interface TabsContext<T> {
4142
tabProps: SpectrumTabsProps<T>,
4243
tabState: {
43-
tabListState: TabListState<T>,
44+
tabListState: TabListState<T> | null,
4445
setTabListState: (state: TabListState<T>) => void,
45-
selectedTab: HTMLElement,
46+
selectedTab: HTMLElement | null,
4647
collapsed: boolean
4748
},
4849
refs: {
49-
wrapperRef: MutableRefObject<HTMLDivElement>,
50-
tablistRef: MutableRefObject<HTMLDivElement>
51-
},
52-
tabPanelProps: {
53-
'aria-labelledby': string
50+
wrapperRef: RefObject<HTMLDivElement | null>,
51+
tablistRef: RefObject<HTMLDivElement | null>
5452
},
53+
tabPanelProps: HTMLAttributes<HTMLElement>,
5554
tabLineState: Array<DOMRect>
5655
}
5756

58-
const TabContext = React.createContext<TabsContext<any>>(null);
57+
const TabContext = React.createContext<TabsContext<any> | null>(null);
5958

6059
function Tabs<T extends object>(props: SpectrumTabsProps<T>, ref: DOMRef<HTMLDivElement>) {
6160
props = useProviderProps(props);
@@ -67,20 +66,20 @@ function Tabs<T extends object>(props: SpectrumTabsProps<T>, ref: DOMRef<HTMLDiv
6766
} = props;
6867

6968
let domRef = useDOMRef(ref);
70-
let tablistRef = useRef<HTMLDivElement>(undefined);
71-
let wrapperRef = useRef<HTMLDivElement>(undefined);
69+
let tablistRef = useRef<HTMLDivElement>(null);
70+
let wrapperRef = useRef<HTMLDivElement>(null);
7271

7372
let {direction} = useLocale();
7473
let {styleProps} = useStyleProps(otherProps);
7574
let [collapsed, setCollapsed] = useState(false);
76-
let [selectedTab, setSelectedTab] = useState<HTMLElement>();
77-
const [tabListState, setTabListState] = useState<TabListState<T>>(null);
78-
let [tabPositions, setTabPositions] = useState([]);
79-
let prevTabPositions = useRef(tabPositions);
75+
let [selectedTab, setSelectedTab] = useState<HTMLElement | null>(null);
76+
const [tabListState, setTabListState] = useState<TabListState<T> | null>(null);
77+
let [tabPositions, setTabPositions] = useState<DOMRect[]>([]);
78+
let prevTabPositions = useRef<DOMRect[]>(tabPositions);
8079

8180
useEffect(() => {
8281
if (tablistRef.current) {
83-
let selectedTab: HTMLElement = tablistRef.current.querySelector(`[data-key="${CSS.escape(tabListState?.selectedKey?.toString())}"]`);
82+
let selectedTab: HTMLElement | null = tablistRef.current.querySelector(`[data-key="${CSS.escape(tabListState?.selectedKey?.toString() ?? '')}"]`);
8483

8584
if (selectedTab != null) {
8685
setSelectedTab(selectedTab);
@@ -92,15 +91,16 @@ function Tabs<T extends object>(props: SpectrumTabsProps<T>, ref: DOMRef<HTMLDiv
9291
let checkShouldCollapse = useCallback(() => {
9392
if (wrapperRef.current && orientation !== 'vertical') {
9493
let tabsComponent = wrapperRef.current;
95-
let tabs = tablistRef.current.querySelectorAll('[role="tab"]');
96-
let tabDimensions = [...tabs].map(tab => tab.getBoundingClientRect());
94+
let tabs: NodeListOf<Element> = tablistRef.current?.querySelectorAll('[role="tab"]') ?? new NodeList() as NodeListOf<Element>;
95+
let tabDimensions = [...tabs].map((tab: Element) => tab.getBoundingClientRect());
9796

9897
let end = direction === 'rtl' ? 'left' : 'right';
9998
let farEdgeTabList = tabsComponent.getBoundingClientRect()[end];
10099
let farEdgeLastTab = tabDimensions[tabDimensions.length - 1][end];
101100
let shouldCollapse = direction === 'rtl' ? farEdgeLastTab < farEdgeTabList : farEdgeTabList < farEdgeLastTab;
102101
setCollapsed(shouldCollapse);
103-
if (tabDimensions.length !== prevTabPositions.current.length || tabDimensions.some((box, index) => box?.left !== prevTabPositions.current[index]?.left || box?.right !== prevTabPositions.current[index]?.right)) {
102+
if (tabDimensions.length !== prevTabPositions.current.length
103+
|| tabDimensions.some((box, index) => box?.left !== prevTabPositions.current[index]?.left || box?.right !== prevTabPositions.current[index]?.right)) {
104104
setTabPositions(tabDimensions);
105105
prevTabPositions.current = tabDimensions;
106106
}
@@ -113,7 +113,7 @@ function Tabs<T extends object>(props: SpectrumTabsProps<T>, ref: DOMRef<HTMLDiv
113113

114114
useResizeObserver({ref: wrapperRef, onResize: checkShouldCollapse});
115115

116-
let tabPanelProps = {
116+
let tabPanelProps: HTMLAttributes<HTMLElement> = {
117117
'aria-labelledby': undefined
118118
};
119119

@@ -202,8 +202,8 @@ function Tab<T>(props: TabProps<T>) {
202202

203203
interface TabLineProps {
204204
orientation?: Orientation,
205-
selectedTab?: HTMLElement,
206-
selectedKey?: Key
205+
selectedTab?: HTMLElement | null,
206+
selectedKey?: Key | null
207207
}
208208

209209
// @private
@@ -218,18 +218,20 @@ function TabLine(props: TabLineProps) {
218218

219219
let {direction} = useLocale();
220220
let {scale} = useProvider();
221-
let {tabLineState} = useContext(TabContext);
221+
let {tabLineState} = useContext(TabContext)!;
222222

223-
let [style, setStyle] = useState({
223+
let [style, setStyle] = useState<CSSProperties>({
224224
width: undefined,
225225
height: undefined
226226
});
227227

228228
let onResize = useCallback(() => {
229229
if (selectedTab) {
230-
let styleObj = {transform: undefined, width: undefined, height: undefined};
230+
let styleObj: CSSProperties = {transform: undefined, width: undefined, height: undefined};
231231
// In RTL, calculate the transform from the right edge of the tablist so that resizing the window doesn't break the Tabline position due to offsetLeft changes
232-
let offset = direction === 'rtl' ? -1 * ((selectedTab.offsetParent as HTMLElement)?.offsetWidth - selectedTab.offsetWidth - selectedTab.offsetLeft) : selectedTab.offsetLeft;
232+
let offset = direction === 'rtl' ?
233+
-1 * ((selectedTab.offsetParent as HTMLElement)?.offsetWidth - selectedTab.offsetWidth - selectedTab.offsetLeft) :
234+
selectedTab.offsetLeft;
233235
styleObj.transform = orientation === 'vertical'
234236
? `translateY(${selectedTab.offsetTop}px)`
235237
: `translateX(${offset}px)`;
@@ -255,7 +257,7 @@ function TabLine(props: TabLineProps) {
255257
* The keys of the items within the <TabList> must match up with a corresponding item inside the <TabPanels>.
256258
*/
257259
export function TabList<T>(props: SpectrumTabListProps<T>) {
258-
const tabContext = useContext(TabContext);
260+
const tabContext = useContext(TabContext)!;
259261
const {refs, tabState, tabProps, tabPanelProps} = tabContext;
260262
const {isQuiet, density, isEmphasized, orientation} = tabProps;
261263
const {selectedTab, collapsed, setTabListState} = tabState;
@@ -330,13 +332,13 @@ export function TabList<T>(props: SpectrumTabListProps<T>) {
330332
* TabPanels is used within Tabs as a container for the content of each tab.
331333
* The keys of the items within the <TabPanels> must match up with a corresponding item inside the <TabList>.
332334
*/
333-
export function TabPanels<T>(props: SpectrumTabPanelsProps<T>) {
334-
const {tabState, tabProps} = useContext(TabContext);
335+
export function TabPanels<T extends object>(props: SpectrumTabPanelsProps<T>) {
336+
const {tabState, tabProps} = useContext(TabContext)!;
335337
const {tabListState} = tabState;
336338

337-
const factory = useCallback(nodes => new ListCollection(nodes), []);
339+
const factory = useCallback((nodes: Iterable<Node<T>>) => new ListCollection(nodes), []);
338340
const collection = useCollection({items: tabProps.items, ...props}, factory, {suppressTextValueWarning: true});
339-
const selectedItem = tabListState ? collection.getItem(tabListState.selectedKey) : null;
341+
const selectedItem = tabListState && tabListState.selectedKey != null ? collection.getItem(tabListState.selectedKey) : null;
340342

341343
return (
342344
<TabPanel {...props} key={tabListState?.selectedKey}>
@@ -351,9 +353,9 @@ interface TabPanelProps extends AriaTabPanelProps, StyleProps {
351353

352354
// @private
353355
function TabPanel(props: TabPanelProps) {
354-
const {tabState, tabPanelProps: ctxTabPanelProps} = useContext(TabContext);
356+
const {tabState, tabPanelProps: ctxTabPanelProps} = useContext(TabContext)!;
355357
const {tabListState} = tabState;
356-
let ref = useRef(undefined);
358+
let ref = useRef<HTMLDivElement | null>(null);
357359
const {tabPanelProps} = useTabPanel(props, tabListState, ref);
358360
let {styleProps} = useStyleProps(props);
359361

@@ -392,8 +394,8 @@ function TabPicker<T>(props: TabPickerProps<T>) {
392394
visible
393395
} = props;
394396

395-
let ref = useRef(undefined);
396-
let [pickerNode, setPickerNode] = useState(null);
397+
let ref = useRef<DOMRefValue<HTMLDivElement>>(null);
398+
let [pickerNode, setPickerNode] = useState<HTMLElement | null>(null);
397399

398400
useEffect(() => {
399401
let node = unwrapDOMRef(ref);
@@ -408,7 +410,6 @@ function TabPicker<T>(props: TabPickerProps<T>) {
408410

409411
const style : React.CSSProperties = visible ? {} : {visibility: 'hidden', position: 'absolute'};
410412

411-
// TODO: Figure out if tabListProps should go onto the div here, v2 doesn't do it
412413
return (
413414
<div
414415
className={classNames(

packages/@react-stately/combobox/src/useComboBoxState.ts

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export interface ComboBoxState<T> extends SelectState<T>, FormValidationState{
2828
/** Selects the currently focused item and updates the input value. */
2929
commit(): void,
3030
/** Controls which item will be auto focused when the menu opens. */
31-
readonly focusStrategy: FocusStrategy,
31+
readonly focusStrategy: FocusStrategy | null,
3232
/** Opens the menu. */
3333
open(focusStrategy?: FocusStrategy | null, trigger?: MenuTriggerAction): void,
3434
/** Toggles the menu. */
@@ -64,7 +64,7 @@ export function useComboBoxState<T extends object>(props: ComboBoxStateOptions<T
6464

6565
let [showAllItems, setShowAllItems] = useState(false);
6666
let [isFocused, setFocusedState] = useState(false);
67-
let [focusStrategy, setFocusStrategy] = useState<FocusStrategy>(null);
67+
let [focusStrategy, setFocusStrategy] = useState<FocusStrategy | null>(null);
6868

6969
let onSelectionChange = (key) => {
7070
if (props.onSelectionChange) {
@@ -79,15 +79,29 @@ export function useComboBoxState<T extends object>(props: ComboBoxStateOptions<T
7979
}
8080
};
8181

82-
let {collection, selectionManager, selectedKey, setSelectedKey, selectedItem, disabledKeys} = useSingleSelectListState({
82+
let {collection,
83+
selectionManager,
84+
selectedKey,
85+
setSelectedKey,
86+
selectedItem,
87+
disabledKeys
88+
} = useSingleSelectListState({
8389
...props,
8490
onSelectionChange,
8591
items: props.items ?? props.defaultItems
8692
});
93+
let defaultInputValue: string | null | undefined = props.defaultInputValue;
94+
if (defaultInputValue == null) {
95+
if (selectedKey == null) {
96+
defaultInputValue = '';
97+
} else {
98+
defaultInputValue = collection.getItem(selectedKey)?.textValue ?? '';
99+
}
100+
}
87101

88102
let [inputValue, setInputValue] = useControlledState(
89103
props.inputValue,
90-
props.defaultInputValue ?? collection.getItem(selectedKey)?.textValue ?? '',
104+
defaultInputValue!,
91105
props.onInputChange
92106
);
93107

@@ -102,7 +116,7 @@ export function useComboBoxState<T extends object>(props: ComboBoxStateOptions<T
102116
let [lastCollection, setLastCollection] = useState(filteredCollection);
103117

104118
// Track what action is attempting to open the menu
105-
let menuOpenTrigger = useRef('focus' as MenuTriggerAction);
119+
let menuOpenTrigger = useRef<MenuTriggerAction | undefined>('focus');
106120
let onOpenChange = (open: boolean) => {
107121
if (props.onOpenChange) {
108122
props.onOpenChange(open, open ? menuOpenTrigger.current : undefined);
@@ -115,7 +129,7 @@ export function useComboBoxState<T extends object>(props: ComboBoxStateOptions<T
115129
};
116130

117131
let triggerState = useOverlayTriggerState({...props, onOpenChange, isOpen: undefined, defaultOpen: undefined});
118-
let open = (focusStrategy: FocusStrategy = null, trigger?: MenuTriggerAction) => {
132+
let open = (focusStrategy: FocusStrategy | null = null, trigger?: MenuTriggerAction) => {
119133
let displayAllItems = (trigger === 'manual' || (trigger === 'focus' && menuTrigger === 'focus'));
120134
// Prevent open operations from triggering if there is nothing to display
121135
// Also prevent open operations from triggering if items are uncontrolled but defaultItems is empty, even if displayAllItems is true.
@@ -132,7 +146,7 @@ export function useComboBoxState<T extends object>(props: ComboBoxStateOptions<T
132146
}
133147
};
134148

135-
let toggle = (focusStrategy: FocusStrategy = null, trigger?: MenuTriggerAction) => {
149+
let toggle = (focusStrategy: FocusStrategy | null = null, trigger?: MenuTriggerAction) => {
136150
let displayAllItems = (trigger === 'manual' || (trigger === 'focus' && menuTrigger === 'focus'));
137151
// If the menu is closed and there is nothing to display, early return so toggle isn't called to prevent extraneous onOpenChange
138152
if (!(allowsEmptyCollection || filteredCollection.size > 0 || (displayAllItems && originalCollection.size > 0) || props.items) && !triggerState.isOpen) {
@@ -158,7 +172,7 @@ export function useComboBoxState<T extends object>(props: ComboBoxStateOptions<T
158172

159173
// If menu is going to close, save the current collection so we can freeze the displayed collection when the
160174
// user clicks outside the popover to close the menu. Prevents the menu contents from updating as the menu closes.
161-
let toggleMenu = useCallback((focusStrategy: FocusStrategy = null) => {
175+
let toggleMenu = useCallback((focusStrategy: FocusStrategy | null = null) => {
162176
if (triggerState.isOpen) {
163177
updateLastCollection();
164178
}
@@ -176,13 +190,15 @@ export function useComboBoxState<T extends object>(props: ComboBoxStateOptions<T
176190

177191
let [lastValue, setLastValue] = useState(inputValue);
178192
let resetInputValue = () => {
179-
let itemText = collection.getItem(selectedKey)?.textValue ?? '';
193+
let itemText = selectedKey != null ? collection.getItem(selectedKey)?.textValue ?? '' : '';
180194
setLastValue(itemText);
181195
setInputValue(itemText);
182196
};
183197

184198
let lastSelectedKey = useRef(props.selectedKey ?? props.defaultSelectedKey ?? null);
185-
let lastSelectedKeyText = useRef(collection.getItem(selectedKey)?.textValue ?? '');
199+
let lastSelectedKeyText = useRef(
200+
selectedKey != null ? collection.getItem(selectedKey)?.textValue ?? '' : ''
201+
);
186202
// intentional omit dependency array, want this to happen on every render
187203
// eslint-disable-next-line react-hooks/exhaustive-deps
188204
useEffect(() => {
@@ -245,7 +261,7 @@ export function useComboBoxState<T extends object>(props: ComboBoxStateOptions<T
245261
// This is to handle cases where a selectedKey is specified but the items aren't available (async loading) or the selected item's text value updates.
246262
// Only reset if the user isn't currently within the field so we don't erroneously modify user input.
247263
// If inputValue is controlled, it is the user's responsibility to update the inputValue when items change.
248-
let selectedItemText = collection.getItem(selectedKey)?.textValue ?? '';
264+
let selectedItemText = selectedKey != null ? collection.getItem(selectedKey)?.textValue ?? '' : '';
249265
if (!isFocused && selectedKey != null && props.inputValue === undefined && selectedKey === lastSelectedKey.current) {
250266
if (lastSelectedKeyText.current !== selectedItemText) {
251267
setLastValue(selectedItemText);
@@ -280,10 +296,10 @@ export function useComboBoxState<T extends object>(props: ComboBoxStateOptions<T
280296
let commitSelection = () => {
281297
// If multiple things are controlled, call onSelectionChange
282298
if (props.selectedKey !== undefined && props.inputValue !== undefined) {
283-
props.onSelectionChange(selectedKey);
299+
props.onSelectionChange?.(selectedKey);
284300

285301
// Stop menu from reopening from useEffect
286-
let itemText = collection.getItem(selectedKey)?.textValue ?? '';
302+
let itemText = selectedKey != null ? collection.getItem(selectedKey)?.textValue ?? '' : '';
287303
setLastValue(itemText);
288304
closeMenu();
289305
} else {
@@ -295,7 +311,7 @@ export function useComboBoxState<T extends object>(props: ComboBoxStateOptions<T
295311

296312
const commitValue = () => {
297313
if (allowsCustomValue) {
298-
const itemText = collection.getItem(selectedKey)?.textValue ?? '';
314+
const itemText = selectedKey != null ? collection.getItem(selectedKey)?.textValue ?? '' : '';
299315
(inputValue === itemText) ? commitSelection() : commitCustomValue();
300316
} else {
301317
// Reset inputValue and close menu
@@ -376,7 +392,7 @@ function filterCollection<T extends object>(collection: Collection<Node<T>>, inp
376392
}
377393

378394
function filterNodes<T>(collection: Collection<Node<T>>, nodes: Iterable<Node<T>>, inputValue: string, filter: FilterFn): Iterable<Node<T>> {
379-
let filteredNode = [];
395+
let filteredNode: Node<T>[] = [];
380396
for (let node of nodes) {
381397
if (node.type === 'section' && node.hasChildNodes) {
382398
let filtered = filterNodes(collection, getChildNodes(node, collection), inputValue, filter);

0 commit comments

Comments
 (0)