Skip to content

Commit

Permalink
Grid gap shorthand control (#6188)
Browse files Browse the repository at this point in the history
## Problem
The inspector only allowed to edit the row-gap and column-gap props, and
not the shorthand prop, `grid-gap`

## Fix
Turn the grid gap control into a cycleable control to allow switching to
the control for the shorthand prop.

`Hide whitespace` advised on the diff


https://github.com/user-attachments/assets/8378ca75-561c-4f44-bebc-b065cf48cd28
  • Loading branch information
bkrmendy authored Aug 6, 2024
1 parent d59fbdb commit 5e1b082
Showing 1 changed file with 144 additions and 38 deletions.
182 changes: 144 additions & 38 deletions editor/src/components/inspector/flex-section.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/** @jsxRuntime classic */
/** @jsxFrag React.Fragment */
/** @jsx jsx */
import { jsx } from '@emotion/react'
import React from 'react'
Expand All @@ -19,8 +20,16 @@ import { MetadataUtils } from '../../core/model/element-metadata-utils'
import { strictEvery } from '../../core/shared/array-utils'
import { useDispatch } from '../editor/store/dispatch-context'
import type { DetectedLayoutSystem } from 'utopia-shared/src/types'
import { NO_OP } from '../../core/shared/utils'
import { Icons, NumberInput, SquareButton, Subdued } from '../../uuiui'
import { NO_OP, assertNever } from '../../core/shared/utils'
import {
FlexRow,
Icons,
InspectorSectionIcons,
NumberInput,
SquareButton,
Subdued,
Tooltip,
} from '../../uuiui'
import type {
CSSKeyword,
CSSNumber,
Expand All @@ -41,7 +50,7 @@ import {
isValidGridDimensionKeyword,
type GridDimension,
} from './common/css-utils'
import { applyCommandsAction } from '../editor/actions/action-creators'
import { applyCommandsAction, transientActions } from '../editor/actions/action-creators'
import type { PropertyToUpdate } from '../canvas/commands/set-property-command'
import {
propertyToDelete,
Expand All @@ -66,6 +75,8 @@ import type { DropdownMenuItem } from '../../uuiui/radix-components'
import { DropdownMenu, regularDropdownMenuItem } from '../../uuiui/radix-components'
import { useInspectorLayoutInfo } from './common/property-path-hooks'
import { NumberOrKeywordControl } from '../../uuiui/inputs/number-or-keyword-control'
import { cssNumberEqual } from '../canvas/controls/select-mode/controls-common'
import type { EditorAction } from '../editor/action-types'

const axisDropdownMenuButton = 'axisDropdownMenuButton'

Expand Down Expand Up @@ -587,6 +598,12 @@ function sanitizeAreaName(areaName: string): string {
return areaName.replace(reAlphanumericDashUnderscore, '-')
}

function serializeValue(v: CSSNumber) {
return v.unit == null || v.unit === 'px' ? v.value : cssNumberToString(v)
}

type GridGapControlSplitState = 'unified' | 'split'

const GapRowColumnControl = React.memo(() => {
const dispatch = useDispatch()

Expand All @@ -612,17 +629,30 @@ const GapRowColumnControl = React.memo(() => {
const columnGap = useInspectorLayoutInfo('columnGap')
const rowGap = useInspectorLayoutInfo('rowGap')

const onSubmit = React.useCallback(
const [controlSplitState, setControlSplitState] = React.useState<GridGapControlSplitState>(
cssNumberEqual(columnGap.value, rowGap.value) ? 'unified' : 'split',
)

const cycleControlSplitState = React.useCallback(() => {
setControlSplitState((state) => {
switch (state) {
case 'split':
return 'unified'
case 'unified':
return 'split'
default:
assertNever(state)
}
})
}, [])

const onSubmitSplitValue = React.useCallback(
(target: 'columnGap' | 'rowGap') =>
(value: UnknownOrEmptyInput<CSSNumber>, transient?: boolean) => {
if (grid == null) {
return
}

function serializeValue(v: CSSNumber) {
return v.unit == null || v.unit === 'px' ? v.value : cssNumberToString(v)
}

if (isCSSNumber(value)) {
let updates: PropertyToUpdate[] = []

Expand Down Expand Up @@ -665,41 +695,117 @@ const GapRowColumnControl = React.memo(() => {
[dispatch, grid, gap, rowGap, columnGap],
)

const onSubmitUnifiedValue = React.useCallback(
(value: UnknownOrEmptyInput<CSSNumber>, transient?: boolean) => {
if (grid == null || !isCSSNumber(value)) {
return
}

const transientWrapper = (actions: EditorAction[]) =>
transient ? [transientActions(actions)] : actions

dispatch(
transientWrapper([
applyCommandsAction([
updateBulkProperties('always', grid.elementPath, [
propertyToDelete(PP.create('style', 'columnGap')),
propertyToDelete(PP.create('style', 'rowGap')),
propertyToDelete(PP.create('style', 'gap')),
propertyToSet(PP.create('style', 'gridGap'), serializeValue(value)),
]),
]),
]),
)
},
[dispatch, grid],
)

const tooltipTitle =
controlSplitState === 'unified'
? 'Longhand gap'
: controlSplitState === 'split'
? 'Shorthand gap'
: assertNever(controlSplitState)

const modeIcon = React.useMemo(() => {
switch (controlSplitState) {
case 'split':
return <InspectorSectionIcons.SplitFull />
case 'unified':
return <InspectorSectionIcons.SplitHalf />
default:
assertNever(controlSplitState)
}
}, [controlSplitState])

if (grid == null) {
return null
}

return (
<UIGridRow padded={false} variant='<--1fr--><--1fr-->'>
<NumberInput
value={columnGap.value}
numberType={'Length'}
onSubmitValue={onSubmit('columnGap')}
onTransientSubmitValue={onSubmit('columnGap')}
onForcedSubmitValue={onSubmit('columnGap')}
defaultUnitToHide={'px'}
testId={'grid-column-gap'}
labelInner={{
category: 'inspector-element',
type: 'gapHorizontal',
color: 'subdued',
}}
/>
<NumberInput
value={rowGap.value}
numberType={'Length'}
onSubmitValue={onSubmit('rowGap')}
onTransientSubmitValue={onSubmit('rowGap')}
onForcedSubmitValue={onSubmit('rowGap')}
defaultUnitToHide={'px'}
testId={'grid-row-gap'}
labelInner={{
category: 'inspector-element',
type: 'gapVertical',
color: 'subdued',
}}
/>
</UIGridRow>
<FlexRow style={{ justifyContent: 'space-between' }}>
{when(
controlSplitState === 'unified',
<UIGridRow padded={false} variant='<--1fr--><--1fr-->'>
<NumberInput
value={columnGap.value}
numberType={'Length'}
onSubmitValue={onSubmitUnifiedValue}
onTransientSubmitValue={onSubmitUnifiedValue}
onForcedSubmitValue={onSubmitUnifiedValue}
defaultUnitToHide={'px'}
testId={'grid-column-gap'}
labelInner={{
category: 'inspector-element',
type: 'gapHorizontal',
color: 'subdued',
}}
/>
</UIGridRow>,
)}
{when(
controlSplitState === 'split',
<UIGridRow padded={false} variant='<--1fr--><--1fr-->'>
<NumberInput
value={columnGap.value}
numberType={'Length'}
onSubmitValue={onSubmitSplitValue('columnGap')}
onTransientSubmitValue={onSubmitSplitValue('columnGap')}
onForcedSubmitValue={onSubmitSplitValue('columnGap')}
defaultUnitToHide={'px'}
testId={'grid-column-gap'}
labelInner={{
category: 'inspector-element',
type: 'gapHorizontal',
color: 'subdued',
}}
/>
<NumberInput
value={rowGap.value}
numberType={'Length'}
onSubmitValue={onSubmitSplitValue('rowGap')}
onTransientSubmitValue={onSubmitSplitValue('rowGap')}
onForcedSubmitValue={onSubmitSplitValue('rowGap')}
defaultUnitToHide={'px'}
testId={'grid-row-gap'}
labelInner={{
category: 'inspector-element',
type: 'gapVertical',
color: 'subdued',
}}
/>
</UIGridRow>,
)}
<Tooltip title={tooltipTitle}>
<SquareButton
data-testid={`grid-gap-cycle-mode`}
onClick={cycleControlSplitState}
style={{ width: 16 }}
>
{modeIcon}
</SquareButton>
</Tooltip>
</FlexRow>
)
})
GapRowColumnControl.displayName = 'GapRowColumnControl'
Expand Down

0 comments on commit 5e1b082

Please sign in to comment.