Skip to content

Commit 6c272a3

Browse files
devongovettdannify
andauthored
Support variable heights and self sizing collection views (#241)
* Support variable heights and self sizing collection views * Fix tests * awaitDomChange in v2 only * Make description wrap Co-authored-by: Danni <darobins@adobe.com>
1 parent fd7ba7c commit 6c272a3

File tree

14 files changed

+186
-74
lines changed

14 files changed

+186
-74
lines changed

packages/@adobe/spectrum-css-temp/components/menu/index.css

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ governing permissions and limitations under the License.
4646

4747
overflow: auto;
4848

49-
height: 255px;
5049
width: 200px;
50+
max-height: inherit; /* inherit from parent popover */
5151

5252
& .spectrum-Menu-sectionHeading {
5353
/* Support headings as LI */
@@ -111,13 +111,6 @@ governing permissions and limitations under the License.
111111
.spectrum-Menu-itemLabel {
112112
grid-area: text;
113113
align-self: center;
114-
line-height: var(--spectrum-selectlist-option-label-line-height);
115-
overflow: hidden;
116-
text-overflow: ellipsis;
117-
white-space: nowrap;
118-
/* Override user agentstyle sheet for <p> */
119-
margin-block-start: 0px;
120-
margin-block-end: 0px;
121114
}
122115

123116
.spectrum-Menu-itemLabel--wrapping {
@@ -165,8 +158,6 @@ governing permissions and limitations under the License.
165158
}
166159

167160
.spectrum-Menu-itemGrid {
168-
/* hard coded height to match row height in Menu.tsx */
169-
height: 32px;
170161
display: grid;
171162
grid-template-columns: calc(var(--spectrum-selectlist-option-padding) - var(--spectrum-selectlist-border-size-key-focus)) auto 1fr auto auto var(--spectrum-selectlist-option-padding);
172163
grid-template-rows: var(--spectrum-selectlist-option-padding-y) 1fr auto var(--spectrum-selectlist-option-padding-y);
@@ -195,12 +186,6 @@ governing permissions and limitations under the License.
195186
align-self: center;
196187
line-height: 1.3;
197188
font-size: var(--spectrum-global-dimension-size-150);
198-
overflow: hidden;
199-
text-overflow: ellipsis;
200-
white-space: nowrap;
201-
/* Override user agentstyle sheet for <p> */
202-
margin-block-start: 0px;
203-
margin-block-end: 0px;
204189
}
205190
.spectrum-Menu-keyboard {
206191
grid-area: keyboard;

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

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,19 @@ import {chain} from '@react-aria/utils';
1414
import {Collection, Layout, LayoutInfo} from '@react-stately/collections';
1515
import React, {CSSProperties, FocusEvent, HTMLAttributes, Key, useCallback, useEffect, useRef} from 'react';
1616
import {ScrollView} from './ScrollView';
17+
import {useCollectionItem} from './useCollectionItem';
1718
import {useCollectionState} from '@react-stately/collections';
1819

1920
interface CollectionViewProps<T extends object, V> extends HTMLAttributes<HTMLElement> {
2021
children: (type: string, content: T) => V,
2122
layout: Layout<T>,
2223
collection: Collection<T>,
23-
focusedKey?: Key
24+
focusedKey?: Key,
25+
sizeToFit?: 'width' | 'height'
2426
}
2527

2628
export function CollectionView<T extends object, V>(props: CollectionViewProps<T, V>) {
27-
let {children: renderView, layout, collection, focusedKey, ...otherProps} = props;
29+
let {children: renderView, layout, collection, focusedKey, sizeToFit, ...otherProps} = props;
2830
let {
2931
visibleViews,
3032
visibleRect,
@@ -37,9 +39,9 @@ export function CollectionView<T extends object, V>(props: CollectionViewProps<T
3739
collection,
3840
renderView,
3941
renderWrapper: (reusableView) => (
40-
<div key={reusableView.key} role="presentation" style={layoutInfoToStyle(reusableView.layoutInfo)}>
42+
<CollectionItem key={reusableView.key} layoutInfo={reusableView.layoutInfo} collectionManager={reusableView.collectionManager}>
4143
{reusableView.rendered}
42-
</div>
44+
</CollectionItem>
4345
)
4446
});
4547

@@ -90,12 +92,28 @@ export function CollectionView<T extends object, V>(props: CollectionViewProps<T
9092
innerStyle={isAnimating ? {transition: `none ${collectionManager.transitionDuration}ms`} : undefined}
9193
contentSize={contentSize}
9294
visibleRect={visibleRect}
93-
onVisibleRectChange={setVisibleRect}>
95+
onVisibleRectChange={setVisibleRect}
96+
sizeToFit={sizeToFit}>
9497
{visibleViews}
9598
</ScrollView>
9699
);
97100
}
98101

102+
function CollectionItem({layoutInfo, collectionManager, children}) {
103+
let ref = useRef();
104+
useCollectionItem({
105+
layoutInfo,
106+
collectionManager,
107+
ref
108+
});
109+
110+
return (
111+
<div role="presentation" ref={ref} style={layoutInfoToStyle(layoutInfo)}>
112+
{children}
113+
</div>
114+
);
115+
}
116+
99117
function layoutInfoToStyle(layoutInfo: LayoutInfo): CSSProperties {
100118
return {
101119
position: 'absolute',

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

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ interface ScrollViewProps extends HTMLAttributes<HTMLElement> {
1919
visibleRect: Rect,
2020
onVisibleRectChange: (rect: Rect) => void,
2121
children: ReactNode,
22-
innerStyle: CSSProperties
22+
innerStyle: CSSProperties,
23+
sizeToFit: 'width' | 'height'
2324
}
2425

2526
function ScrollView(props: ScrollViewProps, ref: RefObject<HTMLDivElement>) {
@@ -29,6 +30,7 @@ function ScrollView(props: ScrollViewProps, ref: RefObject<HTMLDivElement>) {
2930
onVisibleRectChange,
3031
children,
3132
innerStyle,
33+
sizeToFit,
3234
...otherProps
3335
} = props;
3436

@@ -81,6 +83,25 @@ function ScrollView(props: ScrollViewProps, ref: RefObject<HTMLDivElement>) {
8183

8284
let w = dom.offsetWidth;
8385
let h = dom.offsetHeight;
86+
if (sizeToFit && contentSize.width > 0 && contentSize.height > 0) {
87+
let style = window.getComputedStyle(dom);
88+
89+
if (sizeToFit === 'width') {
90+
w = contentSize.width;
91+
92+
let maxWidth = parseInt(style.maxWidth, 10);
93+
if (!isNaN(maxWidth)) {
94+
w = Math.min(maxWidth, w);
95+
}
96+
} else if (sizeToFit === 'height') {
97+
h = contentSize.height;
98+
99+
let maxHeight = parseInt(style.maxHeight, 10);
100+
if (!isNaN(maxHeight)) {
101+
h = Math.min(maxHeight, h);
102+
}
103+
}
104+
}
84105

85106
if (state.width !== w || state.height !== h) {
86107
state.width = w;
@@ -94,7 +115,7 @@ function ScrollView(props: ScrollViewProps, ref: RefObject<HTMLDivElement>) {
94115
return () => {
95116
window.removeEventListener('resize', updateSize, false);
96117
};
97-
}, [onVisibleRectChange, ref, state.height, state.scrollLeft, state.scrollTop, state.width]);
118+
}, [onVisibleRectChange, ref, state.height, state.scrollLeft, state.scrollTop, state.width, sizeToFit, contentSize.width, contentSize.height]);
98119

99120
useLayoutEffect(() => {
100121
let dom = ref.current;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@
1111
*/
1212

1313
export * from './CollectionView';
14+
export * from './useCollectionItem';
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import {CollectionManager, LayoutInfo, Size} from '@react-stately/collections';
2+
import {RefObject, useCallback, useLayoutEffect} from 'react';
3+
4+
interface CollectionItemOptions<T extends object, V, W> {
5+
layoutInfo: LayoutInfo,
6+
collectionManager: CollectionManager<T, V, W>,
7+
ref: RefObject<HTMLElement>
8+
}
9+
10+
export function useCollectionItem<T extends object, V, W>(options: CollectionItemOptions<T, V, W>) {
11+
let {layoutInfo, collectionManager, ref} = options;
12+
13+
let updateSize = useCallback(() => {
14+
let size = getSize(ref.current);
15+
collectionManager.updateItemSize(layoutInfo.key, size);
16+
}, [collectionManager, layoutInfo.key, ref]);
17+
18+
useLayoutEffect(() => {
19+
if (layoutInfo.estimatedSize) {
20+
updateSize();
21+
}
22+
});
23+
24+
return {updateSize};
25+
}
26+
27+
function getSize(node: HTMLElement) {
28+
// Get bounding rect of all children
29+
let top = Infinity, left = Infinity, bottom = 0, right = 0;
30+
for (let child of Array.from(node.childNodes)) {
31+
let rect = (child as HTMLElement).getBoundingClientRect();
32+
top = Math.min(top, rect.top);
33+
left = Math.min(left, rect.left);
34+
bottom = Math.max(bottom, rect.bottom);
35+
right = Math.max(right, rect.right);
36+
}
37+
38+
return new Size(right - left, bottom - top);
39+
}

packages/@react-spectrum/menu/src/Menu.tsx

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,29 @@
1212

1313
import {classNames, filterDOMProps, useStyleProps} from '@react-spectrum/utils';
1414
import {CollectionView} from '@react-aria/collections';
15-
import {Item, ListLayout, Node, Section} from '@react-stately/collections';
15+
import {Item, ListLayout, Section} from '@react-stately/collections';
1616
import {MenuContext} from './context';
17-
import {MenuDivider, MenuHeading, MenuItem} from './';
17+
import {MenuDivider} from './MenuDivider';
18+
import {MenuHeading} from './MenuHeading';
19+
import {MenuItem} from './MenuItem';
1820
import {mergeProps} from '@react-aria/utils';
1921
import React, {Fragment, useContext, useMemo} from 'react';
2022
import {SpectrumMenuProps} from '@react-types/menu';
2123
import styles from '@adobe/spectrum-css-temp/components/menu/vars.css';
2224
import {useMenu} from '@react-aria/menu';
25+
import {useProvider} from '@react-spectrum/provider';
2326
import {useTreeState} from '@react-stately/tree';
2427

2528
export {Item, Section};
2629

2730
export function Menu<T>(props: SpectrumMenuProps<T>) {
31+
let {scale} = useProvider();
2832
let layout = useMemo(() =>
2933
new ListLayout({
30-
rowHeight: 32, // Feel like we should eventually calculate this number (based on the css)? It should probably get a multiplier in order to gracefully handle scaling
31-
headingHeight: 31 // Same as above
34+
estimatedRowHeight: scale === 'large' ? 48 : 32,
35+
estimatedHeadingHeight: scale === 'large' ? 31 : 25
3236
})
33-
, []);
37+
, [scale]);
3438

3539
let contextProps = useContext(MenuContext);
3640
let completeProps = {
@@ -50,6 +54,7 @@ export function Menu<T>(props: SpectrumMenuProps<T>) {
5054
{...styleProps}
5155
{...menuProps}
5256
focusedKey={state.selectionManager.focusedKey}
57+
sizeToFit="height"
5358
className={
5459
classNames(
5560
styles,
@@ -59,7 +64,7 @@ export function Menu<T>(props: SpectrumMenuProps<T>) {
5964
}
6065
layout={layout}
6166
collection={state.tree}>
62-
{(type, item: Node<T>) => {
67+
{(type, item) => {
6368
if (type === 'section') {
6469
// Only render the Divider if it isn't the first Heading (extra equality check to guard against rerenders)
6570
if (item.key === state.tree.getKeys().next().value) {

packages/@react-spectrum/menu/src/index.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,5 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13-
export * from './MenuDivider';
14-
export * from './MenuHeading';
15-
export * from './MenuItem';
1613
export * from './MenuTrigger';
1714
export * from './Menu';

packages/@react-spectrum/menu/test/Menu.test.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,9 @@ describe('Menu', function () {
160160
${'V2Menu'} | ${V2Menu} | ${{}}
161161
`('$Name allows user to change menu item focus via up/down arrow keys', async function ({Component, props}) {
162162
let tree = renderComponent(Component, {}, props);
163-
await waitForDomChange();
163+
if (Component === V2Menu) {
164+
await waitForDomChange();
165+
}
164166
let menu = tree.getByRole('menu');
165167
let menuItems = within(menu).getAllByRole('menuitemradio');
166168
let selectedItem = menuItems[0];
@@ -178,7 +180,9 @@ describe('Menu', function () {
178180
${'Menu'} | ${Menu} | ${{autoFocus: true, wrapAround: true}}
179181
`('$Name wraps focus from first to last/last to first item if up/down arrow is pressed if wrapAround is true', async function ({Component, props}) {
180182
let tree = renderComponent(Component, {}, props);
181-
await waitForDomChange();
183+
if (Component === V2Menu) {
184+
await waitForDomChange();
185+
}
182186
let menu = tree.getByRole('menu');
183187
let menuItems = within(menu).getAllByRole('menuitemradio');
184188
let firstItem = menuItems[0];
@@ -214,7 +218,9 @@ describe('Menu', function () {
214218
`('$Name supports defaultSelectedKeys (uncontrolled)', async function ({Component, props}) {
215219
// Check that correct menu item is selected by default
216220
let tree = renderComponent(Component, {}, props);
217-
await waitForDomChange();
221+
if (Component === V2Menu) {
222+
await waitForDomChange();
223+
}
218224
let menu = tree.getByRole('menu');
219225
let menuItems = within(menu).getAllByRole('menuitemradio');
220226
let selectedItem = menuItems[3];
@@ -249,7 +255,9 @@ describe('Menu', function () {
249255
`('$Name supports selectedKeys (controlled)', async function ({Component, props}) {
250256
// Check that correct menu item is selected by default
251257
let tree = renderComponent(Component, {}, props);
252-
await waitForDomChange();
258+
if (Component === V2Menu) {
259+
await waitForDomChange();
260+
}
253261
let menu = tree.getByRole('menu');
254262
let menuItems = within(menu).getAllByRole('menuitemradio');
255263
let selectedItem = menuItems[3];

packages/@react-spectrum/typography/src/Text.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ export const Text = React.forwardRef((props: TextProps, ref: RefObject<HTMLEleme
2424
let {styleProps} = useStyleProps({slot: 'text', ...otherProps});
2525

2626
return (
27-
<p {...filterDOMProps(otherProps)} {...styleProps} ref={ref}>
27+
<span {...filterDOMProps(otherProps)} {...styleProps} ref={ref}>
2828
{children}
29-
</p>
29+
</span>
3030
);
3131
});

0 commit comments

Comments
 (0)