From 3fbe45a86f241d357b75442ec65925ae63994576 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Mon, 20 Sep 2021 16:34:27 -0700 Subject: [PATCH 1/5] CardView followup patch --- packages/@react-aria/virtualizer/package.json | 1 + .../virtualizer/src/Virtualizer.tsx | 3 +- .../@react-spectrum/cards/intl/ar-AE.json | 4 +- .../@react-spectrum/cards/src/BaseLayout.tsx | 132 +++++-- packages/@react-spectrum/cards/src/Card.tsx | 1 - .../@react-spectrum/cards/src/CardBase.tsx | 4 +- .../@react-spectrum/cards/src/CardView.tsx | 63 +++- .../cards/src/GalleryLayout.tsx | 147 ++++++-- .../@react-spectrum/cards/src/GridLayout.tsx | 62 ++- .../cards/src/WaterfallLayout.tsx | 56 +-- .../@react-spectrum/cards/src/cardview.css | 9 + packages/@react-spectrum/cards/src/index.ts | 1 + .../cards/stories/GalleryCardView.stories.tsx | 14 +- .../cards/stories/GridCardView.stories.tsx | 356 ++++++++++-------- .../stories/WaterfallCardView.stories.tsx | 14 +- .../cards/test/CardView.test.js | 102 ++--- packages/@react-spectrum/list/intl/ar-AE.json | 4 +- .../@react-spectrum/listbox/intl/ar-AE.json | 4 +- .../@react-spectrum/table/intl/ar-AE.json | 4 +- packages/@react-types/cards/src/index.d.ts | 5 +- 20 files changed, 597 insertions(+), 389 deletions(-) diff --git a/packages/@react-aria/virtualizer/package.json b/packages/@react-aria/virtualizer/package.json index d6f2fdb8cbd..541179b6783 100644 --- a/packages/@react-aria/virtualizer/package.json +++ b/packages/@react-aria/virtualizer/package.json @@ -19,6 +19,7 @@ "dependencies": { "@babel/runtime": "^7.6.2", "@react-aria/i18n": "^3.3.2", + "@react-aria/interactions": "^3.5.1", "@react-aria/utils": "^3.8.2", "@react-stately/virtualizer": "^3.1.5", "@react-types/shared": "^3.8.0" diff --git a/packages/@react-aria/virtualizer/src/Virtualizer.tsx b/packages/@react-aria/virtualizer/src/Virtualizer.tsx index 7ad6b1a1f4a..edab61642d8 100644 --- a/packages/@react-aria/virtualizer/src/Virtualizer.tsx +++ b/packages/@react-aria/virtualizer/src/Virtualizer.tsx @@ -12,6 +12,7 @@ import {Collection} from '@react-types/shared'; import {focusWithoutScrolling, mergeProps, useLayoutEffect} from '@react-aria/utils'; +import {getInteractionModality} from '@react-aria/interactions'; import {Layout, Rect, ReusableView, useVirtualizerState, VirtualizerState} from '@react-stately/virtualizer'; import React, {FocusEvent, HTMLAttributes, Key, ReactElement, RefObject, useCallback, useEffect, useRef} from 'react'; import {ScrollView} from './ScrollView'; @@ -126,7 +127,7 @@ export function useVirtualizer(props: VirtualizerOptions return; } - if (focusedKey !== lastFocusedKey.current) { + if (focusedKey !== lastFocusedKey.current && getInteractionModality() !== 'pointer') { if (scrollToItem) { scrollToItem(focusedKey); } else { diff --git a/packages/@react-spectrum/cards/intl/ar-AE.json b/packages/@react-spectrum/cards/intl/ar-AE.json index 4c058195ca9..c0608e89453 100644 --- a/packages/@react-spectrum/cards/intl/ar-AE.json +++ b/packages/@react-spectrum/cards/intl/ar-AE.json @@ -1,4 +1,4 @@ { - "loading": "جار التحميل...", - "loadingMore": "جار تحميل المزيد..." + "loading": "جارٍ التحميل...", + "loadingMore": "جارٍ تحميل المزيد" } diff --git a/packages/@react-spectrum/cards/src/BaseLayout.tsx b/packages/@react-spectrum/cards/src/BaseLayout.tsx index 92689b15170..72e48791cd1 100644 --- a/packages/@react-spectrum/cards/src/BaseLayout.tsx +++ b/packages/@react-spectrum/cards/src/BaseLayout.tsx @@ -10,9 +10,10 @@ * governing permissions and limitations under the License. */ -import {Collection, Direction, KeyboardDelegate, Node} from '@react-types/shared'; +import {Direction, KeyboardDelegate, Node} from '@react-types/shared'; +import {GridCollection} from '@react-stately/grid'; +import {InvalidationContext, Layout, LayoutInfo, Rect, Size} from '@react-stately/virtualizer'; import {Key} from 'react'; -import {Layout, LayoutInfo, Rect, Size} from '@react-stately/virtualizer'; export interface BaseLayoutOptions { collator?: Intl.Collator @@ -22,8 +23,8 @@ export class BaseLayout extends Layout> implements KeyboardDelegate { protected contentSize: Size; protected layoutInfos: Map; protected collator: Intl.Collator; - protected lastCollection: Collection>; - collection: Collection>; + protected lastCollection: GridCollection; + collection: GridCollection; isLoading: boolean; disabledKeys: Set = new Set(); direction: Direction; @@ -35,6 +36,33 @@ export class BaseLayout extends Layout> implements KeyboardDelegate { this.lastCollection = null; } + validate(invalidationContext: InvalidationContext, unknown>) { + this.collection = this.virtualizer.collection as GridCollection; + this.buildCollection(invalidationContext); + + // Remove layout info that doesn't exist in new collection + if (this.lastCollection) { + for (let key of this.lastCollection.getKeys()) { + if (!this.collection.getItem(key)) { + this.layoutInfos.delete(key); + } + } + + if (!this.isLoading) { + this.layoutInfos.delete('loader'); + } + + if (this.collection.size > 0) { + this.layoutInfos.delete('placeholder'); + } + } + + this.lastCollection = this.collection; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + buildCollection(invalidationContext?: InvalidationContext, unknown>) {} + getContentSize() { return this.contentSize; } @@ -101,39 +129,47 @@ export class BaseLayout extends Layout> implements KeyboardDelegate { } getKeyBelow(key: Key) { - let layoutInfo = this.getLayoutInfo(key); + // Expected key is the currently focused cell so we need the parent row key + let parentRowKey = this.collection.getItem(key).parentKey; + let layoutInfo = this.getLayoutInfo(parentRowKey); let rect = new Rect(layoutInfo.rect.x, layoutInfo.rect.maxY + 1, layoutInfo.rect.width, this.virtualizer.visibleRect.height); - - return this._findClosest(layoutInfo.rect, rect)?.key; + let closestRow = this.collection.getItem(this._findClosest(layoutInfo.rect, rect)?.key); + return closestRow?.childNodes[0]?.key; } getKeyAbove(key: Key) { - let layoutInfo = this.getLayoutInfo(key); + // Expected key is the currently focused cell so we need the parent row key + let parentRowKey = this.collection.getItem(key).parentKey; + let layoutInfo = this.getLayoutInfo(parentRowKey); let rect = new Rect(layoutInfo.rect.x, 0, layoutInfo.rect.width, layoutInfo.rect.y - 1); - - return this._findClosest(layoutInfo.rect, rect)?.key; + let closestRow = this.collection.getItem(this._findClosest(layoutInfo.rect, rect)?.key); + return closestRow?.childNodes[0]?.key; } getKeyRightOf(key: Key) { - key = this.direction === 'rtl' ? this.collection.getKeyBefore(key) : this.collection.getKeyAfter(key); + // Expected key is the currently focused cell so we need the parent row key + let parentRowKey = this.collection.getItem(key).parentKey; + key = this.direction === 'rtl' ? this.collection.getKeyBefore(parentRowKey) : this.collection.getKeyAfter(parentRowKey); + while (key != null) { let item = this.collection.getItem(key); // Don't check if item is disabled because we want to be able to focus disabled items in a grid (double check this) if (item.type === 'item') { - return key; + return item.childNodes[0].key; } - key = this.direction === 'rtl' ? this.collection.getKeyBefore(key) : this.collection.getKeyAfter(key); } } getKeyLeftOf(key: Key) { - key = this.direction === 'rtl' ? this.collection.getKeyAfter(key) : this.collection.getKeyBefore(key); + // Expected key is the currently focused cell so we need the parent row key + let parentRowKey = this.collection.getItem(key).parentKey; + key = this.direction === 'rtl' ? this.collection.getKeyAfter(parentRowKey) : this.collection.getKeyBefore(parentRowKey); while (key != null) { let item = this.collection.getItem(key); // Don't check if item is disabled because we want to be able to focus disabled items in a grid (double check this) if (item.type === 'item') { - return key; + return item.childNodes[0].key; } key = this.direction === 'rtl' ? this.collection.getKeyAfter(key) : this.collection.getKeyBefore(key); @@ -141,43 +177,69 @@ export class BaseLayout extends Layout> implements KeyboardDelegate { } getFirstKey() { - return this.collection.getFirstKey(); + let firstRow = this.collection.getItem(this.collection.getFirstKey()); + return firstRow.childNodes[0].key; } getLastKey() { - return this.collection.getLastKey(); + let lastRow = this.collection.getItem(this.collection.getLastKey()); + return lastRow.childNodes[0].key; } + // TODO: pretty unwieldy because it needs to bounce back and forth between the parent key and the child key + // Perhaps have layoutInfo store childKey as well so we don't need to do this? Or maybe make the layoutInfos be the cells instead of the rows? getKeyPageAbove(key: Key) { - let layoutInfo = this.getLayoutInfo(key); + // Expected key is the currently focused cell so we need the parent row key + let parentRowKey = this.collection.getItem(key).parentKey; + let layoutInfo = this.getLayoutInfo(parentRowKey); if (layoutInfo) { let pageY = Math.max(0, layoutInfo.rect.y + layoutInfo.rect.height - this.virtualizer.visibleRect.height); - while (layoutInfo && layoutInfo.rect.y > pageY) { - let keyAbove = this.getKeyAbove(layoutInfo.key); - layoutInfo = this.getLayoutInfo(keyAbove); + // If the node is so large that it spans multiple page heights, return the key of the item immediately above + // Otherwise keep going up until we exceed a single page height worth of nodes + let keyAbove = this.collection.getItem(this.getKeyAbove(key))?.parentKey; + layoutInfo = this.getLayoutInfo(keyAbove); + + if (layoutInfo && layoutInfo.rect.y > pageY) { + while (layoutInfo && layoutInfo.rect.y > pageY) { + let childKey = this.collection.getItem(layoutInfo.key).childNodes[0].key; + let keyAbove = this.collection.getItem(this.getKeyAbove(childKey))?.parentKey; + layoutInfo = this.getLayoutInfo(keyAbove); + } } if (layoutInfo) { - return layoutInfo.key; + let childKey = this.collection.getItem(layoutInfo.key).childNodes[0].key; + return childKey; } } return this.getFirstKey(); } + // TODO: pretty unwieldy because it needs to bounce back and forth between the parent key and the child key + // Perhaps have layoutInfo store childKey as well so we don't need to do this? getKeyPageBelow(key: Key) { - let layoutInfo = this.getLayoutInfo(key != null ? key : this.getFirstKey()); - + // Expected key is the currently focused cell so we need the parent row key + let parentRowKey = this.collection.getItem(key).parentKey; + let layoutInfo = this.getLayoutInfo(parentRowKey); if (layoutInfo) { let pageY = Math.min(this.virtualizer.contentSize.height, layoutInfo.rect.y - layoutInfo.rect.height + this.virtualizer.visibleRect.height); - while (layoutInfo && layoutInfo.rect.y < pageY) { - let keyBelow = this.getKeyBelow(layoutInfo.key); - layoutInfo = this.getLayoutInfo(keyBelow); + // If the node is so large that it spans multiple page heights, return the key of the item immediately below + // Otherwise keep going up until we exceed a single page height worth of nodes + let keyBelow = this.collection.getItem(this.getKeyBelow(key))?.parentKey; + layoutInfo = this.getLayoutInfo(keyBelow); + if (layoutInfo && layoutInfo.rect.y < pageY) { + while (layoutInfo && layoutInfo.rect.y < pageY) { + let childKey = this.collection.getItem(layoutInfo.key).childNodes[0].key; + let keyBelow = this.collection.getItem(this.getKeyBelow(childKey))?.parentKey; + layoutInfo = this.getLayoutInfo(keyBelow); + } } if (layoutInfo) { - return layoutInfo.key; + let childKey = this.collection.getItem(layoutInfo.key).childNodes[0].key; + return childKey; } } @@ -190,12 +252,18 @@ export class BaseLayout extends Layout> implements KeyboardDelegate { } let collection = this.collection; - let key = fromKey || this.getFirstKey(); + let key = fromKey ?? this.getFirstKey(); + + let startItem = collection.getItem(key); + key = startItem.parentKey; + while (key != null) { let item = collection.getItem(key); - let substring = item.textValue.slice(0, search.length); - if (item.textValue && this.collator.compare(substring, search) === 0) { - return key; + if (item.textValue) { + let substring = item.textValue.slice(0, search.length); + if (this.collator.compare(substring, search) === 0) { + return [...item.childNodes][0].key; + } } key = this.collection.getKeyAfter(key); diff --git a/packages/@react-spectrum/cards/src/Card.tsx b/packages/@react-spectrum/cards/src/Card.tsx index 1869b5cfc36..aadae40bccb 100644 --- a/packages/@react-spectrum/cards/src/Card.tsx +++ b/packages/@react-spectrum/cards/src/Card.tsx @@ -45,7 +45,6 @@ Card.getCollectionNode = function* getCollectionNode(props, context: any): Ge }; }; -// We don't want getCollectionNode to show up in the type definition // eslint-disable-next-line let _Card = Card as (props, ref) => JSX.Element; export {_Card as Card}; diff --git a/packages/@react-spectrum/cards/src/CardBase.tsx b/packages/@react-spectrum/cards/src/CardBase.tsx index 6de42f942bb..112f08721f5 100644 --- a/packages/@react-spectrum/cards/src/CardBase.tsx +++ b/packages/@react-spectrum/cards/src/CardBase.tsx @@ -53,7 +53,7 @@ function CardBase(props: CardBaseProps, ref: DOMRef(); // cards are only interactive if there is a selection manager and it allows selection - let {hoverProps, isHovered} = useHover({isDisabled: manager === undefined || manager?.selectionMode === 'none'}); + let {hoverProps, isHovered} = useHover({isDisabled: manager === undefined || manager?.selectionMode === 'none' || isDisabled}); let [isFocused, setIsFocused] = useState(false); let {focusWithinProps} = useFocusWithin({ onFocusWithinChange: setIsFocused, @@ -75,7 +75,7 @@ function CardBase(props: CardBaseProps, ref: DOMRef(props: SpectrumCardViewProps, ref: DOMRef) { + let {scale} = useProvider(); let {styleProps} = useStyleProps(props); let domRef = useDOMRef(ref); let { @@ -39,6 +42,15 @@ function CardView(props: SpectrumCardViewProps, ref: DOMRef let collator = useCollator({usage: 'search', sensitivity: 'base'}); let isLoading = loadingState === 'loading' || loadingState === 'loadingMore'; let cardViewLayout = useMemo(() => typeof layout === 'function' ? new layout({collator}) : layout, [layout, collator]); + let layoutType = cardViewLayout.layoutType; + + if (typeof layout === 'function') { + if (layoutType === 'grid') { + cardViewLayout.itemPadding = scale === 'large' ? 116 : 95; + } else if (layoutType === 'gallery') { + cardViewLayout.itemPadding = scale === 'large' ? 143 : 114; + } + } let formatMessage = useMessageFormatter(intlMessages); let {direction} = useLocale(); @@ -48,13 +60,17 @@ function CardView(props: SpectrumCardViewProps, ref: DOMRef columnCount: 1, items: [...collection].map(item => ({ // Makes the Grid row use the keys the user provides to the cards so that selection change via interactions returns the card keys - type: 'item', - key: item.key, + ...item, + hasChildNodes: true, childNodes: [{ - ...item, - index: 0, + key: `cell-${item.key}`, type: 'cell', - key: `cell-${item.key}` + value: null, + level: 0, + rendered: null, + textValue: item.textValue, + hasChildNodes: false, + childNodes: [] }] })) }), [collection]); @@ -64,10 +80,7 @@ function CardView(props: SpectrumCardViewProps, ref: DOMRef collection: gridCollection }); - // TODO: need to fix the typescript here, perhaps add a new type in Card types which is a Layout w/ these properties - // TODO: double check that this is the correct collection being set (we wanna use the list collection for the keyboard delegate?) - // If not, update the gridlayout code to use the gridCollection - cardViewLayout.collection = collection; + cardViewLayout.collection = gridCollection; cardViewLayout.disabledKeys = state.disabledKeys; cardViewLayout.isLoading = isLoading; cardViewLayout.direction = direction; @@ -75,23 +88,39 @@ function CardView(props: SpectrumCardViewProps, ref: DOMRef let {gridProps} = useGrid({ ...props, isVirtualized: true, - // TODO: fix the typescript here, layout definition need to show that it implements keyboard delegate keyboardDelegate: cardViewLayout }, state, domRef); + type View = ReusableView, unknown>; + let renderWrapper = (parent: View, reusableView: View) => ( + + ); + + let focusedKey = state.selectionManager.focusedKey; + let focusedItem = gridCollection.getItem(state.selectionManager.focusedKey); + if (focusedItem?.parentKey != null) { + focusedKey = focusedItem.parentKey; + } + // TODO: does aria-row count and aria-col count need to be modified? Perhaps aria-col count needs to be omitted return ( + onLoadMore={onLoadMore} + renderWrapper={renderWrapper}> {(type, item) => { if (type === 'item') { return ( @@ -124,7 +153,6 @@ function CardView(props: SpectrumCardViewProps, ref: DOMRef ); } -// TODO filler centerwrapper from ListView, check if is valid function CenteredWrapper({children}) { let {state} = useCardViewContext(); return ( @@ -143,6 +171,7 @@ function InternalCard(props) { let { item } = props; + let cellNode = [...item.childNodes][0]; let {state, cardOrientation, isQuiet, layout} = useCardViewContext(); let layoutType = layout.layoutType; @@ -156,7 +185,7 @@ function InternalCard(props) { }, state, rowRef); let {gridCellProps} = useGridCell({ - node: item, + node: cellNode, focusMode: 'cell' }, state, unwrappedRef); diff --git a/packages/@react-spectrum/cards/src/GalleryLayout.tsx b/packages/@react-spectrum/cards/src/GalleryLayout.tsx index 54b82cf87d8..3ba2774934e 100644 --- a/packages/@react-spectrum/cards/src/GalleryLayout.tsx +++ b/packages/@react-spectrum/cards/src/GalleryLayout.tsx @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import {BaseLayout, BaseLayoutOptions} from './BaseLayout'; +import {BaseLayout, BaseLayoutOptions} from './'; import {LayoutInfo, Rect, Size} from '@react-stately/virtualizer'; export interface GalleryLayoutOptions extends BaseLayoutOptions { @@ -19,20 +19,36 @@ export interface GalleryLayoutOptions extends BaseLayoutOptions { // */ // cardSize?: 'S' | 'M' | 'L', /** - * The the default row height. + * The the default row height. Note this must be larger than the min item height. * @default 208 */ idealRowHeight?: number, /** * The spacing between items. - * @default 24 x 32 + * @default 18 x 18 */ itemSpacing?: Size, /** * The vertical padding for an item. * @default 114 */ - itemPadding?: Size + itemPadding?: number, + /** + * Minimum size for a item in the grid. + * @default 136 x 136 + */ + minItemSize?: Size, + /** + * Target for adding extra weight to elements during linear partitioning. Anything with an aspect ratio smaler than this value + * will be targeted. + * @type {number} + */ + threshold?: number, + /** + * The margin around the grid view between the edges and the items. + * @default 24 + */ + margin?: number // TODO: Perhaps should accept Responsive } // TODO: copied from V2, update this with the proper spectrum values @@ -44,23 +60,27 @@ const DEFAULT_OPTIONS = { itemSpacing: new Size(8, 16), // TODO: will need to update as well itemPadding: 24, - dropSpacing: 50 + dropSpacing: 50, + margin: 8 }, L: { idealRowHeight: 208, minItemSize: new Size(136, 136), - itemSpacing: new Size(24, 32), + itemSpacing: new Size(18, 18), // TODO: updated to work with new v3 cards (there is additional space required for the descriptions if there is a description) itemPadding: 114, - dropSpacing: 100 + dropSpacing: 100, + margin: 24 } }; export class GalleryLayout extends BaseLayout { protected idealRowHeight: number; - // TODO: should this have had a margin option? v2 seems to use itemSpacing + protected margin: number; protected itemSpacing: Size; - protected itemPadding: number; + itemPadding: number; + protected minItemSize: Size; + protected threshold: number; constructor(options: GalleryLayoutOptions = {}) { super(options); @@ -69,51 +89,95 @@ export class GalleryLayout extends BaseLayout { this.idealRowHeight = options.idealRowHeight || DEFAULT_OPTIONS[cardSize].idealRowHeight; this.itemSpacing = options.itemSpacing || DEFAULT_OPTIONS[cardSize].itemSpacing; this.itemPadding = options.itemPadding != null ? options.itemPadding : DEFAULT_OPTIONS[cardSize].itemPadding; + this.minItemSize = options.minItemSize || DEFAULT_OPTIONS[cardSize].minItemSize; + this.threshold = options.threshold || 1; + this.margin = options.margin != null ? options.margin : DEFAULT_OPTIONS[cardSize].margin; } get layoutType() { return 'gallery'; } - validate() { - this.collection = this.virtualizer.collection; - this.buildCollection(); - - // Remove layout info that doesn't exist in new collection - if (this.lastCollection) { - for (let key of this.lastCollection.getKeys()) { - if (!this.collection.getItem(key)) { - this.layoutInfos.delete(key); + /** + * Takes a row of widths and if there are any widths smaller than the min-width, leech width starting from + * the widest in the row until it can't give anymore, then move to the second widest and so forth. + * Do this until all assets meet the min-width. + * */ + _distributeWidths(widths) { + // create a copy of the widths array and sort it largest to smallest + let sortedWidths = widths.concat().sort((a, b) => a[1] > b[1] ? -1 : 1); + for (let width of widths) { + // for each width, if it's smaller than the min width + if (width[1] < this.minItemSize.width) { + // then figure out how much smaller + let delta = this.minItemSize.width - width[1]; + for (let item of sortedWidths) { + // go from the largest width in the row to the smallest + // if the width is greater than the min width + if (widths[item[0]][1] > this.minItemSize.width) { + // subtract the delta from the width, if it's still greater than the min width + // then we have finished, subtract the delta permanently from that width + if (widths[item[0]][1] - delta > this.minItemSize.width) { + widths[item[0]][1] -= delta; + delta = 0; + break; + } else { + // otherwise, we take as much as we can from the current width and then move on to + // the next largest and take some width from it + let maxChange = widths[item[0]][1] - this.minItemSize.width; + delta -= maxChange; + widths[item[0]][1] -= maxChange; + } + } } - } - - if (!this.isLoading) { - this.layoutInfos.delete('loader'); + if (delta > 0) { + return false; + } + // force the width to be the min width that we just rebalanced for + width[1] = this.minItemSize.width; } } - - this.lastCollection = this.collection; + return true; } buildCollection() { let visibleWidth = this.virtualizer.visibleRect.width; let visibleHeight = this.virtualizer.visibleRect.height; - let y = this.itemSpacing.height; - let availableWidth = visibleWidth - this.itemSpacing.width * 2; + let y = this.margin; + let availableWidth = visibleWidth - this.margin * 2; // Compute aspect ratios for all of the items, and the total width if all items were on in a single row. let ratios = []; let totalWidth = 0; + let minRatio = this.minItemSize.width / this.minItemSize.height; + let maxRatio = availableWidth / this.minItemSize.height; + for (let node of this.collection) { let ratio = node.props.width / node.props.height; + if (ratio < minRatio) { + ratio = minRatio; + } else if (ratio > maxRatio && ratio !== minRatio) { + ratio = maxRatio; + } + + let itemWidth = ratio * this.minItemSize.height; ratios.push(ratio); - totalWidth += ratio * this.idealRowHeight; + totalWidth += itemWidth; } + totalWidth += this.itemSpacing.width * (this.collection.size - 1); + // Determine how many rows we'll need, and partition the items into rows // using the aspect ratios as weights. - let rows = Math.max(1, Math.round(totalWidth / availableWidth)); - let partition = linearPartition(ratios, rows); + let rows = Math.max(1, Math.ceil(totalWidth / availableWidth)); + // if the available width can't hold two items, then every item will get its own row + // this leads to a faster run through linear partition and more dependable output for small row widths + if (availableWidth <= (this.minItemSize.width * 2) + (this.itemPadding * 2)) { + rows = this.collection.size; + } + + let weightedRatios = ratios.map(ratio => ratio < this.threshold ? ratio + (0.5 * (1 / ratio)) : ratio); + let partition = linearPartition(weightedRatios, rows); let index = 0; for (let row of partition) { @@ -124,18 +188,29 @@ export class GalleryLayout extends BaseLayout { } // Determine the row height based on the total available width and weight of this row. - let rowHeight = (availableWidth - (row.length - 1) * this.itemSpacing.width) / totalWeight; - if (row === partition[partition.length - 1] && rowHeight > this.idealRowHeight * 2) { - rowHeight = this.idealRowHeight; + let bestRowHeight = (availableWidth - (row.length - 1) * this.itemSpacing.width) / totalWeight; + + // if this is the last row and the row height is >2x the ideal row height, then cap to the ideal height + // probably doing this because if the last row has one extremely tall image, then the row becomes huge + // though that can happen anywhere if a row has lots of tall images... so i'm not sure why this one matters + if (row === partition[partition.length - 1] && bestRowHeight > this.idealRowHeight * 2) { + bestRowHeight = this.idealRowHeight; } + let itemHeight = Math.round(bestRowHeight) + this.itemPadding; + let x = this.margin; - let itemHeight = Math.round(rowHeight) + this.itemPadding; - let x = this.itemSpacing.width; + // if any items are going to end up too small, add a bit of width to them and subtract it from wider objects + let widths = []; + for (let j = index; j < index + row.length; j++) { + let width = Math.round(bestRowHeight * ratios[j]); + widths.push([j - index, width]); + } + this._distributeWidths(widths); // Create items for this row. for (let j = index; j < index + row.length; j++) { - let node = this.collection.at(j); - let itemWidth = Math.round(rowHeight * ratios[j]); + let node = this.collection.rows[j]; + let itemWidth = Math.max(widths[j - index][1], this.minItemSize.width); let rect = new Rect(x, y, itemWidth, itemHeight); let layoutInfo = new LayoutInfo(node.type, node.key, rect); this.layoutInfos.set(node.key, layoutInfo); diff --git a/packages/@react-spectrum/cards/src/GridLayout.tsx b/packages/@react-spectrum/cards/src/GridLayout.tsx index 2814d45ec67..2082fc47109 100644 --- a/packages/@react-spectrum/cards/src/GridLayout.tsx +++ b/packages/@react-spectrum/cards/src/GridLayout.tsx @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import {BaseLayout, BaseLayoutOptions} from './BaseLayout'; +import {BaseLayout, BaseLayoutOptions} from './'; import {Key} from 'react'; import {LayoutInfo, Rect, Size} from '@react-stately/virtualizer'; import {Node} from '@react-types/shared'; @@ -37,7 +37,7 @@ export interface GridLayoutOptions extends BaseLayoutOptions { margin?: number, // TODO: Perhaps should accept Responsive /** * The minimum space required between items. - * @default 24 x 48 + * @default 18 x 18 */ minSpace?: Size, /** @@ -47,7 +47,7 @@ export interface GridLayoutOptions extends BaseLayoutOptions { maxColumns?: number, /** * The vertical padding for an item. - * @default 52 + * @default 95 */ itemPadding?: number } @@ -60,18 +60,18 @@ const DEFAULT_OPTIONS = { minItemSize: new Size(96, 96), maxItemSize: new Size(Infinity, Infinity), margin: 8, - minSpace: new Size(8, 16), + minSpace: new Size(6, 6), maxColumns: Infinity, dropSpacing: 50 }, L: { // TODO: for now bumping this higher since the new cards have more stuff in the content area. // Will need to ask Spectrum what these values should be. Used to be 52. Do the same for S above - itemPadding: 100, + itemPadding: 95, minItemSize: new Size(208, 208), maxItemSize: new Size(Infinity, Infinity), margin: 24, - minSpace: new Size(24, 48), + minSpace: new Size(18, 18), maxColumns: Infinity, dropSpacing: 100 } @@ -83,7 +83,7 @@ export class GridLayout extends BaseLayout { protected margin: number; protected minSpace: Size; protected maxColumns: number; - protected itemPadding: number; + itemPadding: number; protected itemSize: Size; protected numColumns: number; protected numRows: number; @@ -115,7 +115,7 @@ export class GridLayout extends BaseLayout { let itemWidth = this.itemSize.width + this.horizontalSpacing; return Math.max(0, Math.min( - this.collection.size + (allowInsertingAtEnd ? 1 : 0), + this.collection.size - (allowInsertingAtEnd ? 0 : 1), Math.floor(y / itemHeight) * this.numColumns + Math.floor((x - this.horizontalSpacing) / itemWidth) ) ); @@ -138,9 +138,8 @@ export class GridLayout extends BaseLayout { // The approach from v2 uses indexes where other v3 layouts iterate through every node/root node. This feels more efficient let firstVisibleItem = this.getIndexAtPoint(rect.x, rect.y); let lastVisibleItem = this.getIndexAtPoint(rect.maxX, rect.maxY); - - for (let index = firstVisibleItem; index < lastVisibleItem; index++) { - let keyFromIndex = this.collection.at(index).key; + for (let index = firstVisibleItem; index <= lastVisibleItem; index++) { + let keyFromIndex = this.collection.rows[index].key; let layoutInfo = this.layoutInfos.get(keyFromIndex); if (this.isVisible(layoutInfo, rect)) { res.push(layoutInfo); @@ -157,35 +156,13 @@ export class GridLayout extends BaseLayout { return res; } - validate() { - this.collection = this.virtualizer.collection; - this.buildCollection(); - - // Remove layout info that doesn't exist in new collection - if (this.lastCollection) { - for (let key of this.lastCollection.getKeys()) { - if (!this.collection.getItem(key)) { - this.layoutInfos.delete(key); - } - } - - // TODO: dunno why I had to add this but it gets rid of the loading spinner, otherwise it remains on initial async load - // Can't figure out why ListLayout didn't need that - if (!this.isLoading) { - this.layoutInfos.delete('loader'); - } - } - - this.lastCollection = this.collection; - } - buildCollection() { let visibleWidth = this.virtualizer.visibleRect.width; let visibleHeight = this.virtualizer.visibleRect.height; // Compute the number of rows and columns needed to display the content let availableWidth = visibleWidth - this.margin * 2; - let columns = Math.floor(availableWidth / (this.minItemSize.width + this.minSpace.width)); + let columns = Math.floor((availableWidth + this.minSpace.width) / (this.minItemSize.width + this.minSpace.width)); this.numColumns = Math.max(1, Math.min(this.maxColumns, columns)); this.numRows = Math.ceil(this.collection.size / this.numColumns); @@ -204,7 +181,7 @@ export class GridLayout extends BaseLayout { this.itemSize = new Size(itemWidth, itemHeight); // Compute the horizontal spacing and content height - this.horizontalSpacing = Math.floor((visibleWidth - this.numColumns * this.itemSize.width) / (this.numColumns + 1)); + this.horizontalSpacing = this.numColumns < 2 ? 0 : Math.floor((availableWidth - this.numColumns * this.itemSize.width) / (this.numColumns - 1)); let y = this.margin; let index = 0; @@ -247,6 +224,7 @@ export class GridLayout extends BaseLayout { y = this.margin + row * (this.itemSize.height + this.minSpace.height); let rect = new Rect(x, y, this.itemSize.width, this.itemSize.height); + // TODO: Perhaps have it so that the child key for each row is stored with the layoutInfo? let layoutInfo = new LayoutInfo(node.type, node.key, rect); this.layoutInfos.set(node.key, layoutInfo); return layoutInfo; @@ -256,28 +234,30 @@ export class GridLayout extends BaseLayout { // then return the key that occupies the row + column below. This can be done by figuring out how many cards exist per column then dividing the // collection contents by that number (which will give us the row distribution) getKeyBelow(key: Key) { + // Expected key is the currently focused cell so we need the parent row key + let parentRowKey = this.collection.getItem(key).parentKey; let indexRowBelow; - let keyArray = [...this.collection.getKeys()]; - let index = keyArray.findIndex(k => k === key); + let index = this.collection.rows.findIndex(card => card.key === parentRowKey); if (index !== -1) { indexRowBelow = index + this.numColumns; } else { return null; } - return this.collection.at(indexRowBelow)?.key || null; + return this.collection.rows[indexRowBelow]?.childNodes[0].key || null; } getKeyAbove(key: Key) { + // Expected key is the currently focused cell so we need the parent row key + let parentRowKey = this.collection.getItem(key).parentKey; let indexRowAbove; - let keyArray = [...this.collection.getKeys()]; - let index = keyArray.findIndex(k => k === key); + let index = this.collection.rows.findIndex(card => card.key === parentRowKey); if (index !== -1) { indexRowAbove = index - this.numColumns; } else { return null; } - return this.collection.at(indexRowAbove)?.key || null; + return this.collection.rows[indexRowAbove]?.childNodes[0].key || null; } } diff --git a/packages/@react-spectrum/cards/src/WaterfallLayout.tsx b/packages/@react-spectrum/cards/src/WaterfallLayout.tsx index 25a8921ab80..ab425f6528d 100644 --- a/packages/@react-spectrum/cards/src/WaterfallLayout.tsx +++ b/packages/@react-spectrum/cards/src/WaterfallLayout.tsx @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import {BaseLayout, BaseLayoutOptions} from './BaseLayout'; +import {BaseLayout, BaseLayoutOptions} from './'; import {InvalidationContext, LayoutInfo, Rect, Size} from '@react-stately/virtualizer'; import {Key} from 'react'; import {KeyboardDelegate, Node} from '@react-types/shared'; @@ -26,14 +26,14 @@ export interface WaterfallLayoutOptions extends BaseLayoutOptions { * @default Infinity */ maxItemSize?: Size, - // /** - // * The margin around the grid view between the edges and the items. - // * @default 24 - // */ - // margin?: number, // TODO: Perhaps should accept Responsive + /** + * The margin around the grid view between the edges and the items. + * @default 24 + */ + margin?: number, // TODO: Perhaps should accept Responsive /** * The minimum space required between items. - * @default 24 x 24 + * @default 18 x 18 */ minSpace?: Size, /** @@ -55,7 +55,7 @@ export class WaterfallLayout extends BaseLayout implements KeyboardDelegat protected margin: number; protected minSpace: Size; protected maxColumns: number; - protected itemPadding: number; + itemPadding: number; protected numColumns: number; protected itemWidth: number; protected horizontalSpacing: number; @@ -66,9 +66,8 @@ export class WaterfallLayout extends BaseLayout implements KeyboardDelegat super(options); this.minItemSize = options.minItemSize || new Size(240, 136); this.maxItemSize = options.maxItemSize || new Size(Infinity, Infinity); - // TODO: V2 hard coded the margin to 24, current layout calculation breaks if changed from this value - this.margin = 24; - this.minSpace = options.minSpace || new Size(24, 24); + this.margin = options.margin != null ? options.margin : 24; + this.minSpace = options.minSpace || new Size(18, 18); this.maxColumns = options.maxColumns || Infinity; // TODO: not entirely sure what this is for since the layout will automatically shift itself to the correct vertical space for the card this.itemPadding = options.itemPadding != null ? options.itemPadding : 56; @@ -84,33 +83,14 @@ export class WaterfallLayout extends BaseLayout implements KeyboardDelegat return 'waterfall'; } - validate(invalidationContext: InvalidationContext, unknown>) { - this.collection = this.virtualizer.collection; - this.buildCollection(invalidationContext); - - // Remove layout info that doesn't exist in new collection - if (this.lastCollection) { - for (let key of this.lastCollection.getKeys()) { - if (!this.collection.getItem(key)) { - this.layoutInfos.delete(key); - } - } - - if (!this.isLoading) { - this.layoutInfos.delete('loader'); - } - } - - this.lastCollection = this.collection; - } - buildCollection(invalidationContext: InvalidationContext, unknown>) { // Compute the number of columns needed to display the content let visibleWidth = this.virtualizer.visibleRect.width; let availableWidth = visibleWidth - this.margin * 2; - let columns = Math.floor(visibleWidth / (this.minItemSize.width + this.minSpace.width)); + let columns = Math.floor((availableWidth + this.minSpace.width) / (this.minItemSize.width + this.minSpace.width)); this.numColumns = Math.max(1, Math.min(this.maxColumns, columns)); + // Compute the available width (minus the space between items) let width = availableWidth - (this.minSpace.width * (this.numColumns - 1)); @@ -241,7 +221,7 @@ export class WaterfallLayout extends BaseLayout implements KeyboardDelegat key = this._findClosest(layoutInfo.rect, rect)?.key; } - return key; + return this.collection.getItem(key)?.childNodes[0]?.key; } getClosestLeft(key: Key) { @@ -255,14 +235,18 @@ export class WaterfallLayout extends BaseLayout implements KeyboardDelegat key = this._findClosest(layoutInfo.rect, rect)?.key; } - return key; + return this.collection.getItem(key)?.childNodes[0]?.key; } getKeyRightOf(key: Key) { - return this.direction === 'rtl' ? this.getClosestLeft(key) : this.getClosestRight(key); + // Expected key is the currently focused cell so we need the parent row key + let parentRowKey = this.collection.getItem(key).parentKey; + return this.direction === 'rtl' ? this.getClosestLeft(parentRowKey) : this.getClosestRight(parentRowKey); } getKeyLeftOf(key: Key) { - return this.direction === 'rtl' ? this.getClosestRight(key) : this.getClosestLeft(key); + // Expected key is the currently focused cell so we need the parent row key + let parentRowKey = this.collection.getItem(key).parentKey; + return this.direction === 'rtl' ? this.getClosestRight(parentRowKey) : this.getClosestLeft(parentRowKey); } } diff --git a/packages/@react-spectrum/cards/src/cardview.css b/packages/@react-spectrum/cards/src/cardview.css index 610116aebfa..444447cccb4 100644 --- a/packages/@react-spectrum/cards/src/cardview.css +++ b/packages/@react-spectrum/cards/src/cardview.css @@ -5,3 +5,12 @@ width: 100%; height: 100%; } + +.react-spectrum-CardView-CardWrapper { + overflow: visible !important; + contain: size layout style !important; +} + +.react-spectrum-CardView { + outline: none; +} diff --git a/packages/@react-spectrum/cards/src/index.ts b/packages/@react-spectrum/cards/src/index.ts index a7c0611adf4..dfaa9904a50 100644 --- a/packages/@react-spectrum/cards/src/index.ts +++ b/packages/@react-spectrum/cards/src/index.ts @@ -13,6 +13,7 @@ /// export * from './CardView'; +export * from './BaseLayout'; export * from './GalleryLayout'; export * from './GridLayout'; export * from './WaterfallLayout'; diff --git a/packages/@react-spectrum/cards/stories/GalleryCardView.stories.tsx b/packages/@react-spectrum/cards/stories/GalleryCardView.stories.tsx index bb468608eb1..8e25d0e3bf7 100644 --- a/packages/@react-spectrum/cards/stories/GalleryCardView.stories.tsx +++ b/packages/@react-spectrum/cards/stories/GalleryCardView.stories.tsx @@ -12,6 +12,7 @@ import {AsyncLoadingCardView, ControlledCardView, CustomLayout, DynamicCardView, items, NoItemCardView, renderEmptyState, StaticCardView} from './GridCardView.stories'; import {GalleryLayout} from '../'; +import React from 'react'; import {Size} from '@react-stately/virtualizer'; import {useCollator} from '@react-aria/i18n'; import {useMemo} from 'react'; @@ -57,8 +58,11 @@ let itemsNoThinImages = [ {width: 1516, height: 1009, src: 'https://i.imgur.com/1nScMIH.jpg', id: 21, title: 'Bob 5'} ]; +const StoryFn = ({storyFn}) => storyFn(); + export default { - title: 'CardView/Gallery layout' + title: 'CardView/Gallery layout', + decorators: [storyFn => ] }; export const DefaultGalleryStatic = () => StaticCardView({layout: GalleryLayout, items}); @@ -91,19 +95,19 @@ isLoadingNoHeightGallery.storyName = 'loadingState = loading, no height'; export const isLoadingHeightGallery = () => NoItemCardView({layout: GalleryLayout, width: '800px', height: '800px', loadingState: 'loading'}); isLoadingHeightGallery.storyName = 'loadingState = loading, set height'; -export const loadingMoreGallery = () => DynamicCardView({layout: GalleryLayout, width: '800px', height: '800px', loadingState: 'loadingMore', items}); +export const loadingMoreGallery = () => DynamicCardView({layout: GalleryLayout, loadingState: 'loadingMore', items}); loadingMoreGallery.storyName = 'loadingState = loadingMore'; -export const filteringGallery = () => DynamicCardView({layout: GalleryLayout, width: '800px', height: '800px', loadingState: 'filtering', items}); +export const filteringGallery = () => DynamicCardView({layout: GalleryLayout, loadingState: 'filtering', items}); filteringGallery.storyName = 'loadingState = filtering'; export const emptyWithHeightGallery = () => NoItemCardView({layout: GalleryLayout, width: '800px', height: '800px', renderEmptyState}); emptyWithHeightGallery.storyName = 'empty, set height'; -export const AsyncLoading = () => AsyncLoadingCardView({layout: GalleryLayout, width: '800px', height: '800px'}); +export const AsyncLoading = () => AsyncLoadingCardView({layout: GalleryLayout}); AsyncLoading.storyName = 'Async loading'; -export const CustomLayoutOptions = () => CustomGalleryLayout({items: itemsLowVariance}, {idealRowHeight: 270, itemSpacing: new Size(10, 10), itemPadding: 114}); +export const CustomLayoutOptions = () => CustomGalleryLayout({items: itemsLowVariance}, {idealRowHeight: 400, itemSpacing: new Size(10, 10), itemPadding: 114, minItemSize: new Size(150, 400)}); CustomGalleryLayout.storyName = 'Custom layout options'; function CustomGalleryLayout(props, layoutOptions) { diff --git a/packages/@react-spectrum/cards/stories/GridCardView.stories.tsx b/packages/@react-spectrum/cards/stories/GridCardView.stories.tsx index aaccad4ccd7..6f657f4d1e5 100644 --- a/packages/@react-spectrum/cards/stories/GridCardView.stories.tsx +++ b/packages/@react-spectrum/cards/stories/GridCardView.stories.tsx @@ -25,6 +25,7 @@ import {Size} from '@react-stately/virtualizer'; import {TextField} from '@react-spectrum/textfield'; import {useAsyncList} from '@react-stately/data'; import {useCollator} from '@react-aria/i18n'; +import {useProvider} from '@react-spectrum/provider'; export let items = [ {width: 1001, height: 381, src: 'https://i.imgur.com/Z7AzH2c.jpg', title: 'Bob 1'}, @@ -63,16 +64,17 @@ export function renderEmptyState() { ); } +const StoryFn = ({storyFn}) => storyFn(); + export default { title: 'CardView/Grid layout', - excludeStories: ['items', 'renderEmptyState', 'DynamicCardView', 'NoItemCardView', 'StaticCardView', 'ControlledCardView', 'AsyncLoadingCardView', 'CustomLayout'] + excludeStories: ['items', 'renderEmptyState', 'DynamicCardView', 'NoItemCardView', 'StaticCardView', 'ControlledCardView', 'AsyncLoadingCardView', 'CustomLayout'], + decorators: [storyFn => ] }; let onSelectionChange = action('onSelectionChange'); -let onLoadMore = action('onLoadMore'); let actions = { - onSelectionChange: s => onSelectionChange([...s]), - onLoadMore: onLoadMore + onSelectionChange: s => onSelectionChange([...s]) }; // TODO add stories for Layouts with non-default options passed in @@ -104,24 +106,30 @@ isLoadingNoHeightGrid.storyName = 'loadingState = loading, no height'; export const isLoadingHeightGrid = () => NoItemCardView({width: '800px', height: '800px', loadingState: 'loading', items}); isLoadingHeightGrid.storyName = 'loadingState = loading, set height'; -export const loadingMoreGrid = () => DynamicCardView({width: '800px', height: '800px', loadingState: 'loadingMore', items}); +export const loadingMoreGrid = () => DynamicCardView({loadingState: 'loadingMore', items}); loadingMoreGrid.storyName = 'loadingState = loadingMore'; -export const filteringGrid = () => DynamicCardView({width: '800px', height: '800px', loadingState: 'filtering', items}); +export const filteringGrid = () => DynamicCardView({loadingState: 'filtering', items}); filteringGrid.storyName = 'loadingState = filtering'; export const emptyWithHeightGrid = () => NoItemCardView({width: '800px', height: '800px', renderEmptyState}); emptyWithHeightGrid.storyName = 'empty, set height'; -export const AsyncLoading = () => AsyncLoadingCardView({width: '800px', height: '800px'}); +export const AsyncLoading = () => AsyncLoadingCardView({}); AsyncLoading.storyName = 'Async loading'; export const CustomLayoutOptions = () => CustomLayout({items}, {maxColumns: 2, margin: 150, minSpace: new Size(10, 10), itemPadding: 400}); CustomLayoutOptions.storyName = 'Custom layout options'; export function DynamicCardView(props) { + let {scale} = useProvider(); let collator = useCollator({usage: 'search', sensitivity: 'base'}); - let gridLayout = useMemo(() => new GridLayout({collator}), [collator]); + let gridLayout = useMemo(() => + new GridLayout({ + itemPadding: scale === 'large' ? 116 : 95, + collator + }) + , [collator, scale]); let { layout = gridLayout, selectionMode = 'multiple', @@ -137,35 +145,43 @@ export function DynamicCardView(props) { }; return ( - - - - Remove +
+ + + + Remove + + + {(item: any) => ( + + + {item.title} + PNG + Very very very very very very very very very very very very very long description + + Action 1 + Action 2 + +
+ +
+
+ )} +
- - {(item: any) => ( - - - {item.title} - PNG - Very very very very very very very very very very very very very long description - - Action 1 - Action 2 - -
- -
-
- )} -
- +
); } export function ControlledCardView(props) { + let {scale} = useProvider(); let collator = useCollator({usage: 'search', sensitivity: 'base'}); - let gridLayout = useMemo(() => new GridLayout({collator}), [collator]); + let gridLayout = useMemo(() => + new GridLayout({ + itemPadding: scale === 'large' ? 116 : 95, + collator + }) + , [collator, scale]); let { layout = gridLayout, selectionMode = 'multiple', @@ -183,34 +199,43 @@ export function ControlledCardView(props) { }; return ( - - - - Remove +
+ + + + Remove + + + {(item: any) => ( + + + {item.title} + PNG + Very very very very very very very very very very very very very long description + + Action 1 + Action 2 + +
+ +
+
+ )} +
- - {(item: any) => ( - - - {item.title} - PNG - Very very very very very very very very very very very very very long description - - Action 1 - Action 2 - -
- -
-
- )} -
- +
); } export function NoItemCardView(props) { - let gridLayout = useMemo(() => new GridLayout({}), []); + let {scale} = useProvider(); + let collator = useCollator({usage: 'search', sensitivity: 'base'}); + let gridLayout = useMemo(() => + new GridLayout({ + itemPadding: scale === 'large' ? 116 : 95, + collator + }) + , [collator, scale]); let { layout = gridLayout } = props; @@ -219,7 +244,7 @@ export function NoItemCardView(props) { return ( <> setShow(show => !show)}>Toggle items - + {(item: any) => ( @@ -241,69 +266,77 @@ export function NoItemCardView(props) { } export function StaticCardView(props) { + let {scale} = useProvider(); let collator = useCollator({usage: 'search', sensitivity: 'base'}); - let gridLayout = useMemo(() => new GridLayout({collator}), [collator]); + let gridLayout = useMemo(() => + new GridLayout({ + itemPadding: scale === 'large' ? 116 : 95, + collator + }) + , [collator, scale]); let { layout = gridLayout } = props; return ( - - - - Bob 1 - PNG - Very very very very very very very very very very very very very long description - - Action 1 - Action 2 - -
- -
-
- - - Joe 1 - PNG - - Action 1 - Action 2 - - - - - Jane 1 - PNG - Description -
- -
-
- - - Bob 2 - PNG - Very very very very very very very very very very very very very long description - - Action 1 - Action 2 - - - - - Joe 2 - PNG - Description - - Action 1 - Action 2 - -
- -
-
-
+
+ + + + Bob 1 + PNG + Very very very very very very very very very very very very very long description + + Action 1 + Action 2 + +
+ +
+
+ + + Joe 1 + PNG + + Action 1 + Action 2 + + + + + Jane 1 + PNG + Description +
+ +
+
+ + + Bob 2 + PNG + Very very very very very very very very very very very very very long description + + Action 1 + Action 2 + + + + + Joe 2 + PNG + Description + + Action 1 + Action 2 + +
+ +
+
+
+
); } @@ -313,8 +346,14 @@ export function AsyncLoadingCardView(props) { url: string } + let {scale} = useProvider(); let collator = useCollator({usage: 'search', sensitivity: 'base'}); - let gridLayout = useMemo(() => new GridLayout({collator}), [collator]); + let gridLayout = useMemo(() => + new GridLayout({ + itemPadding: scale === 'large' ? 116 : 95, + collator + }) + , [collator, scale]); let { layout = gridLayout } = props; @@ -338,29 +377,38 @@ export function AsyncLoadingCardView(props) { }); return ( - - {(item: any) => ( - - - {item.title} - PNG - Very very very very very very very very very very very very very long description - - Action 1 - Action 2 - -
- -
-
- )} -
+
+ + {(item: any) => ( + + + {item.title} + PNG + Very very very very very very very very very very very very very long description + + Action 1 + Action 2 + +
+ +
+
+ )} +
+
); } export function CustomLayout(props, layoutOptions) { + let {scale} = useProvider(); let collator = useCollator({usage: 'search', sensitivity: 'base'}); - let gridLayout = useMemo(() => new GridLayout({collator, ...layoutOptions}), [collator, layoutOptions]); + let gridLayout = useMemo(() => + new GridLayout({ + itemPadding: scale === 'large' ? 116 : 95, + collator, + ...layoutOptions + }) + , [collator, scale, layoutOptions]); let { layout = gridLayout, selectionMode = 'multiple', @@ -376,28 +424,30 @@ export function CustomLayout(props, layoutOptions) { }; return ( - - - - Remove +
+ + + + Remove + + + {(item: any) => ( + + + {item.title} + PNG + Very very very very very very very very very very very very very long description + + Action 1 + Action 2 + +
+ +
+
+ )} +
- - {(item: any) => ( - - - {item.title} - PNG - Very very very very very very very very very very very very very long description - - Action 1 - Action 2 - -
- -
-
- )} -
- +
); } diff --git a/packages/@react-spectrum/cards/stories/WaterfallCardView.stories.tsx b/packages/@react-spectrum/cards/stories/WaterfallCardView.stories.tsx index 31a7c5f5085..c58abb66bac 100644 --- a/packages/@react-spectrum/cards/stories/WaterfallCardView.stories.tsx +++ b/packages/@react-spectrum/cards/stories/WaterfallCardView.stories.tsx @@ -11,6 +11,7 @@ */ import {AsyncLoadingCardView, ControlledCardView, CustomLayout, DynamicCardView, items, NoItemCardView, renderEmptyState, StaticCardView} from './GridCardView.stories'; +import React from 'react'; import {Size} from '@react-stately/virtualizer'; import {useCollator} from '@react-aria/i18n'; import {useMemo} from 'react'; @@ -41,8 +42,11 @@ let itemsNoSize = [ {src: 'https://i.imgur.com/zzwWogn.jpg', title: 'Bob 8'} ]; +const StoryFn = ({storyFn}) => storyFn(); + export default { - title: 'CardView/Waterfall layout' + title: 'CardView/Waterfall layout', + decorators: [storyFn => ] }; export const DefaultWaterfallStatic = () => StaticCardView({layout: WaterfallLayout, items}); @@ -78,19 +82,19 @@ isLoadingNoHeightWaterfall.storyName = 'loadingState = loading, no height'; export const isLoadingHeightWaterfall = () => NoItemCardView({layout: WaterfallLayout, width: '800px', height: '800px', loadingState: 'loading'}); isLoadingHeightWaterfall.storyName = 'loadingState = loading, set height'; -export const loadingMoreWaterfall = () => DynamicCardView({layout: WaterfallLayout, width: '800px', height: '800px', loadingState: 'loadingMore', items}); +export const loadingMoreWaterfall = () => DynamicCardView({layout: WaterfallLayout, loadingState: 'loadingMore', items}); loadingMoreWaterfall.storyName = 'loadingState = loadingMore'; -export const filteringWaterfall = () => DynamicCardView({layout: WaterfallLayout, width: '800px', height: '800px', loadingState: 'filtering', items}); +export const filteringWaterfall = () => DynamicCardView({layout: WaterfallLayout, loadingState: 'filtering', items}); filteringWaterfall.storyName = 'loadingState = filtering'; export const emptyWithHeightWaterfall = () => NoItemCardView({layout: WaterfallLayout, width: '800px', height: '800px', renderEmptyState}); emptyWithHeightWaterfall.storyName = 'empty, set height'; -export const AsyncLoading = () => AsyncLoadingCardView({layout: WaterfallLayout, width: '800px', height: '800px'}); +export const AsyncLoading = () => AsyncLoadingCardView({layout: WaterfallLayout}); AsyncLoading.storyName = 'Async loading'; -export const CustomLayoutOptions = () => CustomGalleryLayout({items}, {minSpace: new Size(50, 50), maxColumns: 2, itemPadding: 400}); +export const CustomLayoutOptions = () => CustomGalleryLayout({items}, {minSpace: new Size(50, 50), maxColumns: 2, itemPadding: 400, margin: 10}); CustomGalleryLayout.storyName = 'Custom layout options'; function CustomGalleryLayout(props, layoutOptions) { diff --git a/packages/@react-spectrum/cards/test/CardView.test.js b/packages/@react-spectrum/cards/test/CardView.test.js index aa80740ca29..7f371b53c15 100644 --- a/packages/@react-spectrum/cards/test/CardView.test.js +++ b/packages/@react-spectrum/cards/test/CardView.test.js @@ -292,10 +292,11 @@ describe('CardView', function () { expect(div.style.width).toEqual(expectedWidth); expect(div.style.height).toEqual(expectedHeight); if (currentTop === div.style.top) { - currentLeft = `${parseInt(currentLeft, 10) + parseInt(expectedWidth, 10) + 24}px`; + // 19px due to how horizontal spacing is calculated in the layout + currentLeft = `${parseInt(currentLeft, 10) + parseInt(expectedWidth, 10) + 19}px`; } else { - // default space between the two cards vertically is 48px - currentTop = `${parseInt(currentTop, 10) + parseInt(expectedHeight, 10) + 48}px`; + // default space between the two cards vertically is 18px + currentTop = `${parseInt(currentTop, 10) + parseInt(expectedHeight, 10) + 18}px`; currentLeft = '24px'; expect(div.style.top).toEqual(currentTop); } @@ -316,7 +317,7 @@ describe('CardView', function () { expect(document.activeElement).toBe(cards[1]); let cardStyles = getCardStyles(cards[1]); let expectedLeft = cardStyles.left; - let expectedTop = `${parseInt(cardStyles.top, 10) + parseInt(cardStyles.height, 10) + 48}px`; + let expectedTop = `${parseInt(cardStyles.top, 10) + parseInt(cardStyles.height, 10) + 18}px`; act(() => { fireEvent.keyDown(document.activeElement, {key: 'ArrowDown', code: 40, charCode: 40}); @@ -340,7 +341,7 @@ describe('CardView', function () { expect(document.activeElement).toBe(cards[5]); let cardStyles = getCardStyles(cards[5]); let expectedLeft = cardStyles.left; - let expectedTop = `${parseInt(cardStyles.top, 10) - parseInt(cardStyles.height, 10) - 48}px`; + let expectedTop = `${parseInt(cardStyles.top, 10) - parseInt(cardStyles.height, 10) - 18}px`; act(() => { fireEvent.keyDown(document.activeElement, {key: 'ArrowUp', code: 38, charCode: 38}); @@ -357,7 +358,7 @@ describe('CardView', function () { Name | layout ${'Grid layout'} | ${GridLayout} ${'Gallery layout'} | ${GalleryLayout} - `('$Name CardView should move focus via Arrow Left', function ({layout}) { + `('$Name CardView should move focus via Arrow Left', function ({Name, layout}) { let tree = render(); act(() => { jest.runAllTimers(); @@ -379,7 +380,9 @@ describe('CardView', function () { expect(document.activeElement).toBe(cards[0]); cardStyles = getCardStyles(document.activeElement); - expectedLeft = `${parseInt(expectedLeft, 10) - parseInt(cardStyles.width, 10) - 24}px`; + // horizontal spacing in grid is minimum 18px, but in this specific setup the calculated horizontal spacing is 19px due to margins + let horizontalSpacing = Name === 'Grid layout' ? 19 : 18; + expectedLeft = `${parseInt(expectedLeft, 10) - parseInt(cardStyles.width, 10) - horizontalSpacing}px`; expect(cardStyles.top).toEqual(expectedTop); expect(cardStyles.left).toEqual(expectedLeft); }); @@ -397,7 +400,7 @@ describe('CardView', function () { Name | layout ${'Grid layout'} | ${GridLayout} ${'Gallery layout'} | ${GalleryLayout} - `('$Name CardView should move focus via Arrow Right (RTL)', function ({layout}) { + `('$Name CardView should move focus via Arrow Right (RTL)', function ({Name, layout}) { let tree = render(); act(() => { jest.runAllTimers(); @@ -421,7 +424,9 @@ describe('CardView', function () { expect(document.activeElement).toBe(cards[0]); cardStyles = getCardStyles(document.activeElement); - expectedRight = `${parseInt(expectedRight, 10) - parseInt(cardStyles.width, 10) - 24}px`; + // horizontal spacing in grid is minimum 18px, but in this specific setup the calculated horizontal spacing is 19px due to margins + let horizontalSpacing = Name === 'Grid layout' ? 19 : 18; + expectedRight = `${parseInt(expectedRight, 10) - parseInt(cardStyles.width, 10) - horizontalSpacing}px`; expect(cardStyles.top).toEqual(expectedTop); expect(cardStyles.right).toEqual(expectedRight); }); @@ -446,8 +451,8 @@ describe('CardView', function () { let cardStyles = getCardStyles(document.activeElement); let expectedLeft = cardStyles.left; - let numCardsInPage = Math.floor(mockHeight / (parseInt(cardStyles.height, 10) + 48)); - let expectedTop = `${parseInt(cardStyles.top, 10) - numCardsInPage * (parseInt(cardStyles.height, 10) + 48)}px`; + let numCardsInPage = Math.floor(mockHeight / (parseInt(cardStyles.height, 10) + 18)); + let expectedTop = `${parseInt(cardStyles.top, 10) - numCardsInPage * (parseInt(cardStyles.height, 10) + 18)}px`; act(() => { fireEvent.keyDown(document.activeElement, {key: 'PageUp', code: 33, charCode: 33}); @@ -473,8 +478,8 @@ describe('CardView', function () { let cardStyles = getCardStyles(document.activeElement); let expectedLeft = cardStyles.left; - let numCardsInPage = Math.floor(mockHeight / (parseInt(cardStyles.height, 10) + 48)); - let expectedTop = `${parseInt(cardStyles.top, 10) + numCardsInPage * (parseInt(cardStyles.height, 10) + 48)}px`; + let numCardsInPage = Math.floor(mockHeight / (parseInt(cardStyles.height, 10) + 18)); + let expectedTop = `${parseInt(cardStyles.top, 10) + numCardsInPage * (parseInt(cardStyles.height, 10) + 18)}px`; act(() => { fireEvent.keyDown(document.activeElement, {key: 'PageDown', code: 34, charCode: 34}); @@ -509,11 +514,10 @@ describe('CardView', function () { if (!expectedHeight) { currentTop = div.style.top; expectedHeight = div.style.height; - // TODO: should this match the other layouts? - expect(div.style.top).toEqual('32px'); + expect(div.style.top).toEqual('24px'); expect(div.style.left).toEqual('24px'); - expectedLeft = `${parseInt(div.style.left, 10) + parseInt(div.style.width, 10) + 24}px`; + expectedLeft = `${parseInt(div.style.left, 10) + parseInt(div.style.width, 10) + 18}px`; } else { if (currentTop === div.style.top) { expect(div.style.left).toEqual(expectedLeft); @@ -524,7 +528,7 @@ describe('CardView', function () { expect(div.style.left).toEqual('24px'); } expect(div.style.height).toEqual(expectedHeight); - expectedLeft = `${parseInt(div.style.left, 10) + parseInt(div.style.width, 10) + 24}px`; + expectedLeft = `${parseInt(div.style.left, 10) + parseInt(div.style.width, 10) + 18}px`; } } }); @@ -542,7 +546,7 @@ describe('CardView', function () { expect(within(document.activeElement).getByText('Title 1')).toBeTruthy(); let cardStyles = getCardStyles(cards[0]); - let expectedTop = `${parseInt(cardStyles.top, 10) + parseInt(cardStyles.height, 10) + 32}px`; + let expectedTop = `${parseInt(cardStyles.top, 10) + parseInt(cardStyles.height, 10) + 18}px`; act(() => { fireEvent.keyDown(document.activeElement, {key: 'ArrowDown', code: 40, charCode: 40}); @@ -576,7 +580,7 @@ describe('CardView', function () { }); cardStyles = getCardStyles(document.activeElement); - expectedTop = `${parseInt(expectedTop, 10) - parseInt(cardStyles.height, 10) - 32}px`; + expectedTop = `${parseInt(expectedTop, 10) - parseInt(cardStyles.height, 10) - 18}px`; expect(cardStyles.top).toEqual(expectedTop); expect(within(document.activeElement).getByText('Title 1')).toBeTruthy(); }); @@ -648,7 +652,7 @@ describe('CardView', function () { }); let cardStyles = getCardStyles(document.activeElement); - let numCardsInPage = Math.floor(mockHeight / (parseInt(cardStyles.height, 10) + 32)); + let numCardsInPage = Math.floor(mockHeight / (parseInt(cardStyles.height, 10) + 18)); for (let i = 0; i < numCardsInPage; i++) { act(() => { @@ -690,7 +694,7 @@ describe('CardView', function () { expect(div.style.left).toEqual('24px'); columnLefts.push(div.style.left); - let expectedHeight = `${parseInt(div.style.top, 10) + parseInt(div.style.height, 10) + 24}px`; + let expectedHeight = `${parseInt(div.style.top, 10) + parseInt(div.style.height, 10) + 18}px`; columnHeights.push(expectedHeight); } else { expect(div.style.width).toEqual(expectedWidth); @@ -698,13 +702,13 @@ describe('CardView', function () { // Make sure each item is within one of the columns of the waterfall and check the positioning if (div.style.top === '24px') { columnLefts.push(div.style.left); - let expectedHeight = `${parseInt(div.style.top, 10) + parseInt(div.style.height, 10) + 24}px`; + let expectedHeight = `${parseInt(div.style.top, 10) + parseInt(div.style.height, 10) + 18}px`; columnHeights.push(expectedHeight); } else { let index = columnLefts.indexOf(div.style.left); expect(index).not.toEqual(-1); expect(columnHeights[index]).toEqual(div.style.top); - columnHeights[index] = `${parseInt(div.style.top, 10) + parseInt(div.style.height, 10) + 24}px`; + columnHeights[index] = `${parseInt(div.style.top, 10) + parseInt(div.style.height, 10) + 18}px`; } } } @@ -723,7 +727,7 @@ describe('CardView', function () { expect(within(document.activeElement).getByText('Title 1')).toBeTruthy(); let cardStyles = getCardStyles(cards[0]); - let expectedTop = `${parseInt(cardStyles.top, 10) + parseInt(cardStyles.height, 10) + 24}px`; + let expectedTop = `${parseInt(cardStyles.top, 10) + parseInt(cardStyles.height, 10) + 18}px`; let expectedLeft = cardStyles.left; act(() => { @@ -735,7 +739,7 @@ describe('CardView', function () { cardStyles = getCardStyles(document.activeElement); expect(cardStyles.top).toEqual(expectedTop); expect(cardStyles.left).toEqual(expectedLeft); - expect(within(document.activeElement).getByText('Title 4')).toBeTruthy(); + expect(within(document.activeElement).getByText('Title 3')).toBeTruthy(); }); it('should move focus via Arrow Up', function () { @@ -745,11 +749,11 @@ describe('CardView', function () { }); let cards = tree.getAllByRole('gridcell'); - triggerPress(cards[3]); - expect(document.activeElement).toBe(cards[3]); - expect(within(document.activeElement).getByText('Title 4')).toBeTruthy(); + triggerPress(cards[2]); + expect(document.activeElement).toBe(cards[2]); + expect(within(document.activeElement).getByText('Title 3')).toBeTruthy(); - let cardStyles = getCardStyles(cards[3]); + let cardStyles = getCardStyles(cards[2]); let expectedTop = cardStyles.top; let expectedLeft = cardStyles.left; @@ -760,7 +764,7 @@ describe('CardView', function () { }); cardStyles = getCardStyles(document.activeElement); - expectedTop = `${parseInt(expectedTop, 10) - parseInt(cardStyles.height, 10) - 24}px`; + expectedTop = `${parseInt(expectedTop, 10) - parseInt(cardStyles.height, 10) - 18}px`; expect(cardStyles.top).toEqual(expectedTop); expect(cardStyles.left).toEqual(expectedLeft); expect(within(document.activeElement).getByText('Title 1')).toBeTruthy(); @@ -792,7 +796,7 @@ describe('CardView', function () { expect(document.activeElement).toBe(cards[0]); cardStyles = getCardStyles(document.activeElement); - expectedLeft = `${parseInt(expectedLeft, 10) - parseInt(cardStyles.width, 10) - 24}px`; + expectedLeft = `${parseInt(expectedLeft, 10) - parseInt(cardStyles.width, 10) - 18}px`; expect(cardStyles.top).toEqual(expectedTop); expect(cardStyles.left).toEqual(expectedLeft); }); @@ -1064,31 +1068,29 @@ describe('CardView', function () { `('$Name CardView should call loadMore when scrolling to the bottom', function ({layout}) { let onLoadMore = jest.fn(); let tree = render(); - expect(tree).toBeTruthy(); - // TODO: loadMore is being called twice on mount??? debug this - // act(() => { - // jest.runAllTimers(); - // }); + act(() => { + jest.runAllTimers(); + }); - // let cards = tree.getAllByRole('gridcell'); - // expect(cards).toBeTruthy(); - // expect(onLoadMore).toHaveBeenCalledTimes(0); - // triggerPress(cards[1]); + let cards = tree.getAllByRole('gridcell'); + expect(cards).toBeTruthy(); + // Virtualizer calls onLoadMore twice due to initial layout + expect(onLoadMore).toHaveBeenCalledTimes(2); + triggerPress(cards[1]); - // act(() => { - // fireEvent.keyDown(document.activeElement, {key: 'End', code: 35, charCode: 35}); - // fireEvent.keyUp(document.activeElement, {key: 'End', code: 35, charCode: 35}); - // jest.runAllTimers(); - // }); + act(() => { + fireEvent.keyDown(document.activeElement, {key: 'End', code: 35, charCode: 35}); + fireEvent.keyUp(document.activeElement, {key: 'End', code: 35, charCode: 35}); + jest.runAllTimers(); + }); - // grid = tree.getByRole('grid'); - // grid.scrollTop = 3000; - // fireEvent.scroll(grid); - // expect(onLoadMore).toHaveBeenCalledTimes(1); + let grid = tree.getByRole('grid'); + grid.scrollTop = 3000; + fireEvent.scroll(grid); + expect(onLoadMore).toHaveBeenCalledTimes(3); }); - it.each` Name | layout ${'Grid layout'} | ${GridLayout} diff --git a/packages/@react-spectrum/list/intl/ar-AE.json b/packages/@react-spectrum/list/intl/ar-AE.json index 4c058195ca9..c0608e89453 100644 --- a/packages/@react-spectrum/list/intl/ar-AE.json +++ b/packages/@react-spectrum/list/intl/ar-AE.json @@ -1,4 +1,4 @@ { - "loading": "جار التحميل...", - "loadingMore": "جار تحميل المزيد..." + "loading": "جارٍ التحميل...", + "loadingMore": "جارٍ تحميل المزيد" } diff --git a/packages/@react-spectrum/listbox/intl/ar-AE.json b/packages/@react-spectrum/listbox/intl/ar-AE.json index 4c058195ca9..c0608e89453 100644 --- a/packages/@react-spectrum/listbox/intl/ar-AE.json +++ b/packages/@react-spectrum/listbox/intl/ar-AE.json @@ -1,4 +1,4 @@ { - "loading": "جار التحميل...", - "loadingMore": "جار تحميل المزيد..." + "loading": "جارٍ التحميل...", + "loadingMore": "جارٍ تحميل المزيد" } diff --git a/packages/@react-spectrum/table/intl/ar-AE.json b/packages/@react-spectrum/table/intl/ar-AE.json index 4c058195ca9..c0608e89453 100644 --- a/packages/@react-spectrum/table/intl/ar-AE.json +++ b/packages/@react-spectrum/table/intl/ar-AE.json @@ -1,4 +1,4 @@ { - "loading": "جار التحميل...", - "loadingMore": "جار تحميل المزيد..." + "loading": "جارٍ التحميل...", + "loadingMore": "جارٍ تحميل المزيد" } diff --git a/packages/@react-types/cards/src/index.d.ts b/packages/@react-types/cards/src/index.d.ts index 46de53fe1ce..bb35c6a63ec 100644 --- a/packages/@react-types/cards/src/index.d.ts +++ b/packages/@react-types/cards/src/index.d.ts @@ -22,7 +22,6 @@ interface SpectrumCardProps extends AriaCardProps, StyleProps, DOMProps { layout?: 'grid' | 'waterfall' | 'gallery', // TODO: readd size when we get updated designs from spectrum // size?: 'S' | 'M' | 'L', - // not needed for quiet cards orientation?: 'horizontal' | 'vertical' } @@ -37,7 +36,9 @@ interface CardViewLayout extends Layout>, KeyboardDelegate { collection: Collection>, disabledKeys: any, isLoading: boolean, - direction: Direction + direction: Direction, + layoutType: string, + itemPadding: number } export interface CardViewLayoutConstructor { From f8e602bc80f0a6a7d7a70138c33dd7aa96e2c1a5 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Mon, 20 Sep 2021 12:02:27 -0700 Subject: [PATCH 2/5] Update packages/@react-spectrum/cards/src/index.ts Co-authored-by: Danni --- packages/@react-spectrum/cards/src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/@react-spectrum/cards/src/index.ts b/packages/@react-spectrum/cards/src/index.ts index dfaa9904a50..a7c0611adf4 100644 --- a/packages/@react-spectrum/cards/src/index.ts +++ b/packages/@react-spectrum/cards/src/index.ts @@ -13,7 +13,6 @@ /// export * from './CardView'; -export * from './BaseLayout'; export * from './GalleryLayout'; export * from './GridLayout'; export * from './WaterfallLayout'; From b3e1d59578b84e2d6ef983dfd290563786047a9f Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Mon, 20 Sep 2021 15:13:48 -0700 Subject: [PATCH 3/5] Apply suggestions from code review Co-authored-by: Danni --- packages/@react-spectrum/cards/src/GalleryLayout.tsx | 2 +- packages/@react-spectrum/cards/src/GridLayout.tsx | 2 +- packages/@react-spectrum/cards/src/WaterfallLayout.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/@react-spectrum/cards/src/GalleryLayout.tsx b/packages/@react-spectrum/cards/src/GalleryLayout.tsx index 3ba2774934e..f5fcf23ecec 100644 --- a/packages/@react-spectrum/cards/src/GalleryLayout.tsx +++ b/packages/@react-spectrum/cards/src/GalleryLayout.tsx @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import {BaseLayout, BaseLayoutOptions} from './'; +import {BaseLayout, BaseLayoutOptions} from './BaseLayout'; import {LayoutInfo, Rect, Size} from '@react-stately/virtualizer'; export interface GalleryLayoutOptions extends BaseLayoutOptions { diff --git a/packages/@react-spectrum/cards/src/GridLayout.tsx b/packages/@react-spectrum/cards/src/GridLayout.tsx index 2082fc47109..f6658834db4 100644 --- a/packages/@react-spectrum/cards/src/GridLayout.tsx +++ b/packages/@react-spectrum/cards/src/GridLayout.tsx @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import {BaseLayout, BaseLayoutOptions} from './'; +import {BaseLayout, BaseLayoutOptions} from './BaseLayout'; import {Key} from 'react'; import {LayoutInfo, Rect, Size} from '@react-stately/virtualizer'; import {Node} from '@react-types/shared'; diff --git a/packages/@react-spectrum/cards/src/WaterfallLayout.tsx b/packages/@react-spectrum/cards/src/WaterfallLayout.tsx index ab425f6528d..c198f6fafea 100644 --- a/packages/@react-spectrum/cards/src/WaterfallLayout.tsx +++ b/packages/@react-spectrum/cards/src/WaterfallLayout.tsx @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import {BaseLayout, BaseLayoutOptions} from './'; +import {BaseLayout, BaseLayoutOptions} from './BaseLayout'; import {InvalidationContext, LayoutInfo, Rect, Size} from '@react-stately/virtualizer'; import {Key} from 'react'; import {KeyboardDelegate, Node} from '@react-types/shared'; From cc02b5a5dcdceb40269b6aab2fb695e561609bfe Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Mon, 20 Sep 2021 17:18:12 -0700 Subject: [PATCH 4/5] Revert virtualizer changes --- packages/@react-aria/virtualizer/package.json | 1 - packages/@react-aria/virtualizer/src/Virtualizer.tsx | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/@react-aria/virtualizer/package.json b/packages/@react-aria/virtualizer/package.json index 541179b6783..d6f2fdb8cbd 100644 --- a/packages/@react-aria/virtualizer/package.json +++ b/packages/@react-aria/virtualizer/package.json @@ -19,7 +19,6 @@ "dependencies": { "@babel/runtime": "^7.6.2", "@react-aria/i18n": "^3.3.2", - "@react-aria/interactions": "^3.5.1", "@react-aria/utils": "^3.8.2", "@react-stately/virtualizer": "^3.1.5", "@react-types/shared": "^3.8.0" diff --git a/packages/@react-aria/virtualizer/src/Virtualizer.tsx b/packages/@react-aria/virtualizer/src/Virtualizer.tsx index edab61642d8..7ad6b1a1f4a 100644 --- a/packages/@react-aria/virtualizer/src/Virtualizer.tsx +++ b/packages/@react-aria/virtualizer/src/Virtualizer.tsx @@ -12,7 +12,6 @@ import {Collection} from '@react-types/shared'; import {focusWithoutScrolling, mergeProps, useLayoutEffect} from '@react-aria/utils'; -import {getInteractionModality} from '@react-aria/interactions'; import {Layout, Rect, ReusableView, useVirtualizerState, VirtualizerState} from '@react-stately/virtualizer'; import React, {FocusEvent, HTMLAttributes, Key, ReactElement, RefObject, useCallback, useEffect, useRef} from 'react'; import {ScrollView} from './ScrollView'; @@ -127,7 +126,7 @@ export function useVirtualizer(props: VirtualizerOptions return; } - if (focusedKey !== lastFocusedKey.current && getInteractionModality() !== 'pointer') { + if (focusedKey !== lastFocusedKey.current) { if (scrollToItem) { scrollToItem(focusedKey); } else { From 8374646d0e445efe9152a1e20b95334fe4c72678 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Tue, 21 Sep 2021 10:25:57 -0700 Subject: [PATCH 5/5] readding ellipsis to loading more arabic --- packages/@react-spectrum/cards/intl/ar-AE.json | 2 +- packages/@react-spectrum/list/intl/ar-AE.json | 2 +- packages/@react-spectrum/listbox/intl/ar-AE.json | 2 +- packages/@react-spectrum/table/intl/ar-AE.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/@react-spectrum/cards/intl/ar-AE.json b/packages/@react-spectrum/cards/intl/ar-AE.json index c0608e89453..73f8c69a501 100644 --- a/packages/@react-spectrum/cards/intl/ar-AE.json +++ b/packages/@react-spectrum/cards/intl/ar-AE.json @@ -1,4 +1,4 @@ { "loading": "جارٍ التحميل...", - "loadingMore": "جارٍ تحميل المزيد" + "loadingMore": "جارٍ تحميل المزيد..." } diff --git a/packages/@react-spectrum/list/intl/ar-AE.json b/packages/@react-spectrum/list/intl/ar-AE.json index c0608e89453..73f8c69a501 100644 --- a/packages/@react-spectrum/list/intl/ar-AE.json +++ b/packages/@react-spectrum/list/intl/ar-AE.json @@ -1,4 +1,4 @@ { "loading": "جارٍ التحميل...", - "loadingMore": "جارٍ تحميل المزيد" + "loadingMore": "جارٍ تحميل المزيد..." } diff --git a/packages/@react-spectrum/listbox/intl/ar-AE.json b/packages/@react-spectrum/listbox/intl/ar-AE.json index c0608e89453..73f8c69a501 100644 --- a/packages/@react-spectrum/listbox/intl/ar-AE.json +++ b/packages/@react-spectrum/listbox/intl/ar-AE.json @@ -1,4 +1,4 @@ { "loading": "جارٍ التحميل...", - "loadingMore": "جارٍ تحميل المزيد" + "loadingMore": "جارٍ تحميل المزيد..." } diff --git a/packages/@react-spectrum/table/intl/ar-AE.json b/packages/@react-spectrum/table/intl/ar-AE.json index c0608e89453..73f8c69a501 100644 --- a/packages/@react-spectrum/table/intl/ar-AE.json +++ b/packages/@react-spectrum/table/intl/ar-AE.json @@ -1,4 +1,4 @@ { "loading": "جارٍ التحميل...", - "loadingMore": "جارٍ تحميل المزيد" + "loadingMore": "جارٍ تحميل المزيد..." }