Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 57 additions & 2 deletions packages/@react-aria/collections/src/Hidden.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,21 @@
*/

import {forwardRefType} from '@react-types/shared';
import React, {Context, createContext, forwardRef, JSX, ReactElement, ReactNode, useContext} from 'react';
import React, {Context, createContext, forwardRef, JSX, ReactElement, ReactNode, useContext, useRef} from 'react';
import {useLayoutEffect} from '@react-aria/utils';

// React doesn't understand the <template> element, which doesn't have children like a normal element.
// It will throw an error during hydration when it expects the firstChild to contain content rendered
// on the server, when in reality, the browser will have placed this inside the `content` document fragment.
// This monkey patches the firstChild property for our special hidden template elements to work around this error.
// does the same for appendChild/removeChild/insertBefore as per the issue below
// See https://github.com/facebook/react/issues/19932
if (typeof HTMLTemplateElement !== 'undefined') {
const getFirstChild = Object.getOwnPropertyDescriptor(Node.prototype, 'firstChild')!.get!;
const originalAppendChild = Object.getOwnPropertyDescriptor(Node.prototype, 'appendChild')!.value!;
const originalRemoveChild = Object.getOwnPropertyDescriptor(Node.prototype, 'removeChild')!.value!;
const originalInsertBefore = Object.getOwnPropertyDescriptor(Node.prototype, 'insertBefore')!.value!;

Object.defineProperty(HTMLTemplateElement.prototype, 'firstChild', {
configurable: true,
enumerable: true,
Expand All @@ -31,12 +37,61 @@ if (typeof HTMLTemplateElement !== 'undefined') {
}
}
});

Object.defineProperty(HTMLTemplateElement.prototype, 'appendChild', {
configurable: true,
enumerable: true,
value: function (node) {
if (this.dataset.reactAriaHidden) {
return this.content.appendChild(node);
} else {
return originalAppendChild.call(this, node);
}
}
});

Object.defineProperty(HTMLTemplateElement.prototype, 'removeChild', {
configurable: true,
enumerable: true,
value: function (node) {
if (this.dataset.reactAriaHidden) {
return this.content.removeChild(node);
} else {
return originalRemoveChild.call(this, node);
}
}
});

Object.defineProperty(HTMLTemplateElement.prototype, 'insertBefore', {
configurable: true,
enumerable: true,
value: function (node, child) {
if (this.dataset.reactAriaHidden) {
return this.content.insertBefore(node, child);
} else {
return originalInsertBefore.call(this, node, child);
}
}
});
}

export const HiddenContext: Context<boolean> = createContext<boolean>(false);

