Skip to content

Commit

Permalink
Fix inaccurate tabs, add page index change callback (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
roginfarrer authored Jul 19, 2024
1 parent a33123a commit b374cba
Show file tree
Hide file tree
Showing 31 changed files with 717 additions and 304 deletions.
Binary file modified bun.lockb
Binary file not shown.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"devDependencies": {
"@ianvs/prettier-plugin-sort-imports": "^4.2.1",
"prettier": "^3.2.5",
"turbo": "^1.13.3"
"turbo": "^2.0.7"
},
"workspaces": [
"react-aria-carousel",
Expand All @@ -24,5 +24,6 @@
"@pandadev/css",
"example",
"react-aria-carousel"
]
],
"packageManager": "[email protected]"
}
15 changes: 15 additions & 0 deletions react-aria-carousel/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
## Unreleased

- `Carousel`, `CarouselButton`, `CarouselScroller`, `CarouselItem`, `CarouselTabs`, `CarouselTab`, `CarouselAutoplayControl` now forward the refs of their outer elements.
- `useCarousel` and `Carousel` now support a new prop `onActivePageIndexChange`. This prop accepts a function that's called with the new `activePageIndex` when it's changed. Fixes #1.

```jsx
<Carousel
onActivePageIndexChange={(args) => {
setActivePageIndex(args.index);
}}
/>
```

- Fixed a bug where having a number of items not divisible by `itemsPerPage` would not update the `activePageIndex` to the last page as expected. (e.g., if there are 8 items and the `itemsPerPage` option is set to `3`, the tabs would never reflect scrolling to the last page.) Fixes #2.
- Fixed a bug where mouse dragging was jittery and buggy between the first and last items when `mouseDragging` and `loop="infinite"` were set.
2 changes: 1 addition & 1 deletion react-aria-carousel/jsr.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
"version": "0.0.1",
"exports": "./src/index.ts",
"publish": {
"include": ["src"],
"include": ["src", "package.json"],
},
}
1 change: 1 addition & 0 deletions react-aria-carousel/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"build": "tsup",
"dev": "tsup --watch",
"test": "vitest",
"test-storybook": "test-storybook",
"lint": "eslint .",
Expand Down
11 changes: 11 additions & 0 deletions react-aria-carousel/setupTests.ts
Original file line number Diff line number Diff line change
@@ -1 +1,12 @@
import "@testing-library/jest-dom/vitest";

import { vi } from "vitest";

const IntersectionObserverMock = vi.fn(() => ({
disconnect: vi.fn(),
observe: vi.fn(),
takeRecords: vi.fn(),
unobserve: vi.fn(),
}));

vi.stubGlobal("IntersectionObserver", IntersectionObserverMock);
89 changes: 49 additions & 40 deletions react-aria-carousel/src/Carousel.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { ComponentPropsWithoutRef } from "react";
import { ComponentPropsWithoutRef, forwardRef } from "react";

import { Context } from "./context";
import { CarouselOptions, useCarousel } from "./useCarousel";
Expand All @@ -10,43 +10,52 @@ export interface CarouselProps
extends Omit<CarouselOptions, "children">,
ComponentPropsWithoutRef<"div"> {}

export function Carousel({
children,
spaceBetweenItems = "16px",
scrollPadding,
mouseDragging,
autoplay,
autoplayInterval,
itemsPerPage = 1,
loop,
orientation = "horizontal",
scrollBy = "page",
initialPages = [],
...props
}: CarouselProps) {
const carouselProps = {
spaceBetweenItems,
scrollPadding,
mouseDragging,
autoplay,
autoplayInterval,
itemsPerPage,
loop,
orientation,
scrollBy,
initialPages,
};
const [assignRef, carouselState] = useCarousel(carouselProps);
export const Carousel = forwardRef<HTMLDivElement, CarouselProps>(
function Carousel(
{
children,
spaceBetweenItems = "16px",
scrollPadding,
mouseDragging,
autoplay,
autoplayInterval,
itemsPerPage = 1,
loop,
orientation = "horizontal",
scrollBy = "page",
initialPages = [],
onActivePageIndexChange,
...props
},
ref,
) {
const carouselProps = {
spaceBetweenItems,
scrollPadding,
mouseDragging,
autoplay,
autoplayInterval,
itemsPerPage,
loop,
orientation,
scrollBy,
initialPages,
onActivePageIndexChange,
};
const [assignRef, carouselState] = useCarousel(carouselProps);

return (
<Context.Provider
value={{
assignRef,
carouselState,
carouselProps,
}}
>
<div {...mergeProps(carouselState.rootProps, props)}>{children}</div>
</Context.Provider>
);
}
return (
<Context.Provider
value={{
assignRef,
carouselState,
carouselProps,
}}
>
<div {...mergeProps(carouselState.rootProps, props)} ref={ref}>
{children}
</div>
</Context.Provider>
);
},
);
13 changes: 7 additions & 6 deletions react-aria-carousel/src/CarouselAutoplayControl.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { ComponentPropsWithoutRef, ReactNode } from "react";
import { ComponentPropsWithoutRef, forwardRef, ReactNode } from "react";
import { mergeProps } from "@react-aria/utils";

