From e1f1b8aef4c9cd45620f73aa9c85d8fe15f08d71 Mon Sep 17 00:00:00 2001
From: Federico Ruggi <1081051+ruggi@users.noreply.github.com>
Date: Fri, 4 Oct 2024 09:46:38 +0200
Subject: [PATCH] Disable alignment buttons/groups based on element (#6461)
**Problem:**
Following up to https://github.com/concrete-utopia/utopia/pull/6459, the
alignment buttons should be disabled following a comprehensive set of
rules.
**Fix:**
This PR updates the logic to disable alignment buttons and groups
following the following logic.
- Grid children all have alignment buttons enabled
- Flex children have alignment buttons enabled for the orientation
opposite to their parent's flex direction
- Absolute elements have alignment buttons enabled, if they are not
storyboard children
- Flow elements have all alignment buttons disabled
Fixes #6460
---
.../alignment-buttons.spec.browser2.tsx | 290 ++++++++++++++++++
.../inspector/alignment-buttons.tsx | 11 +-
.../inspector/controls/option-control.tsx | 3 +-
.../inspector/use-disable-alignment.tsx | 74 +++++
4 files changed, 373 insertions(+), 5 deletions(-)
create mode 100644 editor/src/components/inspector/alignment-buttons.spec.browser2.tsx
create mode 100644 editor/src/components/inspector/use-disable-alignment.tsx
diff --git a/editor/src/components/inspector/alignment-buttons.spec.browser2.tsx b/editor/src/components/inspector/alignment-buttons.spec.browser2.tsx
new file mode 100644
index 000000000000..1158a6aad77b
--- /dev/null
+++ b/editor/src/components/inspector/alignment-buttons.spec.browser2.tsx
@@ -0,0 +1,290 @@
+import { MetadataUtils } from '../../core/model/element-metadata-utils'
+import * as EP from '../../core/shared/element-path'
+import { renderTestEditorWithCode } from '../canvas/ui-jsx.test-utils'
+import { isAlignmentGroupDisabled } from './use-disable-alignment'
+
+describe('alignment buttons', () => {
+ describe('isAlignmentGroupDisabled', () => {
+ it('enables buttons for grid cells', async () => {
+ const renderResult = await renderTestEditorWithCode(projectCode, 'await-first-dom-report')
+ const metadata = renderResult.getEditorState().editor.jsxMetadata
+
+ expect(
+ isAlignmentGroupDisabled(
+ MetadataUtils.findElementByElementPath(metadata, EP.fromString('sb/grid/grid-child')),
+ MetadataUtils.findElementByElementPath(metadata, EP.fromString('sb/grid')),
+ 'horizontal',
+ [],
+ ),
+ ).toBe(false)
+ })
+ it('enables buttons for flex only in the opposite direction', async () => {
+ const renderResult = await renderTestEditorWithCode(projectCode, 'await-first-dom-report')
+ const metadata = renderResult.getEditorState().editor.jsxMetadata
+
+ // flex-direction: row
+ {
+ expect(
+ isAlignmentGroupDisabled(
+ MetadataUtils.findElementByElementPath(
+ metadata,
+ EP.fromString('sb/flex-horiz/flex-horiz-child'),
+ ),
+ MetadataUtils.findElementByElementPath(metadata, EP.fromString('sb/flex-horiz')),
+ 'vertical',
+ [],
+ ),
+ ).toBe(false)
+ expect(
+ isAlignmentGroupDisabled(
+ MetadataUtils.findElementByElementPath(
+ metadata,
+ EP.fromString('sb/flex-horiz/flex-horiz-child'),
+ ),
+ MetadataUtils.findElementByElementPath(metadata, EP.fromString('sb/flex-horiz')),
+ 'horizontal',
+ [],
+ ),
+ ).toBe(true)
+ }
+
+ // flex-direction: column
+ {
+ expect(
+ isAlignmentGroupDisabled(
+ MetadataUtils.findElementByElementPath(
+ metadata,
+ EP.fromString('sb/flex-vert/flex-vert-child'),
+ ),
+ MetadataUtils.findElementByElementPath(metadata, EP.fromString('sb/flex-vert')),
+ 'horizontal',
+ [],
+ ),
+ ).toBe(false)
+ expect(
+ isAlignmentGroupDisabled(
+ MetadataUtils.findElementByElementPath(
+ metadata,
+ EP.fromString('sb/flex-vert/flex-vert-child'),
+ ),
+ MetadataUtils.findElementByElementPath(metadata, EP.fromString('sb/flex-vert')),
+ 'vertical',
+ [],
+ ),
+ ).toBe(true)
+ }
+ })
+ it('enables buttons for absolute only if not storyboard children', async () => {
+ const renderResult = await renderTestEditorWithCode(projectCode, 'await-first-dom-report')
+ const metadata = renderResult.getEditorState().editor.jsxMetadata
+
+ expect(
+ isAlignmentGroupDisabled(
+ MetadataUtils.findElementByElementPath(
+ metadata,
+ EP.fromString('sb/flow/flow-child-absolute'),
+ ),
+ MetadataUtils.findElementByElementPath(metadata, EP.fromString('sb/flow')),
+ 'horizontal',
+ [],
+ ),
+ ).toBe(false)
+
+ expect(
+ isAlignmentGroupDisabled(
+ MetadataUtils.findElementByElementPath(metadata, EP.fromString('sb/flow')),
+ MetadataUtils.findElementByElementPath(metadata, EP.fromString('sb')),
+ 'horizontal',
+ [],
+ ),
+ ).toBe(true)
+
+ expect(
+ isAlignmentGroupDisabled(
+ MetadataUtils.findElementByElementPath(metadata, EP.fromString('sb/flow')),
+ MetadataUtils.findElementByElementPath(metadata, EP.fromString('sb')),
+ 'horizontal',
+ [EP.fromString('sb/flow'), EP.fromString('sb/grid')],
+ ),
+ ).toBe(false)
+ })
+ it('disables buttons for flow', async () => {
+ const renderResult = await renderTestEditorWithCode(projectCode, 'await-first-dom-report')
+ const metadata = renderResult.getEditorState().editor.jsxMetadata
+
+ expect(
+ isAlignmentGroupDisabled(
+ MetadataUtils.findElementByElementPath(
+ metadata,
+ EP.fromString('sb/flow/flow-child-relative'),
+ ),
+ MetadataUtils.findElementByElementPath(metadata, EP.fromString('sb/flow')),
+ 'horizontal',
+ [],
+ ),
+ ).toBe(true)
+ })
+ })
+})
+
+const projectCode = `import * as React from 'react'
+import { Storyboard } from 'utopia-api'
+
+export var storyboard = (
+
+
+
+
+
+
+)
+`
diff --git a/editor/src/components/inspector/alignment-buttons.tsx b/editor/src/components/inspector/alignment-buttons.tsx
index 3fa2e1841708..130820f8c003 100644
--- a/editor/src/components/inspector/alignment-buttons.tsx
+++ b/editor/src/components/inspector/alignment-buttons.tsx
@@ -26,6 +26,7 @@ import { Substores, useEditorState, useRefEditorState } from '../editor/store/st
import { getControlStyles } from './common/control-styles'
import { OptionChainControl } from './controls/option-chain-control'
import { UIGridRow } from './widgets/ui-grid-row'
+import { useDisableAlignment } from './use-disable-alignment'
type ActiveAlignments = { [key in Alignment]: boolean }
@@ -34,7 +35,6 @@ export const AlignmentButtons = React.memo(() => {
const selectedViews = useRefEditorState((store) => store.editor.selectedViews)
- const disableAlign = selectedViews.current.length === 0
const disableDistribute = selectedViews.current.length < 3
const activeAlignments = useActiveAlignments()
@@ -171,6 +171,9 @@ export const AlignmentButtons = React.memo(() => {
: null
}, [activeAlignments])
+ const disableJustify = useDisableAlignment(selectedViews.current, 'horizontal')
+ const disableAlign = useDisableAlignment(selectedViews.current, 'vertical')
+
return (
{
value: 'left',
icon: { category: 'inspector', type: 'justify-start' },
forceCallOnSubmitValue: true,
- disabled: disableAlign,
+ disabled: disableJustify,
},
{
value: 'hcenter',
icon: { category: 'inspector', type: 'justify-center' },
forceCallOnSubmitValue: true,
- disabled: disableAlign,
+ disabled: disableJustify,
},
{
value: 'right',
icon: { category: 'inspector', type: 'justify-end' },
forceCallOnSubmitValue: true,
- disabled: disableAlign,
+ disabled: disableJustify,
},
]}
/>
diff --git a/editor/src/components/inspector/controls/option-control.tsx b/editor/src/components/inspector/controls/option-control.tsx
index 328e71da2e21..f01c89636fce 100644
--- a/editor/src/components/inspector/controls/option-control.tsx
+++ b/editor/src/components/inspector/controls/option-control.tsx
@@ -118,7 +118,8 @@ export const OptionControl: React.FunctionComponent<
},
},
'&:hover': {
- opacity: props.controlStatus === 'disabled' ? undefined : 1,
+ opacity:
+ controlOptions.disabled || props.controlStatus === 'disabled' ? undefined : 1,
},
'.control-option-icon-component': {
opacity: 0.7,
diff --git a/editor/src/components/inspector/use-disable-alignment.tsx b/editor/src/components/inspector/use-disable-alignment.tsx
new file mode 100644
index 000000000000..839ced711303
--- /dev/null
+++ b/editor/src/components/inspector/use-disable-alignment.tsx
@@ -0,0 +1,74 @@
+import React from 'react'
+import type { ElementPath } from 'utopia-shared/src/types'
+import { MetadataUtils } from '../../core/model/element-metadata-utils'
+import type { ElementInstanceMetadata } from '../../core/shared/element-template'
+import { useEditorState, Substores } from '../editor/store/store-hook'
+import * as EP from '../../core/shared/element-path'
+
+export function useDisableAlignment(
+ selectedViews: ElementPath[],
+ orientation: 'horizontal' | 'vertical',
+) {
+ const selectedElements = useEditorState(
+ Substores.metadata,
+ (store) =>
+ selectedViews.map((path) => {
+ return {
+ element: MetadataUtils.findElementByElementPath(store.editor.jsxMetadata, path),
+ parent: MetadataUtils.findElementByElementPath(
+ store.editor.jsxMetadata,
+ EP.parentPath(path),
+ ),
+ }
+ }),
+ 'useDisableAlignment selectedElements',
+ )
+
+ return React.useMemo(() => {
+ if (selectedViews.length === 0) {
+ return true
+ }
+
+ return selectedElements.some(({ element, parent }) => {
+ return isAlignmentGroupDisabled(element, parent, orientation, selectedViews)
+ })
+ }, [selectedViews, selectedElements, orientation])
+}
+
+export function isAlignmentGroupDisabled(
+ element: ElementInstanceMetadata | null,
+ parent: ElementInstanceMetadata | null,
+ orientation: 'horizontal' | 'vertical',
+ selection: ElementPath[],
+): boolean {
+ if (element == null || parent == null) {
+ return true
+ }
+
+ // grid cells have all alignments available
+ const isGridCell = MetadataUtils.isGridLayoutedContainer(parent)
+ if (isGridCell) {
+ return false
+ }
+
+ // flex children have alignment enabled on the opposite orientation to their parent's flex direction
+ const isFlexChild = MetadataUtils.isFlexLayoutedContainer(parent)
+ if (isFlexChild) {
+ const flexDirection = MetadataUtils.getFlexDirection(parent)
+ return flexDirection === 'column' || flexDirection === 'column-reverse'
+ ? orientation === 'vertical'
+ : orientation === 'horizontal'
+ }
+
+ // absolute elements have all alignments available, unless they are storyboard children or all
+ // selected elements are storyboard children
+ if (
+ MetadataUtils.isPositionAbsolute(element) &&
+ (!EP.isStoryboardChild(element.elementPath) ||
+ (selection.length > 1 && selection.every((other) => EP.isStoryboardChild(other))))
+ ) {
+ return false
+ }
+
+ return true
+}