From 8f3c0ea0931107eceba0127fb524b79b46eb838b Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Wed, 21 Aug 2024 16:54:32 +1000 Subject: [PATCH] TS Strict Stately List (#6567) * TS Strict Stately List --- packages/@react-aria/tabs/src/useTabPanel.ts | 2 +- packages/@react-aria/tabs/src/utils.ts | 6 +- .../autocomplete/src/SearchAutocomplete.tsx | 2 +- packages/@react-spectrum/tabs/src/Tabs.tsx | 75 ++++++++++--------- .../combobox/src/useComboBoxState.ts | 46 ++++++++---- .../@react-stately/list/src/ListCollection.ts | 14 ++-- .../@react-stately/list/src/useListState.ts | 24 +++--- .../list/src/useSingleSelectListState.ts | 10 ++- .../select/src/useSelectState.ts | 2 +- .../@react-stately/selection/src/types.ts | 2 +- .../steplist/src/useStepListState.ts | 4 +- packages/@react-types/combobox/src/index.d.ts | 2 +- scripts/compareAPIs.js | 13 +++- tsconfig.json | 5 +- 14 files changed, 123 insertions(+), 84 deletions(-) diff --git a/packages/@react-aria/tabs/src/useTabPanel.ts b/packages/@react-aria/tabs/src/useTabPanel.ts index 42e9825006d..fb2c42cce0e 100644 --- a/packages/@react-aria/tabs/src/useTabPanel.ts +++ b/packages/@react-aria/tabs/src/useTabPanel.ts @@ -27,7 +27,7 @@ export interface TabPanelAria { * Provides the behavior and accessibility implementation for a tab panel. A tab panel is a container for * the contents of a tab, and is shown when the tab is selected. */ -export function useTabPanel(props: AriaTabPanelProps, state: TabListState, ref: RefObject): TabPanelAria { +export function useTabPanel(props: AriaTabPanelProps, state: TabListState | null, ref: RefObject): TabPanelAria { // The tabpanel should have tabIndex=0 when there are no tabbable elements within it. // Otherwise, tabbing from the focused tab should go directly to the first tabbable element // within the tabpanel. diff --git a/packages/@react-aria/tabs/src/utils.ts b/packages/@react-aria/tabs/src/utils.ts index 8311be6f686..7f7d01b92d4 100644 --- a/packages/@react-aria/tabs/src/utils.ts +++ b/packages/@react-aria/tabs/src/utils.ts @@ -15,7 +15,11 @@ import {TabListState} from '@react-stately/tabs'; export const tabsIds = new WeakMap, string>(); -export function generateId(state: TabListState, key: Key, role: string) { +export function generateId(state: TabListState | null, key: Key | null | undefined, role: string) { + if (!state) { + // this case should only happen in the first render before the tabs are registered + return ''; + } if (typeof key === 'string') { key = key.replace(/\s+/g, ''); } diff --git a/packages/@react-spectrum/autocomplete/src/SearchAutocomplete.tsx b/packages/@react-spectrum/autocomplete/src/SearchAutocomplete.tsx index ad0af675c72..3b97d0286ea 100644 --- a/packages/@react-spectrum/autocomplete/src/SearchAutocomplete.tsx +++ b/packages/@react-spectrum/autocomplete/src/SearchAutocomplete.tsx @@ -172,7 +172,7 @@ function _SearchAutocompleteBase(props: SpectrumSearchAutocomp {...listBoxProps} ref={listBoxRef} disallowEmptySelection - autoFocus={state.focusStrategy} + autoFocus={state.focusStrategy ?? undefined} shouldSelectOnPressUp focusOnPointerEnter layout={layout} diff --git a/packages/@react-spectrum/tabs/src/Tabs.tsx b/packages/@react-spectrum/tabs/src/Tabs.tsx index 99d93271320..0ebfd3a8c13 100644 --- a/packages/@react-spectrum/tabs/src/Tabs.tsx +++ b/packages/@react-spectrum/tabs/src/Tabs.tsx @@ -12,13 +12,14 @@ import {AriaTabPanelProps, SpectrumTabListProps, SpectrumTabPanelsProps, SpectrumTabsProps} from '@react-types/tabs'; import {classNames, SlotProvider, unwrapDOMRef, useDOMRef, useStyleProps} from '@react-spectrum/utils'; -import {DOMProps, DOMRef, Key, Node, Orientation, StyleProps} from '@react-types/shared'; +import {DOMProps, DOMRef, DOMRefValue, Key, Node, Orientation, RefObject, StyleProps} from '@react-types/shared'; import {filterDOMProps, mergeProps, useId, useLayoutEffect, useResizeObserver} from '@react-aria/utils'; import {FocusRing} from '@react-aria/focus'; import {Item, Picker} from '@react-spectrum/picker'; import {ListCollection} from '@react-stately/list'; import React, { - MutableRefObject, + CSSProperties, + HTMLAttributes, ReactElement, ReactNode, useCallback, @@ -40,22 +41,20 @@ import {useTab, useTabList, useTabPanel} from '@react-aria/tabs'; interface TabsContext { tabProps: SpectrumTabsProps, tabState: { - tabListState: TabListState, + tabListState: TabListState | null, setTabListState: (state: TabListState) => void, - selectedTab: HTMLElement, + selectedTab: HTMLElement | null, collapsed: boolean }, refs: { - wrapperRef: MutableRefObject, - tablistRef: MutableRefObject - }, - tabPanelProps: { - 'aria-labelledby': string + wrapperRef: RefObject, + tablistRef: RefObject }, + tabPanelProps: HTMLAttributes, tabLineState: Array } -const TabContext = React.createContext>(null); +const TabContext = React.createContext | null>(null); function Tabs(props: SpectrumTabsProps, ref: DOMRef) { props = useProviderProps(props); @@ -67,20 +66,20 @@ function Tabs(props: SpectrumTabsProps, ref: DOMRef(undefined); - let wrapperRef = useRef(undefined); + let tablistRef = useRef(null); + let wrapperRef = useRef(null); let {direction} = useLocale(); let {styleProps} = useStyleProps(otherProps); let [collapsed, setCollapsed] = useState(false); - let [selectedTab, setSelectedTab] = useState(); - const [tabListState, setTabListState] = useState>(null); - let [tabPositions, setTabPositions] = useState([]); - let prevTabPositions = useRef(tabPositions); + let [selectedTab, setSelectedTab] = useState(null); + const [tabListState, setTabListState] = useState | null>(null); + let [tabPositions, setTabPositions] = useState([]); + let prevTabPositions = useRef(tabPositions); useEffect(() => { if (tablistRef.current) { - let selectedTab: HTMLElement = tablistRef.current.querySelector(`[data-key="${CSS.escape(tabListState?.selectedKey?.toString())}"]`); + let selectedTab: HTMLElement | null = tablistRef.current.querySelector(`[data-key="${CSS.escape(tabListState?.selectedKey?.toString() ?? '')}"]`); if (selectedTab != null) { setSelectedTab(selectedTab); @@ -92,15 +91,16 @@ function Tabs(props: SpectrumTabsProps, ref: DOMRef { if (wrapperRef.current && orientation !== 'vertical') { let tabsComponent = wrapperRef.current; - let tabs = tablistRef.current.querySelectorAll('[role="tab"]'); - let tabDimensions = [...tabs].map(tab => tab.getBoundingClientRect()); + let tabs: NodeListOf = tablistRef.current?.querySelectorAll('[role="tab"]') ?? new NodeList() as NodeListOf; + let tabDimensions = [...tabs].map((tab: Element) => tab.getBoundingClientRect()); let end = direction === 'rtl' ? 'left' : 'right'; let farEdgeTabList = tabsComponent.getBoundingClientRect()[end]; let farEdgeLastTab = tabDimensions[tabDimensions.length - 1][end]; let shouldCollapse = direction === 'rtl' ? farEdgeLastTab < farEdgeTabList : farEdgeTabList < farEdgeLastTab; setCollapsed(shouldCollapse); - if (tabDimensions.length !== prevTabPositions.current.length || tabDimensions.some((box, index) => box?.left !== prevTabPositions.current[index]?.left || box?.right !== prevTabPositions.current[index]?.right)) { + if (tabDimensions.length !== prevTabPositions.current.length + || tabDimensions.some((box, index) => box?.left !== prevTabPositions.current[index]?.left || box?.right !== prevTabPositions.current[index]?.right)) { setTabPositions(tabDimensions); prevTabPositions.current = tabDimensions; } @@ -113,7 +113,7 @@ function Tabs(props: SpectrumTabsProps, ref: DOMRef = { 'aria-labelledby': undefined }; @@ -202,8 +202,8 @@ function Tab(props: TabProps) { interface TabLineProps { orientation?: Orientation, - selectedTab?: HTMLElement, - selectedKey?: Key + selectedTab?: HTMLElement | null, + selectedKey?: Key | null } // @private @@ -218,18 +218,20 @@ function TabLine(props: TabLineProps) { let {direction} = useLocale(); let {scale} = useProvider(); - let {tabLineState} = useContext(TabContext); + let {tabLineState} = useContext(TabContext)!; - let [style, setStyle] = useState({ + let [style, setStyle] = useState({ width: undefined, height: undefined }); let onResize = useCallback(() => { if (selectedTab) { - let styleObj = {transform: undefined, width: undefined, height: undefined}; + let styleObj: CSSProperties = {transform: undefined, width: undefined, height: undefined}; // 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 - let offset = direction === 'rtl' ? -1 * ((selectedTab.offsetParent as HTMLElement)?.offsetWidth - selectedTab.offsetWidth - selectedTab.offsetLeft) : selectedTab.offsetLeft; + let offset = direction === 'rtl' ? + -1 * ((selectedTab.offsetParent as HTMLElement)?.offsetWidth - selectedTab.offsetWidth - selectedTab.offsetLeft) : + selectedTab.offsetLeft; styleObj.transform = orientation === 'vertical' ? `translateY(${selectedTab.offsetTop}px)` : `translateX(${offset}px)`; @@ -255,7 +257,7 @@ function TabLine(props: TabLineProps) { * The keys of the items within the must match up with a corresponding item inside the . */ export function TabList(props: SpectrumTabListProps) { - const tabContext = useContext(TabContext); + const tabContext = useContext(TabContext)!; const {refs, tabState, tabProps, tabPanelProps} = tabContext; const {isQuiet, density, isEmphasized, orientation} = tabProps; const {selectedTab, collapsed, setTabListState} = tabState; @@ -330,13 +332,13 @@ export function TabList(props: SpectrumTabListProps) { * TabPanels is used within Tabs as a container for the content of each tab. * The keys of the items within the must match up with a corresponding item inside the . */ -export function TabPanels(props: SpectrumTabPanelsProps) { - const {tabState, tabProps} = useContext(TabContext); +export function TabPanels(props: SpectrumTabPanelsProps) { + const {tabState, tabProps} = useContext(TabContext)!; const {tabListState} = tabState; - const factory = useCallback(nodes => new ListCollection(nodes), []); + const factory = useCallback((nodes: Iterable>) => new ListCollection(nodes), []); const collection = useCollection({items: tabProps.items, ...props}, factory, {suppressTextValueWarning: true}); - const selectedItem = tabListState ? collection.getItem(tabListState.selectedKey) : null; + const selectedItem = tabListState && tabListState.selectedKey != null ? collection.getItem(tabListState.selectedKey) : null; return ( @@ -351,9 +353,9 @@ interface TabPanelProps extends AriaTabPanelProps, StyleProps { // @private function TabPanel(props: TabPanelProps) { - const {tabState, tabPanelProps: ctxTabPanelProps} = useContext(TabContext); + const {tabState, tabPanelProps: ctxTabPanelProps} = useContext(TabContext)!; const {tabListState} = tabState; - let ref = useRef(undefined); + let ref = useRef(null); const {tabPanelProps} = useTabPanel(props, tabListState, ref); let {styleProps} = useStyleProps(props); @@ -392,8 +394,8 @@ function TabPicker(props: TabPickerProps) { visible } = props; - let ref = useRef(undefined); - let [pickerNode, setPickerNode] = useState(null); + let ref = useRef>(null); + let [pickerNode, setPickerNode] = useState(null); useEffect(() => { let node = unwrapDOMRef(ref); @@ -408,7 +410,6 @@ function TabPicker(props: TabPickerProps) { const style : React.CSSProperties = visible ? {} : {visibility: 'hidden', position: 'absolute'}; - // TODO: Figure out if tabListProps should go onto the div here, v2 doesn't do it return (
extends SelectState, FormValidationState{ /** Selects the currently focused item and updates the input value. */ commit(): void, /** Controls which item will be auto focused when the menu opens. */ - readonly focusStrategy: FocusStrategy, + readonly focusStrategy: FocusStrategy | null, /** Opens the menu. */ open(focusStrategy?: FocusStrategy | null, trigger?: MenuTriggerAction): void, /** Toggles the menu. */ @@ -64,7 +64,7 @@ export function useComboBoxState(props: ComboBoxStateOptions(null); + let [focusStrategy, setFocusStrategy] = useState(null); let onSelectionChange = (key) => { if (props.onSelectionChange) { @@ -79,15 +79,29 @@ export function useComboBoxState(props: ComboBoxStateOptions(props: ComboBoxStateOptions('focus'); let onOpenChange = (open: boolean) => { if (props.onOpenChange) { props.onOpenChange(open, open ? menuOpenTrigger.current : undefined); @@ -115,7 +129,7 @@ export function useComboBoxState(props: ComboBoxStateOptions { + let open = (focusStrategy: FocusStrategy | null = null, trigger?: MenuTriggerAction) => { let displayAllItems = (trigger === 'manual' || (trigger === 'focus' && menuTrigger === 'focus')); // Prevent open operations from triggering if there is nothing to display // 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(props: ComboBoxStateOptions { + let toggle = (focusStrategy: FocusStrategy | null = null, trigger?: MenuTriggerAction) => { let displayAllItems = (trigger === 'manual' || (trigger === 'focus' && menuTrigger === 'focus')); // If the menu is closed and there is nothing to display, early return so toggle isn't called to prevent extraneous onOpenChange if (!(allowsEmptyCollection || filteredCollection.size > 0 || (displayAllItems && originalCollection.size > 0) || props.items) && !triggerState.isOpen) { @@ -158,7 +172,7 @@ export function useComboBoxState(props: ComboBoxStateOptions { + let toggleMenu = useCallback((focusStrategy: FocusStrategy | null = null) => { if (triggerState.isOpen) { updateLastCollection(); } @@ -176,13 +190,15 @@ export function useComboBoxState(props: ComboBoxStateOptions { - let itemText = collection.getItem(selectedKey)?.textValue ?? ''; + let itemText = selectedKey != null ? collection.getItem(selectedKey)?.textValue ?? '' : ''; setLastValue(itemText); setInputValue(itemText); }; let lastSelectedKey = useRef(props.selectedKey ?? props.defaultSelectedKey ?? null); - let lastSelectedKeyText = useRef(collection.getItem(selectedKey)?.textValue ?? ''); + let lastSelectedKeyText = useRef( + selectedKey != null ? collection.getItem(selectedKey)?.textValue ?? '' : '' + ); // intentional omit dependency array, want this to happen on every render // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => { @@ -245,7 +261,7 @@ export function useComboBoxState(props: ComboBoxStateOptions(props: ComboBoxStateOptions { // If multiple things are controlled, call onSelectionChange if (props.selectedKey !== undefined && props.inputValue !== undefined) { - props.onSelectionChange(selectedKey); + props.onSelectionChange?.(selectedKey); // Stop menu from reopening from useEffect - let itemText = collection.getItem(selectedKey)?.textValue ?? ''; + let itemText = selectedKey != null ? collection.getItem(selectedKey)?.textValue ?? '' : ''; setLastValue(itemText); closeMenu(); } else { @@ -295,7 +311,7 @@ export function useComboBoxState(props: ComboBoxStateOptions { if (allowsCustomValue) { - const itemText = collection.getItem(selectedKey)?.textValue ?? ''; + const itemText = selectedKey != null ? collection.getItem(selectedKey)?.textValue ?? '' : ''; (inputValue === itemText) ? commitSelection() : commitCustomValue(); } else { // Reset inputValue and close menu @@ -376,7 +392,7 @@ function filterCollection(collection: Collection>, inp } function filterNodes(collection: Collection>, nodes: Iterable>, inputValue: string, filter: FilterFn): Iterable> { - let filteredNode = []; + let filteredNode: Node[] = []; for (let node of nodes) { if (node.type === 'section' && node.hasChildNodes) { let filtered = filterNodes(collection, getChildNodes(node, collection), inputValue, filter); diff --git a/packages/@react-stately/list/src/ListCollection.ts b/packages/@react-stately/list/src/ListCollection.ts index 63e477f182d..28c3d3f620b 100644 --- a/packages/@react-stately/list/src/ListCollection.ts +++ b/packages/@react-stately/list/src/ListCollection.ts @@ -15,8 +15,8 @@ import {Collection, Key, Node} from '@react-types/shared'; export class ListCollection implements Collection> { private keyMap: Map> = new Map(); private iterable: Iterable>; - private firstKey: Key; - private lastKey: Key; + private firstKey: Key | null = null; + private lastKey: Key | null = null; constructor(nodes: Iterable>) { this.iterable = nodes; @@ -35,7 +35,7 @@ export class ListCollection implements Collection> { visit(node); } - let last: Node; + let last: Node | null = null; let index = 0; for (let [key, node] of this.keyMap) { if (last) { @@ -57,7 +57,7 @@ export class ListCollection implements Collection> { last.nextKey = undefined; } - this.lastKey = last?.key; + this.lastKey = last?.key ?? null; } *[Symbol.iterator]() { @@ -74,12 +74,12 @@ export class ListCollection implements Collection> { getKeyBefore(key: Key) { let node = this.keyMap.get(key); - return node ? node.prevKey : null; + return node ? node.prevKey ?? null : null; } getKeyAfter(key: Key) { let node = this.keyMap.get(key); - return node ? node.nextKey : null; + return node ? node.nextKey ?? null : null; } getFirstKey() { @@ -91,7 +91,7 @@ export class ListCollection implements Collection> { } getItem(key: Key) { - return this.keyMap.get(key); + return this.keyMap.get(key) ?? null; } at(idx: number) { diff --git a/packages/@react-stately/list/src/useListState.ts b/packages/@react-stately/list/src/useListState.ts index 92e5f9c92fd..c6018bb0a45 100644 --- a/packages/@react-stately/list/src/useListState.ts +++ b/packages/@react-stately/list/src/useListState.ts @@ -57,31 +57,31 @@ export function useListState(props: ListProps): ListState> | null>(null); useEffect(() => { - if (selectionState.focusedKey != null && !collection.getItem(selectionState.focusedKey)) { + if (selectionState.focusedKey != null && !collection.getItem(selectionState.focusedKey) && cachedCollection.current) { const startItem = cachedCollection.current.getItem(selectionState.focusedKey); const cachedItemNodes = [...cachedCollection.current.getKeys()].map( key => { - const itemNode = cachedCollection.current.getItem(key); - return itemNode.type === 'item' ? itemNode : null; + const itemNode = cachedCollection.current!.getItem(key); + return itemNode?.type === 'item' ? itemNode : null; } ).filter(node => node !== null); const itemNodes = [...collection.getKeys()].map( key => { const itemNode = collection.getItem(key); - return itemNode.type === 'item' ? itemNode : null; + return itemNode?.type === 'item' ? itemNode : null; } ).filter(node => node !== null); - const diff = cachedItemNodes.length - itemNodes.length; + const diff: number = (cachedItemNodes?.length ?? 0) - (itemNodes?.length ?? 0); let index = Math.min( ( diff > 1 ? - Math.max(startItem.index - diff + 1, 0) : - startItem.index + Math.max((startItem?.index ?? 0) - diff + 1, 0) : + startItem?.index ?? 0 ), - itemNodes.length - 1); - let newNode:Node; + (itemNodes?.length ?? 0) - 1); + let newNode: Node | null = null; let isReverseSearching = false; while (index >= 0) { if (!selectionManager.isDisabled(itemNodes[index].key)) { @@ -94,8 +94,8 @@ export function useListState(props: ListProps): ListState startItem.index) { - index = startItem.index; + if (index > (startItem?.index ?? 0)) { + index = (startItem?.index ?? 0); } index--; } diff --git a/packages/@react-stately/list/src/useSingleSelectListState.ts b/packages/@react-stately/list/src/useSingleSelectListState.ts index 1126aabdb43..b99a98af8ad 100644 --- a/packages/@react-stately/list/src/useSingleSelectListState.ts +++ b/packages/@react-stately/list/src/useSingleSelectListState.ts @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import {CollectionStateBase, Key, Node, SingleSelection} from '@react-types/shared'; +import {CollectionStateBase, Key, Node, Selection, SingleSelection} from '@react-types/shared'; import {ListState, useListState} from './useListState'; import {useControlledState} from '@react-stately/utils'; import {useMemo} from 'react'; @@ -24,7 +24,7 @@ export interface SingleSelectListProps extends CollectionStateBase, Omit extends ListState { /** The key for the currently selected item. */ - readonly selectedKey: Key, + readonly selectedKey: Key | null, /** Sets the selected key. */ setSelectedKey(key: Key | null): void, @@ -46,7 +46,11 @@ export function useSingleSelectListState(props: SingleSelectLi disallowEmptySelection: true, allowDuplicateSelectionEvents: true, selectedKeys, - onSelectionChange: (keys: Set) => { + onSelectionChange: (keys: Selection) => { + // impossible, but TS doesn't know that + if (keys === 'all') { + return; + } let key = keys.values().next().value ?? null; // Always fire onSelectionChange, even if the key is the same diff --git a/packages/@react-stately/select/src/useSelectState.ts b/packages/@react-stately/select/src/useSelectState.ts index 0c19849b33b..f02cf7adb24 100644 --- a/packages/@react-stately/select/src/useSelectState.ts +++ b/packages/@react-stately/select/src/useSelectState.ts @@ -27,7 +27,7 @@ export interface SelectState extends SingleSelectListState, OverlayTrigger setFocused(isFocused: boolean): void, /** Controls which item will be auto focused when the menu opens. */ - readonly focusStrategy: FocusStrategy, + readonly focusStrategy: FocusStrategy | null, /** Opens the menu. */ open(focusStrategy?: FocusStrategy | null): void, diff --git a/packages/@react-stately/selection/src/types.ts b/packages/@react-stately/selection/src/types.ts index c116cb07384..fcc2b1c346f 100644 --- a/packages/@react-stately/selection/src/types.ts +++ b/packages/@react-stately/selection/src/types.ts @@ -23,7 +23,7 @@ export interface FocusState { /** Whether the first or last child of the focused key should receive focus. */ readonly childFocusStrategy: FocusStrategy, /** Sets the focused key, and optionally, whether the first or last child of that key should receive focus. */ - setFocusedKey(key: Key, child?: FocusStrategy): void + setFocusedKey(key: Key | null, child?: FocusStrategy): void } export interface SingleSelectionState extends FocusState { diff --git a/packages/@react-stately/steplist/src/useStepListState.ts b/packages/@react-stately/steplist/src/useStepListState.ts index e63d8316794..7dddc99999a 100644 --- a/packages/@react-stately/steplist/src/useStepListState.ts +++ b/packages/@react-stately/steplist/src/useStepListState.ts @@ -41,7 +41,7 @@ export function useStepListState(props: StepListProps): Ste let [lastCompletedStep, setLastCompletedStep] = useControlledState(props.lastCompletedStep, props.defaultLastCompletedStep ?? null, props.onLastCompletedStepChange); const {setSelectedKey: realSetSelectedKey, selectedKey, collection} = state; const {indexMap, keysLinkedList} = useMemo(() => buildKeysMaps(collection), [collection]); - const selectedIdx = indexMap.get(selectedKey); + const selectedIdx = selectedKey != null ? indexMap.get(selectedKey) : 0; const isCompleted = useCallback((step: Key | null | undefined) => { if (step === undefined) { @@ -71,7 +71,7 @@ export function useStepListState(props: StepListProps): Ste useEffect(() => { // Ensure a step is always selected (in case no selected key was specified or if selected item was deleted from collection) let selectedKey: Key | null = state.selectedKey; - if (state.selectionManager.isEmpty || !state.collection.getItem(selectedKey)) { + if (state.selectionManager.isEmpty || selectedKey == null || !state.collection.getItem(selectedKey)) { selectedKey = findDefaultSelectedKey(state.collection, state.disabledKeys); if (selectedKey !== null) { state.selectionManager.replaceSelection(selectedKey); diff --git a/packages/@react-types/combobox/src/index.d.ts b/packages/@react-types/combobox/src/index.d.ts index 750b552a050..5b96371b82e 100644 --- a/packages/@react-types/combobox/src/index.d.ts +++ b/packages/@react-types/combobox/src/index.d.ts @@ -36,7 +36,7 @@ export type MenuTriggerAction = 'focus' | 'input' | 'manual'; export interface ComboBoxValidationValue { /** The selected key in the ComboBox. */ - selectedKey: Key, + selectedKey: Key | null, /** The value of the ComboBox input. */ inputValue: string } diff --git a/scripts/compareAPIs.js b/scripts/compareAPIs.js index 8a8f1925236..784f5cd3111 100644 --- a/scripts/compareAPIs.js +++ b/scripts/compareAPIs.js @@ -122,7 +122,18 @@ async function compare() { changes.push(` #### ${simplifiedName} ${changedByDeps.length > 0 ? `changed by: - - ${changedByDeps.join('\n - ')}\n\n` : ''}${diff.split('\n').filter(line => line !== ' ').join('\n')}${affected.length > 0 ? ` + - ${changedByDeps.join('\n - ')}\n\n` : ''}${diff.split('\n').filter(line => line !== ' ').join('\n')}${ +// eslint-disable-next-line no-nested-ternary +affected.length > 0 ? +argv.isCI ? ` +
+ it changed +
    +
  • ${affected.join('
  • \n
  • ')}
  • +
+
+` + : ` it changed: - ${affected.join('\n - ')} ` : ''} diff --git a/tsconfig.json b/tsconfig.json index 61df7feffbf..bc6ffbb927c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -102,6 +102,7 @@ "./packages/@react-spectrum/steplist", "./packages/@react-spectrum/story-utils", "./packages/@react-spectrum/switch", + "./packages/@react-spectrum/tabs", "./packages/@react-spectrum/tag", "./packages/@react-spectrum/text", "./packages/@react-spectrum/textfield", @@ -114,12 +115,14 @@ "./packages/@react-stately/calendar", "./packages/@react-stately/checkbox", "./packages/@react-stately/color", + "./packages/@react-stately/combobox", + "./packages/@react-stately/list", "./packages/@react-stately/numberfield", "./packages/@react-stately/overlays", "./packages/@react-stately/pagination", + "./packages/@react-stately/radio", "./packages/@react-stately/searchfield", "./packages/@react-stately/steplist", - "./packages/@react-stately/radio", "./packages/@react-stately/toggle", "./packages/@react-stately/utils", "./packages/@react-types/a",