Skip to content

Commit ac323e9

Browse files
committed
rough implementation for listbox
1 parent c9ee11d commit ac323e9

File tree

5 files changed

+96
-28
lines changed

5 files changed

+96
-28
lines changed

packages/@react-aria/collections/src/CollectionBuilder.tsx

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -127,15 +127,18 @@ function useCollectionDocument<T extends object, C extends BaseCollection<T>>(cr
127127

128128
const SSRContext = createContext<BaseNode<any> | null>(null);
129129

130-
function useSSRCollectionNode<T extends Element>(Type: string, props: object, ref: ForwardedRef<T>, rendered?: any, children?: ReactNode, render?: (node: Node<T>) => ReactElement) {
130+
// TODO: make this any for now, but should be a node class
131+
function useSSRCollectionNode<T extends Element>(Type: any, props: object, ref: ForwardedRef<T>, rendered?: any, children?: ReactNode, render?: (node: Node<T>) => ReactElement) {
132+
// function useSSRCollectionNode<T extends Element>(Type: string, props: object, ref: ForwardedRef<T>, rendered?: any, children?: ReactNode, render?: (node: Node<T>) => ReactElement) {
131133
// During SSR, portals are not supported, so the collection children will be wrapped in an SSRContext.
132134
// Since SSR occurs only once, we assume that the elements are rendered in order and never re-render.
133135
// Therefore we can create elements in our collection document during render so that they are in the
134136
// collection by the time we need to use the collection to render to the real DOM.
135137
// After hydration, we switch to client rendering using the portal.
136138
let itemRef = useCallback((element: ElementNode<any> | null) => {
137-
element?.setProps(props, ref, rendered, render);
138-
}, [props, ref, rendered, render]);
139+
// TODO: now we get the proper node class aka TreeNode
140+
element?.setProps(props, ref, rendered, render, Type);
141+
}, [props, ref, rendered, render, Type]);
139142
let parentNode = useContext(SSRContext);
140143
if (parentNode) {
141144
// Guard against double rendering in strict mode.
@@ -154,12 +157,14 @@ function useSSRCollectionNode<T extends Element>(Type: string, props: object, re
154157
}
155158

156159
// @ts-ignore
157-
return <Type ref={itemRef}>{children}</Type>;
160+
// TODO: make div for now, may not actually matter
161+
return <div ref={itemRef}>{children}</div>;
158162
}
159163

160-
export function createLeafComponent<T extends object, P extends object, E extends Element>(type: string, render: (props: P, ref: ForwardedRef<E>) => ReactElement | null): (props: P & React.RefAttributes<T>) => ReactElement | null;
161-
export function createLeafComponent<T extends object, P extends object, E extends Element>(type: string, render: (props: P, ref: ForwardedRef<E>, node: Node<T>) => ReactElement | null): (props: P & React.RefAttributes<T>) => ReactElement | null;
162-
export function createLeafComponent<P extends object, E extends Element>(type: string, render: (props: P, ref: ForwardedRef<E>, node?: any) => ReactElement | null): (props: P & React.RefAttributes<any>) => ReactElement | null {
164+
// TODO: changed all of these to be any, but should be node class type
165+
export function createLeafComponent<T extends object, P extends object, E extends Element>(type: any, render: (props: P, ref: ForwardedRef<E>) => ReactElement | null): (props: P & React.RefAttributes<T>) => ReactElement | null;
166+
export function createLeafComponent<T extends object, P extends object, E extends Element>(type: any, render: (props: P, ref: ForwardedRef<E>, node: Node<T>) => ReactElement | null): (props: P & React.RefAttributes<T>) => ReactElement | null;
167+
export function createLeafComponent<P extends object, E extends Element>(type: any, render: (props: P, ref: ForwardedRef<E>, node?: any) => ReactElement | null): (props: P & React.RefAttributes<any>) => ReactElement | null {
163168
let Component = ({node}) => render(node.props, node.props.ref, node);
164169
let Result = (forwardRef as forwardRefType)((props: P, ref: ForwardedRef<E>) => {
165170
let focusableProps = useContext(FocusableContext);
@@ -190,7 +195,8 @@ export function createLeafComponent<P extends object, E extends Element>(type: s
190195
return Result;
191196
}
192197

193-
export function createBranchComponent<T extends object, P extends {children?: any}, E extends Element>(type: string, render: (props: P, ref: ForwardedRef<E>, node: Node<T>) => ReactElement | null, useChildren: (props: P) => ReactNode = useCollectionChildren): (props: P & React.RefAttributes<E>) => ReactElement | null {
198+
// TODO: changed all of these to be any, but should be node class type
199+
export function createBranchComponent<T extends object, P extends {children?: any}, E extends Element>(type: any, render: (props: P, ref: ForwardedRef<E>, node: Node<T>) => ReactElement | null, useChildren: (props: P) => ReactNode = useCollectionChildren): (props: P & React.RefAttributes<E>) => ReactElement | null {
194200
let Component = ({node}) => render(node.props, node.props.ref, node);
195201
let Result = (forwardRef as forwardRefType)((props: P, ref: ForwardedRef<E>) => {
196202
let children = useChildren(props);

packages/@react-aria/collections/src/Document.ts

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -256,15 +256,21 @@ export class BaseNode<T> {
256256
*/
257257
export class ElementNode<T> extends BaseNode<T> {
258258
nodeType = 8; // COMMENT_NODE (we'd use ELEMENT_NODE but React DevTools will fail to get its dimensions)
259-
node: CollectionNode<T>;
259+
// TODO: running with assumption that setProps will be called before any other calls to node are made so theoretically
260+
// node will be defined
261+
node: CollectionNode<T> | null;
260262
isMutated = true;
261263
private _index: number = 0;
262264
hasSetProps = false;
263265
isHidden = false;
264266

265267
constructor(type: string, ownerDocument: Document<T, any>) {
266268
super(ownerDocument);
267-
this.node = new CollectionNode(type, `react-aria-${++ownerDocument.nodeId}`);
269+
this.node = null;
270+
// TODO: move this line to setProps
271+
// if ()
272+
// TODO: this is called by Document, seems like we need it?
273+
// this.node = new CollectionNode(type, `react-aria-${++ownerDocument.nodeId}`);
268274
}
269275

270276
get index(): number {
@@ -278,7 +284,7 @@ export class ElementNode<T> extends BaseNode<T> {
278284

279285
get level(): number {
280286
if (this.parentNode instanceof ElementNode) {
281-
return this.parentNode.level + (this.node.type === 'item' ? 1 : 0);
287+
return this.parentNode.level + (this.node?.type === 'item' ? 1 : 0);
282288
}
283289

284290
return 0;
@@ -290,48 +296,64 @@ export class ElementNode<T> extends BaseNode<T> {
290296
*/
291297
private getMutableNode(): Mutable<CollectionNode<T>> {
292298
if (!this.isMutated) {
293-
this.node = this.node.clone();
299+
this.node = this.node!.clone();
294300
this.isMutated = true;
295301
}
296302

297303
this.ownerDocument.markDirty(this);
298-
return this.node;
304+
return this.node!;
299305
}
300306

301307
updateNode(): void {
302308
let nextSibling = this.nextVisibleSibling;
303309
let node = this.getMutableNode();
304310
node.index = this.index;
305311
node.level = this.level;
306-
node.parentKey = this.parentNode instanceof ElementNode ? this.parentNode.node.key : null;
307-
node.prevKey = this.previousVisibleSibling?.node.key ?? null;
308-
node.nextKey = nextSibling?.node.key ?? null;
312+
node.parentKey = this.parentNode instanceof ElementNode ? this.parentNode.node!.key : null;
313+
node.prevKey = this.previousVisibleSibling?.node!.key ?? null;
314+
node.nextKey = nextSibling?.node!.key ?? null;
309315
node.hasChildNodes = !!this.firstChild;
310-
node.firstChildKey = this.firstVisibleChild?.node.key ?? null;
311-
node.lastChildKey = this.lastVisibleChild?.node.key ?? null;
316+
node.firstChildKey = this.firstVisibleChild?.node!.key ?? null;
317+
node.lastChildKey = this.lastVisibleChild?.node!.key ?? null;
312318

313319
// Update the colIndex of sibling nodes if this node has a colSpan.
314320
if ((node.colSpan != null || node.colIndex != null) && nextSibling) {
315321
// This queues the next sibling for update, which means this happens recursively.
316322
let nextColIndex = (node.colIndex ?? node.index) + (node.colSpan ?? 1);
317-
if (nextColIndex !== nextSibling.node.colIndex) {
323+
if (nextColIndex !== nextSibling.node!.colIndex) {
318324
let siblingNode = nextSibling.getMutableNode();
319325
siblingNode.colIndex = nextColIndex;
320326
}
321327
}
322328
}
323329

324-
setProps<E extends Element>(obj: {[key: string]: any}, ref: ForwardedRef<E>, rendered?: ReactNode, render?: (node: Node<T>) => ReactElement): void {
330+
setProps<E extends Element>(obj: {[key: string]: any}, ref: ForwardedRef<E>, rendered?: ReactNode, render?: (node: Node<T>) => ReactElement, type?: any): void {
325331
let node = this.getMutableNode();
326332
let {value, textValue, id, ...props} = obj;
333+
334+
335+
// if called for first time, aka this.node is undef, call
336+
// this.node = new CollectionNode(type, `react-aria-${++ownerDocument.nodeId}`); but make new TreeNode instead of COllectionNode
337+
// Caveat is this assumes we don't need a node before setProps is called on it
338+
// TODO: will get rid of type function check here when we migrate everything to use the class
339+
if (node == null && typeof type === 'function') {
340+
node = new type(`react-aria-${++this.ownerDocument.nodeId}`);
341+
this.node = node;
342+
// node.key = id;
343+
// console.log('making node', node.type, node)
344+
}
345+
// console.log('setting props', props, node, node.props)
327346
props.ref = ref;
328347
node.props = props;
329348
node.rendered = rendered;
330349
node.render = render;
331350
node.value = value;
332351
node.textValue = textValue || (typeof props.children === 'string' ? props.children : '') || obj['aria-label'] || '';
333352
if (id != null && id !== node.key) {
353+
// TODO: still need to use this.hasSetProps so this can run twice (?) instead of setting node.key above
354+
// If we set node.key = id and change this to if (this.node), setting refs fails. If we just check (this.node here), it will fail if the user provides an id
334355
if (this.hasSetProps) {
356+
// if (this.node) {
335357
throw new Error('Cannot change the id of an item');
336358
}
337359
node.key = id;
@@ -341,6 +363,7 @@ export class ElementNode<T> extends BaseNode<T> {
341363
node.colSpan = props.colSpan;
342364
}
343365

366+
// TODO: still need this, see above comment
344367
this.hasSetProps = true;
345368
if (this.isConnected) {
346369
this.ownerDocument.queueUpdate();

packages/react-aria-components/src/Header.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,20 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13+
import {CollectionNode, createLeafComponent} from '@react-aria/collections';
1314
import {ContextValue, useContextProps} from './utils';
14-
import {createLeafComponent} from '@react-aria/collections';
15+
import {Key} from '@react-types/shared';
1516
import React, {createContext, ForwardedRef, HTMLAttributes} from 'react';
1617

1718
export const HeaderContext = createContext<ContextValue<HTMLAttributes<HTMLElement>, HTMLElement>>({});
1819

19-
export const Header = /*#__PURE__*/ createLeafComponent('header', function Header(props: HTMLAttributes<HTMLElement>, ref: ForwardedRef<HTMLElement>) {
20+
class HeaderNode extends CollectionNode<any> {
21+
constructor(key: Key) {
22+
super('header', key);
23+
}
24+
}
25+
26+
export const Header = /*#__PURE__*/ createLeafComponent(HeaderNode, function Header(props: HTMLAttributes<HTMLElement>, ref: ForwardedRef<HTMLElement>) {
2027
[props, ref] = useContextProps(props, ref, HeaderContext);
2128
return (
2229
<header className="react-aria-Header" {...props} ref={ref}>

packages/react-aria-components/src/ListBox.tsx

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
*/
1212

1313
import {AriaListBoxOptions, AriaListBoxProps, DraggableItemResult, DragPreviewRenderer, DroppableCollectionResult, DroppableItemResult, FocusScope, ListKeyboardDelegate, mergeProps, useCollator, useFocusRing, useHover, useListBox, useListBoxSection, useLocale, useOption} from 'react-aria';
14-
import {Collection, CollectionBuilder, createBranchComponent, createLeafComponent} from '@react-aria/collections';
14+
import {Collection, CollectionBuilder, CollectionNode, createBranchComponent, createLeafComponent} from '@react-aria/collections';
1515
import {CollectionProps, CollectionRendererContext, ItemRenderProps, SectionContext, SectionProps} from './Collection';
1616
import {ContextValue, DEFAULT_SLOT, Provider, RenderProps, ScrollableProps, SlotProps, StyleProps, StyleRenderProps, useContextProps, useRenderProps, useSlot} from './utils';
1717
import {DragAndDropContext, DropIndicatorContext, DropIndicatorProps, useDndPersistedKeys, useRenderDropIndicator} from './DragAndDrop';
@@ -304,10 +304,19 @@ function ListBoxSectionInner<T extends object>(props: ListBoxSectionProps<T>, re
304304
);
305305
}
306306

307+
308+
// todo make a class here
309+
310+
class SectionNode extends CollectionNode<any> {
311+
constructor(key: Key) {
312+
super('section', key);
313+
}
314+
}
315+
307316
/**
308317
* A ListBoxSection represents a section within a ListBox.
309318
*/
310-
export const ListBoxSection = /*#__PURE__*/ createBranchComponent('section', ListBoxSectionInner);
319+
export const ListBoxSection = /*#__PURE__*/ createBranchComponent(SectionNode, ListBoxSectionInner);
311320

312321
export interface ListBoxItemRenderProps extends ItemRenderProps {}
313322

@@ -329,10 +338,19 @@ export interface ListBoxItemProps<T = object> extends RenderProps<ListBoxItemRen
329338
onAction?: () => void
330339
}
331340

341+
342+
// TODO create item type here
343+
344+
class ItemNode extends CollectionNode<any> {
345+
constructor(key: Key) {
346+
super('item', key);
347+
}
348+
}
349+
332350
/**
333351
* A ListBoxItem represents an individual option in a ListBox.
334352
*/
335-
export const ListBoxItem = /*#__PURE__*/ createLeafComponent('item', function ListBoxItem<T extends object>(props: ListBoxItemProps<T>, forwardedRef: ForwardedRef<HTMLDivElement>, item: Node<T>) {
353+
export const ListBoxItem = /*#__PURE__*/ createLeafComponent(ItemNode, function ListBoxItem<T extends object>(props: ListBoxItemProps<T>, forwardedRef: ForwardedRef<HTMLDivElement>, item: Node<T>) {
336354
let ref = useObjectRef<any>(forwardedRef);
337355
let state = useContext(ListStateContext)!;
338356
let {dragAndDropHooks, dragState, dropState} = useContext(DragAndDropContext)!;
@@ -466,6 +484,13 @@ function ListBoxDropIndicator(props: ListBoxDropIndicatorProps, ref: ForwardedRe
466484
);
467485
}
468486

487+
// TODO: can reuse this most likely
488+
class LoaderNode extends CollectionNode<any> {
489+
constructor(key: Key) {
490+
super('loader', key);
491+
}
492+
}
493+
469494
const ListBoxDropIndicatorForwardRef = forwardRef(ListBoxDropIndicator);
470495

471496
export interface ListBoxLoadingSentinelProps extends Omit<LoadMoreSentinelProps, 'collection'>, StyleProps {
@@ -479,7 +504,7 @@ export interface ListBoxLoadingSentinelProps extends Omit<LoadMoreSentinelProps,
479504
isLoading?: boolean
480505
}
481506

482-
export const UNSTABLE_ListBoxLoadingSentinel = createLeafComponent('loader', function ListBoxLoadingIndicator<T extends object>(props: ListBoxLoadingSentinelProps, ref: ForwardedRef<HTMLDivElement>, item: Node<T>) {
507+
export const UNSTABLE_ListBoxLoadingSentinel = createLeafComponent(LoaderNode, function ListBoxLoadingIndicator<T extends object>(props: ListBoxLoadingSentinelProps, ref: ForwardedRef<HTMLDivElement>, item: Node<T>) {
483508
let state = useContext(ListStateContext)!;
484509
let {isVirtualized} = useContext(CollectionRendererContext);
485510
let {isLoading, onLoadMore, scrollOffset, ...otherProps} = props;

packages/react-aria-components/src/Separator.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,23 @@
1111
*/
1212

1313
import {SeparatorProps as AriaSeparatorProps, useSeparator} from 'react-aria';
14+
import {CollectionNode, createLeafComponent} from '@react-aria/collections';
1415
import {ContextValue, SlotProps, StyleProps, useContextProps} from './utils';
15-
import {createLeafComponent} from '@react-aria/collections';
1616
import {filterDOMProps} from '@react-aria/utils';
17+
import {Key} from '@react-types/shared';
1718
import React, {createContext, ElementType, ForwardedRef} from 'react';
1819

1920
export interface SeparatorProps extends AriaSeparatorProps, StyleProps, SlotProps {}
2021

2122
export const SeparatorContext = createContext<ContextValue<SeparatorProps, HTMLElement>>({});
2223

23-
export const Separator = /*#__PURE__*/ createLeafComponent('separator', function Separator(props: SeparatorProps, ref: ForwardedRef<HTMLElement>) {
24+
class SeparatorNode extends CollectionNode<any> {
25+
constructor(key: Key) {
26+
super('separator', key);
27+
}
28+
}
29+
30+
export const Separator = /*#__PURE__*/ createLeafComponent(SeparatorNode, function Separator(props: SeparatorProps, ref: ForwardedRef<HTMLElement>) {
2431
[props, ref] = useContextProps(props, ref, SeparatorContext);
2532

2633
let {elementType, orientation, style, className, slot, ...otherProps} = props;

0 commit comments

Comments
 (0)