Skip to content

Commit

Permalink
feat(scene composer): rule icon using color picker
Browse files Browse the repository at this point in the history
This changes contain rules updated with color picker, removed unwanted effects from color picker
sufficient unit tests
  • Loading branch information
divya-sea committed Aug 7, 2023
1 parent b3f4f9d commit f1519c4
Show file tree
Hide file tree
Showing 10 changed files with 171 additions and 82 deletions.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ const SceneRuleMapExpandableInfoSection: React.FC<React.PropsWithChildren<IScene
if (newRule) {
items.push(newRule);
}

return (
<ExpandableInfoSection title={ruleBasedMapId} defaultExpanded={false}>
<AttributeEditor
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Button, FormField, Icon, Input, InputProps, SpaceBetween } from '@awsui/components-react';
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';
Expand All @@ -19,91 +19,114 @@ import { palleteColors } from './ColorPickerUtils/TagColors';

export const ColorPicker = ({ color, onSelectColor, label }: 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 intl = useIntl();

const handleClick = useCallback(() => {
/**
* This method uses a regular expression (`hexRegex`) to validate a hex color code.
* The regex checks if the hex code starts with a "#" symbol, followed by either a
* 6-digit or 3-digit combination of characters from A-F, a-f, and 0-9.
* The `test` method is then used to validate the `hexCode` against the regex pattern.
* @param hexCode
* @returns
*/
const isValidHexCode = (hexCode: string) => {
const hexRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
return hexRegex.test(hexCode);
};

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

const handleColorChange = useCallback(
(color) => {
onSelectColor(color.hex);
},
[color],
);
const handleColorChange = (color) => {
setNewColor(color.hex);
onSelectColor(color.hex);
};

const handleHexCodeChange = useCallback(
(event: NonCancelableCustomEvent<InputProps.ChangeDetail>) => {
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);
},
[color],
);
} else {
setHexCodeError(
intl.formatMessage({ defaultMessage: 'Invalid hex code', description: 'hex validations messages' }),
); // Set the error message
}
}, []);

const handleShowChromePicker = useCallback(() => {
const handleShowChromePicker = () => {
setShowChromePicker(true);
setShowPicker(false);
}, []);
};

const handleClose = useCallback(() => {
const handleClose = () => {
setShowPicker(false);
setShowChromePicker(false);
}, []);
};

const handleCloseChromePicker = useCallback(() => {
const handleCloseChromePicker = () => {
setShowChromePicker(false);
}, []);
};

return (
<SpaceBetween size='m'>
<SpaceBetween size='m' direction='horizontal'>
<FormField label={label} />
<Button
data-testid='color-preview'
ariaLabel={intl.formatMessage({ defaultMessage: 'colorPreview', description: 'color picker preview' })}
variant='inline-icon'
iconSvg={<Icon size='big' svg={colorPickerPreviewSvg(color)} />}
onClick={() => {
handleClick();
onSelectColor(color);
}}
/>
<Input
ariaLabel={intl.formatMessage({ defaultMessage: 'Hex code', description: 'color picker label' })}
data-testid='hexcode'
value={color}
onChange={handleHexCodeChange}
/>
<div style={tmColorPickerContainer}>
{showPicker && !showChromePicker && (
<div style={tmColorPickerPopover}>
<div>
<div style={tmCover} onClick={handleClose} />
<CirclePicker
width='300px'
data-testid='circlePicker'
aria-label={intl.formatMessage({ defaultMessage: 'circlePicker', description: 'circle picker' })}
colors={Object.values(palleteColors)}
color={color}
onChange={handleColorChange}
/>
<FormField errorText={hexCodeError}>
<SpaceBetween size='m' direction='horizontal'>
<TextContent>
<h5>{label}</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)} />}
onClick={() => {
handleClick();
setHexCodeError('');
}}
/>
<Input
ariaLabel={intl.formatMessage({ defaultMessage: 'Hex code', description: 'color picker label' })}
data-testid='hexcode'
value={newColor}
onChange={handleHexCodeChange}
/>
<div style={tmColorPickerContainer}>
{showPicker && !showChromePicker && (
<div style={tmColorPickerPopover}>
<div>
<div style={tmCover} onClick={handleClose} />
<CirclePicker
width='300px'
data-testid='circlePicker'
aria-label={intl.formatMessage({ defaultMessage: 'circlePicker', description: 'circle picker' })}
colors={[...new Set(Object.values(palleteColors))]}
color={color}
onChange={handleColorChange}
/>
</div>
<div style={tmDivider} />
<button style={tmAddButton} onClick={handleShowChromePicker}>
<Icon name='add-plus' />
</button>
</div>
<div style={tmDivider} />
<button style={tmAddButton} onClick={handleShowChromePicker}>
<Icon name='add-plus' />
</button>
</div>
)}
</div>
</SpaceBetween>
)}
</div>
</SpaceBetween>
</FormField>
{showChromePicker && (
<div style={tmPopover}>
<div
aria-label={intl.formatMessage({ defaultMessage: 'chromePicker', description: 'chrome picker' })}
style={tmCover}
onClick={handleCloseChromePicker}
/>
<ChromePicker disableAlpha color={color} onChangeComplete={(newColor) => onSelectColor(newColor.hex)} />
<ChromePicker color={color} onChangeComplete={(newColor) => onSelectColor(newColor.hex)} />
</div>
)}
</SpaceBetween>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import { useIntl, defineMessages } from 'react-intl';
import { Grid, Select } from '@awsui/components-react';

