diff --git a/change/@fluentui-react-carousel-ee0da201-e1da-42ff-9146-a432aa8523b2.json b/change/@fluentui-react-carousel-ee0da201-e1da-42ff-9146-a432aa8523b2.json new file mode 100644 index 0000000000000..fca9d8e1b7a3c --- /dev/null +++ b/change/@fluentui-react-carousel-ee0da201-e1da-42ff-9146-a432aa8523b2.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: Add CarouselViewport to correctly define CarouselSlider within a static container", + "packageName": "@fluentui/react-carousel", + "email": "mifraser@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-components-12ae5402-e28e-4685-a1ca-aa443bb9ede7.json b/change/@fluentui-react-components-12ae5402-e28e-4685-a1ca-aa443bb9ede7.json new file mode 100644 index 0000000000000..748d5309a0a20 --- /dev/null +++ b/change/@fluentui-react-components-12ae5402-e28e-4685-a1ca-aa443bb9ede7.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: Add CarouselViewport and deprecate CarouselSlider", + "packageName": "@fluentui/react-components", + "email": "mifraser@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-provider-f9a27417-2007-47fc-a267-cb9ac9fc5d09.json b/change/@fluentui-react-provider-f9a27417-2007-47fc-a267-cb9ac9fc5d09.json new file mode 100644 index 0000000000000..eb38e8defa132 --- /dev/null +++ b/change/@fluentui-react-provider-f9a27417-2007-47fc-a267-cb9ac9fc5d09.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: Add Carousel CustomStyleHook definitions", + "packageName": "@fluentui/react-provider", + "email": "mifraser@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-shared-contexts-e1d81579-08a1-4129-891e-aeb36131f5be.json b/change/@fluentui-react-shared-contexts-e1d81579-08a1-4129-891e-aeb36131f5be.json new file mode 100644 index 0000000000000..27b2c2ea718bf --- /dev/null +++ b/change/@fluentui-react-shared-contexts-e1d81579-08a1-4129-891e-aeb36131f5be.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: Add custom style hooks for Carousel components", + "packageName": "@fluentui/react-shared-contexts", + "email": "mifraser@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-carousel/library/etc/react-carousel.api.md b/packages/react-components/react-carousel/library/etc/react-carousel.api.md index d16941cb8f6ad..981198ec2ee4f 100644 --- a/packages/react-components/react-carousel/library/etc/react-carousel.api.md +++ b/packages/react-components/react-carousel/library/etc/react-carousel.api.md @@ -106,6 +106,7 @@ export type CarouselContextValue = { enableAutoplay: (autoplay: boolean) => void; resetAutoplay: () => void; containerRef?: React_2.RefObject; + viewportRef?: React_2.RefObject; }; // @public @@ -246,6 +247,23 @@ export type CarouselSlots = { // @public export type CarouselState = ComponentState & CarouselContextValue; +// @public +export const CarouselViewport: ForwardRefComponent; + +// @public (undocumented) +export const carouselViewportClassNames: SlotClassNames; + +// @public +export type CarouselViewportProps = ComponentProps; + +// @public (undocumented) +export type CarouselViewportSlots = { + root: Slot<'div'>; +}; + +// @public +export type CarouselViewportState = ComponentState> & CarouselSliderContextValue; + // @public (undocumented) export type NavButtonRenderFunction = (index: number) => React_2.ReactNode; @@ -276,6 +294,9 @@ export const renderCarouselNavImageButton_unstable: (state: CarouselNavImageButt // @public export const renderCarouselSlider_unstable: (state: CarouselSliderState, contextValues: CarouselSliderContextValues) => JSX.Element; +// @public +export const renderCarouselViewport_unstable: (state: CarouselViewportState, contextValues: CarouselSliderContextValues) => JSX.Element; + // @public export function useCarousel_unstable(props: CarouselProps, ref: React_2.Ref): CarouselState; @@ -333,6 +354,12 @@ export const useCarouselSliderStyles_unstable: (state: CarouselSliderState) => C // @public export const useCarouselStyles_unstable: (state: CarouselState) => CarouselState; +// @public +export const useCarouselViewport_unstable: (props: CarouselViewportProps, ref: React_2.Ref) => CarouselViewportState; + +// @public +export const useCarouselViewportStyles_unstable: (state: CarouselViewportState) => CarouselViewportState; + // (No @packageDocumentation comment for this package) ``` diff --git a/packages/react-components/react-carousel/library/src/CarouselViewport.ts b/packages/react-components/react-carousel/library/src/CarouselViewport.ts new file mode 100644 index 0000000000000..2ac88670f56ce --- /dev/null +++ b/packages/react-components/react-carousel/library/src/CarouselViewport.ts @@ -0,0 +1 @@ +export * from './components/CarouselViewport/index'; diff --git a/packages/react-components/react-carousel/library/src/components/Carousel/Carousel.tsx b/packages/react-components/react-carousel/library/src/components/Carousel/Carousel.tsx index 0daa2279fc06e..19227c2d8f2a7 100644 --- a/packages/react-components/react-carousel/library/src/components/Carousel/Carousel.tsx +++ b/packages/react-components/react-carousel/library/src/components/Carousel/Carousel.tsx @@ -5,6 +5,7 @@ import { renderCarousel_unstable } from './renderCarousel'; import { useCarouselStyles_unstable } from './useCarouselStyles.styles'; import type { CarouselProps } from './Carousel.types'; import { useCarouselContextValues_unstable } from './useCarouselContextValues'; +import { useCustomStyleHook_unstable } from '@fluentui/react-shared-contexts'; /** * Carousel is the context wrapper and container for all carousel content/controls, @@ -16,11 +17,10 @@ export const Carousel: ForwardRefComponent = React.forwardRef((pr const state = useCarousel_unstable(props, ref); useCarouselStyles_unstable(state); + useCustomStyleHook_unstable('useCarouselStyles_unstable')(state); const contextValues = useCarouselContextValues_unstable(state); - // TODO update types in packages/react-components/react-shared-contexts/src/CustomStyleHooksContext/CustomStyleHooksContext.ts - // https://github.com/microsoft/fluentui/blob/master/rfcs/react-components/convergence/custom-styling.md - // useCustomStyleHook_unstable('useCarouselStyles_unstable')(state); + return renderCarousel_unstable(state, contextValues); }); diff --git a/packages/react-components/react-carousel/library/src/components/Carousel/useCarousel.ts b/packages/react-components/react-carousel/library/src/components/Carousel/useCarousel.ts index a69c7073d693a..89e7228e1cf43 100644 --- a/packages/react-components/react-carousel/library/src/components/Carousel/useCarousel.ts +++ b/packages/react-components/react-carousel/library/src/components/Carousel/useCarousel.ts @@ -37,7 +37,7 @@ export function useCarousel_unstable(props: CarouselProps, ref: React.Ref(''); @@ -118,7 +118,7 @@ export function useCarousel_unstable(props: CarouselProps, ref: React.Ref = { root: 'fui-Carousel', @@ -13,8 +12,8 @@ export const carouselClassNames: SlotClassNames = { */ const useStyles = makeStyles({ root: { - paddingTop: tokens.strokeWidthThick, // Leave room for focus border & overflow hidden - overflow: 'hidden', + // Only hide horizontal overflow to enable focus border to bleed bounds vertically + overflowX: 'hidden', overflowAnchor: 'none', position: 'relative', }, diff --git a/packages/react-components/react-carousel/library/src/components/CarouselAutoplayButton/CarouselAutoplayButton.tsx b/packages/react-components/react-carousel/library/src/components/CarouselAutoplayButton/CarouselAutoplayButton.tsx index e34f6aa681b8f..3a91d39ba2d72 100644 --- a/packages/react-components/react-carousel/library/src/components/CarouselAutoplayButton/CarouselAutoplayButton.tsx +++ b/packages/react-components/react-carousel/library/src/components/CarouselAutoplayButton/CarouselAutoplayButton.tsx @@ -4,6 +4,7 @@ import { useCarouselAutoplayButton_unstable } from './useCarouselAutoplayButton' import { renderCarouselAutoplayButton_unstable } from './renderCarouselAutoplayButton'; import { useCarouselAutoplayButtonStyles_unstable } from './useCarouselAutoplayButtonStyles.styles'; import type { CarouselAutoplayButtonProps } from './CarouselAutoplayButton.types'; +import { useCustomStyleHook_unstable } from '@fluentui/react-shared-contexts'; /** * If the Carousel is on auto-play, the user may opt into pausing the auto-play feature via the @@ -16,9 +17,8 @@ export const CarouselAutoplayButton: ForwardRefComponent = React.fo const state = useCarouselButton_unstable(props, ref); useCarouselButtonStyles_unstable(state); - // TODO update types in packages/react-components/react-shared-contexts/src/CustomStyleHooksContext/CustomStyleHooksContext.ts - // https://github.com/microsoft/fluentui/blob/master/rfcs/react-components/convergence/custom-styling.md - // useCustomStyleHook_unstable('useCarouselButtonStyles_unstable')(state); + useCustomStyleHook_unstable('useCarouselButtonStyles_unstable')(state); return renderCarouselButton_unstable(state); }); diff --git a/packages/react-components/react-carousel/library/src/components/CarouselCard/CarouselCard.tsx b/packages/react-components/react-carousel/library/src/components/CarouselCard/CarouselCard.tsx index ce2c013dda650..262577c77e3d1 100644 --- a/packages/react-components/react-carousel/library/src/components/CarouselCard/CarouselCard.tsx +++ b/packages/react-components/react-carousel/library/src/components/CarouselCard/CarouselCard.tsx @@ -4,6 +4,7 @@ import { useCarouselCard_unstable } from './useCarouselCard'; import { renderCarouselCard_unstable } from './renderCarouselCard'; import { useCarouselCardStyles_unstable } from './useCarouselCardStyles.styles'; import type { CarouselCardProps } from './CarouselCard.types'; +import { useCustomStyleHook_unstable } from '@fluentui/react-shared-contexts'; /** * The defining wrapper of a carousel's indexed content, they will take up the full @@ -17,9 +18,8 @@ export const CarouselCard: ForwardRefComponent = React.forwar const state = useCarouselCard_unstable(props, ref); useCarouselCardStyles_unstable(state); - // TODO update types in packages/react-components/react-shared-contexts/src/CustomStyleHooksContext/CustomStyleHooksContext.ts - // https://github.com/microsoft/fluentui/blob/master/rfcs/react-components/convergence/custom-styling.md - // useCustomStyleHook_unstable('useCarouselCardStyles_unstable')(state); + useCustomStyleHook_unstable('useCarouselCardStyles_unstable')(state); + return renderCarouselCard_unstable(state); }); diff --git a/packages/react-components/react-carousel/library/src/components/CarouselCard/useCarouselCard.ts b/packages/react-components/react-carousel/library/src/components/CarouselCard/useCarouselCard.ts index 8ee83a9ac0150..c91ea2a941d57 100644 --- a/packages/react-components/react-carousel/library/src/components/CarouselCard/useCarouselCard.ts +++ b/packages/react-components/react-carousel/library/src/components/CarouselCard/useCarouselCard.ts @@ -66,7 +66,7 @@ export const useCarouselCard_unstable = ( } }, [cardFocus]); - const handleFocusCapture = React.useCallback( + const handleFocus = React.useCallback( (e: React.FocusEvent) => { if (!e.defaultPrevented && isHTMLElement(e.currentTarget) && !isMouseEvent.current) { // We want to prevent any browser scroll intervention for 'offscreen' focus @@ -88,7 +88,7 @@ export const useCarouselCard_unstable = ( } }; - const onFocusCapture = mergeCallbacks(props.onFocusCapture, handleFocusCapture); + const onFocus = mergeCallbacks(props.onFocus, handleFocus); const onMouseUp = mergeCallbacks(props.onMouseUp, handleMouseUp); const onMouseDown = mergeCallbacks(props.onMouseDown, handleMouseDown); const state: CarouselCardState = { @@ -103,7 +103,7 @@ export const useCarouselCard_unstable = ( tabIndex: cardFocus ? 0 : undefined, ...props, id, - onFocusCapture, + onFocus, onMouseDown, onMouseUp, ...focusAttrProps, diff --git a/packages/react-components/react-carousel/library/src/components/CarouselContext.ts b/packages/react-components/react-carousel/library/src/components/CarouselContext.ts index 74e1ec82764d7..1dd1a90bf1f0d 100644 --- a/packages/react-components/react-carousel/library/src/components/CarouselContext.ts +++ b/packages/react-components/react-carousel/library/src/components/CarouselContext.ts @@ -23,6 +23,7 @@ export const carouselContextDefaultValue: CarouselContextValue = { }, circular: false, containerRef: undefined, + viewportRef: undefined, }; const CarouselContext = createContext(undefined); diff --git a/packages/react-components/react-carousel/library/src/components/CarouselContext.types.ts b/packages/react-components/react-carousel/library/src/components/CarouselContext.types.ts index e69bdb5c6f1fc..be6ee9911d55b 100644 --- a/packages/react-components/react-carousel/library/src/components/CarouselContext.types.ts +++ b/packages/react-components/react-carousel/library/src/components/CarouselContext.types.ts @@ -30,7 +30,10 @@ export type CarouselContextValue = { subscribeForValues: (listener: (data: CarouselUpdateData) => void) => () => void; enableAutoplay: (autoplay: boolean) => void; resetAutoplay: () => void; + // Container with controls passed to carousel engine containerRef?: React.RefObject; + // Viewport without controls used for interactive functionality (draggable, pause autoplay etc.) + viewportRef?: React.RefObject; }; /** diff --git a/packages/react-components/react-carousel/library/src/components/CarouselNav/CarouselNav.tsx b/packages/react-components/react-carousel/library/src/components/CarouselNav/CarouselNav.tsx index 5cd341d8c0fb8..7a693c2a2d515 100644 --- a/packages/react-components/react-carousel/library/src/components/CarouselNav/CarouselNav.tsx +++ b/packages/react-components/react-carousel/library/src/components/CarouselNav/CarouselNav.tsx @@ -6,6 +6,7 @@ import { useCarouselNavContextValues_unstable } from './CarouselNavContext'; import { renderCarouselNav_unstable } from './renderCarouselNav'; import { useCarouselNav_unstable } from './useCarouselNav'; import { useCarouselNavStyles_unstable } from './useCarouselNavStyles.styles'; +import { useCustomStyleHook_unstable } from '@fluentui/react-shared-contexts'; /** * Used to jump to a card based on index, using arrow navigation via Tabster. @@ -18,9 +19,7 @@ export const CarouselNav: ForwardRefComponent = React.forwardR const contextValues = useCarouselNavContextValues_unstable(state); useCarouselNavStyles_unstable(state); - // TODO update types in packages/react-components/react-shared-contexts/src/CustomStyleHooksContext/CustomStyleHooksContext.ts - // https://github.com/microsoft/fluentui/blob/master/rfcs/react-components/convergence/custom-styling.md - // useCustomStyleHook_unstable('useCarouselNavStyles_unstable')(state); + useCustomStyleHook_unstable('useCarouselNavStyles_unstable')(state); return renderCarouselNav_unstable(state, contextValues); }); diff --git a/packages/react-components/react-carousel/library/src/components/CarouselNavButton/CarouselNavButton.tsx b/packages/react-components/react-carousel/library/src/components/CarouselNavButton/CarouselNavButton.tsx index 3334aede5bce2..274be6b9fb033 100644 --- a/packages/react-components/react-carousel/library/src/components/CarouselNavButton/CarouselNavButton.tsx +++ b/packages/react-components/react-carousel/library/src/components/CarouselNavButton/CarouselNavButton.tsx @@ -4,6 +4,7 @@ import { useCarouselNavButton_unstable } from './useCarouselNavButton'; import { renderCarouselNavButton_unstable } from './renderCarouselNavButton'; import { useCarouselNavButtonStyles_unstable } from './useCarouselNavButtonStyles.styles'; import type { CarouselNavButtonProps } from './CarouselNavButton.types'; +import { useCustomStyleHook_unstable } from '@fluentui/react-shared-contexts'; /** * The child element of CarouselNav, a singular button that will set the carousels active value on click. @@ -12,9 +13,8 @@ export const CarouselNavButton: ForwardRefComponent = Re const state = useCarouselNavButton_unstable(props, ref); useCarouselNavButtonStyles_unstable(state); - // TODO update types in packages/react-components/react-shared-contexts/src/CustomStyleHooksContext/CustomStyleHooksContext.ts - // https://github.com/microsoft/fluentui/blob/master/rfcs/react-components/convergence/custom-styling.md - // useCustomStyleHook_unstable('useCarouselNavButtonStyles_unstable')(state); + useCustomStyleHook_unstable('useCarouselNavButtonStyles_unstable')(state); + return renderCarouselNavButton_unstable(state); }); diff --git a/packages/react-components/react-carousel/library/src/components/CarouselNavContainer/CarouselNavContainer.tsx b/packages/react-components/react-carousel/library/src/components/CarouselNavContainer/CarouselNavContainer.tsx index 93b78a08e6520..93ab113adb213 100644 --- a/packages/react-components/react-carousel/library/src/components/CarouselNavContainer/CarouselNavContainer.tsx +++ b/packages/react-components/react-carousel/library/src/components/CarouselNavContainer/CarouselNavContainer.tsx @@ -4,6 +4,7 @@ import { useCarouselNavContainer_unstable } from './useCarouselNavContainer'; import { renderCarouselNavContainer_unstable } from './renderCarouselNavContainer'; import { useCarouselNavContainerStyles_unstable } from './useCarouselNavContainerStyles.styles'; import type { CarouselNavContainerProps } from './CarouselNavContainer.types'; +import { useCustomStyleHook_unstable } from '@fluentui/react-shared-contexts'; /** * CarouselNavContainer component - This container will provide multiple valid layout options for the underlying carousel controls @@ -12,16 +13,7 @@ export const CarouselNavContainer: ForwardRefComponent = React.fo const state = useCarouselSlider_unstable(props, ref); useCarouselSliderStyles_unstable(state); + useCustomStyleHook_unstable('useCarouselSliderStyles_unstable')(state); const context = useCarouselSliderContextValues_unstable(state); - // TODO update types in packages/react-components/react-shared-contexts/src/CustomStyleHooksContext/CustomStyleHooksContext.ts - // https://github.com/microsoft/fluentui/blob/master/rfcs/react-components/convergence/custom-styling.md - // useCustomStyleHook_unstable('useCarouselSliderStyles_unstable')(state); return renderCarouselSlider_unstable(state, context); }); diff --git a/packages/react-components/react-carousel/library/src/components/CarouselViewport/CarouselViewport.test.tsx b/packages/react-components/react-carousel/library/src/components/CarouselViewport/CarouselViewport.test.tsx new file mode 100644 index 0000000000000..a5e4a74692542 --- /dev/null +++ b/packages/react-components/react-carousel/library/src/components/CarouselViewport/CarouselViewport.test.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../testing/isConformant'; +import { CarouselViewport } from './CarouselViewport'; + +describe('CarouselViewport', () => { + isConformant({ + Component: CarouselViewport, + displayName: 'CarouselViewport', + }); + + // TODO add more tests here, and create visual regression tests in /apps/vr-tests + + it('renders a default state', () => { + const result = render(Default CarouselViewport); + expect(result.container).toMatchSnapshot(); + }); +}); diff --git a/packages/react-components/react-carousel/library/src/components/CarouselViewport/CarouselViewport.tsx b/packages/react-components/react-carousel/library/src/components/CarouselViewport/CarouselViewport.tsx new file mode 100644 index 0000000000000..eed68cf226e32 --- /dev/null +++ b/packages/react-components/react-carousel/library/src/components/CarouselViewport/CarouselViewport.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { useCarouselViewport_unstable } from './useCarouselViewport'; +import { renderCarouselViewport_unstable } from './renderCarouselViewport'; +import { useCarouselViewportStyles_unstable } from './useCarouselViewportStyles.styles'; +import type { CarouselViewportProps } from './CarouselViewport.types'; +import { useCarouselSliderContextValues_unstable } from '../CarouselSlider/CarouselSliderContext'; +import { useCustomStyleHook_unstable } from '@fluentui/react-shared-contexts'; + +/** + * CarouselViewport component - TODO: add more docs + */ +export const CarouselViewport: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useCarouselViewport_unstable(props, ref); + + useCarouselViewportStyles_unstable(state); + useCustomStyleHook_unstable('useCarouselViewportStyles_unstable')(state); + + const context = useCarouselSliderContextValues_unstable(state); + + return renderCarouselViewport_unstable(state, context); +}); + +CarouselViewport.displayName = 'CarouselViewport'; diff --git a/packages/react-components/react-carousel/library/src/components/CarouselViewport/CarouselViewport.types.ts b/packages/react-components/react-carousel/library/src/components/CarouselViewport/CarouselViewport.types.ts new file mode 100644 index 0000000000000..5f1091dbcfbb4 --- /dev/null +++ b/packages/react-components/react-carousel/library/src/components/CarouselViewport/CarouselViewport.types.ts @@ -0,0 +1,19 @@ +import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; +import { CarouselSliderContextValue } from '../CarouselSlider/CarouselSlider.types'; + +export type CarouselViewportSlots = { + /** + * The viewport outer container, defining the size of the carousels visible and interactable area + */ + root: Slot<'div'>; +}; + +/** + * CarouselViewport Props + */ +export type CarouselViewportProps = ComponentProps; + +/** + * State used in rendering CarouselViewport + */ +export type CarouselViewportState = ComponentState> & CarouselSliderContextValue; diff --git a/packages/react-components/react-carousel/library/src/components/CarouselViewport/__snapshots__/CarouselViewport.test.tsx.snap b/packages/react-components/react-carousel/library/src/components/CarouselViewport/__snapshots__/CarouselViewport.test.tsx.snap new file mode 100644 index 0000000000000..ec4f172ceacbd --- /dev/null +++ b/packages/react-components/react-carousel/library/src/components/CarouselViewport/__snapshots__/CarouselViewport.test.tsx.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CarouselViewport renders a default state 1`] = ` +
+ +
+`; diff --git a/packages/react-components/react-carousel/library/src/components/CarouselViewport/index.ts b/packages/react-components/react-carousel/library/src/components/CarouselViewport/index.ts new file mode 100644 index 0000000000000..e58459eaffcf1 --- /dev/null +++ b/packages/react-components/react-carousel/library/src/components/CarouselViewport/index.ts @@ -0,0 +1,5 @@ +export * from './CarouselViewport'; +export * from './CarouselViewport.types'; +export * from './renderCarouselViewport'; +export * from './useCarouselViewport'; +export * from './useCarouselViewportStyles.styles'; diff --git a/packages/react-components/react-carousel/library/src/components/CarouselViewport/renderCarouselViewport.tsx b/packages/react-components/react-carousel/library/src/components/CarouselViewport/renderCarouselViewport.tsx new file mode 100644 index 0000000000000..6a1ea00c25ffd --- /dev/null +++ b/packages/react-components/react-carousel/library/src/components/CarouselViewport/renderCarouselViewport.tsx @@ -0,0 +1,22 @@ +/** @jsxRuntime automatic */ +/** @jsxImportSource @fluentui/react-jsx-runtime */ + +import { assertSlots } from '@fluentui/react-utilities'; +import type { CarouselViewportState, CarouselViewportSlots } from './CarouselViewport.types'; +import { CarouselSliderContextValues, CarouselSliderContextProvider } from '../CarouselSlider/CarouselSliderContext'; + +/** + * Render the final JSX of CarouselViewport + */ +export const renderCarouselViewport_unstable = ( + state: CarouselViewportState, + contextValues: CarouselSliderContextValues, +) => { + assertSlots(state); + + return ( + + + + ); +}; diff --git a/packages/react-components/react-carousel/library/src/components/CarouselViewport/useCarouselViewport.ts b/packages/react-components/react-carousel/library/src/components/CarouselViewport/useCarouselViewport.ts new file mode 100644 index 0000000000000..b04c25996074d --- /dev/null +++ b/packages/react-components/react-carousel/library/src/components/CarouselViewport/useCarouselViewport.ts @@ -0,0 +1,36 @@ +import * as React from 'react'; +import { getIntrinsicElementProps, slot, useMergedRefs } from '@fluentui/react-utilities'; +import type { CarouselViewportProps, CarouselViewportState } from './CarouselViewport.types'; +import { useCarouselContext_unstable as useCarouselContext } from '../CarouselContext'; + +/** + * Create the state required to render CarouselViewport. + * + * The returned state can be modified with hooks such as useCarouselViewportStyles_unstable, + * before being passed to renderCarouselViewport_unstable. + * + * @param props - props from this instance of CarouselViewport + * @param ref - reference to root HTMLDivElement of CarouselViewport + */ +export const useCarouselViewport_unstable = ( + props: CarouselViewportProps, + ref: React.Ref, +): CarouselViewportState => { + const viewportRef = useCarouselContext(ctx => ctx.viewportRef); + + return { + components: { + root: 'div', + }, + root: slot.always( + getIntrinsicElementProps('div', { + ref: useMergedRefs(ref, viewportRef), + role: 'presentation', + // Draggable ensures dragging is supported (even if not enabled) + draggable: true, + ...props, + }), + { elementType: 'div' }, + ), + }; +}; diff --git a/packages/react-components/react-carousel/library/src/components/CarouselViewport/useCarouselViewportStyles.styles.ts b/packages/react-components/react-carousel/library/src/components/CarouselViewport/useCarouselViewportStyles.styles.ts new file mode 100644 index 0000000000000..dfa88acfd5b65 --- /dev/null +++ b/packages/react-components/react-carousel/library/src/components/CarouselViewport/useCarouselViewportStyles.styles.ts @@ -0,0 +1,29 @@ +import { makeStyles, mergeClasses } from '@griffel/react'; +import type { SlotClassNames } from '@fluentui/react-utilities'; +import type { CarouselViewportSlots, CarouselViewportState } from './CarouselViewport.types'; + +export const carouselViewportClassNames: SlotClassNames = { + root: 'fui-CarouselViewport', +}; + +/** + * Styles for the root slot + */ +const useStyles = makeStyles({ + root: { + maxWidth: '100%', + width: 'auto', + }, +}); + +/** + * Apply styling to the CarouselViewport slots based on the state + */ +export const useCarouselViewportStyles_unstable = (state: CarouselViewportState): CarouselViewportState => { + 'use no memo'; + + const styles = useStyles(); + state.root.className = mergeClasses(carouselViewportClassNames.root, styles.root, state.root.className); + + return state; +}; diff --git a/packages/react-components/react-carousel/library/src/components/pointerEvents.ts b/packages/react-components/react-carousel/library/src/components/pointerEvents.ts index a16f0e209e3c1..5767be5327237 100644 --- a/packages/react-components/react-carousel/library/src/components/pointerEvents.ts +++ b/packages/react-components/react-carousel/library/src/components/pointerEvents.ts @@ -34,6 +34,7 @@ export function pointerEventPlugin(options: PointerEventPluginOptions): PointerE function clearPointerEvent() { pointerEvent = undefined; + pointerUpListener(); } function selectListener() { @@ -41,8 +42,8 @@ export function pointerEventPlugin(options: PointerEventPluginOptions): PointerE const newIndex = emblaApi.selectedScrollSnap() ?? 0; options.onSelectViaDrag(pointerEvent, newIndex); - clearPointerEvent(); } + clearPointerEvent(); } function init(emblaApiInstance: EmblaCarouselType, optionsHandler: OptionsHandlerType): void { diff --git a/packages/react-components/react-carousel/library/src/components/useEmblaCarousel.ts b/packages/react-components/react-carousel/library/src/components/useEmblaCarousel.ts index 0c7f5262a44b5..d1a1212b658f0 100644 --- a/packages/react-components/react-carousel/library/src/components/useEmblaCarousel.ts +++ b/packages/react-components/react-carousel/library/src/components/useEmblaCarousel.ts @@ -95,9 +95,6 @@ export function useEmblaCarousel( stopOnInteraction: !autoplayRef.current, stopOnMouseEnter: true, stopOnFocusIn: true, - rootNode: (emblaRoot: HTMLElement) => { - return emblaRoot.querySelector(sliderClassname) ?? emblaRoot; - }, }), ]; @@ -127,6 +124,7 @@ export function useEmblaCarousel( }; }, []); + const viewportRef: React.RefObject = React.useRef(null); const containerRef: React.RefObject = React.useMemo(() => { let currentElement: HTMLDivElement | null = null; @@ -182,10 +180,12 @@ export function useEmblaCarousel( emblaApi.current?.destroy(); } - if (newElement) { - currentElement = newElement; + // Use direct viewport if available, else fallback to container (includes Carousel controls). + const wrapperElement = viewportRef.current ?? newElement; + if (wrapperElement) { + currentElement = wrapperElement; emblaApi.current = EmblaCarousel( - newElement, + wrapperElement, { ...DEFAULT_EMBLA_OPTIONS, ...emblaOptions.current, @@ -264,6 +264,7 @@ export function useEmblaCarousel( return { activeIndex, carouselApi, + viewportRef, containerRef, subscribeForValues, enableAutoplay, diff --git a/packages/react-components/react-carousel/library/src/index.ts b/packages/react-components/react-carousel/library/src/index.ts index f3474ba465235..7bd893c927abf 100644 --- a/packages/react-components/react-carousel/library/src/index.ts +++ b/packages/react-components/react-carousel/library/src/index.ts @@ -84,3 +84,11 @@ export { } from './CarouselNavContainer'; export { carouselContextDefaultValue, CarouselProvider, useCarouselContext_unstable } from './CarouselContext'; export type { CarouselIndexChangeData, CarouselContextValue, CarouselContextValues } from './CarouselContext'; +export type { CarouselViewportProps, CarouselViewportSlots, CarouselViewportState } from './CarouselViewport'; +export { + CarouselViewport, + carouselViewportClassNames, + renderCarouselViewport_unstable, + useCarouselViewportStyles_unstable, + useCarouselViewport_unstable, +} from './CarouselViewport'; diff --git a/packages/react-components/react-carousel/stories/src/Carousel/CarouselActionCards.stories.tsx b/packages/react-components/react-carousel/stories/src/Carousel/CarouselActionCards.stories.tsx index efcf5f5b78ac7..7c97d73aec099 100644 --- a/packages/react-components/react-carousel/stories/src/Carousel/CarouselActionCards.stories.tsx +++ b/packages/react-components/react-carousel/stories/src/Carousel/CarouselActionCards.stories.tsx @@ -8,6 +8,7 @@ import { Option, Switch, Field, + CarouselSlider, } from '@fluentui/react-components'; import { MoreHorizontalRegular, DocumentLinkRegular } from '@fluentui/react-icons'; import { @@ -18,7 +19,7 @@ import { CarouselNavButton, CarouselNavContainer, CarouselProps, - CarouselSlider, + CarouselViewport, } from '@fluentui/react-components'; import * as React from 'react'; @@ -199,11 +200,13 @@ export const AlignmentAndWhitespace = () => {
- - {POSTS.map((post, index) => ( - - ))} - + + + {POSTS.map((post, index) => ( + + ))} + + {
- - {IMAGES.map((imageSrc, index) => ( - - Card {index + 1} - - ))} - + + + {IMAGES.map((imageSrc, index) => ( + + Card {index + 1} + + ))} + + { -
+ @@ -153,7 +152,7 @@ export const Controlled = () => { -
+ diff --git a/packages/react-components/react-carousel/stories/src/Carousel/CarouselDefault.stories.tsx b/packages/react-components/react-carousel/stories/src/Carousel/CarouselDefault.stories.tsx index 014462f6d7ec7..60d13fb018e91 100644 --- a/packages/react-components/react-carousel/stories/src/Carousel/CarouselDefault.stories.tsx +++ b/packages/react-components/react-carousel/stories/src/Carousel/CarouselDefault.stories.tsx @@ -5,8 +5,9 @@ import { CarouselNav, CarouselNavButton, CarouselNavContainer, - CarouselSlider, + CarouselViewport, CarouselAnnouncerFunction, + CarouselSlider, } from '@fluentui/react-components'; import * as React from 'react'; @@ -78,13 +79,15 @@ const getAnnouncement: CarouselAnnouncerFunction = (index: number, totalSlides: export const Default = () => ( - - {IMAGES.map((imageSrc, index) => ( - - Card {index + 1} - - ))} - + + + {IMAGES.map((imageSrc, index) => ( + + Card {index + 1} + + ))} + + { setStatusLog(prev => [[Date.now(), { type: data.type, index: data.index }], ...prev]); }} > - - - - Lorem Ipsum - - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor... - - - - - - Lorem Ipsum - Lorem ipsum... - - - - - Lorem Ipsum - Lorem ipsum dolor sit amet... - - - - - Lorem Ipsum - Lorem ipsum dolor sit amet... - - - - - Lorem Ipsum - Lorem ipsum dolor sit amet... - - - - - Lorem Ipsum - Lorem ipsum dolor sit amet... - - - - - Lorem Ipsum - Lorem ipsum... - - - + + + + + Lorem Ipsum + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor... + + + + + + Lorem Ipsum + Lorem ipsum... + + + + + Lorem Ipsum + Lorem ipsum dolor sit amet... + + + + + Lorem Ipsum + Lorem ipsum dolor sit amet... + + + + + Lorem Ipsum + Lorem ipsum dolor sit amet... + + + + + Lorem Ipsum + Lorem ipsum dolor sit amet... + + + + + Lorem Ipsum + Lorem ipsum... + + + + { motion="fade" onActiveIndexChange={(e, data) => setActiveIndex(data.index)} > - - {PAGES.map(page => ( - - {page.imgSrc} -

- {page.header} -

- {page.text} -
- ))} -
+ + + {PAGES.map(page => ( + + {page.imgSrc} +

+ {page.header} +

+ {page.text} +
+ ))} +
+
diff --git a/packages/react-components/react-carousel/stories/src/Carousel/CarouselImageBox.stories.tsx b/packages/react-components/react-carousel/stories/src/Carousel/CarouselImageBox.stories.tsx index 6026c4c2b4087..f418cbcae8a34 100644 --- a/packages/react-components/react-carousel/stories/src/Carousel/CarouselImageBox.stories.tsx +++ b/packages/react-components/react-carousel/stories/src/Carousel/CarouselImageBox.stories.tsx @@ -1,4 +1,4 @@ -import { makeStyles, Image } from '@fluentui/react-components'; +import { makeStyles, Image, CarouselSlider } from '@fluentui/react-components'; import { Carousel, CarouselAnnouncerFunction, @@ -6,12 +6,12 @@ import { CarouselNav, CarouselNavContainer, CarouselNavImageButton, - CarouselSlider, + CarouselViewport, } from '@fluentui/react-components'; import * as React from 'react'; const useClasses = makeStyles({ - slider: { + viewport: { /* Optional: Prevent image from overlapping the 'overlay-expanded' controls */ marginBottom: '72px', }, @@ -71,13 +71,15 @@ export const ImageSlideshow = () => { return ( - - {IMAGES.map((image, index) => ( - - - - ))} - + + + {IMAGES.map((image, index) => ( + + + + ))} + + { return ( - - - - Lorem Ipsum - - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor... - - - - - - Lorem Ipsum - Lorem ipsum... - - - - - Lorem Ipsum - Lorem ipsum dolor sit amet... - - - - - Lorem Ipsum - Lorem ipsum dolor sit amet... - - - - - Lorem Ipsum - Lorem ipsum dolor sit amet... - - - - - Lorem Ipsum - Lorem ipsum dolor sit amet... - - - - - Lorem Ipsum - Lorem ipsum... - - - + + + + + Lorem Ipsum + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor... + + + + + + Lorem Ipsum + Lorem ipsum... + + + + + Lorem Ipsum + Lorem ipsum dolor sit amet... + + + + + Lorem Ipsum + Lorem ipsum dolor sit amet... + + + + + Lorem Ipsum + Lorem ipsum dolor sit amet... + + + + + Lorem Ipsum + Lorem ipsum dolor sit amet... + + + + + Lorem Ipsum + Lorem ipsum... + + + + {index => } diff --git a/packages/react-components/react-carousel/stories/src/Carousel/index.stories.tsx b/packages/react-components/react-carousel/stories/src/Carousel/index.stories.tsx index eb741bec041ba..29d2ccf2468ad 100644 --- a/packages/react-components/react-carousel/stories/src/Carousel/index.stories.tsx +++ b/packages/react-components/react-carousel/stories/src/Carousel/index.stories.tsx @@ -8,6 +8,7 @@ import { CarouselNavContainer, CarouselNavImageButton, CarouselSlider, + CarouselViewport, } from '@fluentui/react-components'; import descriptionMd from './CarouselDescription.md'; @@ -34,6 +35,7 @@ export default { CarouselNavContainer, CarouselNavImageButton, CarouselSlider, + CarouselViewport, }, parameters: { docs: { diff --git a/packages/react-components/react-components/etc/react-components.api.md b/packages/react-components/react-components/etc/react-components.api.md index 7ab03687aecc9..7692e5d3ce711 100644 --- a/packages/react-components/react-components/etc/react-components.api.md +++ b/packages/react-components/react-components/etc/react-components.api.md @@ -203,6 +203,11 @@ import { CarouselSliderSlots } from '@fluentui/react-carousel'; import { CarouselSliderState } from '@fluentui/react-carousel'; import { CarouselSlots } from '@fluentui/react-carousel'; import { CarouselState } from '@fluentui/react-carousel'; +import { CarouselViewport } from '@fluentui/react-carousel'; +import { carouselViewportClassNames } from '@fluentui/react-carousel'; +import { CarouselViewportProps } from '@fluentui/react-carousel'; +import { CarouselViewportSlots } from '@fluentui/react-carousel'; +import { CarouselViewportState } from '@fluentui/react-carousel'; import { Checkbox } from '@fluentui/react-checkbox'; import { checkboxClassNames } from '@fluentui/react-checkbox'; import { CheckboxOnChangeData } from '@fluentui/react-checkbox'; @@ -769,6 +774,7 @@ import { renderCarouselNavButton_unstable } from '@fluentui/react-carousel'; import { renderCarouselNavContainer_unstable } from '@fluentui/react-carousel'; import { renderCarouselNavImageButton_unstable } from '@fluentui/react-carousel'; import { renderCarouselSlider_unstable } from '@fluentui/react-carousel'; +import { renderCarouselViewport_unstable } from '@fluentui/react-carousel'; import { renderCheckbox_unstable } from '@fluentui/react-checkbox'; import { renderColorSwatch_unstable } from '@fluentui/react-swatch-picker'; import { renderCombobox_unstable } from '@fluentui/react-combobox'; @@ -1429,6 +1435,8 @@ import { useCarouselNavStyles_unstable } from '@fluentui/react-carousel'; import { useCarouselSlider_unstable } from '@fluentui/react-carousel'; import { useCarouselSliderStyles_unstable } from '@fluentui/react-carousel'; import { useCarouselStyles_unstable } from '@fluentui/react-carousel'; +import { useCarouselViewport_unstable } from '@fluentui/react-carousel'; +import { useCarouselViewportStyles_unstable } from '@fluentui/react-carousel'; import { useCheckbox_unstable } from '@fluentui/react-checkbox'; import { useCheckboxStyles_unstable } from '@fluentui/react-checkbox'; import { useCheckmarkStyles_unstable } from '@fluentui/react-menu'; @@ -2197,6 +2205,16 @@ export { CarouselSlots } export { CarouselState } +export { CarouselViewport } + +export { carouselViewportClassNames } + +export { CarouselViewportProps } + +export { CarouselViewportSlots } + +export { CarouselViewportState } + export { Checkbox } export { checkboxClassNames } @@ -3329,6 +3347,8 @@ export { renderCarouselNavImageButton_unstable } export { renderCarouselSlider_unstable } +export { renderCarouselViewport_unstable } + export { renderCheckbox_unstable } export { renderColorSwatch_unstable } @@ -4649,6 +4669,10 @@ export { useCarouselSliderStyles_unstable } export { useCarouselStyles_unstable } +export { useCarouselViewport_unstable } + +export { useCarouselViewportStyles_unstable } + export { useCheckbox_unstable } export { useCheckboxStyles_unstable } diff --git a/packages/react-components/react-components/src/index.ts b/packages/react-components/react-components/src/index.ts index 74269e64b2bc5..83d8f76686912 100644 --- a/packages/react-components/react-components/src/index.ts +++ b/packages/react-components/react-components/src/index.ts @@ -1935,6 +1935,11 @@ export { carouselContextDefaultValue, CarouselProvider, useCarouselContext_unstable, + CarouselViewport, + carouselViewportClassNames, + renderCarouselViewport_unstable, + useCarouselViewportStyles_unstable, + useCarouselViewport_unstable, } from '@fluentui/react-carousel'; export type { CarouselButtonProps, @@ -1969,4 +1974,7 @@ export type { CarouselIndexChangeData, CarouselContextValue, CarouselContextValues, + CarouselViewportProps, + CarouselViewportSlots, + CarouselViewportState, } from '@fluentui/react-carousel'; diff --git a/packages/react-components/react-provider/library/etc/react-provider.api.md b/packages/react-components/react-provider/library/etc/react-provider.api.md index cc2bf4bcf7fe1..e7e94b84cad4f 100644 --- a/packages/react-components/react-provider/library/etc/react-provider.api.md +++ b/packages/react-components/react-provider/library/etc/react-provider.api.md @@ -161,6 +161,16 @@ export const FluentProvider: React_2.ForwardRefExoticComponent void; useSwatchPickerRowStyles_unstable: (state: unknown) => void; useSwatchPickerStyles_unstable: (state: unknown) => void; + useCarouselViewportStyles_unstable: (state: unknown) => void; + useCarouselSliderStyles_unstable: (state: unknown) => void; + useCarouselStyles_unstable: (state: unknown) => void; + useCarouselAutoplayButtonStyles_unstable: (state: unknown) => void; + useCarouselButtonStyles_unstable: (state: unknown) => void; + useCarouselCardStyles_unstable: (state: unknown) => void; + useCarouselNavStyles_unstable: (state: unknown) => void; + useCarouselNavButtonStyles_unstable: (state: unknown) => void; + useCarouselNavContainerStyles_unstable: (state: unknown) => void; + useCarouselNavImageButtonStyles_unstable: (state: unknown) => void; }> | undefined; dir?: "ltr" | "rtl" | undefined; targetDocument?: Document | undefined; diff --git a/packages/react-components/react-shared-contexts/library/etc/react-shared-contexts.api.md b/packages/react-components/react-shared-contexts/library/etc/react-shared-contexts.api.md index 470ff8ebbff79..6fa34a52a7c02 100644 --- a/packages/react-components/react-shared-contexts/library/etc/react-shared-contexts.api.md +++ b/packages/react-components/react-shared-contexts/library/etc/react-shared-contexts.api.md @@ -170,6 +170,16 @@ export const CustomStyleHooksContext_unstable: React_2.Context | undefined>; // @public (undocumented) @@ -309,6 +319,16 @@ export type CustomStyleHooksContextValue_unstable = Partial<{ useEmptySwatchStyles_unstable: CustomStyleHook; useSwatchPickerRowStyles_unstable: CustomStyleHook; useSwatchPickerStyles_unstable: CustomStyleHook; + useCarouselViewportStyles_unstable: CustomStyleHook; + useCarouselSliderStyles_unstable: CustomStyleHook; + useCarouselStyles_unstable: CustomStyleHook; + useCarouselAutoplayButtonStyles_unstable: CustomStyleHook; + useCarouselButtonStyles_unstable: CustomStyleHook; + useCarouselCardStyles_unstable: CustomStyleHook; + useCarouselNavStyles_unstable: CustomStyleHook; + useCarouselNavButtonStyles_unstable: CustomStyleHook; + useCarouselNavContainerStyles_unstable: CustomStyleHook; + useCarouselNavImageButtonStyles_unstable: CustomStyleHook; }>; // @internal (undocumented) @@ -448,6 +468,16 @@ export const CustomStyleHooksProvider_unstable: React_2.Provider | undefined>; // @internal (undocumented) diff --git a/packages/react-components/react-shared-contexts/library/src/CustomStyleHooksContext/CustomStyleHooksContext.ts b/packages/react-components/react-shared-contexts/library/src/CustomStyleHooksContext/CustomStyleHooksContext.ts index 7db060199c517..82a3815d1fc74 100644 --- a/packages/react-components/react-shared-contexts/library/src/CustomStyleHooksContext/CustomStyleHooksContext.ts +++ b/packages/react-components/react-shared-contexts/library/src/CustomStyleHooksContext/CustomStyleHooksContext.ts @@ -148,6 +148,16 @@ export type CustomStyleHooksContextValue = Partial<{ useEmptySwatchStyles_unstable: CustomStyleHook; useSwatchPickerRowStyles_unstable: CustomStyleHook; useSwatchPickerStyles_unstable: CustomStyleHook; + useCarouselViewportStyles_unstable: CustomStyleHook; + useCarouselSliderStyles_unstable: CustomStyleHook; + useCarouselStyles_unstable: CustomStyleHook; + useCarouselAutoplayButtonStyles_unstable: CustomStyleHook; + useCarouselButtonStyles_unstable: CustomStyleHook; + useCarouselCardStyles_unstable: CustomStyleHook; + useCarouselNavStyles_unstable: CustomStyleHook; + useCarouselNavButtonStyles_unstable: CustomStyleHook; + useCarouselNavContainerStyles_unstable: CustomStyleHook; + useCarouselNavImageButtonStyles_unstable: CustomStyleHook; }>; /**