Skip to content

Commit bbad715

Browse files
Fix token deselection to toggle correct property (#3784)
* Initial plan * Fix gap token deselection to use correct property Co-authored-by: akshay-gupta7 <9948167+akshay-gupta7@users.noreply.github.com> * Remove analysis documents and add dimension token test Co-authored-by: akshay-gupta7 <9948167+akshay-gupta7@users.noreply.github.com> * Address review feedback: use activeStateProperties and remove unrelated changes Co-authored-by: akshay-gupta7 <9948167+akshay-gupta7@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: akshay-gupta7 <9948167+akshay-gupta7@users.noreply.github.com> Co-authored-by: Akshay Gupta <gravity.akshay@gmail.com>
1 parent 0a93964 commit bbad715

12 files changed

Lines changed: 210 additions & 111 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@tokens-studio/figma-plugin": patch
3+
---
4+
5+
Fix gap token deselection when left-clicking. Previously, left-clicking a gap token to deselect it would incorrectly attempt to remove the first property instead of the property that actually has the token applied. This caused the gap value to disappear from Figma's side panel while the token remained active in the inspect panel. The fix ensures that the correct property is toggled when deselecting tokens.

packages/tokens-studio-for-figma/src/app/components/FigmaVariableForm.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,11 @@ import {
66
import Tooltip from './Tooltip';
77
import IconPlus from '@/icons/plus.svg';
88
import IconMinus from '@/icons/minus.svg';
9-
import { EditTokenObject } from '@/types/tokens';
9+
import { EditTokenObject, VariableScope, CodeSyntaxPlatform } from '@/types/tokens';
1010
import Box from './Box';
1111
import Input from './Input';
1212
import { tokenTypesToCreateVariable } from '@/constants/VariableTypes';
1313

14-
import { VariableScope, CodeSyntaxPlatform } from '@/types/tokens';
1514
import {
1615
VARIABLE_SCOPE_OPTIONS, TOKEN_TYPE_TO_SCOPES_MAP, CODE_SYNTAX_PLATFORM_OPTIONS,
1716
} from '@/constants/FigmaVariableMetaData';

packages/tokens-studio-for-figma/src/app/components/MoreButton/MoreButton.test.tsx

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,88 @@ describe('MoreButton', () => {
200200
expect(mockSetNodeData).toBeCalledTimes(0);
201201
});
202202

203+
it('should deselect the correct property when token is applied to specific property', async () => {
204+
// Test case for the bug fix: when a spacing token is applied to gap (itemSpacing),
205+
// left-clicking should toggle gap, not the first property
206+
const mockStore = createMockStore({
207+
uiState: {
208+
mainNodeSelectionValues: {
209+
itemSpacing: token.name,
210+
},
211+
},
212+
});
213+
214+
const result = render(
215+
<Provider store={mockStore}>
216+
<MoreButton
217+
type={TokenTypes.SPACING}
218+
showForm={mockShowForm}
219+
token={token}
220+
/>
221+
</Provider>,
222+
);
223+
await fireEvent.click(result.getByText(token.name));
224+
expect(mockSetNodeData).toHaveBeenCalledWith({
225+
itemSpacing: 'delete',
226+
}, []);
227+
});
228+
229+
it('should deselect paddingLeft when token is applied to paddingLeft, not gap', async () => {
230+
// Test case for the bug fix: when a spacing token is applied to paddingLeft,
231+
// left-clicking should toggle paddingLeft, not gap
232+
const mockStore = createMockStore({
233+
uiState: {
234+
mainNodeSelectionValues: {
235+
paddingLeft: token.name,
236+
},
237+
},
238+
});
239+
240+
const result = render(
241+
<Provider store={mockStore}>
242+
<MoreButton
243+
type={TokenTypes.SPACING}
244+
showForm={mockShowForm}
245+
token={token}
246+
/>
247+
</Provider>,
248+
);
249+
await fireEvent.click(result.getByText(token.name));
250+
expect(mockSetNodeData).toHaveBeenCalledWith({
251+
paddingLeft: 'delete',
252+
}, []);
253+
});
254+
255+
it('should deselect correct child property for dimension tokens', async () => {
256+
// Test case for dimension tokens with child properties
257+
const dimensionToken: SingleToken = {
258+
value: '16px',
259+
name: 'dimension-token',
260+
type: TokenTypes.DIMENSION,
261+
};
262+
const mockStore = createMockStore({
263+
uiState: {
264+
mainNodeSelectionValues: {
265+
itemSpacing: dimensionToken.name,
266+
},
267+
},
268+
});
269+
270+
const result = render(
271+
<Provider store={mockStore}>
272+
<MoreButton
273+
type={TokenTypes.DIMENSION}
274+
showForm={mockShowForm}
275+
token={dimensionToken}
276+
/>
277+
</Provider>,
278+
);
279+
await fireEvent.click(result.getByText(dimensionToken.name));
280+
expect(mockSetNodeData).toHaveBeenCalledWith({
281+
itemSpacing: 'delete',
282+
}, []);
283+
});
284+
203285
it('show all properties about dimension token', async () => {
204286
const dimensionToken: SingleToken = {
205287
value: '16px',

packages/tokens-studio-for-figma/src/app/components/MoreButton/MoreButton.tsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import copy from 'copy-to-clipboard';
88

99
import { ContextMenu } from '@tokens-studio/ui';
1010
import { styled } from '@/stitches.config';
11-
import { activeTokenSetReadOnlySelector, activeTokenSetSelector, editProhibitedSelector } from '@/selectors';
11+
import {
12+
activeTokenSetReadOnlySelector, activeTokenSetSelector, editProhibitedSelector, mainNodeSelectionValuesSelector,
13+
} from '@/selectors';
1214
import { PropertyObject } from '@/types/properties';
1315
import { MoreButtonProperty } from './MoreButtonProperty';
1416
import { DocumentationProperties } from '@/constants/DocumentationProperties';
@@ -54,6 +56,7 @@ export const MoreButton: React.FC<React.PropsWithChildren<React.PropsWithChildre
5456
const editProhibited = useSelector(editProhibitedSelector);
5557
const activeTokenSetReadOnly = useSelector(activeTokenSetReadOnlySelector);
5658
const activeTokenSet = useSelector(activeTokenSetSelector);
59+
const mainNodeSelectionValues = useSelector(mainNodeSelectionValuesSelector);
5760
const { deleteSingleToken } = useManageTokens();
5861

5962
const canEdit = !editProhibited && !activeTokenSetReadOnly;
@@ -132,10 +135,21 @@ export const MoreButton: React.FC<React.PropsWithChildren<React.PropsWithChildre
132135
if (canEdit && ((isMacBrowser && event.metaKey) || (!isMacBrowser && event.ctrlKey))) {
133136
handleEditClick();
134137
} else {
135-
handleClick(properties[0]);
138+
// Find the property that currently has this token
139+
const activeProperty = activeStateProperties.find(
140+
(prop) => mainNodeSelectionValues[prop.name] === token.name,
141+
);
142+
143+
if (activeProperty) {
144+
// If token is active on a specific property, toggle that property
145+
handleClick(activeProperty, true);
146+
} else {
147+
// If token is not active, apply it to the first property
148+
handleClick(properties[0]);
149+
}
136150
}
137151
},
138-
[canEdit, handleEditClick, handleClick, properties],
152+
[canEdit, handleEditClick, handleClick, properties, activeStateProperties, mainNodeSelectionValues, token.name],
139153
);
140154

141155
return (

packages/tokens-studio-for-figma/src/app/store/useTokens.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -569,9 +569,9 @@ export default function useTokens() {
569569
const tokensToSet = uiState.selectionValues
570570
.filter((v) => inspectState.selectedTokens.includes(`${v.category}-${v.value}`))
571571
.map((v) => ({ nodes: v.nodes, property: v.type })) as {
572-
property: Properties;
573-
nodes: NodeInfo[];
574-
}[];
572+
property: Properties;
573+
nodes: NodeInfo[];
574+
}[];
575575

576576
track('setNoneValuesOnNode', tokensToSet);
577577

packages/tokens-studio-for-figma/src/constants/FigmaVariableMetaData.ts

Lines changed: 75 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -2,85 +2,85 @@ import { TokenTypes } from './TokenTypes';
22
import { VariableScope, CodeSyntaxPlatform } from '@/types/tokens';
33

44
export const VARIABLE_SCOPE_OPTIONS: { value: VariableScope; label: string }[] = [
5-
{ value: 'NONE', label: 'None' },
6-
{ value: 'ALL_SCOPES', label: 'Show in all supported properties' },
7-
{ value: 'TEXT_CONTENT', label: 'Text content' },
8-
{ value: 'CORNER_RADIUS', label: 'Corner radius' },
9-
{ value: 'WIDTH_HEIGHT', label: 'Width and height' },
10-
{ value: 'GAP', label: 'Gap' },
11-
{ value: 'STROKE_FLOAT', label: 'Stroke' },
12-
{ value: 'OPACITY', label: 'Layer opacity' },
13-
{ value: 'EFFECT_FLOAT', label: 'Effects' },
14-
{ value: 'FONT_WEIGHT', label: 'Font weight' },
15-
{ value: 'FONT_SIZE', label: 'Font size' },
16-
{ value: 'LINE_HEIGHT', label: 'Line height' },
17-
{ value: 'LETTER_SPACING', label: 'Letter spacing' },
18-
{ value: 'PARAGRAPH_SPACING', label: 'Paragraph spacing' },
19-
{ value: 'PARAGRAPH_INDENT', label: 'Paragraph indent' },
20-
{ value: 'ALL_FILLS', label: 'Fill' },
21-
{ value: 'FRAME_FILL', label: 'Frame' },
22-
{ value: 'SHAPE_FILL', label: 'Shape' },
23-
{ value: 'TEXT_FILL', label: 'Text' },
24-
{ value: 'STROKE_COLOR', label: 'Stroke' },
25-
{ value: 'EFFECT_COLOR', label: 'Effects' },
26-
{ value: 'FONT_FAMILY', label: 'Font family' },
27-
{ value: 'FONT_STYLE', label: 'Font style' },
5+
{ value: 'NONE', label: 'None' },
6+
{ value: 'ALL_SCOPES', label: 'Show in all supported properties' },
7+
{ value: 'TEXT_CONTENT', label: 'Text content' },
8+
{ value: 'CORNER_RADIUS', label: 'Corner radius' },
9+
{ value: 'WIDTH_HEIGHT', label: 'Width and height' },
10+
{ value: 'GAP', label: 'Gap' },
11+
{ value: 'STROKE_FLOAT', label: 'Stroke' },
12+
{ value: 'OPACITY', label: 'Layer opacity' },
13+
{ value: 'EFFECT_FLOAT', label: 'Effects' },
14+
{ value: 'FONT_WEIGHT', label: 'Font weight' },
15+
{ value: 'FONT_SIZE', label: 'Font size' },
16+
{ value: 'LINE_HEIGHT', label: 'Line height' },
17+
{ value: 'LETTER_SPACING', label: 'Letter spacing' },
18+
{ value: 'PARAGRAPH_SPACING', label: 'Paragraph spacing' },
19+
{ value: 'PARAGRAPH_INDENT', label: 'Paragraph indent' },
20+
{ value: 'ALL_FILLS', label: 'Fill' },
21+
{ value: 'FRAME_FILL', label: 'Frame' },
22+
{ value: 'SHAPE_FILL', label: 'Shape' },
23+
{ value: 'TEXT_FILL', label: 'Text' },
24+
{ value: 'STROKE_COLOR', label: 'Stroke' },
25+
{ value: 'EFFECT_COLOR', label: 'Effects' },
26+
{ value: 'FONT_FAMILY', label: 'Font family' },
27+
{ value: 'FONT_STYLE', label: 'Font style' },
2828
];
2929

3030
export const TOKEN_TYPE_TO_SCOPES_MAP: Record<string, VariableScope[]> = {
31-
[TokenTypes.COLOR]: [
32-
'NONE', 'ALL_SCOPES', 'ALL_FILLS', 'FRAME_FILL', 'SHAPE_FILL', 'TEXT_FILL', 'STROKE_COLOR', 'EFFECT_COLOR',
33-
],
34-
[TokenTypes.BORDER_RADIUS]: [
35-
'NONE', 'ALL_SCOPES', 'CORNER_RADIUS',
36-
],
37-
[TokenTypes.SIZING]: [
38-
'NONE', 'ALL_SCOPES', 'WIDTH_HEIGHT', 'GAP',
39-
],
40-
[TokenTypes.SPACING]: [
41-
'NONE', 'ALL_SCOPES', 'WIDTH_HEIGHT', 'GAP',
42-
],
43-
[TokenTypes.DIMENSION]: [
44-
'NONE', 'ALL_SCOPES', 'WIDTH_HEIGHT', 'GAP', 'CORNER_RADIUS',
45-
],
46-
[TokenTypes.BORDER_WIDTH]: [
47-
'NONE', 'ALL_SCOPES', 'STROKE_FLOAT',
48-
],
49-
[TokenTypes.OPACITY]: [
50-
'NONE', 'ALL_SCOPES', 'OPACITY',
51-
],
52-
[TokenTypes.FONT_FAMILIES]: [
53-
'NONE', 'ALL_SCOPES', 'FONT_FAMILY',
54-
],
55-
[TokenTypes.FONT_WEIGHTS]: [
56-
'NONE', 'ALL_SCOPES', 'FONT_WEIGHT',
57-
],
58-
[TokenTypes.FONT_SIZES]: [
59-
'NONE', 'ALL_SCOPES', 'FONT_SIZE',
60-
],
61-
[TokenTypes.LINE_HEIGHTS]: [
62-
'NONE', 'ALL_SCOPES', 'LINE_HEIGHT',
63-
],
64-
[TokenTypes.LETTER_SPACING]: [
65-
'NONE', 'ALL_SCOPES', 'LETTER_SPACING',
66-
],
67-
[TokenTypes.PARAGRAPH_SPACING]: [
68-
'NONE', 'ALL_SCOPES', 'PARAGRAPH_SPACING',
69-
],
70-
[TokenTypes.PARAGRAPH_INDENT]: [
71-
'NONE', 'ALL_SCOPES', 'PARAGRAPH_INDENT',
72-
],
73-
[TokenTypes.TEXT]: [
74-
'NONE', 'ALL_SCOPES', 'TEXT_CONTENT', 'FONT_FAMILY', 'FONT_STYLE', 'FONT_WEIGHT',
75-
],
76-
[TokenTypes.BOOLEAN]: ['NONE'],
77-
[TokenTypes.NUMBER]: [
78-
'NONE', 'ALL_SCOPES', 'TEXT_CONTENT', 'WIDTH_HEIGHT', 'GAP', 'CORNER_RADIUS', 'STROKE_FLOAT', 'EFFECT_FLOAT', 'OPACITY', 'FONT_WEIGHT', 'FONT_SIZE', 'LINE_HEIGHT', 'LETTER_SPACING', 'PARAGRAPH_SPACING', 'PARAGRAPH_INDENT',
79-
],
31+
[TokenTypes.COLOR]: [
32+
'NONE', 'ALL_SCOPES', 'ALL_FILLS', 'FRAME_FILL', 'SHAPE_FILL', 'TEXT_FILL', 'STROKE_COLOR', 'EFFECT_COLOR',
33+
],
34+
[TokenTypes.BORDER_RADIUS]: [
35+
'NONE', 'ALL_SCOPES', 'CORNER_RADIUS',
36+
],
37+
[TokenTypes.SIZING]: [
38+
'NONE', 'ALL_SCOPES', 'WIDTH_HEIGHT', 'GAP',
39+
],
40+
[TokenTypes.SPACING]: [
41+
'NONE', 'ALL_SCOPES', 'WIDTH_HEIGHT', 'GAP',
42+
],
43+
[TokenTypes.DIMENSION]: [
44+
'NONE', 'ALL_SCOPES', 'WIDTH_HEIGHT', 'GAP', 'CORNER_RADIUS',
45+
],
46+
[TokenTypes.BORDER_WIDTH]: [
47+
'NONE', 'ALL_SCOPES', 'STROKE_FLOAT',
48+
],
49+
[TokenTypes.OPACITY]: [
50+
'NONE', 'ALL_SCOPES', 'OPACITY',
51+
],
52+
[TokenTypes.FONT_FAMILIES]: [
53+
'NONE', 'ALL_SCOPES', 'FONT_FAMILY',
54+
],
55+
[TokenTypes.FONT_WEIGHTS]: [
56+
'NONE', 'ALL_SCOPES', 'FONT_WEIGHT',
57+
],
58+
[TokenTypes.FONT_SIZES]: [
59+
'NONE', 'ALL_SCOPES', 'FONT_SIZE',
60+
],
61+
[TokenTypes.LINE_HEIGHTS]: [
62+
'NONE', 'ALL_SCOPES', 'LINE_HEIGHT',
63+
],
64+
[TokenTypes.LETTER_SPACING]: [
65+
'NONE', 'ALL_SCOPES', 'LETTER_SPACING',
66+
],
67+
[TokenTypes.PARAGRAPH_SPACING]: [
68+
'NONE', 'ALL_SCOPES', 'PARAGRAPH_SPACING',
69+
],
70+
[TokenTypes.PARAGRAPH_INDENT]: [
71+
'NONE', 'ALL_SCOPES', 'PARAGRAPH_INDENT',
72+
],
73+
[TokenTypes.TEXT]: [
74+
'NONE', 'ALL_SCOPES', 'TEXT_CONTENT', 'FONT_FAMILY', 'FONT_STYLE', 'FONT_WEIGHT',
75+
],
76+
[TokenTypes.BOOLEAN]: ['NONE'],
77+
[TokenTypes.NUMBER]: [
78+
'NONE', 'ALL_SCOPES', 'TEXT_CONTENT', 'WIDTH_HEIGHT', 'GAP', 'CORNER_RADIUS', 'STROKE_FLOAT', 'EFFECT_FLOAT', 'OPACITY', 'FONT_WEIGHT', 'FONT_SIZE', 'LINE_HEIGHT', 'LETTER_SPACING', 'PARAGRAPH_SPACING', 'PARAGRAPH_INDENT',
79+
],
8080
};
8181

8282
export const CODE_SYNTAX_PLATFORM_OPTIONS: { value: CodeSyntaxPlatform; label: string }[] = [
83-
{ value: 'Web', label: 'Web' },
84-
{ value: 'Android', label: 'Android' },
85-
{ value: 'iOS', label: 'iOS' },
83+
{ value: 'Web', label: 'Web' },
84+
{ value: 'Android', label: 'Android' },
85+
{ value: 'iOS', label: 'iOS' },
8686
];

packages/tokens-studio-for-figma/src/plugin/createLocalVariablesInPlugin.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,8 @@ export default async function createLocalVariablesInPlugin(tokens: Record<string
101101
}
102102

103103
/**
104-
* We perform a pre-flight scan across ALL selected themes. We build a global map
105-
* (`providedPlatformsByVariable`) of every platform that HAS a value defined
104+
* We perform a pre-flight scan across ALL selected themes. We build a global map
105+
* (`providedPlatformsByVariable`) of every platform that HAS a value defined
106106
* for a given variable name.
107107
* During the actual update (in setValuesOnVariable), we use this map:
108108
* 1. If a platform is missing in the current mode BUT exists globally, we skip it (Aggregation mode).

packages/tokens-studio-for-figma/src/plugin/pullVariables.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable no-continue */
2-
import { FIGMA_PLATFORMS } from '@/utils/figma';
32
import { figmaRGBToHex } from '@figma-plugin/helpers';
3+
import { FIGMA_PLATFORMS } from '@/utils/figma';
44
import { notifyVariableValues, notifyRenamedCollections, notifyException } from './notifiers';
55
import { PullVariablesOptions, ThemeObjectsList } from '@/types';
66
import { VariableToCreateToken } from '@/types/payloads';

packages/tokens-studio-for-figma/src/plugin/setValuesOnVariable.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,6 @@ describe('SetValuesOnVariable', () => {
286286
} as unknown as Variable;
287287
});
288288

289-
290289
it('should update scopes with value change', async () => {
291290
const tokens = [{
292291
name: 'test.variable',

packages/tokens-studio-for-figma/src/plugin/setValuesOnVariable.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ export default async function setValuesOnVariable(
242242
// Avoid redundant metadata updates for the same variable in the same run (e.g. across multiple modes)
243243
if (!codeSyntaxUpdateTracker[currentVar.id]) {
244244
try {
245-
// Only update Figma metadata such as scopes, code syntax, and description if we're updating the value as well
245+
// Only update Figma metadata such as scopes, code syntax, and description if we're updating the value as well
246246
if (hasMetadataChanged) {
247247
// Update hiddenFromPublishing — only write when explicitly set in token extensions.
248248
const hiddenFromPublishingExt = token.$extensions?.['com.figma.hiddenFromPublishing'];

0 commit comments

Comments
 (0)