Expand All @@ -9,6 +9,9 @@ import {
getSceneResourceInfo,
} from '../../../utils/sceneResourceUtils';
import useFeature from '../../../hooks/useFeature';
import { getGlobalSettings } from '../../../common/GlobalSettings';
import { ColorPicker } from '../scene-components/tag-style/ColorPicker/ColorPicker';
import { colors } from '../../../utils/styleUtils';

import { SceneRuleTargetColorEditor } from './SceneRuleTargetColorEditor';
import { SceneRuleTargetIconEditor } from './SceneRuleTargetIconEditor';
Expand Down Expand Up @@ -42,11 +45,13 @@ export const SceneRuleTargetEditor: React.FC<ISceneRuleTargetEditorProps> = ({
target,
onChange,
}: ISceneRuleTargetEditorProps) => {
const getCustomColor: string = target.includes('Custom-') ? target.split('-')[1] : colors.customBlue;
const targetInfo = getSceneResourceInfo(target);
const { formatMessage } = useIntl();

const [{ variation: opacityRuleEnabled }] = useFeature(COMPOSER_FEATURES[COMPOSER_FEATURES.OpacityRule]);

const tagStyle = getGlobalSettings().featureConfig[COMPOSER_FEATURES.TagStyle];
const [chosenColor, setChosenColor] = useState<string>(getCustomColor);
const options = Object.values(SceneResourceType)
.filter((type) => {
return opacityRuleEnabled === 'C' ? type !== SceneResourceType.Opacity : true;
Expand All @@ -55,6 +60,7 @@ export const SceneRuleTargetEditor: React.FC<ISceneRuleTargetEditorProps> = ({
label: formatMessage(i18nSceneResourceTypeStrings[SceneResourceType[type]]) || SceneResourceType[type],
value: SceneResourceType[type],
}));
const isAllValid = tagStyle && targetInfo.value === 'Custom';
return (
<Grid gridDefinition={[{ colspan: 4 }, { colspan: 8 }]}>
<Select
Expand All @@ -77,10 +83,23 @@ export const SceneRuleTargetEditor: React.FC<ISceneRuleTargetEditorProps> = ({
})}
/>
{targetInfo.type === SceneResourceType.Icon && (
<SceneRuleTargetIconEditor
targetValue={targetInfo.value}
onChange={(targetValue) => onChange(convertToIotTwinMakerNamespace(targetInfo.type, targetValue))}
/>
<>
<SceneRuleTargetIconEditor
targetValue={targetInfo.value}
onChange={(targetValue) => {
const colorWithIcon = targetValue === 'Custom' ? `${targetValue}-${chosenColor}` : targetValue;
onChange(convertToIotTwinMakerNamespace(targetInfo.type, colorWithIcon));
}}
chosenColor={chosenColor}
/>
{isAllValid && (
<ColorPicker
color={chosenColor}
onSelectColor={(newColor) => setChosenColor(newColor)}
label={formatMessage({ defaultMessage: 'Colors', description: 'Colors' })}
/>
)}
</>
)}
{targetInfo.type === SceneResourceType.Color && (
<SceneRuleTargetColorEditor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,19 @@ import { getGlobalSettings } from '../../../common/GlobalSettings';
import { SCENE_ICONS } from '../../../common/constants';
import { COMPOSER_FEATURES, DefaultAnchorStatus } from '../../../interfaces';
import { i18nSceneIconsKeysStrings } from '../../../utils/polarisUtils';
import { colors } from '../../../utils/styleUtils';
import { DecodeSvgString } from '../scene-components/tag-style/ColorPicker/ColorPickerUtils/DecodeSvgString';

interface ISceneRuleTargetIconEditorProps {
targetValue: string;
onChange: (target: string) => void;
chosenColor?: string;
}

export const SceneRuleTargetIconEditor: React.FC<ISceneRuleTargetIconEditorProps> = ({
targetValue,
onChange,
chosenColor,
}: ISceneRuleTargetIconEditorProps) => {
const propsSelectedIcon = DefaultAnchorStatus[targetValue] ?? DefaultAnchorStatus.Info;
const [selectedIcon, setSelectedIcon] = useState<DefaultAnchorStatus>(propsSelectedIcon);
Expand All @@ -33,6 +37,7 @@ export const SceneRuleTargetIconEditor: React.FC<ISceneRuleTargetIconEditorProps
return btoa(SCENE_ICONS[selectedIcon]);
}, [selectedIcon]);

const isAllValid = tagStyle && targetValue === 'Custom';
return (
<Grid gridDefinition={[{ colspan: 9 }, { colspan: 2 }]}>
<Select
Expand All @@ -55,7 +60,16 @@ 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',
})}
/>
<img width='32px' height='32px' src={`data:image/svg+xml;base64,${iconString}`} />
{isAllValid ? (
<DecodeSvgString
selectedColor={chosenColor ?? colors.customBlue}
iconString={iconString}
width='32px'
height='32px'
/>
) : (
<img width='32px' height='32px' src={`data:image/svg+xml;base64,${iconString}`} />
)}
</Grid>
);
};
Original file line number Diff line number Diff line change
@@ -1,23 +1,52 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import React from 'react';
import { render } from '@testing-library/react';
import wrapper from '@awsui/components-react/test-utils/dom';

