Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(scene composer): Color picker bug fixes #1798

Merged
merged 2 commits into from
Aug 16, 2023
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export interface AnchorWidgetProps {
valueDataBinding?: IValueDataBinding;
rule?: IRuleBasedMap;
navLink?: INavLink;
customColors?: string[];
}
type overrideCustomColorType = (rulevalue: string | undefined) => void;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import renderer, { act } from 'react-test-renderer';
import React from 'react';
import * as THREE from 'three';
import { useLoader } from '@react-three/fiber';
import wrapper from '@awsui/components-react/test-utils/dom';

import { AnchorWidget } from '../AnchorWidget';
import { DefaultAnchorStatus, DEFAULT_TAG_GLOBAL_SETTINGS, KnownComponentType } from '../../../../..';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,8 @@ export const AnchorComponentEditor: React.FC<IAnchorComponentEditorProps> = ({

const hasIcon = iconSelectedOptionIndex >= 0;
const iconGridDefinition = hasIcon ? [{ colspan: 10 }, { colspan: 2 }] : [{ colspan: 12 }];
const isAllValid = tagStyle && iconOptions[iconSelectedOptionIndex]?.value === 'Custom';
const isCustomStyle = tagStyle && iconOptions[iconSelectedOptionIndex]?.value === 'Custom';

return (
<SpaceBetween size='s'>
<FormField label={intl.formatMessage({ defaultMessage: 'Default Icon', description: 'Form field label' })}>
Expand All @@ -189,7 +190,7 @@ export const AnchorComponentEditor: React.FC<IAnchorComponentEditorProps> = ({
placeholder={intl.formatMessage({ defaultMessage: 'Choose an icon', description: 'placeholder' })}
/>
{hasIcon &&
(isAllValid ? (
(isCustomStyle ? (
<DecodeSvgString
selectedColor={anchorComponent.chosenColor ?? colors.customBlue}
iconString={iconString!}
Expand All @@ -201,12 +202,19 @@ export const AnchorComponentEditor: React.FC<IAnchorComponentEditorProps> = ({
))}
</Grid>
</FormField>
{isAllValid ? (
<FormField stretch>
{isCustomStyle ? (
<FormField>
<ColorPicker
color={anchorComponent.chosenColor ?? colors.customBlue}
onSelectColor={(pickedColor) => onUpdateCallback({ chosenColor: pickedColor })}
label={intl.formatMessage({ defaultMessage: 'Colors', description: 'Colors' })}
onSelectColor={(pickedColor) => {
onUpdateCallback({
chosenColor: pickedColor,
});
}}
onUpdateCustomColors={(chosenCustomColors) => onUpdateCallback({ customColors: chosenCustomColors })}
customColors={anchorComponent.customColors}
colorPickerLabel={intl.formatMessage({ defaultMessage: 'Colors', description: 'Colors' })}
customColorLabel={intl.formatMessage({ defaultMessage: 'Custom colors', description: 'Custom colors' })}
/>
</FormField>
) : null}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Button, FormField, Icon, Input, InputProps, SpaceBetween, TextContent } from '@awsui/components-react';
import { NonCancelableCustomEvent } from '@awsui/components-react/internal/events';
import React, { useCallback, useState } from 'react';
import { ChromePicker, CirclePicker } from 'react-color';
import React, { useCallback, useEffect, useState } from 'react';
import { CirclePicker, ColorResult, SketchPicker } from 'react-color';
import { useIntl } from 'react-intl';

import { IColorPickerProps } from '../interface';
Expand All @@ -17,12 +17,19 @@ import {
import { colorPickerPreviewSvg } from './ColorPickerUtils/SvgParserHelper';
import { palleteColors } from './ColorPickerUtils/TagColors';

export const ColorPicker = ({ color, onSelectColor, label }: IColorPickerProps): JSX.Element => {
export const ColorPicker = ({
color,
onSelectColor,
customColors,
onUpdateCustomColors,
colorPickerLabel,
customColorLabel,
}: IColorPickerProps): JSX.Element => {
const [showPicker, setShowPicker] = useState<boolean>(false);
const [newColor, setNewColor] = useState<string>(color);
const [showChromePicker, setShowChromePicker] = useState<boolean>(false);
const [hexCodeError, setHexCodeError] = useState<string>(''); // State variable for hex code error

const [customInternalColors, setCustomInternalColors] = useState<string[]>(customColors ?? []);
const intl = useIntl();

/**
Expand All @@ -34,30 +41,40 @@ export const ColorPicker = ({ color, onSelectColor, label }: IColorPickerProps):
* @returns
*/
const isValidHexCode = (hexCode: string) => {
const hexRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
const hexRegex = /^#([A-Fa-f0-9]{6})$/;
return hexRegex.test(hexCode);
};

const handleClick = () => {
setShowPicker(!showPicker);
};

const handleColorChange = (color) => {
setNewColor(color.hex);
onSelectColor(color.hex);
};
const handleOutsideClick = useCallback((event: MouseEvent) => {
const target = event.target as HTMLElement;
const pickerContainer = document.getElementById('circle-picker');
if (pickerContainer && !pickerContainer.contains(target)) {
setShowPicker(false);
}
}, []);

const handleHexCodeChange = useCallback((event: NonCancelableCustomEvent<InputProps.ChangeDetail>) => {
setNewColor(event.detail.value);
if (isValidHexCode(event.detail.value)) {
setHexCodeError(''); // Clear any existing error message
onSelectColor(event.detail.value);
useEffect(() => {
if (showPicker) {
document.addEventListener('click', handleOutsideClick);
} else {
setHexCodeError(
intl.formatMessage({ defaultMessage: 'Invalid hex code', description: 'hex validations messages' }),
); // Set the error message
document.removeEventListener('click', handleOutsideClick);
}
}, []);

return () => {
document.removeEventListener('click', handleOutsideClick);
};
}, [showPicker, handleOutsideClick]);

const checkIfCustomColor = (color: any) => {
if (!Object.values(palleteColors).includes(color)) {
setCustomInternalColors(customInternalColors.concat(color));
haweston marked this conversation as resolved.
Show resolved Hide resolved
onUpdateCustomColors?.([...new Set(customInternalColors)]);
}
};

const handleClick = () => {
setShowPicker(!showPicker);
};

const handleShowChromePicker = () => {
setShowChromePicker(true);
Expand All @@ -69,22 +86,62 @@ export const ColorPicker = ({ color, onSelectColor, label }: IColorPickerProps):
setShowChromePicker(false);
};

const handleCloseChromePicker = () => {
const handleCloseCustomPicker = () => {
setShowChromePicker(false);
};

const handleColorChange = useCallback(
(color: ColorResult) => {
checkIfCustomColor(color.hex);
setHexCodeError(''); // Clear any existing error message
setNewColor(color.hex);
onSelectColor(color.hex);
},
[color, onSelectColor, onUpdateCustomColors],
);

const handleHexCodeChange = useCallback(
(event: NonCancelableCustomEvent<InputProps.ChangeDetail>) => {
setNewColor(event.detail.value);
haweston marked this conversation as resolved.
Show resolved Hide resolved
if (isValidHexCode(event.detail.value)) {
setHexCodeError(''); // Clear any existing error message
onSelectColor(event.detail.value);
checkIfCustomColor(event.detail.value);
} else {
setHexCodeError(
intl.formatMessage({ defaultMessage: 'Invalid hex code', description: 'hex validations messages' }),
); // Set the error message
}
},
[color, onSelectColor],
);

const handleCustomPickerSelection = (color) => {
checkIfCustomColor(color.hex);
setNewColor(color.hex);
onSelectColor(color.hex);
};

useEffect(() => {
setNewColor(color);
}, [color]);

useEffect(() => {
checkIfCustomColor(color);
}, [color]);

return (
<SpaceBetween size='m'>
<SpaceBetween size='l'>
<FormField errorText={hexCodeError}>
<SpaceBetween size='m' direction='horizontal'>
<TextContent>
<h5>{label}</h5>
<h5>{colorPickerLabel}</h5>
</TextContent>
<Button
data-testid='color-preview'
ariaLabel={intl.formatMessage({ defaultMessage: 'colorPreview', description: 'color picker preview' })}
variant='inline-icon'
iconSvg={<Icon size='big' svg={colorPickerPreviewSvg(color)} />}
iconSvg={<Icon size='big' svg={colorPickerPreviewSvg(newColor)} />}
onClick={() => {
handleClick();
setHexCodeError('');
Expand All @@ -96,7 +153,7 @@ export const ColorPicker = ({ color, onSelectColor, label }: IColorPickerProps):
value={newColor}
onChange={handleHexCodeChange}
/>
<div style={tmColorPickerContainer}>
<div id='circle-picker' style={tmColorPickerContainer}>
{showPicker && !showChromePicker && (
<div style={tmColorPickerPopover}>
<div>
Expand All @@ -110,10 +167,21 @@ export const ColorPicker = ({ color, onSelectColor, label }: IColorPickerProps):
onChange={handleColorChange}
/>
</div>
<div style={tmDivider} />
<button style={tmAddButton} onClick={handleShowChromePicker}>
<Icon name='add-plus' />
</button>
<SpaceBetween size='s'>
<div style={tmDivider} />
<TextContent>
<h5>{customColorLabel}</h5>
</TextContent>
<CirclePicker
width='300px'
colors={[...new Set(customInternalColors)]}
color={newColor}
onChange={handleColorChange}
/>
<button style={tmAddButton} onClick={handleShowChromePicker}>
<Icon name='add-plus' />
</button>
</SpaceBetween>
</div>
)}
</div>
Expand All @@ -124,9 +192,9 @@ export const ColorPicker = ({ color, onSelectColor, label }: IColorPickerProps):
<div
aria-label={intl.formatMessage({ defaultMessage: 'chromePicker', description: 'chrome picker' })}
style={tmCover}
onClick={handleCloseChromePicker}
onClick={handleCloseCustomPicker}
/>
<ChromePicker color={color} onChangeComplete={(newColor) => onSelectColor(newColor.hex)} />
<SketchPicker disableAlpha={true} color={newColor} onChangeComplete={handleCustomPickerSelection} />
</div>
)}
</SpaceBetween>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const replaceFillAttribute = (element: Element, selectedColor: string): v
element.setAttribute('stroke', selectedColor);
}
if (tagName === 'circle') {
element.setAttribute('fill', selectedColor!);
element.setAttribute('fill', selectedColor);
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,8 @@ export interface IColorPickerProps {
color: string;
onSelectColor: (color: string) => void;
iconSvg?: string;
label?: string;
colorPickerLabel?: string;
customColorLabel?: string;
customColors?: string[];
onUpdateCustomColors?: (customColors: string[]) => void;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { useIntl, defineMessages } from 'react-intl';
import { Grid, Select } from '@awsui/components-react';

Expand Down Expand Up @@ -60,7 +60,12 @@ export const SceneRuleTargetEditor: React.FC<ISceneRuleTargetEditorProps> = ({
label: formatMessage(i18nSceneResourceTypeStrings[SceneResourceType[type]]) || SceneResourceType[type],
value: SceneResourceType[type],
}));
const isAllValid = tagStyle && targetInfo.value === 'Custom';
const isCustomStyle = tagStyle && targetInfo.value === 'Custom';

useEffect(() => {
setChosenColor(getCustomColor);
}, [getCustomColor]);

return (
<Grid gridDefinition={[{ colspan: 4 }, { colspan: 8 }]}>
<Select
Expand Down Expand Up @@ -92,7 +97,7 @@ export const SceneRuleTargetEditor: React.FC<ISceneRuleTargetEditorProps> = ({
}}
chosenColor={chosenColor}
/>
{isAllValid && (
{isCustomStyle && (
<ColorPicker
color={chosenColor}
onSelectColor={(newColor) => {
Expand All @@ -101,7 +106,8 @@ export const SceneRuleTargetEditor: React.FC<ISceneRuleTargetEditorProps> = ({
onChange(convertToIotTwinMakerNamespace(targetInfo.type, colorWithIcon));
setChosenColor(newColor);
}}
label={formatMessage({ defaultMessage: 'Colors', description: 'Colors' })}
colorPickerLabel={formatMessage({ defaultMessage: 'Colors', description: 'Colors' })}
customColorLabel={formatMessage({ defaultMessage: 'Custom colors', description: 'Custom colors' })}
/>
)}
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export const SceneRuleTargetIconEditor: React.FC<ISceneRuleTargetIconEditorProps
return btoa(SCENE_ICONS[selectedIcon]);
}, [selectedIcon]);

const isAllValid = tagStyle && targetValue === 'Custom';
const isCustomStyle = tagStyle && targetValue === 'Custom';
return (
<Grid gridDefinition={[{ colspan: 9 }, { colspan: 2 }]}>
<Select
Expand All @@ -60,7 +60,7 @@ export const SceneRuleTargetIconEditor: React.FC<ISceneRuleTargetIconEditorProps
'Specifies the localized string that describes an option as being selected. This is required to provide a good screen reader experience',
})}
/>
{isAllValid ? (
{isCustomStyle ? (
<DecodeSvgString
selectedColor={chosenColor ?? colors.customBlue}
iconString={iconString}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { fireEvent, render, screen } from '@testing-library/react';
import React from 'react';
import wrapper from '@awsui/components-react/test-utils/dom';
import { render, screen } from '@testing-library/react';
import React from 'react';

import { DefaultAnchorStatus, IotTwinMakerNumberNamespace, SceneResourceType } from '../../../../';
import { getGlobalSettings } from '../../../../common/GlobalSettings';
Expand Down Expand Up @@ -37,7 +37,7 @@ describe('SceneRuleTargetEditor', () => {
});
render(<SceneRuleTargetEditor target='Custom-123' onChange={onChange} />);

const sceneRuleTargetIconEditor = screen.getByLabelText('Custom style');
const sceneRuleTargetIconEditor = screen.getByRole('button', { name: /Custom style/i });
expect(sceneRuleTargetIconEditor).toBeTruthy();
const colorPicker = screen.getByTestId('color-preview');
colorPicker.click();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ interface IAnchorComponentProps {
const AnchorComponent: React.FC<IAnchorComponentProps> = ({ node, component }: IAnchorComponentProps) => {
const sceneComposerId = useSceneComposerId();
const rule = useStore(sceneComposerId)((state) => state.getSceneRuleMapById(component.ruleBasedMapId));

return (
<group name={getComponentGroupName(node.ref, 'TAG')}>
<AnchorWidget
Expand Down
3 changes: 2 additions & 1 deletion packages/scene-composer/src/interfaces/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export interface IAnchorComponent extends ISceneComponent {
navLink?: INavLink;
offset?: Vector3;
chosenColor?: string;
customColors?: string[];
}

/**
Expand All @@ -70,9 +71,9 @@ export const SelectedAnchor = 'Selected';
*/
export interface ITagData {
chosenColor?: string;

navLink?: INavLink;
dataBindingContext?: unknown;
customColors?: string[];
}

/**
Expand Down
1 change: 1 addition & 0 deletions packages/scene-composer/src/models/SceneModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ export namespace Component {
navLink?: NavLink;
offset?: Vector3;
chosenColor?: string;
customColors?: string[];
}

export interface ModelShader extends IComponent, IDataBindingRuleMap {}
Expand Down
Loading
Loading