-
Notifications
You must be signed in to change notification settings - Fork 1.5k
[CL-427] Add skeleton loading components to the CL #16728
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
Changes from 2 commits
e323cb6
3559835
1e93f4f
981770f
06f5a9d
8f59113
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| export * from "./skeleton.component"; | ||
| export * from "./skeleton-text.component"; | ||
| export * from "./skeleton-group.component"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 {} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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> | ||
| `, | ||
| }), | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| <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 | ||
| edgeShape="circle" | ||
| class="tw-h-3" | ||
| [ngClass]="{ | ||
| 'tw-w-full': first || !last, | ||
| 'tw-w-1/3': !first && last, | ||
| }" | ||
| ></bit-skeleton> | ||
| } | ||
| </div> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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.from(Array(this.lines()).keys())); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import { Meta, moduleMetadata, StoryObj } from "@storybook/angular"; | ||
|
|
||
| import { SharedModule } from "../shared/shared.module"; | ||
|
|
||
| import { SkeletonTextComponent } from "./skeleton-text.component"; | ||
|
|
||
| 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 [lines]="lines"></bit-skeleton-text> | ||
| `, | ||
| }), | ||
| }; | ||
|
|
||
| export const TextMultiline: Story = { | ||
| render: (args) => ({ | ||
| props: args, | ||
| template: /*html*/ ` | ||
| <bit-skeleton-text [lines]="lines"></bit-skeleton-text> | ||
| `, | ||
| }), | ||
| args: { | ||
| lines: 5, | ||
| }, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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"); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does it make more sense for this to just be 'shape'? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe! I felt like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I guess the consumer could pass There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought about doing width/height inputs but it felt kind of silly when it's super simple with tailwind already, like I'd just be re-implementing classes. The designs have non-circle shapes with rounded corners so that is why I didn't want to imply the object itself being a circle There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, got ya. In that case, I suppose we can't assume everything will be circular. re width/height inputs: I'm not sure the assumption that it's easy to do with tailwind is necessarily true for everyone. We find it easy because we use tailwind every day but, some folks may not be as familiar. Explicit inputs might feel easier for them. IDK which is 'better' per se though There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Imo since tailwind is the standard throughout the apps, as long as it's clearly documented in Storybook how to use tailwind to apply width/height, it shouldn't be too confusing. (I think I need to work on the docs more.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @BryanCunningham I added more to the docs, lmk if you think it's more clear There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks good! |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| 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. The shape of | ||
| the edges is configurable to be either squared or round. Use for non-text shapes. | ||
|
|
||
| <Canvas of={skeletonStories.Square} /> | ||
| <Canvas of={skeletonStories.Circle} /> | ||
|
|
||
| ### Skeleton Text | ||
|
|
||
| Specific skeleton component used to represent lines of text. The number of lines is configurable. | ||
|
|
||
| <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. It | ||
| uses start, default, and end slots to allow any skeleton content to be rendered. | ||
|
|
||
| <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). |
Uh oh!
There was an error while loading. Please reload this page.