fix: Make the entire advanced mode toggle container clickable (#7761)
In this PR: - Use a real `<input type="checkbox" />` element in the `<Toggle />` component - Create an `accessibility` module in the `twenty-ui` package - Export the `VISIBILITY_HIDDEN` CSS object to hide visually any element - Export a `<VisibilityHidden />` component from the `twenty-ui` package to add visually hidden textual information easily - Export a `<VisibilityHiddenInput />` component to create custom form control components easily - Use a `<label>` element for the "Advanced:" text; it will naturally toggle the advanced settings Fixes #7756 --------- Co-authored-by: Devessier <baptiste@devessier.fr>
This commit is contained in:
committed by
GitHub
parent
1a0b387282
commit
1466d44b57
@ -1,8 +1,6 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { useEffect, useState } from 'react';
|
import { VisibilityHiddenInput } from 'twenty-ui';
|
||||||
|
|
||||||
import { isDefined } from '~/utils/isDefined';
|
|
||||||
|
|
||||||
export type ToggleSize = 'small' | 'medium';
|
export type ToggleSize = 'small' | 'medium';
|
||||||
|
|
||||||
@ -10,10 +8,10 @@ type ContainerProps = {
|
|||||||
isOn: boolean;
|
isOn: boolean;
|
||||||
color?: string;
|
color?: string;
|
||||||
toggleSize: ToggleSize;
|
toggleSize: ToggleSize;
|
||||||
disabled?: boolean;
|
'data-disabled'?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledContainer = styled.div<ContainerProps>`
|
const StyledContainer = styled.label<ContainerProps>`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: ${({ theme, isOn, color }) =>
|
background-color: ${({ theme, isOn, color }) =>
|
||||||
isOn ? (color ?? theme.color.blue) : theme.background.transparent.medium};
|
isOn ? (color ?? theme.color.blue) : theme.background.transparent.medium};
|
||||||
@ -23,13 +21,15 @@ const StyledContainer = styled.div<ContainerProps>`
|
|||||||
height: ${({ toggleSize }) => (toggleSize === 'small' ? 16 : 20)}px;
|
height: ${({ toggleSize }) => (toggleSize === 'small' ? 16 : 20)}px;
|
||||||
transition: background-color 0.3s ease;
|
transition: background-color 0.3s ease;
|
||||||
width: ${({ toggleSize }) => (toggleSize === 'small' ? 24 : 32)}px;
|
width: ${({ toggleSize }) => (toggleSize === 'small' ? 24 : 32)}px;
|
||||||
opacity: ${({ disabled }) => (disabled ? 0.5 : 1)};
|
opacity: ${({ 'data-disabled': disabled }) => (disabled ? 0.5 : 1)};
|
||||||
pointer-events: ${({ disabled }) => (disabled ? 'none' : 'auto')};
|
pointer-events: ${({ 'data-disabled': disabled }) =>
|
||||||
|
disabled ? 'none' : 'auto'};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledCircle = styled(motion.div)<{
|
const StyledCircle = styled(motion.span)<{
|
||||||
size: ToggleSize;
|
size: ToggleSize;
|
||||||
}>`
|
}>`
|
||||||
|
display: block;
|
||||||
background-color: ${({ theme }) => theme.background.primary};
|
background-color: ${({ theme }) => theme.background.primary};
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
height: ${({ size }) => (size === 'small' ? 12 : 16)}px;
|
height: ${({ size }) => (size === 'small' ? 12 : 16)}px;
|
||||||
@ -37,6 +37,7 @@ const StyledCircle = styled(motion.div)<{
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export type ToggleProps = {
|
export type ToggleProps = {
|
||||||
|
id?: string;
|
||||||
value?: boolean;
|
value?: boolean;
|
||||||
onChange?: (value: boolean) => void;
|
onChange?: (value: boolean) => void;
|
||||||
color?: string;
|
color?: string;
|
||||||
@ -46,49 +47,39 @@ export type ToggleProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const Toggle = ({
|
export const Toggle = ({
|
||||||
value,
|
id,
|
||||||
|
value = false,
|
||||||
onChange,
|
onChange,
|
||||||
color,
|
color,
|
||||||
toggleSize = 'medium',
|
toggleSize = 'medium',
|
||||||
className,
|
className,
|
||||||
disabled,
|
disabled,
|
||||||
}: ToggleProps) => {
|
}: ToggleProps) => {
|
||||||
const [isOn, setIsOn] = useState(value ?? false);
|
|
||||||
|
|
||||||
const circleVariants = {
|
const circleVariants = {
|
||||||
on: { x: toggleSize === 'small' ? 10 : 14 },
|
on: { x: toggleSize === 'small' ? 10 : 14 },
|
||||||
off: { x: 2 },
|
off: { x: 2 },
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = () => {
|
|
||||||
setIsOn(!isOn);
|
|
||||||
|
|
||||||
if (isDefined(onChange)) {
|
|
||||||
onChange(!isOn);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setIsOn((isOn) => {
|
|
||||||
if (value !== isOn) {
|
|
||||||
return value ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return isOn;
|
|
||||||
});
|
|
||||||
}, [value, setIsOn]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer
|
<StyledContainer
|
||||||
onClick={handleChange}
|
isOn={value}
|
||||||
isOn={isOn}
|
|
||||||
color={color}
|
color={color}
|
||||||
toggleSize={toggleSize}
|
toggleSize={toggleSize}
|
||||||
className={className}
|
className={className}
|
||||||
disabled={disabled}
|
data-disabled={disabled}
|
||||||
>
|
>
|
||||||
|
<VisibilityHiddenInput
|
||||||
|
id={id}
|
||||||
|
type="checkbox"
|
||||||
|
checked={value}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={(event) => {
|
||||||
|
onChange?.(event.target.checked);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<StyledCircle
|
<StyledCircle
|
||||||
animate={isOn ? 'on' : 'off'}
|
animate={value ? 'on' : 'off'}
|
||||||
variants={circleVariants}
|
variants={circleVariants}
|
||||||
size={toggleSize}
|
size={toggleSize}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { Toggle } from '@/ui/input/components/Toggle';
|
import { Toggle } from '@/ui/input/components/Toggle';
|
||||||
import { isAdvancedModeEnabledState } from '@/ui/navigation/navigation-drawer/states/isAdvancedModeEnabledState';
|
import { isAdvancedModeEnabledState } from '@/ui/navigation/navigation-drawer/states/isAdvancedModeEnabledState';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { useId } from 'react';
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
import { IconTool, MAIN_COLORS } from 'twenty-ui';
|
import { IconTool, MAIN_COLORS } from 'twenty-ui';
|
||||||
|
|
||||||
@ -12,7 +13,7 @@ const StyledContainer = styled.div`
|
|||||||
position: relative;
|
position: relative;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledText = styled.span`
|
const StyledLabel = styled.label`
|
||||||
color: ${({ theme }) => theme.font.color.secondary};
|
color: ${({ theme }) => theme.font.color.secondary};
|
||||||
font-size: ${({ theme }) => theme.font.size.sm};
|
font-size: ${({ theme }) => theme.font.size.sm};
|
||||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||||
@ -41,6 +42,7 @@ export const AdvancedSettingsToggle = () => {
|
|||||||
const [isAdvancedModeEnabled, setIsAdvancedModeEnabled] = useRecoilState(
|
const [isAdvancedModeEnabled, setIsAdvancedModeEnabled] = useRecoilState(
|
||||||
isAdvancedModeEnabledState,
|
isAdvancedModeEnabledState,
|
||||||
);
|
);
|
||||||
|
const inputId = useId();
|
||||||
|
|
||||||
const onChange = (newValue: boolean) => {
|
const onChange = (newValue: boolean) => {
|
||||||
setIsAdvancedModeEnabled(newValue);
|
setIsAdvancedModeEnabled(newValue);
|
||||||
@ -52,8 +54,10 @@ export const AdvancedSettingsToggle = () => {
|
|||||||
<StyledIconTool size={12} color={MAIN_COLORS.yellow} />
|
<StyledIconTool size={12} color={MAIN_COLORS.yellow} />
|
||||||
</StyledIconContainer>
|
</StyledIconContainer>
|
||||||
<StyledToggleContainer>
|
<StyledToggleContainer>
|
||||||
<StyledText>Advanced:</StyledText>
|
<StyledLabel htmlFor={inputId}>Advanced:</StyledLabel>
|
||||||
|
|
||||||
<Toggle
|
<Toggle
|
||||||
|
id={inputId}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
color={MAIN_COLORS.yellow}
|
color={MAIN_COLORS.yellow}
|
||||||
value={isAdvancedModeEnabled}
|
value={isAdvancedModeEnabled}
|
||||||
|
|||||||
@ -0,0 +1,14 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { VISIBILITY_HIDDEN } from '@ui/accessibility/utils/visibility-hidden';
|
||||||
|
|
||||||
|
const StyledSpan = styled.span`
|
||||||
|
${VISIBILITY_HIDDEN}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const VisibilityHidden = ({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) => {
|
||||||
|
return <StyledSpan>{children}</StyledSpan>;
|
||||||
|
};
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { VISIBILITY_HIDDEN } from '@ui/accessibility/utils/visibility-hidden';
|
||||||
|
|
||||||
|
// eslint-disable-next-line @nx/workspace-styled-components-prefixed-with-styled
|
||||||
|
export const VisibilityHiddenInput = styled.input`
|
||||||
|
${VISIBILITY_HIDDEN}
|
||||||
|
`;
|
||||||
3
packages/twenty-ui/src/accessibility/index.ts
Normal file
3
packages/twenty-ui/src/accessibility/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './components/VisibilityHidden';
|
||||||
|
export * from './components/VisibilityHiddenInput';
|
||||||
|
export * from './utils/visibility-hidden';
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
import { css } from '@emotion/react';
|
||||||
|
|
||||||
|
export const VISIBILITY_HIDDEN = css`
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
border-width: 0;
|
||||||
|
`;
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
export * from './accessibility';
|
||||||
export * from './components';
|
export * from './components';
|
||||||
export * from './display';
|
export * from './display';
|
||||||
export * from './layout';
|
export * from './layout';
|
||||||
|
|||||||
Reference in New Issue
Block a user