Skip to content

Commit

Permalink
feat: add transition property and onSliderTransitionEnd callback …
Browse files Browse the repository at this point in the history
…to animate the slider, closes #77
  • Loading branch information
nerdyman committed Apr 28, 2023
1 parent ce2bd06 commit 54cd6cd
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 62 deletions.
5 changes: 0 additions & 5 deletions .npmrc.off

This file was deleted.

114 changes: 62 additions & 52 deletions src/Container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,65 +4,75 @@ import type { CSSProperties, HTMLProps, ReactElement } from 'react';
import type { ReactCompareSliderCommonProps } from './types';

/** Container for clipped item. */
export const ContainerClip = forwardRef<HTMLDivElement, HTMLProps<HTMLDivElement>>(
(props, ref): ReactElement => {
const style: CSSProperties = {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
userSelect: 'none',
willChange: 'clip-path',
KhtmlUserSelect: 'none',
MozUserSelect: 'none',
WebkitUserSelect: 'none',
};
export const ContainerClip = forwardRef<
HTMLDivElement,
HTMLProps<HTMLDivElement> & Pick<ReactCompareSliderCommonProps, 'transition'>
>(({ transition, ...props }, ref): ReactElement => {
const style: CSSProperties = {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
transition: transition ? `clip-path ${transition}` : undefined,
userSelect: 'none',
willChange: 'clip-path, transition',
KhtmlUserSelect: 'none',
MozUserSelect: 'none',
WebkitUserSelect: 'none',
};

return <div {...props} style={style} data-rcs="clip-item" ref={ref} />;
},
);
return <div {...props} style={style} data-rcs="clip-item" ref={ref} />;
});

ContainerClip.displayName = 'ContainerClip';

/** Container to control the handle's position. */
export const ContainerHandle = forwardRef<
HTMLButtonElement,
HTMLProps<HTMLButtonElement> &
Pick<ReactCompareSliderCommonProps, 'disabled' | 'portrait' | 'position'>
>(({ children, disabled, portrait, position }, ref): ReactElement => {
const style: CSSProperties = {
position: 'absolute',
top: 0,
width: portrait ? '100%' : undefined,
height: portrait ? undefined : '100%',
background: 'none',
border: 0,
padding: 0,
pointerEvents: 'all',
appearance: 'none',
WebkitAppearance: 'none',
MozAppearance: 'none',
outline: 0,
transform: portrait ? `translate3d(0, -50% ,0)` : `translate3d(-50%, 0, 0)`,
willChange: portrait ? 'top' : 'left',
};
HTMLProps<HTMLButtonElement> & ReactCompareSliderCommonProps
>(
(
{ children, disabled, portrait, position, transition, onSliderTransitionEnd },
ref,
): ReactElement => {
const targetAxis = portrait ? 'top' : 'left';

return (
<button
ref={ref}
aria-orientation={portrait ? 'vertical' : 'horizontal'}
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={position}
data-rcs="handle-container"
disabled={disabled}
role="slider"
style={style}
>
{children}
</button>
);
});
const style: CSSProperties = {
position: 'absolute',
top: 0,
width: portrait ? '100%' : undefined,
height: portrait ? undefined : '100%',
background: 'none',
border: 0,
padding: 0,
pointerEvents: 'all',
appearance: 'none',
WebkitAppearance: 'none',
MozAppearance: 'none',
outline: 0,
transform: portrait ? `translate3d(0, -50% ,0)` : `translate3d(-50%, 0, 0)`,
transition: transition ? `${targetAxis} ${transition}` : undefined,
willChange: 'transform, ${targetAxis}',
};

return (
<button
ref={ref}
aria-orientation={portrait ? 'vertical' : 'horizontal'}
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={position}
data-rcs="handle-container"
disabled={disabled}
role="slider"
style={style}
onTransitionEnd={onSliderTransitionEnd}
>
{children}
</button>
);
},
);

