fix(admin-panel): prevent layout shift when toggling feature flags (#12686)
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 <Toggle> API or behavior.~~ ~~TODO: test all toggles in app and stories if any~~ --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -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;
|
||||
|
||||
@ -12,28 +12,31 @@ type ContainerProps = {
|
||||
};
|
||||
|
||||
const StyledContainer = styled.label<ContainerProps>`
|
||||
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 = ({
|
||||
/>
|
||||
|
||||
<StyledCircle
|
||||
initial="off"
|
||||
animate={value ? 'on' : 'off'}
|
||||
variants={circleVariants}
|
||||
size={toggleSize}
|
||||
|
||||
@ -0,0 +1,98 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import {
|
||||
CatalogDecorator,
|
||||
CatalogStory,
|
||||
ComponentDecorator,
|
||||
} from '@ui/testing';
|
||||
import { MAIN_COLORS } from '@ui/theme';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Toggle, ToggleSize } from '../Toggle';
|
||||
|
||||
type InteractiveToggleProps = {
|
||||
initialValue?: boolean;
|
||||
toggleSize?: ToggleSize;
|
||||
color?: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
const InteractiveToggle = ({
|
||||
initialValue = false,
|
||||
toggleSize = 'medium',
|
||||
color,
|
||||
disabled,
|
||||
}: InteractiveToggleProps) => {
|
||||
const [value, setValue] = useState(initialValue);
|
||||
|
||||
return (
|
||||
<Toggle
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
toggleSize={toggleSize}
|
||||
color={color}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const meta: Meta<typeof InteractiveToggle> = {
|
||||
title: 'UI/Input/Toggle/Toggle',
|
||||
component: InteractiveToggle,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof InteractiveToggle>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
initialValue: false,
|
||||
disabled: false,
|
||||
toggleSize: 'medium',
|
||||
},
|
||||
decorators: [ComponentDecorator],
|
||||
};
|
||||
|
||||
export const Catalog: CatalogStory<Story, typeof InteractiveToggle> = {
|
||||
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],
|
||||
};
|
||||
Reference in New Issue
Block a user