diff --git a/change/@fluentui-react-teaching-popover-preview-4f4039b8-fa4b-4ccf-aaa5-eada8ab82563.json b/change/@fluentui-react-teaching-popover-preview-4f4039b8-fa4b-4ccf-aaa5-eada8ab82563.json new file mode 100644 index 00000000000000..5b5d1fedb1831b --- /dev/null +++ b/change/@fluentui-react-teaching-popover-preview-4f4039b8-fa4b-4ccf-aaa5-eada8ab82563.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: Ensure all sub components are overridable and enable better string pass through via slots", + "packageName": "@fluentui/react-teaching-popover-preview", + "email": "mifraser@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/package.json b/package.json index 2ec98d85b5cf36..9f452bb54ed690 100644 --- a/package.json +++ b/package.json @@ -172,6 +172,7 @@ "@types/scheduler": "0.16.2", "@types/semver": "^6.2.0", "@types/tmp": "0.2.0", + "@types/use-sync-external-store": "0.0.6", "@types/vinyl": "2.0.7", "@types/webpack-bundle-analyzer": "4.7.0", "@types/webpack-dev-middleware": "5.3.0", diff --git a/packages/react-components/react-teaching-popover-preview/docs/Spec.md b/packages/react-components/react-teaching-popover-preview/docs/Spec.md index f4f3252bbf7e98..289967f3441461 100644 --- a/packages/react-components/react-teaching-popover-preview/docs/Spec.md +++ b/packages/react-components/react-teaching-popover-preview/docs/Spec.md @@ -151,7 +151,7 @@ The original PopoverContext provider is preserved, this ensures that popover fun TeachingPopoverTrigger has no additional functionality over PopoverTrigger, and is used to wrap the launch button or connected UI component. Internally, TeachingPopoverButtons provide primary/secondary action functionality and additional styling based on state over tge trigger wrapper. -Carousel logic, such as page change can be accessed via the TeachingPopoverCarousel's onPageChange and onFinish for external use or control. +Carousel logic, such as page change can be accessed via the TeachingPopoverCarousel's onValueChange and onFinish for external use or control. ## Behaviors @@ -183,7 +183,7 @@ TeachingPopoverTitle is intended to provide a sub-header for TeachingPopoverBody #### TeachingPopoverBody -This body section encapsulates a standardized media slot, with short/medium/tall size settings via mediaLength prop (TeachingPopoverBodyMediaLength type). It also acts as a boundary for pages within a TeachingPopoverCarousel, and will be paginated based on this encapsulation. +This body section encapsulates a standardized media slot, with short/medium/tall size settings via mediaLength prop. It also acts as a boundary for pages within a TeachingPopoverCarousel, and will be paginated based on this encapsulation. ## Accessibility diff --git a/packages/react-components/react-teaching-popover-preview/etc/react-teaching-popover-preview.api.md b/packages/react-components/react-teaching-popover-preview/etc/react-teaching-popover-preview.api.md index 341d24ff615116..87f760e6862c4c 100644 --- a/packages/react-components/react-teaching-popover-preview/etc/react-teaching-popover-preview.api.md +++ b/packages/react-components/react-teaching-popover-preview/etc/react-teaching-popover-preview.api.md @@ -8,12 +8,12 @@ import { ARIAButtonSlotProps } from '@fluentui/react-aria'; import { Button } from '@fluentui/react-button'; +import { ButtonProps } from '@fluentui/react-button'; +import { ButtonState } from '@fluentui/react-button'; import { ComponentProps } from '@fluentui/react-utilities'; import { ComponentState } from '@fluentui/react-utilities'; -import type { ContextSelector } from '@fluentui/react-context-selector'; import { EventData } from '@fluentui/react-utilities'; -import { EventHandler } from '@fluentui/react-utilities'; -import { FC } from 'react'; +import type { EventHandler } from '@fluentui/react-utilities'; import type { ForwardRefComponent } from '@fluentui/react-utilities'; import { JSXElementConstructor } from 'react'; import { PopoverContextValue } from '@fluentui/react-popover'; @@ -24,10 +24,9 @@ import { PopoverSurfaceState } from '@fluentui/react-popover'; import { PopoverTriggerChildProps } from '@fluentui/react-popover'; import { PopoverTriggerProps } from '@fluentui/react-popover'; import { PopoverTriggerState } from '@fluentui/react-popover'; -import { Provider } from 'react'; -import { ProviderProps } from 'react'; import * as React_2 from 'react'; import { ReactElement } from 'react'; +import { ReactNode } from 'react'; import { Slot } from '@fluentui/react-utilities'; import type { SlotClassNames } from '@fluentui/react-utilities'; @@ -40,12 +39,24 @@ export const renderTeachingPopoverBody_unstable: (state: TeachingPopoverBodyStat // @public export const renderTeachingPopoverCarousel_unstable: (state: TeachingPopoverCarouselState, contextValues: TeachingPopoverCarouselContextValues) => JSX.Element; +// @public +export const renderTeachingPopoverCarouselCard_unstable: (state: TeachingPopoverCarouselCardState) => JSX.Element; + +// @public +export const renderTeachingPopoverCarouselFooter_unstable: (state: TeachingPopoverCarouselFooterState) => JSX.Element; + +// @public +export const renderTeachingPopoverCarouselFooterButton_unstable: (state: TeachingPopoverCarouselFooterButtonState) => JSX.Element; + // @public export const renderTeachingPopoverCarouselNav_unstable: (state: TeachingPopoverCarouselNavState) => JSX.Element; // @public export const renderTeachingPopoverCarouselNavButton_unstable: (state: TeachingPopoverCarouselNavButtonState) => JSX.Element; +// @public +export const renderTeachingPopoverCarouselPageCount_unstable: (state: TeachingPopoverCarouselPageCountState) => JSX.Element; + // @public export const renderTeachingPopoverFooter_unstable: (state: TeachingPopoverFooterState) => JSX.Element; @@ -70,12 +81,9 @@ export const TeachingPopoverBody: ForwardRefComponent; // @public (undocumented) export const teachingPopoverBodyClassNames: SlotClassNames; -// @public (undocumented) -export type TeachingPopoverBodyMediaLength = 'short' | 'medium' | 'tall'; - // @public (undocumented) export type TeachingPopoverBodyProps = ComponentProps & { - mediaLength?: TeachingPopoverBodyMediaLength; + mediaLength?: 'short' | 'medium' | 'tall'; }; // @public (undocumented) @@ -85,16 +93,77 @@ export type TeachingPopoverBodySlots = { }; // @public (undocumented) -export type TeachingPopoverBodyState = ComponentState & Partial>; +export type TeachingPopoverBodyState = ComponentState & Required>; // @public export const TeachingPopoverCarousel: ForwardRefComponent; +// @public +export const TeachingPopoverCarouselCard: ForwardRefComponent; + +// @public (undocumented) +export const teachingPopoverCarouselCardClassNames: SlotClassNames; + +// @public (undocumented) +export type TeachingPopoverCarouselCardProps = ComponentProps & { + value: string; +}; + +// @public (undocumented) +export type TeachingPopoverCarouselCardSlots = { + root: NonNullable>; +}; + +// @public (undocumented) +export type TeachingPopoverCarouselCardState = ComponentState & Required>; + // @public (undocumented) export const teachingPopoverCarouselClassNames: SlotClassNames; // @public -export type TeachingPopoverCarouselContextValue = Pick; +export const TeachingPopoverCarouselFooter: ForwardRefComponent; + +// @public +export const TeachingPopoverCarouselFooterButton: ForwardRefComponent; + +// @public (undocumented) +export const teachingPopoverCarouselFooterButtonClassNames: SlotClassNames; + +// @public +export type TeachingPopoverCarouselFooterButtonProps = ComponentProps & ButtonProps & { + navType: 'next' | 'prev'; + altText: ReactNode; +}; + +// @public (undocumented) +export type TeachingPopoverCarouselFooterButtonSlots = { + root: NonNullable>>; +}; + +// @public +export type TeachingPopoverCarouselFooterButtonState = ButtonState & ComponentState & Pick & { + popoverAppearance: PopoverContextValue['appearance']; +}; + +// @public (undocumented) +export const teachingPopoverCarouselFooterClassNames: SlotClassNames; + +// @public +export type TeachingPopoverCarouselFooterProps = ComponentProps & { + layout?: TeachingPopoverCarouselFooterLayout; + initialStepText: string; + finalStepText: string; +}; + +// @public (undocumented) +export type TeachingPopoverCarouselFooterSlots = { + root: NonNullable>; + previous?: Slot; + next: NonNullable>; +}; + +// @public +export type TeachingPopoverCarouselFooterState = ComponentState> & Pick; // @public export const TeachingPopoverCarouselNav: ForwardRefComponent; @@ -106,9 +175,7 @@ export const TeachingPopoverCarouselNavButton: ForwardRefComponent; // @public -export type TeachingPopoverCarouselNavButtonProps = ComponentProps> & { - index: number; -}; +export type TeachingPopoverCarouselNavButtonProps = ComponentProps; // @public (undocumented) export type TeachingPopoverCarouselNavButtonSlots = { @@ -124,7 +191,9 @@ export type TeachingPopoverCarouselNavButtonState = ComponentState; // @public (undocumented) -export type TeachingPopoverCarouselNavProps = ComponentProps>; +export type TeachingPopoverCarouselNavProps = Omit>, 'children'> & { + children: NavButtonRenderFunction; +}; // @public (undocumented) export type TeachingPopoverCarouselNavSlots = { @@ -133,39 +202,51 @@ export type TeachingPopoverCarouselNavSlots = { // @public (undocumented) export type TeachingPopoverCarouselNavState = ComponentState & { - currentPage: number; - totalPages: number; + values: string[]; + renderNavButton: NavButtonRenderFunction; }; // @public -export type TeachingPopoverCarouselProps = Partial> & { - layout?: TeachingPopoverCarouselLayout; - paginationType?: 'text' | 'icon'; - strings: TeachingPopoverStrings; - defaultCurrentPage?: number; - onPageChange?: EventHandler; - onFinish?: EventHandler; - currentPage?: number; +export const TeachingPopoverCarouselPageCount: ForwardRefComponent; + +// @public (undocumented) +export const teachingPopoverCarouselPageCountClassNames: SlotClassNames; + +// @public +export type TeachingPopoverCarouselPageCountProps = Omit>, 'children'> & { + children: TeachingPopoverCarouselPageCountRenderFunction; }; // @public (undocumented) -export const TeachingPopoverCarouselProvider: Provider & FC>; +export type TeachingPopoverCarouselPageCountRenderFunction = (currentPage: number, totalPages: number) => React_2.ReactNode; + +// @public (undocumented) +export type TeachingPopoverCarouselPageCountSlots = { + root: Slot<'div'>; +}; + +// @public +export type TeachingPopoverCarouselPageCountState = ComponentState & { + currentIndex: number; + totalPages: number; + renderPageCount: TeachingPopoverCarouselPageCountRenderFunction; +}; + +// @public +export type TeachingPopoverCarouselProps = ComponentProps & { + defaultValue?: string; + value?: string; + onValueChange?: EventHandler; + onFinish?: EventHandler; +}; // @public (undocumented) export type TeachingPopoverCarouselSlots = { root: NonNullable>; - footer: NonNullable>; - previous: Slot; - next: NonNullable>; - nav: Slot; - pageCount: Slot<'div'>; }; // @public -export type TeachingPopoverCarouselState = ComponentState & { - totalPages: number; - setCurrentPage: (page: number) => void; -} & Partial> & Pick & Required>; +export type TeachingPopoverCarouselState = ComponentState> & Partial> & CarouselContextValue; // @public export const TeachingPopoverFooter: ForwardRefComponent; @@ -174,9 +255,7 @@ export const TeachingPopoverFooter: ForwardRefComponent; // @public (undocumented) -export type TeachingPopoverFooterProps = ComponentProps> & { - strings: TeachingPopoverFooterStrings; -} & Pick; +export type TeachingPopoverFooterProps = ComponentProps & Pick; // @public (undocumented) export type TeachingPopoverFooterState = ComponentState & Pick & { @@ -265,17 +344,32 @@ export const useTeachingPopoverBodyStyles_unstable: (state: TeachingPopoverBodyS // @public (undocumented) export const useTeachingPopoverCarousel_unstable: (props: TeachingPopoverCarouselProps, ref: React_2.Ref) => TeachingPopoverCarouselState; -// @public (undocumented) -export const useTeachingPopoverCarouselContext_unstable: (selector: ContextSelector) => T; +// @public +export const useTeachingPopoverCarouselCard_unstable: (props: TeachingPopoverCarouselCardProps, ref: React_2.Ref) => TeachingPopoverCarouselCardState; + +// @public +export const useTeachingPopoverCarouselCardStyles_unstable: (state: TeachingPopoverCarouselCardState) => TeachingPopoverCarouselCardState; // @public (undocumented) export function useTeachingPopoverCarouselContextValues_unstable(state: TeachingPopoverCarouselState): TeachingPopoverCarouselContextValues; +// @public (undocumented) +export const useTeachingPopoverCarouselFooter_unstable: (props: TeachingPopoverCarouselFooterProps, ref: React_2.Ref) => TeachingPopoverCarouselFooterState; + +// @public +export const useTeachingPopoverCarouselFooterButton_unstable: (props: TeachingPopoverCarouselFooterButtonProps, ref: React_2.Ref) => TeachingPopoverCarouselFooterButtonState; + +// @public +export const useTeachingPopoverCarouselFooterButtonStyles_unstable: (state: TeachingPopoverCarouselFooterButtonState) => TeachingPopoverCarouselFooterButtonState; + +// @public +export const useTeachingPopoverCarouselFooterStyles_unstable: (state: TeachingPopoverCarouselFooterState) => TeachingPopoverCarouselFooterState; + // @public export const useTeachingPopoverCarouselNav_unstable: (props: TeachingPopoverCarouselNavProps, ref: React_2.Ref) => TeachingPopoverCarouselNavState; // @public -export const useTeachingPopoverCarouselNavButton_unstable: (props: TeachingPopoverCarouselNavButtonProps, ref: React_2.Ref) => TeachingPopoverCarouselNavButtonState; +export const useTeachingPopoverCarouselNavButton_unstable: (props: TeachingPopoverCarouselNavButtonProps, ref: React_2.Ref) => TeachingPopoverCarouselNavButtonState; // @public export const useTeachingPopoverCarouselNavButtonStyles_unstable: (state: TeachingPopoverCarouselNavButtonState) => TeachingPopoverCarouselNavButtonState; @@ -283,6 +377,12 @@ export const useTeachingPopoverCarouselNavButtonStyles_unstable: (state: Teachin // @public export const useTeachingPopoverCarouselNavStyles_unstable: (state: TeachingPopoverCarouselNavState) => TeachingPopoverCarouselNavState; +// @public +export const useTeachingPopoverCarouselPageCount_unstable: (props: TeachingPopoverCarouselPageCountProps, ref: React_2.Ref) => TeachingPopoverCarouselPageCountState; + +// @public +export const useTeachingPopoverCarouselPageCountStyles_unstable: (state: TeachingPopoverCarouselPageCountState) => TeachingPopoverCarouselPageCountState; + // @public export const useTeachingPopoverCarouselStyles_unstable: (state: TeachingPopoverCarouselState) => TeachingPopoverCarouselState; diff --git a/packages/react-components/react-teaching-popover-preview/package.json b/packages/react-components/react-teaching-popover-preview/package.json index 028ff30fd3e793..7953d3b31e06d4 100644 --- a/packages/react-components/react-teaching-popover-preview/package.json +++ b/packages/react-components/react-teaching-popover-preview/package.json @@ -47,7 +47,8 @@ "@fluentui/react-tabster": "^9.19.6", "@fluentui/react-icons": "^2.0.224", "@fluentui/react-aria": "^9.10.3", - "@fluentui/react-context-selector": "^9.1.57" + "@fluentui/react-context-selector": "^9.1.57", + "use-sync-external-store": "^1.2.0" }, "peerDependencies": { "@types/react": ">=16.8.0 <19.0.0", diff --git a/packages/react-components/react-teaching-popover-preview/src/TeachingPopoverCarouselCard.ts b/packages/react-components/react-teaching-popover-preview/src/TeachingPopoverCarouselCard.ts new file mode 100644 index 00000000000000..c780a698eb1673 --- /dev/null +++ b/packages/react-components/react-teaching-popover-preview/src/TeachingPopoverCarouselCard.ts @@ -0,0 +1 @@ +export * from './components/TeachingPopoverCarouselCard/index'; diff --git a/packages/react-components/react-teaching-popover-preview/src/TeachingPopoverCarouselFooter.ts b/packages/react-components/react-teaching-popover-preview/src/TeachingPopoverCarouselFooter.ts new file mode 100644 index 00000000000000..51e9373d69a9a1 --- /dev/null +++ b/packages/react-components/react-teaching-popover-preview/src/TeachingPopoverCarouselFooter.ts @@ -0,0 +1 @@ +export * from './components/TeachingPopoverCarouselFooter/index'; diff --git a/packages/react-components/react-teaching-popover-preview/src/TeachingPopoverCarouselFooterButton.ts b/packages/react-components/react-teaching-popover-preview/src/TeachingPopoverCarouselFooterButton.ts new file mode 100644 index 00000000000000..83d85652df48b9 --- /dev/null +++ b/packages/react-components/react-teaching-popover-preview/src/TeachingPopoverCarouselFooterButton.ts @@ -0,0 +1 @@ +export * from './components/TeachingPopoverCarouselFooterButton/index'; diff --git a/packages/react-components/react-teaching-popover-preview/src/TeachingPopoverCarouselPageCount.ts b/packages/react-components/react-teaching-popover-preview/src/TeachingPopoverCarouselPageCount.ts new file mode 100644 index 00000000000000..9116591a49ccdf --- /dev/null +++ b/packages/react-components/react-teaching-popover-preview/src/TeachingPopoverCarouselPageCount.ts @@ -0,0 +1 @@ +export * from './components/TeachingPopoverCarouselPageCount/index'; diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverBody/TeachingPopoverBody.types.ts b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverBody/TeachingPopoverBody.types.ts index b167dffa5fa4af..97b83c181722d8 100644 --- a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverBody/TeachingPopoverBody.types.ts +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverBody/TeachingPopoverBody.types.ts @@ -1,7 +1,5 @@ import { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; -export type TeachingPopoverBodyMediaLength = 'short' | 'medium' | 'tall'; - export type TeachingPopoverBodySlots = { /** * The element wrapping the buttons. @@ -14,8 +12,8 @@ export type TeachingPopoverBodySlots = { }; export type TeachingPopoverBodyProps = ComponentProps & { - mediaLength?: TeachingPopoverBodyMediaLength; + mediaLength?: 'short' | 'medium' | 'tall'; }; export type TeachingPopoverBodyState = ComponentState & - Partial>; + Required>; diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverBody/useTeachingPopoverBodyStyles.styles.ts b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverBody/useTeachingPopoverBodyStyles.styles.ts index ba57bcf7fe6063..1dcb6cb4426190 100644 --- a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverBody/useTeachingPopoverBodyStyles.styles.ts +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverBody/useTeachingPopoverBodyStyles.styles.ts @@ -18,28 +18,22 @@ export const useMediaStyles = makeStyles({ display: 'flex', }, short: { + aspectRatio: 288 / 117, '@supports not (aspect-ratio)': { height: '117px', }, - '@supports (aspect-ratio)': { - aspectRatio: 288 / 117, - }, }, medium: { + aspectRatio: 288 / 176, '@supports not (aspect-ratio)': { height: '176px', }, - '@supports (aspect-ratio)': { - aspectRatio: 288 / 176, - }, }, tall: { + aspectRatio: 288 / 288, '@supports not (aspect-ratio)': { height: '288px', }, - '@supports (aspect-ratio)': { - aspectRatio: 288 / 288, - }, }, }); @@ -53,7 +47,7 @@ const useStyles = makeStyles({ /** Applies style classnames to slots */ export const useTeachingPopoverBodyStyles_unstable = (state: TeachingPopoverBodyState) => { - const { mediaLength = 'medium' } = state; + const { mediaLength } = state; const styles = useStyles(); const mediaStyles = useMediaStyles(); diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/Carousel/Carousel.tsx b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/Carousel/Carousel.tsx new file mode 100644 index 00000000000000..a2e6f321ca1d81 --- /dev/null +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/Carousel/Carousel.tsx @@ -0,0 +1,141 @@ +import { + isHTMLElement, + useMergedRefs, + useControllableState, + type EventHandler, + useEventCallback, +} from '@fluentui/react-utilities'; +import * as React from 'react'; + +import { CAROUSEL_ITEM } from './constants'; +import { useCarouselWalker_unstable } from './useCarouselWalker'; +import { createCarouselStore } from './createCarouselStore'; +import type { CarouselValueChangeData } from './Carousel.types'; +import { CarouselContextValue } from './CarouselContext'; + +export type UseCarouselOptions = { + defaultValue?: string; + value?: string; + + onValueChange?: EventHandler; + onFinish?: EventHandler; +}; + +// TODO: Migrate this into an external @fluentui/carousel component +// For now, we won't export this publicly, is only for internal TeachingPopover use until stabilized. +export function useCarousel_unstable(options: UseCarouselOptions) { + const { onValueChange, onFinish } = options; + + const { ref: carouselRef, walker: carouselWalker } = useCarouselWalker_unstable(); + const [store] = React.useState(() => createCarouselStore()); + + const [value, setValue] = useControllableState({ + defaultState: options.defaultValue, + state: options.value, + initialState: null, + }); + const rootRef = React.useRef(null); + + if (process.env.NODE_ENV !== 'production') { + // eslint-disable-next-line react-hooks/rules-of-hooks + React.useEffect(() => { + if (value === null) { + // eslint-disable-next-line no-console + console.error( + 'useCarousel: Carousel needs to have a `defaultValue` or `value` prop set. If you want to control the value, use the `value` prop.', + ); + } + }, [value]); + } + + React.useEffect(() => { + const allItems = rootRef.current?.querySelectorAll(`[${CAROUSEL_ITEM}]`)!; + + for (let i = 0; i < allItems.length; i++) { + store.addValue(allItems.item(i).getAttribute(CAROUSEL_ITEM)!); + } + + return () => { + store.clear(); + }; + }, [store]); + + React.useEffect(() => { + const config: MutationObserverInit = { + attributes: true, + attributeFilter: [CAROUSEL_ITEM], + childList: true, + subtree: true, + }; + + // Callback function to execute when mutations are observed + const callback: MutationCallback = mutationList => { + for (const mutation of mutationList) { + for (const addedNode of Array.from(mutation.addedNodes)) { + if (isHTMLElement(addedNode) && addedNode.hasAttribute(CAROUSEL_ITEM)) { + const newValue = addedNode.getAttribute(CAROUSEL_ITEM)!; + const newNode = carouselWalker.find(newValue); + if (!newNode?.value) { + return; + } + + const previousNode = carouselWalker.prevPage(newNode?.value); + store.insertValue(newValue, previousNode?.value ?? null); + } + } + + for (const removedNode of Array.from(mutation.removedNodes)) { + if (isHTMLElement(removedNode) && removedNode?.hasAttribute(CAROUSEL_ITEM)) { + const removedValue = removedNode.getAttribute(CAROUSEL_ITEM)!; + + store.removeValue(removedValue); + } + } + } + }; + + // Create an observer instance linked to the callback function + const observer = new MutationObserver(callback); + + // Start observing the target node for configured mutations + observer.observe(rootRef.current!, config); + + // Later, you can stop observing + return () => { + observer.disconnect(); + }; + }, [carouselWalker, store]); + + const selectPageByDirection: CarouselContextValue['selectPageByDirection'] = useEventCallback((event, direction) => { + const active = carouselWalker.active(); + + if (!active?.value) { + return; + } + + const newPage = + direction === 'prev' ? carouselWalker.prevPage(active.value) : carouselWalker.nextPage(active.value); + + if (newPage) { + setValue(newPage?.value); + onValueChange?.(event, { event, type: 'click', value: newPage?.value }); + } else { + onFinish?.(event, { event, type: 'click', value: active?.value }); + } + }); + + const selectPageByValue: CarouselContextValue['selectPageByValue'] = useEventCallback((event, _value) => { + setValue(_value); + onValueChange?.(event, { event, type: 'click', value: _value }); + }); + + return { + carouselRef: useMergedRefs(rootRef, carouselRef), + carousel: { + store, + value, + selectPageByDirection, + selectPageByValue, + }, + }; +} diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/Carousel/Carousel.types.ts b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/Carousel/Carousel.types.ts new file mode 100644 index 00000000000000..1b99fabcf06a48 --- /dev/null +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/Carousel/Carousel.types.ts @@ -0,0 +1,23 @@ +import * as React from 'react'; +import { EventData } from '@fluentui/react-utilities'; + +export type CarouselStore = { + clear: () => void; + addValue: (value: string) => void; + insertValue: (value: string, prev: string | null) => void; + removeValue: (value: string) => void; + subscribe: (listener: () => void) => () => void; + getSnapshot: () => string[]; +}; + +export type CarouselItem = { + el: HTMLElement; + value: string | null; +}; + +export type CarouselValueChangeData = EventData<'click', React.MouseEvent> & { + /** + * The value to be set after event has occurred. + */ + value?: string; +}; diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/Carousel/CarouselContext.ts b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/Carousel/CarouselContext.ts new file mode 100644 index 00000000000000..46d3d4921efa8b --- /dev/null +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/Carousel/CarouselContext.ts @@ -0,0 +1,35 @@ +import { type Context, ContextSelector, createContext, useContextSelector } from '@fluentui/react-context-selector'; +import * as React from 'react'; + +import type { CarouselStore } from './Carousel.types'; +import { createCarouselStore } from './createCarouselStore'; + +export type CarouselContextValue = { + store: CarouselStore; + value: string | null; + selectPageByDirection: ( + event: React.MouseEvent, + direction: 'next' | 'prev', + ) => void; + selectPageByValue: (event: React.MouseEvent, value: string) => void; +}; + +export const carouselContextDefaultValue: CarouselContextValue = { + store: createCarouselStore(), + value: null, + selectPageByDirection: () => { + /** noop */ + }, + selectPageByValue: () => { + /** noop */ + }, +}; + +const CarouselContext: Context = createContext( + undefined, +) as Context; + +export const CarouselProvider = CarouselContext.Provider; + +export const useCarouselContext_unstable = (selector: ContextSelector): T => + useContextSelector(CarouselContext, (ctx = carouselContextDefaultValue) => selector(ctx)); diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/Carousel/CarouselItem/CarouselItem.types.ts b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/Carousel/CarouselItem/CarouselItem.types.ts new file mode 100644 index 00000000000000..5c840b7976c462 --- /dev/null +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/Carousel/CarouselItem/CarouselItem.types.ts @@ -0,0 +1,23 @@ +import { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; + +export type CarouselItemSlots = { + /** + * The element wrapping carousel pages and navigation. + */ + root: NonNullable>; +}; + +export type CarouselItemProps = ComponentProps & { + /** + * The value used to identify a page, + * it should be unique and is necessary for pagination + */ + value: string; +}; + +/** + * TeachingPopoverCarousel State and Context Hooks + */ +export type CarouselItemState = ComponentState> & { + visible: boolean; +} & Pick; diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/Carousel/CarouselItem/Carouseltem.tsx b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/Carousel/CarouselItem/Carouseltem.tsx new file mode 100644 index 00000000000000..004d4f67a25188 --- /dev/null +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/Carousel/CarouselItem/Carouseltem.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; + +import { CarouselItemProps } from './CarouselItem.types'; +import { useCarouselItem_unstable } from './useCarouselItem'; +import { renderCarouselItem_unstable } from './renderCarouselItem'; + +/** + * Define a CarouselItem, using the `useCarouselItem_unstable` and 'renderCarouselItem_unstable' hooks. + * It has no styling opinions. + */ +export const CarouselItem: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useCarouselItem_unstable(props, ref); + + return renderCarouselItem_unstable(state); +}); + +CarouselItem.displayName = 'CarouselItem'; diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/Carousel/CarouselItem/renderCarouselItem.tsx b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/Carousel/CarouselItem/renderCarouselItem.tsx new file mode 100644 index 00000000000000..e5c8028f0fd288 --- /dev/null +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/Carousel/CarouselItem/renderCarouselItem.tsx @@ -0,0 +1,14 @@ +/** @jsxRuntime automatic */ +/** @jsxImportSource @fluentui/react-jsx-runtime */ + +import { assertSlots } from '@fluentui/react-utilities'; +import type { CarouselItemState, CarouselItemSlots } from './CarouselItem.types'; + +/** + * Render the final JSX of TeachingPopoverCarousel + */ +export const renderCarouselItem_unstable = (state: CarouselItemState) => { + assertSlots(state); + + return ; +}; diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/Carousel/CarouselItem/useCarouselItem.ts b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/Carousel/CarouselItem/useCarouselItem.ts new file mode 100644 index 00000000000000..d63565c5fa5a3c --- /dev/null +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/Carousel/CarouselItem/useCarouselItem.ts @@ -0,0 +1,37 @@ +import * as React from 'react'; +import { getIntrinsicElementProps, slot } from '@fluentui/react-utilities'; +import { CarouselItemProps, CarouselItemState } from './CarouselItem.types'; +import { useCarouselContext_unstable } from '../CarouselContext'; +import { CAROUSEL_ACTIVE_ITEM, CAROUSEL_ITEM } from '../constants'; + +export const useCarouselItem_unstable = ( + props: CarouselItemProps, + ref: React.Ref, +): CarouselItemState => { + const { value } = props; + + const visible = useCarouselContext_unstable(c => c.value === value); + const state: CarouselItemState = { + value, + visible, + components: { + root: 'div', + }, + root: slot.always( + getIntrinsicElementProps('div', { + ref, + [CAROUSEL_ITEM]: value, + [CAROUSEL_ACTIVE_ITEM]: visible, + hidden: !visible, + ...props, + }), + { elementType: 'div' }, + ), + }; + + if (!visible) { + state.root.children = null; + } + + return state; +}; diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/Carousel/constants.ts b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/Carousel/constants.ts new file mode 100644 index 00000000000000..068a09d0e3c1ff --- /dev/null +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/Carousel/constants.ts @@ -0,0 +1,2 @@ +export const CAROUSEL_ITEM = 'data-carousel-item'; +export const CAROUSEL_ACTIVE_ITEM = 'data-carousel-active-item'; diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/Carousel/createCarouselStore.test.ts b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/Carousel/createCarouselStore.test.ts new file mode 100644 index 00000000000000..1b9077aafcee6b --- /dev/null +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/Carousel/createCarouselStore.test.ts @@ -0,0 +1,72 @@ +import { createCarouselStore } from './createCarouselStore'; + +describe('createCarouselStore', () => { + describe('addValue', () => { + it('adds a value', () => { + const store = createCarouselStore(); + store.addValue('foo'); + + expect(store.getSnapshot()).toEqual(['foo']); + }); + }); + + describe('insertValue', () => { + it('inserts a value', () => { + const store = createCarouselStore(); + + store.addValue('foo'); + store.addValue('bar'); + store.insertValue('baz', 'foo'); + + expect(store.getSnapshot()).toEqual(['foo', 'baz', 'bar']); + }); + }); + + describe('removeValue', () => { + it('removes a value', () => { + const store = createCarouselStore(); + + store.addValue('foo'); + store.addValue('bar'); + store.addValue('baz'); + store.removeValue('bar'); + + expect(store.getSnapshot()).toEqual(['foo', 'baz']); + }); + }); + + describe('clear', () => { + it('clears all values', () => { + const store = createCarouselStore(); + + store.addValue('foo'); + store.addValue('bar'); + store.clear(); + + expect(store.getSnapshot()).toEqual([]); + }); + }); + + describe('subscribe', () => { + it('subscribes to changes', () => { + const store = createCarouselStore(); + const listener = jest.fn(); + + store.subscribe(listener); + store.addValue('foo'); + + expect(listener).toHaveBeenCalled(); + }); + + it('unsubscribes from changes', () => { + const store = createCarouselStore(); + const listener = jest.fn(); + + const unsubscribe = store.subscribe(listener); + unsubscribe(); + store.addValue('foo'); + + expect(listener).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/Carousel/createCarouselStore.ts b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/Carousel/createCarouselStore.ts new file mode 100644 index 00000000000000..293b76fc00168a --- /dev/null +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/Carousel/createCarouselStore.ts @@ -0,0 +1,54 @@ +import { type CarouselStore } from './Carousel.types'; + +export const createCarouselStore = (): CarouselStore => { + let values: string[] = []; + let listeners: Array<() => void> = []; + + const carouselStore = { + clear() { + values = []; + emitChange(); + }, + addValue(value: string) { + values = [...values, value]; + + emitChange(); + }, + insertValue(value: string, prev: string | null) { + if (!prev) { + values = [value, ...values]; + } else { + const pos = values.indexOf(prev); + values.splice(pos + 1, 0, value); + // Required to be defined as a 'new' array for useSyncExternalStore + values = [...values]; + } + emitChange(); + }, + removeValue(value: string) { + const pos = values.indexOf(value); + values.splice(pos, 1); + // Required to be defined as a 'new' array for useSyncExternalStore + values = [...values]; + emitChange(); + }, + subscribe(listener: () => void) { + listeners = [...listeners, listener]; + + return () => { + listeners = listeners.filter(l => l !== listener); + }; + }, + getSnapshot() { + return values; + }, + }; + + function emitChange() { + for (const listener of listeners) { + listener(); + } + } + + return carouselStore; +}; diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/Carousel/useCarouselValues.ts b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/Carousel/useCarouselValues.ts new file mode 100644 index 00000000000000..ff551e37f31229 --- /dev/null +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/Carousel/useCarouselValues.ts @@ -0,0 +1,10 @@ +import { useSyncExternalStore } from 'use-sync-external-store/shim'; + +import { useCarouselContext_unstable } from './CarouselContext'; +import type { CarouselStore } from './Carousel.types'; + +export function useCarouselValues_unstable(getSnapshot: (values: ReturnType) => T): T { + const store = useCarouselContext_unstable(c => c.store); + + return useSyncExternalStore(store.subscribe, () => getSnapshot(store.getSnapshot())); +} diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/Carousel/useCarouselWalker.ts b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/Carousel/useCarouselWalker.ts new file mode 100644 index 00000000000000..96f8eefd2b43f4 --- /dev/null +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/Carousel/useCarouselWalker.ts @@ -0,0 +1,127 @@ +import * as React from 'react'; +import { isHTMLElement } from '@fluentui/react-utilities'; +import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts'; + +import { CAROUSEL_ACTIVE_ITEM, CAROUSEL_ITEM } from './constants'; + +export type CarouselWalker = { + find(value: string): { el: HTMLElement; value: string } | null; + nextPage(value: string): { el: HTMLElement; value: string } | null; + prevPage(value: string): { el: HTMLElement; value: string } | null; + active(): { el: HTMLElement; value: string } | null; +}; + +export const useCarouselWalker_unstable = () => { + const { targetDocument } = useFluent(); + + const treeWalkerRef = React.useRef(targetDocument?.createTreeWalker(targetDocument.body)); + const htmlRef = React.useRef(null); + + const ref = React.useCallback( + (el: HTMLDivElement | null) => { + if (!targetDocument) { + return; + } + + if (!el) { + return; + } + + htmlRef.current = el; + treeWalkerRef.current = targetDocument.createTreeWalker(el, NodeFilter.SHOW_ELEMENT, { + acceptNode(node) { + if (!isHTMLElement(node)) { + return NodeFilter.FILTER_SKIP; + } + + return node.hasAttribute(CAROUSEL_ITEM) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP; + }, + }); + }, + [targetDocument], + ); + + return { + ref, + walker: React.useMemo( + () => ({ + active() { + if (!htmlRef.current) { + return null; + } + + const activeEl = htmlRef.current.querySelector(`[${CAROUSEL_ACTIVE_ITEM}="true"]`)!; + + if (isHTMLElement(activeEl)) { + return { + el: activeEl, + value: activeEl.getAttribute(CAROUSEL_ITEM)!, + }; + } + + return null; + }, + find(value: string) { + if (!treeWalkerRef.current?.currentNode || !htmlRef.current) { + return null; + } + + treeWalkerRef.current.currentNode = htmlRef.current; + let nextNode: Node | null = null; + while ((nextNode = treeWalkerRef.current.nextNode())) { + if (!isHTMLElement(nextNode)) { + continue; + } + + if (nextNode.getAttribute(CAROUSEL_ITEM) === value) { + return { + el: nextNode, + value: nextNode.getAttribute(CAROUSEL_ITEM)!, + }; + } + } + + return null; + }, + nextPage(value: string) { + const res = this.find(value); + if (!res || !treeWalkerRef.current?.currentNode) { + return null; + } + + treeWalkerRef.current.currentNode = res.el; + const next = treeWalkerRef.current.nextNode(); + + if (isHTMLElement(next)) { + return { + el: next, + value: next.getAttribute(CAROUSEL_ITEM)!, + }; + } + + return null; + }, + + prevPage(value: string) { + const res = this.find(value); + if (!res || !treeWalkerRef.current?.currentNode) { + return null; + } + + treeWalkerRef.current.currentNode = res.el; + const next = treeWalkerRef.current.previousNode(); + + if (isHTMLElement(next)) { + return { + el: next, + value: next.getAttribute(CAROUSEL_ITEM)!, + }; + } + + return null; + }, + }), + [], + ), + }; +}; diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/TeachingPopoverCarousel.test.tsx b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/TeachingPopoverCarousel.test.tsx index 00be451929c6d7..8abb9be461ea5d 100644 --- a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/TeachingPopoverCarousel.test.tsx +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/TeachingPopoverCarousel.test.tsx @@ -8,12 +8,7 @@ describe('TeachingPopoverCarousel', () => { Component: TeachingPopoverCarousel, displayName: 'TeachingPopoverCarousel', requiredProps: { - strings: { - next: 'Next', - previous: 'Previous', - initialStepText: 'Close', - finalStepText: 'Finish', - }, + defaultValue: '', }, disabledTests: ['component-has-static-classnames-object'], }); @@ -22,17 +17,7 @@ describe('TeachingPopoverCarousel', () => { it('renders a default state', () => { const result = render( - - Default TeachingPopoverCarousel - , + Default TeachingPopoverCarousel, ); expect(result.container).toMatchSnapshot(); }); diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/TeachingPopoverCarousel.tsx b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/TeachingPopoverCarousel.tsx index 64e6c7576b426b..691938783a35f1 100644 --- a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/TeachingPopoverCarousel.tsx +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/TeachingPopoverCarousel.tsx @@ -1,11 +1,12 @@ import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; + import { useTeachingPopoverCarousel_unstable } from './useTeachingPopoverCarousel'; import { renderTeachingPopoverCarousel_unstable } from './renderTeachingPopoverCarousel'; import { useTeachingPopoverCarouselStyles_unstable } from './useTeachingPopoverCarouselStyles.styles'; import { useCustomStyleHook_unstable } from '@fluentui/react-shared-contexts'; import type { TeachingPopoverCarouselProps } from './TeachingPopoverCarousel.types'; -import type { ForwardRefComponent } from '@fluentui/react-utilities'; -import { useTeachingPopoverCarouselContextValues_unstable } from './TeachingPopoverCarouselContext'; +import { useTeachingPopoverCarouselContextValues_unstable } from './useTeachingPopoverCarouselContextValues'; /** * Define a styled TeachingPopoverCarousel, using the `useTeachingPopoverCarousel_unstable` and `useTeachingPopoverCarouselStyles_unstable` diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/TeachingPopoverCarousel.types.ts b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/TeachingPopoverCarousel.types.ts index c274255a0ab70e..9b2d22514790fc 100644 --- a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/TeachingPopoverCarousel.types.ts +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/TeachingPopoverCarousel.types.ts @@ -1,112 +1,51 @@ -import * as React from 'react'; -import { ComponentProps, ComponentState, EventData, EventHandler, Slot } from '@fluentui/react-utilities'; -import { Button } from '@fluentui/react-button'; -import { PopoverContextValue } from '@fluentui/react-popover'; -import { TeachingPopoverCarouselNavProps } from '../TeachingPopoverCarouselNav/index'; +import type { ComponentProps, ComponentState, EventHandler, Slot } from '@fluentui/react-utilities'; +import { type PopoverContextValue } from '@fluentui/react-popover'; + +import { type CarouselContextValue } from './Carousel/CarouselContext'; +import type { CarouselValueChangeData } from './Carousel/Carousel.types'; export type TeachingPopoverCarouselSlots = { /** * The element wrapping carousel pages and navigation. */ root: NonNullable>; - - /** - * The element wrapping the navigation of the carousel. - */ - footer: NonNullable>; - - /** - * The previous button slot. - */ - previous: Slot; - - /** - * The next button slot. - */ - next: NonNullable>; - - /** - * The page nav slot when using icon navigation. - */ - nav: Slot; - - /** - * The page count text slot for paginationType text - */ - pageCount: Slot<'div'>; -}; - -export type TeachingPopoverCarouselLayout = 'offset' | 'centered'; - -// For localization or customization, users may want to modify this for their own purposes -export type TeachingPopoverPageCountChildRenderFunction = (currentPage: number, totalPages: number) => React.ReactNode; -export type TeachingPopoverPageCountRenderType = TeachingPopoverPageCountChildRenderFunction | string; -export type TeachingPopoverStrings = { - previous: string; - initialStepText: string; - next: string; - finalStepText: string; - /** - * Localize the separator between numbers, or use a function to fully override - */ - pageCountText?: TeachingPopoverPageCountRenderType; -}; - -export type TeachingPopoverPageChangeData = EventData<'click', React.MouseEvent> & { - /** - * The page to be set after event has occurred. - */ - currentPage: number; }; /** * TeachingPopoverCarousel Props */ -export type TeachingPopoverCarouselProps = Partial> & { +export type TeachingPopoverCarouselProps = ComponentProps & { /** - * Controls whether buttons will be centered (balanced) or right aligned - * Defaults to 'centered'. + * The initial page to display in uncontrolled mode. */ - layout?: TeachingPopoverCarouselLayout; + defaultValue?: string; /** - * Dictates whether pagination uses text or icons - * Defaults to icons + * The value of the currently active page. */ - paginationType?: 'text' | 'icon'; + value?: string; /** - * Strings used to localize carousel functionality + * Callback to notify a page change. */ - strings: TeachingPopoverStrings; - - /** - * Page at which carousel should be initialized to - */ - defaultCurrentPage?: number; - - /** - * Callback to notify a page change (can be used to update 'currentPage' externally). - */ - onPageChange?: EventHandler; + onValueChange?: EventHandler; /** * Callback to notify when the final button step of a carousel has been activated. */ - onFinish?: EventHandler; - - /** - * Controllable page state - */ - currentPage?: number; + onFinish?: EventHandler; }; /** * TeachingPopoverCarousel State and Context Hooks */ -export type TeachingPopoverCarouselState = ComponentState & { - totalPages: number; - setCurrentPage: (page: number) => void; -} & Partial> & - Pick & - Required>; +export type TeachingPopoverCarouselState = ComponentState> & + Partial> & + CarouselContextValue; + +/** + * Context shared between TeachingPopoverCarousel and its children components + */ +export type TeachingPopoverCarouselContextValues = { + carousel: CarouselContextValue; +}; diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/TeachingPopoverCarouselContext.ts b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/TeachingPopoverCarouselContext.ts deleted file mode 100644 index fbd121692e98bd..00000000000000 --- a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/TeachingPopoverCarouselContext.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { createContext, useContextSelector } from '@fluentui/react-context-selector'; -import type { ContextSelector, Context } from '@fluentui/react-context-selector'; -import { TeachingPopoverCarouselState } from './TeachingPopoverCarousel.types'; - -const TeachingPopoverCarouselContext: Context = createContext< - TeachingPopoverCarouselContextValue | undefined ->(undefined) as Context; - -const teachingPopoverCarouselContextDefaultValue: TeachingPopoverCarouselContextValue = { - currentPage: 0, - setCurrentPage: () => null, - totalPages: 1, //'One-pager' default state until proven otherwise - onPageChange: () => null, -}; - -export const TeachingPopoverCarouselProvider = TeachingPopoverCarouselContext.Provider; - -/** - * Context shared between TeachingPopoverCarousel and its children components - */ -export type TeachingPopoverCarouselContextValues = { - carousel: TeachingPopoverCarouselContextValue; -}; - -/** - * Context shared between TeachingPopoverCarousel and its children components - */ -export type TeachingPopoverCarouselContextValue = Pick< - TeachingPopoverCarouselState, - 'currentPage' | 'setCurrentPage' | 'totalPages' | 'onPageChange' ->; - -export const useTeachingPopoverCarouselContext_unstable = ( - selector: ContextSelector, -): T => - useContextSelector(TeachingPopoverCarouselContext, (ctx = teachingPopoverCarouselContextDefaultValue) => - selector(ctx), - ); - -export function useTeachingPopoverCarouselContextValues_unstable( - state: TeachingPopoverCarouselState, -): TeachingPopoverCarouselContextValues { - const { currentPage, setCurrentPage, totalPages, onPageChange } = state; - - const carousel = { - currentPage, - setCurrentPage, - totalPages, - onPageChange, - }; - - return { carousel }; -} diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/__snapshots__/TeachingPopoverCarousel.test.tsx.snap b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/__snapshots__/TeachingPopoverCarousel.test.tsx.snap index 5985e856da4d85..2dc532283fcdf4 100644 --- a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/__snapshots__/TeachingPopoverCarousel.test.tsx.snap +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/__snapshots__/TeachingPopoverCarousel.test.tsx.snap @@ -6,36 +6,6 @@ exports[`TeachingPopoverCarousel renders a default state 1`] = ` class="fui-TeachingPopoverCarousel" > Default TeachingPopoverCarousel - `; diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/index.ts b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/index.ts index 68d5c1dc5ed2a1..ab3a20288b31a1 100644 --- a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/index.ts +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/index.ts @@ -3,4 +3,4 @@ export * from './TeachingPopoverCarousel.types'; export * from './renderTeachingPopoverCarousel'; export * from './useTeachingPopoverCarousel'; export * from './useTeachingPopoverCarouselStyles.styles'; -export * from './TeachingPopoverCarouselContext'; +export * from './useTeachingPopoverCarouselContextValues'; diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/renderTeachingPopoverCarousel.tsx b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/renderTeachingPopoverCarousel.tsx index 50404a7a6ec487..1257a575cde1b7 100644 --- a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/renderTeachingPopoverCarousel.tsx +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/renderTeachingPopoverCarousel.tsx @@ -1,12 +1,13 @@ /** @jsxRuntime automatic */ /** @jsxImportSource @fluentui/react-jsx-runtime */ -import type { TeachingPopoverCarouselState } from './TeachingPopoverCarousel.types'; -import { TeachingPopoverCarouselSlots } from './TeachingPopoverCarousel.types'; + import { assertSlots } from '@fluentui/react-utilities'; -import { +import type { + TeachingPopoverCarouselState, + TeachingPopoverCarouselSlots, TeachingPopoverCarouselContextValues, - TeachingPopoverCarouselProvider, -} from './TeachingPopoverCarouselContext'; +} from './TeachingPopoverCarousel.types'; +import { CarouselProvider } from './Carousel/CarouselContext'; /** * Render the final JSX of TeachingPopoverCarousel @@ -17,20 +18,9 @@ export const renderTeachingPopoverCarousel_unstable = ( ) => { assertSlots(state); - const { layout } = state; - return ( - - - {state.root.children} - - {layout === 'centered' && state.previous && } - {state.nav && } - {state.pageCount && } - {layout === 'offset' && state.previous && } - - - - + + + ); }; diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/useTeachingPopoverCarousel.ts b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/useTeachingPopoverCarousel.ts index f1c417ca8b8607..b9c020dd8d0389 100644 --- a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/useTeachingPopoverCarousel.ts +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/useTeachingPopoverCarousel.ts @@ -1,168 +1,39 @@ import * as React from 'react'; -import { - getIntrinsicElementProps, - mergeCallbacks, - slot, - useControllableState, - useEventCallback, -} from '@fluentui/react-utilities'; -import type { - TeachingPopoverCarouselProps, - TeachingPopoverCarouselState, - TeachingPopoverPageCountChildRenderFunction, -} from './TeachingPopoverCarousel.types'; -import { Button } from '@fluentui/react-button'; -import { useState } from 'react'; +import { getIntrinsicElementProps, slot, useEventCallback, useMergedRefs } from '@fluentui/react-utilities'; +import type { TeachingPopoverCarouselProps, TeachingPopoverCarouselState } from './TeachingPopoverCarousel.types'; import { usePopoverContext_unstable } from '@fluentui/react-popover'; -import { TeachingPopoverCarouselNav } from '../TeachingPopoverCarouselNav/TeachingPopoverCarouselNav'; +import { useCarousel_unstable, type UseCarouselOptions } from './Carousel/Carousel'; export const useTeachingPopoverCarousel_unstable = ( props: TeachingPopoverCarouselProps, ref: React.Ref, ): TeachingPopoverCarouselState => { - const { layout = 'centered', paginationType = 'icon', strings, onPageChange, onFinish } = props; - - const appearance = usePopoverContext_unstable(context => context.appearance); - const triggerRef = usePopoverContext_unstable(context => context.triggerRef); - const toggleOpen = usePopoverContext_unstable(context => context.toggleOpen); - - const reactChildArray = React.Children.toArray(props.children); - - const [totalPages, setTotalPages] = useState(reactChildArray.length); - - // ToDo: Imperative setCurrentPage hook - const [currentPage, setCurrentPage] = useControllableState({ - initialState: 0, - defaultState: props.defaultCurrentPage, - state: props.currentPage, + const toggleOpen = usePopoverContext_unstable(c => c.toggleOpen); + const handleFinish: UseCarouselOptions['onFinish'] = useEventCallback((event, data) => { + props.onFinish?.(event, data); + toggleOpen(event as React.MouseEvent); }); - React.useEffect(() => { - // Update total pages if child length changes. - if (totalPages !== reactChildArray.length) { - setTotalPages(reactChildArray.length); - } - }, [reactChildArray.length, totalPages, setTotalPages]); - - // Get current pagination child (w/ array safety) - const currentPageElement = - currentPage < reactChildArray.length ? reactChildArray[currentPage] : reactChildArray[reactChildArray.length - 1]; - - const handleNextButtonClick = useEventCallback( - (event: React.MouseEvent) => { - const nextPage = Math.min(currentPage + 1, totalPages); - if (event.isDefaultPrevented()) { - return; - } - - if (nextPage >= totalPages) { - onFinish?.(event, { event, type: 'click', currentPage }); - if (triggerRef.current) { - triggerRef.current.focus(); - } - toggleOpen(event); - } else { - onPageChange?.(event, { event, type: 'click', currentPage: nextPage }); - setCurrentPage(nextPage); - } - }, - ); - - const handlePrevButtonClick = useEventCallback( - (event: React.MouseEvent) => { - const prevPage = Math.max(currentPage - 1, 0); - if (event.isDefaultPrevented()) { - return; - } - - if (currentPage <= 0) { - if (triggerRef.current) { - triggerRef.current.focus(); - } - toggleOpen(event); - } else { - onPageChange?.(event, { event, type: 'click', currentPage: prevPage }); - setCurrentPage(prevPage); - } - }, - ); - - const previous = slot.optional(props.previous, { - defaultProps: { - appearance: appearance === 'brand' ? 'outline' : undefined, - children: currentPage === 0 ? strings.initialStepText : strings.previous, - }, - renderByDefault: true, - elementType: Button, - }); - - // Merge any provided callback with previous button - if (previous) { - previous.onClick = mergeCallbacks(previous?.onClick, handlePrevButtonClick); - } - - const next = slot.always(props.next, { - defaultProps: { - appearance: appearance === 'brand' ? undefined : 'primary', - children: currentPage === totalPages - 1 ? strings.finalStepText : strings.next, - onClick: handleNextButtonClick, - }, - elementType: Button, - }); - - // Merge any provided callback with next button - if (next) { - next.onClick = mergeCallbacks(next?.onClick, handleNextButtonClick); - } - - const nav = slot.optional(props.nav, { - defaultProps: {}, - renderByDefault: paginationType === 'icon', - elementType: TeachingPopoverCarouselNav, - }); - - const footer = slot.always(props.footer, { elementType: 'div' }); - - let pageCountTextChildren; - if (typeof props.strings.pageCountText === 'function') { - const renderFunc = props.children as TeachingPopoverPageCountChildRenderFunction; - pageCountTextChildren = { children: renderFunc(currentPage, totalPages) }; - } else { - pageCountTextChildren = { children: `${currentPage + 1} ${props.strings.pageCountText ?? '/'} ${totalPages}` }; - } - const pageCount = slot.optional(props.nav, { - defaultProps: { ...pageCountTextChildren }, - renderByDefault: paginationType === 'text', - elementType: TeachingPopoverCarouselNav, + const { carousel, carouselRef } = useCarousel_unstable({ + defaultValue: props.defaultValue, + value: props.value, + onValueChange: props.onValueChange, + onFinish: handleFinish, }); + const appearance = usePopoverContext_unstable(context => context.appearance); return { appearance, - currentPage, - setCurrentPage, - onPageChange, - totalPages, - layout, components: { root: 'div', - footer: 'div', - next: Button, - previous: Button, - nav: TeachingPopoverCarouselNav, - pageCount: 'div', }, root: slot.always( getIntrinsicElementProps('div', { - ref, + ref: useMergedRefs(ref, carouselRef), ...props, - children: currentPageElement, }), { elementType: 'div' }, ), - previous, - next, - nav, - footer, - pageCount, + ...carousel, }; }; diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/useTeachingPopoverCarouselContextValues.ts b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/useTeachingPopoverCarouselContextValues.ts new file mode 100644 index 00000000000000..19e2a489b40751 --- /dev/null +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/useTeachingPopoverCarouselContextValues.ts @@ -0,0 +1,19 @@ +import type { + TeachingPopoverCarouselContextValues, + TeachingPopoverCarouselState, +} from './TeachingPopoverCarousel.types'; + +export function useTeachingPopoverCarouselContextValues_unstable( + state: TeachingPopoverCarouselState, +): TeachingPopoverCarouselContextValues { + const { store, value, selectPageByValue, selectPageByDirection } = state; + + const carousel = { + store, + value, + selectPageByDirection, + selectPageByValue, + }; + + return { carousel }; +} diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/useTeachingPopoverCarouselStyles.styles.ts b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/useTeachingPopoverCarouselStyles.styles.ts index 7d930ce35792d1..7965d4de1233cc 100644 --- a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/useTeachingPopoverCarouselStyles.styles.ts +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarousel/useTeachingPopoverCarouselStyles.styles.ts @@ -1,109 +1,21 @@ -import { makeStyles, mergeClasses, shorthands } from '@griffel/react'; +import { makeStyles, mergeClasses } from '@griffel/react'; import type { TeachingPopoverCarouselSlots, TeachingPopoverCarouselState } from './TeachingPopoverCarousel.types'; import type { SlotClassNames } from '@fluentui/react-utilities'; -import { tokens } from '@fluentui/react-theme'; export const teachingPopoverCarouselClassNames: SlotClassNames = { root: 'fui-TeachingPopoverCarousel', - footer: 'fui-TeachingPopoverCarousel__footer', - previous: 'fui-TeachingPopoverCarousel__previous', - next: 'fui-TeachingPopoverCarousel__next', - nav: 'fui-TeachingPopoverCarousel__nav', - pageCount: 'fui-TeachingPopoverCarousel__pageCount', }; // Todo: Page change animation & styles const useStyles = makeStyles({ root: {}, - footer: { - display: 'flex', - flexDirection: 'row', - }, - footerCentered: { - justifyContent: 'space-between', - ...shorthands.gap('8px'), - }, - footerRightAligned: { - ...shorthands.gap('8px'), - ':nth-child(0)': { - marginRight: 'auto', - }, - }, - previous: { - minWidth: '96px', - }, - next: { - minWidth: '96px', - }, - brandNext: { - color: tokens.colorBrandForeground1, - backgroundColor: tokens.colorNeutralForegroundOnBrand, - ...shorthands.borderColor(tokens.colorTransparentBackground), - ':hover': { - color: tokens.colorCompoundBrandForeground1Hover, - backgroundColor: tokens.colorNeutralForegroundOnBrand, - }, - ':hover:active': { - color: tokens.colorCompoundBrandForeground1Pressed, - backgroundColor: tokens.colorNeutralForegroundOnBrand, - }, - }, - brandPrevious: { - // In brand, this is always 'NeutralForegroundOnBrand' - color: tokens.colorNeutralForegroundOnBrand, - backgroundColor: tokens.colorBrandBackground, - ...shorthands.borderColor(tokens.colorNeutralForegroundOnBrand), - ':hover': { - color: tokens.colorNeutralForegroundOnBrand, - ...shorthands.borderColor(tokens.colorNeutralForegroundOnBrand), - backgroundColor: tokens.colorBrandBackgroundHover, - }, - ':hover:active': { - color: tokens.colorNeutralForegroundOnBrand, - ...shorthands.borderColor(tokens.colorNeutralForegroundOnBrand), - backgroundColor: tokens.colorBrandBackgroundPressed, - }, - }, - nav: {}, - pageCount: { display: 'flex', color: tokens.colorNeutralForeground3, justifyContent: 'center', alignItems: 'center' }, }); /** Applies style classnames to slots */ export const useTeachingPopoverCarouselStyles_unstable = (state: TeachingPopoverCarouselState) => { const styles = useStyles(); - const { appearance, layout } = state; state.root.className = mergeClasses(teachingPopoverCarouselClassNames.root, styles.root, state.root.className); - state.footer.className = mergeClasses( - teachingPopoverCarouselClassNames.footer, - styles.footer, - layout === 'centered' ? styles.footerCentered : styles.footerRightAligned, - state.footer.className, - ); - if (state.previous) { - state.previous.className = mergeClasses( - teachingPopoverCarouselClassNames.previous, - styles.previous, - appearance === 'brand' && styles.brandPrevious, - state.previous.className, - ); - } - state.next.className = mergeClasses( - teachingPopoverCarouselClassNames.next, - styles.next, - appearance === 'brand' && styles.brandNext, - state.next.className, - ); - if (state.nav) { - state.nav.className = mergeClasses(teachingPopoverCarouselClassNames.nav, styles.nav, state.nav.className); - } - if (state.pageCount) { - state.pageCount.className = mergeClasses( - teachingPopoverCarouselClassNames.pageCount, - styles.pageCount, - state.pageCount.className, - ); - } return state; }; diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselCard/TeachingPopoverCarouselCard.test.tsx b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselCard/TeachingPopoverCarouselCard.test.tsx new file mode 100644 index 00000000000000..1a29060aa6ad85 --- /dev/null +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselCard/TeachingPopoverCarouselCard.test.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../testing/isConformant'; +import { TeachingPopoverCarouselCard } from './TeachingPopoverCarouselCard'; + +describe('TeachingPopoverCarouselCard', () => { + isConformant({ + Component: TeachingPopoverCarouselCard, + displayName: 'TeachingPopoverCarouselCard', + requiredProps: { value: 'test' }, + disabledTests: [ + // Carousel card does not render DOM elements + 'component-handles-ref', + 'component-has-root-ref', + 'component-has-static-classnames', + 'component-handles-classname', + 'component-has-static-classnames-object', + ], + }); + + it('renders a default state', () => { + const result = render( + Default TeachingPopoverCarouselCard, + ); + expect(result.container).toMatchSnapshot(); + }); +}); diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselCard/TeachingPopoverCarouselCard.tsx b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselCard/TeachingPopoverCarouselCard.tsx new file mode 100644 index 00000000000000..ed4c40278cb0ce --- /dev/null +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselCard/TeachingPopoverCarouselCard.tsx @@ -0,0 +1,22 @@ +import * as React from 'react'; +import { useTeachingPopoverCarouselCard_unstable } from './useTeachingPopoverCarouselCard'; +import { renderTeachingPopoverCarouselCard_unstable } from './renderTeachingPopoverCarouselCard'; +import type { TeachingPopoverCarouselCardProps } from './TeachingPopoverCarouselCard.types'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { useTeachingPopoverCarouselCardStyles_unstable } from './useTeachingPopoverCarouselCardStyles.styles'; + +/** + * Define a styled TeachingPopoverCarouselCard, using the `useTeachingPopoverCarouselCard_unstable` and `useTeachingPopoverCarouselCardStyles_unstable` + * hooks. + */ +export const TeachingPopoverCarouselCard: ForwardRefComponent = React.forwardRef( + (props, ref) => { + const state = useTeachingPopoverCarouselCard_unstable(props, ref); + + useTeachingPopoverCarouselCardStyles_unstable(state); + + return renderTeachingPopoverCarouselCard_unstable(state); + }, +); + +TeachingPopoverCarouselCard.displayName = 'TeachingPopoverCarouselCard'; diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselCard/TeachingPopoverCarouselCard.types.ts b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselCard/TeachingPopoverCarouselCard.types.ts new file mode 100644 index 00000000000000..37ed889af532b8 --- /dev/null +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselCard/TeachingPopoverCarouselCard.types.ts @@ -0,0 +1,17 @@ +import { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; +import { CarouselItemProps } from '../TeachingPopoverCarousel/Carousel/CarouselItem/CarouselItem.types'; + +export type TeachingPopoverCarouselCardSlots = { + /** + * The element wrapping the buttons. + */ + root: NonNullable>; +}; + +export type TeachingPopoverCarouselCardProps = ComponentProps & { + /* Required: Passed into CarouselItem to identify pages. */ + value: string; +}; + +export type TeachingPopoverCarouselCardState = ComponentState & + Required>; diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselCard/__snapshots__/TeachingPopoverCarouselCard.test.tsx.snap b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselCard/__snapshots__/TeachingPopoverCarouselCard.test.tsx.snap new file mode 100644 index 00000000000000..3a49ea34f7f1c0 --- /dev/null +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselCard/__snapshots__/TeachingPopoverCarouselCard.test.tsx.snap @@ -0,0 +1,12 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TeachingPopoverCarouselCard renders a default state 1`] = ` +
+ +`; diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselCard/index.ts b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselCard/index.ts new file mode 100644 index 00000000000000..68e6ab96853214 --- /dev/null +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselCard/index.ts @@ -0,0 +1,5 @@ +export * from './TeachingPopoverCarouselCard'; +export * from './TeachingPopoverCarouselCard.types'; +export * from './renderTeachingPopoverCarouselCard'; +export * from './useTeachingPopoverCarouselCard'; +export * from './useTeachingPopoverCarouselCardStyles.styles'; diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselCard/renderTeachingPopoverCarouselCard.tsx b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselCard/renderTeachingPopoverCarouselCard.tsx new file mode 100644 index 00000000000000..39c6284865a961 --- /dev/null +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselCard/renderTeachingPopoverCarouselCard.tsx @@ -0,0 +1,14 @@ +/** @jsxRuntime automatic */ +/** @jsxImportSource @fluentui/react-jsx-runtime */ +import type { TeachingPopoverCarouselCardState } from './TeachingPopoverCarouselCard.types'; +import { TeachingPopoverCarouselCardSlots } from './TeachingPopoverCarouselCard.types'; +import { assertSlots } from '@fluentui/react-utilities'; + +/** + * Render the final JSX of TeachingPopoverCarouselCard + */ +export const renderTeachingPopoverCarouselCard_unstable = (state: TeachingPopoverCarouselCardState) => { + assertSlots(state); + + return ; +}; diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselCard/useTeachingPopoverCarouselCard.ts b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselCard/useTeachingPopoverCarouselCard.ts new file mode 100644 index 00000000000000..b4431b0ba3d6b3 --- /dev/null +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselCard/useTeachingPopoverCarouselCard.ts @@ -0,0 +1,37 @@ +import { getIntrinsicElementProps, slot } from '@fluentui/react-utilities'; +import * as React from 'react'; + +import type { + TeachingPopoverCarouselCardProps, + TeachingPopoverCarouselCardState, +} from './TeachingPopoverCarouselCard.types'; +import { CarouselItem } from '../TeachingPopoverCarousel/Carousel/CarouselItem/Carouseltem'; + +/** + * Returns the props and state required to render the component + * @param props - TeachingPopoverCarouselCard properties + * @param ref - reference to root HTMLElement of TeachingPopoverCarouselCard + */ +export const useTeachingPopoverCarouselCard_unstable = ( + props: TeachingPopoverCarouselCardProps, + ref: React.Ref, +): TeachingPopoverCarouselCardState => { + const { value } = props; + + return { + value, + components: { + root: CarouselItem, + }, + root: slot.always( + { + ...getIntrinsicElementProps('div', { + ref, + ...props, + }), + value, + }, + { elementType: CarouselItem }, + ), + }; +}; diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselCard/useTeachingPopoverCarouselCardStyles.styles.ts b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselCard/useTeachingPopoverCarouselCardStyles.styles.ts new file mode 100644 index 00000000000000..3c783e2cbf0fa2 --- /dev/null +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselCard/useTeachingPopoverCarouselCardStyles.styles.ts @@ -0,0 +1,23 @@ +import { makeStyles, mergeClasses } from '@griffel/react'; +import type { + TeachingPopoverCarouselCardSlots, + TeachingPopoverCarouselCardState, +} from './TeachingPopoverCarouselCard.types'; +import type { SlotClassNames } from '@fluentui/react-utilities'; + +export const teachingPopoverCarouselCardClassNames: SlotClassNames = { + root: 'fui-TeachingPopoverCarouselCard', +}; + +const useStyles = makeStyles({ + root: {}, +}); + +/** Applies style classnames to slots */ +export const useTeachingPopoverCarouselCardStyles_unstable = (state: TeachingPopoverCarouselCardState) => { + const styles = useStyles(); + + state.root.className = mergeClasses(teachingPopoverCarouselCardClassNames.root, styles.root, state.root.className); + + return state; +}; diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooter/TeachingPopoverCarouselFooter.test.tsx b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooter/TeachingPopoverCarouselFooter.test.tsx new file mode 100644 index 00000000000000..a15bfd4cbae3c6 --- /dev/null +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooter/TeachingPopoverCarouselFooter.test.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../testing/isConformant'; +import { TeachingPopoverCarouselFooter } from './TeachingPopoverCarouselFooter'; + +describe('TeachingPopoverCarouselFooter', () => { + isConformant({ + Component: TeachingPopoverCarouselFooter, + displayName: 'TeachingPopoverCarouselFooter', + requiredProps: { + next: 'Next', + previous: 'Previous', + initialStepText: 'Close', + finalStepText: 'Finish', + }, + disabledTests: ['component-has-static-classnames-object'], + }); + + it('renders a default state', () => { + const result = render( + + Default TeachingPopoverCarouselFooter + , + ); + expect(result.container).toMatchSnapshot(); + }); +}); diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooter/TeachingPopoverCarouselFooter.tsx b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooter/TeachingPopoverCarouselFooter.tsx new file mode 100644 index 00000000000000..1da75bb3b2def2 --- /dev/null +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooter/TeachingPopoverCarouselFooter.tsx @@ -0,0 +1,22 @@ +import * as React from 'react'; +import { useTeachingPopoverCarouselFooter_unstable } from './useTeachingPopoverCarouselFooter'; +import { renderTeachingPopoverCarouselFooter_unstable } from './renderTeachingPopoverCarouselFooter'; +import { useTeachingPopoverCarouselFooterStyles_unstable } from './useTeachingPopoverCarouselFooterStyles.styles'; +import type { TeachingPopoverCarouselFooterProps } from './TeachingPopoverCarouselFooter.types'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; + +/** + * Define a styled TeachingPopoverCarouselFooter, using the `useTeachingPopoverCarouselFooter_unstable` and `useTeachingPopoverCarouselFooterStyles_unstable` + * hooks. + */ +export const TeachingPopoverCarouselFooter: ForwardRefComponent = React.forwardRef( + (props, ref) => { + const state = useTeachingPopoverCarouselFooter_unstable(props, ref); + + useTeachingPopoverCarouselFooterStyles_unstable(state); + + return renderTeachingPopoverCarouselFooter_unstable(state); + }, +); + +TeachingPopoverCarouselFooter.displayName = 'TeachingPopoverCarouselFooter'; diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooter/TeachingPopoverCarouselFooter.types.ts b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooter/TeachingPopoverCarouselFooter.types.ts new file mode 100644 index 00000000000000..965a1b01aa7fea --- /dev/null +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooter/TeachingPopoverCarouselFooter.types.ts @@ -0,0 +1,52 @@ +import * as React from 'react'; +import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; +import type { TeachingPopoverCarouselFooterButtonProps } from '../TeachingPopoverCarouselFooterButton/TeachingPopoverCarouselFooterButton.types'; + +export type TeachingPopoverCarouselFooterSlots = { + /** + * The element wrapping carousel pages and navigation. + */ + root: NonNullable>; + + /** + * The previous button slot. + */ + previous?: Slot; + + /** + * The next button slot. + */ + next: NonNullable>; +}; + +export type TeachingPopoverCarouselFooterLayout = 'offset' | 'centered'; + +// For localization or customization, users may want to modify this for their own purposes +export type TeachingPopoverPageCountChildRenderFunction = (currentPage: number, totalPages: number) => React.ReactNode; + +/** + * TeachingPopoverCarouselFooter Props + */ +export type TeachingPopoverCarouselFooterProps = ComponentProps & { + /** + * Controls whether buttons will be centered (balanced) or right aligned + * Defaults to 'centered'. + */ + layout?: TeachingPopoverCarouselFooterLayout; + + /** + * The text to be displayed on the initial step of carousel + */ + initialStepText: string; + + /** + * The text to be displayed on the final step of carousel + */ + finalStepText: string; +}; + +/** + * TeachingPopoverCarouselFooter State and Context Hooks + */ +export type TeachingPopoverCarouselFooterState = ComponentState> & + Pick; diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooter/__snapshots__/TeachingPopoverCarouselFooter.test.tsx.snap b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooter/__snapshots__/TeachingPopoverCarouselFooter.test.tsx.snap new file mode 100644 index 00000000000000..4a71f6beaef24f --- /dev/null +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooter/__snapshots__/TeachingPopoverCarouselFooter.test.tsx.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TeachingPopoverCarouselFooter renders a default state 1`] = ` +
+
+ + Default TeachingPopoverCarouselFooter + +
+
+`; diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooter/index.ts b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooter/index.ts new file mode 100644 index 00000000000000..a77e3f65bfc63f --- /dev/null +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooter/index.ts @@ -0,0 +1,5 @@ +export * from './TeachingPopoverCarouselFooter'; +export * from './TeachingPopoverCarouselFooter.types'; +export * from './renderTeachingPopoverCarouselFooter'; +export * from './useTeachingPopoverCarouselFooter'; +export * from './useTeachingPopoverCarouselFooterStyles.styles'; diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooter/renderTeachingPopoverCarouselFooter.tsx b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooter/renderTeachingPopoverCarouselFooter.tsx new file mode 100644 index 00000000000000..2e1daf0f89a4b3 --- /dev/null +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooter/renderTeachingPopoverCarouselFooter.tsx @@ -0,0 +1,23 @@ +/** @jsxRuntime automatic */ +/** @jsxImportSource @fluentui/react-jsx-runtime */ +import type { TeachingPopoverCarouselFooterState } from './TeachingPopoverCarouselFooter.types'; +import { TeachingPopoverCarouselFooterSlots } from './TeachingPopoverCarouselFooter.types'; +import { assertSlots } from '@fluentui/react-utilities'; + +/** + * Render the final JSX of TeachingPopoverCarouselFooter + */ +export const renderTeachingPopoverCarouselFooter_unstable = (state: TeachingPopoverCarouselFooterState) => { + assertSlots(state); + + const { layout } = state; + + return ( + + {layout === 'centered' && state.previous && } + {state.root.children} + {layout === 'offset' && state.previous && } + + + ); +}; diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooter/useTeachingPopoverCarouselFooter.ts b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooter/useTeachingPopoverCarouselFooter.ts new file mode 100644 index 00000000000000..8415a20ab1a73e --- /dev/null +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooter/useTeachingPopoverCarouselFooter.ts @@ -0,0 +1,50 @@ +import { getIntrinsicElementProps, slot } from '@fluentui/react-utilities'; +import * as React from 'react'; + +import type { + TeachingPopoverCarouselFooterProps, + TeachingPopoverCarouselFooterState, +} from './TeachingPopoverCarouselFooter.types'; +import { TeachingPopoverCarouselFooterButton } from '../TeachingPopoverCarouselFooterButton/TeachingPopoverCarouselFooterButton'; + +export const useTeachingPopoverCarouselFooter_unstable = ( + props: TeachingPopoverCarouselFooterProps, + ref: React.Ref, +): TeachingPopoverCarouselFooterState => { + const { layout = 'centered', initialStepText, finalStepText } = props; + + const previous = slot.optional(props.previous, { + defaultProps: { + navType: 'prev', + altText: initialStepText, + }, + renderByDefault: true, + elementType: TeachingPopoverCarouselFooterButton, + }); + + const next = slot.always(props.next, { + defaultProps: { + navType: 'next', + altText: finalStepText, + }, + elementType: TeachingPopoverCarouselFooterButton, + }); + + return { + layout, + components: { + root: 'div', + next: TeachingPopoverCarouselFooterButton, + previous: TeachingPopoverCarouselFooterButton, + }, + root: slot.always( + getIntrinsicElementProps('div', { + ref, + ...props, + }), + { elementType: 'div' }, + ), + previous, + next, + }; +}; diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooter/useTeachingPopoverCarouselFooterStyles.styles.ts b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooter/useTeachingPopoverCarouselFooterStyles.styles.ts new file mode 100644 index 00000000000000..4c2ca55304c098 --- /dev/null +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooter/useTeachingPopoverCarouselFooterStyles.styles.ts @@ -0,0 +1,51 @@ +import { makeStyles, mergeClasses, shorthands } from '@griffel/react'; +import type { + TeachingPopoverCarouselFooterSlots, + TeachingPopoverCarouselFooterState, +} from './TeachingPopoverCarouselFooter.types'; +import type { SlotClassNames } from '@fluentui/react-utilities'; + +export const teachingPopoverCarouselFooterClassNames: SlotClassNames = { + root: 'fui-TeachingPopoverCarouselFooter', + previous: 'fui-TeachingPopoverCarouselFooter__previous', + next: 'fui-TeachingPopoverCarouselFooter__next', +}; + +// Todo: Page change animation & styles +const useStyles = makeStyles({ + root: { + display: 'flex', + flexDirection: 'row', + }, + rootCentered: { + justifyContent: 'space-between', + ...shorthands.gap('8px'), + }, + rootRightAligned: { + ...shorthands.gap('8px'), + '& :first-child': { + marginInlineEnd: 'auto', + }, + }, +}); + +/** Applies style classnames to slots */ +export const useTeachingPopoverCarouselFooterStyles_unstable = (state: TeachingPopoverCarouselFooterState) => { + const styles = useStyles(); + const { layout } = state; + + state.root.className = mergeClasses( + teachingPopoverCarouselFooterClassNames.root, + styles.root, + layout === 'centered' ? styles.rootCentered : styles.rootRightAligned, + state.root.className, + ); + + if (state.previous) { + state.previous.className = mergeClasses(teachingPopoverCarouselFooterClassNames.previous, state.previous.className); + } + + state.next.className = mergeClasses(teachingPopoverCarouselFooterClassNames.next, state.next.className); + + return state; +}; diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooterButton/TeachingPopoverCarouselFooterButton.test.tsx b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooterButton/TeachingPopoverCarouselFooterButton.test.tsx new file mode 100644 index 00000000000000..262d510fb8a677 --- /dev/null +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooterButton/TeachingPopoverCarouselFooterButton.test.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../testing/isConformant'; +import { TeachingPopoverCarouselFooterButton } from './TeachingPopoverCarouselFooterButton'; +import { TeachingPopoverCarouselFooterButtonProps } from './TeachingPopoverCarouselFooterButton.types'; + +describe('TeachingPopoverCarouselFooterButton', () => { + isConformant({ + Component: TeachingPopoverCarouselFooterButton as React.FunctionComponent, + displayName: 'TeachingPopoverCarouselFooterButton', + }); + + // TODO add more tests here, and create visual regression tests in /apps/vr-tests + + it('renders a default state', () => { + const result = render( + + Default TeachingPopoverCarouselFooterButton + , + ); + expect(result.container).toMatchSnapshot(); + }); +}); diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooterButton/TeachingPopoverCarouselFooterButton.tsx b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooterButton/TeachingPopoverCarouselFooterButton.tsx new file mode 100644 index 00000000000000..3f9355b3c6f373 --- /dev/null +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooterButton/TeachingPopoverCarouselFooterButton.tsx @@ -0,0 +1,20 @@ +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { useTeachingPopoverCarouselFooterButton_unstable } from './useTeachingPopoverCarouselFooterButton'; +import { renderTeachingPopoverCarouselFooterButton_unstable } from './renderTeachingPopoverCarouselFooterButton'; +import { useTeachingPopoverCarouselFooterButtonStyles_unstable } from './useTeachingPopoverCarouselFooterButtonStyles.styles'; +import type { TeachingPopoverCarouselFooterButtonProps } from './TeachingPopoverCarouselFooterButton.types'; + +/** + * TeachingPopoverCarouselFooterButton component - TODO: add more docs + */ +export const TeachingPopoverCarouselFooterButton: ForwardRefComponent = + React.forwardRef((props, ref) => { + const state = useTeachingPopoverCarouselFooterButton_unstable(props, ref); + + useTeachingPopoverCarouselFooterButtonStyles_unstable(state); + + return renderTeachingPopoverCarouselFooterButton_unstable(state); + }); + +TeachingPopoverCarouselFooterButton.displayName = 'TeachingPopoverCarouselFooterButton'; diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooterButton/TeachingPopoverCarouselFooterButton.types.ts b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooterButton/TeachingPopoverCarouselFooterButton.types.ts new file mode 100644 index 00000000000000..0edd7424330858 --- /dev/null +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooterButton/TeachingPopoverCarouselFooterButton.types.ts @@ -0,0 +1,35 @@ +import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; +import { PopoverContextValue } from '@fluentui/react-popover'; +import { ButtonProps, ButtonState } from '@fluentui/react-button'; +import { ARIAButtonSlotProps } from '@fluentui/react-aria'; +import { ReactNode } from 'react'; + +export type TeachingPopoverCarouselFooterButtonSlots = { + root: NonNullable>>; +}; + +/** + * TeachingPopoverCarouselFooterButton Props + */ +export type TeachingPopoverCarouselFooterButtonProps = ComponentProps & + ButtonProps & { + /** + * Defines whether the button should be next or previous type - used for both styling and functionality. + */ + navType: 'next' | 'prev'; + + /** + * The ReactNode provided to the button when it is on it's first (navType 'prev') or last (navType 'next') step + */ + altText: ReactNode; + }; + +/** + * State used in rendering TeachingPopoverCarouselFooterButton + */ +export type TeachingPopoverCarouselFooterButtonState = ButtonState & + ComponentState & + Pick & { + /* Rename popover appearance to prevent conflict with button appearance */ + popoverAppearance: PopoverContextValue['appearance']; + }; diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooterButton/__snapshots__/TeachingPopoverCarouselFooterButton.test.tsx.snap b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooterButton/__snapshots__/TeachingPopoverCarouselFooterButton.test.tsx.snap new file mode 100644 index 00000000000000..6d7e1cf885e8c0 --- /dev/null +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooterButton/__snapshots__/TeachingPopoverCarouselFooterButton.test.tsx.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TeachingPopoverCarouselFooterButton renders a default state 1`] = ` +
+ +
+`; diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooterButton/index.ts b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooterButton/index.ts new file mode 100644 index 00000000000000..a340881b36db2d --- /dev/null +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooterButton/index.ts @@ -0,0 +1,5 @@ +export * from './TeachingPopoverCarouselFooterButton'; +export * from './TeachingPopoverCarouselFooterButton.types'; +export * from './renderTeachingPopoverCarouselFooterButton'; +export * from './useTeachingPopoverCarouselFooterButton'; +export * from './useTeachingPopoverCarouselFooterButtonStyles.styles'; diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooterButton/renderTeachingPopoverCarouselFooterButton.tsx b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooterButton/renderTeachingPopoverCarouselFooterButton.tsx new file mode 100644 index 00000000000000..bc2241276f64fe --- /dev/null +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooterButton/renderTeachingPopoverCarouselFooterButton.tsx @@ -0,0 +1,17 @@ +/** @jsxRuntime automatic */ +/** @jsxImportSource @fluentui/react-jsx-runtime */ + +import { assertSlots } from '@fluentui/react-utilities'; +import type { + TeachingPopoverCarouselFooterButtonState, + TeachingPopoverCarouselFooterButtonSlots, +} from './TeachingPopoverCarouselFooterButton.types'; + +/** + * Render the final JSX of TeachingPopoverCarouselFooterButton + */ +export const renderTeachingPopoverCarouselFooterButton_unstable = (state: TeachingPopoverCarouselFooterButtonState) => { + assertSlots(state); + + return ; +}; diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooterButton/useTeachingPopoverCarouselFooterButton.ts b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooterButton/useTeachingPopoverCarouselFooterButton.ts new file mode 100644 index 00000000000000..fb99a549a21f0b --- /dev/null +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooterButton/useTeachingPopoverCarouselFooterButton.ts @@ -0,0 +1,86 @@ +import * as React from 'react'; +import { getIntrinsicElementProps, mergeCallbacks, slot } from '@fluentui/react-utilities'; +import type { + TeachingPopoverCarouselFooterButtonProps, + TeachingPopoverCarouselFooterButtonState, +} from './TeachingPopoverCarouselFooterButton.types'; +import { usePopoverContext_unstable } from '@fluentui/react-popover'; +import { useCarouselContext_unstable } from '../TeachingPopoverCarousel/Carousel/CarouselContext'; +import { useEventCallback } from '@fluentui/react-utilities'; +import { useButton_unstable } from '@fluentui/react-button'; +import { useCarouselValues_unstable } from '../TeachingPopoverCarousel/Carousel/useCarouselValues'; + +/** + * Create the state required to render TeachingPopoverCarouselFooterButton. + * + * The returned state can be modified with hooks such as useTeachingPopoverCarouselFooterButtonStyles_unstable, + * before being passed to renderTeachingPopoverCarouselFooterButton_unstable. + * + * @param props - props from this instance of TeachingPopoverCarouselFooterButton + * @param ref - reference to root HTMLDivElement of TeachingPopoverCarouselFooterButton + */ +export const useTeachingPopoverCarouselFooterButton_unstable = ( + props: TeachingPopoverCarouselFooterButtonProps, + ref: React.Ref, +): TeachingPopoverCarouselFooterButtonState => { + const { navType, altText } = props; + + const popoverAppearance = usePopoverContext_unstable(context => context.appearance); + const selectPageByDirection = useCarouselContext_unstable(c => c.selectPageByDirection); + const values = useCarouselValues_unstable(snapshot => snapshot); + const activeValue = useCarouselContext_unstable(c => c.value); + + const handleClick = (event: React.MouseEvent) => { + if (event.isDefaultPrevented()) { + return; + } + + selectPageByDirection(event, navType); + }; + + const handleButtonClick = useEventCallback(mergeCallbacks(handleClick, props.onClick)); + + const isTrailing = React.useMemo(() => { + if (!activeValue) { + return false; + } + + if (navType === 'prev') { + return values.indexOf(activeValue) === 0; + } + + return values.indexOf(activeValue) === values.length - 1; + }, [navType, activeValue, values]); + + let buttonAppearanceType: 'primary' | 'outline' | undefined; + + if (navType === 'next') { + buttonAppearanceType = popoverAppearance === 'brand' ? undefined : 'primary'; + } else { + buttonAppearanceType = popoverAppearance === 'brand' ? 'outline' : undefined; + } + + /* Handle altText on trailing step */ + let buttonChild = props.children; + if (isTrailing) { + buttonChild = altText; + } + + return { + ...useButton_unstable({ appearance: buttonAppearanceType, ...props }, ref), + navType, + popoverAppearance, + altText, + // Override useButton root slot + root: slot.always( + getIntrinsicElementProps('button', { + ref, + appearance: buttonAppearanceType, + ...props, + onClick: handleButtonClick, + children: buttonChild, + }), + { elementType: 'button' }, + ), + }; +}; diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooterButton/useTeachingPopoverCarouselFooterButtonStyles.styles.ts b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooterButton/useTeachingPopoverCarouselFooterButtonStyles.styles.ts new file mode 100644 index 00000000000000..c9f8c3c4b2a2b1 --- /dev/null +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselFooterButton/useTeachingPopoverCarouselFooterButtonStyles.styles.ts @@ -0,0 +1,73 @@ +import { makeStyles, mergeClasses, shorthands } from '@griffel/react'; +import type { SlotClassNames } from '@fluentui/react-utilities'; +import type { + TeachingPopoverCarouselFooterButtonSlots, + TeachingPopoverCarouselFooterButtonState, +} from './TeachingPopoverCarouselFooterButton.types'; +import { tokens } from '@fluentui/react-theme'; +import { useButtonStyles_unstable } from '@fluentui/react-button'; + +export const teachingPopoverCarouselFooterButtonClassNames: SlotClassNames = { + root: 'fui-TeachingPopoverCarouselFooterButton', +}; + +const useStyles = makeStyles({ + root: { + minWidth: '96px', + }, + brandNext: { + color: tokens.colorBrandForeground1, + backgroundColor: tokens.colorNeutralForegroundOnBrand, + ...shorthands.borderColor(tokens.colorTransparentBackground), + ':hover': { + color: tokens.colorCompoundBrandForeground1Hover, + backgroundColor: tokens.colorNeutralForegroundOnBrand, + }, + ':hover:active': { + color: tokens.colorCompoundBrandForeground1Pressed, + backgroundColor: tokens.colorNeutralForegroundOnBrand, + }, + }, + brandPrevious: { + // In brand, this is always 'NeutralForegroundOnBrand' + color: tokens.colorNeutralForegroundOnBrand, + backgroundColor: tokens.colorBrandBackground, + ...shorthands.borderColor(tokens.colorNeutralForegroundOnBrand), + ':hover': { + color: tokens.colorNeutralForegroundOnBrand, + ...shorthands.borderColor(tokens.colorNeutralForegroundOnBrand), + backgroundColor: tokens.colorBrandBackgroundHover, + }, + ':hover:active': { + color: tokens.colorNeutralForegroundOnBrand, + ...shorthands.borderColor(tokens.colorNeutralForegroundOnBrand), + backgroundColor: tokens.colorBrandBackgroundPressed, + }, + }, +}); + +/** + * Apply styling to the TeachingPopoverCarouselFooterButton slots based on the state + */ +export const useTeachingPopoverCarouselFooterButtonStyles_unstable = ( + state: TeachingPopoverCarouselFooterButtonState, +): TeachingPopoverCarouselFooterButtonState => { + const styles = useStyles(); + const { navType, popoverAppearance } = state; + + // Apply underlying fluent Button styles + state = { + ...state, + ...useButtonStyles_unstable(state), + }; + + state.root.className = mergeClasses( + teachingPopoverCarouselFooterButtonClassNames.root, + styles.root, + navType === 'prev' && popoverAppearance === 'brand' && styles.brandPrevious, + navType === 'next' && popoverAppearance === 'brand' && styles.brandNext, + state.root.className, + ); + + return state; +}; diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselNav/TeachingPopoverCarouselNav.test.tsx b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselNav/TeachingPopoverCarouselNav.test.tsx index 5a69990f34cdd3..b837a587ab2957 100644 --- a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselNav/TeachingPopoverCarouselNav.test.tsx +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselNav/TeachingPopoverCarouselNav.test.tsx @@ -2,10 +2,7 @@ import * as React from 'react'; import { render } from '@testing-library/react'; import { isConformant } from '../../testing/isConformant'; import { TeachingPopoverCarouselNav } from './TeachingPopoverCarouselNav'; -import { resetIdsForTests } from '@fluentui/react-utilities'; -import { mockTeachingPopoverCarouselContext } from '../../testing/mockTeachingPopoverCarouselContext'; - -jest.mock('../TeachingPopoverCarousel/TeachingPopoverCarouselContext'); +import { TeachingPopoverCarouselNavButton } from '../TeachingPopoverCarouselNavButton/TeachingPopoverCarouselNavButton'; describe('TeachingPopoverCarouselNav', () => { isConformant({ @@ -13,15 +10,10 @@ describe('TeachingPopoverCarouselNav', () => { displayName: 'TeachingPopoverCarouselNav', }); - beforeEach(() => { - resetIdsForTests(); - mockTeachingPopoverCarouselContext({ totalPages: 2, currentPage: 0 }); - }); - - // TODO add more tests here, and create visual regression tests in /apps/vr-tests - it('renders a default state', () => { - const result = render(Default TeachingPopoverCarouselNav); + const result = render( + {() => }, + ); expect(result.container).toMatchSnapshot(); }); }); diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselNav/TeachingPopoverCarouselNav.tsx b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselNav/TeachingPopoverCarouselNav.tsx index 5bdd95072052d3..84972ade084a6a 100644 --- a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselNav/TeachingPopoverCarouselNav.tsx +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselNav/TeachingPopoverCarouselNav.tsx @@ -15,9 +15,6 @@ export const TeachingPopoverCarouselNav: ForwardRefComponent>; }; +export type NavButtonRenderFunction = (value: string) => React.ReactNode; + export type TeachingPopoverCarouselNavState = ComponentState & { - /** - * The current carousel page. - */ - currentPage: number; - /** - * The total number of carousel pages, controlled by children within carousel itself. - */ - totalPages: number; + values: string[]; + + renderNavButton: NavButtonRenderFunction; }; -export type TeachingPopoverCarouselNavProps = ComponentProps>; +export type TeachingPopoverCarouselNavProps = Omit< + ComponentProps>, + 'children' +> & { + children: NavButtonRenderFunction; +}; diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselNav/__snapshots__/TeachingPopoverCarouselNav.test.tsx.snap b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselNav/__snapshots__/TeachingPopoverCarouselNav.test.tsx.snap index 19d7cf76b1aa3b..1a970a38974a36 100644 --- a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselNav/__snapshots__/TeachingPopoverCarouselNav.test.tsx.snap +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselNav/__snapshots__/TeachingPopoverCarouselNav.test.tsx.snap @@ -7,21 +7,6 @@ exports[`TeachingPopoverCarouselNav renders a default state 1`] = ` data-tabster="{\\"mover\\":{\\"cyclic\\":false,\\"direction\\":2,\\"memorizeCurrent\\":false,\\"hasDefault\\":true}}" role="tablist" tabindex="0" - > -
+ /> `; diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselNav/renderTeachingPopoverCarouselNav.tsx b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselNav/renderTeachingPopoverCarouselNav.tsx index 067b1accfbe5ff..cee6c5cfb7b638 100644 --- a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselNav/renderTeachingPopoverCarouselNav.tsx +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselNav/renderTeachingPopoverCarouselNav.tsx @@ -3,6 +3,7 @@ import type { TeachingPopoverCarouselNavState } from './TeachingPopoverCarouselNav.types'; import { assertSlots } from '@fluentui/react-utilities'; import { TeachingPopoverCarouselNavSlots } from './TeachingPopoverCarouselNav.types'; +import { ValueIdContextProvider } from './valueIdContext'; /** * Render the final JSX of TeachingPopoverCarouselNav @@ -10,5 +11,15 @@ import { TeachingPopoverCarouselNavSlots } from './TeachingPopoverCarouselNav.ty export const renderTeachingPopoverCarouselNav_unstable = (state: TeachingPopoverCarouselNavState) => { assertSlots(state); - return ; + const { values, renderNavButton } = state; + + return ( + + {values.map(value => ( + + {renderNavButton(value)} + + ))} + + ); }; diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselNav/useTeachingPopoverCarouselNav.tsx b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselNav/useTeachingPopoverCarouselNav.tsx index efa3bd95b55c92..0247da9b0b8e65 100644 --- a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselNav/useTeachingPopoverCarouselNav.tsx +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselNav/useTeachingPopoverCarouselNav.tsx @@ -1,13 +1,12 @@ -import * as React from 'react'; +import { useArrowNavigationGroup } from '@fluentui/react-tabster'; import { getIntrinsicElementProps, slot } from '@fluentui/react-utilities'; +import * as React from 'react'; + import type { TeachingPopoverCarouselNavProps, TeachingPopoverCarouselNavState, } from './TeachingPopoverCarouselNav.types'; - -import { useArrowNavigationGroup } from '@fluentui/react-tabster'; -import { TeachingPopoverCarouselNavButton } from '../TeachingPopoverCarouselNavButton/index'; -import { useTeachingPopoverCarouselContext_unstable } from '../TeachingPopoverCarousel/TeachingPopoverCarouselContext'; +import { useCarouselValues_unstable } from '../TeachingPopoverCarousel/Carousel/useCarouselValues'; /** * Returns the props and state required to render the component @@ -25,18 +24,11 @@ export const useTeachingPopoverCarouselNav_unstable = ( // eslint-disable-next-line @typescript-eslint/naming-convention unstable_hasDefault: true, }); - - const totalPages = useTeachingPopoverCarouselContext_unstable(context => context.totalPages); - const currentPage = useTeachingPopoverCarouselContext_unstable(context => context.currentPage); - - // Generate the child TeachingPopoverCarouselNavButton and memoize them to prevent unnecessary re-rendering - const rootChildren = React.useMemo(() => { - return Array.from(Array(totalPages), (_, i) => ); - }, [totalPages]); + const values = useCarouselValues_unstable(snapshot => snapshot); return { - totalPages, - currentPage, + values, + renderNavButton: props.children, components: { root: 'div', }, @@ -46,8 +38,8 @@ export const useTeachingPopoverCarouselNav_unstable = ( role: 'tablist', tabIndex: 0, ...props, - children: rootChildren, ...focusableGroupAttr, + children: null, }), { elementType: 'div' }, ), diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselNav/valueIdContext.ts b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselNav/valueIdContext.ts new file mode 100644 index 00000000000000..e6bf571357efbb --- /dev/null +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselNav/valueIdContext.ts @@ -0,0 +1,9 @@ +import * as React from 'react'; + +const valueIdContext = React.createContext(undefined); + +export const valueIdContextDefaultValue = ''; + +export const useValueIdContext = () => React.useContext(valueIdContext) ?? valueIdContextDefaultValue; + +export const ValueIdContextProvider = valueIdContext.Provider; diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselNavButton/TeachingPopoverCarouselNavButton.test.tsx b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselNavButton/TeachingPopoverCarouselNavButton.test.tsx index bbd2647d226482..c2234dcaff24de 100644 --- a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselNavButton/TeachingPopoverCarouselNavButton.test.tsx +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselNavButton/TeachingPopoverCarouselNavButton.test.tsx @@ -4,7 +4,7 @@ import { TeachingPopoverCarouselNavButton } from './TeachingPopoverCarouselNavBu describe('TeachingPopoverCarouselNavButton', () => { it('renders a default state', () => { - const result = render(); + const result = render(); expect(result.container).toMatchSnapshot(); }); }); diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselNavButton/TeachingPopoverCarouselNavButton.tsx b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselNavButton/TeachingPopoverCarouselNavButton.tsx index 4a73262a705221..0c3ce2bf1ac3eb 100644 --- a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselNavButton/TeachingPopoverCarouselNavButton.tsx +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselNavButton/TeachingPopoverCarouselNavButton.tsx @@ -13,7 +13,8 @@ export const TeachingPopoverCarouselNavButton: ForwardRefComponent; TeachingPopoverCarouselNavButton.displayName = 'TeachingPopoverCarouselNavButton'; diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselNavButton/TeachingPopoverCarouselNavButton.types.ts b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselNavButton/TeachingPopoverCarouselNavButton.types.ts index a9526ea4d8421c..c715957b1bc3d1 100644 --- a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselNavButton/TeachingPopoverCarouselNavButton.types.ts +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselNavButton/TeachingPopoverCarouselNavButton.types.ts @@ -12,12 +12,7 @@ export type TeachingPopoverCarouselNavButtonSlots = { /** * TeachingPopoverCarouselNavButton Props */ -export type TeachingPopoverCarouselNavButtonProps = ComponentProps> & { - /** - * The page index that will be used to update carousel context - */ - index: number; -}; +export type TeachingPopoverCarouselNavButtonProps = ComponentProps; /** * TeachingPopoverCarouselNavButton State diff --git a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselNavButton/__snapshots__/TeachingPopoverCarouselNavButton.test.tsx.snap b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselNavButton/__snapshots__/TeachingPopoverCarouselNavButton.test.tsx.snap index a3e8062869d26b..78a88bfac2c6d1 100644 --- a/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselNavButton/__snapshots__/TeachingPopoverCarouselNavButton.test.tsx.snap +++ b/packages/react-components/react-teaching-popover-preview/src/components/TeachingPopoverCarouselNavButton/__snapshots__/TeachingPopoverCarouselNavButton.test.tsx.snap @@ -3,9 +3,8 @@ exports[`TeachingPopoverCarouselNavButton renders a default state 1`] = `
+ + + Tips + }> + Teaching Bubble Title +
This is a teaching popover body
+
+ +
+ +); diff --git a/packages/react-components/react-teaching-popover-preview/stories/TeachingPopover/TeachingPopoverBranded.stories.tsx b/packages/react-components/react-teaching-popover-preview/stories/TeachingPopover/TeachingPopoverBranded.stories.tsx deleted file mode 100644 index 3bbd93823052a3..00000000000000 --- a/packages/react-components/react-teaching-popover-preview/stories/TeachingPopover/TeachingPopoverBranded.stories.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import * as React from 'react'; -import { Button, Image } from '@fluentui/react-components'; - -import { - TeachingPopover, - TeachingPopoverBody, - TeachingPopoverHeader, - TeachingPopoverTitle, - TeachingPopoverSurface, - TeachingPopoverTrigger, - TeachingPopoverFooter, -} from '@fluentui/react-teaching-popover-preview'; -import type { TeachingPopoverProps } from '@fluentui/react-teaching-popover-preview'; - -const SwapImage = 'https://fabricweb.azureedge.net/fabric-website/assets/images/wireframe/image-square.png'; - -const ExampleContent = (index: number) => { - return ( - <> -
{`This is page: ${index}`}
- - ); -}; - -export const DefaultBrand = (props: TeachingPopoverProps) => ( - - - - - - {'Tips'} - }> - {'Teaching Bubble Title'} - {ExampleContent(1)} - - - - -); diff --git a/packages/react-components/react-teaching-popover-preview/stories/TeachingPopover/TeachingPopoverCarousel.stories.tsx b/packages/react-components/react-teaching-popover-preview/stories/TeachingPopover/TeachingPopoverCarousel.stories.tsx index 61c2b9f090a745..8b0e3b2a0972f3 100644 --- a/packages/react-components/react-teaching-popover-preview/stories/TeachingPopover/TeachingPopoverCarousel.stories.tsx +++ b/packages/react-components/react-teaching-popover-preview/stories/TeachingPopover/TeachingPopoverCarousel.stories.tsx @@ -9,51 +9,46 @@ import { TeachingPopoverTitle, TeachingPopoverSurface, TeachingPopoverTrigger, + TeachingPopoverCarouselCard, + TeachingPopoverCarouselFooter, + TeachingPopoverCarouselNav, + TeachingPopoverCarouselNavButton, } from '@fluentui/react-teaching-popover-preview'; -import type { TeachingPopoverProps } from '@fluentui/react-teaching-popover-preview'; -const SwapImage = 'https://fabricweb.azureedge.net/fabric-website/assets/images/wireframe/image-square.png'; +const swapImage = 'https://fabricweb.azureedge.net/fabric-website/assets/images/wireframe/image-square.png'; -const ExampleContent = (index: number) => { - return ( - <> -
{`This is page: ${index}`}
- - ); -}; - -export const Carousel = (props: TeachingPopoverProps) => ( - +export const Carousel = () => ( + - {'Tips'} - - {/* Multiple TeachingPopoverBody will be wrapped by a 'TeachingPopoverCarousel'*/} - }> - {'Teaching Bubble Title'} - {ExampleContent(1)} - + Tips + + + }> + Teaching Bubble Title +
This is page: 1
+
+
+ + + }> + Teaching Bubble Title +
This is page: 2
+
+
- {/* Multiple TeachingPopoverBody will be wrapped by a 'TeachingPopoverCarousel'*/} - }> - {'Teaching Bubble Title'} - {ExampleContent(2)} - + + }> + Teaching Bubble Title +
This is page: 3
+
+
- {/* Multiple TeachingPopoverBody will be wrapped by a 'TeachingPopoverCarousel'*/} - }> - {'Teaching Bubble Title'} - {ExampleContent(3)} - + + {() => } +
diff --git a/packages/react-components/react-teaching-popover-preview/stories/TeachingPopover/TeachingPopoverCarouselAppearanceBrand.stories.tsx b/packages/react-components/react-teaching-popover-preview/stories/TeachingPopover/TeachingPopoverCarouselAppearanceBrand.stories.tsx new file mode 100644 index 00000000000000..dabf47863ddf4f --- /dev/null +++ b/packages/react-components/react-teaching-popover-preview/stories/TeachingPopover/TeachingPopoverCarouselAppearanceBrand.stories.tsx @@ -0,0 +1,55 @@ +import * as React from 'react'; +import { Button, Image } from '@fluentui/react-components'; + +import { + TeachingPopover, + TeachingPopoverBody, + TeachingPopoverCarousel, + TeachingPopoverHeader, + TeachingPopoverTitle, + TeachingPopoverSurface, + TeachingPopoverTrigger, + TeachingPopoverCarouselCard, + TeachingPopoverCarouselFooter, + TeachingPopoverCarouselNav, + TeachingPopoverCarouselNavButton, +} from '@fluentui/react-teaching-popover-preview'; + +const swapImage = 'https://fabricweb.azureedge.net/fabric-website/assets/images/wireframe/image-square.png'; + +export const CarouselBrand = () => ( + + + + + + Tips + + + }> + Teaching Bubble Title +
This is page: 1
+
+
+ + + }> + Teaching Bubble Title +
This is page: 2
+
+
+ + + }> + Teaching Bubble Title +
This is page: 3
+
+
+ + + {() => } + +
+
+
+); diff --git a/packages/react-components/react-teaching-popover-preview/stories/TeachingPopover/TeachingPopoverCarouselBranded.stories.tsx b/packages/react-components/react-teaching-popover-preview/stories/TeachingPopover/TeachingPopoverCarouselBranded.stories.tsx deleted file mode 100644 index 50d6c0f05c1846..00000000000000 --- a/packages/react-components/react-teaching-popover-preview/stories/TeachingPopover/TeachingPopoverCarouselBranded.stories.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import * as React from 'react'; -import { Button, Image } from '@fluentui/react-components'; - -import { - TeachingPopover, - TeachingPopoverBody, - TeachingPopoverCarousel, - TeachingPopoverHeader, - TeachingPopoverTitle, - TeachingPopoverSurface, - TeachingPopoverTrigger, -} from '@fluentui/react-teaching-popover-preview'; -import type { TeachingPopoverProps } from '@fluentui/react-teaching-popover-preview'; - -const SwapImage = 'https://fabricweb.azureedge.net/fabric-website/assets/images/wireframe/image-square.png'; - -const ExampleContent = (index: number) => { - return ( - <> -
{`This is page: ${index}`}
- - ); -}; - -export const CarouselBrand = (props: TeachingPopoverProps) => ( - - - - - - {'Tips'} - - {/* Multiple TeachingPopoverBody will be wrapped by a 'TeachingPopoverCarousel'*/} - }> - {'Teaching Bubble Title'} - {ExampleContent(1)} - - - {/* Multiple TeachingPopoverBody will be wrapped by a 'TeachingPopoverCarousel'*/} - }> - {'Teaching Bubble Title'} - {ExampleContent(2)} - - - {/* Multiple TeachingPopoverBody will be wrapped by a 'TeachingPopoverCarousel'*/} - }> - {'Teaching Bubble Title'} - {ExampleContent(3)} - - - - -); diff --git a/packages/react-components/react-teaching-popover-preview/stories/TeachingPopover/TeachingPopoverCarouselText.stories.tsx b/packages/react-components/react-teaching-popover-preview/stories/TeachingPopover/TeachingPopoverCarouselText.stories.tsx index d163cd7d20520f..b68bd93a5e9e5d 100644 --- a/packages/react-components/react-teaching-popover-preview/stories/TeachingPopover/TeachingPopoverCarouselText.stories.tsx +++ b/packages/react-components/react-teaching-popover-preview/stories/TeachingPopover/TeachingPopoverCarouselText.stories.tsx @@ -9,53 +9,47 @@ import { TeachingPopoverTitle, TeachingPopoverSurface, TeachingPopoverTrigger, + TeachingPopoverCarouselCard, + TeachingPopoverCarouselFooter, + TeachingPopoverCarouselPageCount, } from '@fluentui/react-teaching-popover-preview'; -import type { TeachingPopoverProps } from '@fluentui/react-teaching-popover-preview'; -const SwapImage = 'https://fabricweb.azureedge.net/fabric-website/assets/images/wireframe/image-square.png'; +const swapImage = 'https://fabricweb.azureedge.net/fabric-website/assets/images/wireframe/image-square.png'; -const ExampleContent = (index: number) => { - return ( - <> -
{`This is page: ${index}`}
- - ); -}; - -export const CarouselText = (props: TeachingPopoverProps) => ( - +export const CarouselText = () => ( + - {'Tips'} - - {/* Multiple TeachingPopoverBody will be wrapped by a 'TeachingPopoverCarousel'*/} - }> - {'Teaching Bubble Title'} - {ExampleContent(1)} - + Tips" + + + }> + Teaching Bubble Title +
This is page: 1
+
+
+ + + }> + Teaching Bubble Title +
This is page: 2
+
+
- {/* Multiple TeachingPopoverBody will be wrapped by a 'TeachingPopoverCarousel'*/} - }> - {'Teaching Bubble Title'} - {ExampleContent(2)} - + + }> + Teaching Bubble Title +
This is page: 3
+
+
- {/* Multiple TeachingPopoverBody will be wrapped by a 'TeachingPopoverCarousel'*/} - }> - {'Teaching Bubble Title'} - {ExampleContent(3)} - + + + {(currentIndex: number, totalPages: number) => `${currentIndex} of ${totalPages}`} + +
diff --git a/packages/react-components/react-teaching-popover-preview/stories/TeachingPopover/TeachingPopoverDefault.stories.tsx b/packages/react-components/react-teaching-popover-preview/stories/TeachingPopover/TeachingPopoverDefault.stories.tsx index 77cf46c8f49d34..51c3cd3960f899 100644 --- a/packages/react-components/react-teaching-popover-preview/stories/TeachingPopover/TeachingPopoverDefault.stories.tsx +++ b/packages/react-components/react-teaching-popover-preview/stories/TeachingPopover/TeachingPopoverDefault.stories.tsx @@ -10,30 +10,21 @@ import { TeachingPopoverTrigger, TeachingPopoverFooter, } from '@fluentui/react-teaching-popover-preview'; -import type { TeachingPopoverProps } from '@fluentui/react-teaching-popover-preview'; -const SwapImage = 'https://fabricweb.azureedge.net/fabric-website/assets/images/wireframe/image-square.png'; +const swapImage = 'https://fabricweb.azureedge.net/fabric-website/assets/images/wireframe/image-square.png'; -const ExampleContent = (index: number) => { - return ( - <> -
{`This is page: ${index}`}
- - ); -}; - -export const Default = (props: TeachingPopoverProps) => ( - +export const Default = () => ( + - {'Tips'} - }> - {'Teaching Bubble Title'} - {ExampleContent(1)} + Tips + }> + Teaching Bubble Title +
This is a teaching popover body
- +
); diff --git a/packages/react-components/react-teaching-popover-preview/stories/TeachingPopover/TeachingPopoverDefaultBranded.stories.tsx b/packages/react-components/react-teaching-popover-preview/stories/TeachingPopover/TeachingPopoverDefaultBranded.stories.tsx deleted file mode 100644 index 3bbd93823052a3..00000000000000 --- a/packages/react-components/react-teaching-popover-preview/stories/TeachingPopover/TeachingPopoverDefaultBranded.stories.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import * as React from 'react'; -import { Button, Image } from '@fluentui/react-components'; - -import { - TeachingPopover, - TeachingPopoverBody, - TeachingPopoverHeader, - TeachingPopoverTitle, - TeachingPopoverSurface, - TeachingPopoverTrigger, - TeachingPopoverFooter, -} from '@fluentui/react-teaching-popover-preview'; -import type { TeachingPopoverProps } from '@fluentui/react-teaching-popover-preview'; - -const SwapImage = 'https://fabricweb.azureedge.net/fabric-website/assets/images/wireframe/image-square.png'; - -const ExampleContent = (index: number) => { - return ( - <> -
{`This is page: ${index}`}
- - ); -}; - -export const DefaultBrand = (props: TeachingPopoverProps) => ( - - - - - - {'Tips'} - }> - {'Teaching Bubble Title'} - {ExampleContent(1)} - - - - -); diff --git a/packages/react-components/react-teaching-popover-preview/stories/TeachingPopover/index.stories.tsx b/packages/react-components/react-teaching-popover-preview/stories/TeachingPopover/index.stories.tsx index 94e816b5c4fb1b..bc4c867616baaa 100644 --- a/packages/react-components/react-teaching-popover-preview/stories/TeachingPopover/index.stories.tsx +++ b/packages/react-components/react-teaching-popover-preview/stories/TeachingPopover/index.stories.tsx @@ -2,9 +2,9 @@ import { TeachingPopover } from '@fluentui/react-teaching-popover-preview'; import descriptionMd from './TeachingPopoverDescription.md'; export { Default } from './TeachingPopoverDefault.stories'; -export { DefaultBrand } from './TeachingPopoverDefaultBranded.stories'; +export { AppearanceBrand } from './TeachingPopoverAppearanceBrand.stories'; export { Carousel } from './TeachingPopoverCarousel.stories'; -export { CarouselBrand } from './TeachingPopoverCarouselBranded.stories'; +export { CarouselBrand } from './TeachingPopoverCarouselAppearanceBrand.stories'; export { CarouselText } from './TeachingPopoverCarouselText.stories'; export default { diff --git a/yarn.lock b/yarn.lock index 8989d7b76f8c95..b2c07ca88ef7a6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5812,6 +5812,11 @@ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== +"@types/use-sync-external-store@0.0.6": + version "0.0.6" + resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz#60be8d21baab8c305132eb9cb912ed497852aadc" + integrity sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg== + "@types/vinyl-fs@*": version "2.4.11" resolved "https://registry.yarnpkg.com/@types/vinyl-fs/-/vinyl-fs-2.4.11.tgz#b98119b8bb2494141eaf649b09fbfeb311161206" @@ -24126,6 +24131,11 @@ use-subscription@^1.3.0: dependencies: object-assign "^4.1.1" +use-sync-external-store@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" + integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== + use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"