Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,6 @@
},
"patchedDependencies": {
"react-native-gesture-handler@2.31.2": "patches/react-native-gesture-handler+2.31.2.patch",
"react-native-slider@0.11.0": "patches/react-native-slider+0.11.0.patch",
"react-native-snap-carousel@3.9.1": "patches/react-native-snap-carousel+3.9.1.patch",
"@mendix/pluggable-widgets-tools": "patches/@mendix__pluggable-widgets-tools.patch"
}
Expand Down
9 changes: 6 additions & 3 deletions packages/pluggableWidgets/color-picker-native/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

### Changed

- We updated deprecated-react-native-prop-types to 4.2.3.
- We pinned react-native-color to 0.0.10.
- We pinned react-native-slider to 0.11.0.
- We updated tinycolor2 to 1.6.0.
- We replaced react-native-slider and react-native-color with built-in components, and removed deprecated-react-native-prop-types.

### Fixed

- We fixed the sliders not announcing their value to screen readers. They can now also be adjusted with VoiceOver and TalkBack.
- We fixed the other sliders moving slightly while one slider is dragged, and hue and saturation being lost when the color becomes black or white.

## [2.1.0] - 2024-12-3

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "color-picker-native",
"widgetName": "ColorPicker",
"version": "2.3.0",
"version": "2.4.0",
"license": "Apache-2.0",
"repository": {
"type": "git",
Expand All @@ -21,9 +21,6 @@
"dependencies": {
"@mendix/piw-native-utils-internal": "*",
"@mendix/piw-utils-internal": "*",
"deprecated-react-native-prop-types": "4.2.3",
"react-native-color": "0.0.10",
"react-native-slider": "0.11.0",
"tinycolor2": "1.6.0"
},
"devDependencies": {
Expand Down
315 changes: 142 additions & 173 deletions packages/pluggableWidgets/color-picker-native/src/ColorPicker.tsx
Original file line number Diff line number Diff line change
@@ -1,203 +1,172 @@
import { flattenStyles } from "@mendix/piw-native-utils-internal";
import { ValueStatus } from "mendix";
import { Component, ReactElement, ReactNode } from "react";
import { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { View, ViewStyle } from "react-native";

import { HueGradient, LightnessGradient, SaturationGradient } from "react-native-color";
import tinycolor from "tinycolor2";
import { ColorPickerProps } from "../typings/ColorPickerProps";
import { PickerSlider } from "./components/PickerSlider";
import { AlphaGradient } from "./components/AlphaGradient";
import { DisabledHueGradient } from "./components/DisabledHueGradient";
import { HueGradient, LightnessGradient, SaturationGradient } from "./components/Gradients";
import { ColorPickerStyle, defaultColorPickerStyle } from "./ui/Styles";
import HSLA = tinycolor.ColorFormats.HSLA;
import { executeAction } from "@mendix/piw-utils-internal";

interface State {
color?: HSLA;
}

export type Props = ColorPickerProps<ColorPickerStyle>;

export class ColorPicker extends Component<Props, State> {
private readonly onChangeHueHandler = this.onChangeHue.bind(this);
private readonly onChangeSaturationHandler = this.onChangeSaturation.bind(this);
private readonly onChangeLightnessHandler = this.onChangeLightness.bind(this);
private readonly onChangeAlphaHandler = this.onChangeAlpha.bind(this);
private readonly onChangeCompleteHandler = this.onChangeComplete.bind(this);
private readonly styles = flattenStyles(defaultColorPickerStyle, this.props.style);
private readonly defaultSteps = 80;
readonly state: State = {
color: undefined
};

render(): ReactNode {
if (!this.props.color || this.props.color.status !== ValueStatus.Available || !this.props.color.value) {
return null;
}

const colorHex = this.state.color ? this.getColor() : this.props.color.value;
const color = tinycolor(colorHex).toHsl();

return (
<View style={this.styles.container} testID={`${this.props.name}`}>
{this.renderPreview(color)}
{this.renderHue(color)}
{this.renderSaturation(color)}
{this.renderLightness(color)}
{this.props.format !== "hex" && this.renderAlpha(color)}
</View>
);
const defaultSteps = 80;

const formatColor = (hsla: HSLA | undefined, format: Props["format"]): string => {
const color = tinycolor(hsla);
switch (format) {
case "hex":
return color.toHexString();
case "hsl":
return color.toHslString();
case "hsv":
return color.toHsvString();
case "rgb":
return color.toRgbString();
}

componentDidUpdate(prevProps: Readonly<Props>, prevState: Readonly<State>): void {
if (this.props.color.value !== prevProps.color.value && this.state.color === prevState.color) {
this.setState({ color: undefined });
};

const getThumbStyle = (color: HSLA): ViewStyle => ({
borderWidth: 1,
borderColor: tinycolor(color).toHexString()
});

export function ColorPicker(props: Props): ReactElement | null {
const { color: colorValue, format, onChange } = props;
const [stateColor, setStateColor] = useState<HSLA | undefined>(undefined);
// Screen reader actions change and complete in one event, before the state update is applied
const pendingColor = useRef<HSLA | undefined>(undefined);
const previous = useRef({ value: colorValue.value, color: stateColor });
const styles = useMemo(() => flattenStyles(defaultColorPickerStyle, props.style), [props.style]);

useEffect(() => {
const { value: previousValue, color: previousColor } = previous.current;
previous.current = { value: colorValue.value, color: stateColor };
// Keep the unrounded color while it still matches the value, so the other sliders don't jump when it is
// stored with 8-bit precision or hue and saturation are lost at black and white
if (
colorValue.value !== previousValue &&
stateColor === previousColor &&
stateColor &&
formatColor(stateColor, format) !== colorValue.value
) {
setStateColor(undefined);
}
}

private onChangeHue(value: number): void {
const color = tinycolor(this.props.color.value).toHsl();
color.h = value;
this.setState({ color });
}

private onChangeSaturation(value: number): void {
const color = tinycolor(this.props.color.value).toHsl();
color.s = value;
this.setState({ color });
}

private onChangeLightness(value: number): void {
const color = tinycolor(this.props.color.value).toHsl();
color.l = value;
this.setState({ color });
}

private onChangeAlpha(value: number): void {
const color = tinycolor(this.props.color.value).toHsl();
color.a = value;
this.setState({ color });
}
});

private onChangeComplete(): void {
if (this.state.color && this.props.color.value !== this.getColor()) {
this.props.color.setValue(this.getColor());
const renderedColor = useMemo(
(): HSLA => stateColor ?? tinycolor(colorValue.value).toHsl(),
[stateColor, colorValue.value]
);
const getCurrentColor = useCallback((): HSLA => pendingColor.current ?? renderedColor, [renderedColor]);

const setColor = useCallback((color: HSLA): void => {
pendingColor.current = color;
setStateColor(color);
}, []);

const onChangeHue = useCallback(
(value: number): void => setColor({ ...getCurrentColor(), h: value }),
[getCurrentColor, setColor]
);
const onChangeSaturation = useCallback(
(value: number): void => setColor({ ...getCurrentColor(), s: value }),
[getCurrentColor, setColor]
);
const onChangeLightness = useCallback(
(value: number): void => setColor({ ...getCurrentColor(), l: value }),
[getCurrentColor, setColor]
);
const onChangeAlpha = useCallback(
(value: number): void => setColor({ ...getCurrentColor(), a: value }),
[getCurrentColor, setColor]
);

const onChangeComplete = useCallback((): void => {
if (pendingColor.current && colorValue.value !== formatColor(pendingColor.current, format)) {
colorValue.setValue(formatColor(pendingColor.current, format));
}
pendingColor.current = undefined;

executeAction(this.props.onChange);
}
executeAction(onChange);
}, [colorValue, format, onChange]);

private getColor(): string {
const color = tinycolor(this.state.color);
switch (this.props.format) {
case "hex":
return color.toHexString();
case "hsl":
return color.toHslString();
case "hsv":
return color.toHsvString();
case "rgb":
return color.toRgbString();
}
if (!colorValue || colorValue.status !== ValueStatus.Available || !colorValue.value) {
return null;
}

private renderPreview(color: HSLA): ReactNode {
return (
this.props.showPreview && (
<View
testID={`${this.props.name}$preview`}
style={[this.styles.preview, { backgroundColor: tinycolor(color).toHslString() }]}
/>
)
);
}
const color = renderedColor;
const thumbTintColor = tinycolor(color).toHslString();
const thumbStyle = getThumbStyle(color);

private renderHue(color: HSLA): ReactElement {
const component = this.props.color.readOnly ? (
<DisabledHueGradient color={color} />
) : (
<HueGradient gradientSteps={this.defaultSteps} />
);
return (
return (
<View style={styles.container} testID={`${props.name}`}>
{props.showPreview && (
<View testID={`${props.name}$preview`} style={[styles.preview, { backgroundColor: thumbTintColor }]} />
)}
<PickerSlider
testID={`${this.props.name}$hue`}
testID={`${props.name}$hue`}
value={color.h}
onValueChange={this.onChangeHueHandler}
onValueChangeComplete={this.onChangeCompleteHandler}
onValueChange={onChangeHue}
onValueChangeComplete={onChangeComplete}
step={1}
maximumValue={359}
thumbTintColor={tinycolor(color).toHslString()}
thumbStyle={this.getThumbStyle(color)}
disabled={this.props.color.readOnly}
thumbTintColor={thumbTintColor}
thumbStyle={thumbStyle}
disabled={colorValue.readOnly}
>
{component}
{colorValue.readOnly ? (
<DisabledHueGradient color={color} />
) : (
<HueGradient gradientSteps={defaultSteps} />
)}
</PickerSlider>
);
}

private renderSaturation(color: HSLA): ReactNode {
if (!this.props.showSaturation || this.props.color.readOnly) {
return null;
}
return (
<PickerSlider
testID={`${this.props.name}$saturation`}
value={color.s}
onValueChange={this.onChangeSaturationHandler}
onValueChangeComplete={this.onChangeCompleteHandler}
step={0.01}
thumbTintColor={tinycolor(color).toHslString()}
thumbStyle={this.getThumbStyle(color)}
disabled={this.props.color.readOnly}
>
<SaturationGradient color={color} gradientSteps={this.defaultSteps} />
</PickerSlider>
);
}

private renderLightness(color: HSLA): ReactNode {
if (!this.props.showLightness || this.props.color.readOnly) {
return null;
}
return (
<PickerSlider
testID={`${this.props.name}$lightness`}
value={color.l}
onValueChange={this.onChangeLightnessHandler}
onValueChangeComplete={this.onChangeCompleteHandler}
step={0.01}
thumbTintColor={tinycolor(color).toHslString()}
thumbStyle={this.getThumbStyle(color)}
disabled={this.props.color.readOnly}
>
<LightnessGradient color={color} gradientSteps={this.defaultSteps} />
</PickerSlider>
);
}

private renderAlpha(color: HSLA): ReactNode {
if (!this.props.showAlpha || this.props.color.readOnly) {
return null;
}
return (
<PickerSlider
testID={`${this.props.name}$alpha`}
value={color.a}
onValueChange={this.onChangeAlphaHandler}
onValueChangeComplete={this.onChangeCompleteHandler}
step={0.01}
thumbTintColor={tinycolor(color).toHslString()}
thumbStyle={this.getThumbStyle(color)}
disabled={this.props.color.readOnly}
>
<AlphaGradient color={color} gradientSteps={this.defaultSteps} />
</PickerSlider>
);
}

private readonly getThumbStyle = (color: HSLA): ViewStyle => ({
borderWidth: 1,
borderColor: tinycolor(color).toHexString()
});
{props.showSaturation && !colorValue.readOnly && (
<PickerSlider
testID={`${props.name}$saturation`}
value={color.s}
onValueChange={onChangeSaturation}
onValueChangeComplete={onChangeComplete}
step={0.01}
thumbTintColor={thumbTintColor}
thumbStyle={thumbStyle}
disabled={colorValue.readOnly}
>
<SaturationGradient color={color} gradientSteps={defaultSteps} />
</PickerSlider>
)}
{props.showLightness && !colorValue.readOnly && (
<PickerSlider
testID={`${props.name}$lightness`}
value={color.l}
onValueChange={onChangeLightness}
onValueChangeComplete={onChangeComplete}
step={0.01}
thumbTintColor={thumbTintColor}
thumbStyle={thumbStyle}
disabled={colorValue.readOnly}
>
<LightnessGradient color={color} gradientSteps={defaultSteps} />
</PickerSlider>
)}
{format !== "hex" && props.showAlpha && !colorValue.readOnly && (
<PickerSlider
testID={`${props.name}$alpha`}
value={color.a}
onValueChange={onChangeAlpha}
onValueChangeComplete={onChangeComplete}
step={0.01}
thumbTintColor={thumbTintColor}
thumbStyle={thumbStyle}
disabled={colorValue.readOnly}
>
<AlphaGradient color={color} gradientSteps={defaultSteps} />
</PickerSlider>
)}
</View>
);
}
Loading
Loading