Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
Comment thread
ValentinaKozlova marked this conversation as resolved.
"type": "minor",
"comment": "feat(react-skeleton): Add size and shape props to Skeleton component",
"packageName": "@fluentui/react-skeleton",
"email": "v.kozlova13@gmail.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ export interface SkeletonContextValue {
animation?: 'wave' | 'pulse';
// (undocumented)
appearance?: 'opaque' | 'translucent';
// (undocumented)
shape?: 'circle' | 'square' | 'rectangle';
// (undocumented)
size?: SkeletonItemSize;
}

// @public (undocumented)
Expand All @@ -42,11 +46,9 @@ export const SkeletonItem: ForwardRefComponent<SkeletonItemProps>;
export const skeletonItemClassNames: SlotClassNames<SkeletonItemSlots>;

// @public
export type SkeletonItemProps = ComponentProps<SkeletonItemSlots> & {
export type SkeletonItemProps = ComponentProps<SkeletonItemSlots> & Pick<SkeletonProps, 'size' | 'shape'> & {
animation?: 'wave' | 'pulse';
appearance?: 'opaque' | 'translucent';
size?: SkeletonItemSize;
shape?: 'circle' | 'square' | 'rectangle';
};

// @public (undocumented)
Expand All @@ -62,6 +64,8 @@ export type SkeletonProps = Omit<ComponentProps<Partial<SkeletonSlots>>, 'width'
animation?: 'wave' | 'pulse';
appearance?: 'opaque' | 'translucent';
width?: number | string;
size?: SkeletonItemSize;
shape?: 'circle' | 'square' | 'rectangle';
};

// @public (undocumented)
Expand All @@ -70,7 +74,7 @@ export type SkeletonSlots = {
};

// @public
export type SkeletonState = ComponentState<SkeletonSlots> & Required<Pick<SkeletonProps, 'animation' | 'appearance'>>;
export type SkeletonState = ComponentState<SkeletonSlots> & Required<Pick<SkeletonProps, 'animation' | 'appearance'>> & Pick<SkeletonProps, 'size' | 'shape'>;

// @public
export const useSkeleton_unstable: (props: SkeletonProps, ref: React_2.Ref<HTMLElement>) => SkeletonState;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
export type { SkeletonContextValues, SkeletonProps, SkeletonSlots, SkeletonState } from './components/Skeleton/index';
export type {
SkeletonContextValues,
SkeletonItemSize,
SkeletonProps,
SkeletonSlots,
SkeletonState,
} from './components/Skeleton/index';
export {
Skeleton,
renderSkeleton_unstable,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
export type {
SkeletonItemProps,
SkeletonItemSize,
SkeletonItemSlots,
SkeletonItemState,
} from './components/SkeletonItem/index';
export type { SkeletonItemProps, SkeletonItemSlots, SkeletonItemState } from './components/SkeletonItem/index';
export {
SkeletonItem,
renderSkeletonItem_unstable,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import * as React from 'react';
import { render } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';
import { Skeleton } from './Skeleton';
import { useSkeletonItem_unstable } from '../SkeletonItem/useSkeletonItem';
import { SkeletonContextProvider } from '../../contexts/SkeletonContext';
import { isConformant } from '../../testing/isConformant';

describe('Skeleton', () => {
Expand All @@ -23,4 +26,32 @@ describe('Skeleton', () => {
const result = render(<Skeleton />);
expect(result.getByRole('progressbar').getAttribute('aria-busy')).toBeDefined();
});
it('passes size prop to SkeletonItem via context', () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
<SkeletonContextProvider value={{ size: 24 }}>{children}</SkeletonContextProvider>
);
const { result } = renderHook(() => useSkeletonItem_unstable({}, React.createRef()), { wrapper });
expect(result.current.size).toBe(24);
});
it('passes shape prop to SkeletonItem via context', () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
<SkeletonContextProvider value={{ shape: 'circle' }}>{children}</SkeletonContextProvider>
);
const { result } = renderHook(() => useSkeletonItem_unstable({}, React.createRef()), { wrapper });
expect(result.current.shape).toBe('circle');
});
it('allows SkeletonItem to override Skeleton size prop', () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
<SkeletonContextProvider value={{ size: 24 }}>{children}</SkeletonContextProvider>
);
const { result } = renderHook(() => useSkeletonItem_unstable({ size: 48 }, React.createRef()), { wrapper });
expect(result.current.size).toBe(48);
});
it('allows SkeletonItem to override Skeleton shape prop', () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
<SkeletonContextProvider value={{ shape: 'circle' }}>{children}</SkeletonContextProvider>
);
const { result } = renderHook(() => useSkeletonItem_unstable({ shape: 'square' }, React.createRef()), { wrapper });
expect(result.current.shape).toBe('square');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ export type SkeletonSlots = {
root: NonNullable<Slot<'div', 'span'>>;
};

