14 changed files with 427 additions and 0 deletions
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
export * from "./skeleton.component"; |
||||
export * from "./skeleton-text.component"; |
||||
export * from "./skeleton-group.component"; |
||||
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
<div class="tw-flex tw-flex-row tw-justify-between tw-gap-2"> |
||||
<div class="tw-flex tw-gap-2 tw-w-full"> |
||||
<ng-content select="[slot=start]"></ng-content> |
||||
<ng-content></ng-content> |
||||
</div> |
||||
<ng-content select="[slot=end]"></ng-content> |
||||
</div> |
||||
@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
|
||||
import { CommonModule } from "@angular/common"; |
||||
import { Component } from "@angular/core"; |
||||
|
||||
/** |
||||
* Arranges skeleton loaders into a pre-arranged group that mimics the table and item components. |
||||
* |
||||
* Pass skeleton loaders into the start, default, and end content slots. The content within each slot |
||||
* is fully customizable. |
||||
*/ |
||||
@Component({ |
||||
selector: "bit-skeleton-group", |
||||
templateUrl: "./skeleton-group.component.html", |
||||
imports: [CommonModule], |
||||
host: { |
||||
class: "tw-block", |
||||
}, |
||||
}) |
||||
export class SkeletonGroupComponent {} |
||||
@ -0,0 +1,73 @@
@@ -0,0 +1,73 @@
|
||||
import { Meta, moduleMetadata, StoryObj } from "@storybook/angular"; |
||||
|
||||
import { SharedModule } from "../shared/shared.module"; |
||||
|
||||
import { SkeletonGroupComponent } from "./skeleton-group.component"; |
||||
import { SkeletonTextComponent } from "./skeleton-text.component"; |
||||
import { SkeletonComponent } from "./skeleton.component"; |
||||
|
||||
export default { |
||||
title: "Component Library/Skeleton/Skeleton Group", |
||||
component: SkeletonGroupComponent, |
||||
decorators: [ |
||||
moduleMetadata({ |
||||
imports: [SharedModule, SkeletonTextComponent, SkeletonComponent], |
||||
}), |
||||
], |
||||
} as Meta<SkeletonGroupComponent>; |
||||
|
||||
type Story = StoryObj<SkeletonGroupComponent>; |
||||
|
||||
export const Default: Story = { |
||||
render: (args) => ({ |
||||
props: args, |
||||
template: /*html*/ ` |
||||
<bit-skeleton-group> |
||||
<bit-skeleton class="tw-size-8" slot="start"></bit-skeleton> |
||||
<bit-skeleton-text [lines]="2" class="tw-w-1/2"></bit-skeleton-text> |
||||
<bit-skeleton-text [lines]="1" slot="end" class="tw-w-1/4"></bit-skeleton-text> |
||||
</bit-skeleton-group> |
||||
`,
|
||||
}), |
||||
}; |
||||
|
||||
export const NoEndSlot: Story = { |
||||
render: (args) => ({ |
||||
props: args, |
||||
template: /*html*/ ` |
||||
<bit-skeleton-group> |
||||
<bit-skeleton class="tw-size-8" slot="start"></bit-skeleton> |
||||
<bit-skeleton-text [lines]="2" class="tw-w-1/2"></bit-skeleton-text> |
||||
</bit-skeleton-group> |
||||
`,
|
||||
}), |
||||
}; |
||||
|
||||
export const NoStartSlot: Story = { |
||||
render: (args) => ({ |
||||
props: args, |
||||
template: /*html*/ ` |
||||
<bit-skeleton-group> |
||||
<bit-skeleton-text [lines]="2" class="tw-w-1/2"></bit-skeleton-text> |
||||
<bit-skeleton-text [lines]="1" slot="end" class="tw-w-1/4"></bit-skeleton-text> |
||||
</bit-skeleton-group> |
||||
`,
|
||||
}), |
||||
}; |
||||
|
||||
export const CustomContent: Story = { |
||||
render: (args) => ({ |
||||
props: args, |
||||
template: /*html*/ ` |
||||
<bit-skeleton-group> |
||||
<bit-skeleton class="tw-size-12" slot="start" edgeShape="circle"></bit-skeleton> |
||||
<bit-skeleton-text [lines]="3" class="tw-w-full"></bit-skeleton-text> |
||||
<div slot="end" class="tw-flex tw-flex-row tw-gap-1"> |
||||
<bit-skeleton class="tw-size-4" slot="start"></bit-skeleton> |
||||
<bit-skeleton class="tw-size-4" slot="start"></bit-skeleton> |
||||
<bit-skeleton class="tw-size-4" slot="start"></bit-skeleton> |
||||
</div> |
||||
</bit-skeleton-group> |
||||
`,
|
||||
}), |
||||
}; |
||||
@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
|
||||
<div class="tw-w-full tw-flex tw-flex-col tw-gap-2"> |
||||
@for (line of this.linesArray(); track $index; let last = $last, first = $first) { |
||||
<bit-skeleton |
||||
class="tw-h-3" |
||||
[ngClass]="{ |
||||
'tw-w-full': first || !last, |
||||
'tw-w-1/3': !first && last, |
||||
}" |
||||
></bit-skeleton> |
||||
} |
||||
</div> |
||||
@ -0,0 +1,31 @@
@@ -0,0 +1,31 @@
|
||||
import { CommonModule } from "@angular/common"; |
||||
import { Component, computed, input } from "@angular/core"; |
||||
|
||||
import { SkeletonComponent } from "./skeleton.component"; |
||||
|
||||
/** |
||||
* Specific skeleton component used to represent lines of text. It uses the `bit-skeleton` |
||||
* under the hood. |
||||
* |
||||
* Customize the number of lines represented with the `lines` input. Customize the width |
||||
* by applying a class to the `bit-skeleton-text` element (i.e. `tw-w-1/2`). |
||||
*/ |
||||
@Component({ |
||||
selector: "bit-skeleton-text", |
||||
templateUrl: "./skeleton-text.component.html", |
||||
imports: [CommonModule, SkeletonComponent], |
||||
host: { |
||||
class: "tw-block", |
||||
}, |
||||
}) |
||||
export class SkeletonTextComponent { |
||||
/** |
||||
* The number of text lines to display |
||||
*/ |
||||
readonly lines = input<number>(1); |
||||
|
||||
/** |
||||
* Array-transformed version of the `lines` to loop over |
||||
*/ |
||||
protected linesArray = computed(() => [...Array(this.lines()).keys()]); |
||||
} |
||||
@ -0,0 +1,48 @@
@@ -0,0 +1,48 @@
|
||||
import { Meta, moduleMetadata, StoryObj } from "@storybook/angular"; |
||||
|
||||
import { SharedModule } from "../shared/shared.module"; |
||||
|
||||
import { SkeletonTextComponent } from "./skeleton-text.component"; |
||||
|
||||
import { formatArgsForCodeSnippet } from ".storybook/format-args-for-code-snippet"; |
||||
|
||||
export default { |
||||
title: "Component Library/Skeleton/Skeleton Text", |
||||
component: SkeletonTextComponent, |
||||
decorators: [ |
||||
moduleMetadata({ |
||||
imports: [SharedModule], |
||||
}), |
||||
], |
||||
args: { |
||||
lines: 1, |
||||
}, |
||||
argTypes: { |
||||
lines: { |
||||
control: { type: "number", min: 1 }, |
||||
}, |
||||
}, |
||||
} as Meta<SkeletonTextComponent>; |
||||
|
||||
type Story = StoryObj<SkeletonTextComponent>; |
||||
|
||||
export const Text: Story = { |
||||
render: (args) => ({ |
||||
props: args, |
||||
template: /*html*/ ` |
||||
<bit-skeleton-text ${formatArgsForCodeSnippet<SkeletonTextComponent>(args)}></bit-skeleton-text> |
||||
`,
|
||||
}), |
||||
}; |
||||
|
||||
export const TextMultiline: Story = { |
||||
render: (args) => ({ |
||||
props: args, |
||||
template: /*html*/ ` |
||||
<bit-skeleton-text ${formatArgsForCodeSnippet<SkeletonTextComponent>(args)}></bit-skeleton-text> |
||||
`,
|
||||
}), |
||||
args: { |
||||
lines: 5, |
||||
}, |
||||
}; |
||||
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
<div |
||||
class="tw-size-full tw-bg-secondary-100 tw-animate-pulse" |
||||
[ngClass]="{ |
||||
'tw-rounded': edgeShape() === 'box', |
||||
'tw-rounded-full': edgeShape() === 'circle', |
||||
}" |
||||
aria-hidden="true" |
||||
></div> |
||||
@ -0,0 +1,26 @@
@@ -0,0 +1,26 @@
|
||||
import { CommonModule } from "@angular/common"; |
||||
import { Component, input } from "@angular/core"; |
||||
|
||||
/** |
||||
* Basic skeleton loading component that can be used to represent content that is loading. |
||||
* Use for layout-level elements and text, not for interactive elements. |
||||
* |
||||
* Customize the shape's edges with the `edgeShape` input. Customize the shape's size by |
||||
* applying classes to the `bit-skeleton` element (i.e. `tw-w-40 tw-h-8`). |
||||
* |
||||
* If you're looking to represent lines of text, use the `bit-skeleton-text` helper component. |
||||
*/ |
||||
@Component({ |
||||
selector: "bit-skeleton", |
||||
templateUrl: "./skeleton.component.html", |
||||
imports: [CommonModule], |
||||
host: { |
||||
class: "tw-block", |
||||
}, |
||||
}) |
||||
export class SkeletonComponent { |
||||
/** |
||||
* The shape of the corners of the skeleton element |
||||
*/ |
||||
readonly edgeShape = input<"box" | "circle">("box"); |
||||
} |
||||
@ -0,0 +1,112 @@
@@ -0,0 +1,112 @@
|
||||
import { Meta, Canvas, Source } from "@storybook/addon-docs"; |
||||
|
||||
import * as skeletonStories from "./skeleton.stories"; |
||||
import * as skeletonTextStories from "./skeleton-text.stories"; |
||||
import * as skeletonGroupStories from "./skeleton-group.stories"; |
||||
|
||||
<Meta title="Component Library/Skeleton" /> |
||||
|
||||
# Skeleton Loading |
||||
|
||||
The skeleton component can be used as an alternative loading indicator to the spinner by mimicking |
||||
the content that will be loaded such as text, images, or video. It can be used to represent layout |
||||
components as well, but should not be used for interactive elements like form controls or buttons. |
||||
|
||||
## Skeleton Loading Components |
||||
|
||||
There are three components that can be used to create a skeleton loading page. |
||||
|
||||
### Skeleton |
||||
|
||||
Basic skeleton loading component that can be used to represent content that is loading. Use for |
||||
non-text shapes. |
||||
|
||||
#### Customizing |
||||
|
||||
The basic skeleton component is fully customizable in shape and edge appearance to allow consumers |
||||
to more accurately represent the content. |
||||
|
||||
**Inputs** |
||||
|
||||
| Input | Description | Accepted options | Default | |
||||
| ----------- | --------------------------------------------------- | ---------------- | ------- | |
||||
| `edgeShape` | configure whether corners are fully rounded or boxy | `box`, `circle` | `box` | |
||||
|
||||
**Classes** |
||||
|
||||
Customize the shape's size by applying tailwind size classes to the `bit-skeleton` element (example |
||||
`tw-h-3 tw-w-12`). Please refer to the tailwind docs for all height/width options, and note that |
||||
custom values are possible with tailwind as well. |
||||
|
||||
<Canvas of={skeletonStories.BoxEdgeShape} /> |
||||
<Canvas of={skeletonStories.CircleEdgeShape} /> |
||||
|
||||
### Skeleton Text |
||||
|
||||
Specific skeleton component used to represent lines of text. |
||||
|
||||
#### Customizing |
||||
|
||||
The number of lines of text in the skeleton is configurable. |
||||
|
||||
**Inputs** |
||||
|
||||
| Input | Description | Accepted options | Default | |
||||
| ------- | --------------------------------------------- | ---------------- | ------- | |
||||
| `lines` | configure how many lines of text are rendered | any `number` | `1` | |
||||
|
||||
<Canvas of={skeletonTextStories.Text} /> |
||||
<Canvas of={skeletonTextStories.TextMultiline} /> |
||||
|
||||
### Skeleton Group |
||||
|
||||
Arranges skeleton loaders into a pre-arranged group that mimics the table and item components. |
||||
|
||||
#### Customizing |
||||
|
||||
**Slots** |
||||
|
||||
Use the following slots to render `<bit-skeleton>` and/or `bit-skeleton-text` elements. |
||||
|
||||
| Slot | Description | |
||||
| -------------- | ----------------------------------------------------------------------------------------------- | |
||||
| `slot="start"` | content that should appear horizontally before the default content; will not grow to fill space | |
||||
| default | main content area; grows to fill the horizontal space | |
||||
| `slot="end"` | content that should appear horizontally after the default content; will not grow to fill space | |
||||
|
||||
<Canvas of={skeletonGroupStories.Default} /> |
||||
|
||||
## Display Considerations |
||||
|
||||
For pages that load quickly, we want to avoid the skeleton flashing in and out. To avoid this, we |
||||
recommend the following display guidelines: |
||||
|
||||
- After the loading is initiated (by page load or by user action), wait 1 second to display the |
||||
skeleton loader. |
||||
- After waiting 1s, render the loading skeleton. |
||||
- Ideally the skeleton disappears after 10 seconds, but we do not enforce a max duration. Add a max |
||||
duration at your discretion. |
||||
|
||||
## Accessibility |
||||
|
||||
Because there are typically multiple skeleton loaders present on a page that is using skeleton |
||||
loading, the individual skeleton loaders should not announce themselves or be present to |
||||
screenreaders, as this would overwhelm the user with multiple identical announcements. Thus, the |
||||
skeleton components are hidden from screenreaders. |
||||
|
||||
Instead, the recommended strategy is to use a page-level announcement for screenreaders: |
||||
|
||||
- We recommend using the |
||||
[Angular CDK LiveAnnouncer](https://material.angular.dev/cdk/a11y/overview#liveannouncer) to first |
||||
announce that content is loading when the skeleton loader is displayed, and then to announce that |
||||
content has loaded. The announcements should be localized, and the politeness level should be set |
||||
to `polite`. |
||||
|
||||
- Alternatively, you may wish to render your own `role="status"` element or a custom `aria-live` |
||||
region in the template to accomplish the announcements detailed above. |
||||
|
||||
## Example with Browser Extension |
||||
|
||||
To see a full-page example of what skeleton loading might look like using all three skeleton |
||||
components, check the |
||||
[Popup Layout Skeleton Loading story](?path=/docs/browser-popup-layout--skeleton-loading). |
||||
@ -0,0 +1,50 @@
@@ -0,0 +1,50 @@
|
||||
import { Meta, moduleMetadata, StoryObj } from "@storybook/angular"; |
||||
|
||||
import { formatArgsForCodeSnippet } from "../../../../.storybook/format-args-for-code-snippet"; |
||||
import { SharedModule } from "../shared/shared.module"; |
||||
|
||||
import { SkeletonComponent } from "./skeleton.component"; |
||||
|
||||
export default { |
||||
title: "Component Library/Skeleton/Skeleton", |
||||
component: SkeletonComponent, |
||||
decorators: [ |
||||
moduleMetadata({ |
||||
imports: [SharedModule], |
||||
}), |
||||
], |
||||
args: { |
||||
edgeShape: "box", |
||||
}, |
||||
argTypes: { |
||||
edgeShape: { |
||||
control: { type: "radio" }, |
||||
options: ["box", "circle"], |
||||
}, |
||||
}, |
||||
} as Meta<SkeletonComponent>; |
||||
|
||||
type Story = StoryObj<SkeletonComponent>; |
||||
|
||||
export const BoxEdgeShape: Story = { |
||||
render: (args) => ({ |
||||
props: args, |
||||
template: /*html*/ ` |
||||
<div class="tw-mb-4">Examples of different size shapes with edgeShape={{ edgeShape }}</div> |
||||
<div class="tw-flex tw-flex-row tw-gap-8 tw-items-center"> |
||||
<bit-skeleton ${formatArgsForCodeSnippet<SkeletonComponent>(args)} class="tw-size-32"></bit-skeleton> |
||||
<bit-skeleton ${formatArgsForCodeSnippet<SkeletonComponent>(args)} class="tw-w-40 tw-h-5"></bit-skeleton> |
||||
</div> |
||||
`,
|
||||
}), |
||||
args: { |
||||
edgeShape: "box", |
||||
}, |
||||
}; |
||||
|
||||
export const CircleEdgeShape: Story = { |
||||
...BoxEdgeShape, |
||||
args: { |
||||
edgeShape: "circle", |
||||
}, |
||||
}; |
||||
Loading…
Reference in new issue