import { SceneRuleTargetEditor } from '../SceneRuleTargetEditor';
import { DefaultAnchorStatus, IotTwinMakerNumberNamespace, SceneResourceType } from '../../../../';
import { convertToIotTwinMakerNamespace } from '../../../../utils/sceneResourceUtils';
import { getGlobalSettings } from '../../../../common/GlobalSettings';
import { COMPOSER_FEATURES } from '../../../../interfaces/feature';
import { convertToIotTwinMakerNamespace, getSceneResourceInfo } from '../../../../utils/sceneResourceUtils';
import { colors } from '../../../../utils/styleUtils';
import { SceneRuleTargetEditor } from '../SceneRuleTargetEditor';

jest.mock('@awsui/components-react', () => ({
...jest.requireActual('@awsui/components-react'),
}));

jest.mock('../../../../common/GlobalSettings');
jest.mock('../../../../utils/sceneResourceUtils', () => {
const originalModule = jest.requireActual('../../../../utils/sceneResourceUtils');
return {
...originalModule,
getSceneResourceInfo: jest.fn(),
};
});
describe('SceneRuleTargetEditor', () => {
const onChange = jest.fn();

beforeEach(() => {
jest.clearAllMocks();
});

it('should render color picker upon selecting the custom style', async () => {
const globalSettingsMock = getGlobalSettings as jest.Mock;
const mockFeatureConfig = { [COMPOSER_FEATURES.TagStyle]: true };
globalSettingsMock.mockReturnValue({ featureConfig: mockFeatureConfig });
(getSceneResourceInfo as jest.Mock).mockReturnValue({
type: SceneResourceType.Icon,
value: DefaultAnchorStatus.Custom,
});
render(<SceneRuleTargetEditor target='Custom-123' onChange={onChange} />);

const sceneRuleTargetIconEditor = screen.getByRole('button', { name: /Custom style/i });
expect(sceneRuleTargetIconEditor).toBeTruthy();
const colorPicker = screen.getByTestId('color-preview');
colorPicker.click();
await waitFor(() => {
const circlePicker = document.querySelector('.circle-picker');
expect(circlePicker).toBeTruthy();
});
});

it('should change the selection from icon to color', () => {
const { container } = render(<SceneRuleTargetEditor target={DefaultAnchorStatus.Info} onChange={onChange} />);
const polarisWrapper = wrapper(container);
Expand Down

0 comments on commit f1519c4

Please sign in to comment.