From 3fb5134496fab4caf1bf4ac2e83d5a7665e88c65 Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Tue, 18 Jun 2024 13:14:40 +1000 Subject: [PATCH 01/66] TS Strict Collections --- packages/@react-aria/tabs/stories/example.tsx | 8 +++- .../collections/src/CollectionBuilder.ts | 37 +++++++++++-------- .../@react-stately/collections/src/Item.ts | 2 +- .../@react-stately/collections/src/Section.ts | 2 +- .../collections/src/getChildNodes.ts | 17 +++++---- .../collections/src/getItemCount.ts | 9 +++-- .../@react-stately/collections/src/types.ts | 2 +- .../@react-types/shared/src/collections.d.ts | 10 +++-- packages/react-aria-components/src/Tree.tsx | 2 +- tsconfig.json | 1 + 10 files changed, 53 insertions(+), 37 deletions(-) diff --git a/packages/@react-aria/tabs/stories/example.tsx b/packages/@react-aria/tabs/stories/example.tsx index 39d004c3855..e7886d1b53c 100644 --- a/packages/@react-aria/tabs/stories/example.tsx +++ b/packages/@react-aria/tabs/stories/example.tsx @@ -10,11 +10,15 @@ * governing permissions and limitations under the License. */ +import {AriaTabListProps, useTab, useTabList, useTabPanel} from '@react-aria/tabs'; import React from 'react'; -import {useTab, useTabList, useTabPanel} from '@react-aria/tabs'; import {useTabListState} from '@react-stately/tabs'; -export function Tabs({shouldSelectOnPressUp, ...props}) { +interface TabProps extends AriaTabListProps { + shouldSelectOnPressUp?: boolean +} + +export function Tabs({shouldSelectOnPressUp, ...props}: TabProps) { let state = useTabListState(props); let ref = React.useRef(null); let {tabListProps} = useTabList(props, state, ref); diff --git a/packages/@react-stately/collections/src/CollectionBuilder.ts b/packages/@react-stately/collections/src/CollectionBuilder.ts index 09f804818b4..969020a3105 100644 --- a/packages/@react-stately/collections/src/CollectionBuilder.ts +++ b/packages/@react-stately/collections/src/CollectionBuilder.ts @@ -22,12 +22,12 @@ export class CollectionBuilder { private context?: unknown; private cache: WeakMap> = new WeakMap(); - build(props: CollectionBase, context?: unknown) { + build(props: Partial>, context?: unknown) { this.context = context; return iterable(() => this.iterateCollection(props)); } - private *iterateCollection(props: CollectionBase): Generator> { + private *iterateCollection(props: Partial>): Generator> { let {children, items} = props; if (React.isValidElement<{children: CollectionElement}>(children) && children.type === React.Fragment) { @@ -40,7 +40,7 @@ export class CollectionBuilder { throw new Error('props.children was a function but props.items is missing'); } - for (let item of props.items) { + for (let item of items) { yield* this.getFullNode({ value: item }, {renderer: children}); @@ -48,7 +48,9 @@ export class CollectionBuilder { } else { let items: CollectionElement[] = []; React.Children.forEach(children, child => { - items.push(child); + if (child) { + items.push(child); + } }); let index = 0; @@ -66,7 +68,7 @@ export class CollectionBuilder { } } - private getKey(item: CollectionElement, partialNode: PartialNode, state: CollectionBuilderState, parentKey?: Key): Key { + private getKey(item: CollectionElement, partialNode: PartialNode, state: CollectionBuilderState, parentKey?: Key | null): Key { if (item.key != null) { return item.key; } @@ -94,7 +96,7 @@ export class CollectionBuilder { }; } - private *getFullNode(partialNode: PartialNode, state: CollectionBuilderState, parentKey?: Key, parentNode?: Node): Generator> { + private *getFullNode(partialNode: PartialNode, state: CollectionBuilderState, parentKey?: Key | null, parentNode?: Node): Generator> { if (React.isValidElement<{children: CollectionElement}>(partialNode.element) && partialNode.element.type === React.Fragment) { let children: CollectionElement[] = []; @@ -129,24 +131,27 @@ export class CollectionBuilder { element = state.renderer(partialNode.value); } + interface CollectReactElement extends ReactElement { + getCollectionNode(props: any, context: any): Generator, void, Node[]> + } // If there's an element with a getCollectionNode function on its type, then it's a supported component. // Call this function to get a partial node, and recursively build a full node from there. if (React.isValidElement(element)) { - let type = element.type as any; + let type = element.type as unknown as CollectReactElement; if (typeof type !== 'function' && typeof type.getCollectionNode !== 'function') { - let name = typeof element.type === 'function' ? element.type.name : element.type; + let name = element.type; throw new Error(`Unknown element <${name}> in collection.`); } let childNodes = type.getCollectionNode(element.props, this.context) as Generator, void, Node[]>; - let index = partialNode.index; + let index = partialNode.index ?? 0; let result = childNodes.next(); while (!result.done && result.value) { let childNode = result.value; partialNode.index = index; - let nodeKey = childNode.key; + let nodeKey: string | number | undefined | null = childNode.key; if (!nodeKey) { nodeKey = childNode.element ? null : this.getKey(element as CollectionElement, partialNode, state, parentKey); } @@ -169,7 +174,7 @@ export class CollectionBuilder { // The partial node may have specified a type for the child in order to specify a constraint. // Verify that the full node that was built recursively matches this type. if (partialNode.type && node.type !== partialNode.type) { - throw new Error(`Unsupported type <${capitalize(node.type)}> in <${capitalize(parentNode.type)}>. Only <${capitalize(partialNode.type)}> is supported.`); + throw new Error(`Unsupported type <${capitalize(node.type)}> in <${capitalize(parentNode?.type ?? 'unknown parent type')}>. Only <${capitalize(partialNode.type)}> is supported.`); } index++; @@ -183,7 +188,7 @@ export class CollectionBuilder { } // Ignore invalid elements - if (partialNode.key == null) { + if (partialNode.key == null || partialNode.type == null) { return; } @@ -204,7 +209,7 @@ export class CollectionBuilder { shouldInvalidate: partialNode.shouldInvalidate, hasChildNodes: partialNode.hasChildNodes, childNodes: iterable(function *() { - if (!partialNode.hasChildNodes) { + if (!partialNode.hasChildNodes || !partialNode.childNodes) { return; } @@ -235,8 +240,8 @@ export class CollectionBuilder { // Wraps an iterator function as an iterable object, and caches the results. function iterable(iterator: () => IterableIterator>): Iterable> { - let cache = []; - let iterable = null; + let cache: Array> = []; + let iterable: null | IterableIterator> = null; return { *[Symbol.iterator]() { for (let item of cache) { @@ -256,7 +261,7 @@ function iterable(iterator: () => IterableIterator>): Iterable ReactElement; -function compose(outer: Wrapper | void, inner: Wrapper | void): Wrapper { +function compose(outer: Wrapper | void, inner: Wrapper | void): Wrapper | undefined { if (outer && inner) { return (element) => outer(inner(element)); } diff --git a/packages/@react-stately/collections/src/Item.ts b/packages/@react-stately/collections/src/Item.ts index 4ea49f91c71..de29898f5ff 100644 --- a/packages/@react-stately/collections/src/Item.ts +++ b/packages/@react-stately/collections/src/Item.ts @@ -14,7 +14,7 @@ import {ItemElement, ItemProps} from '@react-types/shared'; import {PartialNode} from './types'; import React, {JSX, ReactElement} from 'react'; -function Item(props: ItemProps): ReactElement { // eslint-disable-line @typescript-eslint/no-unused-vars +function Item(props: ItemProps): ReactElement | null { // eslint-disable-line @typescript-eslint/no-unused-vars return null; } diff --git a/packages/@react-stately/collections/src/Section.ts b/packages/@react-stately/collections/src/Section.ts index 97fceb5d4b9..d015ac6236d 100644 --- a/packages/@react-stately/collections/src/Section.ts +++ b/packages/@react-stately/collections/src/Section.ts @@ -14,7 +14,7 @@ import {PartialNode} from './types'; import React, {JSX, ReactElement} from 'react'; import {SectionProps} from '@react-types/shared'; -function Section(props: SectionProps): ReactElement { // eslint-disable-line @typescript-eslint/no-unused-vars +function Section(props: SectionProps): ReactElement | null { // eslint-disable-line @typescript-eslint/no-unused-vars return null; } diff --git a/packages/@react-stately/collections/src/getChildNodes.ts b/packages/@react-stately/collections/src/getChildNodes.ts index 8692c2030b0..5db5d8df771 100644 --- a/packages/@react-stately/collections/src/getChildNodes.ts +++ b/packages/@react-stately/collections/src/getChildNodes.ts @@ -42,7 +42,7 @@ export function getNthItem(iterable: Iterable, index: number): T | undefin } export function getLastItem(iterable: Iterable): T | undefined { - let lastItem = undefined; + let lastItem: undefined | T = undefined; for (let value of iterable) { lastItem = value; } @@ -52,7 +52,7 @@ export function getLastItem(iterable: Iterable): T | undefined { export function compareNodeOrder(collection: Collection>, a: Node, b: Node) { // If the two nodes have the same parent, compare their indices. - if (a.parentKey === b.parentKey) { + if (a.parentKey === b.parentKey && a.index != null && b.index != null) { return a.index - b.index; } @@ -66,7 +66,9 @@ export function compareNodeOrder(collection: Collection>, a: Node, // Compare the indices of two children within the common ancestor. a = aAncestors[firstNonMatchingAncestor]; b = bAncestors[firstNonMatchingAncestor]; - return a.index - b.index; + if (a.index != null && b.index != null) { + return a.index - b.index; + } } // If there isn't a non matching ancestor, we might be in a case where one of the nodes is the ancestor of the other. @@ -81,11 +83,12 @@ export function compareNodeOrder(collection: Collection>, a: Node, } function getAncestors(collection: Collection>, node: Node): Node[] { - let parents = []; + let parents: Array> = []; - while (node?.parentKey != null) { - node = collection.getItem(node.parentKey); - parents.unshift(node); + let currNode: Node | null = node; + while (currNode?.parentKey != null) { + currNode = collection.getItem(currNode.parentKey); + parents.unshift(currNode!); } return parents; diff --git a/packages/@react-stately/collections/src/getItemCount.ts b/packages/@react-stately/collections/src/getItemCount.ts index 952054fe64a..e1f892be544 100644 --- a/packages/@react-stately/collections/src/getItemCount.ts +++ b/packages/@react-stately/collections/src/getItemCount.ts @@ -21,18 +21,19 @@ export function getItemCount(collection: Collection>): number { return count; } - count = 0; + // TS isn't smart enough to know we've ensured count is a number, so use a new variable + let counter = 0; let countItems = (items: Iterable>) => { for (let item of items) { if (item.type === 'section') { countItems(getChildNodes(item, collection)); } else { - count++; + counter++; } } }; countItems(collection); - cache.set(collection, count); - return count; + cache.set(collection, counter); + return counter; } diff --git a/packages/@react-stately/collections/src/types.ts b/packages/@react-stately/collections/src/types.ts index 73436def7c8..c546b1038bd 100644 --- a/packages/@react-stately/collections/src/types.ts +++ b/packages/@react-stately/collections/src/types.ts @@ -15,7 +15,7 @@ import {ReactElement, ReactNode} from 'react'; export interface PartialNode { type?: string, - key?: Key, + key?: Key | null, value?: T, element?: ReactElement, wrapper?: (element: ReactElement) => ReactElement, diff --git a/packages/@react-types/shared/src/collections.d.ts b/packages/@react-types/shared/src/collections.d.ts index 15271fa4da5..3efb2c12098 100644 --- a/packages/@react-types/shared/src/collections.d.ts +++ b/packages/@react-types/shared/src/collections.d.ts @@ -189,11 +189,11 @@ export interface Node { /** A unique key for the node. */ key: Key, /** The object value the node was created from. */ - value: T | null, + value: T | null | undefined, /** The level of depth this node is at in the hierarchy. */ level: number, /** Whether this item has children, even if not loaded yet. */ - hasChildNodes: boolean, + hasChildNodes?: boolean, /** * The loaded children of this node. * @deprecated Use `collection.getChildren(node.key)` instead. @@ -201,8 +201,10 @@ export interface Node { childNodes: Iterable>, /** The rendered contents of this node (e.g. JSX). */ rendered: ReactNode, - /** A string value for this node, used for features like typeahead. */ - textValue: string, + /** A string value for this node, used for features like typeahead. + * Sections do not need to have a textValue. + */ + textValue?: string, /** An accessibility label for this node. */ 'aria-label'?: string, /** The index of this node within its parent. */ diff --git a/packages/react-aria-components/src/Tree.tsx b/packages/react-aria-components/src/Tree.tsx index 54e8ed029d5..fed08d66ede 100644 --- a/packages/react-aria-components/src/Tree.tsx +++ b/packages/react-aria-components/src/Tree.tsx @@ -92,7 +92,7 @@ class TreeCollection implements ICollection> { getTextValue(key: Key): string { let item = this.getItem(key); - return item ? item.textValue : ''; + return item ? item.textValue ?? '' : ''; } } diff --git a/tsconfig.json b/tsconfig.json index c7a069cd943..dc153cc0b09 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -123,6 +123,7 @@ "./packages/@react-stately/combobox", "./packages/@react-stately/disclosure", "./packages/@react-stately/list", + "./packages/@react-stately/collection", "./packages/@react-stately/numberfield", "./packages/@react-stately/overlays", "./packages/@react-stately/pagination", From 22be104f41bd5f872278b1ec89f1c77d84288d2b Mon Sep 17 00:00:00 2001 From: GitHub Date: Mon, 14 Oct 2024 16:48:12 +1100 Subject: [PATCH 02/66] fix rebase --- packages/@react-stately/collections/src/CollectionBuilder.ts | 2 +- packages/@react-stately/combobox/src/useComboBoxState.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@react-stately/collections/src/CollectionBuilder.ts b/packages/@react-stately/collections/src/CollectionBuilder.ts index 969020a3105..e6be1f86f1f 100644 --- a/packages/@react-stately/collections/src/CollectionBuilder.ts +++ b/packages/@react-stately/collections/src/CollectionBuilder.ts @@ -104,7 +104,7 @@ export class CollectionBuilder { children.push(child); }); - let index = partialNode.index; + let index = partialNode.index ?? 0; for (const child of children) { yield* this.getFullNode({ diff --git a/packages/@react-stately/combobox/src/useComboBoxState.ts b/packages/@react-stately/combobox/src/useComboBoxState.ts index 13f17f235da..5b4184f09fc 100644 --- a/packages/@react-stately/combobox/src/useComboBoxState.ts +++ b/packages/@react-stately/combobox/src/useComboBoxState.ts @@ -399,7 +399,7 @@ function filterNodes(collection: Collection>, nodes: Iterable if ([...filtered].some(node => node.type === 'item')) { filteredNode.push({...node, childNodes: filtered}); } - } else if (node.type === 'item' && filter(node.textValue, inputValue)) { + } else if (node.type === 'item' && node.textValue && filter(node.textValue, inputValue)) { filteredNode.push({...node}); } else if (node.type !== 'item') { filteredNode.push({...node}); From 076a4441ce8652f820807d7a1d5e95d8f590b83e Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 14 Nov 2024 06:09:14 +1100 Subject: [PATCH 03/66] Start ts strict remaining --- packages/@react-aria/overlays/src/Overlay.tsx | 2 +- .../overlays/src/PortalProvider.tsx | 2 +- .../overlays/src/ariaHideOutside.ts | 12 +- .../overlays/src/calculatePosition.ts | 50 ++++---- .../overlays/src/useCloseOnScroll.ts | 2 +- .../@react-aria/overlays/src/useModal.tsx | 4 +- .../overlays/src/useModalOverlay.ts | 2 +- .../@react-aria/overlays/src/useOverlay.ts | 4 +- .../overlays/src/useOverlayPosition.ts | 24 ++-- .../overlays/src/useOverlayTrigger.ts | 4 +- .../@react-aria/overlays/src/usePopover.ts | 4 +- .../overlays/src/usePreventScroll.ts | 17 ++- .../selection/src/DOMLayoutDelegate.ts | 19 +-- .../selection/src/ListKeyboardDelegate.ts | 80 +++++++----- .../selection/src/useSelectableCollection.ts | 66 +++++----- .../selection/src/useSelectableItem.ts | 14 +- .../selection/src/useTypeSelect.ts | 30 +++-- tsconfig.json | 121 +----------------- 18 files changed, 186 insertions(+), 271 deletions(-) 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..ee7f1a4feb1 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}); }, @@ -148,7 +152,7 @@ export function ariaHideOutside(targets: Element[], root = document.body) { observer.disconnect(); for (let node of hiddenNodes) { - let count = refCountMap.get(node); + let count = refCountMap.get(node) ?? 0; 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..e45921a8e9f 100644 --- a/packages/@react-aria/overlays/src/calculatePosition.ts +++ b/packages/@react-aria/overlays/src/calculatePosition.ts @@ -103,12 +103,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 +141,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 +174,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 +240,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 +288,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 +315,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 +389,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 +410,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 +420,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 +479,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); 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..a3f028fc917 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): ReactNode { 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({ + let [position, setPosition] = useState>({ position: {}, arrowOffsetLeft: undefined, arrowOffsetTop: undefined, @@ -154,17 +154,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 +192,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 +203,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 +272,7 @@ export function useOverlayPosition(props: AriaPositionProps): PositionAria { let close = useCallback(() => { if (!isResizing.current) { - onClose(); + onClose?.(); } }, [onClose, isResizing]); 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 7a43ea49fc4..361b1cd11bf 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 } /** @@ -97,7 +97,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..21ed52ef8db 100644 --- a/packages/@react-aria/overlays/src/usePreventScroll.ts +++ b/packages/@react-aria/overlays/src/usePreventScroll.ts @@ -195,7 +195,7 @@ function preventScrollMobileSafari() { } }; - let restoreStyles = null; + let restoreStyles: null | (() => void) = null; let setupStyles = () => { if (restoreStyles) { return; @@ -259,26 +259,29 @@ function addEvent( handler: (this: Document, ev: GlobalEventHandlersEventMap[K]) => any, options?: boolean | AddEventListenerOptions ) { + // @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/selection/src/DOMLayoutDelegate.ts b/packages/@react-aria/selection/src/DOMLayoutDelegate.ts index 4f7d778fc22..3431e66fc8d 100644 --- a/packages/@react-aria/selection/src/DOMLayoutDelegate.ts +++ b/packages/@react-aria/selection/src/DOMLayoutDelegate.ts @@ -13,14 +13,17 @@ import {Key, LayoutDelegate, Rect, RefObject, Size} from '@react-types/shared'; export class DOMLayoutDelegate implements LayoutDelegate { - private ref: RefObject; + private ref: RefObject; - constructor(ref: RefObject) { + constructor(ref: RefObject) { this.ref = ref; } getItemRect(key: Key): Rect | null { let container = this.ref.current; + if (!container) { + return null; + } let item = key != null ? container.querySelector(`[data-key="${CSS.escape(key.toString())}"]`) : null; if (!item) { return null; @@ -40,18 +43,18 @@ export class DOMLayoutDelegate implements LayoutDelegate { getContentSize(): Size { let container = this.ref.current; return { - width: container.scrollWidth, - height: container.scrollHeight + width: container?.scrollWidth ?? 0, + height: container?.scrollHeight ?? 0 }; } getVisibleRect(): Rect { let container = this.ref.current; return { - x: container.scrollLeft, - y: container.scrollTop, - width: container.offsetWidth, - height: container.offsetHeight + x: container?.scrollLeft ?? 0, + y: container?.scrollTop ?? 0, + width: container?.offsetWidth ?? 0, + height: container?.offsetHeight ?? 0 }; } } diff --git a/packages/@react-aria/selection/src/ListKeyboardDelegate.ts b/packages/@react-aria/selection/src/ListKeyboardDelegate.ts index 7a8a5f6ed34..3c4827e8384 100644 --- a/packages/@react-aria/selection/src/ListKeyboardDelegate.ts +++ b/packages/@react-aria/selection/src/ListKeyboardDelegate.ts @@ -74,47 +74,54 @@ export class ListKeyboardDelegate implements KeyboardDelegate { return this.disabledBehavior === 'all' && (item.props?.isDisabled || this.disabledKeys.has(item.key)); } - private findNextNonDisabled(key: Key, getNext: (key: Key) => Key | null): Key | null { - while (key != null) { - let item = this.collection.getItem(key); + private findNextNonDisabled(key: Key | null, getNext: (key: Key) => Key | null): Key | null { + let nextKey = key; + while (nextKey != null) { + let item = this.collection.getItem(key!); if (item?.type === 'item' && !this.isDisabled(item)) { - return key; + return nextKey; } - key = getNext(key); + nextKey = getNext(nextKey); } return null; } getNextKey(key: Key) { - key = this.collection.getKeyAfter(key); - return this.findNextNonDisabled(key, key => this.collection.getKeyAfter(key)); + let nextKey: Key | null = key; + nextKey = this.collection.getKeyAfter(nextKey); + return this.findNextNonDisabled(nextKey, key => this.collection.getKeyAfter(key)); } getPreviousKey(key: Key) { - key = this.collection.getKeyBefore(key); - return this.findNextNonDisabled(key, key => this.collection.getKeyBefore(key)); + let nextKey: Key | null = key; + nextKey = this.collection.getKeyBefore(nextKey); + return this.findNextNonDisabled(nextKey, key => this.collection.getKeyBefore(key)); } private findKey( key: Key, - nextKey: (key: Key) => Key, + nextKey: (key: Key) => Key | null, shouldSkip: (prevRect: Rect, itemRect: Rect) => boolean ) { - let itemRect = this.layoutDelegate.getItemRect(key); - if (!itemRect) { + let tempKey: Key | null = key; + let itemRect = this.layoutDelegate.getItemRect(tempKey); + if (!itemRect || tempKey == null) { return null; } // Find the item above or below in the same column. let prevRect = itemRect; do { - key = nextKey(key); - itemRect = this.layoutDelegate.getItemRect(key); - } while (itemRect && shouldSkip(prevRect, itemRect)); + tempKey = nextKey(tempKey); + if (tempKey == null) { + break; + } + itemRect = this.layoutDelegate.getItemRect(tempKey); + } while (itemRect && shouldSkip(prevRect, itemRect) && tempKey != null); - return key; + return tempKey; } private isSameRow(prevRect: Rect, itemRect: Rect) { @@ -145,7 +152,7 @@ export class ListKeyboardDelegate implements KeyboardDelegate { return right ? this.getPreviousKey(key) : this.getNextKey(key); } - getKeyRightOf(key: Key) { + getKeyRightOf?(key: Key) { // This is a temporary solution for CardView until we refactor useSelectableCollection. // https://github.com/orgs/adobe/projects/19/views/32?pane=issue&itemId=77825042 let layoutDelegateMethod = this.direction === 'ltr' ? 'getKeyRightOf' : 'getKeyLeftOf'; @@ -167,7 +174,7 @@ export class ListKeyboardDelegate implements KeyboardDelegate { return null; } - getKeyLeftOf(key: Key) { + getKeyLeftOf?(key: Key) { let layoutDelegateMethod = this.direction === 'ltr' ? 'getKeyLeftOf' : 'getKeyRightOf'; if (this.layoutDelegate[layoutDelegateMethod]) { key = this.layoutDelegate[layoutDelegateMethod](key); @@ -204,27 +211,28 @@ export class ListKeyboardDelegate implements KeyboardDelegate { return null; } - if (!isScrollable(menu)) { + if (menu && !isScrollable(menu)) { return this.getFirstKey(); } + let nextKey: Key | null = key; if (this.orientation === 'horizontal') { let pageX = Math.max(0, itemRect.x + itemRect.width - this.layoutDelegate.getVisibleRect().width); - while (itemRect && itemRect.x > pageX) { - key = this.getKeyAbove(key); - itemRect = key == null ? null : this.layoutDelegate.getItemRect(key); + while (itemRect && itemRect.x > pageX && nextKey != null) { + nextKey = this.getKeyAbove(nextKey); + itemRect = nextKey == null ? null : this.layoutDelegate.getItemRect(nextKey); } } else { let pageY = Math.max(0, itemRect.y + itemRect.height - this.layoutDelegate.getVisibleRect().height); - while (itemRect && itemRect.y > pageY) { - key = this.getKeyAbove(key); - itemRect = key == null ? null : this.layoutDelegate.getItemRect(key); + while (itemRect && itemRect.y > pageY && nextKey != null) { + nextKey = this.getKeyAbove(nextKey); + itemRect = nextKey == null ? null : this.layoutDelegate.getItemRect(nextKey); } } - return key ?? this.getFirstKey(); + return nextKey ?? this.getFirstKey(); } getKeyPageBelow(key: Key) { @@ -234,27 +242,28 @@ export class ListKeyboardDelegate implements KeyboardDelegate { return null; } - if (!isScrollable(menu)) { + if (menu && !isScrollable(menu)) { return this.getLastKey(); } + let nextKey: Key | null = key; if (this.orientation === 'horizontal') { let pageX = Math.min(this.layoutDelegate.getContentSize().width, itemRect.y - itemRect.width + this.layoutDelegate.getVisibleRect().width); - while (itemRect && itemRect.x < pageX) { - key = this.getKeyBelow(key); - itemRect = key == null ? null : this.layoutDelegate.getItemRect(key); + while (itemRect && itemRect.x < pageX && nextKey != null) { + nextKey = this.getKeyBelow(nextKey); + itemRect = nextKey == null ? null : this.layoutDelegate.getItemRect(nextKey); } } else { let pageY = Math.min(this.layoutDelegate.getContentSize().height, itemRect.y - itemRect.height + this.layoutDelegate.getVisibleRect().height); - while (itemRect && itemRect.y < pageY) { - key = this.getKeyBelow(key); - itemRect = key == null ? null : this.layoutDelegate.getItemRect(key); + while (itemRect && itemRect.y < pageY && nextKey != null) { + nextKey = this.getKeyBelow(nextKey); + itemRect = nextKey == null ? null : this.layoutDelegate.getItemRect(nextKey); } } - return key ?? this.getLastKey(); + return nextKey ?? this.getLastKey(); } getKeyForSearch(search: string, fromKey?: Key) { @@ -266,6 +275,9 @@ export class ListKeyboardDelegate implements KeyboardDelegate { let key = fromKey || this.getFirstKey(); while (key != null) { let item = collection.getItem(key); + if (!item) { + return null; + } let substring = item.textValue.slice(0, search.length); if (item.textValue && this.collator.compare(substring, search) === 0) { return key; diff --git a/packages/@react-aria/selection/src/useSelectableCollection.ts b/packages/@react-aria/selection/src/useSelectableCollection.ts index c136d127ed7..6ac3f2bbe7c 100644 --- a/packages/@react-aria/selection/src/useSelectableCollection.ts +++ b/packages/@react-aria/selection/src/useSelectableCollection.ts @@ -128,7 +128,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions // Keyboard events bubble through portals. Don't handle keyboard events // for elements outside the collection (e.g. menus). - if (!ref.current.contains(e.target as Element)) { + if (!ref.current?.contains(e.target as Element)) { return; } @@ -140,9 +140,11 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions manager.setFocusedKey(key, childFocus); }); - let item = scrollRef.current.querySelector(`[data-key="${CSS.escape(key.toString())}"]`); + let item = scrollRef.current?.querySelector(`[data-key="${CSS.escape(key.toString())}"]`); let itemProps = manager.getItemProps(key); - router.open(item, e, itemProps.href, itemProps.routerOptions); + if (item) { + router.open(item, e, itemProps.href, itemProps.routerOptions); + } return; } @@ -194,7 +196,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions } case 'ArrowLeft': { if (delegate.getKeyLeftOf) { - let nextKey = delegate.getKeyLeftOf?.(manager.focusedKey); + let nextKey: Key | undefined | null = delegate.getKeyLeftOf?.(manager.focusedKey); if (nextKey == null && shouldFocusWrap) { nextKey = direction === 'rtl' ? delegate.getFirstKey?.(manager.focusedKey) : delegate.getLastKey?.(manager.focusedKey); } @@ -207,7 +209,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions } case 'ArrowRight': { if (delegate.getKeyRightOf) { - let nextKey = delegate.getKeyRightOf?.(manager.focusedKey); + let nextKey: Key | undefined | null = delegate.getKeyRightOf?.(manager.focusedKey); if (nextKey == null && shouldFocusWrap) { nextKey = direction === 'rtl' ? delegate.getLastKey?.(manager.focusedKey) : delegate.getFirstKey?.(manager.focusedKey); } @@ -221,12 +223,14 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions case 'Home': if (delegate.getFirstKey) { e.preventDefault(); - let firstKey = delegate.getFirstKey(manager.focusedKey, isCtrlKeyPressed(e)); + let firstKey: Key | null = delegate.getFirstKey(manager.focusedKey, isCtrlKeyPressed(e)); manager.setFocusedKey(firstKey); - if (isCtrlKeyPressed(e) && e.shiftKey && manager.selectionMode === 'multiple') { - manager.extendSelection(firstKey); - } else if (selectOnFocus) { - manager.replaceSelection(firstKey); + if (firstKey != null) { + if (isCtrlKeyPressed(e) && e.shiftKey && manager.selectionMode === 'multiple') { + manager.extendSelection(firstKey); + } else if (selectOnFocus) { + manager.replaceSelection(firstKey); + } } } break; @@ -235,10 +239,12 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions e.preventDefault(); let lastKey = delegate.getLastKey(manager.focusedKey, isCtrlKeyPressed(e)); manager.setFocusedKey(lastKey); - if (isCtrlKeyPressed(e) && e.shiftKey && manager.selectionMode === 'multiple') { - manager.extendSelection(lastKey); - } else if (selectOnFocus) { - manager.replaceSelection(lastKey); + if (lastKey != null) { + if (isCtrlKeyPressed(e) && e.shiftKey && manager.selectionMode === 'multiple') { + manager.extendSelection(lastKey); + } else if (selectOnFocus) { + manager.replaceSelection(lastKey); + } } } break; @@ -285,7 +291,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions ref.current.focus(); } else { let walker = getFocusableTreeWalker(ref.current, {tabbable: true}); - let next: FocusableElement; + let next: FocusableElement | undefined = undefined; let last: FocusableElement; do { last = walker.lastChild() as FocusableElement; @@ -307,10 +313,10 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions // Store the scroll position so we can restore it later. /// TODO: should this happen all the time?? let scrollPos = useRef({top: 0, left: 0}); - useEvent(scrollRef, 'scroll', isVirtualized ? null : () => { + useEvent(scrollRef, 'scroll', isVirtualized ? undefined : () => { scrollPos.current = { - top: scrollRef.current.scrollTop, - left: scrollRef.current.scrollLeft + top: scrollRef.current?.scrollTop ?? 0, + left: scrollRef.current?.scrollLeft ?? 0 }; }); @@ -345,17 +351,17 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions // and either focus the first or last item accordingly. let relatedTarget = e.relatedTarget as Element; if (relatedTarget && (e.currentTarget.compareDocumentPosition(relatedTarget) & Node.DOCUMENT_POSITION_FOLLOWING)) { - navigateToFirstKey(manager.lastSelectedKey ?? delegate.getLastKey()); + navigateToFirstKey(manager.lastSelectedKey ?? delegate.getLastKey?.() ?? undefined); } else { - navigateToFirstKey(manager.firstSelectedKey ?? delegate.getFirstKey()); + navigateToFirstKey(manager.firstSelectedKey ?? delegate.getFirstKey?.() ?? undefined); } - } else if (!isVirtualized) { + } else if (!isVirtualized && scrollRef.current) { // Restore the scroll position to what it was before. scrollRef.current.scrollTop = scrollPos.current.top; scrollRef.current.scrollLeft = scrollPos.current.left; } - if (manager.focusedKey != null) { + if (manager.focusedKey != null && scrollRef.current) { // Refocus and scroll the focused item into view if it exists within the scrollable region. let element = scrollRef.current.querySelector(`[data-key="${CSS.escape(manager.focusedKey.toString())}"]`) as HTMLElement; if (element) { @@ -366,7 +372,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions let modality = getInteractionModality(); if (modality === 'keyboard') { - scrollIntoViewport(element, {containingElement: ref.current}); + scrollIntoViewport(element, {containingElement: ref.current ?? undefined}); } } } @@ -382,13 +388,13 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions const autoFocusRef = useRef(autoFocus); useEffect(() => { if (autoFocusRef.current) { - let focusedKey = null; + let focusedKey: Key | null = null; // Check focus strategy to determine which item to focus if (autoFocus === 'first') { - focusedKey = delegate.getFirstKey(); + focusedKey = delegate.getFirstKey?.() ?? null; } if (autoFocus === 'last') { - focusedKey = delegate.getLastKey(); + focusedKey = delegate.getLastKey?.() ?? null; } // If there are any selected keys, make the first one the new focus target @@ -406,7 +412,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions manager.setFocusedKey(focusedKey); // If no default focus key is selected, focus the collection itself. - if (focusedKey == null && !shouldUseVirtualFocus) { + if (focusedKey == null && !shouldUseVirtualFocus && ref.current) { focusSafely(ref.current); } } @@ -416,7 +422,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions // Scroll the focused element into view when the focusedKey changes. let lastFocusedKey = useRef(manager.focusedKey); useEffect(() => { - if (manager.isFocused && manager.focusedKey != null && (manager.focusedKey !== lastFocusedKey.current || autoFocusRef.current) && scrollRef?.current) { + if (manager.isFocused && manager.focusedKey != null && (manager.focusedKey !== lastFocusedKey.current || autoFocusRef.current) && scrollRef.current && ref.current) { let modality = getInteractionModality(); let element = ref.current.querySelector(`[data-key="${CSS.escape(manager.focusedKey.toString())}"]`) as HTMLElement; if (!element) { @@ -436,7 +442,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions } // If the focused key becomes null (e.g. the last item is deleted), focus the whole collection. - if (!shouldUseVirtualFocus && manager.isFocused && manager.focusedKey == null && lastFocusedKey.current != null) { + if (!shouldUseVirtualFocus && manager.isFocused && manager.focusedKey == null && lastFocusedKey.current != null && ref.current) { focusSafely(ref.current); } @@ -476,7 +482,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions // This will be marshalled to either the first or last item depending on where focus came from. // If using virtual focus, don't set a tabIndex at all so that VoiceOver on iOS 14 doesn't try // to move real DOM focus to the element anyway. - let tabIndex: number; + let tabIndex: number | undefined = undefined; if (!shouldUseVirtualFocus) { tabIndex = manager.focusedKey == null ? 0 : -1; } diff --git a/packages/@react-aria/selection/src/useSelectableItem.ts b/packages/@react-aria/selection/src/useSelectableItem.ts index a9e73b9ea34..3a3d80c731a 100644 --- a/packages/@react-aria/selection/src/useSelectableItem.ts +++ b/packages/@react-aria/selection/src/useSelectableItem.ts @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import {DOMAttributes, FocusableElement, Key, LongPressEvent, PressEvent, RefObject} from '@react-types/shared'; +import {DOMAttributes, FocusableElement, Key, LongPressEvent, PointerType, PressEvent, RefObject} from '@react-types/shared'; import {focusSafely} from '@react-aria/focus'; import {isCtrlKeyPressed, isNonContiguousSelectionModifier} from './utils'; import {mergeProps, openLink, useRouter} from '@react-aria/utils'; @@ -130,7 +130,7 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte } if (manager.isLink(key)) { - if (linkBehavior === 'selection') { + if (linkBehavior === 'selection' && ref.current) { let itemProps = manager.getItemProps(key); router.open(ref.current, e, itemProps.href, itemProps.routerOptions); // Always set selected keys back to what they were so that select and combobox close. @@ -164,7 +164,7 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte if (isFocused && manager.isFocused && !shouldUseVirtualFocus) { if (focus) { focus(); - } else if (document.activeElement !== ref.current) { + } else if (document.activeElement !== ref.current && ref.current) { focusSafely(ref.current); } } @@ -207,7 +207,7 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte ); let hasSecondaryAction = allowsActions && allowsSelection && manager.selectionBehavior === 'replace'; let hasAction = hasPrimaryAction || hasSecondaryAction; - let modality = useRef(null); + let modality = useRef(null); let longPressEnabled = hasAction && allowsSelection; let longPressEnabledOnPressStart = useRef(false); @@ -218,7 +218,7 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte onAction(); } - if (hasLinkAction) { + if (hasLinkAction && ref.current) { let itemProps = manager.getItemProps(key); router.open(ref.current, e, itemProps.href, itemProps.routerOptions); } @@ -256,13 +256,13 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte } }; } else { - itemPressProps.onPressUp = hasPrimaryAction ? null : (e) => { + itemPressProps.onPressUp = hasPrimaryAction ? undefined : (e) => { if (e.pointerType !== 'keyboard' && allowsSelection) { onSelect(e); } }; - itemPressProps.onPress = hasPrimaryAction ? performAction : null; + itemPressProps.onPress = hasPrimaryAction ? performAction : undefined; } } else { itemPressProps.onPressStart = (e) => { diff --git a/packages/@react-aria/selection/src/useTypeSelect.ts b/packages/@react-aria/selection/src/useTypeSelect.ts index c1814c470d0..01cea26fc57 100644 --- a/packages/@react-aria/selection/src/useTypeSelect.ts +++ b/packages/@react-aria/selection/src/useTypeSelect.ts @@ -46,9 +46,9 @@ export interface TypeSelectAria { */ export function useTypeSelect(options: AriaTypeSelectOptions): TypeSelectAria { let {keyboardDelegate, selectionManager, onTypeSelect} = options; - let state = useRef({ + let state = useRef<{search: string, timeout: ReturnType | undefined}>({ search: '', - timeout: null + timeout: undefined }).current; let onKeyDown = (e: KeyboardEvent) => { @@ -70,19 +70,21 @@ export function useTypeSelect(options: AriaTypeSelectOptions): TypeSelectAria { state.search += character; - // Use the delegate to find a key to focus. - // Prioritize items after the currently focused item, falling back to searching the whole list. - let key = keyboardDelegate.getKeyForSearch(state.search, selectionManager.focusedKey); + if (keyboardDelegate.getKeyForSearch != null) { + // Use the delegate to find a key to focus. + // Prioritize items after the currently focused item, falling back to searching the whole list. + let key = keyboardDelegate.getKeyForSearch(state.search, selectionManager.focusedKey); - // If no key found, search from the top. - if (key == null) { - key = keyboardDelegate.getKeyForSearch(state.search); - } + // If no key found, search from the top. + if (key == null) { + key = keyboardDelegate.getKeyForSearch(state.search); + } - if (key != null) { - selectionManager.setFocusedKey(key); - if (onTypeSelect) { - onTypeSelect(key); + if (key != null) { + selectionManager.setFocusedKey(key); + if (onTypeSelect) { + onTypeSelect(key); + } } } @@ -96,7 +98,7 @@ export function useTypeSelect(options: AriaTypeSelectOptions): TypeSelectAria { typeSelectProps: { // Using a capturing listener to catch the keydown event before // other hooks in order to handle the Spacebar event. - onKeyDownCapture: keyboardDelegate.getKeyForSearch ? onKeyDown : null + onKeyDownCapture: keyboardDelegate.getKeyForSearch ? onKeyDown : undefined } }; } diff --git a/tsconfig.json b/tsconfig.json index 6d7b7c33d7d..86520a65b66 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -33,126 +33,7 @@ "dom.iterable" ], "skipLibCheck": false, - "strict": false, - "plugins": [ - { - "name": "@react-spectrum/ts-plugin" - }, - { - "name": "typescript-strict-plugin", - "paths": [ - "./packages/@internationalized", - "./packages/@react-aria/a", - "./packages/@react-aria/b", - "./packages/@react-aria/c", - "./packages/@react-aria/d", - "./packages/@react-aria/l", - "./packages/@react-aria/disclosure", - "./packages/@react-aria/e", - "./packages/@react-aria/f", - "./packages/@react-aria/h", - "./packages/@react-aria/i", - "./packages/@react-aria/j", - "./packages/@react-aria/k", - "./packages/@react-aria/l", - "./packages/@react-aria/meter", - "./packages/@react-aria/numberfield", - "./packages/@react-aria/progress", - "./packages/@react-aria/q", - "./packages/@react-aria/radio", - "./packages/@react-aria/searchfield", - "./packages/@react-aria/separator", - "./packages/@react-aria/sidenav", - "./packages/@react-aria/spinbutton", - "./packages/@react-aria/ssr", - "./packages/@react-aria/steplist", - "./packages/@react-aria/switch", - "./packages/@react-aria/tabs", - "./packages/@react-aria/tag", - "./packages/@react-aria/test-utils", - "./packages/@react-aria/toggle", - "./packages/@react-aria/u", - "./packages/@react-aria/visually-hidden", - "./packages/@react-aria/w", - "./packages/@react-aria/x", - "./packages/@react-aria/y", - "./packages/@react-aria/z", - "./packages/@react-spectrum/a", - "./packages/@react-spectrum/b", - "./packages/@react-spectrum/calendar", - "./packages/@react-spectrum/color", - "./packages/@react-spectrum/contextualhelp", - "./packages/@react-spectrum/checkbox", - "./packages/@react-spectrum/dialog", - "./packages/@react-spectrum/divider", - "./packages/@react-spectrum/dropzone", - "./packages/@react-spectrum/form", - "./packages/@react-spectrum/icon", - "./packages/@react-spectrum/illustratedmessage", - "./packages/@react-spectrum/inlinealert", - "./packages/@react-spectrum/image", - "./packages/@react-spectrum/label", - "./packages/@react-spectrum/link", - "./packages/@react-spectrum/meter", - "./packages/@react-spectrum/numberfield", - "./packages/@react-spectrum/progress", - "./packages/@react-spectrum/radio", - "./packages/@react-spectrum/s2", - "./packages/@react-spectrum/searchfield", - "./packages/@react-spectrum/searchwithin", - "./packages/@react-spectrum/statuslight", - "./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", - "./packages/@react-spectrum/theme-dark", - "./packages/@react-spectrum/theme-default", - "./packages/@react-spectrum/theme-light", - "./packages/@react-spectrum/codemods", - "./packages/@react-spectrum/view", - "./packages/@react-spectrum/well", - "./packages/@react-stately/calendar", - "./packages/@react-stately/checkbox", - "./packages/@react-stately/color", - "./packages/@react-stately/combobox", - "./packages/@react-stately/disclosure", - "./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/toggle", - "./packages/@react-stately/utils", - "./packages/@react-types/a", - "./packages/@react-types/b", - "./packages/@react-types/checkbox", - "./packages/@react-types/color", - "./packages/@react-types/dialog", - "./packages/@react-types/divider", - "./packages/@react-types/illustratedmessage", - "./packages/@react-types/image", - "./packages/@react-types/l", - "./packages/@react-types/meter", - "./packages/@react-types/numberfield", - "./packages/@react-types/progress", - "./packages/@react-types/searchfield", - "./packages/@react-types/shared", - "./packages/@react-types/statuslight", - "./packages/@react-types/tabs", - "./packages/@react-types/text", - "./packages/@react-types/tooltip", - "./packages/@react-types/view", - "./packages/@react-types/well", - "./packages/@spectrum-icons", - "./packages/react-aria-components" - ] - } - ], + "strict": true, "paths": { "tailwind-starter/*": ["./starters/tailwind/src/*"] } From a93d83248df12765327226e4608c5a7d7ac9ae96 Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 14 Nov 2024 07:37:21 +1100 Subject: [PATCH 04/66] GridKeyboardDelegate and review --- .../grid/src/GridKeyboardDelegate.ts | 173 ++++++++++++------ .../overlays/src/ariaHideOutside.ts | 5 +- .../overlays/src/calculatePosition.ts | 61 +++--- .../@react-aria/overlays/src/useModal.tsx | 2 +- .../overlays/src/useOverlayPosition.ts | 21 +-- .../@react-aria/overlays/src/usePopover.ts | 2 +- .../overlays/src/usePreventScroll.ts | 5 +- .../selection/src/useSelectableCollection.ts | 8 +- .../@react-aria/utils/src/scrollIntoView.ts | 2 +- .../collections/src/getChildNodes.ts | 2 +- .../@react-types/shared/src/collections.d.ts | 2 +- 11 files changed, 172 insertions(+), 111 deletions(-) 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/overlays/src/ariaHideOutside.ts b/packages/@react-aria/overlays/src/ariaHideOutside.ts index ee7f1a4feb1..31be6884dda 100644 --- a/packages/@react-aria/overlays/src/ariaHideOutside.ts +++ b/packages/@react-aria/overlays/src/ariaHideOutside.ts @@ -152,7 +152,10 @@ export function ariaHideOutside(targets: Element[], root = document.body) { observer.disconnect(); for (let node of hiddenNodes) { - let count = refCountMap.get(node) ?? 0; + 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 e45921a8e9f..cc9bdc052f9 100644 --- a/packages/@react-aria/overlays/src/calculatePosition.ts +++ b/packages/@react-aria/overlays/src/calculatePosition.ts @@ -14,10 +14,10 @@ import {Axis, Placement, PlacementAxis, SizeAxis} from '@react-types/overlays'; import {clamp, isWebKit} from '@react-aria/utils'; interface Position { - top?: number, - left?: number, - bottom?: number, - right?: number + top: number, + left: number, + bottom: number, + right: number } interface Dimensions { @@ -64,10 +64,10 @@ interface PositionOpts { type HeightGrowthDirection = 'top' | 'bottom'; export interface PositionResult { - position?: Position, - arrowOffsetLeft?: number, - arrowOffsetTop?: number, - maxHeight?: number, + position: Position, + arrowOffsetLeft: number, + arrowOffsetTop: number, + maxHeight: number, placement: PlacementAxis } @@ -102,12 +102,11 @@ const TOTAL_SIZE = { const PARSED_PLACEMENT_CACHE = {}; -// @ts-ignore 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 scroll: Position = {left: 0, top: 0, right: 0, bottom: 0}; let isPinchZoomedIn = (visualViewport?.scale ?? 1) > 1; if (containerNode.tagName === 'BODY') { @@ -174,7 +173,7 @@ function getDelta( padding: number, containerOffsetWithBoundary: Offset ) { - let containerScroll = containerDimensions.scroll[axis] ?? 0; + let containerScroll = containerDimensions.scroll[axis]; // 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. @@ -237,29 +236,29 @@ function computePosition( arrowBoundaryOffset: number ) { let {placement, crossPlacement, axis, crossAxis, size, crossSize} = placementInfo; - let position: Position = {}; + let position: Position = {left: 0, top: 0, right: 0, bottom: 0}; // button position - position[crossAxis] = childOffset[crossAxis] ?? 0; + position[crossAxis] = childOffset[crossAxis]; if (crossPlacement === 'center') { // + (button size / 2) - (overlay size / 2) // at this point the overlay center should match the button center - position[crossAxis]! += ((childOffset[crossSize] ?? 0) - (overlaySize[crossSize] ?? 0)) / 2; + position[crossAxis]! += ((childOffset[crossSize]) - (overlaySize[crossSize])) / 2; } else if (crossPlacement !== crossAxis) { // + (button size) - (overlay size) // at this point the overlay bottom should match the button bottom - position[crossAxis]! += (childOffset[crossSize] ?? 0) - (overlaySize[crossSize] ?? 0); + position[crossAxis]! += (childOffset[crossSize]) - (overlaySize[crossSize]); }/* 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 ?? 0) - overlayHeight); + let overlayTop = position.top != null ? containerOffsetWithBoundary.top + position.top : containerOffsetWithBoundary.top + (containerHeight - position.bottom - 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 ?? 0)) // this is the bottom of the boundary + (boundaryDimensions.height + boundaryDimensions.top + boundaryDimensions.scroll.top) // this is the bottom of the boundary - overlayTop // this is the top of the overlay - - ((margins.top ?? 0) + (margins.bottom ?? 0) + padding) // save additional space for margin and padding + - (margins.top + margins.bottom + 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 ?? 0)) // this is the top of the boundary - - ((margins.top ?? 0) + (margins.bottom ?? 0) + padding) // save additional space for margin and padding + - (boundaryDimensions.top + (boundaryDimensions.scroll.top)) // this is the top of the boundary + - (margins.top + margins.bottom + 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] ?? 0) + containerOffsetWithBoundary[axis] - (margins[axis] ?? 0) - margins[FLIPPED_DIRECTION[axis]] - padding); + return Math.max(0, childOffset[axis] - boundaryDimensions[axis] - boundaryDimensions.scroll[axis] + containerOffsetWithBoundary[axis] - 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); + return Math.max(0, boundaryDimensions[size] + boundaryDimensions[axis] + boundaryDimensions.scroll[axis] - containerOffsetWithBoundary[axis] - childOffset[axis] - childOffset[size] - margins[axis] - 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, @@ -413,19 +412,19 @@ export function calculatePositionInternal( delta = getDelta(crossAxis, position[crossAxis]!, overlaySize[crossSize], boundaryDimensions, containerDimensions, padding, containerOffsetWithBoundary); position[crossAxis]! += delta; - let arrowPosition: Position = {}; + let arrowPosition: Position = {left: 0, top: 0, right: 0, bottom: 0}; // All values are transformed so that 0 is at the top/left of the overlay depending on the orientation // Prefer the arrow being in the center of the trigger/overlay anchor element // 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 ?? 0) + (margins.right ?? 0) : (margins.top ?? 0) + (margins.bottom ?? 0); + const overlayMargin = AXIS[crossAxis] === 'left' ? margins.left + margins.right : margins.top + margins.bottom; 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 ?? 0) + (margins.right ?? 0); - overlaySize.height += (margins.top ?? 0) + (margins.bottom ?? 0); + overlaySize.width += margins.left + margins.right; + overlaySize.height += margins.top + margins.bottom; let scrollSize = getScroll(scrollNode); let boundaryDimensions = getContainerDimensions(boundaryElement); diff --git a/packages/@react-aria/overlays/src/useModal.tsx b/packages/@react-aria/overlays/src/useModal.tsx index a3f028fc917..a51c5ce2a5a 100644 --- a/packages/@react-aria/overlays/src/useModal.tsx +++ b/packages/@react-aria/overlays/src/useModal.tsx @@ -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): ReactNode { +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/useOverlayPosition.ts b/packages/@react-aria/overlays/src/useOverlayPosition.ts index 58d0207ea0e..bb1ad72f5e6 100644 --- a/packages/@react-aria/overlays/src/useOverlayPosition.ts +++ b/packages/@react-aria/overlays/src/useOverlayPosition.ts @@ -67,7 +67,7 @@ export interface PositionAria { /** Props for the overlay tip arrow if any. */ arrowProps: DOMAttributes, /** Placement of the overlay with respect to the overlay trigger. */ - placement?: PlacementAxis, + placement: PlacementAxis | null, /** Updates the position of the overlay. */ updatePosition(): void } @@ -77,7 +77,6 @@ interface ScrollAnchor { offset: number } -// @ts-ignore let visualViewport = typeof document !== 'undefined' ? window.visualViewport : null; /** @@ -103,13 +102,7 @@ export function useOverlayPosition(props: AriaPositionProps): PositionAria { maxHeight, arrowBoundaryOffset = 0 } = props; - let [position, setPosition] = useState>({ - position: {}, - arrowOffsetLeft: undefined, - arrowOffsetTop: undefined, - maxHeight: undefined, - placement: undefined - }); + let [position, setPosition] = useState(null); let deps = [ shouldUpdatePosition, @@ -289,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/usePopover.ts b/packages/@react-aria/overlays/src/usePopover.ts index 361b1cd11bf..759576ad4ef 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 } /** diff --git a/packages/@react-aria/overlays/src/usePreventScroll.ts b/packages/@react-aria/overlays/src/usePreventScroll.ts index 21ed52ef8db..b67418a7661 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. @@ -254,9 +253,9 @@ 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 ) { // @ts-ignore diff --git a/packages/@react-aria/selection/src/useSelectableCollection.ts b/packages/@react-aria/selection/src/useSelectableCollection.ts index 6ac3f2bbe7c..8a47782caca 100644 --- a/packages/@react-aria/selection/src/useSelectableCollection.ts +++ b/packages/@react-aria/selection/src/useSelectableCollection.ts @@ -338,7 +338,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions manager.setFocused(true); if (manager.focusedKey == null) { - let navigateToFirstKey = (key: Key | undefined) => { + let navigateToFirstKey = (key: Key | undefined | null) => { if (key != null) { manager.setFocusedKey(key); if (selectOnFocus) { @@ -351,9 +351,9 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions // and either focus the first or last item accordingly. let relatedTarget = e.relatedTarget as Element; if (relatedTarget && (e.currentTarget.compareDocumentPosition(relatedTarget) & Node.DOCUMENT_POSITION_FOLLOWING)) { - navigateToFirstKey(manager.lastSelectedKey ?? delegate.getLastKey?.() ?? undefined); + navigateToFirstKey(manager.lastSelectedKey ?? delegate.getLastKey?.()); } else { - navigateToFirstKey(manager.firstSelectedKey ?? delegate.getFirstKey?.() ?? undefined); + navigateToFirstKey(manager.firstSelectedKey ?? delegate.getFirstKey?.()); } } else if (!isVirtualized && scrollRef.current) { // Restore the scroll position to what it was before. @@ -372,7 +372,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions let modality = getInteractionModality(); if (modality === 'keyboard') { - scrollIntoViewport(element, {containingElement: ref.current ?? undefined}); + scrollIntoViewport(element, {containingElement: ref.current}); } } } diff --git a/packages/@react-aria/utils/src/scrollIntoView.ts b/packages/@react-aria/utils/src/scrollIntoView.ts index 43e1c61b60d..cafcdf08a02 100644 --- a/packages/@react-aria/utils/src/scrollIntoView.ts +++ b/packages/@react-aria/utils/src/scrollIntoView.ts @@ -14,7 +14,7 @@ import {getScrollParents} from './getScrollParents'; interface ScrollIntoViewportOpts { /** The optional containing element of the target to be centered in the viewport. */ - containingElement?: Element + containingElement?: Element | null } /** diff --git a/packages/@react-stately/collections/src/getChildNodes.ts b/packages/@react-stately/collections/src/getChildNodes.ts index 8692c2030b0..2d914e284ee 100644 --- a/packages/@react-stately/collections/src/getChildNodes.ts +++ b/packages/@react-stately/collections/src/getChildNodes.ts @@ -42,7 +42,7 @@ export function getNthItem(iterable: Iterable, index: number): T | undefin } export function getLastItem(iterable: Iterable): T | undefined { - let lastItem = undefined; + let lastItem: T | undefined = undefined; for (let value of iterable) { lastItem = value; } diff --git a/packages/@react-types/shared/src/collections.d.ts b/packages/@react-types/shared/src/collections.d.ts index 7c34092be7d..b0f10478c51 100644 --- a/packages/@react-types/shared/src/collections.d.ts +++ b/packages/@react-types/shared/src/collections.d.ts @@ -206,7 +206,7 @@ export interface Node { /** An accessibility label for this node. */ 'aria-label'?: string, /** The index of this node within its parent. */ - index?: number, + index: number, /** A function that should be called to wrap the rendered node. */ wrapper?: (element: ReactElement) => ReactElement, /** The key of the parent node. */ From 74da9e1a851e45c395398d34be0a01025a4d69bf Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 14 Nov 2024 08:04:30 +1100 Subject: [PATCH 05/66] fix some calculateposition tests --- packages/@react-aria/grid/src/useGrid.ts | 2 +- packages/@react-aria/grid/src/useGridCell.ts | 56 +++--- packages/@react-aria/grid/src/utils.ts | 4 +- .../overlays/src/calculatePosition.ts | 10 +- .../overlays/test/calculatePosition.test.ts | 170 +++++++++--------- 5 files changed, 122 insertions(+), 120 deletions(-) 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. @@ -69,27 +69,29 @@ export function useGridCell>(props: GridCellProps // 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) ); } 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/overlays/src/calculatePosition.ts b/packages/@react-aria/overlays/src/calculatePosition.ts index cc9bdc052f9..3dee463ed90 100644 --- a/packages/@react-aria/overlays/src/calculatePosition.ts +++ b/packages/@react-aria/overlays/src/calculatePosition.ts @@ -243,11 +243,11 @@ function computePosition( 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] - (overlaySize[crossSize])) / 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] - overlaySize[crossSize]); }/* else { the overlay top should match the button top } */ @@ -298,7 +298,7 @@ function getMaxHeight( // 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 + - (boundaryDimensions.top + boundaryDimensions.scroll.top) // this is the top of the boundary - (margins.top + margins.bottom + padding) // save additional space for margin and padding ); return Math.min(boundaryDimensions.height - (padding * 2), maxHeight); @@ -409,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 = {left: 0, top: 0, right: 0, bottom: 0}; diff --git a/packages/@react-aria/overlays/test/calculatePosition.test.ts b/packages/@react-aria/overlays/test/calculatePosition.test.ts index 747495b1314..1f6d127a654 100644 --- a/packages/@react-aria/overlays/test/calculatePosition.test.ts +++ b/packages/@react-aria/overlays/test/calculatePosition.test.ts @@ -104,7 +104,7 @@ describe('calculatePosition', function () { const placementCrossAxis = placementArray[1]; // The tests are all based on top/left positioning. Convert to bottom/right positioning if needed. - let pos: {right?: number, top?: number, left?: number, bottom?: number} = {}; + let pos: {right?: number, top?: number, left?: number, bottom?: number} = {top: 0, left: 0, right: 0, bottom: 0}; if ((placementAxis === 'left' && !flip) || (placementAxis === 'right' && flip)) { pos.right = boundaryDimensions.width - (expected[0] + overlaySize.width); pos.top = expected[1]; @@ -199,123 +199,123 @@ describe('calculatePosition', function () { const testCases = [ { placement: 'left', - noOffset: [50, 200, undefined, 100, 350], - offsetBefore: [-200, 50, undefined, 4, 500], - offsetAfter: [300, 350, undefined, 196, 200], - crossAxisOffsetPositive: [50, 210, undefined, 90, 340], - crossAxisOffsetNegative: [50, 190, undefined, 110, 360], - mainAxisOffset: [40, 200, undefined, 100, 350], - arrowBoundaryOffset: [50, 322, undefined, 24, 228] + noOffset: [50, 200, 0, 100, 350], + offsetBefore: [-200, 50, 0, 4, 500], + offsetAfter: [300, 350, 0, 196, 200], + crossAxisOffsetPositive: [50, 210, 0, 90, 340], + crossAxisOffsetNegative: [50, 190, 0, 110, 360], + mainAxisOffset: [40, 200, 0, 100, 350], + arrowBoundaryOffset: [50, 322, 0, 24, 228] }, { placement: 'left top', - noOffset: [50, 250, undefined, 50, 300], - offsetBefore: [-200, 50, undefined, 4, 500], - offsetAfter: [300, 350, undefined, 196, 200], - crossAxisOffsetPositive: [50, 260, undefined, 40, 290], - crossAxisOffsetNegative: [50, 240, undefined, 60, 310], - mainAxisOffset: [40, 250, undefined, 50, 300], - arrowBoundaryOffset: [50, 322, undefined, 24, 228] + noOffset: [50, 250, 0, 50, 300], + offsetBefore: [-200, 50, 0, 4, 500], + offsetAfter: [300, 350, 0, 196, 200], + crossAxisOffsetPositive: [50, 260, 0, 40, 290], + crossAxisOffsetNegative: [50, 240, 0, 60, 310], + mainAxisOffset: [40, 250, 0, 50, 300], + arrowBoundaryOffset: [50, 322, 0, 24, 228] }, { placement: 'left bottom', - noOffset: [50, 150, undefined, 150, 300], - offsetBefore: [-200, 50, undefined, 4, 200], - offsetAfter: [300, 350, undefined, 196, 500], - crossAxisOffsetPositive: [50, 160, undefined, 140, 310], - crossAxisOffsetNegative: [50, 140, undefined, 160, 290], - mainAxisOffset: [40, 150, undefined, 150, 300], - arrowBoundaryOffset: [50, 322, undefined, 24, 472] + noOffset: [50, 150, 0, 150, 300], + offsetBefore: [-200, 50, 0, 4, 200], + offsetAfter: [300, 350, 0, 196, 500], + crossAxisOffsetPositive: [50, 160, 0, 140, 310], + crossAxisOffsetNegative: [50, 140, 0, 160, 290], + mainAxisOffset: [40, 150, 0, 150, 300], + arrowBoundaryOffset: [50, 322, 0, 24, 472] }, { placement: 'top', - noOffset: [200, 50, 100, undefined, 200], - offsetBefore: [50, -200, 4, undefined, 0], - offsetAfter: [350, 300, 196, undefined, 450], - crossAxisOffsetPositive: [210, 50, 90, undefined, 200], - crossAxisOffsetNegative: [190, 50, 110, undefined, 200], - mainAxisOffset: [200, 40, 100, undefined, 190], - arrowBoundaryOffset: [322, 50, 24, undefined, 200] + noOffset: [200, 50, 100, 0, 200], + offsetBefore: [50, -200, 4, 0, 0], + offsetAfter: [350, 300, 196, 0, 450], + crossAxisOffsetPositive: [210, 50, 90, 0, 200], + crossAxisOffsetNegative: [190, 50, 110, 0, 200], + mainAxisOffset: [200, 40, 100, 0, 190], + arrowBoundaryOffset: [322, 50, 24, 0, 200] }, { placement: 'top left', - noOffset: [250, 50, 50, undefined, 200], - offsetBefore: [50, -200, 4, undefined, 0], - offsetAfter: [350, 300, 196, undefined, 450], - crossAxisOffsetPositive: [260, 50, 40, undefined, 200], - crossAxisOffsetNegative: [240, 50, 60, undefined, 200], - mainAxisOffset: [250, 40, 50, undefined, 190], - arrowBoundaryOffset: [322, 50, 24, undefined, 200] + noOffset: [250, 50, 50, 0, 200], + offsetBefore: [50, -200, 4, 0, 0], + offsetAfter: [350, 300, 196, 0, 450], + crossAxisOffsetPositive: [260, 50, 40, 0, 200], + crossAxisOffsetNegative: [240, 50, 60, 0, 200], + mainAxisOffset: [250, 40, 50, 0, 190], + arrowBoundaryOffset: [322, 50, 24, 0, 200] }, { placement: 'top right', - noOffset: [150, 50, 150, undefined, 200], - offsetBefore: [50, -200, 4, undefined, 0], - offsetAfter: [350, 300, 196, undefined, 450], - crossAxisOffsetPositive: [160, 50, 140, undefined, 200], - crossAxisOffsetNegative: [140, 50, 160, undefined, 200], - mainAxisOffset: [150, 40, 150, undefined, 190], - arrowBoundaryOffset: [322, 50, 24, undefined, 200] + noOffset: [150, 50, 150, 0, 200], + offsetBefore: [50, -200, 4, 0, 0], + offsetAfter: [350, 300, 196, 0, 450], + crossAxisOffsetPositive: [160, 50, 140, 0, 200], + crossAxisOffsetNegative: [140, 50, 160, 0, 200], + mainAxisOffset: [150, 40, 150, 0, 190], + arrowBoundaryOffset: [322, 50, 24, 0, 200] }, { placement: 'bottom', - noOffset: [200, 350, 100, undefined, 200], - offsetBefore: [50, 100, 4, undefined, 450], - offsetAfter: [350, 600, 196, undefined, 0], - crossAxisOffsetPositive: [210, 350, 90, undefined, 200], - crossAxisOffsetNegative: [190, 350, 110, undefined, 200], - mainAxisOffset: [200, 360, 100, undefined, 190], - arrowBoundaryOffset: [322, 350, 24, undefined, 200] + noOffset: [200, 350, 100, 0, 200], + offsetBefore: [50, 100, 4, 0, 450], + offsetAfter: [350, 600, 196, 0, 0], + crossAxisOffsetPositive: [210, 350, 90, 0, 200], + crossAxisOffsetNegative: [190, 350, 110, 0, 200], + mainAxisOffset: [200, 360, 100, 0, 190], + arrowBoundaryOffset: [322, 350, 24, 0, 200] }, { placement: 'bottom left', - noOffset: [250, 350, 50, undefined, 200], - offsetBefore: [50, 100, 4, undefined, 450], - offsetAfter: [350, 600, 196, undefined, 0], - crossAxisOffsetPositive: [260, 350, 40, undefined, 200], - crossAxisOffsetNegative: [240, 350, 60, undefined, 200], - mainAxisOffset: [250, 360, 50, undefined, 190], - arrowBoundaryOffset: [322, 350, 24, undefined, 200] + noOffset: [250, 350, 50, 0, 200], + offsetBefore: [50, 100, 4, 0, 450], + offsetAfter: [350, 600, 196, 0, 0], + crossAxisOffsetPositive: [260, 350, 40, 0, 200], + crossAxisOffsetNegative: [240, 350, 60, 0, 200], + mainAxisOffset: [250, 360, 50, 0, 190], + arrowBoundaryOffset: [322, 350, 24, 0, 200] }, { placement: 'bottom right', - noOffset: [150, 350, 150, undefined, 200], - offsetBefore: [50, 100, 4, undefined, 450], - offsetAfter: [350, 600, 196, undefined, 0], - crossAxisOffsetPositive: [160, 350, 140, undefined, 200], - crossAxisOffsetNegative: [140, 350, 160, undefined, 200], - mainAxisOffset: [150, 360, 150, undefined, 190], - arrowBoundaryOffset: [322, 350, 24, undefined, 200] + noOffset: [150, 350, 150, 0, 200], + offsetBefore: [50, 100, 4, 0, 450], + offsetAfter: [350, 600, 196, 0, 0], + crossAxisOffsetPositive: [160, 350, 140, 0, 200], + crossAxisOffsetNegative: [140, 350, 160, 0, 200], + mainAxisOffset: [150, 360, 150, 0, 190], + arrowBoundaryOffset: [322, 350, 24, 0, 200] }, { placement: 'right', - noOffset: [350, 200, undefined, 100, 350], - offsetBefore: [100, 50, undefined, 4, 500], - offsetAfter: [600, 350, undefined, 196, 200], - crossAxisOffsetPositive: [350, 210, undefined, 90, 340], - crossAxisOffsetNegative: [350, 190, undefined, 110, 360], - mainAxisOffset: [360, 200, undefined, 100, 350], - arrowBoundaryOffset: [350, 322, undefined, 24, 228] + noOffset: [350, 200, 0, 100, 350], + offsetBefore: [100, 50, 0, 4, 500], + offsetAfter: [600, 350, 0, 196, 200], + crossAxisOffsetPositive: [350, 210, 0, 90, 340], + crossAxisOffsetNegative: [350, 190, 0, 110, 360], + mainAxisOffset: [360, 200, 0, 100, 350], + arrowBoundaryOffset: [350, 322, 0, 24, 228] }, { placement: 'right top', - noOffset: [350, 250, undefined, 50, 300], - offsetBefore: [100, 50, undefined, 4, 500], - offsetAfter: [600, 350, undefined, 196, 200], - crossAxisOffsetPositive: [350, 260, undefined, 40, 290], - crossAxisOffsetNegative: [350, 240, undefined, 60, 310], - mainAxisOffset: [360, 250, undefined, 50, 300], - arrowBoundaryOffset: [350, 322, undefined, 24, 228] + noOffset: [350, 250, 0, 50, 300], + offsetBefore: [100, 50, 0, 4, 500], + offsetAfter: [600, 350, 0, 196, 200], + crossAxisOffsetPositive: [350, 260, 0, 40, 290], + crossAxisOffsetNegative: [350, 240, 0, 60, 310], + mainAxisOffset: [360, 250, 0, 50, 300], + arrowBoundaryOffset: [350, 322, 0, 24, 228] }, { placement: 'right bottom', - noOffset: [350, 150, undefined, 150, 300], - offsetBefore: [100, 50, undefined, 4, 200], - offsetAfter: [600, 350, undefined, 196, 500], - crossAxisOffsetPositive: [350, 160, undefined, 140, 310], - crossAxisOffsetNegative: [350, 140, undefined, 160, 290], - mainAxisOffset: [360, 150, undefined, 150, 300], - arrowBoundaryOffset: [350, 322, undefined, 24, 472] + noOffset: [350, 150, 0, 150, 300], + offsetBefore: [100, 50, 0, 4, 200], + offsetAfter: [600, 350, 0, 196, 500], + crossAxisOffsetPositive: [350, 160, 0, 140, 310], + crossAxisOffsetNegative: [350, 140, 0, 160, 290], + mainAxisOffset: [360, 150, 0, 150, 300], + arrowBoundaryOffset: [350, 322, 0, 24, 472] } ]; From d5cddec0ddaef3a7aa83253455e094fb0c9acac4 Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 14 Nov 2024 08:14:43 +1100 Subject: [PATCH 06/66] fix calculate tests --- .../overlays/src/calculatePosition.ts | 2 +- .../overlays/test/calculatePosition.test.ts | 46 +++++++++---------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/packages/@react-aria/overlays/src/calculatePosition.ts b/packages/@react-aria/overlays/src/calculatePosition.ts index 3dee463ed90..fd1952c46ff 100644 --- a/packages/@react-aria/overlays/src/calculatePosition.ts +++ b/packages/@react-aria/overlays/src/calculatePosition.ts @@ -243,7 +243,7 @@ function computePosition( 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] - overlaySize[crossSize]) / 2; } else if (crossPlacement !== crossAxis) { // + (button size) - (overlay size) // at this point the overlay bottom should match the button bottom diff --git a/packages/@react-aria/overlays/test/calculatePosition.test.ts b/packages/@react-aria/overlays/test/calculatePosition.test.ts index 1f6d127a654..7e46ed62c27 100644 --- a/packages/@react-aria/overlays/test/calculatePosition.test.ts +++ b/packages/@react-aria/overlays/test/calculatePosition.test.ts @@ -229,33 +229,33 @@ describe('calculatePosition', function () { }, { placement: 'top', - noOffset: [200, 50, 100, 0, 200], - offsetBefore: [50, -200, 4, 0, 0], - offsetAfter: [350, 300, 196, 0, 450], - crossAxisOffsetPositive: [210, 50, 90, 0, 200], - crossAxisOffsetNegative: [190, 50, 110, 0, 200], - mainAxisOffset: [200, 40, 100, 0, 190], - arrowBoundaryOffset: [322, 50, 24, 0, 200] + noOffset: [200, 50, 100, 0, 150], + offsetBefore: [50, -200, 4, 0, 150], + offsetAfter: [350, 300, 196, 0, 150], + crossAxisOffsetPositive: [210, 50, 90, 0, 150], + crossAxisOffsetNegative: [190, 50, 110, 0, 150], + mainAxisOffset: [200, 40, 100, 0, 150], + arrowBoundaryOffset: [322, 50, 24, 0, 150] }, { placement: 'top left', - noOffset: [250, 50, 50, 0, 200], - offsetBefore: [50, -200, 4, 0, 0], - offsetAfter: [350, 300, 196, 0, 450], - crossAxisOffsetPositive: [260, 50, 40, 0, 200], - crossAxisOffsetNegative: [240, 50, 60, 0, 200], - mainAxisOffset: [250, 40, 50, 0, 190], - arrowBoundaryOffset: [322, 50, 24, 0, 200] + noOffset: [250, 50, 50, 0, 150], + offsetBefore: [50, -200, 4, 0, 150], + offsetAfter: [350, 300, 196, 0, 150], + crossAxisOffsetPositive: [260, 50, 40, 0, 150], + crossAxisOffsetNegative: [240, 50, 60, 0, 150], + mainAxisOffset: [250, 40, 50, 0, 150], + arrowBoundaryOffset: [322, 50, 24, 0, 150] }, { placement: 'top right', - noOffset: [150, 50, 150, 0, 200], - offsetBefore: [50, -200, 4, 0, 0], - offsetAfter: [350, 300, 196, 0, 450], - crossAxisOffsetPositive: [160, 50, 140, 0, 200], - crossAxisOffsetNegative: [140, 50, 160, 0, 200], - mainAxisOffset: [150, 40, 150, 0, 190], - arrowBoundaryOffset: [322, 50, 24, 0, 200] + noOffset: [150, 50, 150, 0, 150], + offsetBefore: [50, -200, 4, 0, 150], + offsetAfter: [350, 300, 196, 0, 150], + crossAxisOffsetPositive: [160, 50, 140, 0, 150], + crossAxisOffsetNegative: [140, 50, 160, 0, 150], + mainAxisOffset: [150, 40, 150, 0, 150], + arrowBoundaryOffset: [322, 50, 24, 0, 150] }, { placement: 'bottom', @@ -381,14 +381,14 @@ describe('calculatePosition', function () { describe('positive offset', function () { checkPosition( - 'left', getTargetDimension({left: 0, top: 250}), [110, 200, undefined, 100, 350], 10, 0, true + 'left', getTargetDimension({left: 0, top: 250}), [110, 200, 0, 100, 350], 10, 0, true ); }); }); describe('overlay smaller than target aligns in center', function () { checkPosition( - 'right', getTargetDimension({left: 250, top: 250}, overlaySize.height + 100, overlaySize.width + 100), [550, 300, undefined, 100, 250] + 'right', getTargetDimension({left: 250, top: 250}, overlaySize.height + 100, overlaySize.width + 100), [550, 300, 0, 100, 250] ); }); From bdd0d56acd4e91d8ae8126a9b63bcf8aa63a4f38 Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 14 Nov 2024 08:23:02 +1100 Subject: [PATCH 07/66] fix listkeyboarddelegate --- packages/@react-aria/selection/src/ListKeyboardDelegate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@react-aria/selection/src/ListKeyboardDelegate.ts b/packages/@react-aria/selection/src/ListKeyboardDelegate.ts index 3c4827e8384..8037ee83dcd 100644 --- a/packages/@react-aria/selection/src/ListKeyboardDelegate.ts +++ b/packages/@react-aria/selection/src/ListKeyboardDelegate.ts @@ -77,7 +77,7 @@ export class ListKeyboardDelegate implements KeyboardDelegate { private findNextNonDisabled(key: Key | null, getNext: (key: Key) => Key | null): Key | null { let nextKey = key; while (nextKey != null) { - let item = this.collection.getItem(key!); + let item = this.collection.getItem(nextKey); if (item?.type === 'item' && !this.isDisabled(item)) { return nextKey; } From 7fd3794a20feb0e231ed8a83345ed31a8cc1e7ef Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 14 Nov 2024 08:49:16 +1100 Subject: [PATCH 08/66] fix more tests --- .../overlays/src/calculatePosition.ts | 2 +- .../overlays/test/calculatePosition.test.ts | 42 +++++++++---------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/@react-aria/overlays/src/calculatePosition.ts b/packages/@react-aria/overlays/src/calculatePosition.ts index fd1952c46ff..f8191286108 100644 --- a/packages/@react-aria/overlays/src/calculatePosition.ts +++ b/packages/@react-aria/overlays/src/calculatePosition.ts @@ -287,7 +287,7 @@ 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 = heightGrowthDirection !== 'top' ? containerOffsetWithBoundary.top + position.top : containerOffsetWithBoundary.top + (containerHeight - position.bottom - overlayHeight); let maxHeight = heightGrowthDirection !== 'top' ? // We want the distance between the top of the overlay to the bottom of the boundary Math.max(0, diff --git a/packages/@react-aria/overlays/test/calculatePosition.test.ts b/packages/@react-aria/overlays/test/calculatePosition.test.ts index 7e46ed62c27..98e2f678cba 100644 --- a/packages/@react-aria/overlays/test/calculatePosition.test.ts +++ b/packages/@react-aria/overlays/test/calculatePosition.test.ts @@ -229,33 +229,33 @@ describe('calculatePosition', function () { }, { placement: 'top', - noOffset: [200, 50, 100, 0, 150], - offsetBefore: [50, -200, 4, 0, 150], - offsetAfter: [350, 300, 196, 0, 150], - crossAxisOffsetPositive: [210, 50, 90, 0, 150], - crossAxisOffsetNegative: [190, 50, 110, 0, 150], - mainAxisOffset: [200, 40, 100, 0, 150], - arrowBoundaryOffset: [322, 50, 24, 0, 150] + noOffset: [200, 50, 100, 0, 200], + offsetBefore: [50, -200, 4, 0, 0], + offsetAfter: [350, 300, 196, 0, 450], + crossAxisOffsetPositive: [210, 50, 90, 0, 200], + crossAxisOffsetNegative: [190, 50, 110, 0, 200], + mainAxisOffset: [200, 40, 100, 0, 190], + arrowBoundaryOffset: [322, 50, 24, 0, 200] }, { placement: 'top left', - noOffset: [250, 50, 50, 0, 150], - offsetBefore: [50, -200, 4, 0, 150], - offsetAfter: [350, 300, 196, 0, 150], - crossAxisOffsetPositive: [260, 50, 40, 0, 150], - crossAxisOffsetNegative: [240, 50, 60, 0, 150], - mainAxisOffset: [250, 40, 50, 0, 150], - arrowBoundaryOffset: [322, 50, 24, 0, 150] + noOffset: [250, 50, 50, 0, 200], + offsetBefore: [50, -200, 4, 0, 0], + offsetAfter: [350, 300, 196, 0, 450], + crossAxisOffsetPositive: [260, 50, 40, 0, 200], + crossAxisOffsetNegative: [240, 50, 60, 0, 200], + mainAxisOffset: [250, 40, 50, 0, 190], + arrowBoundaryOffset: [322, 50, 24, 0, 200] }, { placement: 'top right', - noOffset: [150, 50, 150, 0, 150], - offsetBefore: [50, -200, 4, 0, 150], - offsetAfter: [350, 300, 196, 0, 150], - crossAxisOffsetPositive: [160, 50, 140, 0, 150], - crossAxisOffsetNegative: [140, 50, 160, 0, 150], - mainAxisOffset: [150, 40, 150, 0, 150], - arrowBoundaryOffset: [322, 50, 24, 0, 150] + noOffset: [150, 50, 150, 0, 200], + offsetBefore: [50, -200, 4, 0, 0], + offsetAfter: [350, 300, 196, 0, 450], + crossAxisOffsetPositive: [160, 50, 140, 0, 200], + crossAxisOffsetNegative: [140, 50, 160, 0, 200], + mainAxisOffset: [150, 40, 150, 0, 190], + arrowBoundaryOffset: [322, 50, 24, 0, 200] }, { placement: 'bottom', From d01db089f5ab98649cc4c8b2847427e8bd88bd5a Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 14 Nov 2024 10:41:57 +1100 Subject: [PATCH 09/66] fix calculate position --- .../overlays/src/calculatePosition.ts | 65 +++---- .../overlays/test/calculatePosition.test.ts | 174 +++++++++--------- 2 files changed, 120 insertions(+), 119 deletions(-) diff --git a/packages/@react-aria/overlays/src/calculatePosition.ts b/packages/@react-aria/overlays/src/calculatePosition.ts index f8191286108..e45921a8e9f 100644 --- a/packages/@react-aria/overlays/src/calculatePosition.ts +++ b/packages/@react-aria/overlays/src/calculatePosition.ts @@ -14,10 +14,10 @@ import {Axis, Placement, PlacementAxis, SizeAxis} from '@react-types/overlays'; import {clamp, isWebKit} from '@react-aria/utils'; interface Position { - top: number, - left: number, - bottom: number, - right: number + top?: number, + left?: number, + bottom?: number, + right?: number } interface Dimensions { @@ -64,10 +64,10 @@ interface PositionOpts { type HeightGrowthDirection = 'top' | 'bottom'; export interface PositionResult { - position: Position, - arrowOffsetLeft: number, - arrowOffsetTop: number, - maxHeight: number, + position?: Position, + arrowOffsetLeft?: number, + arrowOffsetTop?: number, + maxHeight?: number, placement: PlacementAxis } @@ -102,11 +102,12 @@ const TOTAL_SIZE = { const PARSED_PLACEMENT_CACHE = {}; +// @ts-ignore 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 = {left: 0, top: 0, right: 0, bottom: 0}; + let scroll: Position = {}; let isPinchZoomedIn = (visualViewport?.scale ?? 1) > 1; if (containerNode.tagName === 'BODY') { @@ -173,7 +174,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. @@ -236,29 +237,29 @@ function computePosition( arrowBoundaryOffset: number ) { let {placement, crossPlacement, axis, crossAxis, size, crossSize} = placementInfo; - let position: Position = {left: 0, top: 0, right: 0, bottom: 0}; + 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) { @@ -287,19 +288,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 = heightGrowthDirection !== 'top' ? 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); } @@ -314,10 +315,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( @@ -388,8 +389,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, @@ -409,22 +410,22 @@ 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 = {left: 0, top: 0, right: 0, bottom: 0}; + let arrowPosition: Position = {}; // All values are transformed so that 0 is at the top/left of the overlay depending on the orientation // Prefer the arrow being in the center of the trigger/overlay anchor element // 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 @@ -478,8 +479,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); diff --git a/packages/@react-aria/overlays/test/calculatePosition.test.ts b/packages/@react-aria/overlays/test/calculatePosition.test.ts index 98e2f678cba..747495b1314 100644 --- a/packages/@react-aria/overlays/test/calculatePosition.test.ts +++ b/packages/@react-aria/overlays/test/calculatePosition.test.ts @@ -104,7 +104,7 @@ describe('calculatePosition', function () { const placementCrossAxis = placementArray[1]; // The tests are all based on top/left positioning. Convert to bottom/right positioning if needed. - let pos: {right?: number, top?: number, left?: number, bottom?: number} = {top: 0, left: 0, right: 0, bottom: 0}; + let pos: {right?: number, top?: number, left?: number, bottom?: number} = {}; if ((placementAxis === 'left' && !flip) || (placementAxis === 'right' && flip)) { pos.right = boundaryDimensions.width - (expected[0] + overlaySize.width); pos.top = expected[1]; @@ -199,123 +199,123 @@ describe('calculatePosition', function () { const testCases = [ { placement: 'left', - noOffset: [50, 200, 0, 100, 350], - offsetBefore: [-200, 50, 0, 4, 500], - offsetAfter: [300, 350, 0, 196, 200], - crossAxisOffsetPositive: [50, 210, 0, 90, 340], - crossAxisOffsetNegative: [50, 190, 0, 110, 360], - mainAxisOffset: [40, 200, 0, 100, 350], - arrowBoundaryOffset: [50, 322, 0, 24, 228] + noOffset: [50, 200, undefined, 100, 350], + offsetBefore: [-200, 50, undefined, 4, 500], + offsetAfter: [300, 350, undefined, 196, 200], + crossAxisOffsetPositive: [50, 210, undefined, 90, 340], + crossAxisOffsetNegative: [50, 190, undefined, 110, 360], + mainAxisOffset: [40, 200, undefined, 100, 350], + arrowBoundaryOffset: [50, 322, undefined, 24, 228] }, { placement: 'left top', - noOffset: [50, 250, 0, 50, 300], - offsetBefore: [-200, 50, 0, 4, 500], - offsetAfter: [300, 350, 0, 196, 200], - crossAxisOffsetPositive: [50, 260, 0, 40, 290], - crossAxisOffsetNegative: [50, 240, 0, 60, 310], - mainAxisOffset: [40, 250, 0, 50, 300], - arrowBoundaryOffset: [50, 322, 0, 24, 228] + noOffset: [50, 250, undefined, 50, 300], + offsetBefore: [-200, 50, undefined, 4, 500], + offsetAfter: [300, 350, undefined, 196, 200], + crossAxisOffsetPositive: [50, 260, undefined, 40, 290], + crossAxisOffsetNegative: [50, 240, undefined, 60, 310], + mainAxisOffset: [40, 250, undefined, 50, 300], + arrowBoundaryOffset: [50, 322, undefined, 24, 228] }, { placement: 'left bottom', - noOffset: [50, 150, 0, 150, 300], - offsetBefore: [-200, 50, 0, 4, 200], - offsetAfter: [300, 350, 0, 196, 500], - crossAxisOffsetPositive: [50, 160, 0, 140, 310], - crossAxisOffsetNegative: [50, 140, 0, 160, 290], - mainAxisOffset: [40, 150, 0, 150, 300], - arrowBoundaryOffset: [50, 322, 0, 24, 472] + noOffset: [50, 150, undefined, 150, 300], + offsetBefore: [-200, 50, undefined, 4, 200], + offsetAfter: [300, 350, undefined, 196, 500], + crossAxisOffsetPositive: [50, 160, undefined, 140, 310], + crossAxisOffsetNegative: [50, 140, undefined, 160, 290], + mainAxisOffset: [40, 150, undefined, 150, 300], + arrowBoundaryOffset: [50, 322, undefined, 24, 472] }, { placement: 'top', - noOffset: [200, 50, 100, 0, 200], - offsetBefore: [50, -200, 4, 0, 0], - offsetAfter: [350, 300, 196, 0, 450], - crossAxisOffsetPositive: [210, 50, 90, 0, 200], - crossAxisOffsetNegative: [190, 50, 110, 0, 200], - mainAxisOffset: [200, 40, 100, 0, 190], - arrowBoundaryOffset: [322, 50, 24, 0, 200] + noOffset: [200, 50, 100, undefined, 200], + offsetBefore: [50, -200, 4, undefined, 0], + offsetAfter: [350, 300, 196, undefined, 450], + crossAxisOffsetPositive: [210, 50, 90, undefined, 200], + crossAxisOffsetNegative: [190, 50, 110, undefined, 200], + mainAxisOffset: [200, 40, 100, undefined, 190], + arrowBoundaryOffset: [322, 50, 24, undefined, 200] }, { placement: 'top left', - noOffset: [250, 50, 50, 0, 200], - offsetBefore: [50, -200, 4, 0, 0], - offsetAfter: [350, 300, 196, 0, 450], - crossAxisOffsetPositive: [260, 50, 40, 0, 200], - crossAxisOffsetNegative: [240, 50, 60, 0, 200], - mainAxisOffset: [250, 40, 50, 0, 190], - arrowBoundaryOffset: [322, 50, 24, 0, 200] + noOffset: [250, 50, 50, undefined, 200], + offsetBefore: [50, -200, 4, undefined, 0], + offsetAfter: [350, 300, 196, undefined, 450], + crossAxisOffsetPositive: [260, 50, 40, undefined, 200], + crossAxisOffsetNegative: [240, 50, 60, undefined, 200], + mainAxisOffset: [250, 40, 50, undefined, 190], + arrowBoundaryOffset: [322, 50, 24, undefined, 200] }, { placement: 'top right', - noOffset: [150, 50, 150, 0, 200], - offsetBefore: [50, -200, 4, 0, 0], - offsetAfter: [350, 300, 196, 0, 450], - crossAxisOffsetPositive: [160, 50, 140, 0, 200], - crossAxisOffsetNegative: [140, 50, 160, 0, 200], - mainAxisOffset: [150, 40, 150, 0, 190], - arrowBoundaryOffset: [322, 50, 24, 0, 200] + noOffset: [150, 50, 150, undefined, 200], + offsetBefore: [50, -200, 4, undefined, 0], + offsetAfter: [350, 300, 196, undefined, 450], + crossAxisOffsetPositive: [160, 50, 140, undefined, 200], + crossAxisOffsetNegative: [140, 50, 160, undefined, 200], + mainAxisOffset: [150, 40, 150, undefined, 190], + arrowBoundaryOffset: [322, 50, 24, undefined, 200] }, { placement: 'bottom', - noOffset: [200, 350, 100, 0, 200], - offsetBefore: [50, 100, 4, 0, 450], - offsetAfter: [350, 600, 196, 0, 0], - crossAxisOffsetPositive: [210, 350, 90, 0, 200], - crossAxisOffsetNegative: [190, 350, 110, 0, 200], - mainAxisOffset: [200, 360, 100, 0, 190], - arrowBoundaryOffset: [322, 350, 24, 0, 200] + noOffset: [200, 350, 100, undefined, 200], + offsetBefore: [50, 100, 4, undefined, 450], + offsetAfter: [350, 600, 196, undefined, 0], + crossAxisOffsetPositive: [210, 350, 90, undefined, 200], + crossAxisOffsetNegative: [190, 350, 110, undefined, 200], + mainAxisOffset: [200, 360, 100, undefined, 190], + arrowBoundaryOffset: [322, 350, 24, undefined, 200] }, { placement: 'bottom left', - noOffset: [250, 350, 50, 0, 200], - offsetBefore: [50, 100, 4, 0, 450], - offsetAfter: [350, 600, 196, 0, 0], - crossAxisOffsetPositive: [260, 350, 40, 0, 200], - crossAxisOffsetNegative: [240, 350, 60, 0, 200], - mainAxisOffset: [250, 360, 50, 0, 190], - arrowBoundaryOffset: [322, 350, 24, 0, 200] + noOffset: [250, 350, 50, undefined, 200], + offsetBefore: [50, 100, 4, undefined, 450], + offsetAfter: [350, 600, 196, undefined, 0], + crossAxisOffsetPositive: [260, 350, 40, undefined, 200], + crossAxisOffsetNegative: [240, 350, 60, undefined, 200], + mainAxisOffset: [250, 360, 50, undefined, 190], + arrowBoundaryOffset: [322, 350, 24, undefined, 200] }, { placement: 'bottom right', - noOffset: [150, 350, 150, 0, 200], - offsetBefore: [50, 100, 4, 0, 450], - offsetAfter: [350, 600, 196, 0, 0], - crossAxisOffsetPositive: [160, 350, 140, 0, 200], - crossAxisOffsetNegative: [140, 350, 160, 0, 200], - mainAxisOffset: [150, 360, 150, 0, 190], - arrowBoundaryOffset: [322, 350, 24, 0, 200] + noOffset: [150, 350, 150, undefined, 200], + offsetBefore: [50, 100, 4, undefined, 450], + offsetAfter: [350, 600, 196, undefined, 0], + crossAxisOffsetPositive: [160, 350, 140, undefined, 200], + crossAxisOffsetNegative: [140, 350, 160, undefined, 200], + mainAxisOffset: [150, 360, 150, undefined, 190], + arrowBoundaryOffset: [322, 350, 24, undefined, 200] }, { placement: 'right', - noOffset: [350, 200, 0, 100, 350], - offsetBefore: [100, 50, 0, 4, 500], - offsetAfter: [600, 350, 0, 196, 200], - crossAxisOffsetPositive: [350, 210, 0, 90, 340], - crossAxisOffsetNegative: [350, 190, 0, 110, 360], - mainAxisOffset: [360, 200, 0, 100, 350], - arrowBoundaryOffset: [350, 322, 0, 24, 228] + noOffset: [350, 200, undefined, 100, 350], + offsetBefore: [100, 50, undefined, 4, 500], + offsetAfter: [600, 350, undefined, 196, 200], + crossAxisOffsetPositive: [350, 210, undefined, 90, 340], + crossAxisOffsetNegative: [350, 190, undefined, 110, 360], + mainAxisOffset: [360, 200, undefined, 100, 350], + arrowBoundaryOffset: [350, 322, undefined, 24, 228] }, { placement: 'right top', - noOffset: [350, 250, 0, 50, 300], - offsetBefore: [100, 50, 0, 4, 500], - offsetAfter: [600, 350, 0, 196, 200], - crossAxisOffsetPositive: [350, 260, 0, 40, 290], - crossAxisOffsetNegative: [350, 240, 0, 60, 310], - mainAxisOffset: [360, 250, 0, 50, 300], - arrowBoundaryOffset: [350, 322, 0, 24, 228] + noOffset: [350, 250, undefined, 50, 300], + offsetBefore: [100, 50, undefined, 4, 500], + offsetAfter: [600, 350, undefined, 196, 200], + crossAxisOffsetPositive: [350, 260, undefined, 40, 290], + crossAxisOffsetNegative: [350, 240, undefined, 60, 310], + mainAxisOffset: [360, 250, undefined, 50, 300], + arrowBoundaryOffset: [350, 322, undefined, 24, 228] }, { placement: 'right bottom', - noOffset: [350, 150, 0, 150, 300], - offsetBefore: [100, 50, 0, 4, 200], - offsetAfter: [600, 350, 0, 196, 500], - crossAxisOffsetPositive: [350, 160, 0, 140, 310], - crossAxisOffsetNegative: [350, 140, 0, 160, 290], - mainAxisOffset: [360, 150, 0, 150, 300], - arrowBoundaryOffset: [350, 322, 0, 24, 472] + noOffset: [350, 150, undefined, 150, 300], + offsetBefore: [100, 50, undefined, 4, 200], + offsetAfter: [600, 350, undefined, 196, 500], + crossAxisOffsetPositive: [350, 160, undefined, 140, 310], + crossAxisOffsetNegative: [350, 140, undefined, 160, 290], + mainAxisOffset: [360, 150, undefined, 150, 300], + arrowBoundaryOffset: [350, 322, undefined, 24, 472] } ]; @@ -381,14 +381,14 @@ describe('calculatePosition', function () { describe('positive offset', function () { checkPosition( - 'left', getTargetDimension({left: 0, top: 250}), [110, 200, 0, 100, 350], 10, 0, true + 'left', getTargetDimension({left: 0, top: 250}), [110, 200, undefined, 100, 350], 10, 0, true ); }); }); describe('overlay smaller than target aligns in center', function () { checkPosition( - 'right', getTargetDimension({left: 250, top: 250}, overlaySize.height + 100, overlaySize.width + 100), [550, 300, 0, 100, 250] + 'right', getTargetDimension({left: 250, top: 250}, overlaySize.height + 100, overlaySize.width + 100), [550, 300, undefined, 100, 250] ); }); From 3082d4547b6235f1092bd795a29fe1afb8045723 Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 14 Nov 2024 10:52:58 +1100 Subject: [PATCH 10/66] changes to calculate position --- packages/@react-aria/overlays/src/calculatePosition.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@react-aria/overlays/src/calculatePosition.ts b/packages/@react-aria/overlays/src/calculatePosition.ts index e45921a8e9f..b956985ad09 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 } From 40280dbfc3b4764991078a08162539f0133b9d71 Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 14 Nov 2024 11:14:01 +1100 Subject: [PATCH 11/66] grid aria hooks --- packages/@react-aria/grid/src/useGridCell.ts | 14 ++++++++------ packages/@react-aria/grid/src/useGridRow.ts | 4 ++-- .../grid/src/useGridSelectionAnnouncement.ts | 2 +- .../grid/src/useHighlightSelectionDescription.ts | 2 +- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/@react-aria/grid/src/useGridCell.ts b/packages/@react-aria/grid/src/useGridCell.ts index 58f4a2b7e09..a04d4f3fca1 100644 --- a/packages/@react-aria/grid/src/useGridCell.ts +++ b/packages/@react-aria/grid/src/useGridCell.ts @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import {DOMAttributes, FocusableElement, RefObject} from '@react-types/shared'; +import {DOMAttributes, FocusableElement, Key, RefObject} from '@react-types/shared'; import {focusSafely, getFocusableTreeWalker} from '@react-aria/focus'; import {getScrollParent, mergeProps, scrollIntoViewport} from '@react-aria/utils'; import {GridCollection, GridNode} from '@react-types/grid'; @@ -64,7 +64,7 @@ export function useGridCell>(props: GridCellProps // 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. @@ -268,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); + } }); }; } @@ -280,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'); } From 437e149018865e9ef6d538a725a83877c59e719e Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 14 Nov 2024 12:11:28 +1100 Subject: [PATCH 12/66] aria menu --- packages/@react-aria/grid/stories/example.tsx | 22 ++++++++--- .../gridlist/src/useGridListItem.ts | 39 ++++++++++++------- packages/@react-aria/gridlist/src/utils.ts | 4 +- packages/@react-aria/menu/src/useMenu.ts | 2 +- packages/@react-aria/menu/src/useMenuItem.ts | 10 ++--- .../@react-aria/menu/src/useMenuTrigger.ts | 3 +- packages/@react-aria/utils/src/openLink.tsx | 2 +- .../src/OverlayArrow.tsx | 8 ++-- .../react-aria-components/src/Popover.tsx | 2 +- .../react-aria-components/src/Tooltip.tsx | 4 +- 10 files changed, 61 insertions(+), 35 deletions(-) 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/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 30ae1b9c18b..f409a2851eb 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. @@ -127,7 +127,7 @@ export function useMenuItem(props: AriaMenuItemProps, state: TreeState, re let isTrigger = !!hasPopup; 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(); @@ -148,7 +148,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); } }; @@ -277,9 +277,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/utils/src/openLink.tsx b/packages/@react-aria/utils/src/openLink.tsx index e0b099508cc..a35b1adc258 100644 --- a/packages/@react-aria/utils/src/openLink.tsx +++ b/packages/@react-aria/utils/src/openLink.tsx @@ -171,7 +171,7 @@ export function getSyntheticLinkProps(props: LinkDOMProps) { }; } -export function useLinkProps(props: LinkDOMProps) { +export function useLinkProps(props?: LinkDOMProps) { let router = useRouter(); const href = router.useHref(props?.href ?? ''); return { diff --git a/packages/react-aria-components/src/OverlayArrow.tsx b/packages/react-aria-components/src/OverlayArrow.tsx index b4ac146e253..b1b44a59a1c 100644 --- a/packages/react-aria-components/src/OverlayArrow.tsx +++ b/packages/react-aria-components/src/OverlayArrow.tsx @@ -16,7 +16,7 @@ import {PlacementAxis} from 'react-aria'; import React, {createContext, CSSProperties, ForwardedRef, forwardRef, HTMLAttributes} from 'react'; interface OverlayArrowContextValue extends OverlayArrowProps { - placement: PlacementAxis + placement: PlacementAxis | null } export const OverlayArrowContext = createContext>({ @@ -30,7 +30,7 @@ export interface OverlayArrowRenderProps { * The placement of the overlay relative to the trigger. * @selector [data-placement="left | right | top | bottom"] */ - placement: PlacementAxis + placement: PlacementAxis | null } function OverlayArrow(props: OverlayArrowProps, ref: ForwardedRef) { @@ -38,9 +38,11 @@ function OverlayArrow(props: OverlayArrowProps, ref: ForwardedRef From b2d1dab3f3d83046e3d106800b066520b07ed5b8 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Wed, 13 Nov 2024 17:28:27 -0800 Subject: [PATCH 13/66] Virtualizer stuff --- .../test/useSearchAutocomplete.test.js | 12 +- .../combobox/test/useComboBox.test.js | 9 +- packages/@react-aria/dnd/src/DragPreview.tsx | 2 +- .../virtualizer/src/Virtualizer.tsx | 19 +- .../virtualizer/src/useVirtualizerItem.ts | 9 +- .../list/src/InsertionIndicator.tsx | 8 +- .../@react-spectrum/list/src/ListView.tsx | 58 ++-- .../@react-spectrum/list/src/ListViewItem.tsx | 53 ++-- .../list/src/ListViewLayout.ts | 4 +- .../list/src/RootDropIndicator.tsx | 10 +- .../listbox/src/ListBoxBase.tsx | 29 +- .../listbox/src/ListBoxContext.ts | 2 +- .../listbox/src/ListBoxLayout.ts | 7 +- .../listbox/src/ListBoxOption.tsx | 2 +- .../listbox/src/ListBoxSection.tsx | 9 +- .../table/src/InsertionIndicator.tsx | 10 +- .../@react-spectrum/table/src/Resizer.tsx | 17 +- .../table/src/RootDropIndicator.tsx | 8 +- .../table/src/TableViewBase.tsx | 250 +++++++++--------- .../table/src/TableViewLayout.ts | 16 +- .../@react-stately/layout/src/GridLayout.ts | 30 +-- .../@react-stately/layout/src/ListLayout.ts | 106 ++++---- .../@react-stately/layout/src/TableLayout.ts | 93 ++++--- .../@react-stately/virtualizer/src/Layout.ts | 8 +- .../virtualizer/src/ReusableView.ts | 41 ++- .../virtualizer/src/Virtualizer.ts | 48 ++-- .../@react-stately/virtualizer/src/types.ts | 4 +- .../virtualizer/src/useVirtualizerState.ts | 20 +- .../react-aria-components/src/Virtualizer.tsx | 4 +- .../react-aria-components/test/Table.test.js | 8 +- 30 files changed, 484 insertions(+), 412 deletions(-) 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/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/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/virtualizer/src/Virtualizer.tsx b/packages/@react-aria/virtualizer/src/Virtualizer.tsx index 84a8c0b9728..7d0bd5ecbaf 100644 --- a/packages/@react-aria/virtualizer/src/Virtualizer.tsx +++ b/packages/@react-aria/virtualizer/src/Virtualizer.tsx @@ -12,8 +12,8 @@ import {Collection, Key, RefObject} from '@react-types/shared'; import {Layout, Rect, ReusableView, useVirtualizerState} from '@react-stately/virtualizer'; -import {mergeProps, useLoadMore} from '@react-aria/utils'; -import React, {HTMLAttributes, ReactElement, ReactNode, useCallback, useRef} from 'react'; +import {mergeProps, useLoadMore, useObjectRef} from '@react-aria/utils'; +import React, {ForwardedRef, HTMLAttributes, ReactElement, ReactNode, useCallback} from 'react'; import {ScrollView} from './ScrollView'; import {VirtualizerItem} from './VirtualizerItem'; @@ -22,7 +22,7 @@ type RenderWrapper = ( reusableView: ReusableView, children: ReusableView[], renderChildren: (views: ReusableView[]) => ReactElement[] -) => ReactElement; +) => ReactElement | null; interface VirtualizerProps extends Omit, 'children'> { children: (type: string, content: T) => V, @@ -36,32 +36,31 @@ interface VirtualizerProps extends Omit(props: VirtualizerProps, ref: RefObject) { +function Virtualizer(props: VirtualizerProps, forwardedRef: ForwardedRef) { let { children: renderView, renderWrapper, layout, collection, scrollDirection, - isLoading, - onLoadMore, persistedKeys, layoutOptions, ...otherProps } = props; - let fallbackRef = useRef(undefined); - ref = ref || fallbackRef; + let ref = useObjectRef(forwardedRef); let state = useVirtualizerState({ layout, collection, renderView, onVisibleRectChange(rect) { - ref.current.scrollLeft = rect.x; - ref.current.scrollTop = rect.y; + if (ref.current) { + ref.current.scrollLeft = rect.x; + ref.current.scrollTop = rect.y; + } }, persistedKeys, layoutOptions diff --git a/packages/@react-aria/virtualizer/src/useVirtualizerItem.ts b/packages/@react-aria/virtualizer/src/useVirtualizerItem.ts index 366d70c0565..e4214ccd3c1 100644 --- a/packages/@react-aria/virtualizer/src/useVirtualizerItem.ts +++ b/packages/@react-aria/virtualizer/src/useVirtualizerItem.ts @@ -20,20 +20,21 @@ interface IVirtualizer { } export interface VirtualizerItemOptions { - layoutInfo: LayoutInfo, + layoutInfo: LayoutInfo | null, virtualizer: IVirtualizer, ref: RefObject } export function useVirtualizerItem(options: VirtualizerItemOptions) { let {layoutInfo, virtualizer, ref} = options; + let key = layoutInfo?.key; let updateSize = useCallback(() => { - if (layoutInfo) { + if (key != null && ref.current) { let size = getSize(ref.current); - virtualizer.updateItemSize(layoutInfo.key, size); + virtualizer.updateItemSize(key, size); } - }, [virtualizer, layoutInfo?.key, ref]); + }, [virtualizer, key, ref]); useLayoutEffect(() => { if (layoutInfo?.estimatedSize) { 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/listbox/src/ListBoxBase.tsx b/packages/@react-spectrum/listbox/src/ListBoxBase.tsx index a48a42d3b2f..aec1f0ddaa1 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'))} ); @@ -109,7 +110,7 @@ function ListBoxBase(props: ListBoxBaseProps, ref: RefObject(props: ListBoxBaseProps, ref: RefObject; } 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/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..47193c8bba6 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-stately/layout/src/GridLayout.ts b/packages/@react-stately/layout/src/GridLayout.ts index 03eef6167bf..af94c5f8c9a 100644 --- a/packages/@react-stately/layout/src/GridLayout.ts +++ b/packages/@react-stately/layout/src/GridLayout.ts @@ -47,10 +47,10 @@ export class GridLayout extends Layout, O> implements DropTa protected minSpace: Size; protected maxColumns: number; protected dropIndicatorThickness: number; - protected itemSize: Size; - protected numColumns: number; - protected horizontalSpacing: number; - protected layoutInfos: LayoutInfo[]; + protected itemSize: Size = new Size(); + protected numColumns: number = 0; + protected horizontalSpacing: number = 0; + protected layoutInfos: LayoutInfo[] = []; constructor(options: GridLayoutOptions) { super(); @@ -62,7 +62,7 @@ export class GridLayout extends Layout, O> implements DropTa } update(): void { - 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. @@ -92,7 +92,7 @@ export class GridLayout extends Layout, O> implements DropTa this.horizontalSpacing = Math.floor((visibleWidth - this.numColumns * this.itemSize.width) / (this.numColumns + 1)); this.layoutInfos = []; - for (let node of this.virtualizer.collection) { + for (let node of this.virtualizer!.collection) { this.layoutInfos.push(this.getLayoutInfoForNode(node)); } } @@ -102,8 +102,8 @@ export class GridLayout extends Layout, O> implements DropTa let lastVisibleItem = this.getIndexAtPoint(rect.maxX, rect.maxY); let result = this.layoutInfos.slice(firstVisibleItem, lastVisibleItem + 1); let persistedIndices: number[] = []; - for (let key of this.virtualizer.persistedKeys) { - let item = this.virtualizer.collection.getItem(key); + for (let key of this.virtualizer!.persistedKeys) { + let item = this.virtualizer!.collection.getItem(key); if (item?.index != null) { persistedIndices.push(item.index); } @@ -127,14 +127,14 @@ export class GridLayout extends Layout, O> implements DropTa let itemWidth = this.itemSize.width + this.horizontalSpacing; return Math.max(0, Math.min( - this.virtualizer.collection.size - 1, + this.virtualizer!.collection.size - 1, Math.floor(y / itemHeight) * this.numColumns + Math.floor((x - this.horizontalSpacing) / itemWidth) ) ); } getLayoutInfo(key: Key): LayoutInfo | null { - let node = this.virtualizer.collection.getItem(key); + let node = this.virtualizer!.collection.getItem(key); return node ? this.layoutInfos[node.index] : null; } @@ -149,9 +149,9 @@ export class GridLayout extends Layout, O> implements DropTa } getContentSize(): Size { - let numRows = Math.ceil(this.virtualizer.collection.size / this.numColumns); + let numRows = Math.ceil(this.virtualizer!.collection.size / this.numColumns); let contentHeight = this.minSpace.height + numRows * (this.itemSize.height + this.minSpace.height); - return new Size(this.virtualizer.visibleRect.width, contentHeight); + return new Size(this.virtualizer!.visibleRect.width, contentHeight); } getDropTargetFromPoint(x: number, y: number, isValidDropTarget: (target: DropTarget) => boolean): DropTarget { @@ -159,8 +159,8 @@ export class GridLayout extends Layout, O> implements DropTa return {type: 'root'}; } - x += this.virtualizer.visibleRect.x; - y += this.virtualizer.visibleRect.y; + x += this.virtualizer!.visibleRect.x; + y += this.virtualizer!.visibleRect.y; let index = this.getIndexAtPoint(x, y); let layoutInfo = this.layoutInfos[index]; @@ -195,7 +195,7 @@ export class GridLayout extends Layout, O> implements DropTa } getDropTargetLayoutInfo(target: ItemDropTarget): LayoutInfo { - let layoutInfo = this.getLayoutInfo(target.key); + let layoutInfo = this.getLayoutInfo(target.key)!; let rect: Rect; if (this.numColumns === 1) { // Flip from vertical to horizontal if only one column is visible. diff --git a/packages/@react-stately/layout/src/ListLayout.ts b/packages/@react-stately/layout/src/ListLayout.ts index f8c39b6607f..01df3e609ef 100644 --- a/packages/@react-stately/layout/src/ListLayout.ts +++ b/packages/@react-stately/layout/src/ListLayout.ts @@ -51,16 +51,15 @@ const DEFAULT_HEIGHT = 48; * the virtualizer itself). */ export class ListLayout extends Layout, O> implements DropTargetDelegate { - protected rowHeight: number; - protected estimatedRowHeight: number; - protected headingHeight: number; - protected estimatedHeadingHeight: number; - protected loaderHeight: number; + protected rowHeight: number | null; + protected estimatedRowHeight: number | null; + protected headingHeight: number | null; + protected estimatedHeadingHeight: number | null; + protected loaderHeight: number | null; protected dropIndicatorThickness: number; protected layoutNodes: Map; protected contentSize: Size; - protected collection: Collection>; - private lastCollection: Collection>; + protected lastCollection: Collection> | null; private lastWidth: number; protected rootNodes: LayoutNode[]; private invalidateEverything: boolean; @@ -75,21 +74,27 @@ export class ListLayout extends Layout, O> implements DropTa */ constructor(options: ListLayoutOptions = {}) { super(); - this.rowHeight = options.rowHeight; - this.estimatedRowHeight = options.estimatedRowHeight; - this.headingHeight = options.headingHeight; - this.estimatedHeadingHeight = options.estimatedHeadingHeight; - this.loaderHeight = options.loaderHeight; + this.rowHeight = options.rowHeight ?? null; + this.estimatedRowHeight = options.estimatedRowHeight ?? null; + this.headingHeight = options.headingHeight ?? null; + this.estimatedHeadingHeight = options.estimatedHeadingHeight ?? null; + this.loaderHeight = options.loaderHeight ?? null; this.dropIndicatorThickness = options.dropIndicatorThickness || 2; this.layoutNodes = new Map(); this.rootNodes = []; this.lastWidth = 0; this.lastCollection = null; + this.invalidateEverything = false; this.validRect = new Rect(); this.requestedRect = new Rect(); this.contentSize = new Size(); } + // Backward compatibility for subclassing. + protected get collection(): Collection> { + return this.virtualizer!.collection; + } + getLayoutInfo(key: Key) { this.ensureLayoutInfo(key); return this.layoutNodes.get(key)?.layoutInfo || null; @@ -99,7 +104,7 @@ export class ListLayout extends Layout, O> implements DropTa // Adjust rect to keep number of visible rows consistent. // (only if height > 1 for getDropTargetFromPoint) if (rect.height > 1) { - let rowHeight = (this.rowHeight ?? this.estimatedRowHeight); + let rowHeight = this.rowHeight ?? this.estimatedRowHeight ?? DEFAULT_HEIGHT; rect.y = Math.floor(rect.y / rowHeight) * rowHeight; rect.height = Math.ceil(rect.height / rowHeight) * rowHeight; } @@ -137,7 +142,7 @@ export class ListLayout extends Layout, O> implements DropTa } // Ensure all of the persisted keys are available. - for (let key of this.virtualizer.persistedKeys) { + for (let key of this.virtualizer!.persistedKeys) { if (this.ensureLayoutInfo(key)) { return; } @@ -159,32 +164,32 @@ export class ListLayout extends Layout, O> implements DropTa } protected isVisible(node: LayoutNode, rect: Rect) { - return node.layoutInfo.rect.intersects(rect) || node.layoutInfo.isSticky || node.layoutInfo.type === 'header' || this.virtualizer.isPersistedKey(node.layoutInfo.key); + return node.layoutInfo.rect.intersects(rect) || node.layoutInfo.isSticky || node.layoutInfo.type === 'header' || this.virtualizer!.isPersistedKey(node.layoutInfo.key); } protected shouldInvalidateEverything(invalidationContext: InvalidationContext) { // Invalidate cache if the size of the collection changed. // In this case, we need to recalculate the entire layout. - return invalidationContext.sizeChanged; + return invalidationContext.sizeChanged || false; } update(invalidationContext: InvalidationContext) { - this.collection = this.virtualizer.collection; + let collection = this.virtualizer!.collection; // Reset valid rect if we will have to invalidate everything. // Otherwise we can reuse cached layout infos outside the current visible rect. this.invalidateEverything = this.shouldInvalidateEverything(invalidationContext); if (this.invalidateEverything) { - this.requestedRect = this.virtualizer.visibleRect.copy(); + this.requestedRect = this.virtualizer!.visibleRect.copy(); this.layoutNodes.clear(); } this.rootNodes = this.buildCollection(); // Remove deleted layout nodes - if (this.lastCollection && this.collection !== this.lastCollection) { + if (this.lastCollection && collection !== this.lastCollection) { for (let key of this.lastCollection.getKeys()) { - if (!this.collection.getItem(key)) { + if (!collection.getItem(key)) { let layoutNode = this.layoutNodes.get(key); if (layoutNode) { this.layoutNodes.delete(key); @@ -193,17 +198,18 @@ export class ListLayout extends Layout, O> implements DropTa } } - this.lastWidth = this.virtualizer.visibleRect.width; - this.lastCollection = this.collection; + this.lastWidth = this.virtualizer!.visibleRect.width; + this.lastCollection = collection; this.invalidateEverything = false; this.validRect = this.requestedRect.copy(); } protected buildCollection(y = 0): LayoutNode[] { + let collection = this.virtualizer!.collection; let skipped = 0; - let nodes = []; - for (let node of this.collection) { - let rowHeight = this.rowHeight ?? this.estimatedRowHeight; + let nodes: LayoutNode[] = []; + for (let node of collection) { + let rowHeight = this.rowHeight ?? this.estimatedRowHeight ?? DEFAULT_HEIGHT; // Skip rows before the valid rectangle unless they are already cached. if (node.type === 'item' && y + rowHeight < this.requestedRect.y && !this.isValid(node, y)) { @@ -217,12 +223,12 @@ export class ListLayout extends Layout, O> implements DropTa nodes.push(layoutNode); if (node.type === 'item' && y > this.requestedRect.maxY) { - y += (this.collection.size - (nodes.length + skipped)) * rowHeight; + y += (collection.size - (nodes.length + skipped)) * rowHeight; break; } } - this.contentSize = new Size(this.virtualizer.visibleRect.width, y); + this.contentSize = new Size(this.virtualizer!.visibleRect.width, y); return nodes; } @@ -240,7 +246,7 @@ export class ListLayout extends Layout, O> implements DropTa protected buildChild(node: Node, x: number, y: number, parentKey: Key | null): LayoutNode { if (this.isValid(node, y)) { - return this.layoutNodes.get(node.key); + return this.layoutNodes.get(node.key)!; } let layoutNode = this.buildNode(node, x, y); @@ -260,14 +266,16 @@ export class ListLayout extends Layout, O> implements DropTa return this.buildSectionHeader(node, x, y); case 'loader': return this.buildLoader(node, x, y); + default: + throw new Error('Unsupported node type: ' + node.type); } } protected buildLoader(node: Node, x: number, y: number): LayoutNode { let rect = new Rect(x, y, 0, 0); let layoutInfo = new LayoutInfo('loader', node.key, rect); - rect.width = this.virtualizer.contentSize.width; - rect.height = this.loaderHeight || this.rowHeight || this.estimatedRowHeight; + rect.width = this.virtualizer!.contentSize.width; + rect.height = this.loaderHeight || this.rowHeight || this.estimatedRowHeight || DEFAULT_HEIGHT; return { layoutInfo, @@ -276,15 +284,16 @@ export class ListLayout extends Layout, O> implements DropTa } protected buildSection(node: Node, x: number, y: number): LayoutNode { - let width = this.virtualizer.visibleRect.width; + let collection = this.virtualizer!.collection; + let width = this.virtualizer!.visibleRect.width; let rect = new Rect(0, y, width, 0); let layoutInfo = new LayoutInfo(node.type, node.key, rect); let startY = y; let skipped = 0; - let children = []; - for (let child of getChildNodes(node, this.collection)) { - let rowHeight = (this.rowHeight ?? this.estimatedRowHeight); + let children: LayoutNode[] = []; + for (let child of getChildNodes(node, collection)) { + let rowHeight = (this.rowHeight ?? this.estimatedRowHeight ?? DEFAULT_HEIGHT); // Skip rows before the valid rectangle unless they are already cached. if (y + rowHeight < this.requestedRect.y && !this.isValid(node, y)) { @@ -299,7 +308,7 @@ export class ListLayout extends Layout, O> implements DropTa if (y > this.requestedRect.maxY) { // Estimate the remaining height for rows that we don't need to layout right now. - y += ([...getChildNodes(node, this.collection)].length - (children.length + skipped)) * rowHeight; + y += ([...getChildNodes(node, collection)].length - (children.length + skipped)) * rowHeight; break; } } @@ -315,7 +324,7 @@ export class ListLayout extends Layout, O> implements DropTa } protected buildSectionHeader(node: Node, x: number, y: number): LayoutNode { - let width = this.virtualizer.visibleRect.width; + let width = this.virtualizer!.visibleRect.width; let rectHeight = this.headingHeight; let isEstimated = false; @@ -327,7 +336,7 @@ export class ListLayout extends Layout, O> implements DropTa let previousLayoutNode = this.layoutNodes.get(node.key); let previousLayoutInfo = previousLayoutNode?.layoutInfo; if (previousLayoutInfo) { - let curNode = this.collection.getItem(node.key); + let curNode = this.virtualizer!.collection.getItem(node.key); let lastNode = this.lastCollection ? this.lastCollection.getItem(node.key) : null; rectHeight = previousLayoutInfo.rect.height; isEstimated = width !== this.lastWidth || curNode !== lastNode || previousLayoutInfo.estimatedSize; @@ -353,7 +362,7 @@ export class ListLayout extends Layout, O> implements DropTa } protected buildItem(node: Node, x: number, y: number): LayoutNode { - let width = this.virtualizer.visibleRect.width; + let width = this.virtualizer!.visibleRect.width; let rectHeight = this.rowHeight; let isEstimated = false; @@ -394,6 +403,7 @@ export class ListLayout extends Layout, O> implements DropTa return false; } + let collection = this.virtualizer!.collection; let layoutInfo = layoutNode.layoutInfo; layoutInfo.estimatedSize = false; if (layoutInfo.rect.height !== size.height) { @@ -412,10 +422,10 @@ export class ListLayout extends Layout, O> implements DropTa // Invalidate layout for this layout node and all parents this.updateLayoutNode(key, layoutInfo, newLayoutInfo); - let node = this.collection.getItem(layoutInfo.parentKey); + let node = layoutInfo.parentKey != null ? collection.getItem(layoutInfo.parentKey) : null; while (node) { this.updateLayoutNode(node.key, layoutInfo, newLayoutInfo); - node = this.collection.getItem(node.parentKey); + node = node.parentKey != null ? collection.getItem(node.parentKey) : null; } return true; @@ -441,16 +451,20 @@ export class ListLayout extends Layout, O> implements DropTa return this.contentSize; } - getDropTargetFromPoint(x: number, y: number, isValidDropTarget: (target: DropTarget) => boolean): DropTarget { - x += this.virtualizer.visibleRect.x; - y += this.virtualizer.visibleRect.y; + getDropTargetFromPoint(x: number, y: number, isValidDropTarget: (target: DropTarget) => boolean): DropTarget | null { + x += this.virtualizer!.visibleRect.x; + y += this.virtualizer!.visibleRect.y; - let key = this.virtualizer.keyAtPoint(new Point(x, y)); - if (key == null || this.collection.size === 0) { + let key = this.virtualizer!.keyAtPoint(new Point(x, y)); + if (key == null || this.virtualizer!.collection.size === 0) { return {type: 'root'}; } let layoutInfo = this.getLayoutInfo(key); + if (!layoutInfo) { + return null; + } + let rect = layoutInfo.rect; let target: DropTarget = { type: 'item', @@ -477,7 +491,7 @@ export class ListLayout extends Layout, O> implements DropTa } getDropTargetLayoutInfo(target: ItemDropTarget): LayoutInfo { - let layoutInfo = this.getLayoutInfo(target.key); + let layoutInfo = this.getLayoutInfo(target.key)!; let rect: Rect; if (target.dropPosition === 'before') { rect = new Rect(layoutInfo.rect.x, layoutInfo.rect.y - this.dropIndicatorThickness / 2, layoutInfo.rect.width, this.dropIndicatorThickness); diff --git a/packages/@react-stately/layout/src/TableLayout.ts b/packages/@react-stately/layout/src/TableLayout.ts index e80dbb042d3..07c943fa320 100644 --- a/packages/@react-stately/layout/src/TableLayout.ts +++ b/packages/@react-stately/layout/src/TableLayout.ts @@ -22,11 +22,13 @@ export interface TableLayoutProps { columnWidths?: Map } +const DEFAULT_ROW_HEIGHT = 48; + export class TableLayout extends ListLayout { - protected collection: TableCollection; - private columnWidths: Map; + protected lastCollection: TableCollection | null = null; + private columnWidths: Map = new Map(); private stickyColumnIndices: number[]; - private lastPersistedKeys: Set = null; + private lastPersistedKeys: Set | null = null; private persistedIndices: Map = new Map(); constructor(options: ListLayoutOptions) { @@ -34,6 +36,11 @@ export class TableLayout exten this.stickyColumnIndices = []; } + // Backward compatibility for subclassing. + protected get collection(): TableCollection { + return this.virtualizer!.collection as TableCollection; + } + private columnsChanged(newCollection: TableCollection, oldCollection: TableCollection | null) { return !oldCollection || newCollection.columns !== oldCollection.columns && @@ -47,7 +54,7 @@ export class TableLayout exten } update(invalidationContext: InvalidationContext): void { - let newCollection = this.virtualizer.collection as TableCollection; + let newCollection = this.virtualizer!.collection as TableCollection; // If columnWidths were provided via layoutOptions, update those. // Otherwise, calculate column widths ourselves. @@ -56,9 +63,9 @@ export class TableLayout exten this.columnWidths = invalidationContext.layoutOptions.columnWidths; invalidationContext.sizeChanged = true; } - } else if (invalidationContext.sizeChanged || this.columnsChanged(newCollection, this.collection)) { + } else if (invalidationContext.sizeChanged || this.columnsChanged(newCollection, this.lastCollection)) { let columnLayout = new TableColumnLayout({}); - this.columnWidths = columnLayout.buildColumnWidths(this.virtualizer.visibleRect.width, newCollection, new Map()); + this.columnWidths = columnLayout.buildColumnWidths(this.virtualizer!.visibleRect.width, newCollection, new Map()); invalidationContext.sizeChanged = true; } @@ -68,10 +75,11 @@ export class TableLayout exten protected buildCollection(): LayoutNode[] { this.stickyColumnIndices = []; - for (let column of this.collection.columns) { + let collection = this.virtualizer!.collection as TableCollection; + for (let column of collection.columns) { // The selection cell and any other sticky columns always need to be visible. // In addition, row headers need to be in the DOM for accessibility labeling. - if (this.isStickyColumn(column) || this.collection.rowHeaderColumnKeys.has(column.key)) { + if (this.isStickyColumn(column) || collection.rowHeaderColumnKeys.has(column.key)) { this.stickyColumnIndices.push(column.index); } } @@ -90,15 +98,16 @@ export class TableLayout exten } protected buildTableHeader(): LayoutNode { + let collection = this.virtualizer!.collection as TableCollection; let rect = new Rect(0, 0, 0, 0); - let layoutInfo = new LayoutInfo('header', this.collection.head?.key ?? 'header', rect); + let layoutInfo = new LayoutInfo('header', collection.head?.key ?? 'header', rect); layoutInfo.isSticky = true; layoutInfo.zIndex = 1; let y = 0; let width = 0; let children: LayoutNode[] = []; - for (let headerRow of this.collection.headerRows) { + for (let headerRow of collection.headerRows) { let layoutNode = this.buildChild(headerRow, 0, y, layoutInfo.key); layoutNode.layoutInfo.parentKey = layoutInfo.key; y = layoutNode.layoutInfo.rect.maxY; @@ -114,7 +123,7 @@ export class TableLayout exten layoutInfo, children, validRect: layoutInfo.rect, - node: this.collection.head + node: collection.head }; } @@ -124,7 +133,7 @@ export class TableLayout exten let height = 0; let columns: LayoutNode[] = []; - for (let cell of getChildNodes(headerRow, this.collection)) { + for (let cell of getChildNodes(headerRow, this.virtualizer!.collection)) { let layoutNode = this.buildChild(cell, x, y, row.key); layoutNode.layoutInfo.parentKey = row.key; x = layoutNode.layoutInfo.rect.maxX; @@ -161,20 +170,21 @@ export class TableLayout exten // used to get the column widths when rendering to the DOM private getRenderedColumnWidth(node: GridNode) { + let collection = this.virtualizer!.collection as TableCollection; let colspan = node.colspan ?? 1; let colIndex = node.colIndex ?? node.index; let width = 0; for (let i = colIndex; i < colIndex + colspan; i++) { - let column = this.collection.columns[i]; + let column = collection.columns[i]; if (column?.key != null) { - width += this.columnWidths.get(column.key); + width += this.columnWidths.get(column.key) ?? 0; } } return width; } - private getEstimatedHeight(node: GridNode, width: number, height: number, estimatedHeight: number) { + private getEstimatedHeight(node: GridNode, width: number, height: number | null, estimatedHeight: number | null) { let isEstimated = false; // If no explicit height is available, use an estimated height. @@ -187,7 +197,7 @@ export class TableLayout exten height = previousLayoutNode.layoutInfo.rect.height; isEstimated = node !== previousLayoutNode.node || width !== previousLayoutNode.layoutInfo.rect.width || previousLayoutNode.layoutInfo.estimatedSize; } else { - height = estimatedHeight; + height = estimatedHeight ?? DEFAULT_ROW_HEIGHT; isEstimated = true; } } @@ -196,12 +206,12 @@ export class TableLayout exten } protected getEstimatedRowHeight(): number { - return this.rowHeight ?? this.estimatedRowHeight; + return this.rowHeight ?? this.estimatedRowHeight ?? DEFAULT_ROW_HEIGHT; } protected buildColumn(node: GridNode, x: number, y: number): LayoutNode { let width = this.getRenderedColumnWidth(node); - let {height, isEstimated} = this.getEstimatedHeight(node, width, this.headingHeight, this.estimatedHeadingHeight); + let {height, isEstimated} = this.getEstimatedHeight(node, width, this.headingHeight ?? this.rowHeight, this.estimatedHeadingHeight ?? this.estimatedRowHeight); let rect = new Rect(x, y, width, height); let layoutInfo = new LayoutInfo(node.type, node.key, rect); layoutInfo.isSticky = this.isStickyColumn(node); @@ -223,15 +233,16 @@ export class TableLayout exten } protected buildBody(y: number): LayoutNode { + let collection = this.virtualizer!.collection as TableCollection; let rect = new Rect(0, y, 0, 0); - let layoutInfo = new LayoutInfo('rowgroup', this.collection.body.key, rect); + let layoutInfo = new LayoutInfo('rowgroup', collection.body.key, rect); let startY = y; let skipped = 0; let width = 0; let children: LayoutNode[] = []; let rowHeight = this.getEstimatedRowHeight(); - for (let node of getChildNodes(this.collection.body, this.collection)) { + for (let node of getChildNodes(collection.body, collection)) { // Skip rows before the valid rectangle unless they are already cached. if (y + rowHeight < this.requestedRect.y && !this.isValid(node, y)) { y += rowHeight; @@ -248,13 +259,13 @@ export class TableLayout exten if (y > this.requestedRect.maxY) { // Estimate the remaining height for rows that we don't need to layout right now. - y += (this.collection.size - (skipped + children.length)) * rowHeight; + y += (collection.size - (skipped + children.length)) * rowHeight; break; } } if (children.length === 0) { - y = this.virtualizer.visibleRect.maxY; + y = this.virtualizer!.visibleRect.maxY; } rect.width = width; @@ -264,7 +275,7 @@ export class TableLayout exten layoutInfo, children, validRect: layoutInfo.rect.intersection(this.requestedRect), - node: this.collection.body + node: collection.body }; } @@ -287,12 +298,13 @@ export class TableLayout exten } protected buildRow(node: GridNode, x: number, y: number): LayoutNode { + let collection = this.virtualizer!.collection as TableCollection; let rect = new Rect(x, y, 0, 0); let layoutInfo = new LayoutInfo('row', node.key, rect); let children: LayoutNode[] = []; let height = 0; - for (let child of getChildNodes(node, this.collection)) { + for (let child of getChildNodes(node, collection)) { if (child.type === 'cell') { if (x > this.requestedRect.maxX) { // Adjust existing cached layoutInfo to ensure that it is out of view. @@ -316,7 +328,7 @@ export class TableLayout exten this.setChildHeights(children, height); - rect.width = this.layoutNodes.get(this.collection.head?.key ?? 'header').layoutInfo.rect.width; + rect.width = this.layoutNodes.get(collection.head?.key ?? 'header')!.layoutInfo.rect.width; rect.height = height; return { @@ -480,20 +492,20 @@ export class TableLayout exten } private buildPersistedIndices() { - if (this.virtualizer.persistedKeys === this.lastPersistedKeys) { + if (this.virtualizer!.persistedKeys === this.lastPersistedKeys) { return; } - this.lastPersistedKeys = this.virtualizer.persistedKeys; + this.lastPersistedKeys = this.virtualizer!.persistedKeys; this.persistedIndices.clear(); // Build a map of parentKey => indices of children to persist. - for (let key of this.virtualizer.persistedKeys) { + for (let key of this.virtualizer!.persistedKeys) { let layoutInfo = this.layoutNodes.get(key)?.layoutInfo; // Walk up ancestors so parents are also persisted if children are. while (layoutInfo && layoutInfo.parentKey) { - let collectionNode = this.collection.getItem(layoutInfo.key); + let collectionNode = this.virtualizer!.collection.getItem(layoutInfo.key); let indices = this.persistedIndices.get(layoutInfo.parentKey); if (!indices) { // stickyColumnIndices are always persisted along with any cells from persistedKeys. @@ -501,9 +513,8 @@ export class TableLayout exten this.persistedIndices.set(layoutInfo.parentKey, indices); } - let index = this.layoutNodes.get(layoutInfo.key).index; - - if (!indices.includes(index)) { + let index = this.layoutNodes.get(layoutInfo.key)?.index; + if (index != null && !indices.includes(index)) { indices.push(index); } @@ -516,15 +527,15 @@ export class TableLayout exten } } - getDropTargetFromPoint(x: number, y: number, isValidDropTarget: (target: DropTarget) => boolean): DropTarget { - x += this.virtualizer.visibleRect.x; - y += this.virtualizer.visibleRect.y; + getDropTargetFromPoint(x: number, y: number, isValidDropTarget: (target: DropTarget) => boolean): DropTarget | null { + x += this.virtualizer!.visibleRect.x; + y += this.virtualizer!.visibleRect.y; // Custom variation of this.virtualizer.keyAtPoint that ignores body - let key: Key; + let key: Key | null = null; let point = new Point(x, y); let rectAtPoint = new Rect(point.x, point.y, 1, 1); - let layoutInfos = this.virtualizer.layout.getVisibleLayoutInfos(rectAtPoint).filter(info => info.type === 'row'); + let layoutInfos = this.virtualizer!.layout.getVisibleLayoutInfos(rectAtPoint).filter(info => info.type === 'row'); // Layout may return multiple layout infos in the case of // persisted keys, so find the first one that actually intersects. @@ -534,11 +545,15 @@ export class TableLayout exten } } - if (key == null || this.collection.size === 0) { + if (key == null || this.virtualizer!.collection.size === 0) { return {type: 'root'}; } let layoutInfo = this.getLayoutInfo(key); + if (!layoutInfo) { + return null; + } + let rect = layoutInfo.rect; let target: DropTarget = { type: 'item', @@ -566,7 +581,7 @@ export class TableLayout exten getDropTargetLayoutInfo(target: ItemDropTarget): LayoutInfo { let layoutInfo = super.getDropTargetLayoutInfo(target); - layoutInfo.parentKey = this.collection.body.key; + layoutInfo.parentKey = (this.virtualizer!.collection as TableCollection).body.key; return layoutInfo; } } diff --git a/packages/@react-stately/virtualizer/src/Layout.ts b/packages/@react-stately/virtualizer/src/Layout.ts index 1cb27f9b865..d2190d2be6a 100644 --- a/packages/@react-stately/virtualizer/src/Layout.ts +++ b/packages/@react-stately/virtualizer/src/Layout.ts @@ -32,7 +32,7 @@ import {Virtualizer} from './Virtualizer'; */ export abstract class Layout implements LayoutDelegate { /** The Virtualizer the layout is currently attached to. */ - virtualizer: Virtualizer; + virtualizer: Virtualizer | null = null; /** * Returns whether the layout should invalidate in response to @@ -83,11 +83,11 @@ export abstract class Layout implements LayoutDelegat */ getDropTargetLayoutInfo?(target: ItemDropTarget): LayoutInfo; - getItemRect(key: Key): Rect { - return this.getLayoutInfo(key)?.rect; + getItemRect(key: Key): Rect | null { + return this.getLayoutInfo(key)?.rect ?? null; } getVisibleRect(): Rect { - return this.virtualizer.visibleRect; + return this.virtualizer!.visibleRect; } } diff --git a/packages/@react-stately/virtualizer/src/ReusableView.ts b/packages/@react-stately/virtualizer/src/ReusableView.ts index 535c678d279..f033495d901 100644 --- a/packages/@react-stately/virtualizer/src/ReusableView.ts +++ b/packages/@react-stately/virtualizer/src/ReusableView.ts @@ -28,23 +28,25 @@ export class ReusableView { layoutInfo: LayoutInfo | null; /** The content currently being displayed by this view, set by the virtualizer. */ - content: T; + content: T | null; - rendered: V; + rendered: V | null; viewType: string; key: Key; - parent: ReusableView | null; - children: Set>; - reusableViews: Map[]>; + children: Set>; + reusableViews: Map[]>; - constructor(virtualizer: Virtualizer) { + constructor(virtualizer: Virtualizer, viewType: string) { this.virtualizer = virtualizer; this.key = ++KEY; - this.parent = null; + this.viewType = viewType; this.children = new Set(); this.reusableViews = new Map(); + this.layoutInfo = null; + this.content = null; + this.rendered = null; } /** @@ -62,16 +64,14 @@ export class ReusableView { // The cells within a row are removed from their parent in order. If the row is reused, the cells // should be reused in the new row in the same order they were before. let reusable = this.reusableViews.get(reuseType); - let view = reusable?.length > 0 - ? reusable.shift() - : new ReusableView(this.virtualizer); + let view = reusable && reusable.length > 0 + ? reusable.shift()! + : new ChildView(this.virtualizer, this, reuseType); - view.viewType = reuseType; - view.parent = this; return view; } - reuseChild(child: ReusableView) { + reuseChild(child: ChildView) { child.prepareForReuse(); let reusable = this.reusableViews.get(child.viewType); if (!reusable) { @@ -81,3 +81,18 @@ export class ReusableView { reusable.push(child); } } + +export class RootView extends ReusableView { + constructor(virtualizer: Virtualizer) { + super(virtualizer, 'root'); + } +} + +export class ChildView extends ReusableView { + parent: ReusableView; + + constructor(virtualizer: Virtualizer, parent: ReusableView, viewType: string) { + super(virtualizer, viewType); + this.parent = parent; + } +} diff --git a/packages/@react-stately/virtualizer/src/Virtualizer.ts b/packages/@react-stately/virtualizer/src/Virtualizer.ts index 9279ffa8bda..d41dbbe5a1e 100644 --- a/packages/@react-stately/virtualizer/src/Virtualizer.ts +++ b/packages/@react-stately/virtualizer/src/Virtualizer.ts @@ -10,6 +10,7 @@ * governing permissions and limitations under the License. */ +import {ChildView, ReusableView, RootView} from './ReusableView'; import {Collection, Key} from '@react-types/shared'; import {InvalidationContext, Mutable, VirtualizerDelegate, VirtualizerRenderOptions} from './types'; import {isSetEqual} from './utils'; @@ -18,9 +19,14 @@ import {LayoutInfo} from './LayoutInfo'; import {OverscanManager} from './OverscanManager'; import {Point} from './Point'; import {Rect} from './Rect'; -import {ReusableView} from './ReusableView'; import {Size} from './Size'; +interface VirtualizerOptions { + delegate: VirtualizerDelegate, + collection: Collection, + layout: Layout +} + /** * The Virtualizer class renders a scrollable collection of data using customizable layouts. * It supports very large collections by only rendering visible views to the DOM, reusing @@ -55,23 +61,25 @@ export class Virtualizer { /** The set of persisted keys that are always present in the DOM, even if not currently in view. */ readonly persistedKeys: Set; - private _visibleViews: Map>; + private _visibleViews: Map>; private _renderedContent: WeakMap; - private _rootView: ReusableView; + private _rootView: RootView; private _isScrolling: boolean; - private _invalidationContext: InvalidationContext | null; + private _invalidationContext: InvalidationContext; private _overscanManager: OverscanManager; - constructor(delegate: VirtualizerDelegate) { - this.delegate = delegate; + constructor(options: VirtualizerOptions) { + this.delegate = options.delegate; + this.collection = options.collection; + this.layout = options.layout; this.contentSize = new Size; this.visibleRect = new Rect; this.persistedKeys = new Set(); this._visibleViews = new Map(); this._renderedContent = new WeakMap(); - this._rootView = new ReusableView(this); + this._rootView = new RootView(this); this._isScrolling = false; - this._invalidationContext = null; + this._invalidationContext = {}; this._overscanManager = new OverscanManager(); } @@ -86,7 +94,7 @@ export class Virtualizer { for (let k of this.persistedKeys) { while (k != null) { let layoutInfo = this.layout.getLayoutInfo(k); - if (!layoutInfo) { + if (!layoutInfo || layoutInfo.parentKey == null) { break; } @@ -105,7 +113,7 @@ export class Virtualizer { return layoutInfo.parentKey != null ? this._visibleViews.get(layoutInfo.parentKey) : this._rootView; } - private getReusableView(layoutInfo: LayoutInfo): ReusableView { + private getReusableView(layoutInfo: LayoutInfo): ChildView { let parentView = this.getParentView(layoutInfo)!; let view = parentView.getReusableView(layoutInfo.type); view.layoutInfo = layoutInfo; @@ -114,13 +122,15 @@ export class Virtualizer { } private _renderView(reusableView: ReusableView) { - let {type, key, content} = reusableView.layoutInfo; - reusableView.content = content || this.collection.getItem(key); - reusableView.rendered = this._renderContent(type, reusableView.content); + if (reusableView.layoutInfo) { + let {type, key, content} = reusableView.layoutInfo; + reusableView.content = content || this.collection.getItem(key); + reusableView.rendered = this._renderContent(type, reusableView.content); + } } - private _renderContent(type: string, content: T) { - let cached = this._renderedContent.get(content); + private _renderContent(type: string, content: T | null) { + let cached = content != null ? this._renderedContent.get(content) : null; if (cached != null) { return cached; } @@ -196,7 +206,7 @@ export class Virtualizer { private updateSubviews() { let visibleLayoutInfos = this.getVisibleLayoutInfos(); - let removed = new Set>(); + let removed = new Set>(); for (let [key, view] of this._visibleViews) { let layoutInfo = visibleLayoutInfos.get(key); // If a view's parent changed, treat it as a delete and re-create in the new parent. @@ -219,7 +229,9 @@ export class Virtualizer { let item = this.collection.getItem(layoutInfo.key); if (view.content !== item) { - this._renderedContent.delete(view.content); + if (view.content != null) { + this._renderedContent.delete(view.content); + } this._renderView(view); } } @@ -261,7 +273,7 @@ export class Virtualizer { needsLayout = true; } - if (opts.layout !== this.layout) { + if (opts.layout !== this.layout || this.layout.virtualizer !== this) { if (this.layout) { this.layout.virtualizer = null; } diff --git a/packages/@react-stately/virtualizer/src/types.ts b/packages/@react-stately/virtualizer/src/types.ts index bb5fa65cc61..df8c88d55f9 100644 --- a/packages/@react-stately/virtualizer/src/types.ts +++ b/packages/@react-stately/virtualizer/src/types.ts @@ -24,14 +24,14 @@ export interface InvalidationContext { export interface VirtualizerDelegate { setVisibleRect(rect: Rect): void, - renderView(type: string, content: T): V, + renderView(type: string, content: T | null): V, invalidate(ctx: InvalidationContext): void } export interface VirtualizerRenderOptions { layout: Layout, collection: Collection, - persistedKeys?: Set, + persistedKeys?: Set | null, visibleRect: Rect, invalidationContext: InvalidationContext, isScrolling: boolean, diff --git a/packages/@react-stately/virtualizer/src/useVirtualizerState.ts b/packages/@react-stately/virtualizer/src/useVirtualizerState.ts index dff243c91a2..9844e2acdd0 100644 --- a/packages/@react-stately/virtualizer/src/useVirtualizerState.ts +++ b/packages/@react-stately/virtualizer/src/useVirtualizerState.ts @@ -21,7 +21,7 @@ import {useLayoutEffect} from '@react-aria/utils'; import {Virtualizer} from './Virtualizer'; interface VirtualizerProps { - renderView(type: string, content: T): V, + renderView(type: string, content: T | null): V, layout: Layout, collection: Collection, onVisibleRectChange(rect: Rect): void, @@ -45,13 +45,17 @@ export function useVirtualizerState(opts: Virtuali let [invalidationContext, setInvalidationContext] = useState({}); let visibleRectChanged = useRef(false); let [virtualizer] = useState(() => new Virtualizer({ - setVisibleRect(rect) { - setVisibleRect(rect); - visibleRectChanged.current = true; - }, - // TODO: should changing these invalidate the entire cache? - renderView: opts.renderView, - invalidate: setInvalidationContext + collection: opts.collection, + layout: opts.layout, + delegate: { + setVisibleRect(rect) { + setVisibleRect(rect); + visibleRectChanged.current = true; + }, + // TODO: should changing these invalidate the entire cache? + renderView: opts.renderView, + invalidate: setInvalidationContext + } })); // onVisibleRectChange must be called from an effect, not during render. diff --git a/packages/react-aria-components/src/Virtualizer.tsx b/packages/react-aria-components/src/Virtualizer.tsx index 7b9edb2a858..c1c83554888 100644 --- a/packages/react-aria-components/src/Virtualizer.tsx +++ b/packages/react-aria-components/src/Virtualizer.tsx @@ -126,7 +126,7 @@ function renderWrapper( ); let {collection, layout} = reusableView.virtualizer; - let {key, type} = reusableView.content; + let {key, type} = reusableView.content!; if (type === 'item' && renderDropIndicator && layout.getDropTargetLayoutInfo) { rendered = ( @@ -146,7 +146,7 @@ function renderDropIndicatorWrapper( dropPosition: DropPosition, renderDropIndicator: (target: ItemDropTarget) => ReactNode ) { - let target: DropTarget = {type: 'item', key: reusableView.content.key, dropPosition}; + let target: DropTarget = {type: 'item', key: reusableView.content!.key, dropPosition}; let indicator = renderDropIndicator(target); if (indicator) { let layoutInfo = reusableView.virtualizer.layout.getDropTargetLayoutInfo!(target); diff --git a/packages/react-aria-components/test/Table.test.js b/packages/react-aria-components/test/Table.test.js index c3ab8d4a6e4..a2fd27f788f 100644 --- a/packages/react-aria-components/test/Table.test.js +++ b/packages/react-aria-components/test/Table.test.js @@ -873,8 +873,8 @@ describe('Table', () => { ); let rows = getAllByRole('row'); - expect(rows).toHaveLength(8); - expect(rows.map(r => r.textContent)).toEqual(['FooBar', 'Foo 0Bar 0', 'Foo 1Bar 1', 'Foo 2Bar 2', 'Foo 3Bar 3', 'Foo 4Bar 4', 'Foo 5Bar 5', 'Foo 6Bar 6']); + expect(rows).toHaveLength(7); + expect(rows.map(r => r.textContent)).toEqual(['FooBar', 'Foo 0Bar 0', 'Foo 1Bar 1', 'Foo 2Bar 2', 'Foo 3Bar 3', 'Foo 4Bar 4', 'Foo 5Bar 5']); for (let row of rows) { expect(row).toHaveAttribute('aria-rowindex'); } @@ -885,14 +885,14 @@ describe('Table', () => { rows = getAllByRole('row'); expect(rows).toHaveLength(8); - expect(rows.map(r => r.textContent)).toEqual(['FooBar', 'Foo 8Bar 8', 'Foo 9Bar 9', 'Foo 10Bar 10', 'Foo 11Bar 11', 'Foo 12Bar 12', 'Foo 13Bar 13', 'Foo 14Bar 14']); + expect(rows.map(r => r.textContent)).toEqual(['FooBar', 'Foo 7Bar 7', 'Foo 8Bar 8', 'Foo 9Bar 9', 'Foo 10Bar 10', 'Foo 11Bar 11', 'Foo 12Bar 12', 'Foo 13Bar 13']); await user.tab(); await user.keyboard('{End}'); rows = getAllByRole('row'); expect(rows).toHaveLength(9); - expect(rows.map(r => r.textContent)).toEqual(['FooBar', 'Foo 8Bar 8', 'Foo 9Bar 9', 'Foo 10Bar 10', 'Foo 11Bar 11', 'Foo 12Bar 12', 'Foo 13Bar 13', 'Foo 14Bar 14', 'Foo 49Bar 49']); + expect(rows.map(r => r.textContent)).toEqual(['FooBar', 'Foo 7Bar 7', 'Foo 8Bar 8', 'Foo 9Bar 9', 'Foo 10Bar 10', 'Foo 11Bar 11', 'Foo 12Bar 12', 'Foo 13Bar 13', 'Foo 49Bar 49']); }); describe('drag and drop', () => { From d044ab6aba53a74a5128aeb4ae7469624b0e44a3 Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 14 Nov 2024 12:45:23 +1100 Subject: [PATCH 14/66] aria menus more --- .../menu/src/useSafelyMouseToSubmenu.ts | 2 +- packages/@react-aria/menu/src/useSubmenuTrigger.ts | 8 ++++---- .../@react-spectrum/menu/src/SubmenuTrigger.tsx | 2 +- packages/@react-spectrum/menu/src/context.ts | 6 +++--- .../menu/src/useSubmenuTriggerState.ts | 14 +++++++------- 5 files changed, 16 insertions(+), 16 deletions(-) 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..8ccb60cb2c9 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': @@ -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-spectrum/menu/src/SubmenuTrigger.tsx b/packages/@react-spectrum/menu/src/SubmenuTrigger.tsx index 94f5bdaf35b..a5a7420e422 100644 --- a/packages/@react-spectrum/menu/src/SubmenuTrigger.tsx +++ b/packages/@react-spectrum/menu/src/SubmenuTrigger.tsx @@ -40,7 +40,7 @@ function SubmenuTrigger(props: SubmenuTriggerProps) { } = props; let [menuTrigger, menu] = React.Children.toArray(children); - let {popoverContainer, trayContainerRef, menu: parentMenuRef, submenu: menuRef, rootMenuTriggerState} = useMenuStateContext(); + let {popoverContainer, trayContainerRef, menu: parentMenuRef, submenu: menuRef, rootMenuTriggerState} = useMenuStateContext()!; let submenuTriggerState = useSubmenuTriggerState({triggerKey: targetKey}, rootMenuTriggerState); let {submenuTriggerProps, submenuProps, popoverProps} = useSubmenuTrigger({ parentMenuRef, diff --git a/packages/@react-spectrum/menu/src/context.ts b/packages/@react-spectrum/menu/src/context.ts index e6b092f7b32..8952103d133 100644 --- a/packages/@react-spectrum/menu/src/context.ts +++ b/packages/@react-spectrum/menu/src/context.ts @@ -43,7 +43,7 @@ export interface SubmenuTriggerContextValue extends DOMProps, Pick(undefined); -export function useSubmenuTriggerContext(): SubmenuTriggerContextValue { +export function useSubmenuTriggerContext() { return useContext(SubmenuTriggerContext); } @@ -56,8 +56,8 @@ export interface MenuStateContextValue { 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-stately/menu/src/useSubmenuTriggerState.ts b/packages/@react-stately/menu/src/useSubmenuTriggerState.ts index e04449c6548..2113c5e30de 100644 --- a/packages/@react-stately/menu/src/useSubmenuTriggerState.ts +++ b/packages/@react-stately/menu/src/useSubmenuTriggerState.ts @@ -24,9 +24,9 @@ export interface SubmenuTriggerState extends OverlayTriggerState { /** Whether the submenu is currently open. */ isOpen: boolean, /** Controls which item will be auto focused when the submenu opens. */ - focusStrategy: FocusStrategy | null, + focusStrategy?: FocusStrategy, /** Opens the submenu. */ - open: (focusStrategy?: FocusStrategy | null) => void, + open: (focusStrategy?: FocusStrategy) => void, /** Closes the submenu. */ close: () => void, /** Closes all menus and submenus in the menu tree. */ @@ -34,7 +34,7 @@ export interface SubmenuTriggerState extends OverlayTriggerState { /** The level of the submenu. */ submenuLevel: number, /** Toggles the submenu. */ - toggle: (focusStrategy?: FocusStrategy | null) => void, + toggle: (focusStrategy?: FocusStrategy) => void, /** @private */ setOpen: () => void } @@ -48,19 +48,19 @@ export function useSubmenuTriggerState(props: SubmenuTriggerProps, state: RootMe let {expandedKeysStack, openSubmenu, closeSubmenu, close: closeAll} = state; let [submenuLevel] = useState(expandedKeysStack?.length); let isOpen = useMemo(() => expandedKeysStack[submenuLevel] === triggerKey, [expandedKeysStack, triggerKey, submenuLevel]); - let [focusStrategy, setFocusStrategy] = useState(null); + let [focusStrategy, setFocusStrategy] = useState(undefined); - let open = useCallback((focusStrategy: FocusStrategy = null) => { + let open = useCallback((focusStrategy?: FocusStrategy) => { setFocusStrategy(focusStrategy); openSubmenu(triggerKey, submenuLevel); }, [openSubmenu, submenuLevel, triggerKey]); let close = useCallback(() => { - setFocusStrategy(null); + setFocusStrategy(undefined); closeSubmenu(triggerKey, submenuLevel); }, [closeSubmenu, submenuLevel, triggerKey]); - let toggle = useCallback((focusStrategy: FocusStrategy = null) => { + let toggle = useCallback((focusStrategy?: FocusStrategy) => { setFocusStrategy(focusStrategy); if (isOpen) { close(); From 1426b32f33493333a95ff3ab97eae621929393a7 Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 14 Nov 2024 13:03:21 +1100 Subject: [PATCH 15/66] last of aria menu and overlay --- .../@react-aria/menu/stories/useMenu.stories.tsx | 8 ++++---- .../overlays/test/calculatePosition.test.ts | 2 +- .../overlays/test/useOverlayPosition.test.tsx | 14 ++++++++++---- 3 files changed, 15 insertions(+), 9 deletions(-) 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/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..93518f24e30 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 ( @@ -213,7 +213,7 @@ 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(() => { @@ -222,6 +222,7 @@ describe('useOverlayPosition with positioned container', () => { stubs.push( jest.spyOn(window.HTMLElement.prototype, 'offsetParent', 'get').mockImplementation(function () { // Make sure container is is the offsetParent of overlay + // @ts-ignore if (this.attributes.getNamedItem('data-testid')?.value === 'overlay') { return document.querySelector('[data-testid="container"]'); } else { @@ -229,15 +230,20 @@ describe('useOverlayPosition with positioned container', () => { } }), jest.spyOn(window.HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(function () { + // @ts-ignore if (this.attributes.getNamedItem('data-testid')?.value === 'container') { // Say, overlay is positioned somewhere + // @ts-ignore + let real = realGetBoundingClientRect.apply(this); return { + ...real, top: 150, left: 0, width: 400, height: 400 }; } else { + // @ts-ignore return realGetBoundingClientRect.apply(this); } }), From 23c5d693104162629884a56ac7fe0ad1c06582e0 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Wed, 13 Nov 2024 18:12:45 -0800 Subject: [PATCH 16/66] Stately table --- .../@react-stately/collections/src/types.ts | 2 +- .../@react-stately/grid/src/GridCollection.ts | 33 ++++++++++--------- packages/@react-stately/table/src/Cell.ts | 4 +-- packages/@react-stately/table/src/Column.ts | 4 +-- packages/@react-stately/table/src/Row.ts | 4 +-- .../table/src/TableColumnLayout.ts | 4 +-- .../@react-stately/table/src/TableHeader.ts | 4 +-- .../@react-stately/table/src/TableUtils.ts | 23 +++++++++---- .../table/src/useTableColumnResizeState.ts | 2 +- .../@react-stately/table/src/useTableState.ts | 6 ++-- .../table/src/useTreeGridState.ts | 23 ++++++------- packages/@react-types/grid/src/index.d.ts | 2 +- .../@react-types/shared/src/collections.d.ts | 2 +- 13 files changed, 63 insertions(+), 50 deletions(-) diff --git a/packages/@react-stately/collections/src/types.ts b/packages/@react-stately/collections/src/types.ts index 73436def7c8..5d567854d26 100644 --- a/packages/@react-stately/collections/src/types.ts +++ b/packages/@react-stately/collections/src/types.ts @@ -27,5 +27,5 @@ export interface PartialNode { hasChildNodes?: boolean, childNodes?: () => IterableIterator>, props?: any, - shouldInvalidate?: (context: unknown) => boolean + shouldInvalidate?: (context: any) => boolean } diff --git a/packages/@react-stately/grid/src/GridCollection.ts b/packages/@react-stately/grid/src/GridCollection.ts index aef3c489268..daeb09a4771 100644 --- a/packages/@react-stately/grid/src/GridCollection.ts +++ b/packages/@react-stately/grid/src/GridCollection.ts @@ -24,7 +24,7 @@ export class GridCollection implements IGridCollection { columnCount: number; rows: GridNode[]; - constructor(opts?: GridCollectionOptions) { + constructor(opts: GridCollectionOptions) { this.keyMap = new Map(); this.columnCount = opts?.columnCount; this.rows = []; @@ -41,7 +41,7 @@ export class GridCollection implements IGridCollection { this.keyMap.set(node.key, node); let childKeys = new Set(); - let last: GridNode; + let last: GridNode | null = null; for (let child of node.childNodes) { if (child.type === 'cell' && child.parentKey == null) { // if child is a cell parent key isn't already established by the collection, match child node to parent row @@ -83,19 +83,20 @@ export class GridCollection implements IGridCollection { } }; - let last: GridNode; - opts.items.forEach((node, i) => { - let rowNode = { - level: 0, - key: 'row-' + i, - type: 'row', - value: undefined, + let last: GridNode | null = null; + for (let [i, node] of opts.items.entries()) { + let rowNode: GridNode = { + ...node, + level: node.level ?? 0, + key: node.key ?? 'row-' + i, + type: node.type ?? 'row', + value: node.value ?? null, hasChildNodes: true, childNodes: [...node.childNodes], - rendered: undefined, - textValue: undefined, - ...node - } as GridNode; + rendered: node.rendered, + textValue: node.textValue ?? '', + index: node.index ?? i + }; if (last) { last.nextKey = rowNode.key; @@ -108,7 +109,7 @@ export class GridCollection implements IGridCollection { visit(rowNode); last = rowNode; - }); + } if (last) { last.nextKey = null; @@ -137,11 +138,11 @@ export class GridCollection implements IGridCollection { return node ? node.nextKey ?? null : null; } - getFirstKey() { + getFirstKey(): Key | null { return [...this.rows][0]?.key; } - getLastKey() { + getLastKey(): Key | null { let rows = [...this.rows]; return rows[rows.length - 1]?.key; } diff --git a/packages/@react-stately/table/src/Cell.ts b/packages/@react-stately/table/src/Cell.ts index ed193bae470..93128b8717d 100644 --- a/packages/@react-stately/table/src/Cell.ts +++ b/packages/@react-stately/table/src/Cell.ts @@ -11,10 +11,10 @@ */ import {CellProps} from '@react-types/table'; -import {JSX, ReactElement} from 'react'; +import {JSX} from 'react'; import {PartialNode} from '@react-stately/collections'; -function Cell(props: CellProps): ReactElement { // eslint-disable-line @typescript-eslint/no-unused-vars +function Cell(props: CellProps): JSX.Element | null { // eslint-disable-line @typescript-eslint/no-unused-vars return null; } diff --git a/packages/@react-stately/table/src/Column.ts b/packages/@react-stately/table/src/Column.ts index b6aa07d669f..bb96257cd42 100644 --- a/packages/@react-stately/table/src/Column.ts +++ b/packages/@react-stately/table/src/Column.ts @@ -16,7 +16,7 @@ import {GridNode} from '@react-types/grid'; import {PartialNode} from '@react-stately/collections'; import React, {JSX, ReactElement} from 'react'; -function Column(props: ColumnProps): ReactElement { // eslint-disable-line @typescript-eslint/no-unused-vars +function Column(props: ColumnProps): JSX.Element | null { // eslint-disable-line @typescript-eslint/no-unused-vars return null; } @@ -28,7 +28,7 @@ Column.getCollectionNode = function* getCollectionNode(props: ColumnProps, let fullNodes = yield { type: 'column', - hasChildNodes: !!childColumns || (title && React.Children.count(children) > 0), + hasChildNodes: !!childColumns || (!!title && React.Children.count(children) > 0), rendered, textValue, props, diff --git a/packages/@react-stately/table/src/Row.ts b/packages/@react-stately/table/src/Row.ts index 5f864a2ecd6..54a0b5b7a03 100644 --- a/packages/@react-stately/table/src/Row.ts +++ b/packages/@react-stately/table/src/Row.ts @@ -12,10 +12,10 @@ import {CollectionBuilderContext} from './useTableState'; import {PartialNode} from '@react-stately/collections'; -import React, {JSX, ReactElement} from 'react'; +import React, {JSX} from 'react'; import {RowProps} from '@react-types/table'; -function Row(props: RowProps): ReactElement { // eslint-disable-line @typescript-eslint/no-unused-vars +function Row(props: RowProps): JSX.Element | null { // eslint-disable-line @typescript-eslint/no-unused-vars return null; } diff --git a/packages/@react-stately/table/src/TableColumnLayout.ts b/packages/@react-stately/table/src/TableColumnLayout.ts index 1e2ba6f8a65..42a1299e17c 100644 --- a/packages/@react-stately/table/src/TableColumnLayout.ts +++ b/packages/@react-stately/table/src/TableColumnLayout.ts @@ -54,7 +54,7 @@ export class TableColumnLayout { if (uncontrolledColumns.has(col.key)) { return [col.key, uncontrolledWidths.get(col.key)]; } else { - return [col.key, controlledColumns.get(col.key).props.width]; + return [col.key, controlledColumns.get(col.key)!.props.width]; } })); } @@ -91,7 +91,7 @@ export class TableColumnLayout { freeze = false; } else if (freeze) { // freeze columns to the left to their previous pixel value - newWidths.set(column.key, prevColumnWidths.get(column.key)); + newWidths.set(column.key, prevColumnWidths.get(column.key) ?? 0); } else { newWidths.set(column.key, column.props.width ?? uncontrolledWidths.get(column.key)); } diff --git a/packages/@react-stately/table/src/TableHeader.ts b/packages/@react-stately/table/src/TableHeader.ts index a2d982c568d..9326c20c4b9 100644 --- a/packages/@react-stately/table/src/TableHeader.ts +++ b/packages/@react-stately/table/src/TableHeader.ts @@ -12,10 +12,10 @@ import {CollectionBuilderContext} from './useTableState'; import {PartialNode} from '@react-stately/collections'; -import React, {JSX, ReactElement} from 'react'; +import React, {JSX} from 'react'; import {TableHeaderProps} from '@react-types/table'; -function TableHeader(props: TableHeaderProps): ReactElement { // eslint-disable-line @typescript-eslint/no-unused-vars +function TableHeader(props: TableHeaderProps): JSX.Element | null { // eslint-disable-line @typescript-eslint/no-unused-vars return null; } diff --git a/packages/@react-stately/table/src/TableUtils.ts b/packages/@react-stately/table/src/TableUtils.ts index 4d359ebe16a..346251ccffc 100644 --- a/packages/@react-stately/table/src/TableUtils.ts +++ b/packages/@react-stately/table/src/TableUtils.ts @@ -44,7 +44,7 @@ export function parseStaticWidth(width: number | string, tableWidth: number): nu } -export function getMaxWidth(maxWidth: number | string, tableWidth: number): number { +export function getMaxWidth(maxWidth: number | string | null | undefined, tableWidth: number): number { return maxWidth != null ? parseStaticWidth(maxWidth, tableWidth) : Number.MAX_SAFE_INTEGER; @@ -63,7 +63,18 @@ export interface IColumn { maxWidth?: number | string, width?: number | string, defaultWidth?: number | string, - key?: Key + key: Key +} + +interface FlexItem { + frozen: boolean, + baseSize: number, + hypotheticalMainSize: number, + min: number, + max: number, + flex: number, + targetMainSize: number, + violation: number } /** @@ -93,12 +104,12 @@ export interface IColumn { */ export function calculateColumnSizes(availableWidth: number, columns: IColumn[], changedColumns: Map, getDefaultWidth, getDefaultMinWidth) { let hasNonFrozenItems = false; - let flexItems = columns.map((column, index) => { + let flexItems: FlexItem[] = columns.map((column, index) => { let width = changedColumns.get(column.key) != null ? changedColumns.get(column.key) : column.width ?? column.defaultWidth ?? getDefaultWidth?.(index) ?? '1fr'; let frozen = false; let baseSize = 0; let flex = 0; - let targetMainSize = null; + let targetMainSize = 0; if (isStatic(width)) { baseSize = parseStaticWidth(width, availableWidth); frozen = true; @@ -232,7 +243,7 @@ export function calculateColumnSizes(availableWidth: number, columns: IColumn[], return cascadeRounding(flexItems); } -function cascadeRounding(flexItems): number[] { +function cascadeRounding(flexItems: FlexItem[]): number[] { /* Given an array of floats that sum to an integer, this rounds the floats and returns an array of integers with the same sum. @@ -240,7 +251,7 @@ function cascadeRounding(flexItems): number[] { let fpTotal = 0; let intTotal = 0; - let roundedArray = []; + let roundedArray: number[] = []; flexItems.forEach(function (item) { let float = item.targetMainSize; let integer = Math.round(float + fpTotal) - intTotal; diff --git a/packages/@react-stately/table/src/useTableColumnResizeState.ts b/packages/@react-stately/table/src/useTableColumnResizeState.ts index 6bc80951a2a..7ba050cb1e8 100644 --- a/packages/@react-stately/table/src/useTableColumnResizeState.ts +++ b/packages/@react-stately/table/src/useTableColumnResizeState.ts @@ -108,7 +108,7 @@ export function useTableColumnResizeState(props: TableColumnResizeStateProps< let updateResizedColumns = useCallback((key: Key, width: number): Map => { let newSizes = columnLayout.resizeColumnWidth(state.collection, uncontrolledWidths, key, width); - let map = new Map(Array.from(uncontrolledColumns).map(([key]) => [key, newSizes.get(key)])); + let map = new Map(Array.from(uncontrolledColumns).map(([key]) => [key, newSizes.get(key)!])); map.set(key, width); setUncontrolledWidths(map); return newSizes; diff --git a/packages/@react-stately/table/src/useTableState.ts b/packages/@react-stately/table/src/useTableState.ts index ca2448702b1..0addeb10a35 100644 --- a/packages/@react-stately/table/src/useTableState.ts +++ b/packages/@react-stately/table/src/useTableState.ts @@ -24,7 +24,7 @@ export interface TableState extends GridState> { /** Whether the row selection checkboxes should be displayed. */ showSelectionCheckboxes: boolean, /** The current sorted column and direction. */ - sortDescriptor: SortDescriptor, + sortDescriptor: SortDescriptor | null, /** Calls the provided onSortChange handler with the provided column key and sort direction. */ sort(columnKey: Key, direction?: 'ascending' | 'descending'): void, /** Whether keyboard navigation is disabled, such as when the arrow keys should be handled by a component within a cell. */ @@ -94,11 +94,11 @@ export function useTableState(props: TableStateProps): Tabl disabledKeys, selectionManager, showSelectionCheckboxes: props.showSelectionCheckboxes || false, - sortDescriptor: props.sortDescriptor, + sortDescriptor: props.sortDescriptor ?? null, isKeyboardNavigationDisabled: collection.size === 0 || isKeyboardNavigationDisabled, setKeyboardNavigationDisabled, sort(columnKey: Key, direction?: 'ascending' | 'descending') { - props.onSortChange({ + props.onSortChange?.({ column: columnKey, direction: direction ?? (props.sortDescriptor?.column === columnKey ? OPPOSITE_SORT_DIRECTION[props.sortDescriptor.direction] diff --git a/packages/@react-stately/table/src/useTreeGridState.ts b/packages/@react-stately/table/src/useTreeGridState.ts index e9027bb24f9..3e6e3247927 100644 --- a/packages/@react-stately/table/src/useTreeGridState.ts +++ b/packages/@react-stately/table/src/useTreeGridState.ts @@ -140,11 +140,11 @@ function generateTreeGridCollection(nodes, opts: TreeGridCollectionOptions): expandedKeys = new Set() } = opts; - let body: GridNode; - let flattenedRows = []; + let body: GridNode | null = null; + let flattenedRows: GridNode[] = []; let columnCount = 0; let userColumnCount = 0; - let originalColumns = []; + let originalColumns: GridNode[] = []; let keyMap = new Map(); if (opts?.showSelectionCheckboxes) { @@ -155,7 +155,7 @@ function generateTreeGridCollection(nodes, opts: TreeGridCollectionOptions): columnCount++; } - let topLevelRows = []; + let topLevelRows: GridNode[] = []; let visit = (node: GridNode) => { switch (node.type) { case 'body': @@ -183,6 +183,7 @@ function generateTreeGridCollection(nodes, opts: TreeGridCollectionOptions): } visit(node); } + columnCount += userColumnCount; // Update each grid node in the treegrid table with values specific to a treegrid structure. Also store a set of flattened row nodes for TableCollection to consume @@ -192,7 +193,7 @@ function generateTreeGridCollection(nodes, opts: TreeGridCollectionOptions): // to TableCollection. Index, level, and parent keys are all changed to reflect a flattened row structure rather than the treegrid structure // values automatically calculated via CollectionBuilder if (node.type === 'item') { - let childNodes = []; + let childNodes: GridNode[] = []; for (let child of node.childNodes) { if (child.type === 'cell') { let cellClone = {...child}; @@ -202,7 +203,7 @@ function generateTreeGridCollection(nodes, opts: TreeGridCollectionOptions): childNodes.push({...cellClone}); } } - let clone = {...node, childNodes: childNodes, parentKey: body.key, level: 1, index: globalRowCount++}; + let clone: GridNode = {...node, childNodes: childNodes, parentKey: body!.key, level: 1, index: globalRowCount++}; flattenedRows.push(clone); } @@ -218,7 +219,7 @@ function generateTreeGridCollection(nodes, opts: TreeGridCollectionOptions): Object.assign(node, newProps); keyMap.set(node.key, node); - let lastNode: GridNode; + let lastNode: GridNode | null = null; let rowIndex = 0; for (let child of node.childNodes) { if (!(child.type === 'item' && expandedKeys !== 'all' && !expandedKeys.has(node.key))) { @@ -250,8 +251,8 @@ function generateTreeGridCollection(nodes, opts: TreeGridCollectionOptions): } }; - let last: GridNode; - topLevelRows.forEach((node: GridNode, i) => { + let last: GridNode | null = null; + for (let [i, node] of topLevelRows.entries()) { visitNode(node as GridNode, i); if (last) { @@ -262,7 +263,7 @@ function generateTreeGridCollection(nodes, opts: TreeGridCollectionOptions): } last = node; - }); + } if (last) { last.nextKey = null; @@ -272,6 +273,6 @@ function generateTreeGridCollection(nodes, opts: TreeGridCollectionOptions): keyMap, userColumnCount, flattenedRows, - tableNodes: [...originalColumns, {...body, childNodes: flattenedRows}] + tableNodes: [...originalColumns, {...body!, childNodes: flattenedRows}] }; } diff --git a/packages/@react-types/grid/src/index.d.ts b/packages/@react-types/grid/src/index.d.ts index e56c3480bba..7b12b7fb39e 100644 --- a/packages/@react-types/grid/src/index.d.ts +++ b/packages/@react-types/grid/src/index.d.ts @@ -19,7 +19,7 @@ export interface GridCollection extends Collection> { rows: GridNode[] } -export interface GridRow { +export interface GridRow extends Partial> { key?: Key, type: string, childNodes: Iterable> diff --git a/packages/@react-types/shared/src/collections.d.ts b/packages/@react-types/shared/src/collections.d.ts index b0f10478c51..41fd674aefa 100644 --- a/packages/@react-types/shared/src/collections.d.ts +++ b/packages/@react-types/shared/src/collections.d.ts @@ -218,7 +218,7 @@ export interface Node { /** Additional properties specific to a particular node type. */ props?: any, /** @private */ - shouldInvalidate?: (context: unknown) => boolean, + shouldInvalidate?: (context: any) => boolean, /** A function that renders this node to a React Element in the DOM. */ render?: (node: Node) => ReactElement } From 48690101a02360c7afac4a6b7270f2bd21121c7f Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Wed, 13 Nov 2024 18:17:44 -0800 Subject: [PATCH 17/66] More table --- .../@react-stately/table/src/TableBody.ts | 4 +- .../table/src/TableCollection.ts | 42 ++++++++++--------- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/packages/@react-stately/table/src/TableBody.ts b/packages/@react-stately/table/src/TableBody.ts index 513ca04126a..a3d1fa8604c 100644 --- a/packages/@react-stately/table/src/TableBody.ts +++ b/packages/@react-stately/table/src/TableBody.ts @@ -11,10 +11,10 @@ */ import {PartialNode} from '@react-stately/collections'; -import React, {JSX, ReactElement} from 'react'; +import React, {JSX} from 'react'; import {TableBodyProps} from '@react-types/table'; -function TableBody(props: TableBodyProps): ReactElement { // eslint-disable-line @typescript-eslint/no-unused-vars +function TableBody(props: TableBodyProps): JSX.Element | null { // eslint-disable-line @typescript-eslint/no-unused-vars return null; } diff --git a/packages/@react-stately/table/src/TableCollection.ts b/packages/@react-stately/table/src/TableCollection.ts index 12d8f5cb7fd..e4fb89e67c2 100644 --- a/packages/@react-stately/table/src/TableCollection.ts +++ b/packages/@react-stately/table/src/TableCollection.ts @@ -40,7 +40,7 @@ export function buildHeaderRows(keyMap: Map>, columnNodes: G let col = [column]; while (parentKey) { - let parent: GridNode = keyMap.get(parentKey); + let parent: GridNode | undefined = keyMap.get(parentKey); if (!parent) { break; } @@ -50,6 +50,7 @@ export function buildHeaderRows(keyMap: Map>, columnNodes: G // than the previous column, than we need to shift the parent // in the previous column so it's level with the current column. if (seen.has(parent)) { + parent.colspan ??= 0; parent.colspan++; let {column, index} = seen.get(parent); @@ -82,7 +83,7 @@ export function buildHeaderRows(keyMap: Map>, columnNodes: G } let maxLength = Math.max(...columns.map(c => c.length)); - let headerRows = Array(maxLength).fill(0).map(() => []); + let headerRows: GridNode[][] = Array(maxLength).fill(0).map(() => []); // Convert columns into rows. let colIndex = 0; @@ -92,7 +93,7 @@ export function buildHeaderRows(keyMap: Map>, columnNodes: G if (item) { // Fill the space up until the current column with a placeholder let row = headerRows[i]; - let rowLength = row.reduce((p, c) => p + c.colspan, 0); + let rowLength = row.reduce((p, c) => p + (c.colspan ?? 1), 0); if (rowLength < colIndex) { let placeholder: GridNode = { type: 'placeholder', @@ -104,7 +105,7 @@ export function buildHeaderRows(keyMap: Map>, columnNodes: G level: i, hasChildNodes: false, childNodes: [], - textValue: null + textValue: '' }; // eslint-disable-next-line max-depth @@ -135,7 +136,7 @@ export function buildHeaderRows(keyMap: Map>, columnNodes: G // Add placeholders at the end of each row that is shorter than the maximum let i = 0; for (let row of headerRows) { - let rowLength = row.reduce((p, c) => p + c.colspan, 0); + let rowLength = row.reduce((p, c) => p + (c.colspan ?? 1), 0); if (rowLength < columnNodes.length) { let placeholder: GridNode = { type: 'placeholder', @@ -147,7 +148,7 @@ export function buildHeaderRows(keyMap: Map>, columnNodes: G level: i, hasChildNodes: false, childNodes: [], - textValue: null, + textValue: '', prevKey: row[row.length - 1].key }; @@ -167,7 +168,7 @@ export function buildHeaderRows(keyMap: Map>, columnNodes: G level: 0, hasChildNodes: true, childNodes, - textValue: null + textValue: '' }; return row; @@ -181,9 +182,9 @@ export class TableCollection extends GridCollection implements ITableColle body: GridNode; _size: number = 0; - constructor(nodes: Iterable>, prev?: ITableCollection, opts?: GridCollectionOptions) { + constructor(nodes: Iterable>, prev?: ITableCollection | null, opts?: GridCollectionOptions) { let rowHeaderColumnKeys: Set = new Set(); - let body: GridNode; + let body: GridNode | null = null; let columns: GridNode[] = []; // Add cell for selection checkboxes if needed. if (opts?.showSelectionCheckboxes) { @@ -225,7 +226,7 @@ export class TableCollection extends GridCollection implements ITableColle columns.unshift(rowHeaderColumn); } - let rows = []; + let rows: GridNode[] = []; let columnKeyMap = new Map(); let visit = (node: GridNode) => { switch (node.type) { @@ -268,13 +269,16 @@ export class TableCollection extends GridCollection implements ITableColle }); this.columns = columns; this.rowHeaderColumnKeys = rowHeaderColumnKeys; - this.body = body; + this.body = body!; this.headerRows = headerRows; - this._size = [...body.childNodes].length; + this._size = [...body!.childNodes].length; // Default row header column to the first one. if (this.rowHeaderColumnKeys.size === 0) { - this.rowHeaderColumnKeys.add(this.columns.find(column => !column.props?.isDragButtonCell && !column.props?.isSelectionCell).key); + let col = this.columns.find(column => !column.props?.isDragButtonCell && !column.props?.isSelectionCell); + if (col) { + this.rowHeaderColumnKeys.add(col.key); + } } } @@ -292,24 +296,24 @@ export class TableCollection extends GridCollection implements ITableColle getKeyBefore(key: Key) { let node = this.keyMap.get(key); - return node ? node.prevKey : null; + return node?.prevKey ?? null; } getKeyAfter(key: Key) { let node = this.keyMap.get(key); - return node ? node.nextKey : null; + return node?.nextKey ?? null; } getFirstKey() { - return getFirstItem(this.body.childNodes)?.key; + return getFirstItem(this.body.childNodes)?.key ?? null; } getLastKey() { - return getLastItem(this.body.childNodes)?.key; + return getLastItem(this.body.childNodes)?.key ?? null; } getItem(key: Key) { - return this.keyMap.get(key); + return this.keyMap.get(key) ?? null; } at(idx: number) { @@ -339,7 +343,7 @@ export class TableCollection extends GridCollection implements ITableColle // Otherwise combine the text of each of the row header columns. let rowHeaderColumnKeys = this.rowHeaderColumnKeys; if (rowHeaderColumnKeys) { - let text = []; + let text: string[] = []; for (let cell of row.childNodes) { let column = this.columns[cell.index]; if (rowHeaderColumnKeys.has(column.key) && cell.textValue) { From fe6085d3c348afec06fd1d8f2fbbde0faf6f5ff2 Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 14 Nov 2024 13:26:05 +1100 Subject: [PATCH 18/66] delete pagination, aria select --- packages/@react-aria/pagination/README.md | 3 - packages/@react-aria/pagination/index.ts | 13 -- .../@react-aria/pagination/intl/ar-AE.json | 4 - .../@react-aria/pagination/intl/bg-BG.json | 4 - .../@react-aria/pagination/intl/cs-CZ.json | 4 - .../@react-aria/pagination/intl/da-DK.json | 4 - .../@react-aria/pagination/intl/de-DE.json | 4 - .../@react-aria/pagination/intl/el-GR.json | 4 - .../@react-aria/pagination/intl/en-US.json | 4 - .../@react-aria/pagination/intl/es-ES.json | 4 - .../@react-aria/pagination/intl/et-EE.json | 4 - .../@react-aria/pagination/intl/fi-FI.json | 4 - .../@react-aria/pagination/intl/fr-FR.json | 4 - .../@react-aria/pagination/intl/he-IL.json | 4 - .../@react-aria/pagination/intl/hr-HR.json | 4 - .../@react-aria/pagination/intl/hu-HU.json | 4 - packages/@react-aria/pagination/intl/index.js | 53 -------- .../@react-aria/pagination/intl/it-IT.json | 4 - .../@react-aria/pagination/intl/ja-JP.json | 4 - .../@react-aria/pagination/intl/ko-KR.json | 4 - .../@react-aria/pagination/intl/lt-LT.json | 4 - .../@react-aria/pagination/intl/lv-LV.json | 4 - .../@react-aria/pagination/intl/nb-NO.json | 4 - .../@react-aria/pagination/intl/nl-NL.json | 4 - .../@react-aria/pagination/intl/pl-PL.json | 4 - .../@react-aria/pagination/intl/pt-BR.json | 4 - .../@react-aria/pagination/intl/pt-PT.json | 4 - .../@react-aria/pagination/intl/ro-RO.json | 4 - .../@react-aria/pagination/intl/ru-RU.json | 4 - .../@react-aria/pagination/intl/sk-SK.json | 4 - .../@react-aria/pagination/intl/sl-SI.json | 4 - .../@react-aria/pagination/intl/sr-SP.json | 4 - .../@react-aria/pagination/intl/sv-SE.json | 4 - .../@react-aria/pagination/intl/tr-TR.json | 4 - .../@react-aria/pagination/intl/uk-UA.json | 4 - .../@react-aria/pagination/intl/zh-CN.json | 4 - .../@react-aria/pagination/intl/zh-TW.json | 4 - packages/@react-aria/pagination/package.json | 36 ------ packages/@react-aria/pagination/src/index.ts | 12 -- .../pagination/src/usePagination.ts | 73 ----------- .../pagination/test/usePagination.test.js | 121 ------------------ .../@react-aria/select/src/HiddenSelect.tsx | 4 +- packages/@react-aria/select/src/useSelect.ts | 10 +- .../@react-aria/select/stories/example.tsx | 4 +- .../select/test/HiddenSelect.test.tsx | 2 +- packages/@react-spectrum/pagination/README.md | 3 - packages/@react-spectrum/pagination/index.ts | 13 -- .../pagination/intl/ar-AE.json | 3 - .../pagination/intl/bg-BG.json | 3 - .../pagination/intl/cs-CZ.json | 3 - .../pagination/intl/da-DK.json | 3 - .../pagination/intl/de-DE.json | 3 - .../pagination/intl/el-GR.json | 3 - .../pagination/intl/en-US.json | 3 - .../pagination/intl/es-ES.json | 3 - .../pagination/intl/et-EE.json | 3 - .../pagination/intl/fi-FI.json | 3 - .../pagination/intl/fr-FR.json | 3 - .../pagination/intl/he-IL.json | 3 - .../pagination/intl/hr-HR.json | 3 - .../pagination/intl/hu-HU.json | 3 - .../pagination/intl/it-IT.json | 3 - .../pagination/intl/ja-JP.json | 3 - .../pagination/intl/ko-KR.json | 3 - .../pagination/intl/lt-LT.json | 3 - .../pagination/intl/lv-LV.json | 3 - .../pagination/intl/nb-NO.json | 3 - .../pagination/intl/nl-NL.json | 3 - .../pagination/intl/pl-PL.json | 3 - .../pagination/intl/pt-BR.json | 3 - .../pagination/intl/pt-PT.json | 3 - .../pagination/intl/ro-RO.json | 3 - .../pagination/intl/ru-RU.json | 3 - .../pagination/intl/sk-SK.json | 3 - .../pagination/intl/sl-SI.json | 3 - .../pagination/intl/sr-SP.json | 3 - .../pagination/intl/sv-SE.json | 3 - .../pagination/intl/tr-TR.json | 3 - .../pagination/intl/uk-UA.json | 3 - .../pagination/intl/zh-CN.json | 3 - .../pagination/intl/zh-TW.json | 3 - .../@react-spectrum/pagination/package.json | 60 --------- .../pagination/src/Pagination.tsx | 64 --------- .../@react-spectrum/pagination/src/index.ts | 15 --- .../pagination/stories/pagination.stories.js | 31 ----- packages/@react-stately/pagination/README.md | 3 - packages/@react-stately/pagination/index.ts | 13 -- .../@react-stately/pagination/package.json | 36 ------ .../@react-stately/pagination/src/index.ts | 13 -- .../pagination/src/usePaginationState.ts | 66 ---------- .../test/usePaginationState.test.js | 121 ------------------ packages/@react-types/pagination/README.md | 3 - packages/@react-types/pagination/package.json | 21 --- .../@react-types/pagination/src/index.d.ts | 26 ---- 94 files changed, 10 insertions(+), 1047 deletions(-) delete mode 100644 packages/@react-aria/pagination/README.md delete mode 100644 packages/@react-aria/pagination/index.ts delete mode 100644 packages/@react-aria/pagination/intl/ar-AE.json delete mode 100644 packages/@react-aria/pagination/intl/bg-BG.json delete mode 100644 packages/@react-aria/pagination/intl/cs-CZ.json delete mode 100644 packages/@react-aria/pagination/intl/da-DK.json delete mode 100644 packages/@react-aria/pagination/intl/de-DE.json delete mode 100644 packages/@react-aria/pagination/intl/el-GR.json delete mode 100644 packages/@react-aria/pagination/intl/en-US.json delete mode 100644 packages/@react-aria/pagination/intl/es-ES.json delete mode 100644 packages/@react-aria/pagination/intl/et-EE.json delete mode 100644 packages/@react-aria/pagination/intl/fi-FI.json delete mode 100644 packages/@react-aria/pagination/intl/fr-FR.json delete mode 100644 packages/@react-aria/pagination/intl/he-IL.json delete mode 100644 packages/@react-aria/pagination/intl/hr-HR.json delete mode 100644 packages/@react-aria/pagination/intl/hu-HU.json delete mode 100644 packages/@react-aria/pagination/intl/index.js delete mode 100644 packages/@react-aria/pagination/intl/it-IT.json delete mode 100644 packages/@react-aria/pagination/intl/ja-JP.json delete mode 100644 packages/@react-aria/pagination/intl/ko-KR.json delete mode 100644 packages/@react-aria/pagination/intl/lt-LT.json delete mode 100644 packages/@react-aria/pagination/intl/lv-LV.json delete mode 100644 packages/@react-aria/pagination/intl/nb-NO.json delete mode 100644 packages/@react-aria/pagination/intl/nl-NL.json delete mode 100644 packages/@react-aria/pagination/intl/pl-PL.json delete mode 100644 packages/@react-aria/pagination/intl/pt-BR.json delete mode 100644 packages/@react-aria/pagination/intl/pt-PT.json delete mode 100644 packages/@react-aria/pagination/intl/ro-RO.json delete mode 100644 packages/@react-aria/pagination/intl/ru-RU.json delete mode 100644 packages/@react-aria/pagination/intl/sk-SK.json delete mode 100644 packages/@react-aria/pagination/intl/sl-SI.json delete mode 100644 packages/@react-aria/pagination/intl/sr-SP.json delete mode 100644 packages/@react-aria/pagination/intl/sv-SE.json delete mode 100644 packages/@react-aria/pagination/intl/tr-TR.json delete mode 100644 packages/@react-aria/pagination/intl/uk-UA.json delete mode 100644 packages/@react-aria/pagination/intl/zh-CN.json delete mode 100644 packages/@react-aria/pagination/intl/zh-TW.json delete mode 100644 packages/@react-aria/pagination/package.json delete mode 100644 packages/@react-aria/pagination/src/index.ts delete mode 100644 packages/@react-aria/pagination/src/usePagination.ts delete mode 100644 packages/@react-aria/pagination/test/usePagination.test.js delete mode 100644 packages/@react-spectrum/pagination/README.md delete mode 100644 packages/@react-spectrum/pagination/index.ts delete mode 100644 packages/@react-spectrum/pagination/intl/ar-AE.json delete mode 100644 packages/@react-spectrum/pagination/intl/bg-BG.json delete mode 100644 packages/@react-spectrum/pagination/intl/cs-CZ.json delete mode 100644 packages/@react-spectrum/pagination/intl/da-DK.json delete mode 100644 packages/@react-spectrum/pagination/intl/de-DE.json delete mode 100644 packages/@react-spectrum/pagination/intl/el-GR.json delete mode 100644 packages/@react-spectrum/pagination/intl/en-US.json delete mode 100644 packages/@react-spectrum/pagination/intl/es-ES.json delete mode 100644 packages/@react-spectrum/pagination/intl/et-EE.json delete mode 100644 packages/@react-spectrum/pagination/intl/fi-FI.json delete mode 100644 packages/@react-spectrum/pagination/intl/fr-FR.json delete mode 100644 packages/@react-spectrum/pagination/intl/he-IL.json delete mode 100644 packages/@react-spectrum/pagination/intl/hr-HR.json delete mode 100644 packages/@react-spectrum/pagination/intl/hu-HU.json delete mode 100644 packages/@react-spectrum/pagination/intl/it-IT.json delete mode 100644 packages/@react-spectrum/pagination/intl/ja-JP.json delete mode 100644 packages/@react-spectrum/pagination/intl/ko-KR.json delete mode 100644 packages/@react-spectrum/pagination/intl/lt-LT.json delete mode 100644 packages/@react-spectrum/pagination/intl/lv-LV.json delete mode 100644 packages/@react-spectrum/pagination/intl/nb-NO.json delete mode 100644 packages/@react-spectrum/pagination/intl/nl-NL.json delete mode 100644 packages/@react-spectrum/pagination/intl/pl-PL.json delete mode 100644 packages/@react-spectrum/pagination/intl/pt-BR.json delete mode 100644 packages/@react-spectrum/pagination/intl/pt-PT.json delete mode 100644 packages/@react-spectrum/pagination/intl/ro-RO.json delete mode 100644 packages/@react-spectrum/pagination/intl/ru-RU.json delete mode 100644 packages/@react-spectrum/pagination/intl/sk-SK.json delete mode 100644 packages/@react-spectrum/pagination/intl/sl-SI.json delete mode 100644 packages/@react-spectrum/pagination/intl/sr-SP.json delete mode 100644 packages/@react-spectrum/pagination/intl/sv-SE.json delete mode 100644 packages/@react-spectrum/pagination/intl/tr-TR.json delete mode 100644 packages/@react-spectrum/pagination/intl/uk-UA.json delete mode 100644 packages/@react-spectrum/pagination/intl/zh-CN.json delete mode 100644 packages/@react-spectrum/pagination/intl/zh-TW.json delete mode 100644 packages/@react-spectrum/pagination/package.json delete mode 100644 packages/@react-spectrum/pagination/src/Pagination.tsx delete mode 100644 packages/@react-spectrum/pagination/src/index.ts delete mode 100644 packages/@react-spectrum/pagination/stories/pagination.stories.js delete mode 100644 packages/@react-stately/pagination/README.md delete mode 100644 packages/@react-stately/pagination/index.ts delete mode 100644 packages/@react-stately/pagination/package.json delete mode 100644 packages/@react-stately/pagination/src/index.ts delete mode 100644 packages/@react-stately/pagination/src/usePaginationState.ts delete mode 100644 packages/@react-stately/pagination/test/usePaginationState.test.js delete mode 100644 packages/@react-types/pagination/README.md delete mode 100644 packages/@react-types/pagination/package.json delete mode 100644 packages/@react-types/pagination/src/index.d.ts 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 73d7a770b44..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" - }, - "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) { +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( @@ -305,10 +306,12 @@ const ComboBoxButton = React.forwardRef(function ComboBoxButton(props: ComboBoxB
) ); -}); +}; + +const ComboBoxButton = React.forwardRef(_ComboBoxButton); -interface ComboBoxTrayProps extends SpectrumComboBoxProps { - state: ComboBoxState, +interface ComboBoxTrayProps extends SpectrumComboBoxProps { + state: ComboBoxState, overlayProps: HTMLAttributes, loadingIndicator?: ReactElement, onClose: () => void @@ -327,12 +330,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 +356,9 @@ function ComboBoxTray(props: ComboBoxTrayProps) { ); React.useEffect(() => { - focusSafely(inputRef.current); + if (inputRef.current) { + focusSafely(inputRef.current); + } }, []); React.useEffect(() => { @@ -388,7 +393,7 @@ function ComboBoxTray(props: ComboBoxTrayProps) { excludeFromTabOrder onPress={() => { state.setInputValue(''); - inputRef.current.focus(); + inputRef.current?.focus(); }} UNSAFE_className={ classNames( @@ -431,7 +436,7 @@ function ComboBoxTray(props: ComboBoxTrayProps) { return; } - popoverRef.current.focus(); + popoverRef.current?.focus(); }, [inputRef, popoverRef, isTouchDown]); let inputValue = inputProps.value; @@ -454,7 +459,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 +471,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 +496,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); }; From 538101f448b2c280be2fffe1589f5562ea63f3bb Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Wed, 13 Nov 2024 21:28:25 -0800 Subject: [PATCH 35/66] Make useAsyncList sort function have a required sortDescriptor --- packages/@react-stately/data/src/useAsyncList.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/@react-stately/data/src/useAsyncList.ts b/packages/@react-stately/data/src/useAsyncList.ts index 5f0adab2533..89ec81d698b 100644 --- a/packages/@react-stately/data/src/useAsyncList.ts +++ b/packages/@react-stately/data/src/useAsyncList.ts @@ -29,10 +29,10 @@ export interface AsyncListOptions { * An optional function that performs sorting. If not provided, * then `sortDescriptor` is passed to the `load` function. */ - sort?: AsyncListLoadFunction + sort?: AsyncListLoadFunction & {sortDescriptor: SortDescriptor}> } -type AsyncListLoadFunction = (state: AsyncListLoadOptions) => AsyncListStateUpdate | Promise>; +type AsyncListLoadFunction = AsyncListLoadOptions> = (state: S) => AsyncListStateUpdate | Promise>; interface AsyncListLoadOptions { /** The items currently in the list. */ @@ -345,7 +345,7 @@ export function useAsyncList(options: AsyncListOptions): As dispatchFetch({type: 'loadingMore'}, load); }, sort(sortDescriptor: SortDescriptor) { - dispatchFetch({type: 'sorting', sortDescriptor}, sort || load); + dispatchFetch({type: 'sorting', sortDescriptor}, (sort || load) as AsyncListLoadFunction); }, ...createListActions({...options, getKey, cursor: data.cursor}, fn => { dispatch({type: 'update', updater: fn}); From 260224c0a8780d93bfe439682e485c7e0cb32e28 Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 14 Nov 2024 16:38:39 +1100 Subject: [PATCH 36/66] stately fixes --- .../form/src/useFormValidationState.ts | 12 ++++++++--- .../slider/src/useSliderState.ts | 6 +++--- .../@react-stately/toast/src/useToastState.ts | 8 ++++---- .../tooltip/src/useTooltipTriggerState.ts | 20 ++++++++++++------- 4 files changed, 29 insertions(+), 17 deletions(-) diff --git a/packages/@react-stately/form/src/useFormValidationState.ts b/packages/@react-stately/form/src/useFormValidationState.ts index 8eddecfb6cc..4dd785d6111 100644 --- a/packages/@react-stately/form/src/useFormValidationState.ts +++ b/packages/@react-stately/form/src/useFormValidationState.ts @@ -89,10 +89,16 @@ function useFormValidationStateImpl(props: FormValidationProps): FormValid } : null; // Perform custom client side validation. - let clientError: ValidationResult | null = useMemo(() => getValidationResult(runValidate(validate, value)), [validate, value]); + let clientError: ValidationResult | null = useMemo(() => { + if (!validate || value == null) { + return null; + } + let validateErrors = runValidate(validate, value); + return getValidationResult(validateErrors); + }, [validate, value]); if (builtinValidation?.validationDetails.valid) { - builtinValidation = null; + builtinValidation = undefined; } // Get relevant server errors from the form. @@ -217,7 +223,7 @@ function isEqualValidation(a: ValidationResult | null, b: ValidationResult | nul return true; } - return a && b + return !!a && !!b && a.isInvalid === b.isInvalid && a.validationErrors.length === b.validationErrors.length && a.validationErrors.every((a, i) => a === b.validationErrors[i]) diff --git a/packages/@react-stately/slider/src/useSliderState.ts b/packages/@react-stately/slider/src/useSliderState.ts index bacee3e0bff..d2de8e145a6 100644 --- a/packages/@react-stately/slider/src/useSliderState.ts +++ b/packages/@react-stately/slider/src/useSliderState.ts @@ -176,14 +176,14 @@ export function useSliderState(props: SliderStateOp return Math.max(calcPageSize, step); }, [step, maxValue, minValue]); - let restrictValues = useCallback((values: number[]) => values?.map((val, idx) => { + let restrictValues = useCallback((values: number[] | undefined) => values?.map((val, idx) => { let min = idx === 0 ? minValue : values[idx - 1]; let max = idx === values.length - 1 ? maxValue : values[idx + 1]; return snapValueToStep(val, min, max, step); }), [minValue, maxValue, step]); let value = useMemo(() => restrictValues(convertValue(props.value)), [props.value]); - let defaultValue = useMemo(() => restrictValues(convertValue(props.defaultValue) ?? [minValue]), [props.defaultValue, minValue]); + let defaultValue = useMemo(() => restrictValues(convertValue(props.defaultValue) ?? [minValue])!, [props.defaultValue, minValue]); let onChange = createOnChange(props.value, props.defaultValue, props.onChange); let onChangeEnd = createOnChange(props.value, props.defaultValue, props.onChangeEnd); @@ -321,7 +321,7 @@ function replaceIndex(array: T[], index: number, value: T) { return [...array.slice(0, index), value, ...array.slice(index + 1)]; } -function convertValue(value: number | number[]) { +function convertValue(value?: number | number[]) { if (value == null) { return undefined; } diff --git a/packages/@react-stately/toast/src/useToastState.ts b/packages/@react-stately/toast/src/useToastState.ts index cd84a379e36..c51514def42 100644 --- a/packages/@react-stately/toast/src/useToastState.ts +++ b/packages/@react-stately/toast/src/useToastState.ts @@ -120,7 +120,7 @@ export class ToastQueue { ...options, content, key: toastKey, - timer: options.timeout ? new Timer(() => this.close(toastKey), options.timeout) : null + timer: options.timeout ? new Timer(() => this.close(toastKey), options.timeout) : undefined }; let low = 0; @@ -173,7 +173,7 @@ export class ToastQueue { let prevToasts: QueuedToast[] = this.visibleToasts .filter(t => !toasts.some(t2 => t.key === t2.key)) .map(t => ({...t, animation: 'exiting'})); - this.visibleToasts = prevToasts.concat(toasts).sort((a, b) => b.priority - a.priority); + this.visibleToasts = prevToasts.concat(toasts).sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0)); } else if (action === 'close' && this.hasExitAnimation) { // Cause a rerender to happen for exit animation this.visibleToasts = this.visibleToasts.map(t => { @@ -213,7 +213,7 @@ export class ToastQueue { class Timer { private timerId; - private startTime: number; + private startTime: number | null = null; private remaining: number; private callback: () => void; @@ -234,7 +234,7 @@ class Timer { clearTimeout(this.timerId); this.timerId = null; - this.remaining -= Date.now() - this.startTime; + this.remaining -= Date.now() - this.startTime!; } resume() { diff --git a/packages/@react-stately/tooltip/src/useTooltipTriggerState.ts b/packages/@react-stately/tooltip/src/useTooltipTriggerState.ts index 48d6d29bf8e..32227e8ba60 100644 --- a/packages/@react-stately/tooltip/src/useTooltipTriggerState.ts +++ b/packages/@react-stately/tooltip/src/useTooltipTriggerState.ts @@ -33,8 +33,8 @@ export interface TooltipTriggerState { let tooltips = {}; let tooltipId = 0; let globalWarmedUp = false; -let globalWarmUpTimeout = null; -let globalCooldownTimeout = null; +let globalWarmUpTimeout: ReturnType | null = null; +let globalCooldownTimeout: ReturnType | null = null; /** * Manages state for a tooltip trigger. Tracks whether the tooltip is open, and provides @@ -45,7 +45,7 @@ export function useTooltipTriggerState(props: TooltipTriggerProps = {}): Tooltip let {delay = TOOLTIP_DELAY, closeDelay = TOOLTIP_COOLDOWN} = props; let {isOpen, open, close} = useOverlayTriggerState(props); let id = useMemo(() => `${++tooltipId}`, []); - let closeTimeout = useRef>(undefined); + let closeTimeout = useRef | null>(null); let closeCallback = useRef<() => void>(close); let ensureTooltipEntry = () => { @@ -62,7 +62,9 @@ export function useTooltipTriggerState(props: TooltipTriggerProps = {}): Tooltip }; let showTooltip = () => { - clearTimeout(closeTimeout.current); + if (closeTimeout.current) { + clearTimeout(closeTimeout.current); + } closeTimeout.current = null; closeOpenTooltips(); ensureTooltipEntry(); @@ -80,7 +82,9 @@ export function useTooltipTriggerState(props: TooltipTriggerProps = {}): Tooltip let hideTooltip = (immediate?: boolean) => { if (immediate || closeDelay <= 0) { - clearTimeout(closeTimeout.current); + if (closeTimeout.current) { + clearTimeout(closeTimeout.current); + } closeTimeout.current = null; closeCallback.current(); } else if (!closeTimeout.current) { @@ -124,10 +128,12 @@ export function useTooltipTriggerState(props: TooltipTriggerProps = {}): Tooltip closeCallback.current = close; }, [close]); - + useEffect(() => { return () => { - clearTimeout(closeTimeout.current); + if (closeTimeout.current) { + clearTimeout(closeTimeout.current); + } let tooltip = tooltips[id]; if (tooltip) { delete tooltips[id]; From 4aeb49d4d90c107706b345d1845dd25b062cd796 Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 14 Nov 2024 16:43:23 +1100 Subject: [PATCH 37/66] fix toast test --- packages/@react-stately/toast/test/useToastState.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@react-stately/toast/test/useToastState.test.js b/packages/@react-stately/toast/test/useToastState.test.js index b27e9af087a..8c1182cf8f7 100644 --- a/packages/@react-stately/toast/test/useToastState.test.js +++ b/packages/@react-stately/toast/test/useToastState.test.js @@ -36,7 +36,7 @@ describe('useToastState', () => { expect(result.current.visibleToasts[0].content).toBe(newValue[0].content); expect(result.current.visibleToasts[0].animation).toBe('entering'); expect(result.current.visibleToasts[0].timeout).toBe(0); - expect(result.current.visibleToasts[0].timer).toBe(null); + expect(result.current.visibleToasts[0].timer).toBe(undefined); expect(result.current.visibleToasts[0]).toHaveProperty('key'); }); @@ -49,7 +49,7 @@ describe('useToastState', () => { expect(result.current.visibleToasts[0].content).toBe('Test'); expect(result.current.visibleToasts[0].animation).toBe('entering'); expect(result.current.visibleToasts[0].timeout).toBe(5000); - expect(result.current.visibleToasts[0].timer).not.toBe(null); + expect(result.current.visibleToasts[0].timer).not.toBe(undefined); expect(result.current.visibleToasts[0]).toHaveProperty('key'); }); From b5b538627bcb95d84829270f8d6f171a12130be2 Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 14 Nov 2024 17:19:46 +1100 Subject: [PATCH 38/66] rsp layout and stately tree --- .../layout/chromatic/Flex.stories.tsx | 7 ++++--- .../layout/chromatic/Grid.stories.tsx | 7 ++++--- packages/@react-spectrum/layout/src/Grid.tsx | 4 +++- .../layout/stories/Flex.stories.tsx | 7 ++++--- .../layout/stories/Grid.stories.tsx | 9 +++++---- packages/@react-stately/tree/src/TreeCollection.ts | 14 +++++++------- .../tree/stories/useTreeState.stories.tsx | 10 +++++----- 7 files changed, 32 insertions(+), 26 deletions(-) diff --git a/packages/@react-spectrum/layout/chromatic/Flex.stories.tsx b/packages/@react-spectrum/layout/chromatic/Flex.stories.tsx index 2b4386f94b2..5890b377bb5 100644 --- a/packages/@react-spectrum/layout/chromatic/Flex.stories.tsx +++ b/packages/@react-spectrum/layout/chromatic/Flex.stories.tsx @@ -13,6 +13,7 @@ import {Flex} from '@react-spectrum/layout'; import React from 'react'; import {View} from '@react-spectrum/view'; +import { BackgroundColorValue, Responsive } from '@react-types/shared'; let baseColors = [ 'celery', @@ -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..9cac175f7aa 100644 --- a/packages/@react-spectrum/layout/chromatic/Grid.stories.tsx +++ b/packages/@react-spectrum/layout/chromatic/Grid.stories.tsx @@ -13,6 +13,7 @@ import {Grid, repeat} from '@react-spectrum/layout'; import React from 'react'; import {View} from '@react-spectrum/view'; +import { BackgroundColorValue, Responsive } from '@react-types/shared'; let baseColors = [ 'celery', @@ -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..df36e2c5b1d 100644 --- a/packages/@react-spectrum/layout/stories/Flex.stories.tsx +++ b/packages/@react-spectrum/layout/stories/Flex.stories.tsx @@ -13,6 +13,7 @@ import {Flex} from '@react-spectrum/layout'; import React from 'react'; import {View} from '@react-spectrum/view'; +import { BackgroundColorValue, Responsive as TResponsive } from '@react-types/shared'; let baseColors = [ 'celery', @@ -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..f4cf67ea82c 100644 --- a/packages/@react-spectrum/layout/stories/Grid.stories.tsx +++ b/packages/@react-spectrum/layout/stories/Grid.stories.tsx @@ -13,6 +13,7 @@ import {Grid, repeat} from '@react-spectrum/layout'; import React from 'react'; import {View} from '@react-spectrum/view'; +import { BackgroundColorValue, Responsive as TResponsive } from '@react-types/shared'; let baseColors = [ 'celery', @@ -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-stately/tree/src/TreeCollection.ts b/packages/@react-stately/tree/src/TreeCollection.ts index e465bd0366e..83b61609e3a 100644 --- a/packages/@react-stately/tree/src/TreeCollection.ts +++ b/packages/@react-stately/tree/src/TreeCollection.ts @@ -15,8 +15,8 @@ import {Collection, Key, Node} from '@react-types/shared'; export class TreeCollection 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>, {expandedKeys}: {expandedKeys?: Set} = {}) { this.iterable = nodes; @@ -36,7 +36,7 @@ export class TreeCollection implements Collection> { visit(node); } - let last: Node; + let last: Node | null = null; let index = 0; for (let [key, node] of this.keyMap) { if (last) { @@ -58,7 +58,7 @@ export class TreeCollection implements Collection> { last.nextKey = undefined; } - this.lastKey = last?.key; + this.lastKey = last?.key ?? null; } *[Symbol.iterator]() { @@ -75,12 +75,12 @@ export class TreeCollection 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() { @@ -92,7 +92,7 @@ export class TreeCollection 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/tree/stories/useTreeState.stories.tsx b/packages/@react-stately/tree/stories/useTreeState.stories.tsx index 355f3e01275..10b97a15663 100644 --- a/packages/@react-stately/tree/stories/useTreeState.stories.tsx +++ b/packages/@react-stately/tree/stories/useTreeState.stories.tsx @@ -11,7 +11,7 @@ */ import {Item} from '@react-stately/collections'; -import {Key, Node} from '@react-types/shared'; +import {Collection, Key, Node} from '@react-types/shared'; import React, {useMemo, useRef} from 'react'; import {TreeCollection} from '../src/TreeCollection'; import {usePress} from '@react-aria/interactions'; @@ -51,7 +51,7 @@ function TreeExample(props = {}) { function Tree(props) { let state = useTreeState(props); - let ref = useRef(undefined); + let ref = useRef(null); let keyboardDelegate = useMemo(() => new TreeKeyboardDelegate(state.collection, state.disabledKeys), [state.collection, state.disabledKeys]); @@ -71,8 +71,8 @@ function Tree(props) { ); } -function TreeNodes({nodes, state}) { - return Array.from(nodes).map((node: Node) => ( +function TreeNodes({nodes, state}: {nodes: Collection>, state: any}) { + return Array.from(nodes).map(node => ( Date: Thu, 14 Nov 2024 18:49:16 +1100 Subject: [PATCH 39/66] rsp menu --- .../menu/chromatic/Submenu.stories.tsx | 2 +- .../menu/src/ContextualHelpTrigger.tsx | 11 +++++----- packages/@react-spectrum/menu/src/Menu.tsx | 21 ++++++++++++------- .../@react-spectrum/menu/src/MenuItem.tsx | 2 +- .../@react-spectrum/menu/src/MenuSection.tsx | 3 ++- .../@react-spectrum/menu/src/MenuTrigger.tsx | 6 +++--- .../menu/src/SubmenuTrigger.tsx | 12 +++++------ packages/@react-spectrum/menu/src/context.ts | 16 +++++++------- .../menu/stories/Submenu.stories.tsx | 2 +- .../menu/test/SubMenuTrigger.test.tsx | 4 ++-- 10 files changed, 43 insertions(+), 36 deletions(-) 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 1bac3742c23..66d7027e89f 100644 --- a/packages/@react-spectrum/menu/src/ContextualHelpTrigger.tsx +++ b/packages/@react-spectrum/menu/src/ContextualHelpTrigger.tsx @@ -42,8 +42,8 @@ function ContextualHelpTrigger(props: InternalMenuDialogTriggerProps): ReactElem let triggerRef = useRef(null); let popoverRef = useRef(null); - let {popoverContainer, trayContainerRef, rootMenuTriggerState, menu: parentMenuRef, state} = useMenuStateContext(); - let submenuTriggerState = useSubmenuTriggerState({triggerKey: targetKey}, {...rootMenuTriggerState, ...state}); + 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,8 @@ 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'))) { + // @ts-ignore - TODO refs strike again + 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 +144,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 +190,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..4589dc6fb32 100644 --- a/packages/@react-spectrum/menu/src/MenuTrigger.tsx +++ b/packages/@react-spectrum/menu/src/MenuTrigger.tsx @@ -12,7 +12,7 @@ import {classNames, SlotProvider, useDOMRef, useIsMobileDevice} from '@react-spectrum/utils'; import {DOMRef} from '@react-types/shared'; -import {MenuContext} from './context'; +import {MenuContext, MenuContextValue} from './context'; import {Placement} from '@react-types/overlays'; import {Popover, Tray} from '@react-spectrum/overlays'; import {PressResponder} from '@react-aria/interactions'; @@ -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 a5a7420e422..902870bc922 100644 --- a/packages/@react-spectrum/menu/src/SubmenuTrigger.tsx +++ b/packages/@react-spectrum/menu/src/SubmenuTrigger.tsx @@ -33,7 +33,7 @@ interface SubmenuTriggerProps { export interface SpectrumSubmenuTriggerProps extends Omit {} function SubmenuTrigger(props: SubmenuTriggerProps) { - let triggerRef = useRef(undefined); + let triggerRef = useRef(null); let { children, targetKey @@ -41,7 +41,7 @@ function SubmenuTrigger(props: SubmenuTriggerProps) { let [menuTrigger, menu] = React.Children.toArray(children); let {popoverContainer, trayContainerRef, menu: parentMenuRef, submenu: menuRef, rootMenuTriggerState} = useMenuStateContext()!; - let submenuTriggerState = useSubmenuTriggerState({triggerKey: targetKey}, rootMenuTriggerState); + 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 8952103d133..98a13017951 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, MutableRefObject, Ref, 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', @@ -48,11 +48,11 @@ export function useSubmenuTriggerContext() { } 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 } 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'); }); From 8a560f7fad09001ac370cd6924c05c10890ce9e4 Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 14 Nov 2024 19:00:46 +1100 Subject: [PATCH 40/66] rsp overlays --- .../@react-spectrum/overlays/src/Modal.tsx | 19 +++++++++++-------- .../@react-spectrum/overlays/src/Popover.tsx | 19 ++++++++++--------- .../@react-spectrum/overlays/src/Tray.tsx | 13 +++++++------ 3 files changed, 28 insertions(+), 23 deletions(-) diff --git a/packages/@react-spectrum/overlays/src/Modal.tsx b/packages/@react-spectrum/overlays/src/Modal.tsx index 02b8c7f25b1..35c79b07112 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, MutableRefObject, 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 = (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} @@ -104,7 +106,8 @@ let ModalWrapper = forwardRef(function (props: ModalWrapperProps, ref: RefObject
); -}); +}; +let ModalWrapper = forwardRef(_ModalWrapper); let _Modal = forwardRef(Modal); export {_Modal as Modal}; 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} From ea62db8744150f52ec0aa0bad3075381c155f47f Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 14 Nov 2024 19:10:10 +1100 Subject: [PATCH 41/66] rsp picker --- packages/@react-spectrum/picker/src/Picker.tsx | 10 +++++----- .../@react-spectrum/picker/stories/Picker.stories.tsx | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) 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}} From fe0874d604b3c6a3ed2cccb4d7e0b3a5be7e015f Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 14 Nov 2024 20:11:45 +1100 Subject: [PATCH 42/66] more rsp --- .../autocomplete/src/SearchAutocomplete.tsx | 2 +- .../buttongroup/src/ButtonGroup.tsx | 2 +- .../chromatic/CalendarCell.stories.tsx | 2 +- .../@react-spectrum/combobox/src/ComboBox.tsx | 2 +- .../@react-spectrum/list/src/ListView.tsx | 2 +- .../listbox/src/ListBoxBase.tsx | 2 +- .../numberfield/src/NumberField.tsx | 2 +- .../numberfield/src/StepButton.tsx | 2 +- .../@react-spectrum/picker/src/Picker.tsx | 2 +- .../provider/chromatic/Provider.stories.tsx | 4 ++-- .../@react-spectrum/provider/src/Provider.tsx | 24 +++++++++---------- .../provider/stories/Provider.stories.tsx | 4 ++-- .../provider/test/Provider.test.tsx | 5 ++-- .../searchwithin/src/SearchWithin.tsx | 2 +- .../@react-spectrum/slider/src/Slider.tsx | 4 ++-- .../table/src/TableViewBase.tsx | 10 ++++---- packages/@react-spectrum/tabs/src/Tabs.tsx | 2 +- packages/@react-spectrum/tag/src/TagGroup.tsx | 10 ++++---- .../toast/src/ToastContainer.tsx | 20 ++++++++-------- .../@react-spectrum/toast/src/Toaster.tsx | 2 +- .../@react-spectrum/tooltip/src/Tooltip.tsx | 2 +- .../tooltip/src/TooltipTrigger.tsx | 6 ++--- .../@react-spectrum/tooltip/src/context.ts | 4 ++-- .../@react-spectrum/tree/src/TreeView.tsx | 1 + .../utils/src/BreakpointProvider.tsx | 8 +++---- .../@react-stately/toast/src/useToastState.ts | 2 +- 26 files changed, 65 insertions(+), 63 deletions(-) diff --git a/packages/@react-spectrum/autocomplete/src/SearchAutocomplete.tsx b/packages/@react-spectrum/autocomplete/src/SearchAutocomplete.tsx index be33e4e4a76..781f0d26f6b 100644 --- a/packages/@react-spectrum/autocomplete/src/SearchAutocomplete.tsx +++ b/packages/@react-spectrum/autocomplete/src/SearchAutocomplete.tsx @@ -116,7 +116,7 @@ function ForwardSearchAutocompleteBase(props: SpectrumSearchAu // Measure the width of the inputfield to inform the width of the menu (below). let [menuWidth, setMenuWidth] = useState(0); - let {scale} = useProvider(); + let {scale} = useProvider()!; let onResize = useCallback(() => { if (inputRef.current) { diff --git a/packages/@react-spectrum/buttongroup/src/ButtonGroup.tsx b/packages/@react-spectrum/buttongroup/src/ButtonGroup.tsx index 7cdae6b3d27..b7191a7a81c 100644 --- a/packages/@react-spectrum/buttongroup/src/ButtonGroup.tsx +++ b/packages/@react-spectrum/buttongroup/src/ButtonGroup.tsx @@ -26,7 +26,7 @@ import {SpectrumButtonGroupProps} from '@react-types/buttongroup'; import styles from '@adobe/spectrum-css-temp/components/buttongroup/vars.css'; function ButtonGroup(props: SpectrumButtonGroupProps, ref: DOMRef) { - let {scale} = useProvider(); + let {scale} = useProvider()!; props = useProviderProps(props); props = useSlotProps(props, 'buttonGroup'); diff --git a/packages/@react-spectrum/calendar/chromatic/CalendarCell.stories.tsx b/packages/@react-spectrum/calendar/chromatic/CalendarCell.stories.tsx index 5bf28ee17fa..258e7ba576e 100644 --- a/packages/@react-spectrum/calendar/chromatic/CalendarCell.stories.tsx +++ b/packages/@react-spectrum/calendar/chromatic/CalendarCell.stories.tsx @@ -75,7 +75,7 @@ DarkLarge.parameters = { }; export function AllStates() { - let {scale} = useProvider(); + let {scale} = useProvider()!; let size = scale === 'medium' ? 40 : 50; return ( diff --git a/packages/@react-spectrum/combobox/src/ComboBox.tsx b/packages/@react-spectrum/combobox/src/ComboBox.tsx index 740f56e392e..ed3666bb553 100644 --- a/packages/@react-spectrum/combobox/src/ComboBox.tsx +++ b/packages/@react-spectrum/combobox/src/ComboBox.tsx @@ -126,7 +126,7 @@ const ComboBoxBase = React.forwardRef(function ComboBoxBase(props: SpectrumCombo // Measure the width of the inputfield and the button to inform the width of the menu (below). let [menuWidth, setMenuWidth] = useState(undefined); - let {scale} = useProvider(); + let {scale} = useProvider()!; let onResize = useCallback(() => { if (unwrappedButtonRef.current && inputRef.current) { diff --git a/packages/@react-spectrum/list/src/ListView.tsx b/packages/@react-spectrum/list/src/ListView.tsx index d56f2a0997f..e4fc7e27dcd 100644 --- a/packages/@react-spectrum/list/src/ListView.tsx +++ b/packages/@react-spectrum/list/src/ListView.tsx @@ -93,7 +93,7 @@ const ROW_HEIGHTS = { }; function useListLayout(state: ListState, density: SpectrumListViewProps['density'], overflowMode: SpectrumListViewProps['overflowMode']) { - let {scale} = useProvider(); + let {scale} = useProvider()!; let layout = useMemo(() => new ListViewLayout({ estimatedRowHeight: ROW_HEIGHTS[density || 'regular'][scale] diff --git a/packages/@react-spectrum/listbox/src/ListBoxBase.tsx b/packages/@react-spectrum/listbox/src/ListBoxBase.tsx index 15c5944bba3..645bf713902 100644 --- a/packages/@react-spectrum/listbox/src/ListBoxBase.tsx +++ b/packages/@react-spectrum/listbox/src/ListBoxBase.tsx @@ -49,7 +49,7 @@ interface ListBoxBaseProps extends AriaListBoxOptions, DOMProps, AriaLabel /** @private */ export function useListBoxLayout(): ListBoxLayout { - let {scale} = useProvider(); + let {scale} = useProvider()!; let layout = useMemo(() => new ListBoxLayout({ estimatedRowHeight: scale === 'large' ? 48 : 32, diff --git a/packages/@react-spectrum/numberfield/src/NumberField.tsx b/packages/@react-spectrum/numberfield/src/NumberField.tsx index 9fc44a1e256..3d967b86bdf 100644 --- a/packages/@react-spectrum/numberfield/src/NumberField.tsx +++ b/packages/@react-spectrum/numberfield/src/NumberField.tsx @@ -31,7 +31,7 @@ import {useProvider, useProviderProps} from '@react-spectrum/provider'; function NumberField(props: SpectrumNumberFieldProps, ref: FocusableRef) { props = useProviderProps(props); props = useFormProps(props); - let provider = useProvider(); + let provider = useProvider()!; let { isQuiet, isReadOnly, diff --git a/packages/@react-spectrum/numberfield/src/StepButton.tsx b/packages/@react-spectrum/numberfield/src/StepButton.tsx index 609fbc605dc..caa1c56a7cb 100644 --- a/packages/@react-spectrum/numberfield/src/StepButton.tsx +++ b/packages/@react-spectrum/numberfield/src/StepButton.tsx @@ -32,7 +32,7 @@ interface StepButtonProps extends AriaButtonProps { function StepButton(props: StepButtonProps, ref: FocusableRef) { props = useProviderProps(props); - let {scale} = useProvider(); + let {scale} = useProvider()!; let {direction, isDisabled, isQuiet} = props; let domRef = useFocusableRef(ref); /** diff --git a/packages/@react-spectrum/picker/src/Picker.tsx b/packages/@react-spectrum/picker/src/Picker.tsx index 06bbd50cc14..8112aa9eac0 100644 --- a/packages/@react-spectrum/picker/src/Picker.tsx +++ b/packages/@react-spectrum/picker/src/Picker.tsx @@ -106,7 +106,7 @@ function Picker(props: SpectrumPickerProps, ref: DOMRef(undefined); - let {scale} = useProvider(); + let {scale} = useProvider()!; let onResize = useCallback(() => { if (!isMobile && unwrappedTriggerRef.current) { 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..dac4bd134f6 100644 --- a/packages/@react-spectrum/provider/src/Provider.tsx +++ b/packages/@react-spectrum/provider/src/Provider.tsx @@ -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} @@ -133,20 +133,20 @@ const ProviderWrapper = React.forwardRef(function ProviderWrapper(props: Provide ...otherProps } = props; let {locale, direction} = useLocale(); - let {theme, colorScheme, scale} = useProvider(); + let {theme, colorScheme, scale} = useProvider()!; let {modalProviderProps} = useModalProvider(); 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,7 +195,7 @@ 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 { +export function useProvider() { return useContext(Context); } 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..857802445a8 100644 --- a/packages/@react-spectrum/provider/test/Provider.test.tsx +++ b/packages/@react-spectrum/provider/test/Provider.test.tsx @@ -21,6 +21,7 @@ import {Switch} from '@react-spectrum/switch'; import {TextField} from '@react-spectrum/textfield'; import {useBreakpoint} from '@react-spectrum/utils'; import userEvent from '@testing-library/user-event'; +import { Breakpoints } from '@react-types/provider'; let theme = { global: {}, @@ -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/searchwithin/src/SearchWithin.tsx b/packages/@react-spectrum/searchwithin/src/SearchWithin.tsx index f091615c804..3329d9d6510 100644 --- a/packages/@react-spectrum/searchwithin/src/SearchWithin.tsx +++ b/packages/@react-spectrum/searchwithin/src/SearchWithin.tsx @@ -54,7 +54,7 @@ function SearchWithin(props: SpectrumSearchWithinProps, ref: FocusableRef(null); - let {scale} = useProvider(); + let {scale} = useProvider()!; let onResize = useCallback(() => { let shouldUseGroup = !!label; diff --git a/packages/@react-spectrum/slider/src/Slider.tsx b/packages/@react-spectrum/slider/src/Slider.tsx index 7c16db71ebb..4da67749f10 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/table/src/TableViewBase.tsx b/packages/@react-spectrum/table/src/TableViewBase.tsx index 47193c8bba6..b912ee65a52 100644 --- a/packages/@react-spectrum/table/src/TableViewBase.tsx +++ b/packages/@react-spectrum/table/src/TableViewBase.tsx @@ -170,7 +170,7 @@ function TableViewBase(props: TableBaseProps, ref: DOMRef(props: TableBaseProps, ref: DOMRef(props: TableVirtualizerProps) { let isLoading = loadingState === 'loading' || loadingState === 'loadingMore'; let onLoadMore = collection.body.props.onLoadMore; let [tableWidth, setTableWidth] = useState(0); - let {scale} = useProvider(); + let {scale} = useProvider()!; const getDefaultWidth = useCallback(({props: {hideHeader, isSelectionCell, showDivider, isDragButtonCell}}: GridNode): ColumnSize | null | undefined => { if (hideHeader) { @@ -1178,7 +1178,7 @@ function TableRow({item, children, layoutInfo, parent, ...otherProps}: {item: Gr 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); } @@ -1355,7 +1355,7 @@ function TableCheckboxCell({cell}) { } function TableCell({cell}) { - let {scale} = useProvider(); + let {scale} = useProvider()!; let {state} = useTableContext(); let isExpandableTable = 'expandedKeys' in state; let ref = useRef(null); diff --git a/packages/@react-spectrum/tabs/src/Tabs.tsx b/packages/@react-spectrum/tabs/src/Tabs.tsx index 0ebfd3a8c13..eab882808f7 100644 --- a/packages/@react-spectrum/tabs/src/Tabs.tsx +++ b/packages/@react-spectrum/tabs/src/Tabs.tsx @@ -217,7 +217,7 @@ function TabLine(props: TabLineProps) { } = props; let {direction} = useLocale(); - let {scale} = useProvider(); + let {scale} = useProvider()!; let {tabLineState} = useContext(TabContext)!; let [style, setStyle] = useState({ diff --git a/packages/@react-spectrum/tag/src/TagGroup.tsx b/packages/@react-spectrum/tag/src/TagGroup.tsx index ad34d300a1e..132730b70eb 100644 --- a/packages/@react-spectrum/tag/src/TagGroup.tsx +++ b/packages/@react-spectrum/tag/src/TagGroup.tsx @@ -62,10 +62,10 @@ 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(); + let {scale} = useProvider()!; let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/tag'); let [isCollapsed, setIsCollapsed] = useState(maxRows != null); let state = useListState(props); @@ -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..693c3405459 100644 --- a/packages/@react-spectrum/toast/src/ToastContainer.tsx +++ b/packages/@react-spectrum/toast/src/ToastContainer.tsx @@ -14,7 +14,7 @@ import {AriaToastRegionProps} from '@react-aria/toast'; import {classNames} from '@react-spectrum/utils'; import {DOMProps} from '@react-types/shared'; import {filterDOMProps} from '@react-aria/utils'; -import React, {ReactElement, useEffect, useRef} from 'react'; +import React, {ReactElement, ReactNode, useEffect, useRef} from 'react'; import {SpectrumToastValue, Toast} from './Toast'; import toastContainerStyles from './toastContainer.css'; import {Toaster} from './Toaster'; @@ -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): ReactNode { // 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 null; } } @@ -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); @@ -168,19 +168,19 @@ function addToast(children: string, variant: SpectrumToastValue['variant'], opti const SpectrumToastQueue = { /** Queues a neutral toast. */ - neutral(children: string, options: SpectrumToastOptions = {}): CloseFunction { + neutral(children: string, options: SpectrumToastOptions = {}): CloseFunction | null { return addToast(children, 'neutral', options); }, /** Queues a positive toast. */ - positive(children: string, options: SpectrumToastOptions = {}): CloseFunction { + positive(children: string, options: SpectrumToastOptions = {}): CloseFunction | null { return addToast(children, 'positive', options); }, /** Queues a negative toast. */ - negative(children: string, options: SpectrumToastOptions = {}): CloseFunction { + negative(children: string, options: SpectrumToastOptions = {}): CloseFunction | null { return addToast(children, 'negative', options); }, /** Queues an informational toast. */ - info(children: string, options: SpectrumToastOptions = {}): CloseFunction { + info(children: string, options: SpectrumToastOptions = {}): CloseFunction | null { return addToast(children, 'info', options); } }; 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/tooltip/src/Tooltip.tsx b/packages/@react-spectrum/tooltip/src/Tooltip.tsx index 4ee99054e93..6be620245c9 100644 --- a/packages/@react-spectrum/tooltip/src/Tooltip.tsx +++ b/packages/@react-spectrum/tooltip/src/Tooltip.tsx @@ -30,7 +30,7 @@ let iconMap = { function Tooltip(props: SpectrumTooltipProps, ref: DOMRef) { let {ref: overlayRef, arrowProps, state, arrowRef, ...tooltipProviderProps} = useContext(TooltipContext); - let defaultRef = useRef(undefined); + let defaultRef = useRef(null); overlayRef = overlayRef || defaultRef; props = mergeProps(props, tooltipProviderProps); let { diff --git a/packages/@react-spectrum/tooltip/src/TooltipTrigger.tsx b/packages/@react-spectrum/tooltip/src/TooltipTrigger.tsx index 2a294e2cbdf..0478fff35f5 100644 --- a/packages/@react-spectrum/tooltip/src/TooltipTrigger.tsx +++ b/packages/@react-spectrum/tooltip/src/TooltipTrigger.tsx @@ -35,8 +35,8 @@ function TooltipTrigger(props: SpectrumTooltipTriggerProps) { let [trigger, tooltip] = React.Children.toArray(children) as [ReactElement, ReactElement]; let state = useTooltipTriggerState(props); - let tooltipTriggerRef = useRef(undefined); - let overlayRef = useRef(undefined); + let tooltipTriggerRef = useRef(null); + let overlayRef = useRef(null); let {triggerProps, tooltipProps} = useTooltipTrigger({ isDisabled, @@ -52,7 +52,7 @@ function TooltipTrigger(props: SpectrumTooltipTriggerProps) { } } }, [state.isOpen, overlayRef]); - let arrowRef = useRef(null); + let arrowRef = useRef(null); let [arrowWidth, setArrowWidth] = useState(0); useLayoutEffect(() => { if (arrowRef.current && state.isOpen) { diff --git a/packages/@react-spectrum/tooltip/src/context.ts b/packages/@react-spectrum/tooltip/src/context.ts index 6ea911470ce..1dcecea82f3 100644 --- a/packages/@react-spectrum/tooltip/src/context.ts +++ b/packages/@react-spectrum/tooltip/src/context.ts @@ -18,9 +18,9 @@ import {TooltipTriggerState} from '@react-stately/tooltip'; interface TooltipContextProps extends StyleProps { state?: TooltipTriggerState, ref?: RefObject, - placement?: PlacementAxis, + placement: PlacementAxis | null, arrowProps?: HTMLAttributes, arrowRef?: RefObject } -export const TooltipContext = React.createContext({}); +export const TooltipContext = React.createContext({placement: null}); diff --git a/packages/@react-spectrum/tree/src/TreeView.tsx b/packages/@react-spectrum/tree/src/TreeView.tsx index 1bdcad50e08..b65dbf38094 100644 --- a/packages/@react-spectrum/tree/src/TreeView.tsx +++ b/packages/@react-spectrum/tree/src/TreeView.tsx @@ -333,6 +333,7 @@ const expandButton = style({ function ExpandableRowChevron(props: ExpandableRowChevronProps) { let expandButtonRef = useRef(null); + // @ts-ignore - not sure how to type this or convert to a button (it fails a test if i convert) let [fullProps, ref] = useContextProps({...props, slot: 'chevron'}, expandButtonRef, ButtonContext); let {isExpanded, isDisabled} = fullProps; let {direction} = useLocale(); diff --git a/packages/@react-spectrum/utils/src/BreakpointProvider.tsx b/packages/@react-spectrum/utils/src/BreakpointProvider.tsx index ed9080dec25..1d22c7b7e26 100644 --- a/packages/@react-spectrum/utils/src/BreakpointProvider.tsx +++ b/packages/@react-spectrum/utils/src/BreakpointProvider.tsx @@ -12,7 +12,7 @@ interface BreakpointContext { matchedBreakpoints: string[] } -const Context = React.createContext(null); +const Context = React.createContext(null); Context.displayName = 'BreakpointContext'; interface BreakpointProviderProps { @@ -34,12 +34,12 @@ export function BreakpointProvider(props: BreakpointProviderProps) { } export function useMatchedBreakpoints(breakpoints: Breakpoints): string[] { - let entries = Object.entries(breakpoints).sort(([, valueA], [, valueB]) => valueB - valueA); + let entries = Object.entries(breakpoints).sort(([, valueA], [, valueB]) => valueB! - valueA!); let breakpointQueries = entries.map(([, value]) => `(min-width: ${value}px)`); let supportsMatchMedia = typeof window !== 'undefined' && typeof window.matchMedia === 'function'; let getBreakpointHandler = () => { - let matched = []; + let matched: string[] = []; for (let i in breakpointQueries) { let query = breakpointQueries[i]; if (window.matchMedia(query).matches) { @@ -87,6 +87,6 @@ export function useMatchedBreakpoints(breakpoints: Breakpoints): string[] { return isSSR ? ['base'] : breakpoint; } -export function useBreakpoint(): BreakpointContext { +export function useBreakpoint() { return useContext(Context); } diff --git a/packages/@react-stately/toast/src/useToastState.ts b/packages/@react-stately/toast/src/useToastState.ts index c51514def42..2d04528f1f8 100644 --- a/packages/@react-stately/toast/src/useToastState.ts +++ b/packages/@react-stately/toast/src/useToastState.ts @@ -42,7 +42,7 @@ export interface QueuedToast extends ToastOptions { /** A timer for the toast, if a timeout was set. */ timer?: Timer, /** The current animation state for the toast. */ - animation?: 'entering' | 'queued' | 'exiting' + animation?: 'entering' | 'queued' | 'exiting' | null } export interface ToastState { From e33a4527ce08af0fb2e0b7cd77560e7e0a565a3c Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 14 Nov 2024 20:32:44 +1100 Subject: [PATCH 43/66] remaining rsp and dev --- packages/@react-spectrum/utils/src/Slots.tsx | 3 ++- packages/@react-spectrum/utils/src/classNames.ts | 2 +- packages/@react-types/shared/src/refs.d.ts | 2 +- packages/dev/docs/pages/react-aria/home/I18n.tsx | 7 ++++--- packages/dev/docs/pages/react-aria/home/KanbanExample.tsx | 6 +++--- .../dev/docs/pages/react-aria/home/SwitchAnimation.tsx | 8 ++++---- packages/dev/docs/pages/react-aria/home/components.tsx | 4 ++-- starters/tailwind/src/ListBox.tsx | 1 + 8 files changed, 18 insertions(+), 15 deletions(-) diff --git a/packages/@react-spectrum/utils/src/Slots.tsx b/packages/@react-spectrum/utils/src/Slots.tsx index 80fa4c9988f..ed068801c0b 100644 --- a/packages/@react-spectrum/utils/src/Slots.tsx +++ b/packages/@react-spectrum/utils/src/Slots.tsx @@ -17,10 +17,11 @@ interface SlotProps { slot?: string } -let SlotContext = React.createContext(null); +let SlotContext = React.createContext<{} | null>(null); export function useSlotProps(props: T & {id?: string}, defaultSlot?: string): T { let slot = (props as SlotProps).slot || defaultSlot; + // @ts-ignore TODO why is slot an object and not just string or undefined? let {[slot]: slotProps = {}} = useContext(SlotContext) || {}; return mergeProps(props, mergeProps(slotProps, {id: props.id})); diff --git a/packages/@react-spectrum/utils/src/classNames.ts b/packages/@react-spectrum/utils/src/classNames.ts index 1385d1f5948..f5f31093cd2 100644 --- a/packages/@react-spectrum/utils/src/classNames.ts +++ b/packages/@react-spectrum/utils/src/classNames.ts @@ -24,7 +24,7 @@ export function keepSpectrumClassNames() { } export function classNames(cssModule: {[key: string]: string}, ...values: Array): string { - let classes = []; + let classes: Array<{} | undefined> = []; for (let value of values) { if (typeof value === 'object' && value) { let mapped = {}; diff --git a/packages/@react-types/shared/src/refs.d.ts b/packages/@react-types/shared/src/refs.d.ts index 828adc07c14..16bcbe30265 100644 --- a/packages/@react-types/shared/src/refs.d.ts +++ b/packages/@react-types/shared/src/refs.d.ts @@ -13,7 +13,7 @@ import {ReactElement, Ref, RefAttributes} from 'react'; export interface DOMRefValue { - UNSAFE_getDOMNode(): T + UNSAFE_getDOMNode(): T | null } export interface FocusableRefValue extends DOMRefValue { diff --git a/packages/dev/docs/pages/react-aria/home/I18n.tsx b/packages/dev/docs/pages/react-aria/home/I18n.tsx index 8b8a8157d20..0b316a88034 100644 --- a/packages/dev/docs/pages/react-aria/home/I18n.tsx +++ b/packages/dev/docs/pages/react-aria/home/I18n.tsx @@ -121,7 +121,8 @@ export function I18n() { }, [langDisplay, regionDisplay]); let pref = preferences.find(p => p.value === locale); - let preferredCalendars = React.useMemo(() => pref ? (pref.ordering || 'gregory').split(' ').map(p => calendars.find(c => c.key === p)).filter(Boolean) : [calendars[0]], [pref]); + // @ts-ignore there cannot be any undefined values in the array + let preferredCalendars: Array<{key: string, name: string}> = React.useMemo(() => pref ? (pref.ordering || 'gregory').split(' ').map(p => calendars.find(c => c.key === p)).filter(Boolean) : [calendars[0]], [pref]); let otherCalendars = React.useMemo(() => calendars.filter(c => !preferredCalendars.some(p => p?.key === c.key)), [preferredCalendars]); let updateLocale = locale => { @@ -176,10 +177,10 @@ export function I18n() {