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:
Vardhaman Bhandari
2024-10-21 21:52:10 +05:30
committed by GitHub
parent 1a0b387282
commit 1466d44b57
7 changed files with 68 additions and 35 deletions

View File

@ -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}
/> />

View File

@ -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}

View File

@ -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>;
};

View File

@ -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}
`;

View File

@ -0,0 +1,3 @@
export * from './components/VisibilityHidden';
export * from './components/VisibilityHiddenInput';
export * from './utils/visibility-hidden';

View File

@ -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;
`;

View File

@ -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';