Skip to content

Commit

Permalink
feat: redesign carousel (#3177)
Browse files Browse the repository at this point in the history
  • Loading branch information
cschroeter authored Dec 28, 2024
1 parent ded1c53 commit c3f895d
Show file tree
Hide file tree
Showing 80 changed files with 1,334 additions and 1,065 deletions.
Binary file modified bun.lockb
Binary file not shown.
97 changes: 93 additions & 4 deletions packages/react/.storybook/styles/carousel.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,94 @@
[data-scope='carousel'][data-part='viewport'] {
max-width: 600px;
margin-top: 40px;
overflow-x: hidden;
[data-scope="carousel"][data-part="root"] {
display: flex;
align-items: flex-start;
flex-direction: column;
gap: 16px;
}

[data-scope="carousel"][data-part="root"][data-orientation="horizontal"] {
max-width: 400px;
}

[data-scope="carousel"][data-part="root"][data-orientation="vertical"] {
max-height: 400px;
}

[data-scope="carousel"][data-part="control"] {
display: flex;
gap: 8px;
align-self: stretch;
}

[data-scope="carousel"][data-part="item-group"] {
align-self: stretch;
/* Hide scrollbar */
scrollbar-width: none;
-webkit-scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
}
}

[data-scope="carousel"][data-part="item"] {
display: flex;
justify-content: center;
align-items: center;
font-size: 24px;
border: 1px solid lightgray;
}

[data-scope="carousel"][data-part="item"] img {
margin: 0;
object-fit: cover;
}

[data-scope="carousel"][data-part="item"][data-orientation="horizontal"] img {
height: 300px;
width: 100%;
}

[data-scope="carousel"][data-part="item"][data-orientation="vertical"] img {
height: 100%;
width: 400px;
}

[data-scope="carousel"][data-part="indicator-group"] {
display: flex;
justify-content: center;
align-items: center;
margin-block: 8px;
gap: 8px;
}

[data-scope="carousel"][data-part="indicator"] {
display: flex;
border: none;
justify-content: center;
align-items: center;
height: 12px;
width: 12px;
border-radius: 6px;
cursor: pointer;
background-color: lightgray;
transition: background-color 0.2s ease-in-out;
}

[data-scope="carousel"][data-part="indicator"][data-current] {
background-color: blue;
}

[data-scope="carousel"][data-part="autoplay-trigger"] {
display: flex;
justify-content: center;
align-items: center;
}

.carousel-spacer {
flex-grow: 1;
}

[data-scope="carousel"][data-part="autoplay-trigger"] svg {
width: 12px;
height: 12px;
}
11 changes: 9 additions & 2 deletions packages/react/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,16 @@ description: All notable changes will be documented in this file.

## [Unreleased]

### Fixed
### Added

- **Carousel [Breaking]:** Redesigned the carousel for better touch handling and performance. See the [Carousel docs](https://ark-ui.com/docs/react/components/carousel) for more info.

### Fixed

- **Select**: Fixed regression where scroll restoration in overflowing select menus was not working.
- **FileUpload:** Resolved an issue where the `accept` attribute wasn’t applied to the hidden input.
- **NumberInput:** Fixed a bug where the input event wasn’t triggered on the first click of the increment/decrement controls.
- **TreeView:** Addressed a limitation where React elements couldn’t be used in the tree view. The machine store has been revamped to support complex objects like React and Vue elements.
- **Select:** Fixed a regression where scroll restoration didn’t work in overflowing select menus.

## [4.5.0] - 2024-12-12

Expand Down
100 changes: 50 additions & 50 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -166,55 +166,55 @@
"sideEffects": false,
"dependencies": {
"@internationalized/date": "3.6.0",
"@zag-js/accordion": "0.78.3",
"@zag-js/anatomy": "0.78.3",
"@zag-js/auto-resize": "0.78.3",
"@zag-js/avatar": "0.78.3",
"@zag-js/carousel": "0.78.3",
"@zag-js/checkbox": "0.78.3",
"@zag-js/clipboard": "0.78.3",
"@zag-js/collapsible": "0.78.3",
"@zag-js/collection": "0.78.3",
"@zag-js/color-picker": "0.78.3",
"@zag-js/color-utils": "0.78.3",
"@zag-js/combobox": "0.78.3",
"@zag-js/core": "0.78.3",
"@zag-js/date-picker": "0.78.3",
"@zag-js/date-utils": "0.78.3",
"@zag-js/dialog": "0.78.3",
"@zag-js/dom-query": "0.78.3",
"@zag-js/editable": "0.78.3",
"@zag-js/file-upload": "0.78.3",
"@zag-js/file-utils": "0.78.3",
"@zag-js/highlight-word": "0.78.3",
"@zag-js/hover-card": "0.78.3",
"@zag-js/i18n-utils": "0.78.3",
"@zag-js/menu": "0.78.3",
"@zag-js/number-input": "0.78.3",
"@zag-js/pagination": "0.78.3",
"@zag-js/pin-input": "0.78.3",
"@zag-js/popover": "0.78.3",
"@zag-js/presence": "0.78.3",
"@zag-js/progress": "0.78.3",
"@zag-js/qr-code": "0.78.3",
"@zag-js/radio-group": "0.78.3",
"@zag-js/rating-group": "0.78.3",
"@zag-js/react": "0.78.3",
"@zag-js/select": "0.78.3",
"@zag-js/signature-pad": "0.78.3",
"@zag-js/slider": "0.78.3",
"@zag-js/splitter": "0.78.3",
"@zag-js/steps": "0.78.3",
"@zag-js/switch": "0.78.3",
"@zag-js/tabs": "0.78.3",
"@zag-js/tags-input": "0.78.3",
"@zag-js/time-picker": "0.78.3",
"@zag-js/timer": "0.78.3",
"@zag-js/toast": "0.78.3",
"@zag-js/toggle-group": "0.78.3",
"@zag-js/tooltip": "0.78.3",
"@zag-js/tree-view": "0.78.3",
"@zag-js/types": "0.78.3"
"@zag-js/accordion": "0.79.1",
"@zag-js/anatomy": "0.79.1",
"@zag-js/auto-resize": "0.79.1",
"@zag-js/avatar": "0.79.1",
"@zag-js/carousel": "0.79.1",
"@zag-js/checkbox": "0.79.1",
"@zag-js/clipboard": "0.79.1",
"@zag-js/collapsible": "0.79.1",
"@zag-js/collection": "0.79.1",
"@zag-js/color-picker": "0.79.1",
"@zag-js/color-utils": "0.79.1",
"@zag-js/combobox": "0.79.1",
"@zag-js/core": "0.79.1",
"@zag-js/date-picker": "0.79.1",
"@zag-js/date-utils": "0.79.1",
"@zag-js/dialog": "0.79.1",
"@zag-js/dom-query": "0.79.1",
"@zag-js/editable": "0.79.1",
"@zag-js/file-upload": "0.79.1",
"@zag-js/file-utils": "0.79.1",
"@zag-js/highlight-word": "0.79.1",
"@zag-js/hover-card": "0.79.1",
"@zag-js/i18n-utils": "0.79.1",
"@zag-js/menu": "0.79.1",
"@zag-js/number-input": "0.79.1",
"@zag-js/pagination": "0.79.1",
"@zag-js/pin-input": "0.79.1",
"@zag-js/popover": "0.79.1",
"@zag-js/presence": "0.79.1",
"@zag-js/progress": "0.79.1",
"@zag-js/qr-code": "0.79.1",
"@zag-js/radio-group": "0.79.1",
"@zag-js/rating-group": "0.79.1",
"@zag-js/react": "0.79.1",
"@zag-js/select": "0.79.1",
"@zag-js/signature-pad": "0.79.1",
"@zag-js/slider": "0.79.1",
"@zag-js/splitter": "0.79.1",
"@zag-js/steps": "0.79.1",
"@zag-js/switch": "0.79.1",
"@zag-js/tabs": "0.79.1",
"@zag-js/tags-input": "0.79.1",
"@zag-js/time-picker": "0.79.1",
"@zag-js/timer": "0.79.1",
"@zag-js/toast": "0.79.1",
"@zag-js/toggle-group": "0.79.1",
"@zag-js/tooltip": "0.79.1",
"@zag-js/tree-view": "0.79.1",
"@zag-js/types": "0.79.1"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
Expand All @@ -231,7 +231,7 @@
"@types/react": "18.3.18",
"@types/react-dom": "18.3.5",
"@vitejs/plugin-react": "4.3.4",
"@zag-js/stringify-state": "0.78.3",
"@zag-js/stringify-state": "0.79.1",
"clean-package": "2.2.0",
"globby": "14.0.2",
"jsdom": "25.0.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { mergeProps } from '@zag-js/react'
import { forwardRef } from 'react'
import { type HTMLProps, type PolymorphicProps, ark } from '../factory'
import { useCarouselContext } from './use-carousel-context'

export interface CarouselAutoplayTriggerBaseProps extends PolymorphicProps {}
export interface CarouselAutoplayTriggerProps
extends HTMLProps<'button'>,
CarouselAutoplayTriggerBaseProps {}

export const CarouselAutoplayTrigger = forwardRef<HTMLButtonElement, CarouselAutoplayTriggerProps>(
(props, ref) => {
const carousel = useCarouselContext()
const mergedProps = mergeProps(carousel.getAutoplayTriggerProps(), props)

return <ark.button {...mergedProps} ref={ref} />
},
)

CarouselAutoplayTrigger.displayName = 'CarouselAutoplayTrigger'
12 changes: 8 additions & 4 deletions packages/react/src/components/carousel/carousel-control.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { mergeProps } from '@zag-js/react'
import { forwardRef } from 'react'
import { type HTMLProps, type PolymorphicProps, ark } from '../factory'
import { carouselAnatomy } from './carousel.anatomy'
import { useCarouselContext } from './use-carousel-context'

export interface CarouselControlBaseProps extends PolymorphicProps {}
export interface CarouselControlProps extends HTMLProps<'div'>, CarouselControlBaseProps {}

export const CarouselControl = forwardRef<HTMLDivElement, CarouselControlProps>((props, ref) => (
<ark.div {...carouselAnatomy.build().control.attrs} {...props} ref={ref} />
))
export const CarouselControl = forwardRef<HTMLDivElement, CarouselControlProps>((props, ref) => {
const carousel = useCarouselContext()
const mergedProps = mergeProps(carousel.getControlProps(), props)

return <ark.div {...mergedProps} {...props} ref={ref} />
})

CarouselControl.displayName = 'CarouselControl'
2 changes: 1 addition & 1 deletion packages/react/src/components/carousel/carousel-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export interface CarouselItemBaseProps extends ItemProps, PolymorphicProps {}
export interface CarouselItemProps extends HTMLProps<'div'>, CarouselItemBaseProps {}

export const CarouselItem = forwardRef<HTMLDivElement, CarouselItemProps>((props, ref) => {
const [itemProps, localProps] = createSplitProps<ItemProps>()(props, ['index'])
const [itemProps, localProps] = createSplitProps<ItemProps>()(props, ['index', 'snapAlign'])
const carousel = useCarouselContext()
const mergedProps = mergeProps(carousel.getItemProps(itemProps), localProps)

Expand Down
19 changes: 14 additions & 5 deletions packages/react/src/components/carousel/carousel-root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,25 @@ export interface CarouselRootProps extends HTMLProps<'div'>, CarouselRootBasePro

export const CarouselRoot = forwardRef<HTMLDivElement, CarouselRootProps>((props, ref) => {
const [useCarouselProps, localProps] = createSplitProps<UseCarouselProps>()(props, [
'align',
'defaultIndex',
'allowMouseDrag',
'autoplay',
'defaultPage',
'id',
'ids',
'index',
'inViewThreshold',
'loop',
'onIndexChange',
'onAutoplayStatusChange',
'onDragStatusChange',
'onPageChange',
'orientation',
'slidesPerView',
'padding',
'page',
'slideCount',
'slidesPerMove',
'slidesPerPage',
'snapType',
'spacing',
'translations',
])
const carousel = useCarousel(useCarouselProps)
const mergedProps = mergeProps(carousel.getRootProps(), localProps)
Expand Down
16 changes: 0 additions & 16 deletions packages/react/src/components/carousel/carousel-viewport.tsx

This file was deleted.

4 changes: 1 addition & 3 deletions packages/react/src/components/carousel/carousel.anatomy.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
import { anatomy } from '@zag-js/carousel'

export const carouselAnatomy = anatomy.extendWith('control')
export { anatomy as carouselAnatomy } from '@zag-js/carousel'
4 changes: 2 additions & 2 deletions packages/react/src/components/carousel/carousel.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const meta: Meta = {

export default meta

export { Autoplay } from './examples/autoplay'
export { Basic } from './examples/basic'
export { RootProvider } from './examples/root-provider'
export { Controlled } from './examples/controlled'
export { Customized } from './examples/customized'
export { RootProvider } from './examples/root-provider'
47 changes: 47 additions & 0 deletions packages/react/src/components/carousel/carousel.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { cleanup, render, screen, waitFor } from '@testing-library/react/pure'
import { axe } from 'vitest-axe'
import { Carousel, carouselAnatomy } from '.'
import { getExports, getParts } from '../../setup-test'
import { Basic as ComponentUnderTest } from './examples/basic'

describe('Carousel / Parts & Exports', () => {
afterAll(() => {
cleanup()
})

render(<ComponentUnderTest />)

const renderedParts = getParts(carouselAnatomy).filter(
(part) => !part.includes('[data-part="autoplay-trigger"]'),
)

it.each(renderedParts)('should render part %s', async (part) => {
expect(document.querySelector(part)).toBeInTheDocument()
})

it.each(getExports(carouselAnatomy))('should export %s', async (part) => {
expect(Carousel[part]).toBeDefined()
})
})

describe('Carousel', () => {
afterEach(() => {
cleanup()
})

it('should have no a11y violations', async () => {
const { container } = render(<ComponentUnderTest />)
const results = await axe(container)

expect(results).toHaveNoViolations()
})

it('should have the correct disabled / enabled states for control buttons', async () => {
render(<ComponentUnderTest />)
const prevButton = screen.getByRole('button', { name: 'Previous slide' })
const nextButton = screen.getByRole('button', { name: 'Next slide' })

await waitFor(() => expect(prevButton).toBeDisabled())
await waitFor(() => expect(nextButton).toBeEnabled())
})
})
Loading

0 comments on commit c3f895d

Please sign in to comment.