From fb6f2610c5ed69e873dd8cecd8cafce29bbc4f51 Mon Sep 17 00:00:00 2001 From: nitin <142569587+ehconitin@users.noreply.github.com> Date: Wed, 18 Jun 2025 15:26:19 +0530 Subject: [PATCH] fix(admin-panel): prevent layout shift when toggling feature flags (#12686) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #12571 ~~## What’s broken?~~ ~~Toggling a feature flag in the admin panel re-renders the entire table and uses a Framer Motion slide animation for the knob. Each animation frame forces a layout recalculation all the way up, causing the page to “jump.”~~ ~~## What’s the fix?~~ ~~Swap out the Framer Motion toggle for a pure-CSS version:~~ ~~- Container is position: relative~~ ~~- Knob is position: absolute and moves via left~~ ~~- A transition: left 0.3s ease + will-change: left hint runs on the compositor layer~~ ~~This keeps the smooth slide effect but never triggers parent layout reflows.~~ ~~No changes to the public API or behavior.~~ ~~TODO: test all toggles in app and stories if any~~ --------- Co-authored-by: Charles Bochet --- .../SettingsAdminWorkspaceContent.tsx | 14 +-- .../twenty-ui/src/input/components/Toggle.tsx | 12 ++- .../components/__stories__/Toggle.stories.tsx | 98 +++++++++++++++++++ 3 files changed, 113 insertions(+), 11 deletions(-) create mode 100644 packages/twenty-ui/src/input/components/__stories__/Toggle.stories.tsx diff --git a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminWorkspaceContent.tsx b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminWorkspaceContent.tsx index c9ec41c4f..0dd1db164 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminWorkspaceContent.tsx +++ b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminWorkspaceContent.tsx @@ -20,15 +20,8 @@ import { useLingui } from '@lingui/react/macro'; import { isNonEmptyString } from '@sniptt/guards'; import { useState } from 'react'; import { useRecoilState, useRecoilValue } from 'recoil'; -import { REACT_APP_SERVER_BASE_URL } from '~/config'; -import { - FeatureFlagKey, - useImpersonateMutation, - useUpdateWorkspaceFeatureFlagMutation, -} from '~/generated/graphql'; import { getImageAbsoluteURI, isDefined } from 'twenty-shared/utils'; import { AvatarChip } from 'twenty-ui/components'; -import { Button, Toggle } from 'twenty-ui/input'; import { H2Title, IconEyeShare, @@ -36,7 +29,14 @@ import { IconId, IconUser, } from 'twenty-ui/display'; +import { Button, Toggle } from 'twenty-ui/input'; import { Section } from 'twenty-ui/layout'; +import { REACT_APP_SERVER_BASE_URL } from '~/config'; +import { + FeatureFlagKey, + useImpersonateMutation, + useUpdateWorkspaceFeatureFlagMutation, +} from '~/generated/graphql'; type SettingsAdminWorkspaceContentProps = { activeWorkspace: WorkspaceInfo | undefined; diff --git a/packages/twenty-ui/src/input/components/Toggle.tsx b/packages/twenty-ui/src/input/components/Toggle.tsx index c9f95385d..240bffa02 100644 --- a/packages/twenty-ui/src/input/components/Toggle.tsx +++ b/packages/twenty-ui/src/input/components/Toggle.tsx @@ -12,28 +12,31 @@ type ContainerProps = { }; const StyledContainer = styled.label` - flex-shrink: 0; align-items: center; background-color: ${({ theme, isOn, color }) => isOn ? (color ?? theme.color.blue) : theme.background.transparent.medium}; border-radius: 10px; cursor: pointer; display: flex; + flex-shrink: 0; height: ${({ toggleSize }) => (toggleSize === 'small' ? 16 : 20)}px; - transition: background-color 0.3s ease; - width: ${({ toggleSize }) => (toggleSize === 'small' ? 24 : 32)}px; opacity: ${({ 'data-disabled': disabled }) => (disabled ? 0.5 : 1)}; pointer-events: ${({ 'data-disabled': disabled }) => disabled ? 'none' : 'auto'}; + position: relative; + transition: background-color 0.3s ease; + width: ${({ toggleSize }) => (toggleSize === 'small' ? 24 : 32)}px; `; const StyledCircle = styled(motion.span)<{ size: ToggleSize; }>` - display: block; background-color: ${({ theme }) => theme.background.primary}; border-radius: 50%; + display: block; height: ${({ size }) => (size === 'small' ? 12 : 16)}px; + left: 0; + position: absolute; width: ${({ size }) => (size === 'small' ? 12 : 16)}px; `; @@ -80,6 +83,7 @@ export const Toggle = ({ /> { + const [value, setValue] = useState(initialValue); + + return ( + + ); +}; + +const meta: Meta = { + title: 'UI/Input/Toggle/Toggle', + component: InteractiveToggle, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + initialValue: false, + disabled: false, + toggleSize: 'medium', + }, + decorators: [ComponentDecorator], +}; + +export const Catalog: CatalogStory = { + args: {}, + argTypes: { + toggleSize: { control: false }, + initialValue: { control: false }, + disabled: { control: false }, + color: { control: false }, + }, + parameters: { + catalog: { + dimensions: [ + { + name: 'state', + values: ['disabled', 'off', 'on'], + props: (state: string) => { + if (state === 'disabled') { + return { disabled: true, initialValue: false }; + } + if (state === 'on') { + return { initialValue: true }; + } + return { initialValue: false }; + }, + }, + { + name: 'size', + values: ['small', 'medium'] satisfies ToggleSize[], + props: (toggleSize: ToggleSize) => ({ toggleSize }), + }, + { + name: 'color', + values: ['default', 'custom color'], + props: (color: string) => { + if (color === 'default') { + return {}; + } + return { color: MAIN_COLORS.yellow }; + }, + }, + ], + }, + }, + decorators: [CatalogDecorator], +};