Skip to content

Commit

Permalink
feat(react-color-picker) Combined components of the ColorPicker (#33273)
Browse files Browse the repository at this point in the history
  • Loading branch information
ValentinaKozlova authored Nov 25, 2024
1 parent 5ea1183 commit 6c21364
Show file tree
Hide file tree
Showing 23 changed files with 157 additions and 173 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@ export const AlphaSlider: ForwardRefComponent<AlphaSliderProps>;
export const alphaSliderClassNames: SlotClassNames<AlphaSliderSlots>;

// @public
export type AlphaSliderProps = Omit<ComponentProps<Partial<AlphaSliderSlots>, 'input'>, 'defaultValue' | 'onChange' | 'value'> & ColorSliderProps & {
overlayColor?: string;
};
export type AlphaSliderProps = ColorSliderProps;

// @public (undocumented)
export type AlphaSliderSlots = ColorSliderSlots;
Expand Down Expand Up @@ -61,8 +59,8 @@ export const ColorPicker: ForwardRefComponent<ColorPickerProps>;
export const colorPickerClassNames: SlotClassNames<ColorPickerSlots>;

// @public
export type ColorPickerProps = ComponentProps<ColorPickerSlots> & {
color: string;
export type ColorPickerProps = Omit<ComponentProps<Partial<ColorPickerSlots>>, 'color'> & {
color: HsvColor;
onColorChange?: EventHandler<ColorPickerOnChangeData>;
};

Expand All @@ -81,11 +79,12 @@ export const ColorSlider: ForwardRefComponent<ColorSliderProps>;
export const colorSliderClassNames: SlotClassNames<ColorSliderSlots>;

// @public
export type ColorSliderProps = Omit<ComponentProps<Partial<ColorSliderSlots>, 'input'>, 'defaultValue' | 'onChange' | 'value'> & {
export type ColorSliderProps = Omit<ComponentProps<Partial<ColorSliderSlots>, 'input'>, 'defaultValue' | 'onChange' | 'value' | 'color'> & {
channel?: string;
onChange?: EventHandler<SliderOnChangeData>;
vertical?: boolean;
color?: string;
color?: HsvColor;
defaultColor?: HsvColor;
};

// @public (undocumented)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ describe('AlphaSlider', () => {
});

it('renders a default state', () => {
const result = render(<AlphaSlider overlayColor="red" />);
const result = render(<AlphaSlider color={{ h: 0, s: 1, v: 1 }} />);
expect(result.container).toMatchInlineSnapshot(`
<div>
<div
class="fui-ColorSlider fui-AlphaSlider"
style="--fui-AlphaSlider--direction: 90deg; --fui-AlphaSlider--progress: 100%; --fui-AlphaSlider__thumb--color: transparent; --fui-AlphaSlider__rail--color: hsl(0 0%, 0%);"
style="--fui-AlphaSlider--direction: 90deg; --fui-AlphaSlider--progress: 100%; --fui-AlphaSlider__thumb--color: transparent; --fui-AlphaSlider__rail--color: hsl(0 100%, 50%);"
>
<input
class="fui-ColorSlider__input fui-AlphaSlider__input"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,12 @@
import type { ComponentProps, ComponentState } from '@fluentui/react-utilities';
import type { ComponentState } from '@fluentui/react-utilities';
import type { ColorSliderSlots, ColorSliderProps, ColorSliderState } from '../ColorSlider/ColorSlider.types';

export type AlphaSliderSlots = ColorSliderSlots;

/**
* AlphaSlider Props
*/
export type AlphaSliderProps = Omit<
ComponentProps<Partial<AlphaSliderSlots>, 'input'>,
'defaultValue' | 'onChange' | 'value'
> &
ColorSliderProps & {
/**
* The color to overlay on the alpha slider.
*/
overlayColor?: string;
};
export type AlphaSliderProps = ColorSliderProps;

/**
* State used in rendering AlphaSlider
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const useAlphaSlider_unstable = (
const nativeProps = getPartitionedNativeProps({
props,
primarySlotTagName: 'input',
excludedPropNames: ['onChange'],
excludedPropNames: ['onChange', 'color'],
});

const {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,9 @@ import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts
import { alphaSliderCSSVars } from './useAlphaSliderStyles.styles';
import type { AlphaSliderState, AlphaSliderProps } from './AlphaSlider.types';
import { useColorPickerContextValue_unstable } from '../../contexts/colorPicker';

const { sliderProgressVar, sliderDirectionVar, thumbColorVar, railColorVar } = alphaSliderCSSVars;

const MIN = 0;
const MAX = 100;

const getPercent = (value: number, min: number, max: number) => {
return max === min ? 0 : ((value - min) / (max - min)) * 100;
};
import { MIN, MAX } from '../../utils/constants';
import { getPercent } from '../../utils/getPercent';
import type { HsvColor } from '../../types/color';

export const useAlphaSliderState_unstable = (state: AlphaSliderState, props: AlphaSliderProps) => {
'use no memo';
Expand All @@ -22,12 +16,13 @@ export const useAlphaSliderState_unstable = (state: AlphaSliderState, props: Alp
const onChangeFromContext = useColorPickerContextValue_unstable(ctx => ctx.requestChange);
const colorFromContext = useColorPickerContextValue_unstable(ctx => ctx.color);
const { color, onChange = onChangeFromContext } = props;
const _color = colorFromContext || color;
const hslColor = tinycolor(_color).toHsl();
const hsvColor = color || colorFromContext;
const hslColor = tinycolor(hsvColor).toHsl();

const [currentValue, setCurrentValue] = useControllableState({
state: hslColor.a * 100,
initialState: 0,
defaultState: props.defaultColor?.a ? props.defaultColor.a * 100 : undefined,
state: hsvColor?.a ? hsvColor.a * 100 : undefined,
initialState: 100,
});
const clampedValue = clamp(currentValue, MIN, MAX);
const valuePercent = getPercent(clampedValue, MIN, MAX);
Expand All @@ -36,20 +31,17 @@ export const useAlphaSliderState_unstable = (state: AlphaSliderState, props: Alp

const _onChange: React.ChangeEventHandler<HTMLInputElement> = useEventCallback(event => {
const newValue = Number(event.target.value);
const newColor = tinycolor({ ...hslColor, a: newValue / 100 }).toRgbString();
setCurrentValue(clamp(newValue, MIN, MAX));
const newColor: HsvColor = { ...hsvColor, a: newValue / 100 };
setCurrentValue(newValue);
inputOnChange?.(event);
onChange?.(event, { type: 'change', event, color: newColor });
onChangeFromContext(event, {
color: newColor,
});
});

const rootVariables = {
[sliderDirectionVar]: state.vertical ? '0deg' : dir === 'ltr' ? '90deg' : '-90deg',
[sliderProgressVar]: `${valuePercent}%`,
[thumbColorVar]: `transparent`,
[railColorVar]: `hsl(${hslColor.h} ${hslColor.s * 100}%, ${hslColor.l * 100}%)`,
[alphaSliderCSSVars.sliderDirectionVar]: state.vertical ? '0deg' : dir === 'ltr' ? '90deg' : '-90deg',
[alphaSliderCSSVars.sliderProgressVar]: `${valuePercent}%`,
[alphaSliderCSSVars.thumbColorVar]: `transparent`,
[alphaSliderCSSVars.railColorVar]: `hsl(${hslColor.h} ${hslColor.s * 100}%, ${hslColor.l * 100}%)`,
};

// Root props
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,17 @@ export const alphaSliderClassNames: SlotClassNames<AlphaSliderSlots> = {
export const alphaSliderCSSVars = {
sliderDirectionVar: `--fui-AlphaSlider--direction`,
sliderProgressVar: `--fui-AlphaSlider--progress`,
sliderStepsPercentVar: `--fui-AlphaSlider--steps-percent`,
thumbColorVar: `--fui-AlphaSlider__thumb--color`,
railColorVar: `--fui-AlphaSlider__rail--color`,
};

const { sliderDirectionVar, railColorVar, sliderProgressVar, thumbColorVar } = alphaSliderCSSVars;

/**
* Styles for the root slot
*/
const useStyles = makeStyles({
rail: {
border: `1px solid ${tokens.colorNeutralStroke1}`,
backgroundImage: `linear-gradient(var(${sliderDirectionVar}), transparent, var(${railColorVar})), url(${TRANSPARENT_IMAGE_URL})`,
backgroundImage: `linear-gradient(var(${alphaSliderCSSVars.sliderDirectionVar}), transparent, var(${alphaSliderCSSVars.railColorVar})), url(${TRANSPARENT_IMAGE_URL})`,
},
});

Expand All @@ -38,15 +35,15 @@ const useStyles = makeStyles({
*/
const useThumbStyles = makeStyles({
thumb: {
backgroundColor: `var(${thumbColorVar})`,
backgroundColor: `var(${alphaSliderCSSVars.thumbColorVar})`,
},
horizontal: {
transform: 'translateX(-50%)',
left: `var(${sliderProgressVar})`,
left: `var(${alphaSliderCSSVars.sliderProgressVar})`,
},
vertical: {
transform: 'translateY(50%)',
bottom: `var(${sliderProgressVar})`,
bottom: `var(${alphaSliderCSSVars.sliderProgressVar})`,
},
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as React from 'react';
import type { ComponentState, Slot, EventHandler, EventData, ComponentProps } from '@fluentui/react-utilities';
import type { HsvColor } from '../../types/color';

export type ColorAreaOnColorChangeData = EventData<'change', React.SyntheticEvent | MouseEvent> & {
color: HsvColor;
Expand All @@ -12,13 +13,6 @@ export type ColorAreaSlots = {
inputY?: NonNullable<Slot<'input'>>;
};

export type HsvColor = {
h: number;
s: number;
v: number;
a?: number;
};

/**
* ColorArea Props
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import * as React from 'react';
import { useId, slot, useMergedRefs, mergeCallbacks, getIntrinsicElementProps } from '@fluentui/react-utilities';
import type { ColorAreaProps, ColorAreaState, HsvColor } from './ColorArea.types';
import type { ColorAreaProps, ColorAreaState } from './ColorArea.types';
import type { HsvColor } from '../../types/color';
import { colorAreaCSSVars } from './useColorAreaStyles.styles';
import { useEventCallback, useControllableState } from '@fluentui/react-utilities';
import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts';
import { useFocusWithin } from '@fluentui/react-tabster';
import { INITIAL_COLOR_HSV } from '../../utils/constants';
import { getCoordinates } from '../../utils/getCoordinates';
import { useColorPickerContextValue_unstable } from '../../contexts/colorPicker';

/**
* Create the state required to render ColorArea.
Expand All @@ -23,10 +25,11 @@ export const useColorArea_unstable = (props: ColorAreaProps, ref: React.Ref<HTML
const xRef = React.useRef<HTMLInputElement>(null);
const yRef = React.useRef<HTMLInputElement>(null);
const focusWithinRef = useFocusWithin();
const onChangeFromContext = useColorPickerContextValue_unstable(ctx => ctx.requestChange);
const colorFromContext = useColorPickerContextValue_unstable(ctx => ctx.color);

const {
onChange,

onChange = onChangeFromContext as unknown as ColorAreaProps['onChange'],
// Slots
inputX,
inputY,
Expand All @@ -37,7 +40,7 @@ export const useColorArea_unstable = (props: ColorAreaProps, ref: React.Ref<HTML

const [hsvColor, setColor] = useControllableState<HsvColor>({
defaultState: props.defaultColor,
state: color,
state: color || colorFromContext,
initialState: INITIAL_COLOR_HSV,
});
const saturation = Math.round(hsvColor.s * 100);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ describe('ColorPicker', () => {
});

it('renders a default state', () => {
const result = render(<ColorPicker color="red" />);
const result = render(<ColorPicker color={{ h: 0, s: 1, v: 1 }} />);
expect(result.container).toMatchInlineSnapshot(`
<div>
<div
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import * as React from 'react';
import type { ComponentProps, ComponentState, Slot, EventHandler, EventData } from '@fluentui/react-utilities';
import { ColorPickerContextValue } from '../../contexts/colorPicker';
import type { HsvColor } from '../../types/color';

export type ColorPickerOnChangeData = EventData<'change', React.ChangeEvent<HTMLInputElement>> & {
color: string;
color: HsvColor;
};

export type ColorPickerSlots = {
Expand All @@ -13,11 +14,11 @@ export type ColorPickerSlots = {
/**
* ColorPicker Props
*/
export type ColorPickerProps = ComponentProps<ColorPickerSlots> & {
export type ColorPickerProps = Omit<ComponentProps<Partial<ColorPickerSlots>>, 'color'> & {
/**
* Selected color.
*/
color: string;
color: HsvColor;

/**
* Callback for when the user changes the color.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import type { ColorPickerSlots, ColorPickerState } from './ColorPicker.types';

export const colorPickerClassNames: SlotClassNames<ColorPickerSlots> = {
root: 'fui-ColorPicker',
// TODO: add class names for all slots on ColorPickerSlots.
// Should be of the form `<slotName>: 'fui-ColorPicker__<slotName>`
};

/**
Expand All @@ -16,8 +14,6 @@ const useStyles = makeStyles({
display: 'flex',
flexDirection: 'column',
},

// TODO add additional classes for different states and/or slots
});

/**
Expand All @@ -29,8 +25,5 @@ export const useColorPickerStyles_unstable = (state: ColorPickerState): ColorPic
const styles = useStyles();
state.root.className = mergeClasses(colorPickerClassNames.root, styles.root, state.root.className);

// TODO Add class names to slots, for example:
// state.mySlot.className = mergeClasses(styles.mySlot, state.mySlot.className);

return state;
};
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ describe('ColorSlider', () => {
<input
class="fui-ColorSlider__input"
id="slider-9"
max="360"
min="0"
type="range"
value="0"
/>
Expand All @@ -42,7 +44,7 @@ describe('ColorSlider', () => {
});

it('applies the color prop', () => {
render(<ColorSlider color="#f09" />);
render(<ColorSlider color={{ h: 324, s: 1, v: 1 }} />);
expect(screen.getByRole('slider').getAttribute('value')).toEqual('324');
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import * as React from 'react';
import type { ComponentProps, ComponentState, Slot, EventHandler, EventData } from '@fluentui/react-utilities';
import type { HsvColor } from '../../types/color';

export type SliderOnChangeData = EventData<'change', React.ChangeEvent<HTMLInputElement>> & {
color: string;
color: HsvColor;
};

export type ColorSliderSlots = {
Expand All @@ -17,7 +18,7 @@ export type ColorSliderSlots = {
*/
export type ColorSliderProps = Omit<
ComponentProps<Partial<ColorSliderSlots>, 'input'>,
'defaultValue' | 'onChange' | 'value'
'defaultValue' | 'onChange' | 'value' | 'color'
> & {
channel?: string;

Expand All @@ -35,7 +36,12 @@ export type ColorSliderProps = Omit<
/**
* Color of the COlorPicker
*/
color?: string;
color?: HsvColor;

/**
* The starting color for an uncontrolled ColorSlider.
*/
defaultColor?: HsvColor;
};

/**
Expand Down
Loading

0 comments on commit 6c21364

Please sign in to comment.