-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Conversation
…-card # Conflicts: # packages/@react-spectrum/s2/src/Avatar.tsx # packages/@react-spectrum/s2/src/Content.tsx # packages/@react-spectrum/s2/src/Dialog.tsx # packages/@react-spectrum/s2/src/Divider.tsx # packages/@react-spectrum/s2/src/IllustratedMessage.tsx # packages/@react-spectrum/s2/src/Menu.tsx # packages/@react-spectrum/s2/src/index.ts # packages/@react-spectrum/s2/src/useSpectrumContextProps.ts
# Conflicts: # .parcelrc-build # .storybook-s2/.parcelrc # Makefile # packages/@react-spectrum/s2/package.json # packages/@react-spectrum/s2/spectrum-illustrations/linear/AlertNotice.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Apps.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/ArrowDown.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/ArrowLeft.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/ArrowRight.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/ArrowUp.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Artboard.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Bell.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Bolt.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Brand.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Briefcase.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Browser.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/BrowserError.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/BrowserNotCompatible.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/BuildTable.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Buildings.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Calendar.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Camera.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Chatbubble.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Check.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Clipboard.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Clock.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Close.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Cloud.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/CloudStateDisconnected.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/CloudStateError.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/CloudUpload.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/CodeBrackets.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/ConfettiCelebration.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Conversationbubbles.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Cursor.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Desktop.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Document.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/DropToUpload.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/EmptyStateExport.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Error.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/FileAlert.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/FileImage.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/FileText.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/FileVideo.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/FileZip.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Filmstrip.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Filter.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Fireworks.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/FolderClose.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/FolderOpen.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/GearSetting.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/GraphBarChart.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Handshake.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Heart.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Image.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/ImageStack.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Information.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Laptop.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Layers.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Libraries.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Lightbulb.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/LightbulbRays.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Link.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Location.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/LockClose.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/LockOpen.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Logo.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/MailClose.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/MegaphonePromote.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/MegaphonePromoteExpressive.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Paperairplane.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Paperclip.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Phone.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/PieChart.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Pin.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Play.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Plugin.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Rocket.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Search.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Server.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Sparkles.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Star.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Tablet.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Tag.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Trash.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Trophy.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Update.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/User.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/UserGroup.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Video.tsx # packages/@react-spectrum/s2/spectrum-illustrations/linear/Warning.tsx # packages/@react-spectrum/s2/src/ActionButton.tsx # packages/@react-spectrum/s2/src/ActionMenu.tsx # packages/@react-spectrum/s2/src/Avatar.tsx # packages/@react-spectrum/s2/src/Badge.tsx # packages/@react-spectrum/s2/src/Breadcrumbs.tsx # packages/@react-spectrum/s2/src/Button.tsx # packages/@react-spectrum/s2/src/Checkbox.tsx # packages/@react-spectrum/s2/src/Content.tsx # packages/@react-spectrum/s2/src/ContextualHelp.tsx # packages/@react-spectrum/s2/src/Dialog.tsx # packages/@react-spectrum/s2/src/DropZone.tsx # packages/@react-spectrum/s2/src/InlineAlert.tsx # packages/@react-spectrum/s2/src/Link.tsx # packages/@react-spectrum/s2/src/Menu.tsx # packages/@react-spectrum/s2/src/Meter.tsx # packages/@react-spectrum/s2/src/NumberField.tsx # packages/@react-spectrum/s2/src/Picker.tsx # packages/@react-spectrum/s2/src/ProgressBar.tsx # packages/@react-spectrum/s2/src/RangeSlider.tsx # packages/@react-spectrum/s2/src/StatusLight.tsx # packages/@react-spectrum/s2/src/TagGroup.tsx # packages/@react-spectrum/s2/src/ToggleButton.tsx # packages/@react-spectrum/s2/src/bar-utils.ts # packages/@react-spectrum/s2/src/index.ts # packages/@react-spectrum/s2/stories/DropZone.stories.tsx # packages/@react-spectrum/s2/stories/IllustratedMessage.stories.tsx # packages/@react-spectrum/s2/style/runtime.ts # packages/dev/parcel-namer-s2/S2Namer.js # packages/dev/parcel-transformer-s2-icon/IconTransformer.js # scripts/generateS2IconIndex.js # yarn.lock
if (isHidden) { | ||
return null; | ||
} | ||
|
||
slot = slot && racContext && 'slots' in racContext && !racContext.slots?.[slot] ? undefined : slot; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if we want this or not. The idea is to not forward Spectrum-specific slots that RAC doesn't expect
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't had a better Idea yet
We'd talked about clearing our the context around those components in question. Or possibly using a mergeContexts
(like has been requested in issues).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems related: #6992
} | ||
|
||
return result; | ||
}, [ctx, props, isSkeleton]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it a bad idea to do this here? It was an easy way to mark all form components within a skeleton as disabled.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems somewhat unrelated to forms, but we could always move it out to a separate hook later. useInteractiveSpectrumComponent
or whatever
or useIsSkeleton
could could accept props and handle the merge? though then we're sensitive to the order call the hooks in, so in some regards this is better
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with @snowystinger here, I would've expected that there could be all kinds of components within a skeleton, not just form components so a more general hook feels like it makes more sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, the alternative is to manually do this in every individual component rather than here in one place. Or I guess we could rename useFormProps
to something else...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is fine for now then, we can see how things shake out when we use Skeleton in more places
/** | ||
* Content for this view if it was generated by the layout rather than coming from the Collection. | ||
*/ | ||
content: any | null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally we'd use a generic here but that would require a lot of changes to propagate everywhere...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lot of changes, but would they be breaking?
onLoadMore?: () => void | ||
} | ||
|
||
class FlexibleGridLayout<T extends object, O> extends Layout<Node<T>, O> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is different from the GridLayout in RAC because it supports variable row heights - the height of a row is equal to the maximum height of the items in that row. The RAC GridLayout just scales the sizes of its items proportionally, which won't work for wrapping text. I could see potentially adding this as an option in RAC at some point.
let childRef = 'ref' in children ? children.ref as any : children.props.ref; | ||
return ( | ||
<SkeletonContext.Provider value={null}> | ||
{isLoading ? cloneElement(children, { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just double checking, this cloneElement is okay because outside users won't get between this and the things we're rendering. The children will always be our own component internals.
Looks like the plan is to let developers use these components, so we'll just have to document that they need to put the styled child as an immediate child, can't wrap it with anything
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, SkeletonWrapper
is not exported publicly.
} | ||
|
||
return result; | ||
}, [ctx, props, isSkeleton]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems somewhat unrelated to forms, but we could always move it out to a separate hook later. useInteractiveSpectrumComponent
or whatever
or useIsSkeleton
could could accept props and handle the merge? though then we're sensitive to the order call the hooks in, so in some regards this is better
if (isHidden) { | ||
return null; | ||
} | ||
|
||
slot = slot && racContext && 'slots' in racContext && !racContext.slots?.[slot] ? undefined : slot; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't had a better Idea yet
We'd talked about clearing our the context around those components in question. Or possibly using a mergeContexts
(like has been requested in issues).
} | ||
|
||
export function ImageCoordinator(props: ImageCoordinatorProps) { | ||
// If we are already inside another ImageList, just pass |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// If we are already inside another ImageList, just pass | |
// If we are already inside another ImageCoordinator, just pass |
Is this what it's saying? Or can there be multiple lists in one coordinator?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nested ImageCoordinators don't do anything, they just delegate to the parent.
|
||
useLayoutEffect(() => { | ||
if (domRef.current) { | ||
// TODO: should RAC Link pass through inert? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think so, seems like an ok thing to pass through in general and might make it easier for people to make their website/form builders instead of having to wrap all their components
export const SkeletonCollection = createLeafComponent('skeleton', (props: SkeletonCollectionProps, ref, node) => { | ||
// Cache rendering based on node object identity. This allows the children function to randomize | ||
// its content (e.g. heights) and preserve on re-renders. | ||
// TODO: do we need a `dependencies` prop here? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you expand on why we might need dependencies here? what kinds of things can change where we'd want to update?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just like <Collection>
, this component caches the results. So if the child function depends on values from outside, they'd need to be dependencies so that the cache invalidates.
@@ -114,8 +114,8 @@ export class Virtualizer<T extends object, V> { | |||
} | |||
|
|||
private _renderView(reusableView: ReusableView<T, V>) { | |||
let {type, key} = reusableView.layoutInfo; | |||
reusableView.content = this.collection.getItem(key); | |||
let {type, key, content} = reusableView.layoutInfo; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't quite follow the implications of this change
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I recall correctly, the skeleton cards aren't actually in the collection so this.collection.getItem(key)
won't actually work here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yep. this enables the layout to produce items that aren't in the original collection.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The docs page for Cards has a warning about missing labels.
Noticed a bit of a bug with virtualizer reuse. Selection can appear to move to other un-selected items when moving around via keyboard. I know we have this in TableView and ListView, but it feels more noticeable because of the more obvious selected border. Can discuss/handle outside the PR.
cardview-selections.mov
// If the image is already loaded, update state immediately instead of waiting for onLoad. | ||
if (state === 'loading' && imgRef.current?.complete) { | ||
// Queue a microtask so we don't hit React's update limit. | ||
// TODO: is this necessary? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume you tested it both ways? I don't think it should be necessary, but interested in what you ran into
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's sort of hard to tell if I'm seeing placebo or how much difference it actually makes, but I noticed that the image's complete
property could be true before the onLoad
event fires (async). So the theory here is that we could show the image slightly earlier, which is important while scrolling. Just not sure how much difference it makes.
objectFit: 'inherit', | ||
objectPosition: 'inherit', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
clever, i like this approach for setting these styles instead of trying to pull out the img specific styles
[AvatarContext, { | ||
size: avatarSize[size], | ||
UNSAFE_style: { | ||
'--size': avatarSize[size] + 'px' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we've got to start an audit of all these variables we introduce
or hash them for internal module use or something. it'd be nice if we could export/import them internally as well, so we know where they are defined vs used
don't worry about in this PR
let {children, layout: layoutName = 'grid', size = 'M', density = 'regular', variant = 'primary', selectionStyle = 'checkbox', UNSAFE_className = '', UNSAFE_style, styles, ...otherProps} = props; | ||
let options = layoutOptions[size][density]; | ||
let layout = useMemo(() => { | ||
variant; // needed to invalidate useMemo |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think this is preferable over eslint-ignoring the entire dep array, should update this in other places. outside scope of pr
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The styles and behavior look great! Just a few initial comments
<Card href={topic.links.html} target="_blank" textValue={topic.title}> | ||
<CollectionCardPreview> | ||
{topic.preview_photos.slice(0, 4).map(photo => ( | ||
<Image key={photo.id} alt="" src={photo.urls.small} /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would we support wrapping these in links?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still going through the Skeleton and layout logic, some initial comments from testing. Overall, performs nicely, tested a variety of keyboard operations/prefers reduced motion/etc
@@ -114,8 +114,8 @@ export class Virtualizer<T extends object, V> { | |||
} | |||
|
|||
private _renderView(reusableView: ReusableView<T, V>) { | |||
let {type, key} = reusableView.layoutInfo; | |||
reusableView.content = this.collection.getItem(key); | |||
let {type, key, content} = reusableView.layoutInfo; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I recall correctly, the skeleton cards aren't actually in the collection so this.collection.getItem(key)
won't actually work here.
} | ||
|
||
return result; | ||
}, [ctx, props, isSkeleton]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with @snowystinger here, I would've expected that there could be all kinds of components within a skeleton, not just form components so a more general hook feels like it makes more sense.
outlineStyle: 'none' | ||
}, getAllowedOverrides({height: true})); | ||
|
||
export function CardView<T extends object>(props: CardViewProps<T>) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I remember for the old CardView implementation that we offered the ability to customize the layout via maxColumns, inter row/column padding, etc. I assume these were scrapped by Spectrum but just wanna make sure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Testing was pretty good, happy to get in and address anything else in new PRs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still see the horizontal scrollbar on initial render. I'm not fussed about it. It goes away if I resize the page.
That's embarrassing. I had it working and then optimized it and apparently it broke and I didn't notice? Reverted to the previous version I had, which updates more frequently (whenever the content size changes at all) rather than trying to detect only when the scrollbars show and hide. |
Updated with the following changes based on my meeting with design today:
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, double checked disabled/selected/onAction etc locally just in case and things seem to perform well. Only thing is that we might want to hide DnD from the props since isn't ready yet right?
} | ||
|
||
return result; | ||
}, [ctx, props, isSkeleton]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is fine for now then, we can see how things shake out when we use Skeleton in more places
I can do this in a followup |
This adds S2 Cards, which come in the following configurations:
Card
- a generic card container, into which you can place a<CardPreview>
,<Content>
withtitle
,description
and action menu slots, and a<Footer>
. Cards come in 4 sizes, 3 densities, and 4 variants. A<CollectionCardPreview>
can also be used to display a grid of multiple images in a preview.AssetCard
- a pre-configuredCard
for displaying an asset, with an image preview and metadata.UserCard
- a pre-configuredCard
for displaying a user, with an optional preview, avatar, and metadata.ProductCard
- a pre-configuredCard
for displaying a product, with an optional preview, thumbnail, metadata, and footer call to action.CardView
displays a grid or waterfall layout of cards, with selection, keyboard navigation, item actions/links, infinite loading, etc.Cards also integrate with a new system for ghost/skeleton loading.
Image
component displays a loading animation and supports custom error states. Once the image finishes loading, it fades in if it took longer than 200ms.ImageCoordinator
component coordinates the loading states for a group of images, e.g. in a CardView or individual Card. Rather than popping in a random order as soon as they finish loading, the images all fade in together once they have all finished. If this takes longer than 5 seconds (by default), the images are shown as they are ready.CardView
wraps its children in anImageCoordinator
so that this behavior happens automatically, and it can be used in other collection components too.Skeleton
component causes all<Image>
,<Avatar>
,<Text>
, and icons within it to render as a skeleton, with a shimmer animation instead of displaying its content. Other components likeStatusLight
andBadge
are also skeleton-aware. This enables you to create skeletons with accurate layout by rendering your usual components with mock content. Skeletons are not specific to cards, and can be used anywhere.SkeletonCollection
component generates skeleton items within a collection component such as aCardView
. It accepts a function as its children which you can use to generate mock data and render your Card component. This function is called as many times as needed to either fill the viewport in case of initial load, or fill the current row in case of load more.