Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

keyboard navigation for field selection #6969

Closed
wants to merge 16 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,36 @@ type SettingsCardProps = {
onClick?: () => void;
title: string;
className?: string;
isActive?: boolean;
isFocused?: boolean;
};

const StyledCard = styled(Card)<{
disabled?: boolean;
onClick?: () => void;
isActive?: boolean;
isFocused?: boolean;
}>`
color: ${({ disabled, theme }) =>
disabled ? theme.font.color.extraLight : theme.font.color.tertiary};
cursor: ${({ disabled, onClick }) =>
disabled ? 'not-allowed' : onClick ? 'pointer' : 'default'};
width: 100%;
& :hover {
background-color: ${({ theme }) => theme.background.quaternary};
background-color: ${({ theme }) => theme.background.tertiary};
}
`;

const StyledCardContent = styled(CardContent)<object>`
const StyledCardContent = styled(CardContent)<{
isActive?: boolean;
isFocused?: boolean;
}>`
background: ${({ theme, isActive, isFocused }) =>
isActive
? theme.background.quaternary
: isFocused
? theme.background.tertiary
: theme.background.secondary};
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(2)};
Expand Down Expand Up @@ -78,6 +91,8 @@ export const SettingsCard = ({
onClick,
title,
className,
isActive,
isFocused,
}: SettingsCardProps) => {
const theme = useTheme();

Expand All @@ -88,7 +103,7 @@ export const SettingsCard = ({
className={className}
rounded={true}
>
<StyledCardContent>
<StyledCardContent isActive={isActive} isFocused={isFocused}>
<StyledHeader>
<StyledIconContainer>{Icon}</StyledIconContainer>
<StyledTitle disabled={disabled}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useFormContext } from 'react-hook-form';
import { IconChevronDown } from 'twenty-ui';
import { SettingsDataModelNewFieldFormValues } from '~/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2';

type SettingsDataModelNewFieldBreadcrumbDropDownProps = {
isConfigureStep: boolean;
Expand All @@ -15,11 +17,12 @@ type SettingsDataModelNewFieldBreadcrumbDropDownProps = {

const StyledContainer = styled.div`
align-items: center;
color: ${({ theme }) => theme.font.color.secondary};
cursor: pointer;
color: ${({ theme }) => theme.font.color.tertiary};
cursor: default;
display: flex;
font-size: ${({ theme }) => theme.font.size.md};
`;

const StyledButtonContainer = styled.div`
position: relative;
width: 100%;
Expand All @@ -33,10 +36,19 @@ const StyledDownChevron = styled(IconChevronDown)`
transform: translateY(-50%);
`;

const StyledMenuItem = styled(MenuItem)<{ selected?: boolean }>`
const StyledMenuItemWrapper = styled.div<{ disabled?: boolean }>`
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
width: 100%;
`;

const StyledMenuItem = styled(MenuItem)<{
selected?: boolean;
disabled?: boolean;
}>`
background: ${({ theme, selected }) =>
selected ? theme.background.quaternary : 'transparent'};
cursor: pointer;
opacity: ${({ disabled }) => (disabled ? 0.5 : 1)};
pointer-events: ${({ disabled }) => (disabled ? 'none' : 'auto')};
`;

const StyledSpan = styled.span`
Expand All @@ -56,6 +68,9 @@ export const SettingsDataModelNewFieldBreadcrumbDropDown = ({

const { closeDropdown } = useDropdown(dropdownId);

const { getValues } = useFormContext<SettingsDataModelNewFieldFormValues>();
const selectedType = getValues('type');

const handleClick = (step: boolean) => {
onBreadcrumbClick(step);
closeDropdown();
Expand All @@ -81,16 +96,21 @@ export const SettingsDataModelNewFieldBreadcrumbDropDown = ({
dropdownComponents={
<DropdownMenu>
<DropdownMenuItemsContainer>
<StyledMenuItem
text="1. Type"
onClick={() => handleClick(false)}
selected={!isConfigureStep}
/>
<StyledMenuItem
text="2. Configure"
onClick={() => handleClick(true)}
selected={isConfigureStep}
/>
<StyledMenuItemWrapper>
<StyledMenuItem
text="1. Type"
onClick={() => handleClick(false)}
selected={!isConfigureStep}
/>
</StyledMenuItemWrapper>
<StyledMenuItemWrapper disabled={!selectedType}>
<StyledMenuItem
text="2. Configure"
onClick={() => (selectedType ? handleClick(true) : null)}
selected={isConfigureStep}
disabled={!selectedType}
/>
</StyledMenuItemWrapper>
</DropdownMenuItemsContainer>
</DropdownMenu>
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { FIELD_NAME_MAXIMUM_LENGTH } from '@/settings/data-model/constants/FieldNameMaximumLength';
import { SettingsDataModelFieldDescriptionForm } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldDescriptionForm';
import { SettingsDataModelFieldIconLabelForm } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldIconLabelForm';
import { SettingsDataModelFieldSettingsFormCard } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard';
import { SettingsDataModelHotkeyScope } from '@/settings/data-model/types/SettingsDataModelHotKeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { Section } from '@react-email/components';
import { UseFormReturn } from 'react-hook-form';
import { Key } from 'ts-key-enum';
import { H2Title } from 'twenty-ui';
import { useHotkeyScopeOnMount } from '~/hooks/useHotkeyScopeOnMount';
import { SettingsDataModelNewFieldFormValues } from '~/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2';

type SettingsDataModelFieldConfigurationFormProps = {
formConfig: UseFormReturn<SettingsDataModelNewFieldFormValues>;
activeObjectMetadataItem: ObjectMetadataItem;
setIsConfigureStep: React.Dispatch<React.SetStateAction<boolean>>;
};

export const SettingsDataModelFieldConfigurationForm = ({
formConfig,
activeObjectMetadataItem,
setIsConfigureStep,
}: SettingsDataModelFieldConfigurationFormProps) => {
useHotkeyScopeOnMount(
SettingsDataModelHotkeyScope.SettingsDataModelFieldConfigurationForm,
);

useScopedHotkeys(
Key.Escape,
() => setIsConfigureStep(false),
SettingsDataModelHotkeyScope.SettingsDataModelFieldConfigurationForm,
);

return (
<>
<Section>
<H2Title
title="Icon and Name"
description="The name and icon of this field"
/>
<SettingsDataModelFieldIconLabelForm
maxLength={FIELD_NAME_MAXIMUM_LENGTH}
/>
</Section>
<Section>
<H2Title title="Values" description="The values of this field" />
<SettingsDataModelFieldSettingsFormCard
fieldMetadataItem={{
icon: formConfig.watch('icon'),
label: formConfig.watch('label') || 'Employees',
bosiraphael marked this conversation as resolved.
Show resolved Hide resolved
type: formConfig.watch('type'),
}}
objectMetadataItem={activeObjectMetadataItem}
/>
</Section>
<Section>
<H2Title
title="Description"
description="The description of this field"
/>
<SettingsDataModelFieldDescriptionForm />
</Section>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,20 @@ import {
import { useBooleanSettingsFormInitialValues } from '@/settings/data-model/fields/forms/boolean/hooks/useBooleanSettingsFormInitialValues';
import { useCurrencySettingsFormInitialValues } from '@/settings/data-model/fields/forms/currency/hooks/useCurrencySettingsFormInitialValues';
import { useSelectSettingsFormInitialValues } from '@/settings/data-model/fields/forms/select/hooks/useSelectSettingsFormInitialValues';
import { SettingsDataModelHotkeyScope } from '@/settings/data-model/types/SettingsDataModelHotKeyScope';
import { SettingsSupportedFieldType } from '@/settings/data-model/types/SettingsSupportedFieldType';
import { TextInput } from '@/ui/input/components/TextInput';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { Section } from '@react-email/components';
import { useState } from 'react';
import { useCallback, useState } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import { Key } from 'ts-key-enum';
import { H2Title, IconSearch } from 'twenty-ui';
import { z } from 'zod';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { useHotkeyScopeOnMount } from '~/hooks/useHotkeyScopeOnMount';

export const settingsDataModelFieldTypeFormSchema = z.object({
type: z.enum(
Expand All @@ -29,7 +33,7 @@ export const settingsDataModelFieldTypeFormSchema = z.object({
),
});

export type SettingsDataModelFieldTypeFormValues = z.infer<
type SettingsDataModelFieldTypeFormValues = z.infer<
typeof settingsDataModelFieldTypeFormSchema
>;

Expand Down Expand Up @@ -76,8 +80,10 @@ export const SettingsDataModelFieldTypeSelect = ({
onFieldTypeSelect,
}: SettingsDataModelFieldTypeSelectProps) => {
const theme = useTheme();
const { control } = useFormContext<SettingsDataModelFieldTypeFormValues>();
const { control, getValues, setValue } =
useFormContext<SettingsDataModelFieldTypeFormValues>();
const [searchQuery, setSearchQuery] = useState('');

const fieldTypeConfigs = Object.entries<SettingsFieldTypeConfig>(
SETTINGS_FIELD_TYPE_CONFIGS,
).filter(
Expand All @@ -86,6 +92,70 @@ export const SettingsDataModelFieldTypeSelect = ({
config.label.toLowerCase().includes(searchQuery.toLowerCase()),
);

const getFlattenedConfigs = useCallback(() => {
return SETTINGS_FIELD_TYPE_CATEGORIES.flatMap((category) =>
fieldTypeConfigs.filter(([, config]) => config.category === category),
);
}, [fieldTypeConfigs]);

const initialType = getValues('type');

const getInitialFocusedIndex = useCallback(() => {
const flattenedConfigs = getFlattenedConfigs();
return flattenedConfigs.findIndex(([key]) => key === initialType);
}, [getFlattenedConfigs, initialType]);

const [focusedIndex, setFocusedIndex] = useState(getInitialFocusedIndex());

useHotkeyScopeOnMount(
SettingsDataModelHotkeyScope.SettingsDataModelFieldTypeSelect,
);

useScopedHotkeys(
Key.Tab,
(keyboardEvent) => {
keyboardEvent.preventDefault();
const flattenedConfigs = getFlattenedConfigs();
setFocusedIndex((prevIndex) => (prevIndex + 1) % flattenedConfigs.length);
},
SettingsDataModelHotkeyScope.SettingsDataModelFieldTypeSelect,
[fieldTypeConfigs],
);

useScopedHotkeys(
`${Key.Shift} + ${Key.Tab}`,
(keyboardEvent) => {
keyboardEvent.preventDefault();
const flattenedConfigs = getFlattenedConfigs();
setFocusedIndex(
(prevIndex) =>
(prevIndex - 1 + flattenedConfigs.length) % flattenedConfigs.length,
);
},
SettingsDataModelHotkeyScope.SettingsDataModelFieldTypeSelect,
[fieldTypeConfigs],
);

useScopedHotkeys(
Key.Enter,
(keyboardEvent) => {
keyboardEvent.preventDefault();
if (focusedIndex !== null) {
const flattenedConfigs = getFlattenedConfigs();
const [key] = flattenedConfigs[focusedIndex];
handleSelectFieldType(key as SettingsSupportedFieldType);
}
},
SettingsDataModelHotkeyScope.SettingsDataModelFieldTypeSelect,
[focusedIndex, fieldTypeConfigs],
);

const handleSelectFieldType = (key: SettingsSupportedFieldType) => {
setValue('type', key);
resetDefaultValueField(key);
onFieldTypeSelect();
};

const { resetDefaultValueField: resetBooleanDefaultValueField } =
useBooleanSettingsFormInitialValues({ fieldMetadataItem });

Expand Down Expand Up @@ -116,12 +186,7 @@ export const SettingsDataModelFieldTypeSelect = ({
<Controller
name="type"
control={control}
defaultValue={
bosiraphael marked this conversation as resolved.
Show resolved Hide resolved
fieldMetadataItem && fieldMetadataItem.type in fieldTypeConfigs
? (fieldMetadataItem.type as SettingsSupportedFieldType)
: FieldMetadataType.Text
}
render={({ field: { onChange } }) => (
render={({ field: { value } }) => (
<StyledTypeSelectContainer className={className}>
<Section>
<StyledSearchInput
Expand All @@ -142,27 +207,35 @@ export const SettingsDataModelFieldTypeSelect = ({
<StyledContainer>
{fieldTypeConfigs
.filter(([, config]) => config.category === category)
.map(([key, config]) => (
<StyledCardContainer>
<SettingsCard
key={key}
onClick={() => {
onChange(key as SettingsSupportedFieldType);
resetDefaultValueField(
key as SettingsSupportedFieldType,
);
onFieldTypeSelect();
}}
Icon={
<config.Icon
size={theme.icon.size.xl}
stroke={theme.icon.stroke.sm}
/>
}
title={config.label}
/>
</StyledCardContainer>
))}
.map(([key, config]) => {
const flatIndex = getFlattenedConfigs().findIndex(
([k]) => k === key,
);
const isActive = value === key;
const isFocused = focusedIndex === flatIndex;
return (
<StyledCardContainer key={key}>
<SettingsCard
onClick={() => {
handleSelectFieldType(
key as SettingsSupportedFieldType,
);
setFocusedIndex(flatIndex);
onFieldTypeSelect();
}}
Icon={
<config.Icon
size={theme.icon.size.xl}
stroke={theme.icon.stroke.sm}
/>
}
title={config.label}
isActive={isActive}
isFocused={isFocused}
/>
</StyledCardContainer>
);
})}
</StyledContainer>
</Section>
))}
Expand Down
Loading
Loading