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

1166 introduce color modifiers #1498

Merged
merged 46 commits into from Jan 11, 2023
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
dd71404
create color token form
swordEdge Dec 19, 2022
fecc1c4
complete color tokenform ui
swordEdge Dec 19, 2022
eee15f6
add extension property to singleGenericToken
swordEdge Dec 20, 2022
3098d06
add modifier to internalEditToken
swordEdge Dec 21, 2022
3c95b4c
introduce mixModifier
swordEdge Dec 21, 2022
24c517c
change color by modifies
swordEdge Dec 21, 2022
29c77c7
save extension to the token
swordEdge Dec 21, 2022
e8b4053
refactor modifyColor function
swordEdge Dec 21, 2022
a3b9325
resolve mix value
swordEdge Dec 21, 2022
4d24ad0
validate the color token
swordEdge Dec 21, 2022
8bbdee2
remove modify
swordEdge Dec 21, 2022
f367a8b
use colorjs instead chorma
swordEdge Dec 23, 2022
df1b268
mix color
swordEdge Dec 23, 2022
a7952d1
make cssColor
swordEdge Dec 23, 2022
953fdbe
calculate modified value when getAliasValue
swordEdge Dec 27, 2022
00133c6
fix error in tokenHelper
swordEdge Dec 28, 2022
fdc592b
remove console
swordEdge Dec 28, 2022
3435e16
set inGamut false
swordEdge Dec 28, 2022
bb5ae55
fix error
swordEdge Dec 28, 2022
6d45741
code review
swordEdge Dec 28, 2022
e96c9f1
fix double calculating modify value
swordEdge Dec 28, 2022
8703abf
fix displayValue
swordEdge Dec 29, 2022
37fcc3c
remove extension on resolvedToken
swordEdge Dec 29, 2022
0e3eb3d
add tokenState test coverage
swordEdge Dec 29, 2022
f971e3a
add test coverage
swordEdge Dec 29, 2022
4ebd0a7
Merge branch 'next' of https://github.com/six7/figma-tokens into 1166…
swordEdge Dec 29, 2022
5c20ed9
resolve code review
swordEdge Dec 30, 2022
e8b3db6
test color.js
swordEdge Jan 4, 2023
bdaa8e9
fixes import
six7 Jan 5, 2023
b37f99f
Update src/utils/convertModifiedColorToHex.ts
six7 Jan 5, 2023
2d2c17b
remove declaration
six7 Jan 5, 2023
981ee2f
resolve merge conflict
swordEdge Jan 5, 2023
021b537
fix test error
swordEdge Jan 5, 2023
9699dd3
add convertModifiedColorToHex test coverage
swordEdge Jan 5, 2023
cdd9398
add modifyColor test
swordEdge Jan 5, 2023
a5fcbdd
save value as number
swordEdge Jan 6, 2023
fdb3547
improve color token tooltip content value
swordEdge Jan 7, 2023
da9a86b
convert tailwind to stitch
swordEdge Jan 7, 2023
be1d2a7
fix test coverage
swordEdge Jan 7, 2023
9a47dfd
resolve code review
swordEdge Jan 9, 2023
4040af8
add test coverage
swordEdge Jan 9, 2023
e2cedf8
Merge branch 'next' of https://github.com/six7/figma-tokens into 1166…
swordEdge Jan 10, 2023
5b80235
fix colorPickerTrigger error & convert input type
swordEdge Jan 10, 2023
c2c43d9
fix test coverage
swordEdge Jan 10, 2023
a143db4
resolve merge conflict
swordEdge Jan 11, 2023
7768485
append extension property
swordEdge Jan 11, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion babel.config.js
@@ -1,6 +1,6 @@
module.exports = {
presets: ['@babel/preset-env', '@babel/preset-typescript', '@babel/preset-react'],
plugins: ['@babel/proposal-class-properties', '@babel/proposal-object-rest-spread', "@babel/transform-typescript"],
plugins: ['@babel/proposal-class-properties', '@babel/proposal-object-rest-spread', "@babel/transform-typescript", "@babel/plugin-proposal-private-methods"],
env: {
test: {
presets: [
Expand Down
6 changes: 3 additions & 3 deletions cypress/integration/tokens.spec.js
Expand Up @@ -508,15 +508,15 @@ describe('TokenListing', () => {
value: 'composition.regular',
});
cy.get('[data-cy=composition-token-dropdown]').click();
cy.get('[data-cy=property-dropdown-menu-element-sizing]').click();
cy.get('[data-cy=item-dropdown-menu-element-sizing]').click();
fillInput({
input: 'value',
value: '$sizing.xs',
});
cy.get('[data-cy=button-style-add-multiple]').click();

cy.get('[data-cy=composition-token-dropdown]').eq(1).click();
cy.get('[data-cy=property-dropdown-menu-element-opacity]').click();
cy.get('[data-cy=item-dropdown-menu-element-opacity]').click();
fillInputNth({
input: 'value',
value: '$opacity.30',
Expand All @@ -525,7 +525,7 @@ describe('TokenListing', () => {

cy.get('[data-cy=button-style-add-multiple]').click();
cy.get('[data-cy=composition-token-dropdown]').eq(2).click();
cy.get('[data-cy=property-dropdown-menu-element-fontSizes]').click();
cy.get('[data-cy=item-dropdown-menu-element-fontSizes]').click();
fillInputNth({
input: 'value',
value: '$font-size.4',
Expand Down
5 changes: 5 additions & 0 deletions package.json
Expand Up @@ -41,6 +41,8 @@
"@sentry/react": "^6.2.5",
"@stitches/react": "^1.2.8",
"@storybook/addon-postcss": "^2.0.0",
"@types/chroma-js": "^2.1.4",
"@types/color": "^3.0.3",
"@types/file-saver": "^2.0.5",
"@welldone-software/why-did-you-render": "^6.2.3",
"autoprefixer": "^10.2.5",
Expand All @@ -49,8 +51,10 @@
"bitbucket": "^2.7.0",
"buffer": "^6.0.3",
"case": "^1.6.3",
"chroma-js": "^2.4.2",
"classnames": "^2.3.1",
"color2k": "^1.2.4",
"colorjs.io": "^0.4.2",
"core-js": "^3.9.1",
"cypress-react-selector": "^2.3.6",
"dnd-core": "^12.0.1",
Expand Down Expand Up @@ -111,6 +115,7 @@
},
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/plugin-proposal-private-methods": "^7.18.6",
"@babel/preset-env": "^7.12.16",
"@babel/preset-react": "^7.12.13",
"@babel/preset-typescript": "^7.12.16",
Expand Down
328 changes: 328 additions & 0 deletions src/app/components/ColorTokenForm.tsx
@@ -0,0 +1,328 @@
import React, { useCallback, useMemo } from 'react';
import { useUIDSeed } from 'react-uid';
import IconPlus from '@/icons/plus.svg';
import IconMinus from '@/icons/minus.svg';
import { EditTokenObject } from '@/types/tokens';
import Heading from './Heading';
import IconButton from './IconButton';
import Box from './Box';
import { TokenTypes } from '@/constants/TokenTypes';
import { ResolveTokenValuesResult } from '@/plugin/tokenHelpers';
import DownshiftInput from './DownshiftInput';
import ColorPicker from './ColorPicker';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuRadioGroup,
} from './DropdownMenu';
import { checkIfContainsAlias, getAliasValue } from '@/utils/alias';
import { DropdownMenuRadioElement } from './DropdownMenuRadioElement';
import IconToggleableDisclosure from './IconToggleableDisclosure';
import { StyledPrefix, StyledInput } from './Input';
import { getLabelForProperty } from '@/utils/getLabelForProperty';
import { ColorModifier, MixModifier } from '@/types/Modifier';
import { ColorModifierTypes } from '@/constants/ColorModifierTypes';
import { ColorSpaceTypes } from '@/constants/ColorSpaceTypes';
import { modifyColor } from '@/utils/modifyColor';
import { convertModifiedColorToHex } from '@/utils/convertModifiedColorToHex';

const defaultValue = 0;

export default function ColorTokenForm({
internalEditToken,
resolvedTokens,
resolvedValue,
handleColorChange,
handleColorDownShiftInputChange,
handleColorModifyChange,
handleRemoveColorModify,
}: {
internalEditToken: Extract<EditTokenObject, { type: TokenTypes.COLOR }>;
resolvedTokens: ResolveTokenValuesResult[];
resolvedValue: ReturnType<typeof getAliasValue>
handleColorChange: React.ChangeEventHandler;
handleColorDownShiftInputChange: (newInputValue: string) => void;
handleColorModifyChange: (newModify: ColorModifier) => void;
handleRemoveColorModify: () => void;
}) {
const seed = useUIDSeed();
const [inputHelperOpen, setInputHelperOpen] = React.useState(false);
const [inputMixHelperOpen, setInputMixHelperOpen] = React.useState(false);
const [operationMenuOpened, setOperationMenuOpened] = React.useState(false);
const [colorSpaceMenuOpened, setColorSpaceMenuOpened] = React.useState(false);
const [modifyVisible, setModifyVisible] = React.useState(false);

React.useEffect(() => {
if (internalEditToken?.$extensions?.['studio.tokens']?.modify) {
setModifyVisible(true);
}
}, [internalEditToken]);

const resolvedMixValue = React.useMemo(() => {
if (internalEditToken?.$extensions?.['studio.tokens']?.modify?.type === ColorModifierTypes.MIX && internalEditToken?.$extensions?.['studio.tokens']?.modify?.color) {
return typeof internalEditToken?.$extensions?.['studio.tokens']?.modify?.color === 'string'
? getAliasValue(internalEditToken?.$extensions?.['studio.tokens']?.modify?.color, resolvedTokens)
: null;
}
return null;
}, [internalEditToken, resolvedTokens]);

const modifiedColor = useMemo(() => {
if (resolvedValue) {
if (internalEditToken?.$extensions?.['studio.tokens']?.modify) {
const modifierType = internalEditToken?.$extensions?.['studio.tokens']?.modify?.type;
if (modifierType === ColorModifierTypes.LIGHTEN || modifierType === ColorModifierTypes.DARKEN || modifierType === ColorModifierTypes.ALPHA) {
return modifyColor(String(resolvedValue), internalEditToken?.$extensions?.['studio.tokens']?.modify);
}
if (modifierType === ColorModifierTypes.MIX && resolvedMixValue) {
return modifyColor(String(resolvedValue), { ...internalEditToken?.$extensions?.['studio.tokens']?.modify, color: String(resolvedMixValue) });
}
return resolvedValue;
}
return resolvedValue;
}
return null;
}, [internalEditToken, resolvedValue, resolvedMixValue]);

const displayColor = useMemo(() => {
if (resolvedValue) {
if (internalEditToken?.$extensions?.['studio.tokens']?.modify) {
const modifierType = internalEditToken?.$extensions?.['studio.tokens']?.modify?.type;
if (modifierType === ColorModifierTypes.LIGHTEN || modifierType === ColorModifierTypes.DARKEN || modifierType === ColorModifierTypes.ALPHA) {
return convertModifiedColorToHex(String(resolvedValue), internalEditToken?.$extensions?.['studio.tokens']?.modify);
}
if (modifierType === ColorModifierTypes.MIX && resolvedMixValue) {
return convertModifiedColorToHex(String(resolvedValue), { ...internalEditToken?.$extensions?.['studio.tokens']?.modify, color: String(resolvedMixValue) });
}
return resolvedValue;
}
return resolvedValue;
}
return null;
}, [internalEditToken, resolvedValue, resolvedMixValue]);

const handleToggleInputHelper = useCallback(() => {
setInputHelperOpen(!inputHelperOpen);
}, [inputHelperOpen]);

const handleToggleMixInputHelper = useCallback(() => {
setInputMixHelperOpen(!inputMixHelperOpen);
}, [inputMixHelperOpen]);

const handleColorValueChange = useCallback((color: string) => {
handleColorDownShiftInputChange(color);
}, [handleColorDownShiftInputChange]);

const handleModifyChange = React.useCallback((newModify: ColorModifier) => {
handleColorModifyChange(newModify);
}, [handleColorModifyChange]);

const addModify = useCallback(() => {
handleModifyChange({
type: ColorModifierTypes.LIGHTEN,
value: defaultValue,
space: ColorSpaceTypes.LCH,
});
}, [handleModifyChange]);

const removeModify = useCallback(() => {
setModifyVisible(false);
handleRemoveColorModify();
}, [handleRemoveColorModify]);

const handleOperationToggleMenu = useCallback(() => {
setOperationMenuOpened(!operationMenuOpened);
}, [operationMenuOpened]);

const handleColorSpaceToggleMenu = useCallback(() => {
setColorSpaceMenuOpened(!colorSpaceMenuOpened);
}, [colorSpaceMenuOpened]);

const onOperationSelected = useCallback((operation: string) => {
if (internalEditToken?.$extensions?.['studio.tokens']?.modify) {
handleModifyChange({
...internalEditToken?.$extensions?.['studio.tokens']?.modify,
type: operation,
} as ColorModifier);
}
}, [internalEditToken, handleModifyChange]);

const onColorSpaceSelected = useCallback((colorSpace: string) => {
if (internalEditToken?.$extensions?.['studio.tokens']?.modify) {
handleModifyChange({
...internalEditToken?.$extensions?.['studio.tokens']?.modify,
space: colorSpace,
} as ColorModifier);
}
}, [internalEditToken, handleModifyChange]);

const handleModifyValueChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
if (internalEditToken?.$extensions?.['studio.tokens']?.modify) {
handleModifyChange({
...internalEditToken?.$extensions?.['studio.tokens']?.modify,
value: e.target.valueAsNumber, // accept only number
});
}
}, [internalEditToken, handleModifyChange]);

const handleMixColorChange = useCallback((mixColor: string) => {
if (internalEditToken?.$extensions?.['studio.tokens']?.modify) {
handleModifyChange({
...internalEditToken?.$extensions?.['studio.tokens']?.modify,
color: mixColor,
} as MixModifier);
}
}, [internalEditToken, handleModifyChange]);

const handleMixColorInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
if (internalEditToken?.$extensions?.['studio.tokens']?.modify) {
handleModifyChange({
...internalEditToken?.$extensions?.['studio.tokens']?.modify,
color: e.target.value,
} as MixModifier);
}
}, [internalEditToken, handleModifyChange]);

const getIconComponent = React.useMemo(() => getLabelForProperty(internalEditToken?.$extensions?.['studio.tokens']?.modify?.type || 'Amount'), [internalEditToken]);

return (
<>
<DownshiftInput
value={internalEditToken.value}
type={TokenTypes.COLOR}
label={internalEditToken.schema?.property}
resolvedTokens={resolvedTokens}
initialName={internalEditToken.initialName}
handleChange={handleColorChange}
setInputValue={handleColorDownShiftInputChange}
placeholder="#000000, hsla(), rgba() or {alias}"
prefix={(
<button
type="button"
className="block w-4 h-4 rounded-sm cursor-pointer shadow-border shadow-gray-300 focus:shadow-focus focus:shadow-primary-400"
six7 marked this conversation as resolved.
Show resolved Hide resolved
style={{ background: String(resolvedValue), fontSize: 0 }}
onClick={handleToggleInputHelper}
>
{internalEditToken.value}
</button>
)}
suffix
/>
{inputHelperOpen && (
<ColorPicker value={internalEditToken.value} onChange={handleColorValueChange} />
)}
<Box css={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Heading size="xsmall">Modify</Heading>
{
!modifyVisible ? (
<IconButton
tooltip="Add new modifier"
dataCy="button-add-new-modify"
onClick={addModify}
icon={<IconPlus />}
/>
) : (
<IconButton
tooltip="Remove modifier"
dataCy="button-remove=modify"
onClick={removeModify}
icon={<IconMinus />}
/>
)
}
</Box>
{
modifyVisible && (
<>
<Box css={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
gap: '$3',
'& > .relative ': {
flex: '2',
},
}}
>
<DropdownMenu open={operationMenuOpened} onOpenChange={handleOperationToggleMenu}>
<DropdownMenuTrigger
data-cy="colortokenform-operation-selector"
bordered
css={{
flex: 1, height: '$10', display: 'flex', justifyContent: 'space-between',
}}
>
<span>{internalEditToken?.$extensions?.['studio.tokens']?.modify?.type || 'Choose an operation'}</span>
<IconToggleableDisclosure />
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={2} className="content scroll-container" css={{ maxHeight: '$dropdownMaxHeight' }}>
<DropdownMenuRadioGroup value={internalEditToken?.$extensions?.['studio.tokens']?.modify?.type}>
{Object.values(ColorModifierTypes).map((operation, index) => <DropdownMenuRadioElement key={`operation-${seed(index)}`} item={operation} index={index} itemSelected={onOperationSelected} />)}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu open={colorSpaceMenuOpened} onOpenChange={handleColorSpaceToggleMenu}>
<DropdownMenuTrigger
data-cy="colortokenform-colorspace-selector"
bordered
css={{
flex: 1, height: '$10', display: 'flex', justifyContent: 'space-between',
}}
>
<span>{internalEditToken?.$extensions?.['studio.tokens']?.modify?.space || 'Choose a color space'}</span>
<IconToggleableDisclosure />
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={2} className="content scroll-container" css={{ maxHeight: '$dropdownMaxHeight' }}>
<DropdownMenuRadioGroup value={internalEditToken?.$extensions?.['studio.tokens']?.modify?.space}>
{Object.values(ColorSpaceTypes).map((colorSpace, index) => <DropdownMenuRadioElement key={`colorspace-${seed(index)}`} item={colorSpace} index={index} itemSelected={onColorSpaceSelected} />)}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</Box>
{
internalEditToken?.$extensions?.['studio.tokens']?.modify?.type === ColorModifierTypes.MIX && (
<>
<DownshiftInput
value={internalEditToken?.$extensions?.['studio.tokens']?.modify?.color}
type={TokenTypes.COLOR}
resolvedTokens={resolvedTokens}
handleChange={handleMixColorInputChange}
setInputValue={handleMixColorChange}
placeholder="#000000, hsla(), rgba() or {alias}"
prefix={(
<button
type="button"
className="block w-4 h-4 rounded-sm cursor-pointer shadow-border shadow-gray-300 focus:shadow-focus focus:shadow-primary-400"
six7 marked this conversation as resolved.
Show resolved Hide resolved
style={{ background: String(resolvedMixValue), fontSize: 0 }}
onClick={handleToggleMixInputHelper}
>
{internalEditToken?.$extensions?.['studio.tokens']?.modify?.color}
</button>
)}
suffix
/>
{inputMixHelperOpen && (
<ColorPicker value={internalEditToken?.$extensions?.['studio.tokens']?.modify?.color} onChange={handleMixColorChange} />
)}
</>
)
}
<Box css={{ display: 'flex', position: 'relative', width: '100%' }} className="input">
<StyledPrefix isText>{getIconComponent}</StyledPrefix>
<StyledInput type="number" onChange={handleModifyValueChange} value={internalEditToken?.$extensions?.['studio.tokens']?.modify?.value} required />
</Box>
</>
)
}
{(checkIfContainsAlias(internalEditToken.value) || internalEditToken?.$extensions?.['studio.tokens']?.modify) && (
<div className="flex p-2 mt-2 font-mono text-gray-700 bg-gray-100 border-gray-300 rounded text-xxs itms-center">
six7 marked this conversation as resolved.
Show resolved Hide resolved
{internalEditToken.type === 'color' ? (
<div className="w-4 h-4 mr-1 border border-gray-200 rounded" style={{ background: String(displayColor) }} />
six7 marked this conversation as resolved.
Show resolved Hide resolved
) : null}
{modifiedColor?.toString()}
</div>
)}
</>
);
}