Skip to content

Commit

Permalink
Added [x]Props for ColorPicker component (#2211)
Browse files Browse the repository at this point in the history
Co-authored-by: Rohan <45748283+r100-stack@users.noreply.github.com>
  • Loading branch information
smmr-dn and r100-stack authored Sep 16, 2024
1 parent bc9a058 commit 8203d0b
Show file tree
Hide file tree
Showing 7 changed files with 430 additions and 40 deletions.
9 changes: 9 additions & 0 deletions .changeset/thick-deers-act.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@itwin/itwinui-react': minor
---

Added the ability to have custom `props` for each sub-component of `ColorPicker`.
New props added associated with each sub-component are:
- `ColorBuilder`: `colorFieldProps`, `colorDotProps`, `opacitySliderProps`, and `hueSliderProps`.
- `ColorInputPanel`: `panelLabelProps`, `colorInputContainerProps`, `inputFieldsGroupProps` and `swapColorFormatButtonProps`.
- `ColorPalette`: `labelProps`, and `paletteContainerProps`.
122 changes: 93 additions & 29 deletions packages/itwinui-react/src/core/ColorPicker/ColorBuilder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
useEventListener,
useMergedRefs,
Box,
mergeEventHandlers,
} from '../../utils/index.js';
import type {
HsvColor,
Expand All @@ -27,13 +28,39 @@ const getHorizontalPercentageOfRectangle = (rect: DOMRect, pointer: number) => {
return ((position - rect.left) / rect.width) * 100;
};

type ColorBuilderProps = {
/**
* Passes props to the color field.
*/
colorFieldProps?: React.ComponentProps<'div'>;
/**
* Passes props to the color dot.
*/
colorDotProps?: React.ComponentProps<'div'>;
/**
* Passes props to the color opacity slider.
*/
opacitySliderProps?: React.ComponentProps<typeof Slider>;
/**
* Passes props to the color hue slider.
*/
hueSliderProps?: React.ComponentProps<typeof Slider>;
};

/**
* `ColorBuilder` consists of two parts:
* a color canvas to adjust saturation and lightness values,
* and a set of sliders to adjust hue and alpha values.
*/
export const ColorBuilder = React.forwardRef((props, ref) => {
const { className, ...rest } = props;
const {
className,
colorFieldProps,
colorDotProps,
opacitySliderProps,
hueSliderProps,
...rest
} = props;

const builderRef = React.useRef<HTMLDivElement>();
const refs = useMergedRefs(builderRef, ref);
Expand Down Expand Up @@ -253,86 +280,123 @@ export const ColorBuilder = React.forwardRef((props, ref) => {
{...rest}
>
<Box
className='iui-color-field'
as='div'
{...colorFieldProps}
className={cx('iui-color-field', colorFieldProps?.className)}
style={
{
'--iui-color-field-hue': hueColorString,
'--iui-color-picker-selected-color': dotColorString,
...colorFieldProps?.style,
} as React.CSSProperties
}
ref={squareRef}
onPointerDown={(event: React.PointerEvent) => {
event.preventDefault();
updateSquareValue(event, 'onClick');
setColorDotActive(true);
colorDotRef.current?.focus();
}}
ref={useMergedRefs(squareRef, colorFieldProps?.ref)}
onPointerDown={mergeEventHandlers(
colorFieldProps?.onPointerDown,
(event: React.PointerEvent) => {
event.preventDefault();
updateSquareValue(event, 'onClick');
setColorDotActive(true);
colorDotRef.current?.focus();
},
)}
>
<Box
className='iui-color-dot'
as='div'
{...colorDotProps}
className={cx('iui-color-dot', colorDotProps?.className)}
style={
{
'--iui-color-dot-inset-block': `${squareTop.toString()}% auto`,
'--iui-color-dot-inset-inline': `${squareLeft.toString()}% auto`,
...colorDotProps?.style,
} as React.CSSProperties
}
onPointerDown={() => {
setColorDotActive(true);
colorDotRef.current?.focus();
}}
onKeyDown={handleColorDotKeyDown}
onKeyUp={handleColorDotKeyUp}
onPointerDown={mergeEventHandlers(
colorDotProps?.onPointerDown,
() => {
setColorDotActive(true);
colorDotRef.current?.focus();
},
)}
onKeyDown={mergeEventHandlers(
colorDotProps?.onKeyDown,
handleColorDotKeyDown,
)}
onKeyUp={mergeEventHandlers(
colorDotProps?.onKeyUp,
handleColorDotKeyUp,
)}
tabIndex={0}
ref={colorDotRef}
ref={useMergedRefs(colorDotRef, colorDotProps?.ref)}
/>
</Box>

<Slider
minLabel=''
maxLabel=''
values={[sliderValue]}
className='iui-hue-slider'
trackDisplayMode='none'
tooltipProps={() => ({ visible: false })}
min={0}
max={359}
{...hueSliderProps}
className={cx('iui-hue-slider', hueSliderProps?.className)}
tooltipProps={() => ({
visible: false,
...hueSliderProps?.tooltipProps,
})}
onChange={(values) => {
hueSliderProps?.onChange?.(values);
updateHueSlider(values[0], true);
}}
onUpdate={(values) => {
hueSliderProps?.onUpdate?.(values);
updateHueSlider(values[0], false);
}}
min={0}
max={359}
thumbProps={() => ({ 'aria-label': 'Hue' })}
thumbProps={() => ({
'aria-label': 'Hue',
...hueSliderProps?.thumbProps,
})}
/>

{showAlpha && (
<Slider
minLabel=''
maxLabel=''
values={[alphaValue]}
className='iui-opacity-slider'
trackDisplayMode='none'
tooltipProps={() => ({ visible: false })}
min={0}
max={1}
step={0.01}
{...opacitySliderProps}
className={cx('iui-opacity-slider', opacitySliderProps?.className)}
tooltipProps={() => ({
visible: false,
...opacitySliderProps?.tooltipProps,
})}
onChange={(values) => {
opacitySliderProps?.onChange?.(values);
updateOpacitySlider(values[0], true);
}}
onUpdate={(values) => {
opacitySliderProps?.onUpdate?.(values);
updateOpacitySlider(values[0], false);
}}
min={0}
max={1}
step={0.01}
style={
{
'--iui-color-picker-selected-color': hueColorString,
...opacitySliderProps?.style,
} as React.CSSProperties
}
thumbProps={() => ({ 'aria-label': 'Opacity' })}
thumbProps={() => ({
'aria-label': 'Opacity',
...opacitySliderProps?.thumbProps,
})}
/>
)}
</Box>
);
}) as PolymorphicForwardRefComponent<'div'>;
}) as PolymorphicForwardRefComponent<'div', ColorBuilderProps>;
if (process.env.NODE_ENV === 'development') {
ColorBuilder.displayName = 'ColorBuilder';
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,92 @@ import { ColorInputPanel } from './ColorInputPanel.js';
import { ColorValue } from '../../utils/index.js';
import { userEvent } from '@testing-library/user-event';

it('should pass custom label with props', async () => {
const { container } = render(
<ColorPicker>
<ColorInputPanel
defaultColorFormat='hex'
panelLabelProps={{
className: 'test-panel-label',
style: { color: 'red' },
}}
/>
</ColorPicker>,
);

const inputPanelLabel = container.querySelector(
'.iui-color-picker-section-label.test-panel-label',
) as HTMLElement;
expect(inputPanelLabel).toBeTruthy();
expect(inputPanelLabel.style.color).toBe('red');
});

it('should render input field with custom container props', async () => {
const { container } = render(
<ColorPicker>
<ColorInputPanel
defaultColorFormat='hex'
colorInputContainerProps={{
className: 'test-input-panel',
style: { padding: '10px' },
}}
/>
</ColorPicker>,
);

const inputPanel = container.querySelector(
'.iui-color-input.test-input-panel',
) as HTMLElement;
expect(inputPanel).toBeTruthy();
expect(inputPanel.style.padding).toBe('10px');
});

it('should render swap color format button with custom props', async () => {
const { container } = render(
<ColorPicker>
<ColorInputPanel
defaultColorFormat='hex'
swapColorFormatButtonProps={{
className: 'test-swap-color-button',
styleType: 'high-visibility',
}}
/>
</ColorPicker>,
);

const swapColorButton = container.querySelector(
'.iui-button[data-iui-variant="high-visibility"]',
) as HTMLElement;
expect(swapColorButton).toBeTruthy();
expect(swapColorButton).toHaveClass('test-swap-color-button');
});

it('should render input field with custom input field props', async () => {
const logSpy = vitest.spyOn(console, 'log');
const { container } = render(
<ColorPicker>
<ColorInputPanel
defaultColorFormat='hex'
inputFieldsGroupProps={{
className: 'test-input-field',
style: {
borderRadius: '10px',
},
onClick: () => console.log('clicked'),
}}
/>
</ColorPicker>,
);

const inputPanel = container.querySelector(
'.iui-color-input-fields.test-input-field',
) as HTMLElement;
expect(inputPanel).toBeTruthy();
expect(inputPanel.style.borderRadius).toBe('10px');
await userEvent.click(inputPanel);
expect(logSpy).toHaveBeenCalledWith('clicked');
});

it('should render ColorInputPanel with input fields', async () => {
const { container } = render(
<ColorPicker>
Expand Down
Loading

0 comments on commit 8203d0b

Please sign in to comment.