Skip to content

Commit 5dbbeb1

Browse files
committed
feat(alpha): support reversing slider direction #202
#202
1 parent 1b3e430 commit 5dbbeb1

3 files changed

Lines changed: 229 additions & 21 deletions

File tree

packages/color-alpha/README.md

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,42 @@ export default function Demo() {
6767
const [hsva, setHsva] = useState({ h: 0, s: 0, v: 68, a: 1 });
6868
return (
6969
<>
70-
<Alpha
71-
hsva={hsva}
72-
onChange={(newAlpha) => {
73-
setHsva({ ...hsva, ...newAlpha });
74-
}}
75-
/>
70+
<div style={{ display: "flex", gap: 10, flexDirection: "column", paddingBottom: 20 }}>
71+
<Alpha
72+
hsva={hsva}
73+
onChange={(newAlpha) => {
74+
setHsva({ ...hsva, ...newAlpha });
75+
}}
76+
/>
77+
<Alpha
78+
hsva={hsva}
79+
reverse
80+
onChange={(newAlpha) => {
81+
setHsva({ ...hsva, ...newAlpha });
82+
}}
83+
/>
84+
</div>
85+
<div style={{ display: "flex", gap: 10, flexDirection: "row" }}>
86+
<Alpha
87+
hsva={hsva}
88+
direction="vertical"
89+
width={16}
90+
height={120}
91+
onChange={(newAlpha) => {
92+
setHsva({ ...hsva, ...newAlpha });
93+
}}
94+
/>
95+
<Alpha
96+
hsva={hsva}
97+
direction="vertical"
98+
reverse={true}
99+
width={16}
100+
height={120}
101+
onChange={(newAlpha) => {
102+
setHsva({ ...hsva, ...newAlpha });
103+
}}
104+
/>
105+
</div>
76106
<div style={{ background: hsvaToRgbaString(hsva), marginTop: 30, padding: 10 }}>
77107
{JSON.stringify(hsva)}
78108
</div>
@@ -111,6 +141,8 @@ export interface AlphaProps extends Omit<React.HTMLAttributes<HTMLDivElement>, '
111141
pointerProps?: PointerProps;
112142
/** String Enum, horizontal or vertical. Default `horizontal` */
113143
direction?: 'vertical' | 'horizontal';
144+
/** Flip alpha progression along the current axis. */
145+
reverse?: boolean;
114146
onChange?: (newAlpha: {
115147
a: number;
116148
}, offset: Interaction) => void;

packages/color-alpha/src/index.tsx

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ export interface AlphaProps extends Omit<React.HTMLAttributes<HTMLDivElement>, '
2727
pointerProps?: PointerProps;
2828
/** String Enum, horizontal or vertical. Default `horizontal` */
2929
direction?: 'vertical' | 'horizontal';
30+
/** Flip alpha progression along the current axis. */
31+
reverse?: boolean;
3032
onChange?: (newAlpha: { a: number }, offset: Interaction) => void;
3133
}
3234

@@ -46,25 +48,52 @@ const Alpha = React.forwardRef<HTMLDivElement, AlphaProps>((props, ref) => {
4648
width,
4749
height = 16,
4850
direction = 'horizontal',
51+
reverse = false,
4952
style,
5053
onChange,
5154
pointer,
5255
...other
5356
} = props;
5457

58+
const offsetToAlpha = useCallback(
59+
(offset: Interaction) => {
60+
const value = direction === 'horizontal' ? offset.left : offset.top;
61+
if (direction === 'horizontal') {
62+
return reverse ? 1 - value : value;
63+
}
64+
return reverse ? value : 1 - value;
65+
},
66+
[direction, reverse],
67+
);
68+
69+
const alphaToOffset = useCallback(
70+
(alpha: number) => {
71+
if (direction === 'horizontal') {
72+
return reverse ? 1 - alpha : alpha;
73+
}
74+
return reverse ? alpha : 1 - alpha;
75+
},
76+
[direction, reverse],
77+
);
78+
5579
const handleChange = (offset: Interaction) => {
56-
onChange && onChange({ ...hsva, a: direction === 'horizontal' ? offset.left : offset.top }, offset);
80+
const alpha = offsetToAlpha(offset);
81+
onChange && onChange({ ...hsva, a: alpha }, offset);
5782
};
5883

5984
const colorTo = hsvaToHslaString(Object.assign({}, hsva, { a: 1 }));
60-
const innerBackground = `linear-gradient(to ${
61-
direction === 'horizontal' ? 'right' : 'bottom'
62-
}, rgba(244, 67, 54, 0) 0%, ${colorTo} 100%)`;
85+
const horizontalGradient = reverse
86+
? `linear-gradient(to right, ${colorTo} 0%, rgba(244, 67, 54, 0) 100%)`
87+
: `linear-gradient(to right, rgba(244, 67, 54, 0) 0%, ${colorTo} 100%)`;
88+
const verticalGradient = reverse
89+
? `linear-gradient(to bottom, rgba(244, 67, 54, 0) 0%, ${colorTo} 100%)`
90+
: `linear-gradient(to bottom, ${colorTo} 0%, rgba(244, 67, 54, 0) 100%)`;
91+
const innerBackground = direction === 'horizontal' ? horizontalGradient : verticalGradient;
6392
const comProps: { left?: string; top?: string } = {};
6493
if (direction === 'horizontal') {
65-
comProps.left = `${hsva.a * 100}%`;
94+
comProps.left = `${alphaToOffset(hsva.a) * 100}%`;
6695
} else {
67-
comProps.top = `${hsva.a * 100}%`;
96+
comProps.top = `${alphaToOffset(hsva.a) * 100}%`;
6897
}
6998
const styleWrapper: CSS.Properties<string | number> = {
7099
'--alpha-background-color': '#fff',
@@ -86,25 +115,25 @@ const Alpha = React.forwardRef<HTMLDivElement, AlphaProps>((props, ref) => {
86115
switch (event.key) {
87116
case 'ArrowLeft':
88117
if (direction === 'horizontal') {
89-
newAlpha = Math.max(0, currentAlpha - step);
118+
newAlpha = reverse ? Math.min(1, currentAlpha + step) : Math.max(0, currentAlpha - step);
90119
event.preventDefault();
91120
}
92121
break;
93122
case 'ArrowRight':
94123
if (direction === 'horizontal') {
95-
newAlpha = Math.min(1, currentAlpha + step);
124+
newAlpha = reverse ? Math.max(0, currentAlpha - step) : Math.min(1, currentAlpha + step);
96125
event.preventDefault();
97126
}
98127
break;
99128
case 'ArrowUp':
100129
if (direction === 'vertical') {
101-
newAlpha = Math.max(0, currentAlpha - step);
130+
newAlpha = reverse ? Math.max(0, currentAlpha - step) : Math.min(1, currentAlpha + step);
102131
event.preventDefault();
103132
}
104133
break;
105134
case 'ArrowDown':
106135
if (direction === 'vertical') {
107-
newAlpha = Math.min(1, currentAlpha + step);
136+
newAlpha = reverse ? Math.min(1, currentAlpha + step) : Math.max(0, currentAlpha - step);
108137
event.preventDefault();
109138
}
110139
break;
@@ -113,9 +142,10 @@ const Alpha = React.forwardRef<HTMLDivElement, AlphaProps>((props, ref) => {
113142
}
114143

115144
if (newAlpha !== currentAlpha) {
145+
const syntheticAxisOffset = alphaToOffset(newAlpha);
116146
const syntheticOffset: Interaction = {
117-
left: direction === 'horizontal' ? newAlpha : hsva.a,
118-
top: direction === 'vertical' ? newAlpha : hsva.a,
147+
left: direction === 'horizontal' ? syntheticAxisOffset : hsva.a,
148+
top: direction === 'vertical' ? syntheticAxisOffset : hsva.a,
119149
width: 0,
120150
height: 0,
121151
x: 0,
@@ -124,7 +154,7 @@ const Alpha = React.forwardRef<HTMLDivElement, AlphaProps>((props, ref) => {
124154
onChange && onChange({ ...hsva, a: newAlpha }, syntheticOffset);
125155
}
126156
},
127-
[hsva, direction, onChange],
157+
[alphaToOffset, hsva, direction, onChange, reverse],
128158
);
129159

130160
const handleClick = useCallback((event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {

test/alpha.test.tsx

Lines changed: 148 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,41 @@ it('Alpha direction = vertical', async () => {
6262
if (tree && !Array.isArray(tree)) {
6363
expect(tree.type).toEqual('div');
6464
expect(tree.props.className).toEqual('w-color-alpha w-color-alpha-vertical');
65+
expect(tree.children?.[0]).toMatchObject({
66+
type: 'div',
67+
props: {
68+
style: expect.objectContaining({
69+
background: 'linear-gradient(to bottom, hsla(0, 0%, 68%, 1) 0%, rgba(244, 67, 54, 0) 100%)',
70+
}),
71+
},
72+
});
73+
}
74+
});
75+
76+
it('Alpha reverse renders opposite horizontal gradient and pointer position', async () => {
77+
const component = TestRenderer.create(<Alpha hsva={{ h: 0, s: 0, v: 68, a: 0.25 }} reverse />);
78+
const tree = component.toJSON();
79+
if (tree && !Array.isArray(tree)) {
80+
expect(tree.children?.[0]).toMatchObject({
81+
type: 'div',
82+
props: {
83+
style: expect.objectContaining({
84+
background: 'linear-gradient(to right, hsla(0, 0%, 68%, 1) 0%, rgba(244, 67, 54, 0) 100%)',
85+
}),
86+
},
87+
});
88+
expect(tree.children?.[1]).toMatchObject({
89+
type: 'div',
90+
children: expect.arrayContaining([
91+
expect.objectContaining({
92+
props: expect.objectContaining({
93+
style: expect.objectContaining({
94+
left: '75%',
95+
}),
96+
}),
97+
}),
98+
]),
99+
});
65100
}
66101
});
67102

@@ -100,7 +135,118 @@ it('Alpha direction = vertical & onChange', async () => {
100135
/>,
101136
);
102137
const elm = getByTitle('test');
103-
fireEvent(elm, new FakeMouseEvent('mousedown', { pageX: 20, pageY: 1 }));
138+
jest.spyOn(elm, 'getBoundingClientRect').mockReturnValue({
139+
x: 0,
140+
y: 0,
141+
top: 0,
142+
left: 0,
143+
bottom: 100,
144+
right: 20,
145+
width: 20,
146+
height: 100,
147+
toJSON: () => ({}),
148+
});
149+
fireEvent(elm, new FakeMouseEvent('mousedown', { pageX: 10, pageY: 1 }));
104150
fireEvent(elm, new FakeMouseEvent('mousemove', { pageX: 10, pageY: 1 }));
105-
expect(handleChange).toHaveReturnedWith({ a: 1, h: 0, s: 0, v: 68 });
151+
expect(handleChange).toHaveBeenLastCalledWith({ a: 0.99, h: 0, s: 0, v: 68 }, expect.objectContaining({ top: 0.01 }));
152+
});
153+
154+
it('Alpha direction = vertical maps top to 1 and bottom to 0', async () => {
155+
const handleChange = jest.fn((newAlpha) => newAlpha);
156+
const { getByTitle } = render(
157+
<Alpha
158+
innerProps={{
159+
title: 'vertical-alpha',
160+
}}
161+
direction="vertical"
162+
onChange={handleChange}
163+
hsva={{ h: 0, s: 0, v: 68, a: 0.5 }}
164+
/>,
165+
);
166+
167+
const elm = getByTitle('vertical-alpha');
168+
jest.spyOn(elm, 'getBoundingClientRect').mockReturnValue({
169+
x: 0,
170+
y: 0,
171+
top: 0,
172+
left: 0,
173+
bottom: 100,
174+
right: 20,
175+
width: 20,
176+
height: 100,
177+
toJSON: () => ({}),
178+
});
179+
180+
fireEvent(elm, new FakeMouseEvent('mousedown', { pageX: 10, pageY: 0 }));
181+
fireEvent(elm, new FakeMouseEvent('mousemove', { pageX: 10, pageY: 100 }));
182+
183+
expect(handleChange).toHaveBeenNthCalledWith(1, { a: 1, h: 0, s: 0, v: 68 }, expect.objectContaining({ top: 0 }));
184+
expect(handleChange).toHaveBeenNthCalledWith(2, { a: 0, h: 0, s: 0, v: 68 }, expect.objectContaining({ top: 1 }));
185+
});
186+
187+
it('Alpha reverse maps horizontal left to 1 and right to 0', async () => {
188+
const handleChange = jest.fn((newAlpha) => newAlpha);
189+
const { getByTitle } = render(
190+
<Alpha
191+
innerProps={{
192+
title: 'reverse-horizontal-alpha',
193+
}}
194+
reverse
195+
onChange={handleChange}
196+
hsva={{ h: 0, s: 0, v: 68, a: 0.5 }}
197+
/>,
198+
);
199+
200+
const elm = getByTitle('reverse-horizontal-alpha');
201+
jest.spyOn(elm, 'getBoundingClientRect').mockReturnValue({
202+
x: 0,
203+
y: 0,
204+
top: 0,
205+
left: 0,
206+
bottom: 20,
207+
right: 100,
208+
width: 100,
209+
height: 20,
210+
toJSON: () => ({}),
211+
});
212+
213+
fireEvent(elm, new FakeMouseEvent('mousedown', { pageX: 0, pageY: 10 }));
214+
fireEvent(elm, new FakeMouseEvent('mousemove', { pageX: 100, pageY: 10 }));
215+
216+
expect(handleChange).toHaveBeenNthCalledWith(1, { a: 1, h: 0, s: 0, v: 68 }, expect.objectContaining({ left: 0 }));
217+
expect(handleChange).toHaveBeenNthCalledWith(2, { a: 0, h: 0, s: 0, v: 68 }, expect.objectContaining({ left: 1 }));
218+
});
219+
220+
it('Alpha reverse maps vertical top to 0 and bottom to 1', async () => {
221+
const handleChange = jest.fn((newAlpha) => newAlpha);
222+
const { getByTitle } = render(
223+
<Alpha
224+
innerProps={{
225+
title: 'reverse-vertical-alpha',
226+
}}
227+
direction="vertical"
228+
reverse
229+
onChange={handleChange}
230+
hsva={{ h: 0, s: 0, v: 68, a: 0.5 }}
231+
/>,
232+
);
233+
234+
const elm = getByTitle('reverse-vertical-alpha');
235+
jest.spyOn(elm, 'getBoundingClientRect').mockReturnValue({
236+
x: 0,
237+
y: 0,
238+
top: 0,
239+
left: 0,
240+
bottom: 100,
241+
right: 20,
242+
width: 20,
243+
height: 100,
244+
toJSON: () => ({}),
245+
});
246+
247+
fireEvent(elm, new FakeMouseEvent('mousedown', { pageX: 10, pageY: 0 }));
248+
fireEvent(elm, new FakeMouseEvent('mousemove', { pageX: 10, pageY: 100 }));
249+
250+
expect(handleChange).toHaveBeenNthCalledWith(1, { a: 0, h: 0, s: 0, v: 68 }, expect.objectContaining({ top: 0 }));
251+
expect(handleChange).toHaveBeenNthCalledWith(2, { a: 1, h: 0, s: 0, v: 68 }, expect.objectContaining({ top: 1 }));
106252
});

0 commit comments

Comments
 (0)