Skip to content

Commit fc13e93

Browse files
committed
feat(primitives): add Skeleton component
1 parent 3cfb07e commit fc13e93

File tree

16 files changed

+1147
-0
lines changed

16 files changed

+1147
-0
lines changed

packages/primitives/src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export * as Link from './link';
1515
export * as Popover from './popover';
1616
export * as Primitive from './primitive';
1717
export * as Separator from './separator';
18+
export * as Skeleton from './skeleton';
1819
export * as Spinner from './spinner';
1920
export * as Spoiler from './spoiler';
2021
export * as Tabs from './tabs';
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
# Skeleton
2+
3+
Shows a placeholder for loading content.
4+
5+
## Features
6+
7+
- Supports customizable width, height, and shape to fit various UI elements.
8+
9+
- Offers manual control over visibility and rendering duration.
10+
11+
## Import
12+
13+
```tsx
14+
import { Skeleton } from 'qwik-primitives';
15+
```
16+
17+
## Anatomy
18+
19+
Import all parts and piece them together.
20+
21+
```tsx
22+
import { component$ } from '@builder.io/qwik';
23+
import { Skeleton } from 'qwik-primitives';
24+
25+
const SkeletonDemo = component$(() => {
26+
return (
27+
<Skeleton.Root>
28+
<Skeleton.Content />
29+
<Skeleton.Fallback />
30+
</Skeleton.Root>
31+
);
32+
});
33+
```
34+
35+
## Usage
36+
37+
```tsx
38+
import { component$, useSignal, useVisibleTask$ } from '@builder.io/qwik';
39+
import { Skeleton } from 'qwik-primitives';
40+
41+
const SkeletonDemo = component$(() => {
42+
const isLoading = useSignal(true);
43+
44+
// eslint-disable-next-line qwik/no-use-visible-task
45+
useVisibleTask$(() => {
46+
setTimeout(() => {
47+
isLoading.value = false;
48+
}, 5000);
49+
});
50+
51+
return (
52+
<Skeleton.Root loading={isLoading.value}>
53+
<Skeleton.Content>
54+
{!isLoading.value && (
55+
<p style={{ height: '6rem', overflow: 'hidden' }}>
56+
Lorem ipsum dolor sit amet consectetur adipisicing elit. Laudantium sunt, cumque praesentium qui nobis
57+
tenetur aliquam repellendus neque dicta ipsam reprehenderit dolores voluptas architecto blanditiis vel,
58+
exercitationem at. Natus, aliquam!
59+
</p>
60+
)}
61+
</Skeleton.Content>
62+
63+
<Skeleton.Fallback style={{ height: '6rem', backgroundColor: 'purple' }} />
64+
</Skeleton.Root>
65+
);
66+
});
67+
```
68+
69+
## API Reference
70+
71+
### Root
72+
73+
Contains all the parts of a skeleton. By default, it will be rendered with the passed children only if the `loading` prop is set to `true` otherwise the component will render the passed children. Use the `forceMount` prop to always render this component with the passed children. This component is based on the `div` element.
74+
75+
| Prop | Type | Default | Description |
76+
| ------------ | ------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
77+
| `as` | `FunctionComponent` | `-` | Change the default rendered element for the one passed as, merging their props and behavior. Read our [Composition](https://github.com/ZAHON/qwik-primitives/blob/main/packages/primitives/docs/composition.md) guide for more details. |
78+
| `loading` | `boolean` | `-` | Toggles whether to hide the content and display the fallback. |
79+
| `forceMount` | `boolean` | `-` | Used to force mounting when more control is needed. |
80+
| `style` | `CSSProperties` | `-` | The inline style for the element. |
81+
82+
| Data attribute | Values |
83+
| -------------------- | ----------------------------------------- |
84+
| `[data-scope]` | `"skeleton"` |
85+
| `[data-part]` | `"root"` |
86+
| `[data-loading]` | Present when the skeleton is loading |
87+
| `[data-force-mount]` | Present when the component is force mount |
88+
89+
### Content
90+
91+
Contains loading content. By default, it will be rendered with the passed children only if the `loading` prop is set to `true` on `Skeleton.Root` component otherwise the component will render the passed children. Use the `forceMount` prop to always render this component with the passed children. This component is based on the `div` element.
92+
93+
| Prop | Type | Default | Description |
94+
| ------------ | ------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
95+
| `as` | `FunctionComponent` | `-` | Change the default rendered element for the one passed as, merging their props and behavior. Read our [Composition](https://github.com/ZAHON/qwik-primitives/blob/main/packages/primitives/docs/composition.md) guide for more details. |
96+
| `forceMount` | `boolean` | `-` | Used to force mounting when more control is needed. |
97+
| `style` | `CSSProperties` | `-` | The inline style for the element. |
98+
99+
| Data attribute | Values |
100+
| -------------------- | --------------------------------------- |
101+
| `[data-scope]` | `"skeleton"` |
102+
| `[data-part]` | `"content"` |
103+
| `[data-loading]` | Present when the skeleton is loading |
104+
| `[data-force-mount]` | Present when the content is force mount |
105+
106+
### Fallback
107+
108+
Contains alternate content while the actual content has not yet been loaded. By default, it will render only when the `loading` prop is set to `true` on `Skeleton.Root component,` use the `forceMount` prop to always render this component. This component is based on the `div` element.
109+
110+
| Prop | Type | Default | Description |
111+
| ------------ | ------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
112+
| `as` | `FunctionComponent` | `-` | Change the default rendered element for the one passed as, merging their props and behavior. Read our [Composition](https://github.com/ZAHON/qwik-primitives/blob/main/packages/primitives/docs/composition.md) guide for more details. |
113+
| `forceMount` | `boolean` | `-` | Used to force mounting when more control is needed. |
114+
| `style` | `CSSProperties` | `-` | The inline style for the element. |
115+
116+
| Data attribute | Values |
117+
| -------------------- | ---------------------------------------- |
118+
| `[data-scope]` | `"skeleton"` |
119+
| `[data-part]` | `"fallback"` |
120+
| `[data-loading]` | Present when the skeleton is loading |
121+
| `[data-force-mount]` | Present when the fallback is force mount |
122+
123+
## Examples
124+
125+
### With content that is already on page
126+
127+
If you want to indicate the loading state of content that is already on page, wrap it with `Skeleton.Content` and control `Skeleton.Fallback` visibility with `loading` prop.
128+
129+
```tsx
130+
import { component$, useSignal } from '@builder.io/qwik';
131+
import { Skeleton } from 'qwik-primitives';
132+
133+
const SkeletonDemo = component$(() => {
134+
const isLoading = useSignal(false);
135+
136+
return (
137+
<>
138+
<Skeleton.Root loading={isLoading.value}>
139+
<Skeleton.Content>
140+
<p>
141+
Lorem ipsum dolor sit amet consectetur adipisicing elit. Laudantium sunt, cumque praesentium qui nobis
142+
tenetur aliquam repellendus neque dicta ipsam reprehenderit dolores voluptas architecto blanditiis vel,
143+
exercitationem at. Natus, aliquam!
144+
</p>
145+
</Skeleton.Content>
146+
147+
<Skeleton.Fallback style={{ backgroundColor: 'purple' }} />
148+
</Skeleton.Root>
149+
150+
<button type="button" onClick$={() => (isLoading.value = !isLoading.value)}>
151+
Toggle content
152+
</button>
153+
</>
154+
);
155+
});
156+
```
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export type { SkeletonRootProps as RootProps } from './skeleton-root';
2+
export type { SkeletonContentProps as ContentProps } from './skeleton-content';
3+
export type { SkeletonFallbackProps as FallbackProps } from './skeleton-fallback';
4+
5+
export { SkeletonRoot as Root } from './skeleton-root';
6+
export { SkeletonContent as Content } from './skeleton-content';
7+
export { SkeletonFallback as Fallback } from './skeleton-fallback';
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export type { SkeletonContentProps } from './skeleton-content.types';
2+
export { SkeletonContent } from './skeleton-content';
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import type { SkeletonContentProps } from './skeleton-content.types';
2+
import { component$, useContext, Slot } from '@builder.io/qwik';
3+
import { SkeletonContext } from '../skeleton-context';
4+
5+
/**
6+
* Contains loading content.
7+
* By default, it will be rendered with the passed children only if the `loading` prop is set to `true` on `Skeleton.Root` component
8+
* otherwise the component will render the passed children.
9+
* Use the `forceMount` prop to always render this component with the passed children.
10+
* This component is based on the `div` element.
11+
*/
12+
export const SkeletonContent = component$<SkeletonContentProps>((props) => {
13+
const { as, forceMount, style, ...others } = props;
14+
15+
const { isLoading } = useContext(SkeletonContext);
16+
17+
const Component = as || 'div';
18+
19+
if (!isLoading.value && !forceMount) return <Slot />;
20+
21+
return (
22+
<Component
23+
inert={undefined}
24+
aria-hidden={isLoading.value ? 'true' : undefined}
25+
data-qwik-primitives-skeleton-content=""
26+
data-scope="skeleton"
27+
data-part="content"
28+
data-loading={isLoading.value ? '' : undefined}
29+
data-force-mount={forceMount ? '' : undefined}
30+
style={{
31+
// `display: contents` removes the content from the accessibility tree in some browsers,
32+
// so we force remove it with `aria-hidden`
33+
display: isLoading.value ? 'contents' : undefined,
34+
visibility: isLoading.value ? 'hidden' : undefined,
35+
pointerEvents: isLoading.value ? 'none' : undefined,
36+
...style,
37+
}}
38+
{...others}
39+
>
40+
<Slot />
41+
</Component>
42+
);
43+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type { PropsOf, FunctionComponent, CSSProperties } from '@builder.io/qwik';
2+
3+
export interface SkeletonContentProps extends PropsOf<'div'> {
4+
/**
5+
* Change the default rendered element for the one passed as, merging their props and behavior.
6+
*
7+
* Read our [Composition](https://github.com/ZAHON/qwik-primitives/blob/main/packages/primitives/docs/composition.md) guide for more details.
8+
*/
9+
as?: FunctionComponent;
10+
11+
/**
12+
* Used to force mounting when more control is needed.
13+
*/
14+
forceMount?: boolean;
15+
16+
/**
17+
* The inline style for the element.
18+
*/
19+
style?: CSSProperties;
20+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export type { SkeletonContextValue } from './skeleton-context.types';
2+
export { SkeletonContext } from './skeleton-context';
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import type { SkeletonContextValue } from './skeleton-context.types';
2+
import { createContextId } from '@builder.io/qwik';
3+
4+
export const SkeletonContext = createContextId<SkeletonContextValue>('qwik-primitives-skeleton-context');
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import type { ReadonlySignal } from '@builder.io/qwik';
2+
3+
export interface SkeletonContextValue {
4+
/**
5+
* Toggles whether to hide the content and display the fallback.
6+
*/
7+
isLoading: ReadonlySignal<boolean | undefined>;
8+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export type { SkeletonFallbackProps } from './skeleton-fallback.types';
2+
export { SkeletonFallback } from './skeleton-fallback';

0 commit comments

Comments
 (0)