diff --git a/.changeset/six-trees-tie.md b/.changeset/six-trees-tie.md new file mode 100644 index 0000000000..3bc859c0b5 --- /dev/null +++ b/.changeset/six-trees-tie.md @@ -0,0 +1,6 @@ +--- +"@digdir/designsystemet-css": patch +"@digdir/designsystemet-react": patch +--- + +Skeleton: Replace Skeleton.Text, Skeleton.Circle and Skeleton.Rectangle with diff --git a/apps/theme/components/Previews/Components/Components.tsx b/apps/theme/components/Previews/Components/Components.tsx index 1c2ea9d5df..41b1fd6dbb 100644 --- a/apps/theme/components/Previews/Components/Components.tsx +++ b/apps/theme/components/Previews/Components/Components.tsx @@ -405,11 +405,11 @@ export const Components = () => {
- - + +
- - + +
diff --git a/packages/css/skeleton.css b/packages/css/skeleton.css index 4699cc62f2..c590cf4a3b 100644 --- a/packages/css/skeleton.css +++ b/packages/css/skeleton.css @@ -2,58 +2,50 @@ --dsc-skeleton-animation-duration: 0.8s; --dsc-skeleton-background: var(--ds-color-neutral-surface-default); + animation: ds-skeleton-opacity-fade var(--dsc-skeleton-animation-duration) linear infinite alternate; + background: var(--dsc-skeleton-background); + border-radius: min(1rem, var(--ds-border-radius-lg)); + box-sizing: border-box; + display: block; height: 1.3em; pointer-events: none; user-select: none; - background-color: var(--dsc-skeleton-background); - animation: ds-skeleton-opacity-fade var(--dsc-skeleton-animation-duration) linear infinite alternate; -} - -.ds-skeleton--circle { - width: 1.3em; - border-radius: var(--ds-border-radius-full); - aspect-ratio: 1 / 1; -} - -.ds-skeleton--rectangle { width: 100%; - border-radius: min(1rem, var(--ds-border-radius-lg)); -} -.ds-skeleton--text { - width: 100%; - height: auto; - transform-origin: 0 55%; - transform: scale(1, 0.6); - border-radius: var(--ds-border-radius-full); -} + &[data-variant='circle'] { + aspect-ratio: 1 / 1; + border-radius: var(--ds-border-radius-full); + width: 1.3em; + } -.ds-skeleton--text:empty::before { - content: '\00a0'; -} + &[data-variant='text'] { + border-radius: var(--ds-border-radius-full); + height: auto; + margin-block: calc(1lh - 1em); -.ds-skeleton--has-children { - width: fit-content; - height: fit-content; - color: transparent !important; -} + &:empty::before { + content: '\00a0'; + } + } -.ds-skeleton--has-children > * { - visibility: hidden; -} + /* When having children, let them define size */ + &:not(:empty) { + width: fit-content; + height: fit-content; + color: transparent !important; + + & > * { + visibility: hidden; + } + } -@media (prefers-reduced-motion: reduce) { - .ds-skeleton { + @media (prefers-reduced-motion: reduce) { --dsc-skeleton-animation-duration: 1.6s; } } @keyframes ds-skeleton-opacity-fade { - 0% { - opacity: 1; - } - - 100% { + to { opacity: 0.4; } } diff --git a/packages/react/src/components/loaders/Skeleton/Circle/Circle.test.tsx b/packages/react/src/components/loaders/Skeleton/Circle/Circle.test.tsx deleted file mode 100644 index d877c49e1d..0000000000 --- a/packages/react/src/components/loaders/Skeleton/Circle/Circle.test.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { render, screen } from '@testing-library/react'; - -import { Skeleton } from '..'; - -beforeAll(() => { - document.getAnimations = () => []; -}); - -describe('Skeleton.Circle', () => { - it('should render skeleton', () => { - render(); - - expect(screen.getByTestId('skeleton-circle')); - }); -}); diff --git a/packages/react/src/components/loaders/Skeleton/Circle/Circle.tsx b/packages/react/src/components/loaders/Skeleton/Circle/Circle.tsx deleted file mode 100644 index fb417114ad..0000000000 --- a/packages/react/src/components/loaders/Skeleton/Circle/Circle.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import cl from 'clsx/lite'; -import type { HTMLAttributes } from 'react'; - -import { useSynchronizedAnimation } from '../../../../utilities'; - -export type CircleProps = { - /** The width of the component */ - width?: string | number; - /** The height of the component */ - height?: string | number; -} & HTMLAttributes; - -/** Skeleton component used for indicating loading elements of circular shape */ -export const Circle = ({ - width, - height, - className, - children, - style, - ...rest -}: CircleProps) => { - const ref = useSynchronizedAnimation( - 'ds-skeleton-opacity-fade', - ); - - return ( -
- {children} -
- ); -}; - -Circle.displayName = 'SkeletonCircle'; diff --git a/packages/react/src/components/loaders/Skeleton/Rectangle/Rectangle.test.tsx b/packages/react/src/components/loaders/Skeleton/Rectangle/Rectangle.test.tsx deleted file mode 100644 index 4448a56891..0000000000 --- a/packages/react/src/components/loaders/Skeleton/Rectangle/Rectangle.test.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { render, screen } from '@testing-library/react'; - -import { Skeleton } from '..'; - -beforeAll(() => { - document.getAnimations = () => []; -}); - -describe('Skeleton.Rectangle', () => { - it('should render skeleton', () => { - render(); - - expect(screen.getByTestId('skeleton-rectangle')); - }); -}); diff --git a/packages/react/src/components/loaders/Skeleton/Rectangle/Rectangle.tsx b/packages/react/src/components/loaders/Skeleton/Rectangle/Rectangle.tsx deleted file mode 100644 index 9afccb73ca..0000000000 --- a/packages/react/src/components/loaders/Skeleton/Rectangle/Rectangle.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import cl from 'clsx/lite'; -import type { HTMLAttributes } from 'react'; - -import { useSynchronizedAnimation } from '../../../../utilities'; - -export type RectangleProps = { - /** The width of the component */ - width?: string | number; - /** The height of the component */ - height?: string | number; -} & HTMLAttributes; - -/** Skeleton component used for indicating loading elements of rectangle shape */ -export const Rectangle = ({ - width, - height, - className, - children, - style, - ...rest -}: RectangleProps) => { - const ref = useSynchronizedAnimation( - 'ds-skeleton-opacity-fade', - ); - - return ( -
- {children} -
- ); -}; - -Rectangle.displayName = 'SkeletonRectangle'; diff --git a/packages/react/src/components/loaders/Skeleton/Skeleton.mdx b/packages/react/src/components/loaders/Skeleton/Skeleton.mdx index 95d4d33d30..3bb9fa4b8b 100644 --- a/packages/react/src/components/loaders/Skeleton/Skeleton.mdx +++ b/packages/react/src/components/loaders/Skeleton/Skeleton.mdx @@ -15,7 +15,7 @@ import * as SkeletonStories from './Skeleton.stories'; -Skeleton kommer i tre ulike varianter: `Skeleton.Circle`, `Skeleton.Rectangle`, og `Skeleton.Text` +Skeleton kommer i tre ulike varianter: `Skeleton variant="rectangle"`, `Skeleton variant="circle"`, og `Skeleton variant="text"` Du kan bygge opp komponenter og seksjoner av siden din ved å bruke disse som byggeklosser. Målet er at resultatet skal etterligne innholdet som lastes. @@ -23,7 +23,7 @@ Du kan bygge opp komponenter og seksjoner av siden din ved å bruke disse som by ### Skalering av komponenten Alle varianter av skeleton har `height` og `width` props, som kan brukes til å manuelt sette størrelser. Du kan oppgi størrelsene i `px`, `%`, eller andre enheter som kan settes direkte på style. -For `Skeleton.Text` holder det ofte å sette kun `width`, da høyden automatisk skaleres etter tekst-størrelsen til `parent`-elementet. +For `Skeleton variant="text"` holder det ofte å sette kun `width`, da høyden automatisk skaleres etter tekst-størrelsen til `parent`-elementet. I de fleste tilfeller er manuell setting av høyde og bredde nok, men du kan også sette andre elementer til å rendres som skeleton, gjennom å bruke propen `asChild`. Dette er hovedsaklig tenkt brukt for typografi komponenter. @@ -35,7 +35,7 @@ Skeleton vil også tilpasse seg etter `children` som du sender inn til komponent ## Text -`Skeleton.Text` skalerer automatisk etter den lokale fontstørrelsen, enten den kommer fra `parent`, `children` , eller fordi typografi-komponenter er satt til Skeletons gjennom `asChild`-propen. -For best mulig resultat anbefaler vi at du bruker flere `Skeleton.Text` komponenter for å representere en blokk med tekst, én for hver linje. +`Skeleton variant="text"` skalerer automatisk etter den lokale fontstørrelsen, enten den kommer fra `parent`, `children` , eller fordi typografi-komponenter er satt til Skeletons gjennom `asChild`-propen. +For best mulig resultat anbefaler vi at du bruker flere `Skeleton variant="text"` komponenter for å representere en blokk med tekst, én for hver linje. diff --git a/packages/react/src/components/loaders/Skeleton/Skeleton.stories.tsx b/packages/react/src/components/loaders/Skeleton/Skeleton.stories.tsx index ae2b8e982a..f7a98cd340 100644 --- a/packages/react/src/components/loaders/Skeleton/Skeleton.stories.tsx +++ b/packages/react/src/components/loaders/Skeleton/Skeleton.stories.tsx @@ -4,11 +4,11 @@ import { Button, Heading, Paragraph } from '../../'; import { Skeleton } from '.'; -type Story = StoryObj; +type Story = StoryObj; export default { title: 'Komponenter/Loaders/Skeleton', - component: Skeleton.Rectangle, + component: Skeleton, } as Meta; export const Preview: Story = { @@ -27,9 +27,9 @@ export const Components: StoryFn = () => { gap: '20px', }} > - - - + + +
); }; @@ -41,7 +41,7 @@ export const UsageExample: StoryFn = () => { width: '400px', }} > - +
= () => { padding: '5px 0 5px 0', }} > - + - En medium tittel + En medium tittel
- - - + + +
); }; export const Children: StoryFn = () => { return ( - <> - - - Her er en tekst som blir sendt inn som barn av en Skeleton.Text. - - - Se hvordan Skeleton da dekker den samlede bredden og høyden til barna. - - - - + + + Her er en tekst som blir sendt inn som barn av en Skeleton. + + + Se hvordan Skeleton da dekker den samlede bredden og høyden til barna. + + + ); }; @@ -82,18 +80,19 @@ export const As: StoryFn = () => { return ( <> - Her er en heading + Her er en heading - - Her er en paragraf-komponent som blir rendret som en Skeleton.Text. - + + Her er en paragraf-komponent som blir rendret som en Skeleton + variant="text". + - + Se hvordan Skeleton da overskriver stylingen til det enkelte elementet. - +
); @@ -111,13 +110,13 @@ export const TextExample: StoryFn = () => {
- Heading + Heading - + - - + +
diff --git a/packages/react/src/components/loaders/Skeleton/Text/Text.test.tsx b/packages/react/src/components/loaders/Skeleton/Skeleton.test.tsx similarity index 63% rename from packages/react/src/components/loaders/Skeleton/Text/Text.test.tsx rename to packages/react/src/components/loaders/Skeleton/Skeleton.test.tsx index 9b89c32e2c..abb30149ea 100644 --- a/packages/react/src/components/loaders/Skeleton/Text/Text.test.tsx +++ b/packages/react/src/components/loaders/Skeleton/Skeleton.test.tsx @@ -1,14 +1,14 @@ import { render, screen } from '@testing-library/react'; -import { Skeleton } from '..'; +import { Skeleton } from '.'; beforeAll(() => { document.getAnimations = () => []; }); -describe('Skeleton.Text', () => { +describe('Skeleton', () => { it('should render skeleton', () => { - render(); + render(); expect(screen.getByTestId('skeleton-text')); }); diff --git a/packages/react/src/components/loaders/Skeleton/Skeleton.tsx b/packages/react/src/components/loaders/Skeleton/Skeleton.tsx new file mode 100644 index 0000000000..ae1eeabaf4 --- /dev/null +++ b/packages/react/src/components/loaders/Skeleton/Skeleton.tsx @@ -0,0 +1,55 @@ +import { useMergeRefs } from '@floating-ui/react'; +import { Slot } from '@radix-ui/react-slot'; +import cl from 'clsx/lite'; +import { type HTMLAttributes, forwardRef } from 'react'; + +import { useSynchronizedAnimation } from '../../../utilities'; + +export type SkeletonProps = { + /** + * Change the default rendered element for the one passed as a child, merging their props and behavior. + * @default false + */ + asChild?: boolean; + /** The width of the component */ + width?: string | number; + /** The height of the component */ + height?: string | number; + /** + * The shape variant + * @default 'rectangle' + * */ + variant?: 'rectangle' | 'circle' | 'text'; +} & HTMLAttributes; + +export const Skeleton = forwardRef( + function Skeleton( + { + asChild, + className, + height, + style, + variant = 'rectangle', + width, + ...rest + }, + ref, + ) { + const Component = asChild ? Slot : 'span'; + const animationRef = useSynchronizedAnimation( + 'ds-skeleton-opacity-fade', + ); + const mergedRefs = useMergeRefs([animationRef, ref]); + + return ( +