Skip to content

Commit

Permalink
Feature: Additional render*Controls helpers (#966)
Browse files Browse the repository at this point in the history
* feat: pass nextDisabled and previousDisabled to custom controls

* feat: pass dotNavigationIndices to custom controls

* docs: use new render props in storybook story

* refactor: create new util to see where unbounded indices lie in the original (0 ≥ index > slideCount) range

* chore: remove unnecessary comment
  • Loading branch information
fritz-c authored Sep 8, 2022
1 parent c533714 commit 9f88275
Show file tree
Hide file tree
Showing 9 changed files with 198 additions and 144 deletions.
5 changes: 5 additions & 0 deletions .changeset/shiny-cats-smell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'nuka-carousel': minor
---

pass nextDisabled, previousDisabled, and dotNavigationIndices to render\*Controls callbacks to aid in the creation of custom controls
43 changes: 37 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,24 +114,55 @@ A set of eight render props for rendering controls in different positions around

- The default props are set as `renderCenterLeftControls` for `Previous` button, `renderCenterRightControls` for the `Next` button and `renderBottomCenterControls` for the "Paging dots". To change the position or remove "Paging dots", the default positions need to be disabled by setting them to null.

- You can remove all render controls using the `withoutControls` prop on `Carousel`.

- The render functions receive a `ControlProps` argument containing the following props from the Carousel props, using default values if not originally defined:

```
cellAlign
cellSpacing
defaultControlsConfig
scrollMode
slidesToScroll
slidesToShow
wrapAround
```

Additionally, the following data and callbacks are provided to make creating controls easier:

| Name | Type | Description |
| :------------------- | ------------------------------- | :------------------------------------------------------ |
| currentSlide | `number` | Current slide index |
| dotNavigationIndices | `number[]` | The indices for the navigation dots |
| goToSlide | `(targetIndex: number) => void` | Go to a specific slide |
| nextDisabled | `boolean` | Whether the "next" button should be disabled or not |
| nextSlide | `() => void` | Go to the next slide |
| previousDisabled | `boolean` | Whether the "previous" button should be disabled or not |
| previousSlide | `() => void` | Go to the previous slide |
| slideCount | `number` | Total number of slides |

Example:

```jsx
<Carousel
renderTopCenterControls={({ currentSlide }) => (
<div>Slide: {currentSlide}</div>
)}
renderCenterLeftControls={({ previousSlide }) => (
<button onClick={previousSlide}>Previous</button>
renderCenterLeftControls={({ previousDisabled, previousSlide }) => (
<button onClick={previousSlide} disabled={previousDisabled}>
Previous
</button>
)}
renderCenterRightControls={({ nextSlide }) => (
<button onClick={nextSlide}>Next</button>
renderCenterRightControls={({ nextDisabled, nextSlide }) => (
<button onClick={nextSlide} disabled={nextDisabled}>
Next
</button>
)}
>
{/* Carousel Content */}
</Carousel>
```

- The function returns the props for `goToSlide`, `nextSlide` and `previousSlide` functions, in addition to `slideCount` and `currentSlide` values. You can also remove all render controls using `withoutControls`.

- NOTE: The className `slide-visible` is added to the currently visible slide or slides (when `slidesToShow` > 1). The className `slide-current` is added to the currently "active" slide.

#### renderAnnounceSlideMessage
Expand Down
21 changes: 7 additions & 14 deletions packages/nuka/src/carousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import {
import renderControls from './controls';
import defaultProps from './default-carousel-props';
import {
getIndexes,
getNextMoveIndex,
getPrevMoveIndex,
getDefaultSlideIndex,
getBoundedIndex,
} from './utils';
import { useFrameHeight } from './hooks/use-frame-height';
import { getDotIndexes } from './default-controls';
Expand Down Expand Up @@ -99,11 +99,7 @@ export const Carousel = (rawProps: CarouselProps): React.ReactElement => {
const animationEndTimeout = useRef<ReturnType<typeof setTimeout>>();
const isMounted = useRef<boolean>(true);

const [slide] = getIndexes(
currentSlide,
currentSlide - slidesToScroll,
slideCount
);
const currentSlideBounded = getBoundedIndex(currentSlide, slideCount);

useEffect(() => {
isMounted.current = true;
Expand All @@ -123,13 +119,10 @@ export const Carousel = (rawProps: CarouselProps): React.ReactElement => {

const goToSlide = useCallback(
(targetSlideIndex: number) => {
// Boil down the target index (-Infinity < targetSlideIndex < Infinity) to
// a user-friendly index (0 ≤ targetSlideIndex < slideCount)
const userFacingIndex =
((targetSlideIndex % slideCount) + slideCount) % slideCount;
const nextSlideBounded = getBoundedIndex(targetSlideIndex, slideCount);

const slideChanged = targetSlideIndex !== currentSlide;
slideChanged && beforeSlide(slide, userFacingIndex);
slideChanged && beforeSlide(currentSlideBounded, nextSlideBounded);

// if animation is disabled decrease the speed to 40
const msToEndOfAnimation = !disableAnimation ? propsSpeed || 500 : 40;
Expand All @@ -149,12 +142,12 @@ export const Carousel = (rawProps: CarouselProps): React.ReactElement => {

setTimeout(() => {
if (!isMounted.current) return;
afterSlide(userFacingIndex);
afterSlide(nextSlideBounded);
}, msToEndOfAnimation);
}
},
[
slide,
currentSlideBounded,
afterSlide,
beforeSlide,
slideCount,
Expand Down Expand Up @@ -601,7 +594,7 @@ export const Carousel = (rawProps: CarouselProps): React.ReactElement => {
<AnnounceSlide
ariaLive={autoplay && !pause ? 'off' : 'polite'}
message={renderAnnounceSlideMessage({
currentSlide: slide,
currentSlide: currentSlideBounded,
count: slideCount,
})}
/>
Expand Down
25 changes: 25 additions & 0 deletions packages/nuka/src/controls.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import React, { Fragment } from 'react';
import { getControlContainerStyles } from './control-styles';
import {
getDotIndexes,
nextButtonDisabled,
prevButtonDisabled,
} from './default-controls';
import {
InternalCarouselProps,
Positions,
Expand Down Expand Up @@ -31,6 +36,23 @@ const renderControls = (
if (props.withoutControls) {
return null;
}

const disableCheckProps = {
...props,
currentSlide,
slideCount,
};
const nextDisabled = nextButtonDisabled(disableCheckProps);
const previousDisabled = prevButtonDisabled(disableCheckProps);
const dotNavigationIndices = getDotIndexes(
slideCount,
slidesToScroll,
props.scrollMode,
props.slidesToShow,
props.wrapAround,
props.cellAlign
);

return controlsMap.map((control) => {
if (
!props[control.funcName] ||
Expand Down Expand Up @@ -62,8 +84,11 @@ const renderControls = (
cellSpacing: props.cellSpacing,
currentSlide,
defaultControlsConfig: props.defaultControlsConfig || {},
dotNavigationIndices,
goToSlide,
nextDisabled,
nextSlide,
previousDisabled,
previousSlide: prevSlide,
scrollMode: props.scrollMode,
slideCount,
Expand Down
111 changes: 54 additions & 57 deletions packages/nuka/src/default-controls.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable complexity */
import React, { CSSProperties, useCallback } from 'react';
import { Alignment, ControlProps, ScrollMode } from './types';
import { getBoundedIndex } from './utils';

const defaultButtonStyles = (disabled: boolean): CSSProperties => ({
border: 0,
Expand All @@ -13,11 +13,14 @@ const defaultButtonStyles = (disabled: boolean): CSSProperties => ({
});

export const prevButtonDisabled = ({
currentSlide,
wrapAround,
cellAlign,
currentSlide,
slidesToShow,
}: ControlProps) => {
wrapAround,
}: Pick<
ControlProps,
'cellAlign' | 'currentSlide' | 'slidesToShow' | 'wrapAround'
>) => {
// inifite carousel
if (wrapAround) {
return false;
Expand All @@ -36,19 +39,19 @@ export const prevButtonDisabled = ({
return false;
};

export const PreviousButton = (props: ControlProps) => {
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
event.preventDefault();
props?.previousSlide();
};

const {
export const PreviousButton = ({
previousSlide,
defaultControlsConfig: {
prevButtonClassName,
prevButtonStyle = {},
prevButtonText,
} = props.defaultControlsConfig || {};

const disabled = prevButtonDisabled(props);
},
previousDisabled: disabled,
}: ControlProps) => {
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
event.preventDefault();
previousSlide();
};

return (
<button
Expand All @@ -68,12 +71,15 @@ export const PreviousButton = (props: ControlProps) => {
};

export const nextButtonDisabled = ({
cellAlign,
currentSlide,
slideCount,
slidesToShow,
wrapAround,
cellAlign,
}: ControlProps) => {
}: Pick<
ControlProps,
'cellAlign' | 'currentSlide' | 'slideCount' | 'slidesToShow' | 'wrapAround'
>) => {
// inifite carousel
if (wrapAround) {
return false;
Expand All @@ -95,21 +101,19 @@ export const nextButtonDisabled = ({
return false;
};

export const NextButton = (props: ControlProps) => {
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
event.preventDefault();
props.nextSlide();
};

const { defaultControlsConfig } = props;

const {
export const NextButton = ({
nextSlide,
defaultControlsConfig: {
nextButtonClassName,
nextButtonStyle = {},
nextButtonText,
} = defaultControlsConfig;

const disabled = nextButtonDisabled(props);
},
nextDisabled: disabled,
}: ControlProps) => {
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
event.preventDefault();
nextSlide();
};

return (
<button
Expand Down Expand Up @@ -219,7 +223,17 @@ export const getDotIndexes = (
return dotIndexes;
};

export const PagingDots = (props: ControlProps) => {
export const PagingDots = ({
dotNavigationIndices,
defaultControlsConfig: {
pagingDotsContainerClassName,
pagingDotsClassName,
pagingDotsStyle = {},
},
currentSlide,
slideCount,
goToSlide,
}: ControlProps) => {
const listStyles: CSSProperties = {
position: 'relative',
top: -10,
Expand All @@ -239,37 +253,20 @@ export const PagingDots = (props: ControlProps) => {
}),
[]
);

const indexes = getDotIndexes(
props.slideCount,
props.slidesToScroll,
props.scrollMode,
props.slidesToShow,
props.wrapAround,
props.cellAlign
);

const {
pagingDotsContainerClassName,
pagingDotsClassName,
pagingDotsStyle = {},
} = props.defaultControlsConfig;
const currentSlideBounded = getBoundedIndex(currentSlide, slideCount);

return (
<ul className={pagingDotsContainerClassName} style={listStyles}>
{indexes.map((index, i) => {
let isActive =
props.currentSlide === index ||
props.currentSlide - props.slideCount === index ||
props.currentSlide + props.slideCount === index;

// the below condition checks and sets navigation dots active if the current slide falls in the current index range
if (props.currentSlide < index && props.currentSlide > indexes[i - 1]) {
isActive = true;
}
{dotNavigationIndices.map((slideIndex, i) => {
const isActive =
currentSlideBounded === slideIndex ||
// sets navigation dots active if the current slide falls in the current index range
(currentSlideBounded < slideIndex &&
(i === 0 || currentSlideBounded > dotNavigationIndices[i - 1]));

return (
<li
key={index}
key={slideIndex}
className={isActive ? 'paging-item active' : 'paging-item'}
>
<button
Expand All @@ -279,8 +276,8 @@ export const PagingDots = (props: ControlProps) => {
...getButtonStyles(isActive),
...pagingDotsStyle,
}}
onClick={props.goToSlide.bind(null, index)}
aria-label={`slide ${index + 1} bullet`}
onClick={() => goToSlide(slideIndex)}
aria-label={`slide ${slideIndex + 1} bullet`}
aria-selected={isActive}
>
<svg
Expand Down
Loading

1 comment on commit 9f88275

@vercel
Copy link

@vercel vercel bot commented on 9f88275 Sep 8, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.