Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

S2 CardView and ghost loading #6978

Merged
merged 24 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0468889
Add S2 Illustrations
devongovett Aug 8, 2024
3710152
Fix storybook
devongovett Aug 8, 2024
aa61cba
fix name
devongovett Aug 8, 2024
28f6a86
S2 cards
devongovett Aug 7, 2024
660e768
Add slot contexts to all S2 components
devongovett Aug 9, 2024
20efb05
Merge branch 's2-contexts' of github.com:adobe/react-spectrum into s2…
devongovett Aug 9, 2024
f510229
separate button and menu size in ActionMenu
devongovett Aug 9, 2024
bb848fc
Support illustrations in cards and add auto sizing for action menu an…
devongovett Aug 9, 2024
337a562
Add skeleton loading states
devongovett Aug 13, 2024
e5c307a
Merge branch 'main' of github.com:adobe/react-spectrum into s2-card
devongovett Aug 26, 2024
9ce8efa
Fix merge
devongovett Aug 26, 2024
8ec3f04
Add SkeletonCollection, improve waterfall keyboard navigation, clean up
devongovett Aug 30, 2024
c1487ac
Fixes and cleanup
devongovett Aug 30, 2024
62d17f3
Review comments
devongovett Sep 9, 2024
6a4ee24
Merge branch 'main' into s2-card
devongovett Sep 9, 2024
30f45ef
fix background
devongovett Sep 9, 2024
d28ec8a
Merge branch 's2-card' of github.com:adobe/react-spectrum into s2-card
devongovett Sep 9, 2024
179f36c
Fix RTL keyboard navigation
devongovett Sep 9, 2024
f79182a
Update layout when content size changes might cause scrollbars to app…
devongovett Sep 10, 2024
7f57237
Fix React < 18 tests
devongovett Sep 10, 2024
d412c67
Always update whenever content size changes
devongovett Sep 10, 2024
13d7b64
Add prop docs
devongovett Sep 10, 2024
c3efc31
More cleanup and docs
devongovett Sep 11, 2024
1bff41b
Design updates
devongovett Sep 11, 2024
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
3 changes: 2 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ module.exports = {
'AsyncIterable': 'readonly',
'FileSystemFileEntry': 'readonly',
'FileSystemDirectoryEntry': 'readonly',
'FileSystemEntry': 'readonly'
'FileSystemEntry': 'readonly',
'IS_REACT_ACT_ENVIRONMENT': 'readonly'
},
settings: {
jsdoc: {
Expand Down
61 changes: 26 additions & 35 deletions packages/@react-aria/selection/src/ListKeyboardDelegate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,32 +74,27 @@ export class ListKeyboardDelegate<T> implements KeyboardDelegate {
return this.disabledBehavior === 'all' && (item.props?.isDisabled || this.disabledKeys.has(item.key));
}

getNextKey(key: Key) {
key = this.collection.getKeyAfter(key);
private findNextNonDisabled(key: Key, getNext: (key: Key) => Key | null): Key | null {
while (key != null) {
let item = this.collection.getItem(key);
if (item.type === 'item' && !this.isDisabled(item)) {
if (item?.type === 'item' && !this.isDisabled(item)) {
return key;
}

key = this.collection.getKeyAfter(key);
key = getNext(key);
}

return null;
}

getNextKey(key: Key) {
key = this.collection.getKeyAfter(key);
return this.findNextNonDisabled(key, key => this.collection.getKeyAfter(key));
}

getPreviousKey(key: Key) {
key = this.collection.getKeyBefore(key);
while (key != null) {
let item = this.collection.getItem(key);
if (item.type === 'item' && !this.isDisabled(item)) {
return key;
}

key = this.collection.getKeyBefore(key);
}

return null;
return this.findNextNonDisabled(key, key => this.collection.getKeyBefore(key));
}

private findKey(
Expand Down Expand Up @@ -151,6 +146,14 @@ export class ListKeyboardDelegate<T> implements KeyboardDelegate {
}

getKeyRightOf(key: Key) {
// This is a temporary solution for CardView until we refactor useSelectableCollection.
// https://github.com/orgs/adobe/projects/19/views/32?pane=issue&itemId=77825042
let layoutDelegateMethod = this.direction === 'ltr' ? 'getKeyRightOf' : 'getKeyLeftOf';
if (this.layoutDelegate[layoutDelegateMethod]) {
key = this.layoutDelegate[layoutDelegateMethod](key);
return this.findNextNonDisabled(key, key => this.layoutDelegate[layoutDelegateMethod](key));
}

if (this.layout === 'grid') {
if (this.orientation === 'vertical') {
return this.getNextColumn(key, this.direction === 'rtl');
Expand All @@ -165,6 +168,12 @@ export class ListKeyboardDelegate<T> implements KeyboardDelegate {
}

getKeyLeftOf(key: Key) {
let layoutDelegateMethod = this.direction === 'ltr' ? 'getKeyLeftOf' : 'getKeyRightOf';
if (this.layoutDelegate[layoutDelegateMethod]) {
key = this.layoutDelegate[layoutDelegateMethod](key);
return this.findNextNonDisabled(key, key => this.layoutDelegate[layoutDelegateMethod](key));
}

if (this.layout === 'grid') {
if (this.orientation === 'vertical') {
return this.getNextColumn(key, this.direction === 'ltr');
Expand All @@ -180,30 +189,12 @@ export class ListKeyboardDelegate<T> implements KeyboardDelegate {

getFirstKey() {
let key = this.collection.getFirstKey();
while (key != null) {
let item = this.collection.getItem(key);
if (item?.type === 'item' && !this.isDisabled(item)) {
return key;
}

key = this.collection.getKeyAfter(key);
}

return null;
return this.findNextNonDisabled(key, key => this.collection.getKeyAfter(key));
}

getLastKey() {
let key = this.collection.getLastKey();
while (key != null) {
let item = this.collection.getItem(key);
if (item.type === 'item' && !this.isDisabled(item)) {
return key;
}

key = this.collection.getKeyBefore(key);
}

return null;
return this.findNextNonDisabled(key, key => this.collection.getKeyBefore(key));
}

getKeyPageAbove(key: Key) {
Expand Down Expand Up @@ -280,7 +271,7 @@ export class ListKeyboardDelegate<T> implements KeyboardDelegate {
return key;
}

key = this.getKeyBelow(key);
key = this.getNextKey(key);
}

return null;
Expand Down
2 changes: 1 addition & 1 deletion packages/@react-aria/utils/src/useLoadMore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export interface LoadMoreProps {
*/
scrollOffset?: number,
/** The data currently loaded. */
items?: any[]
items?: any
}

export function useLoadMore(props: LoadMoreProps, ref: RefObject<HTMLElement | null>) {
Expand Down
43 changes: 26 additions & 17 deletions packages/@react-aria/virtualizer/src/ScrollView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,12 +143,17 @@ export function useScrollView(props: ScrollViewProps, ref: RefObject<HTMLElement
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

let isUpdatingSize = useRef(false);
let updateSize = useEffectEvent((flush: typeof flushSync) => {
let dom = ref.current;
if (!dom) {
if (!dom && !isUpdatingSize.current) {
return;
}

// Prevent reentrancy when resize observer fires, triggers re-layout that results in
// content size update, causing below layout effect to fire. This avoids infinite loops.
isUpdatingSize.current = true;

let isTestEnv = process.env.NODE_ENV === 'test' && !process.env.VIRT_ON;
let isClientWidthMocked = Object.getOwnPropertyNames(window.HTMLElement.prototype).includes('clientWidth');
let isClientHeightMocked = Object.getOwnPropertyNames(window.HTMLElement.prototype).includes('clientHeight');
Expand Down Expand Up @@ -177,27 +182,31 @@ export function useScrollView(props: ScrollViewProps, ref: RefObject<HTMLElement
});
}
}

isUpdatingSize.current = false;
});

let didUpdateSize = useRef(false);
// Update visible rect when the content size changes, in case scrollbars need to appear or disappear.
let lastContentSize = useRef<Size | null>(null);
useLayoutEffect(() => {
// React doesn't allow flushSync inside effects, so queue a microtask.
// We also need to wait until all refs are set (e.g. when passing a ref down from a parent).
queueMicrotask(() => {
if (!didUpdateSize.current) {
didUpdateSize.current = true;
updateSize(flushSync);
if (!isUpdatingSize.current && (lastContentSize.current == null || !contentSize.equals(lastContentSize.current))) {
// React doesn't allow flushSync inside effects, so queue a microtask.
// We also need to wait until all refs are set (e.g. when passing a ref down from a parent).
// If we are in an `act` environment, update immediately without a microtask so you don't need
// to mock timers in tests. In this case, the update is synchronous already.
// IS_REACT_ACT_ENVIRONMENT is used by React 18. Previous versions checked for the `jest` global.
// https://github.com/reactwg/react-18/discussions/102
// @ts-ignore
if (typeof IS_REACT_ACT_ENVIRONMENT === 'boolean' ? IS_REACT_ACT_ENVIRONMENT : typeof jest !== 'undefined') {
updateSize(fn => fn());
} else {
queueMicrotask(() => updateSize(flushSync));
}
});
}, [updateSize]);
useEffect(() => {
if (!didUpdateSize.current) {
// If useEffect ran before the above microtask, we are in a synchronous render (e.g. act).
// Update the size here so that you don't need to mock timers in tests.
didUpdateSize.current = true;
updateSize(fn => fn());
}
}, [updateSize]);

lastContentSize.current = contentSize;
});

let onResize = useCallback(() => {
updateSize(flushSync);
}, [updateSize]);
Expand Down
1 change: 1 addition & 0 deletions packages/@react-spectrum/s2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@
"@react-aria/interactions": "^3.22.2",
"@react-aria/utils": "^3.25.2",
"@react-spectrum/utils": "^3.11.10",
"@react-stately/virtualizer": "^4.0.1",
"@react-types/color": "3.0.0-rc.1",
"@react-types/dialog": "^3.5.8",
"@react-types/provider": "^3.7.2",
Expand Down
8 changes: 6 additions & 2 deletions packages/@react-spectrum/s2/src/ActionButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,17 @@
*/

import {baseColor, fontRelative, style} from '../style/spectrum-theme' with { type: 'macro' };
import {ButtonProps, ButtonRenderProps, ContextValue, OverlayTriggerStateContext, Provider, Button as RACButton} from 'react-aria-components';
import {ButtonProps, ButtonRenderProps, ContextValue, OverlayTriggerStateContext, Provider, Button as RACButton, Text} from 'react-aria-components';
import {centerBaseline} from './CenterBaseline';
import {createContext, forwardRef, ReactNode, useContext} from 'react';
import {FocusableRef, FocusableRefValue} from '@react-types/shared';
import {focusRing, getAllowedOverrides, StyleProps} from './style-utils' with { type: 'macro' };
import {IconContext} from './Icon';
import {pressScale} from './pressScale';
import {Text, TextContext} from './Content';
import {SkeletonContext} from './Skeleton';
import {TextContext} from './Content';
import {useFocusableRef} from '@react-spectrum/utils';
import {useFormProps} from './Form';
import {useSpectrumContextProps} from './useSpectrumContextProps';

export interface ActionButtonStyleProps {
Expand Down Expand Up @@ -175,6 +177,7 @@ export const ActionButtonContext = createContext<ContextValue<ActionButtonProps,

function ActionButton(props: ActionButtonProps, ref: FocusableRef<HTMLButtonElement>) {
[props, ref] = useSpectrumContextProps(props, ref, ActionButtonContext);
props = useFormProps(props as any);
let domRef = useFocusableRef(ref);
let overlayTriggerState = useContext(OverlayTriggerStateContext);

Expand All @@ -193,6 +196,7 @@ function ActionButton(props: ActionButtonProps, ref: FocusableRef<HTMLButtonElem
}, props.styles)}>
<Provider
values={[
[SkeletonContext, null],
[TextContext, {styles: style({paddingY: '--labelPadding', order: 1, truncate: true})}],
[IconContext, {
render: centerBaseline({slot: 'icon', styles: style({order: 0})}),
Expand Down
12 changes: 6 additions & 6 deletions packages/@react-spectrum/s2/src/ActionMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@ import {useSpectrumContextProps} from './useSpectrumContextProps';

export interface ActionMenuProps<T> extends
Pick<MenuTriggerProps, 'isOpen' | 'defaultOpen' | 'onOpenChange' | 'align' | 'direction' | 'shouldFlip'>,
Pick<MenuProps<T>, 'children' | 'items' | 'disabledKeys' | 'onAction' | 'size'>,
Pick<ActionButtonProps, 'isDisabled' | 'isQuiet' | 'autoFocus'>,
Pick<MenuProps<T>, 'children' | 'items' | 'disabledKeys' | 'onAction'>,
Pick<ActionButtonProps, 'isDisabled' | 'isQuiet' | 'autoFocus' | 'size'>,
StyleProps, DOMProps, AriaLabelingProps {
}
menuSize?: 'S' | 'M' | 'L' | 'XL'
}

export const ActionMenuContext = createContext<ContextValue<ActionMenuProps<any>, FocusableRefValue<HTMLButtonElement>>>(null);

Expand All @@ -41,7 +42,6 @@ function ActionMenu<T extends object>(props: ActionMenuProps<T>, ref: FocusableR
buttonProps['aria-label'] = stringFormatter.format('menu.moreActions');
}

// size independently controlled?
return (
<MenuTrigger
isOpen={props.isOpen}
Expand All @@ -52,19 +52,19 @@ function ActionMenu<T extends object>(props: ActionMenuProps<T>, ref: FocusableR
shouldFlip={props.shouldFlip}>
<ActionButton
ref={ref}
aria-label="Help"
size={props.size}
isDisabled={props.isDisabled}
autoFocus={props.autoFocus}
isQuiet={props.isQuiet}
styles={props.styles}
{...buttonProps}>
<MoreIcon />
</ActionButton>
<Menu
items={props.items}
disabledKeys={props.disabledKeys}
onAction={props.onAction}
size={props.size}>
size={props.menuSize}>
{/* @ts-ignore TODO: fix type, right now this component is the same as Menu */}
{props.children}
</Menu>
Expand Down
8 changes: 5 additions & 3 deletions packages/@react-spectrum/s2/src/Avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {createContext, forwardRef} from 'react';
import {DOMProps, DOMRef, DOMRefValue} from '@react-types/shared';
import {filterDOMProps} from '@react-aria/utils';
import {getAllowedOverrides, StylesPropWithoutWidth, UnsafeStyles} from './style-utils' with {type: 'macro'};
import {Image} from './Image';
import {style} from '../style/spectrum-theme' with { type: 'macro' };
import {useDOMRef} from '@react-spectrum/utils';
import {useSpectrumContextProps} from './useSpectrumContextProps';
Expand Down Expand Up @@ -71,16 +72,17 @@ function Avatar(props: AvatarProps, ref: DOMRef<HTMLImageElement>) {
let remSize = size / 16 + 'rem';
let isLarge = size >= 64;
return (
<img
<Image
{...domProps}
ref={domRef}
alt={alt}
style={{
UNSAFE_style={{
...UNSAFE_style,
width: remSize,
height: remSize
}}
className={(UNSAFE_className ?? '') + imageStyles({isOverBackground, isLarge}, props.styles)}
UNSAFE_className={UNSAFE_className}
styles={imageStyles({isOverBackground, isLarge}, props.styles)}
src={src} />
);
}
Expand Down
30 changes: 17 additions & 13 deletions packages/@react-spectrum/s2/src/Badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,21 @@ import {filterDOMProps} from '@react-aria/utils';
import {fontRelative, style} from '../style/spectrum-theme' with {type: 'macro'};
import {IconContext} from './Icon';
import React, {createContext, forwardRef, ReactNode} from 'react';
import {SkeletonWrapper} from './Skeleton';
import {Text, TextContext} from './Content';
import {useDOMRef} from '@react-spectrum/utils';
import {useSpectrumContextProps} from './useSpectrumContextProps';

export interface BadgeStyleProps {
/**
* The size of the badge.
*
*
* @default 'S'
*/
size?: 'S' | 'M' | 'L' | 'XL',
/**
* The variant changes the background color of the badge. When badge has a semantic meaning, they should use the variant for semantic colors.
*
*
* @default 'neutral'
*/
variant?: 'accent' | 'informative' | 'neutral' | 'positive' | 'notice' | 'negative' | 'gray' | 'red' | 'orange' | 'yellow' | 'charteuse' | 'celery' | 'green' | 'seafoam' | 'cyan' | 'blue' | 'indigo' | 'purple' | 'fuchsia' | 'magenta' | 'pink' | 'turquoise' | 'brown' | 'cinnamon' | 'silver',
Expand Down Expand Up @@ -201,17 +202,20 @@ function Badge(props: BadgeProps, ref: DOMRef<HTMLDivElement>) {
styles: style({size: fontRelative(20), marginStart: '--iconMargin', flexShrink: 0})
}]
]}>
<span
{...filterDOMProps(otherProps)}
role="presentation"
className={(props.UNSAFE_className || '') + badge({variant, size, fillStyle}, props.styles)}
ref={domRef}>
{
typeof children === 'string' || isTextOnly
? <Text>{children}</Text>
: children
}
</span>
<SkeletonWrapper>
<span
{...filterDOMProps(otherProps)}
role="presentation"
className={(props.UNSAFE_className || '') + badge({variant, size, fillStyle}, props.styles)}
style={props.UNSAFE_style}
ref={domRef}>
{
typeof children === 'string' || isTextOnly
? <Text>{children}</Text>
: children
}
</span>
</SkeletonWrapper>
</Provider>
);
}
Expand Down
Loading