Skip to content

Commit 953da7d

Browse files
committed
refactor: use clip-path and percentages internally, #80
1 parent 3469f66 commit 953da7d

File tree

3 files changed

+67
-70
lines changed

3 files changed

+67
-70
lines changed

src/Container.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export const ContainerHandle = forwardRef<
4444
MozAppearance: 'none',
4545
outline: 0,
4646
transform: portrait ? `translate3d(0, -50% ,0)` : `translate3d(-50%, 0, 0)`,
47-
willChange: 'left',
47+
willChange: portrait ? 'top' : 'left',
4848
};
4949

5050
return (

src/ReactCompareSlider.tsx

+44-69
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ContainerClip, ContainerHandle } from './Container';
55
import { ReactCompareSliderHandle } from './ReactCompareSliderHandle';
66
import type { ReactCompareSliderDetailedProps } from './types';
77
import type { UseResizeObserverHandlerProps } from './utils';
8+
import { getPositionAsPercentage } from './utils';
89
import { KeyboardEventKeys, useEventListener, usePrevious, useResizeObserver } from './utils';
910

1011
/** Properties for internal `updateInternalPosition` callback. */
@@ -52,9 +53,12 @@ export const ReactCompareSlider: FC<ReactCompareSliderDetailedProps> = ({
5253
const hasWindowBinding = useRef(false);
5354
/** Target container for pointer events. */
5455
const [interactiveTarget, setInteractiveTarget] = useState<HTMLElement | null>();
55-
/** Whether the bounds of the container element have been synchronised. */
56-
// @TODO Remove this.
57-
const [didSyncBounds, setDidSyncBounds] = useState(false);
56+
const [didMount, setDidMount] = useState(false);
57+
58+
// Set mount state to ensure initial position setter is not skipped.
59+
useEffect(() => {
60+
setDidMount(true);
61+
}, []);
5862

5963
// Set target container for pointer events.
6064
useEffect(() => {
@@ -72,98 +76,69 @@ export const ReactCompareSlider: FC<ReactCompareSliderDetailedProps> = ({
7276
portrait: _portrait,
7377
boundsPadding: _boundsPadding,
7478
}: UpdateInternalPositionProps) {
75-
const { top, left, width, height } = (
79+
const { left, top, width, height } = (
7680
rootContainerRef.current as HTMLDivElement
7781
).getBoundingClientRect();
7882

79-
// Early out if width or height are zero, can't calculate values
80-
// from zeros.
83+
// Early out if width or height are zero, can't calculate values from zeros.
8184
if (width === 0 || height === 0) return;
8285

83-
/**
84-
* Pixel position clamped within the container's bounds.
85-
* @NOTE This does *not* take `boundsPadding` into account because we need
86-
* the full coords to correctly position the handle.
87-
*/
88-
const positionPx = Math.min(
89-
Math.max(
90-
// Determine bounds based on orientation
91-
_portrait
92-
? isOffset
93-
? y - top - window.pageYOffset
94-
: y
95-
: isOffset
96-
? x - left - window.pageXOffset
97-
: x,
98-
// Min value
99-
0,
100-
),
101-
// Max value
102-
_portrait ? height : width,
103-
);
104-
10586
/** Width or height with CSS scaling accounted for. */
10687
const zoomScale = _portrait
10788
? height / ((rootContainerRef.current as HTMLDivElement).offsetHeight || 1)
10889
: width / ((rootContainerRef.current as HTMLDivElement).offsetWidth || 1);
10990

110-
const adjustedPosition = positionPx / zoomScale;
111-
const adjustedWidth = width / zoomScale;
112-
const adjustedHeight = height / zoomScale;
113-
114-
/**
115-
* Internal position percentage *without* bounds.
116-
* @NOTE This uses the entire container bounds **without** `boundsPadding`
117-
* to get the *real* bounds.
118-
*/
119-
const nextInternalPositionPc =
120-
(adjustedPosition / (_portrait ? adjustedHeight : adjustedWidth)) * 100;
121-
122-
/** Whether the current pixel position meets the min/max bounds. */
123-
const positionMeetsBounds = _portrait
124-
? adjustedPosition === 0 || adjustedPosition === adjustedHeight
125-
: adjustedPosition === 0 || adjustedPosition === adjustedWidth;
126-
127-
const canSkipPositionPc =
128-
nextInternalPositionPc === internalPositionPc.current &&
91+
// Convert passed pixel to percentage using the container's bounds.
92+
const boundsPaddingPercentage =
93+
((_boundsPadding * zoomScale) / (_portrait ? height : width)) * 100;
94+
95+
const nextPosition = getPositionAsPercentage({
96+
bounds: { x, y, width, height, left, top },
97+
isOffset,
98+
portrait: _portrait,
99+
});
100+
101+
/** Next position clamped within padded `boundsPadding` box. */
102+
const nextPositionWithBoundsPadding = Math.min(
103+
Math.max(nextPosition, boundsPaddingPercentage * zoomScale),
104+
100 - boundsPaddingPercentage * zoomScale,
105+
);
106+
107+
const canSkipUpdate =
108+
didMount &&
109+
nextPosition === internalPositionPc.current &&
110+
(nextPosition === 100 || nextPosition === 0) &&
129111
(internalPositionPc.current === 0 || internalPositionPc.current === 100);
130112

131-
// Early out if pixel and percentage positions are already at the min/max
132-
// to prevent update spamming when the user is sliding outside of the
133-
// container.
134-
if (didSyncBounds && canSkipPositionPc && positionMeetsBounds) {
113+
// Early out if pixel and percentage positions are already at the min/max to prevent update
114+
// spamming when the user is sliding outside of the container.
115+
if (canSkipUpdate) {
135116
return;
136-
} else {
137-
setDidSyncBounds(true);
138117
}
139118

140119
// Set new internal position.
141-
internalPositionPc.current = nextInternalPositionPc;
142-
143-
/** Pixel position clamped to extremities *with* bounds padding. */
144-
const clampedPx = Math.min(
145-
// Get largest from pixel position *or* bounds padding.
146-
Math.max(adjustedPosition, 0 + _boundsPadding),
147-
// Use height *or* width based on orientation.
148-
(_portrait ? adjustedHeight : adjustedWidth) - _boundsPadding,
149-
);
120+
internalPositionPc.current = nextPosition;
150121

151122
(handleContainerRef.current as HTMLButtonElement).setAttribute(
152123
'aria-valuenow',
153124
`${Math.round(internalPositionPc.current)}`,
154125
);
155126

156-
(clipContainerRef.current as HTMLElement).style.clipPath = _portrait
157-
? `inset(${clampedPx}px 0 0 0)`
158-
: `inset(0 0 0 ${clampedPx}px)`;
127+
(handleContainerRef.current as HTMLElement).style.top = _portrait
128+
? `${nextPositionWithBoundsPadding}%`
129+
: '0';
130+
131+
(handleContainerRef.current as HTMLElement).style.left = _portrait
132+
? '0'
133+
: `${nextPositionWithBoundsPadding}%`;
159134

160-
(handleContainerRef.current as HTMLElement).style.transform = _portrait
161-
? `translate3d(0,calc(-50% + ${clampedPx}px),0)`
162-
: `translate3d(calc(-50% + ${clampedPx}px),0,0)`;
135+
(clipContainerRef.current as HTMLElement).style.clipPath = _portrait
136+
? `inset(${nextPositionWithBoundsPadding}% 0 0 0)`
137+
: `inset(0 0 0 ${nextPositionWithBoundsPadding}%)`;
163138

164139
if (onPositionChange) onPositionChange(internalPositionPc.current);
165140
},
166-
[didSyncBounds, onPositionChange],
141+
[didMount, onPositionChange],
167142
);
168143

169144
// Update internal position when other user controllable props change.

src/utils.ts

+22
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,25 @@ export const useResizeObserver = (
114114
};
115115
}, [handler, observe]);
116116
};
117+
118+
/** Get pixel bounds as a percentage. */
119+
export const getPositionAsPercentage = ({
120+
bounds,
121+
isOffset,
122+
portrait,
123+
}: {
124+
bounds: { x: number; y: number; width: number; height: number; top: number; left: number };
125+
isOffset?: boolean;
126+
portrait: boolean;
127+
}): number => {
128+
const targetPlane = portrait ? bounds.height : bounds.width;
129+
const targetPosition = portrait
130+
? isOffset
131+
? bounds.y - bounds.top - window.pageYOffset
132+
: bounds.y
133+
: isOffset
134+
? bounds.x - bounds.left - window.pageXOffset
135+
: bounds.x;
136+
137+
return (Math.min(Math.max(targetPosition, 0), targetPlane) / targetPlane) * 100;
138+
};

0 commit comments

Comments
 (0)