Skip to content

Commit

Permalink
Carousel: Autoplay (microsoft#32154)
Browse files Browse the repository at this point in the history
  • Loading branch information
Mitch-At-Work authored Aug 2, 2024
1 parent fcc0d35 commit 759ba6f
Show file tree
Hide file tree
Showing 21 changed files with 403 additions and 85 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "feat: Implement carousel autoplay button and functionality",
"packageName": "@fluentui/react-carousel-preview",
"email": "mifraser@microsoft.com",
"dependentChangeType": "patch"
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,8 @@
"doctrine": "3.0.0",
"dotparser": "1.1.1",
"ejs": "3.1.10",
"embla-carousel": "8.1.8",
"embla-carousel-autoplay": "8.1.8",
"enquirer": "2.3.6",
"enzyme": "3.10.0",
"enzyme-to-json": "3.6.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import type { ForwardRefComponent } from '@fluentui/react-utilities';
import * as React_2 from 'react';
import type { Slot } from '@fluentui/react-utilities';
import type { SlotClassNames } from '@fluentui/react-utilities';
import { ToggleButtonProps } from '@fluentui/react-button';
import { ToggleButtonState } from '@fluentui/react-button';

// @public
export const Carousel: ForwardRefComponent<CarouselProps>;
Expand All @@ -30,15 +32,19 @@ export const CarouselAutoplayButton: ForwardRefComponent<CarouselAutoplayButtonP
export const carouselAutoplayButtonClassNames: SlotClassNames<CarouselAutoplayButtonSlots>;

// @public
export type CarouselAutoplayButtonProps = ComponentProps<CarouselAutoplayButtonSlots> & {};
export type CarouselAutoplayButtonProps = ToggleButtonProps & ComponentProps<CarouselAutoplayButtonSlots> & {
defaultAutoplay?: boolean;
autoplay?: boolean;
onAutoplayChange?: EventHandler<CarouselAutoplayChangeData>;
};

// @public (undocumented)
export type CarouselAutoplayButtonSlots = {
root: Slot<'div'>;
export type CarouselAutoplayButtonSlots = ButtonSlots & {
root: NonNullable<Slot<ARIAButtonSlotProps>>;
};

// @public
export type CarouselAutoplayButtonState = ComponentState<CarouselAutoplayButtonSlots>;
export type CarouselAutoplayButtonState = ToggleButtonState & ComponentState<CarouselAutoplayButtonSlots> & Pick<CarouselAutoplayButtonProps, 'autoplay'>;

// @public
export const CarouselButton: ForwardRefComponent<CarouselButtonProps>;
Expand Down Expand Up @@ -226,7 +232,7 @@ export const renderCarouselSlider_unstable: (state: CarouselSliderState) => JSX.
export function useCarousel_unstable(props: CarouselProps, ref: React_2.Ref<HTMLDivElement>): CarouselState;

// @public
export const useCarouselAutoplayButton_unstable: (props: CarouselAutoplayButtonProps, ref: React_2.Ref<HTMLDivElement>) => CarouselAutoplayButtonState;
export const useCarouselAutoplayButton_unstable: (props: CarouselAutoplayButtonProps, ref: React_2.Ref<ARIAButtonElement>) => CarouselAutoplayButtonState;

// @public
export const useCarouselAutoplayButtonStyles_unstable: (state: CarouselAutoplayButtonState) => CarouselAutoplayButtonState;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,19 @@
"@fluentui/scripts-tasks": "*"
},
"dependencies": {
"@fluentui/react-aria": "^9.13.2",
"@fluentui/react-button": "^9.3.87",
"@fluentui/react-context-selector": "^9.1.65",
"@fluentui/react-icons": "^2.0.245",
"@fluentui/react-jsx-runtime": "^9.0.42",
"@fluentui/react-shared-contexts": "^9.20.0",
"@fluentui/react-tabster": "^9.22.3",
"@fluentui/react-theme": "^9.1.19",
"@fluentui/react-utilities": "^9.18.13",
"@fluentui/react-button": "^9.3.87",
"@fluentui/react-tabster": "^9.22.3",
"@fluentui/react-aria": "^9.13.2",
"@fluentui/react-context-selector": "^9.1.65",
"@fluentui/react-icons": "^2.0.245",
"@griffel/react": "^1.5.22",
"@swc/helpers": "^0.5.1",
"embla-carousel": "8.1.5",
"embla-carousel": "8.1.8",
"embla-carousel-autoplay": "8.1.8",
"use-sync-external-store": "^1.2.0"
},
"peerDependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,11 @@ export function useCarousel_unstable(props: CarouselProps, ref: React.Ref<HTMLDi
const { align = 'center', circular = false, onActiveIndexChange, groupSize = 'auto' } = props;

const { dir } = useFluent();
const { activeIndex, carouselApi, containerRef, subscribeForValues } = useEmblaCarousel({
const { activeIndex, carouselApi, containerRef, subscribeForValues, enableAutoplay } = useEmblaCarousel({
align,
direction: dir,
loop: circular,
slidesToScroll: groupSize,

defaultActiveIndex: props.defaultActiveIndex,
activeIndex: props.activeIndex,
});
Expand Down Expand Up @@ -63,5 +62,6 @@ export function useCarousel_unstable(props: CarouselProps, ref: React.Ref<HTMLDi
selectPageByIndex,

subscribeForValues,
enableAutoplay,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,18 @@ import type { CarouselContextValues } from '../CarouselContext.types';
import type { CarouselState } from './Carousel.types';

export function useCarouselContextValues_unstable(state: CarouselState): CarouselContextValues {
const { activeIndex, selectPageByDirection, selectPageByIndex, subscribeForValues, circular } = state;
const { activeIndex, selectPageByDirection, selectPageByIndex, subscribeForValues, enableAutoplay, circular } = state;

const carousel = React.useMemo(
() => ({
activeIndex,
selectPageByDirection,
selectPageByIndex,
subscribeForValues,
enableAutoplay,
circular,
}),
[activeIndex, selectPageByDirection, selectPageByIndex, subscribeForValues, circular],
[activeIndex, selectPageByDirection, selectPageByIndex, subscribeForValues, enableAutoplay, circular],
);

return { carousel };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,27 @@ import * as React from 'react';
import { render } from '@testing-library/react';
import { isConformant } from '../../testing/isConformant';
import { CarouselAutoplayButton } from './CarouselAutoplayButton';
import { CarouselAutoplayButtonProps } from './CarouselAutoplayButton.types';

describe('CarouselAutoplayButton', () => {
isConformant({
Component: CarouselAutoplayButton,
Component: CarouselAutoplayButton as React.FunctionComponent<CarouselAutoplayButtonProps>,
displayName: 'CarouselAutoplayButton',
testOptions: {
'has-static-classnames': [
{
props: {
icon: 'Test Icon',
},
},
],
},
});

// TODO add more tests here, and create visual regression tests in /apps/vr-tests

it('renders a default state', () => {
const result = render(<CarouselAutoplayButton>Default CarouselAutoplayButton</CarouselAutoplayButton>);
const result = render(<CarouselAutoplayButton />);
expect(result.container).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
@@ -1,17 +1,44 @@
import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities';
import * as React from 'react';
import { ARIAButtonSlotProps } from '@fluentui/react-aria';
import { ButtonSlots, ToggleButtonProps, ToggleButtonState } from '@fluentui/react-button';
import type { ComponentProps, ComponentState, EventData, EventHandler, Slot } from '@fluentui/react-utilities';

export type CarouselAutoplayButtonSlots = {
root: Slot<'div'>;
export type CarouselAutoplayButtonSlots = ButtonSlots & {
root: NonNullable<Slot<ARIAButtonSlotProps>>;
};

export type CarouselAutoplayChangeData = EventData<'click', React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>> & {
/**
* The updated autoplay value.
*/
autoplay: boolean;
};

/**
* CarouselAutoplayButton Props
*/
export type CarouselAutoplayButtonProps = ComponentProps<CarouselAutoplayButtonSlots> & {};
export type CarouselAutoplayButtonProps = ToggleButtonProps &
ComponentProps<CarouselAutoplayButtonSlots> & {
/**
* Controls whether autoplay will initialized as true or false
* Default: true
*/
defaultAutoplay?: boolean;

/**
* User controlled autoplay state
*/
autoplay?: boolean;

/**
* Callback that informs the user when internal autoplay value has changed
*/
onAutoplayChange?: EventHandler<CarouselAutoplayChangeData>;
};

/**
* State used in rendering CarouselAutoplayButton
*/
export type CarouselAutoplayButtonState = ComponentState<CarouselAutoplayButtonSlots>;
// TODO: Remove semicolon from previous line, uncomment next line, and provide union of props to pick from CarouselAutoplayButtonProps.
// & Required<Pick<CarouselAutoplayButtonProps, 'propName'>>
export type CarouselAutoplayButtonState = ToggleButtonState &
ComponentState<CarouselAutoplayButtonSlots> &
Pick<CarouselAutoplayButtonProps, 'autoplay'>;
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,29 @@

exports[`CarouselAutoplayButton renders a default state 1`] = `
<div>
<div
class="fui-CarouselAutoplayButton"
<button
aria-pressed="true"
class="fui-CarouselAutoplayButton fui-Button fui-ToggleButton"
type="button"
>
Default CarouselAutoplayButton
</div>
<span
class="fui-CarouselAutoplayButton__icon fui-Button__icon fui-ToggleButton__icon"
>
<svg
aria-hidden="true"
class=""
fill="currentColor"
height="1em"
viewBox="0 0 20 20"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9 7.5a.5.5 0 0 0-1 0v5a.5.5 0 0 0 1 0v-5Zm3 0a.5.5 0 0 0-1 0v5a.5.5 0 0 0 1 0v-5ZM10 2a8 8 0 1 0 0 16 8 8 0 0 0 0-16Zm-7 8a7 7 0 1 1 14 0 7 7 0 0 1-14 0Z"
fill="currentColor"
/>
</svg>
</span>
</button>
</div>
`;
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@

import { assertSlots } from '@fluentui/react-utilities';
import type { CarouselAutoplayButtonState, CarouselAutoplayButtonSlots } from './CarouselAutoplayButton.types';
import { renderToggleButton_unstable } from '@fluentui/react-button';

/**
* Render the final JSX of CarouselAutoplayButton
*/
export const renderCarouselAutoplayButton_unstable = (state: CarouselAutoplayButtonState) => {
assertSlots<CarouselAutoplayButtonSlots>(state);

// TODO Add additional slots in the appropriate place
return <state.root />;
// We render the underlying react-button with injected carousel functionality
return renderToggleButton_unstable(state);
};

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import * as React from 'react';
import { slot, useControllableState, useIsomorphicLayoutEffect } from '@fluentui/react-utilities';
import type { CarouselAutoplayButtonProps, CarouselAutoplayButtonState } from './CarouselAutoplayButton.types';
import { useToggleButton_unstable } from '@fluentui/react-button';
import { PlayCircleRegular, PauseCircleRegular } from '@fluentui/react-icons';
import { useEventCallback } from '@fluentui/react-utilities';
import { mergeCallbacks } from '@fluentui/react-utilities';
import { useCarouselContext_unstable as useCarouselContext } from '../CarouselContext';
import { ARIAButtonElement } from '@fluentui/react-aria';

/**
* Create the state required to render CarouselAutoplayButton.
*
* The returned state can be modified with hooks such as useCarouselAutoplayButtonStyles_unstable,
* before being passed to renderCarouselAutoplayButton_unstable.
*
* @param props - props from this instance of CarouselAutoplayButton
* @param ref - reference to root HTMLDivElement of CarouselAutoplayButton
*/
export const useCarouselAutoplayButton_unstable = (
props: CarouselAutoplayButtonProps,
ref: React.Ref<ARIAButtonElement>,
): CarouselAutoplayButtonState => {
const { onAutoplayChange } = props;
const [autoplay, setAutoplay] = useControllableState({
state: props.autoplay,
defaultState: props.defaultAutoplay,
initialState: true,
});

const enableAutoplay = useCarouselContext(ctx => ctx.enableAutoplay);

useIsomorphicLayoutEffect(() => {
// Enable/disable autoplay on state change
enableAutoplay(autoplay);
}, [autoplay, enableAutoplay]);

const handleClick = (event: React.MouseEvent<HTMLButtonElement & HTMLAnchorElement>) => {
if (event.isDefaultPrevented()) {
return;
}
const newValue = !autoplay;
setAutoplay(newValue);
onAutoplayChange?.(event, { event, type: 'click', autoplay: newValue });
};

const handleButtonClick = useEventCallback(mergeCallbacks(handleClick, props.onClick));

return {
autoplay,
// We lean on react-button class to handle styling and icon enhancements
...useToggleButton_unstable(
{
icon: slot.optional(props.icon, {
defaultProps: {
children: autoplay ? <PauseCircleRegular /> : <PlayCircleRegular />,
},
renderByDefault: true,
elementType: 'span',
}),
...props,
checked: autoplay,
onClick: handleButtonClick,
},
ref as React.Ref<HTMLButtonElement>,
),
};
};
Loading

0 comments on commit 759ba6f

Please sign in to comment.