-
Notifications
You must be signed in to change notification settings - Fork 2.8k
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: Set controlled index to newly added cards #33177
Carousel: Set controlled index to newly added cards #33177
Conversation
📊 Bundle size report
Unchanged fixtures
|
change/@fluentui-react-carousel-f4012f52-a955-4dfb-929c-e5d93ac1f787.json
Outdated
Show resolved
Hide resolved
82c4ad8
to
8fe0cd3
Compare
packages/react-components/react-carousel/library/src/components/useEmblaCarousel.ts
Outdated
Show resolved
Hide resolved
packages/react-components/react-carousel/library/src/components/useEmblaCarousel.ts
Show resolved
Hide resolved
@Mitch-At-Work do you have a reproduction case for this issue? I tried to build a simple repro: import {
Body1,
Button,
Divider,
makeStyles,
mergeClasses,
Title1,
tokens,
Tooltip,
Toolbar,
ToolbarButton,
} from '@fluentui/react-components';
import {
Carousel,
CarouselAnnouncerFunction,
CarouselButton,
CarouselCard,
CarouselSlider,
} from '@fluentui/react-components';
import * as React from 'react';
const useClasses = makeStyles({
carousel: {
display: 'grid',
gridTemplateColumns: '1fr auto 1fr',
gridTemplateRows: '1fr auto',
gap: '10px',
placeItems: 'center',
// width: '300px',
},
container: {
display: 'flex',
flexDirection: 'column',
gap: '20px',
},
carouselSlider: {
display: 'flex',
overflow: 'hidden',
},
footer: {
display: 'flex',
gap: '10px',
alignSelf: 'center',
width: 'max-content',
border: `${tokens.strokeWidthThin} solid ${tokens.colorNeutralStroke1}`,
borderRadius: tokens.borderRadiusMedium,
boxShadow: tokens.shadow16,
padding: '10px',
},
controls: {
padding: 0,
},
controlButton: {
minWidth: '32px',
},
code: {
display: 'flex',
placeItems: 'center',
padding: '4px 8px',
fontSize: tokens.fontSizeBase200,
lineHeight: tokens.lineHeightBase200,
backgroundColor: tokens.colorNeutralBackground4,
borderRadius: tokens.borderRadiusMedium,
},
card: {
// display: 'block',
// width: '500px',
// maxWidth: '500px',
// flex: 'initial',
},
wireframe: {
backgroundColor: tokens.colorNeutralBackground3,
border: `${tokens.strokeWidthThin} solid ${tokens.colorNeutralStroke1}`,
display: 'flex',
flexDirection: 'column',
gap: '12px',
placeContent: 'center',
padding: '40px',
height: '200px',
position: 'relative',
},
wireframeEven: {
backgroundColor: tokens.colorBrandBackground2,
border: `${tokens.strokeWidthThin} solid ${tokens.colorBrandStroke1}`,
},
wireframeInfo: {
position: 'absolute',
right: '12px',
top: '12px',
backgroundColor: tokens.colorPaletteRedBackground2,
border: `${tokens.strokeWidthThin} dotted ${tokens.colorPaletteRedBorder2}`,
fontSize: tokens.fontSizeBase200,
padding: '4px 8px',
},
});
const getAnnouncement: CarouselAnnouncerFunction = (index: number, totalSlides: number, slideGroupList: number[][]) => {
return `Carousel slide ${index + 1} of ${totalSlides}`;
};
const WireframeContent: React.FC<{
index: number;
}> = props => {
const classes = useClasses();
return (
<div className={mergeClasses(classes.wireframe, props.index % 2 === 0 && classes.wireframeEven)}>
<div className={classes.wireframeInfo}>
<code>index: {props.index}</code>
</div>
<Title1 align="center">Lorem Ipsum</Title1>
<Body1 align="center">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor...</Body1>
</div>
);
};
export const Controlled = () => {
const [activeIndex, setActiveIndex] = React.useState(1);
const classes = useClasses();
const [allCardsVisible, setAllCardsVisible] = React.useState(false);
return (
<div className={classes.container}>
<Carousel
activeIndex={activeIndex}
className={classes.carousel}
groupSize={1}
onActiveIndexChange={(e, data) => setActiveIndex(data.index)}
announcement={getAnnouncement}
>
<Tooltip content="Go To Previous Page" relationship="label">
<CarouselButton navType="prev" aria-label="Previous Carousel Page Button" />
</Tooltip>
<div className={classes.carouselSlider}>
<CarouselSlider>
<CarouselCard aria-label="1 of 5" className={classes.card}>
<WireframeContent index={0} />
</CarouselCard>
<CarouselCard aria-label="2 of 5" className={classes.card}>
<WireframeContent index={1} />
</CarouselCard>
<CarouselCard aria-label="3 of 5" className={classes.card}>
<WireframeContent index={2} />
</CarouselCard>
<CarouselCard aria-label="4 of 5" className={classes.card}>
<WireframeContent index={3} />
</CarouselCard>
<CarouselCard aria-label="5 of 5" className={classes.card}>
<WireframeContent index={4} />
</CarouselCard>
{allCardsVisible && (
<CarouselCard aria-label="5 of 7" className={classes.card}>
<WireframeContent index={5} />
</CarouselCard>
)}
{allCardsVisible && (
<CarouselCard aria-label="5 of 7" className={classes.card}>
<WireframeContent index={6} />
</CarouselCard>
)}
</CarouselSlider>
</div>
<Tooltip content="Go To Next Page" relationship="label">
<CarouselButton navType="next" aria-label="Next Carousel Page Button" />
</Tooltip>
</Carousel>
<div className={classes.footer}>
<code className={classes.code}>{JSON.stringify({ allCardsVisible, activeIndex }, null, 2)}</code>
<Divider vertical />
<Toolbar className={classes.controls}>
{new Array(allCardsVisible ? 7 : 5).fill(null).map((_, index) => (
<ToolbarButton
key={`toolbar-button-${index}`}
aria-label={`Carousel Nav Button ${index} `}
className={classes.controlButton}
appearance="subtle"
disabled={index === activeIndex}
onClick={() => setActiveIndex(index)}
>
{index}
</ToolbarButton>
))}
</Toolbar>
<Button
appearance="primary"
onClick={() => {
setAllCardsVisible(true);
setActiveIndex(6);
}}
>
Add cards
</Button>
</div>
</div>
);
}; And it works on |
Have a test story I was using here: https://github.com/Mitch-At-Work/fluentui/blob/user/mifraser/story-test-new-cards/packages/react-components/react-carousel/stories/src/Carousel/CarouselControlled.stories.tsx I'll double check this is working on master, previously it was not but perhaps latest Embla upgrade is able to handle. Edit: Can confirm this works on master if an animation occurs, i.e. if you are on the 1st index and add a new card it will work. It will not work however if 'jump' is set to true, or if the users is on the last card already, i.e. 5 cards total and user is on 5th card when adding a 6th |
Good catch 👍 Indeed it reproes. As I see, Embla also does What if we will instead move out - const handleReinit = () => {
+ const handleReinit = useEventCallback(() => {
const nodes: HTMLElement[] = emblaApi.current?.slideNodes() ?? [];
const groupIndexList: number[][] = emblaApi.current?.internalEngine().slideRegistry ?? [];
const navItemsCount = groupIndexList.length > 0 ? groupIndexList.length : nodes.length;
const data: CarouselUpdateData = {
navItemsCount,
activeIndex: emblaApi.current?.selectedScrollSnap() ?? 0,
groupIndexList,
slideNodes: nodes,
};
for (const listener of listeners.current) {
listener(data);
}
+ emblaApi.current?.scrollTo(activeIndex);
- };
+ });
const viewportRef: React.RefObject<HTMLDivElement> = React.useRef(null);
const containerRef: React.RefObject<HTMLDivElement> = React.useMemo(() => { |
8fe0cd3
to
33b762e
Compare
Todo before merge: Add E2E cypress tests EDIT: Will land E2E tests separately |
2b0069d
to
a4df727
Compare
Previous Behavior
Controlled index implementation, user adds a CarouselCard and sets index to index of newly added card (simultaneous state update).
As index is not registered at time of state change, state is not updated.
New Behavior
Controlled index implementation, user adds a CarouselCard and sets index to index of newly added card (simultaneous state update).
Engine will update to new card in this scenario only.