Skeleton: Abstract out attach/animation logic (#79309)
* apply styles globally to skeleton * use abstraction everywhere * just use withSkeleton * add comment * update docs * use it in News as well * rename withSkeleton to attachSkeleton * move to @grafana/ui/src/unstable * rename skeletonProps to rootProps
This commit is contained in:
@@ -7,6 +7,7 @@ import { GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
import { useStyles2 } from '../../themes/ThemeContext';
|
||||
import { IconName } from '../../types';
|
||||
import { SkeletonComponent, attachSkeleton } from '../../utils/skeleton';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
import { Tooltip } from '../Tooltip/Tooltip';
|
||||
|
||||
@@ -38,19 +39,13 @@ const BadgeComponent = React.memo<BadgeProps>(({ icon, color, text, tooltip, cla
|
||||
});
|
||||
BadgeComponent.displayName = 'Badge';
|
||||
|
||||
const BadgeSkeleton = () => {
|
||||
const BadgeSkeleton: SkeletonComponent = ({ rootProps }) => {
|
||||
const styles = useStyles2(getSkeletonStyles);
|
||||
|
||||
return <Skeleton width={60} height={22} containerClassName={styles.container} />;
|
||||
return <Skeleton width={60} height={22} containerClassName={styles.container} {...rootProps} />;
|
||||
};
|
||||
|
||||
interface BadgeWithSkeleton extends React.NamedExoticComponent<BadgeProps> {
|
||||
Skeleton: typeof BadgeSkeleton;
|
||||
}
|
||||
|
||||
export const Badge: BadgeWithSkeleton = Object.assign(BadgeComponent, { Skeleton: BadgeSkeleton });
|
||||
|
||||
Badge.Skeleton = BadgeSkeleton;
|
||||
export const Badge = attachSkeleton(BadgeComponent, BadgeSkeleton);
|
||||
|
||||
const getSkeletonStyles = () => ({
|
||||
container: css({
|
||||
|
||||
@@ -7,6 +7,7 @@ import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { useStyles2, useTheme2 } from '../../themes';
|
||||
import { IconName } from '../../types/icon';
|
||||
import { getTagColor, getTagColorsFromName } from '../../utils';
|
||||
import { SkeletonComponent, attachSkeleton } from '../../utils/skeleton';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
|
||||
/**
|
||||
@@ -50,18 +51,12 @@ const TagComponent = forwardRef<HTMLElement, Props>(({ name, onClick, icon, clas
|
||||
});
|
||||
TagComponent.displayName = 'Tag';
|
||||
|
||||
const TagSkeleton = () => {
|
||||
const TagSkeleton: SkeletonComponent = ({ rootProps }) => {
|
||||
const styles = useStyles2(getSkeletonStyles);
|
||||
return <Skeleton width={60} height={22} containerClassName={styles.container} />;
|
||||
return <Skeleton width={60} height={22} containerClassName={styles.container} {...rootProps} />;
|
||||
};
|
||||
|
||||
interface TagWithSkeleton extends React.ForwardRefExoticComponent<Props & React.RefAttributes<HTMLElement>> {
|
||||
Skeleton: typeof TagSkeleton;
|
||||
}
|
||||
|
||||
export const Tag: TagWithSkeleton = Object.assign(TagComponent, {
|
||||
Skeleton: TagSkeleton,
|
||||
});
|
||||
export const Tag = attachSkeleton(TagComponent, TagSkeleton);
|
||||
|
||||
const getSkeletonStyles = () => ({
|
||||
container: css({
|
||||
|
||||
@@ -5,6 +5,7 @@ import { GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
import { useStyles2, useTheme2 } from '../../themes';
|
||||
import { IconName } from '../../types/icon';
|
||||
import { SkeletonComponent, attachSkeleton } from '../../utils/skeleton';
|
||||
|
||||
import { OnTagClick, Tag } from './Tag';
|
||||
|
||||
@@ -56,23 +57,17 @@ const TagListComponent = memo(
|
||||
);
|
||||
TagListComponent.displayName = 'TagList';
|
||||
|
||||
const TagListSkeleton = () => {
|
||||
const TagListSkeleton: SkeletonComponent = ({ rootProps }) => {
|
||||
const styles = useStyles2(getSkeletonStyles);
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.container} {...rootProps}>
|
||||
<Tag.Skeleton />
|
||||
<Tag.Skeleton />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface TagListWithSkeleton extends React.ForwardRefExoticComponent<Props & React.RefAttributes<HTMLUListElement>> {
|
||||
Skeleton: typeof TagListSkeleton;
|
||||
}
|
||||
|
||||
export const TagList: TagListWithSkeleton = Object.assign(TagListComponent, {
|
||||
Skeleton: TagListSkeleton,
|
||||
});
|
||||
export const TagList = attachSkeleton(TagListComponent, TagListSkeleton);
|
||||
|
||||
const getSkeletonStyles = (theme: GrafanaTheme2) => ({
|
||||
container: css({
|
||||
|
||||
@@ -10,6 +10,7 @@ import { getExtraStyles } from './extra';
|
||||
import { getFormElementStyles } from './forms';
|
||||
import { getMarkdownStyles } from './markdownStyles';
|
||||
import { getPageStyles } from './page';
|
||||
import { getSkeletonStyles } from './skeletonStyles';
|
||||
|
||||
/** @internal */
|
||||
export function GlobalStyles() {
|
||||
@@ -25,6 +26,7 @@ export function GlobalStyles() {
|
||||
getCardStyles(theme),
|
||||
getAgularPanelStyles(theme),
|
||||
getMarkdownStyles(theme),
|
||||
getSkeletonStyles(theme),
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
import { skeletonAnimation } from '../../utils/skeleton';
|
||||
|
||||
export const getSkeletonStyles = (theme: GrafanaTheme2) => {
|
||||
return css({
|
||||
'.react-loading-skeleton': skeletonAnimation,
|
||||
});
|
||||
};
|
||||
@@ -8,3 +8,5 @@
|
||||
* Once mature, they will be moved to the main export, be available to plugins, and
|
||||
* be subject to the standard policies
|
||||
*/
|
||||
|
||||
export * from './utils/skeleton';
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { keyframes } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
const fadeIn = keyframes({
|
||||
'0%': {
|
||||
opacity: 0,
|
||||
},
|
||||
'100%': {
|
||||
opacity: 1,
|
||||
},
|
||||
});
|
||||
|
||||
export const skeletonAnimation = {
|
||||
animationName: fadeIn,
|
||||
animationDelay: '100ms',
|
||||
animationTimingFunction: 'ease-in',
|
||||
animationDuration: '100ms',
|
||||
animationFillMode: 'backwards',
|
||||
};
|
||||
|
||||
interface SkeletonProps {
|
||||
/**
|
||||
* Spread these props at the root of your skeleton to handle animation logic
|
||||
*/
|
||||
rootProps: {
|
||||
style: React.CSSProperties;
|
||||
};
|
||||
}
|
||||
|
||||
export type SkeletonComponent<P = {}> = React.ComponentType<P & SkeletonProps>;
|
||||
|
||||
/**
|
||||
* Use this to attach a skeleton as a static property on the component.
|
||||
* e.g. if you render a component with `<Component />`, you can render the skeleton with `<Component.Skeleton />`.
|
||||
* @param Component A functional or class component
|
||||
* @param Skeleton A functional or class skeleton component
|
||||
* @returns A wrapped component with a static skeleton property
|
||||
*/
|
||||
export const attachSkeleton = <C extends object, P>(Component: C, Skeleton: SkeletonComponent<P>) => {
|
||||
const skeletonWrapper = (props: P) => {
|
||||
return (
|
||||
<Skeleton
|
||||
{...props}
|
||||
rootProps={{
|
||||
style: skeletonAnimation,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
return Object.assign(Component, { Skeleton: skeletonWrapper });
|
||||
};
|
||||
Reference in New Issue
Block a user