Skip to content

Commit 2632b5f

Browse files
authored
feat(ui): allow cmd+click on style panel items to skip next-shape style updates (tldraw#8452)
In order to let users make one-off style changes without affecting defaults for future shapes, this PR adds accelerator key (Cmd on Mac / Ctrl on Windows/Linux) modifier support to all style panel controls. When the modifier is held while shapes are selected, the style change applies only to those shapes and does not update `stylesForNextShape` or `opacityForNextShape`. Closes tldraw#8431. The implementation checks `editor.inputs.getAccelKey()` directly in `StylePanelContext`, which provides both `onValueChange` (for styles) and a new `onOpacityChange` (for opacity). This keeps the modifier logic centralized — no changes needed in individual picker components. ### Change type - [x] `feature` ### Test plan 1. Select a shape on the canvas 2. Click a color in the style panel — verify it changes the shape AND updates the default for new shapes 3. Select another shape, hold Cmd (Mac) / Ctrl (Win/Linux) and click a different color — verify it changes the shape but new shapes still use the previous default 4. Test the same with opacity slider, fill, dash, size, font, geo, arrowhead, and spline pickers 5. Test drag-to-paint (pointer down + drag across items) with Cmd held — verify modifier is respected throughout the drag - [ ] Unit tests - [ ] End to end tests ### Release notes - Add accelerator key modifier (Cmd on Mac / Ctrl on Windows/Linux) to style panel items to apply styles to selected shapes only, without changing the default style for new shapes. ### API changes - Added `StylePanelContext.onOpacityChange(opacity: number)` — centralizes opacity change handling with the same modifier-key behavior as `onValueChange` ### Code changes | Section | LOC change | | --------------- | ---------- | | Core code | +43 / -16 | | Automated files | +2 / -0 |
1 parent 18558fd commit 2632b5f

3 files changed

Lines changed: 45 additions & 16 deletions

File tree

packages/tldraw/api-report.api.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3149,6 +3149,8 @@ export interface StylePanelContext {
31493149
// (undocumented)
31503150
onHistoryMark(id: string): void;
31513151
// (undocumented)
3152+
onOpacityChange(opacity: number): void;
3153+
// (undocumented)
31523154
onValueChange<T>(style: StyleProp<T>, value: T): void;
31533155
// (undocumented)
31543156
styles: ReadonlySharedStyleMap;

packages/tldraw/src/lib/ui/components/StylePanel/DefaultStylePanelContent.tsx

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import {
2020
} from '@tldraw/editor'
2121
import React from 'react'
2222
import { getColorStyleItems, getFontStyleItems, STYLES } from '../../../styles'
23-
import { useUiEvents } from '../../context/events'
2423
import { useTranslation } from '../../hooks/useTranslation/useTranslation'
2524
import { TldrawUiButtonIcon } from '../primitives/Button/TldrawUiButtonIcon'
2625
import { TldrawUiSlider } from '../primitives/TldrawUiSlider'
@@ -97,26 +96,16 @@ const tldrawSupportedOpacities = [0.1, 0.25, 0.5, 0.75, 1] as const
9796
/** @public @react */
9897
export function StylePanelOpacityPicker() {
9998
const editor = useEditor()
100-
const { onHistoryMark, enhancedA11yMode } = useStylePanelContext()
99+
const { onHistoryMark, onOpacityChange, enhancedA11yMode } = useStylePanelContext()
101100

102101
const opacity = useValue('opacity', () => editor.getSharedOpacity(), [editor])
103-
const trackEvent = useUiEvents()
104102
const msg = useTranslation()
105103

106104
const handleOpacityValueChange = React.useCallback(
107105
(value: number) => {
108-
const item = tldrawSupportedOpacities[value]
109-
editor.run(() => {
110-
if (editor.isIn('select')) {
111-
editor.setOpacityForSelectedShapes(item)
112-
}
113-
editor.setOpacityForNextShapes(item)
114-
editor.updateInstanceState({ isChangingStyle: true })
115-
})
116-
117-
trackEvent('set-style', { source: 'style-panel', id: 'opacity', value })
106+
onOpacityChange(tldrawSupportedOpacities[value])
118107
},
119-
[editor, trackEvent]
108+
[onOpacityChange]
120109
)
121110

122111
if (opacity === undefined) return null

packages/tldraw/src/lib/ui/components/StylePanel/StylePanelContext.tsx

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { ReadonlySharedStyleMap, StyleProp, useEditor, useValue } from '@tldraw/editor'
1+
import {
2+
ReadonlySharedStyleMap,
3+
StyleProp,
4+
unsafe__withoutCapture,
5+
useEditor,
6+
useValue,
7+
} from '@tldraw/editor'
28
import { createContext, useCallback, useContext } from 'react'
39
import { useUiEvents } from '../../context/events'
410

@@ -8,6 +14,7 @@ export interface StylePanelContext {
814
enhancedA11yMode: boolean
915
onHistoryMark(id: string): void
1016
onValueChange<T>(style: StyleProp<T>, value: T): void
17+
onOpacityChange(opacity: number): void
1118
}
1219
const StylePanelContext = createContext<null | StylePanelContext>(null)
1320

@@ -28,11 +35,20 @@ export function StylePanelContextProvider({ children, styles }: StylePanelContex
2835
])
2936
const onValueChange = useCallback(
3037
function <T>(style: StyleProp<T>, value: T) {
38+
// If the user is holding down the accelerator key (Ctrl on Windows/Linux, Cmd on Mac)
39+
// while shapes are selected, interpret that as a wish to change the style for their
40+
// current selected shapes but not for the next shapes.
41+
const skipNextShapeStyle = unsafe__withoutCapture(
42+
() => editor.getSelectedShapeIds().length > 0 && editor.inputs.getAccelKey()
43+
)
44+
3145
editor.run(() => {
3246
if (editor.isIn('select')) {
3347
editor.setStyleForSelectedShapes(style, value)
3448
}
35-
editor.setStyleForNextShapes(style, value)
49+
if (!skipNextShapeStyle) {
50+
editor.setStyleForNextShapes(style, value)
51+
}
3652
editor.updateInstanceState({ isChangingStyle: true })
3753
})
3854

@@ -41,13 +57,35 @@ export function StylePanelContextProvider({ children, styles }: StylePanelContex
4157
[editor, trackEvent]
4258
)
4359

60+
const onOpacityChange = useCallback(
61+
function (opacity: number) {
62+
const skipNextShapeStyle = unsafe__withoutCapture(
63+
() => editor.getSelectedShapeIds().length > 0 && editor.inputs.getAccelKey()
64+
)
65+
66+
editor.run(() => {
67+
if (editor.isIn('select')) {
68+
editor.setOpacityForSelectedShapes(opacity)
69+
}
70+
if (!skipNextShapeStyle) {
71+
editor.setOpacityForNextShapes(opacity)
72+
}
73+
editor.updateInstanceState({ isChangingStyle: true })
74+
})
75+
76+
trackEvent('set-style', { source: 'style-panel', id: 'opacity', value: opacity })
77+
},
78+
[editor, trackEvent]
79+
)
80+
4481
return (
4582
<StylePanelContext.Provider
4683
value={{
4784
styles: styles,
4885
enhancedA11yMode,
4986
onHistoryMark,
5087
onValueChange,
88+
onOpacityChange,
5189
}}
5290
>
5391
{children}

0 commit comments

Comments
 (0)