ContainerHandle.displayName = 'ThisHandleContainer';
14 changes: 13 additions & 1 deletion src/ReactCompareSlider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export const ReactCompareSlider = forwardRef<
changePositionOnHover = false,
keyboardIncrement = '5%',
style,
transition,
...props
},
ref,
Expand All @@ -63,6 +64,8 @@ export const ReactCompareSlider = forwardRef<
const internalPosition = useRef(position);
/** Whether user is currently dragging. */
const [isDragging, setIsDragging] = useState(false);
/** Whether the `transition` property can be applied. */
const [canTransition, setCanTransition] = useState(true);
/** Whether component has a `window` event binding. */
const hasWindowBinding = useRef(false);
/** Target container for pointer events. */
Expand Down Expand Up @@ -156,6 +159,7 @@ export const ReactCompareSlider = forwardRef<

updateInternalPosition({ isOffset: true, x: ev.pageX, y: ev.pageY });
setIsDragging(true);
setCanTransition(true);
},
[disabled, updateInternalPosition],
);
Expand All @@ -164,13 +168,15 @@ export const ReactCompareSlider = forwardRef<
const handlePointerMove = useCallback(
function moveCall(ev: PointerEvent) {
updateInternalPosition({ isOffset: true, x: ev.pageX, y: ev.pageY });
setCanTransition(false);
},
[updateInternalPosition],
);

/** Handle mouse/touch up. */
const handlePointerUp = useCallback(() => {
setIsDragging(false);
setCanTransition(true);
}, []);

/** Resync internal position on resize. */
Expand Down Expand Up @@ -206,6 +212,8 @@ export const ReactCompareSlider = forwardRef<

ev.preventDefault();

setCanTransition(true);

const { top, left } = (
handleContainerRef.current as HTMLButtonElement
).getBoundingClientRect();
Expand Down Expand Up @@ -337,6 +345,7 @@ export const ReactCompareSlider = forwardRef<

// Use custom handle if requested.
const Handle = handle || <ReactCompareSliderHandle disabled={disabled} portrait={portrait} />;
const appliedTransition = canTransition ? transition : undefined;

const rootStyle: CSSProperties = {
position: 'relative',
Expand All @@ -354,13 +363,16 @@ export const ReactCompareSlider = forwardRef<
return (
<div {...props} ref={rootContainerRef} style={rootStyle} data-rcs="root">
{itemOne}
<ContainerClip ref={clipContainerRef}>{itemTwo}</ContainerClip>
<ContainerClip ref={clipContainerRef} transition={appliedTransition}>
{itemTwo}
</ContainerClip>

<ContainerHandle
disabled={disabled}
portrait={portrait}
position={Math.round(internalPosition.current)}
ref={handleContainerRef}
transition={appliedTransition}
>
{Handle}
</ContainerHandle>
Expand Down
6 changes: 5 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { HtmlHTMLAttributes, ReactNode } from 'react';
import type { HtmlHTMLAttributes, ReactNode, TransitionEventHandler } from 'react';

/** Slider position property. */
export type ReactCompareSliderPropPosition = number;
Expand All @@ -11,6 +11,10 @@ export interface ReactCompareSliderCommonProps {
portrait?: boolean;
/** Divider position. */
position: ReactCompareSliderPropPosition;
/** CSS transition properties to apply to handle movement. */
transition?: string;
/** Callback fired when applied `transition` completes. */
onSliderTransitionEnd?: TransitionEventHandler<HTMLElement>;
}

/** Comparison slider properties. */
Expand Down
6 changes: 3 additions & 3 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,11 @@ export const useEventListener = (

/**
* Conditionally use `useLayoutEffect` for client *or* `useEffect` for SSR.
* @see https://github.com/reduxjs/react-redux/blob/c581d480dd675f2645851fb006bef91aeb6ac24d/src/utils/useIsomorphicLayoutEffect.js
* @see https://github.com/reduxjs/react-redux/blob/89a86805f2fcf9e8fbd2d1dae345ec791de4a71f/src/utils/useIsomorphicLayoutEffect.ts
*/
export const useIsomorphicLayoutEffect =
const useIsomorphicLayoutEffect =
typeof window !== 'undefined' &&
window.document &&
typeof window.document !== 'undefined' &&
typeof window.document.createElement !== 'undefined'
? useLayoutEffect
: useEffect;
Expand Down

0 comments on commit 54cd6cd

Please sign in to comment.