/**
* Sizes for the SkeletonItem
*/
export type SkeletonItemSize = 8 | 12 | 16 | 20 | 24 | 28 | 32 | 36 | 40 | 48 | 56 | 64 | 72 | 96 | 120 | 128;

/**
* Skeleton Props
*/
Expand All @@ -31,6 +36,19 @@ export type SkeletonProps = Omit<ComponentProps<Partial<SkeletonSlots>>, 'width'
* @deprecated Use `className` prop to set width
*/
width?: number | string;

/**
* Sets the size of the SkeletonItems inside the Skeleton in pixels.
* Size is restricted to a limited set of values recommended for most uses (see SkeletonItemSize).
* This value can be overridden by the individual SkeletonItem's `size` prop.
*/
size?: SkeletonItemSize;

/**
* Sets the shape of the SkeletonItems inside the Skeleton.
* This value can be overridden by the individual SkeletonItem's `shape` prop.
*/
shape?: 'circle' | 'square' | 'rectangle';
};

export type SkeletonContextValues = {
Expand All @@ -40,4 +58,6 @@ export type SkeletonContextValues = {
/**
* State used in rendering Skeleton
*/
export type SkeletonState = ComponentState<SkeletonSlots> & Required<Pick<SkeletonProps, 'animation' | 'appearance'>>;
export type SkeletonState = ComponentState<SkeletonSlots> &
Required<Pick<SkeletonProps, 'animation' | 'appearance'>> &
Pick<SkeletonProps, 'size' | 'shape'>;
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
export { Skeleton } from './Skeleton';
export type { SkeletonContextValues, SkeletonProps, SkeletonSlots, SkeletonState } from './Skeleton.types';
export type {
SkeletonContextValues,
SkeletonItemSize,
SkeletonProps,
SkeletonSlots,
SkeletonState,
} from './Skeleton.types';
export { renderSkeleton_unstable } from './renderSkeleton';
export { useSkeleton_unstable } from './useSkeleton';
export { useSkeletonContextValues } from './useSkeletonContextValues';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { useSkeletonContext } from '../../contexts/SkeletonContext';
*/
export const useSkeleton_unstable = (props: SkeletonProps, ref: React.Ref<HTMLElement>): SkeletonState => {
const { animation: contextAnimation, appearance: contextAppearance } = useSkeletonContext();
const { animation = contextAnimation ?? 'wave', appearance = contextAppearance ?? 'opaque' } = props;
const { animation = contextAnimation ?? 'wave', appearance = contextAppearance ?? 'opaque', size, shape } = props;

const root = slot.always(
getIntrinsicElementProps('div', {
Expand All @@ -30,5 +30,5 @@ export const useSkeleton_unstable = (props: SkeletonProps, ref: React.Ref<HTMLEl
}),
{ elementType: 'div' },
);
return { animation, appearance, components: { root: 'div' }, root };
return { animation, appearance, size, shape, components: { root: 'div' }, root };
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ import * as React from 'react';
import type { SkeletonContextValues, SkeletonState } from '../Skeleton';

export const useSkeletonContextValues = (state: SkeletonState): SkeletonContextValues => {
const { animation, appearance } = state;
const { animation, appearance, size, shape } = state;

const skeletonGroup = React.useMemo(
() => ({
animation,
appearance,
size,
shape,
}),
[animation, appearance],
[animation, appearance, size, shape],
);

return { skeletonGroup };
Expand Down
Original file line number Diff line number Diff line change
@@ -1,44 +1,27 @@
import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities';
import type { SkeletonProps } from '../Skeleton/Skeleton.types';

export type SkeletonItemSlots = {
root: Slot<'div', 'span'>;
};

/**
* Sizes for the SkeletonItem
*/
export type SkeletonItemSize = 8 | 12 | 16 | 20 | 24 | 28 | 32 | 36 | 40 | 48 | 56 | 64 | 72 | 96 | 120 | 128;

/**
* SkeletonItem Props
*/
export type SkeletonItemProps = ComponentProps<SkeletonItemSlots> & {
/**
* Sets the animation of the SkeletonItem
* @default wave
*/
animation?: 'wave' | 'pulse';
export type SkeletonItemProps = ComponentProps<SkeletonItemSlots> &
Pick<SkeletonProps, 'size' | 'shape'> & {
/**
* Sets the animation of the SkeletonItem
* @default wave
*/
animation?: 'wave' | 'pulse';

/**
* Sets the appearance of the SkeletonItem
* @default opaque
*/
appearance?: 'opaque' | 'translucent';

/**
* Sets the size of the SkeletonItem in pixels.
* Size is restricted to a limited set of values recommended for most uses(see SkeletonItemSize).
* To set a non-supported size, set `width` and `height` to override the rendered size.
* @default 16
*/
size?: SkeletonItemSize;

/**
* Sets the shape of the SkeletonItem.
* @default rectangle
*/
shape?: 'circle' | 'square' | 'rectangle';
};
/**
* Sets the appearance of the SkeletonItem
* @default opaque
*/
appearance?: 'opaque' | 'translucent';
};

/**
* State used in rendering SkeletonItem
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export { SkeletonItem } from './SkeletonItem';
export type { SkeletonItemProps, SkeletonItemSize, SkeletonItemSlots, SkeletonItemState } from './SkeletonItem.types';
export type { SkeletonItemProps, SkeletonItemSlots, SkeletonItemState } from './SkeletonItem.types';
export { renderSkeletonItem_unstable } from './renderSkeletonItem';
export { useSkeletonItem_unstable } from './useSkeletonItem';
export { skeletonItemClassNames, useSkeletonItemStyles_unstable } from './useSkeletonItemStyles.styles';
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,17 @@ import type { SkeletonItemProps, SkeletonItemState } from './SkeletonItem.types'
* @param ref - reference to root HTMLElement of SkeletonItem
*/
export const useSkeletonItem_unstable = (props: SkeletonItemProps, ref: React.Ref<HTMLElement>): SkeletonItemState => {
const { animation: contextAnimation, appearance: contextAppearance } = useSkeletonContext();
const {
animation: contextAnimation,
appearance: contextAppearance,
size: contextSize,
shape: contextShape,
} = useSkeletonContext();
const {
animation = contextAnimation ?? 'wave',
appearance = contextAppearance ?? 'opaque',
size = 16,
shape = 'rectangle',
size = contextSize ?? 16,
shape = contextShape ?? 'rectangle',
} = props;

const root = slot.always(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
'use client';

import * as React from 'react';
import type { SkeletonItemSize } from '../components/Skeleton/Skeleton.types';

const SkeletonContext = React.createContext<SkeletonContextValue | undefined>(undefined);

export interface SkeletonContextValue {
animation?: 'wave' | 'pulse';
appearance?: 'opaque' | 'translucent';
size?: SkeletonItemSize;
shape?: 'circle' | 'square' | 'rectangle';
}

const skeletonContextDefaultValue: SkeletonContextValue = {};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,24 +29,24 @@ export const Row = (props: Partial<SkeletonProps>): JSXElement => {
const styles = useStyles();
return (
<div className={styles.invertedWrapper}>
<Skeleton {...props} aria-label="Loading Content">
<Skeleton {...props} size={20} aria-label="Loading Content">
<div className={styles.firstRow}>
<SkeletonItem shape="circle" size={24} />
<SkeletonItem shape="rectangle" size={16} />
<SkeletonItem shape="rectangle" />
</div>
<div className={styles.secondThirdRow}>
<SkeletonItem shape="circle" size={24} />
<SkeletonItem size={16} />
<SkeletonItem size={16} />
<SkeletonItem size={16} />
<SkeletonItem size={16} />
<SkeletonItem />
<SkeletonItem />
<SkeletonItem />
<SkeletonItem />
</div>
<div className={styles.secondThirdRow}>
<SkeletonItem shape="square" size={24} />
<SkeletonItem size={16} />
<SkeletonItem size={16} />
<SkeletonItem size={16} />
<SkeletonItem size={16} />
<SkeletonItem />
<SkeletonItem />
<SkeletonItem />
<SkeletonItem />
</div>
</Skeleton>
</div>
Expand Down