import { useCarouselContext } from "./context";
Expand All @@ -12,15 +12,16 @@ export interface CarouselAutoplayControlProps
| ((props: { autoplayUserPreference: boolean }) => ReactNode);
}

export function CarouselAutoplayControl({
children,
...props
}: CarouselAutoplayControlProps) {
export const CarouselAutoplayControl = forwardRef<
HTMLButtonElement,
CarouselAutoplayControlProps
>(function CarouselAutoplayControl({ children, ...props }, forwardedRef) {
const { carouselState } = useCarouselContext();

return (
<button
type="button"
ref={forwardedRef}
{...mergeProps(carouselState?.autoplayControlProps, props)}
>
{typeof children === "function"
Expand All @@ -30,4 +31,4 @@ export function CarouselAutoplayControl({
: children}
</button>
);
}
});
17 changes: 13 additions & 4 deletions react-aria-carousel/src/CarouselButton.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { ComponentPropsWithoutRef } from "react";
import { ComponentPropsWithoutRef, forwardRef } from "react";
import { mergeProps } from "@react-aria/utils";

import { useCarouselContext } from "./context";
Expand All @@ -11,13 +11,22 @@ export interface CarouselButtonProps
dir: "next" | "prev";
}

export function CarouselButton({ dir, ...props }: CarouselButtonProps) {
export const CarouselButton = forwardRef<
HTMLButtonElement,
CarouselButtonProps
>(function CarouselButton({ dir, ...props }, forwardedRef) {
const { carouselState } = useCarouselContext();

const buttonProps =
dir === "prev"
? carouselState?.prevButtonProps
: carouselState?.nextButtonProps;

return <button type="button" {...mergeProps(buttonProps, props)} />;
}
return (
<button
ref={forwardedRef}
type="button"
{...mergeProps(buttonProps, props)}
/>
);
});
22 changes: 12 additions & 10 deletions react-aria-carousel/src/CarouselItem.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ComponentPropsWithoutRef, useContext } from "react";
import { ComponentPropsWithoutRef, forwardRef, useContext } from "react";

import { IndexContext, useCarouselContext } from "./context";
import { useCarouselItem } from "./useCarouselItem";
Expand All @@ -9,14 +9,16 @@ export interface CarouselItemProps extends ComponentPropsWithoutRef<"div"> {
index?: number;
}

export function CarouselItem({ index, ...props }: CarouselItemProps) {
const ctx = useCarouselContext();
const itemIndex = useContext(IndexContext);
export const CarouselItem = forwardRef<HTMLDivElement, CarouselItemProps>(
function CarouselItem({ index, ...props }, forwardedRef) {
const ctx = useCarouselContext();
const itemIndex = useContext(IndexContext);

const { itemProps } = useCarouselItem(
{ index: index ?? itemIndex },
ctx.carouselState,
);
const { itemProps } = useCarouselItem(
{ index: index ?? itemIndex },
ctx.carouselState,
);

return <div {...mergeProps(itemProps, props)} />;
}
return <div ref={forwardedRef} {...mergeProps(itemProps, props)} />;
},
);
38 changes: 28 additions & 10 deletions react-aria-carousel/src/CarouselScroller.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as React from "react";
import { ComponentPropsWithoutRef, ReactElement } from "react";

import { IndexContext, useCarouselContext } from "./context";
import { mergeProps } from "./utils";
import { mergeProps, useMergedRef } from "./utils";

export interface CarouselScrollerProps<T>
extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
Expand All @@ -21,25 +21,43 @@ export interface CarouselScrollerProps<T>
| ((item: T, index: number) => ReactElement);
}

export function CarouselScroller<T>({
children,
items,
...props
}: CarouselScrollerProps<T>) {
function _CarouselScroller<T>(
{ children, items, ...props }: CarouselScrollerProps<T>,
forwardedRef: React.ForwardedRef<HTMLElement>,
) {
const context = useCarouselContext();
const { assignRef, carouselState } = context;
const ref = useMergedRef(assignRef, forwardedRef);

const kids = typeof children === "function" ? items!.map(children) : children;

function getChildren() {
let kidsArr = React.Children.toArray(kids);
// @ts-ignore
if (kidsArr.length === 1 && kidsArr[0].type === React.Fragment) {
// @ts-ignore
kidsArr = React.Children.toArray(kidsArr[0].props.children);
}
return kidsArr;
}

return (
<div
ref={assignRef}
ref={ref}
{...mergeProps(carouselState?.scrollerProps, props)}
style={{ ...carouselState?.scrollerProps.style, ...props?.style }}
>
{React.Children.map(kids, (child, index) => (
<IndexContext.Provider value={index}>{child}</IndexContext.Provider>
))}
{getChildren().map((child, index) => {
return (
<IndexContext.Provider key={index} value={index}>
{child}
</IndexContext.Provider>
);
})}
</div>
);
}

export const CarouselScroller = React.forwardRef(_CarouselScroller) as <T>(
props: CarouselScrollerProps<T> & { ref?: React.ForwardedRef<HTMLElement> },
) => JSX.Element;
Loading

0 comments on commit b374cba

Please sign in to comment.