Skip to content

Commit

Permalink
Only render ListView when non-zero height (deephaven#1909)
Browse files Browse the repository at this point in the history
  • Loading branch information
bmingles committed Apr 11, 2024
1 parent a111d5a commit d488c92
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 12 deletions.
2 changes: 2 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

49 changes: 37 additions & 12 deletions packages/components/src/spectrum/listView/ListView.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { useMemo } from 'react';
import {
Flex,
ListView as SpectrumListView,
SpectrumListViewProps,
} from '@adobe/react-spectrum';
import { EMPTY_FUNCTION } from '@deephaven/utils';
import {
extractSpectrumHTMLElement,
useContentRect,
useOnScrollRef,
} from '@deephaven/react-hooks';
import cl from 'classnames';
Expand Down Expand Up @@ -68,7 +70,7 @@ export function ListView({
onScroll = EMPTY_FUNCTION,
onSelectionChange,
...spectrumListViewProps
}: ListViewProps): JSX.Element {
}: ListViewProps): JSX.Element | null {
const normalizedItems = useMemo(
() => normalizeItemList(children),
[children]
Expand Down Expand Up @@ -96,20 +98,43 @@ export function ListView({

const scrollRef = useOnScrollRef(onScroll, extractSpectrumHTMLElement);

// Spectrum ListView crashes when it has zero height. Trac the contentRect
// of the parent container and only render the ListView when it has a height.
const { ref: contentRectRef, contentRect } = useContentRect(
extractSpectrumHTMLElement
);

return (
<SpectrumListView
// eslint-disable-next-line react/jsx-props-no-spreading
{...spectrumListViewProps}
ref={scrollRef}
<Flex
ref={contentRectRef}
direction="column"
flex={spectrumListViewProps.flex ?? 1}
minHeight={0}
UNSAFE_className={cl('dh-list-view', UNSAFE_className)}
items={normalizedItems}
selectedKeys={selectedStringKeys}
defaultSelectedKeys={defaultSelectedStringKeys}
disabledKeys={disabledStringKeys}
onSelectionChange={onStringSelectionChange}
>
{renderNormalizedItem}
</SpectrumListView>
{contentRect.height === 0 ? (
// Ensure content has a non-zero height so that the container has a height
// whenever it is visible. This helps differentiate when the container
// height has been set to zero (e.g. when a tab is not visible) vs when
// the container height has not been constrained but has not yet rendered
// the ListView.
<>&nbsp;</>
) : (
<SpectrumListView
// eslint-disable-next-line react/jsx-props-no-spreading
{...spectrumListViewProps}
minHeight={10}
ref={scrollRef}
items={normalizedItems}
selectedKeys={selectedStringKeys}
defaultSelectedKeys={defaultSelectedStringKeys}
disabledKeys={disabledStringKeys}
onSelectionChange={onStringSelectionChange}
>
{renderNormalizedItem}
</SpectrumListView>
)}
</Flex>
);
}

Expand Down
1 change: 1 addition & 0 deletions packages/jsapi-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"build:sass": "sass --embed-sources --load-path=../../node_modules ./src:./dist"
},
"dependencies": {
"@adobe/react-spectrum": "^3.34.1",
"@deephaven/components": "file:../components",
"@deephaven/jsapi-bootstrap": "file:../jsapi-bootstrap",
"@deephaven/jsapi-types": "1.0.0-dev0.33.1",
Expand Down
1 change: 1 addition & 0 deletions packages/react-hooks/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export * from './SpectrumUtils';
export * from './useAsyncInterval';
export * from './useCallbackWithAction';
export * from './useCheckOverflow';
export * from './useContentRect';
export { default as useContextOrThrow } from './useContextOrThrow';
export * from './useDebouncedCallback';
export * from './useDelay';
Expand Down
44 changes: 44 additions & 0 deletions packages/react-hooks/src/useContentRect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { identityExtractHTMLElement } from '@deephaven/utils';
import { useCallback, useRef, useState } from 'react';
import useMappedRef from './useMappedRef';
import useResizeObserver from './useResizeObserver';

export interface UseContentRectResult<T> {
contentRect: DOMRectReadOnly;
ref: (refValue: T) => void;
}

/**
* Returns a callback ref that will track the `contentRect` of a given refValue.
* If the `contentRect` is undefined, it will be set to a new `DOMRect` with
* zeros for all dimensions.
* @param map Optional mapping function to extract an HTMLElement from the given
* refValue
* @returns Content rect and a ref callback
*/
export function useContentRect<T>(
map: (ref: T) => HTMLElement | null = identityExtractHTMLElement
): UseContentRectResult<T> {
const [contentRect, setContentRect] = useState<DOMRectReadOnly>(
new DOMRect()
);

const handleResize = useCallback(
([firstEntry]: ResizeObserverEntry[], _observer: ResizeObserver): void => {
setContentRect(firstEntry?.contentRect ?? new DOMRect());
},
[]
);

const observerRef = useRef<HTMLElement>(null);
useResizeObserver(observerRef.current, handleResize);

const ref = useMappedRef(observerRef, map);

return {
ref,
contentRect,
};
}

export default useContentRect;

0 comments on commit d488c92

Please sign in to comment.