Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Carousel: Fade and Carousel Modal example (FRE) #33000

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
Mitch-At-Work marked this conversation as resolved.
Show resolved Hide resolved
Mitch-At-Work marked this conversation as resolved.
Show resolved Hide resolved
Mitch-At-Work marked this conversation as resolved.
Show resolved Hide resolved
"type": "minor",
"comment": "feat: Add fade motion to carousel as an optional prop",
"packageName": "@fluentui/react-carousel",
"email": "mifraser@microsoft.com",
"dependentChangeType": "patch"
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@
"ejs": "3.1.10",
"embla-carousel": "8.3.0",
"embla-carousel-autoplay": "8.3.0",
"embla-carousel-fade": "8.3.0",
"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 @@ -212,6 +212,7 @@ export type CarouselProps = ComponentProps<CarouselSlots> & {
groupSize?: number | 'auto';
draggable?: boolean;
whitespace?: boolean;
motion?: CarouselMotion;
announcement?: CarouselAnnouncerFunction;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@
"@griffel/react": "^1.5.22",
"@swc/helpers": "^0.5.1",
"embla-carousel": "^8.3.0",
"embla-carousel-autoplay": "^8.3.0"
"embla-carousel-autoplay": "^8.3.0",
"embla-carousel-fade": "^8.3.0"
},
"peerDependencies": {
"@types/react": ">=16.14.0 <19.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ export type CarouselSlots = {
*/
export type CarouselAnnouncerFunction = (index: number, totalSlides: number, slideGroupList: number[][]) => string;

/**
* List of integrated motion types
*/
export type CarouselMotion = 'slide' | 'fade';

/**
* Carousel Props
*/
Expand Down Expand Up @@ -57,6 +62,12 @@ export type CarouselProps = ComponentProps<CarouselSlots> & {
*/
whitespace?: boolean;

/**
* Sets motion to fade in/out style with minimal movement
* Defaults: false
*/
motion?: CarouselMotion;

/**
* Localizes the string used to announce carousel page changes
* Defaults to: undefined
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export function useCarousel_unstable(props: CarouselProps, ref: React.Ref<HTMLDi
draggable = false,
whitespace = false,
announcement,
motion = 'slide',
} = props;

const { dir } = useFluent();
Expand All @@ -46,6 +47,7 @@ export function useCarousel_unstable(props: CarouselProps, ref: React.Ref<HTMLDi
activeIndex: props.activeIndex,
watchDrag: draggable,
containScroll: whitespace ? false : 'keepSnaps',
motion,
});

const selectPageByElement: CarouselContextValue['selectPageByElement'] = useEventCallback((event, element, jump) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { useControllableState } from '@fluentui/react-utilities';
import EmblaCarousel, { type EmblaCarouselType, type EmblaOptionsType } from 'embla-carousel';
import EmblaCarousel, { EmblaPluginType, type EmblaCarouselType, type EmblaOptionsType } from 'embla-carousel';
import * as React from 'react';

import { carouselCardClassNames } from './CarouselCard/useCarouselCardStyles.styles';
import { carouselSliderClassNames } from './CarouselSlider/useCarouselSliderStyles.styles';
import { CarouselUpdateData, CarouselVisibilityEventDetail } from '../Carousel';
import { CarouselMotion, CarouselUpdateData, CarouselVisibilityEventDetail } from '../Carousel';
import Autoplay from 'embla-carousel-autoplay';
import Fade from 'embla-carousel-fade';

const sliderClassname = `.${carouselSliderClassNames.root}`;

Expand Down Expand Up @@ -38,9 +39,10 @@ export function useEmblaCarousel(
options: Pick<EmblaOptionsType, 'align' | 'direction' | 'loop' | 'slidesToScroll' | 'watchDrag' | 'containScroll'> & {
defaultActiveIndex: number | undefined;
activeIndex: number | undefined;
motion?: CarouselMotion;
},
) {
const { align, direction, loop, slidesToScroll, watchDrag, containScroll } = options;
const { align, direction, loop, slidesToScroll, watchDrag, containScroll, motion } = options;
const [activeIndex, setActiveIndex] = useControllableState({
defaultState: options.defaultActiveIndex,
state: options.activeIndex,
Expand Down Expand Up @@ -79,6 +81,27 @@ export function useEmblaCarousel(
[resetAutoplay],
);

const getPlugins = React.useCallback(() => {
const plugins: EmblaPluginType[] = [
Autoplay({
playOnInit: autoplayRef.current,
stopOnInteraction: !autoplayRef.current,
stopOnMouseEnter: true,
stopOnFocusIn: true,
rootNode: (emblaRoot: HTMLElement) => {
return emblaRoot.querySelector(sliderClassname) ?? emblaRoot;
},
}),
];

// Optionally add Fade plugin
if (motion === 'fade') {
plugins.push(Fade());
}

return plugins;
}, [motion]);

// Listeners contains callbacks for UI elements that may require state update based on embla changes
const listeners = React.useRef(new Set<(data: CarouselUpdateData) => void>());
const subscribeForValues = React.useCallback((listener: (data: CarouselUpdateData) => void) => {
Expand Down Expand Up @@ -132,6 +155,8 @@ export function useEmblaCarousel(
});
};

const plugins = getPlugins();

return {
set current(newElement: HTMLDivElement | null) {
if (currentElement) {
Expand All @@ -149,17 +174,7 @@ export function useEmblaCarousel(
...DEFAULT_EMBLA_OPTIONS,
...emblaOptions.current,
},
[
Autoplay({
playOnInit: autoplayRef.current,
stopOnInteraction: !autoplayRef.current,
stopOnMouseEnter: true,
stopOnFocusIn: true,
rootNode: (emblaRoot: HTMLElement) => {
return emblaRoot.querySelector(sliderClassname) ?? emblaRoot;
},
}),
],
plugins,
);

emblaApi.current?.on('reInit', handleReinit);
Expand All @@ -168,7 +183,7 @@ export function useEmblaCarousel(
}
},
};
}, [setActiveIndex]);
}, [getPlugins, setActiveIndex]);

const carouselApi = React.useMemo(
() => ({
Expand Down Expand Up @@ -209,25 +224,17 @@ export function useEmblaCarousel(
}, [activeIndex]);

React.useEffect(() => {
const plugins = getPlugins();

emblaOptions.current = { align, direction, loop, slidesToScroll, watchDrag, containScroll };
emblaApi.current?.reInit(
{
...DEFAULT_EMBLA_OPTIONS,
...emblaOptions.current,
},
[
Autoplay({
playOnInit: autoplayRef.current,
stopOnInteraction: !autoplayRef.current,
stopOnMouseEnter: true,
stopOnFocusIn: true,
rootNode: (emblaRoot: HTMLElement) => {
return emblaRoot.querySelector(sliderClassname) ?? emblaRoot;
},
}),
],
plugins,
);
}, [align, direction, loop, slidesToScroll, watchDrag, containScroll]);
}, [align, direction, loop, slidesToScroll, watchDrag, containScroll, getPlugins]);

return {
activeIndex,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ const WireframeContent: React.FC<{
};

export const Controlled = () => {
const [activeIndex, setActiveIndex] = React.useState(0);
const [activeIndex, setActiveIndex] = React.useState(1);
const classes = useClasses();

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import * as React from 'react';
import {
Button,
Carousel,
CarouselAnnouncerFunction,
CarouselCard,
CarouselNav,
CarouselNavButton,
CarouselSlider,
Dialog,
DialogSurface,
DialogTrigger,
Image,
makeStyles,
shorthands,
tokens,
typographyStyles,
} from '@fluentui/react-components';
import { useEffect } from 'react';

const useStyles = makeStyles({
surface: {
padding: 0,
...shorthands.border('none'),
overflow: 'hidden',
},
carousel: { padding: 0 },
card: {},
footer: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
width: 'auto',
padding: `${tokens.spacingVerticalS} ${tokens.spacingVerticalXXL} ${tokens.spacingVerticalXXL} ${tokens.spacingVerticalXXL}`,
},
header: {
display: 'block',
// We use margin instead of padding to avoid messing with the focus indicator in the header
margin: `${tokens.spacingVerticalXXL} ${tokens.spacingVerticalXXL} ${tokens.spacingVerticalS} ${tokens.spacingVerticalXXL}`,
...typographyStyles.subtitle1,
},
text: {
display: 'block',
padding: `${tokens.spacingVerticalS} ${tokens.spacingVerticalXXL}`,
...typographyStyles.body1,
},
});

const PAGES = [
{
id: 'Copilot-page-1',
alt: 'Copilot logo',
imgSrc: 'https://fabricweb.azureedge.net/fabric-website/assets/images/swatch-picker/sea-full-img.jpg',
header: 'Discover Copilot, a whole new way to work',
text: 'Explore new ways to work smarter and faster using the power of AI. Copilot in [Word] can help you [get started from scratch], [work from an existing file], [get actionable insights about documents], and more.',
},
{
id: 'Copilot-page-2',
alt: 'Copilot logo 2',
imgSrc: 'https://fabricweb.azureedge.net/fabric-website/assets/images/swatch-picker/bridge-full-img.jpg',
header: 'Use your own judgment',
text: 'Copilot can make mistakes so remember to verify the results. To help improve the experience, please share your feedback with us.',
},
];

const getAnnouncement: CarouselAnnouncerFunction = (index: number, totalSlides: number, slideGroupList: number[][]) => {
return `Carousel slide ${index + 1} of ${totalSlides}, ${PAGES[index].header}`;
};

export const CarouselFirstRunExperience = () => {
const styles = useStyles();
const [activeIndex, setActiveIndex] = React.useState(0);
const [open, setModalOpen] = React.useState(false);
const totalPages = PAGES.length;

const setPage = (page: number) => {
if (page < 0 || page >= totalPages) {
setModalOpen(false);
return;
}
setActiveIndex(page);
};
// NearButton and FarButton are function components that handle navigation and focus management
const NearButton = () => (
<Button onClick={() => setPage(activeIndex - 1)}>{activeIndex <= 0 ? 'Not Now' : 'Previous'}</Button>
);

const FarButton = () => (
<Button appearance="primary" onClick={() => setPage(activeIndex + 1)}>
{activeIndex === totalPages - 1 ? 'Try Copilot' : 'Next'}
</Button>
);

useEffect(() => {
// Reset or initialize page on open if nessecary
if (open) {
setActiveIndex(0);
}
}, [open]);

return (
<Dialog open={open} onOpenChange={(e, data) => setModalOpen(data.open)}>
<DialogTrigger>
<Button>Open Dialog</Button>
</DialogTrigger>
<DialogSurface className={styles.surface}>
<Carousel
className={styles.carousel}
groupSize={1}
circular
announcement={getAnnouncement}
activeIndex={activeIndex}
motion="fade"
onActiveIndexChange={(e, data) => setActiveIndex(data.index)}
>
<CarouselSlider>
{PAGES.map((page, index) => (
<CarouselCard className={styles.card} key={page.id}>
<Image src={page.imgSrc} width={600} height={324} alt={page.imgSrc} />
<h1 tabIndex={-1} className={styles.header}>
{page.header}
</h1>
<span className={styles.text}>{page.text}</span>
</CarouselCard>
))}
</CarouselSlider>
<div className={styles.footer}>
{NearButton()}
<CarouselNav appearance="brand">
{index => <CarouselNavButton aria-label={`Carousel Nav Button ${index}`} />}
</CarouselNav>
{FarButton()}
</div>
</Carousel>
</DialogSurface>
</Dialog>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export { Controlled } from './CarouselControlled.stories';
export { ImageSlideshow } from './CarouselImageBox.stories';
export { AlignmentAndWhitespace } from './CarouselActionCards.stories';
export { Autoplay } from './CarouselAutoplay.stories';
export { CarouselFirstRunExperience } from './CarouselFirstRunExperience.stories';

export default {
title: 'Components/Carousel',
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -10283,6 +10283,11 @@ embla-carousel-autoplay@8.3.0, embla-carousel-autoplay@^8.3.0:
resolved "https://registry.yarnpkg.com/embla-carousel-autoplay/-/embla-carousel-autoplay-8.3.0.tgz#2878e7c67c7c6f5c4cb0a06a8cb06e53d8f32f2f"
integrity sha512-h7DFJLf9uQD+XDxr1NwA3/oFIjsnj/iED2RjET5u6/svMec46IbF1CYPhmB5Q/1Fc0WkcvhPpsEsrtVXQLxNzA==

embla-carousel-fade@8.3.0, embla-carousel-fade@^8.3.0:
version "8.3.0"
resolved "https://registry.yarnpkg.com/embla-carousel-fade/-/embla-carousel-fade-8.3.0.tgz#44be8f2c00a771828bd02078fed26bce005d1f7a"
integrity sha512-m0NbkNPTAr6ghINhJrCnI0BRgWWoGRIGUd1tYCxTK00Exm9+kzOVL5KBPkrMVzXRXHe6TRgkmsCkb/7npfwRFQ==

embla-carousel@8.3.0, embla-carousel@^8.3.0:
version "8.3.0"
resolved "https://registry.yarnpkg.com/embla-carousel/-/embla-carousel-8.3.0.tgz#dc27c63c405aa98320cb36893e4be2fbdc787ee1"
Expand Down
Loading