export function Hidden(props: {children: ReactNode}): JSX.Element {
let isHidden = useContext(HiddenContext);
let templateRef = useRef<HTMLTemplateElement>(null);
// somehow React might add children to the template and we never hit the reactAriaHidden parts of the above overrides
// so we need to move those children into the content of the template since templates can't have direct children
useLayoutEffect(() => {
let el = templateRef.current;
if (!el?.dataset.reactAriaHidden) {
return;
}
while (el.childNodes.length > 0) {
el.content.appendChild(el.childNodes[0]);
}
}, []);

if (isHidden) {
// Don't hide again if we are already hidden.
return <>{props.children}</>;
Expand All @@ -51,7 +106,7 @@ export function Hidden(props: {children: ReactNode}): JSX.Element {
// In SSR, portals are not supported by React. Instead, always render into a <template>
// element, which the browser will never display to the user. In addition, the
// content is not part of the accessible DOM tree, so it won't affect ids or other accessibility attributes.
return <template data-react-aria-hidden>{children}</template>;
return <template ref={templateRef} data-react-aria-hidden>{children}</template>;
}

/** Creates a component that forwards its ref and returns null if it is in a hidden subtree. */
Expand Down
5 changes: 5 additions & 0 deletions packages/@react-aria/overlays/src/usePreventScroll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ function preventScrollStandard() {
// Safari from scrolling the page. After a small delay, focus the real input and scroll it into view
// ourselves, without scrolling the whole page.
function preventScrollMobileSafari() {
// Set overflow hidden so scrollIntoViewport() (useSelectableCollection) sees isScrollPrevented and
// scrolls only scroll parents instead of calling native scrollIntoView() which moves the window.
let restoreOverflow = setStyle(document.documentElement, 'overflow', 'hidden');

let scrollable: Element;
let allowTouchMove = false;
let onTouchStart = (e: TouchEvent) => {
Expand Down Expand Up @@ -201,6 +205,7 @@ function preventScrollMobileSafari() {
);

return () => {
restoreOverflow();
removeEvents();
style.remove();
HTMLElement.prototype.focus = focus;
Expand Down
49 changes: 43 additions & 6 deletions packages/@react-spectrum/s2/src/TreeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,16 @@ import {ProgressCircle} from './ProgressCircle';
import {raw} from '../style/style-macro' with {type: 'macro'};
import React, {createContext, forwardRef, JSXElementConstructor, ReactElement, ReactNode, useRef} from 'react';
import {Text, TextContext} from './Content';
import {useActionBarContainer} from './ActionBar';
import {useDOMRef} from '@react-spectrum/utils';
import {useLocale, useLocalizedStringFormatter} from 'react-aria';
import {useScale} from './utils';

interface S2TreeProps {
/** Handler that is called when a user performs an action on a row. */
onAction?: (key: Key) => void
onAction?: (key: Key) => void,
/** Provides the ActionBar to display when items are selected in the TreeView. */
renderActionBar?: (selectedKeys: 'all' | Set<Key>) => ReactElement
}

export interface TreeViewProps<T> extends Omit<RACTreeProps<T>, 'style' | 'className' | 'render' | 'onRowAction' | 'selectionBehavior' | 'onScroll' | 'onCellAction' | 'dragAndDropHooks' | keyof GlobalDOMAttributes>, UnsafeStyles, S2TreeProps {
Expand All @@ -71,6 +74,16 @@ interface TreeRendererContextValue {
const TreeRendererContext = createContext<TreeRendererContextValue>({});


const treeViewWrapper = style({
minHeight: 0,
minWidth: 0,
display: 'flex',
isolation: 'isolate',
disableTapHighlight: true,
position: 'relative',
overflow: 'clip'
}, getAllowedOverrides({height: true}));

// TODO: the below is needed so the borders of the top and bottom row isn't cut off if the TreeView is wrapped within a container by always reserving the 2px needed for the
// keyboard focus ring. Perhaps find a different way of rendering the outlines since the top of the item doesn't
// scroll into view due to how the ring is offset. Alternatively, have the tree render the top/bottom outline like it does in Listview
Expand All @@ -94,7 +107,7 @@ const tree = style({
type: 'width',
value: 16
}
}, getAllowedOverrides({height: true}));
});

/**
* A tree view provides users with a way to navigate nested hierarchical information.
Expand All @@ -109,8 +122,11 @@ export const TreeView = /*#__PURE__*/ (forwardRef as forwardRefType)(function Tr
}

let domRef = useDOMRef(ref);
let scrollRef = useRef<HTMLElement | null>(null);

return (
let {selectedKeys, onSelectionChange, actionBar, actionBarHeight} = useActionBarContainer({...props, scrollRef});

let treeContent = (
<Virtualizer
layout={ListLayout}
layoutOptions={{
Expand All @@ -119,15 +135,36 @@ export const TreeView = /*#__PURE__*/ (forwardRef as forwardRefType)(function Tr
<TreeRendererContext.Provider value={{renderer}}>
<Tree
{...props}
style={UNSAFE_style}
className={renderProps => (UNSAFE_className ?? '') + tree({...renderProps}, props.styles)}
style={{
...(!props.renderActionBar ? UNSAFE_style : {}),
paddingBottom: actionBarHeight > 0 ? actionBarHeight + 8 : 0,
scrollPaddingBottom: actionBarHeight > 0 ? actionBarHeight + 8 : 0
}}
className={renderProps => (!props.renderActionBar ? (UNSAFE_className ?? '') : '') + tree({...renderProps})}
selectionBehavior="toggle"
ref={domRef}>
selectedKeys={selectedKeys}
defaultSelectedKeys={undefined}
onSelectionChange={onSelectionChange}
ref={props.renderActionBar ? (scrollRef as any) : domRef}>
{props.children}
</Tree>
</TreeRendererContext.Provider>
</Virtualizer>
);

if (props.renderActionBar) {
return (
<div
ref={domRef}
className={(UNSAFE_className ?? '') + treeViewWrapper(null, props.styles)}
style={UNSAFE_style}>
{treeContent}
{actionBar}
</div>
);
}

return treeContent;
});

const rowBackgroundColor = {
Expand Down
Loading
Loading