diff --git a/.changeset/swift-garlics-wave.md b/.changeset/swift-garlics-wave.md new file mode 100644 index 0000000000..30048d22dc --- /dev/null +++ b/.changeset/swift-garlics-wave.md @@ -0,0 +1,5 @@ +--- +'@leafygreen-ui/skeleton-loader': minor +--- + +Adds `ListSkeleton` component diff --git a/packages/skeleton-loader/package.json b/packages/skeleton-loader/package.json index 7301b42002..d27dfd70a6 100644 --- a/packages/skeleton-loader/package.json +++ b/packages/skeleton-loader/package.json @@ -35,7 +35,8 @@ "@leafygreen-ui/lib": "^13.3.0", "@leafygreen-ui/palette": "^4.0.9", "@leafygreen-ui/tokens": "^2.5.2", - "@leafygreen-ui/typography": "^18.3.0" + "@leafygreen-ui/typography": "^18.3.0", + "lodash": "^4.17.21" }, "peerDependencies": { "@leafygreen-ui/leafygreen-provider": "^3.1.12" diff --git a/packages/skeleton-loader/src/ListSkeleton/ListSkeleton.spec.tsx b/packages/skeleton-loader/src/ListSkeleton/ListSkeleton.spec.tsx new file mode 100644 index 0000000000..6e8c3a5cae --- /dev/null +++ b/packages/skeleton-loader/src/ListSkeleton/ListSkeleton.spec.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { render } from '@testing-library/react'; + +import { ListSkeleton } from '.'; + +describe('packages/skeleton-list', () => { + test('renders', () => { + const { queryByTestId } = render(); + expect(queryByTestId('lg-skeleton-list')).toBeInTheDocument(); + }); + + test('renders `count` items', () => { + const { queryAllByTestId } = render(); + const listItems = queryAllByTestId('lg-skeleton-list_item'); + expect(listItems.length).toBe(3); + }); +}); diff --git a/packages/skeleton-loader/src/ListSkeleton/ListSkeleton.stories.tsx b/packages/skeleton-loader/src/ListSkeleton/ListSkeleton.stories.tsx new file mode 100644 index 0000000000..3d5aa48b03 --- /dev/null +++ b/packages/skeleton-loader/src/ListSkeleton/ListSkeleton.stories.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { StoryMetaType, StoryType } from '@lg-tools/storybook-utils'; + +import { ListSkeleton, type ListSkeletonProps } from '.'; + +const meta: StoryMetaType = { + title: 'Components/SkeletonLoader/List', + component: ListSkeleton, + parameters: { + default: null, + }, + args: { + count: 5, + bulletsOnly: false, + }, +}; + +export default meta; + +export const Basic: StoryType = ( + args: ListSkeletonProps, +) => { + return ( +
+ +
+ ); +}; + +export const BulletsOnly: StoryType = () => { + return ( +
+ +
+ ); +}; diff --git a/packages/skeleton-loader/src/ListSkeleton/ListSkeleton.styles.ts b/packages/skeleton-loader/src/ListSkeleton/ListSkeleton.styles.ts new file mode 100644 index 0000000000..cc3c11c18e --- /dev/null +++ b/packages/skeleton-loader/src/ListSkeleton/ListSkeleton.styles.ts @@ -0,0 +1,40 @@ +import { css } from '@leafygreen-ui/emotion'; +import { spacing } from '@leafygreen-ui/tokens'; + +export const skeletonListWrapperStyles = css` + width: 100%; + padding: 0; + margin: 0; +`; + +export const getSkeletonListItemStyles = ( + index = 0, + bulletsOnly?: boolean, +) => css` + list-style: none; + margin-block: ${spacing[300]}px; + + width: ${getWidth(index, bulletsOnly)}; +`; + +const getWidth = (index = 0, bulletsOnly?: boolean) => { + if (bulletsOnly) { + return spacing[400] + 'px'; + } + + /** + * The first item will take up 100% of the available width. + * Subsequent items will take up 1/4 less space, + * until the item is 50% the available width. + * Then repeat + * + * ---------- + * ------- + * ----- + * ---------- + * ------- + * ... etc + */ + const offset = 25 * (index % 3); + return 100 - offset + '%'; +}; diff --git a/packages/skeleton-loader/src/ListSkeleton/ListSkeleton.tsx b/packages/skeleton-loader/src/ListSkeleton/ListSkeleton.tsx new file mode 100644 index 0000000000..e5d45aa709 --- /dev/null +++ b/packages/skeleton-loader/src/ListSkeleton/ListSkeleton.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import range from 'lodash/range'; + +import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; + +import { Skeleton } from '../Skeleton'; + +import { + getSkeletonListItemStyles, + skeletonListWrapperStyles, +} from './ListSkeleton.styles'; +import { ListSkeletonProps } from './ListSkeleton.types'; + +export function ListSkeleton({ + count = 5, + bulletsOnly, + darkMode, + ...rest +}: ListSkeletonProps) { + return ( + +
    + {range(count).map(i => ( +
  • + +
  • + ))} +
+
+ ); +} + +ListSkeleton.displayName = 'ListSkeleton'; diff --git a/packages/skeleton-loader/src/ListSkeleton/ListSkeleton.types.ts b/packages/skeleton-loader/src/ListSkeleton/ListSkeleton.types.ts new file mode 100644 index 0000000000..c801e6e071 --- /dev/null +++ b/packages/skeleton-loader/src/ListSkeleton/ListSkeleton.types.ts @@ -0,0 +1,15 @@ +import { DarkModeProps, HTMLElementProps } from '@leafygreen-ui/lib'; + +export interface ListSkeletonProps + extends DarkModeProps, + HTMLElementProps<'ul'> { + /** + * Defines the number of skeleton list items to render + */ + count?: number; + /** + * Defines whether to render the full list item, or only a "bullet" skeleton. + * (A "bullet" skeleton renders as just a 16x16 rounded rectangle) + */ + bulletsOnly?: boolean; +} diff --git a/packages/skeleton-loader/src/ListSkeleton/index.ts b/packages/skeleton-loader/src/ListSkeleton/index.ts new file mode 100644 index 0000000000..faf10e90eb --- /dev/null +++ b/packages/skeleton-loader/src/ListSkeleton/index.ts @@ -0,0 +1,2 @@ +export { ListSkeleton } from './ListSkeleton'; +export { ListSkeletonProps } from './ListSkeleton.types'; diff --git a/packages/skeleton-loader/src/SkeletonLoader.story.tsx b/packages/skeleton-loader/src/SkeletonLoader.story.tsx index 26eb49d51e..66cbac6816 100644 --- a/packages/skeleton-loader/src/SkeletonLoader.story.tsx +++ b/packages/skeleton-loader/src/SkeletonLoader.story.tsx @@ -3,7 +3,6 @@ import { storybookArgTypes } from '@lg-tools/storybook-utils'; import { StoryFn } from '@storybook/react'; import { css } from '@leafygreen-ui/emotion'; -import { DarkModeProps } from '@leafygreen-ui/lib'; import { spacing } from '@leafygreen-ui/tokens'; import { Body, InlineCode } from '@leafygreen-ui/typography'; @@ -11,6 +10,7 @@ import { CardSkeleton, CodeSkeleton, FormSkeleton, + ListSkeleton, ParagraphSkeleton, Skeleton, TableSkeleton, @@ -46,44 +46,26 @@ const labelStyles = css` margin-top: ${spacing[5]}px; `; -export const LiveExample: StoryFn = (props: DarkModeProps) => ( +const skeletonComponents = { + Skeleton, + CardSkeleton, + CodeSkeleton, + FormSkeleton, + ListSkeleton, + ParagraphSkeleton, + TableSkeleton, +}; + +export const LiveExample: StoryFn = () => (
-
- - - Skeleton - -
-
- - - ParagraphSkeleton - -
-
- - - CardSkeleton - -
-
- - - FormSkeleton - -
-
- - - TableSkeleton - -
-
- - - CodeSkeleton - -
+ {Object.entries(skeletonComponents).map(([name, SkeletonVariant]) => ( +
+ + + {name} + +
+ ))}
); LiveExample.parameters = { diff --git a/packages/skeleton-loader/src/index.ts b/packages/skeleton-loader/src/index.ts index 9619280092..2f9723f666 100644 --- a/packages/skeleton-loader/src/index.ts +++ b/packages/skeleton-loader/src/index.ts @@ -1,6 +1,7 @@ export { CardSkeleton, type CardSkeletonProps } from './CardSkeleton'; export { CodeSkeleton, type CodeSkeletonProps } from './CodeSkeleton'; export { FormSkeleton, type FormSkeletonProps } from './FormSkeleton'; +export { ListSkeleton, type ListSkeletonProps } from './ListSkeleton'; export { ParagraphSkeleton, type ParagraphSkeletonProps,