diff --git a/apps/client/src/components/Elements/ColorsGrid/ColorsGrid.tsx b/apps/client/src/components/Elements/ColorsGrid/ColorsGrid.tsx index 4222f24..7662bba 100644 --- a/apps/client/src/components/Elements/ColorsGrid/ColorsGrid.tsx +++ b/apps/client/src/components/Elements/ColorsGrid/ColorsGrid.tsx @@ -1,7 +1,7 @@ import * as RadioGroupPrimitive from '@radix-ui/react-radio-group'; import * as PanelStyled from '@/components/Panels/StylePanel/StylePanel.styled'; import * as Styled from './ColorsGrid.styled'; -import { getStyleTitle } from '@/utils/string'; +import { createTitle } from '@/utils/string'; import type { NodeColor } from 'shared'; import ColorCircle from '../ColorCircle/ColorCircle'; @@ -45,7 +45,7 @@ const ColorsGrid = ({ withLabel = false, value, onSelect }: Props) => { checked={color.value === value} value={color.value} aria-label={`${color.name} color`} - title={getStyleTitle('Color', color.name)} + title={createTitle('Color', color.name)} color={ color.value === value ? 'secondary-dark' : 'secondary-light' } diff --git a/apps/client/src/components/Panels/ControlPanel/ControlPanel.tsx b/apps/client/src/components/Panels/ControlPanel/ControlPanel.tsx index 66953f9..0bcfbca 100644 --- a/apps/client/src/components/Panels/ControlPanel/ControlPanel.tsx +++ b/apps/client/src/components/Panels/ControlPanel/ControlPanel.tsx @@ -1,6 +1,6 @@ import { memo } from 'react'; import { CONTROL } from '@/constants/panels/control'; -import { getKeyTitle } from '@/utils/string'; +import { createKeyTitle } from '@/utils/string'; import * as PanelStyled from '../Panels.styled'; import Icon from '@/components/Elements/Icon/Icon'; @@ -35,8 +35,8 @@ const ControlPanel = ({ enabledControls, onControl }: Props) => { return ( key.replace(/key/i, '')), + title={createKeyTitle(control.name, [ + ...control.modifierKeys, control.key, ])} disabled={getDisabledByControlValue(control.value)} diff --git a/apps/client/src/components/Panels/StylePanel/AnimatedSection.tsx b/apps/client/src/components/Panels/StylePanel/AnimatedSection.tsx index b674756..86d22f6 100644 --- a/apps/client/src/components/Panels/StylePanel/AnimatedSection.tsx +++ b/apps/client/src/components/Panels/StylePanel/AnimatedSection.tsx @@ -2,7 +2,7 @@ import { memo } from 'react'; import type { NodeStyle } from 'shared'; import { ANIMATED } from '@/constants/panels/style'; import * as Styled from './StylePanel.styled'; -import { getStyleTitle } from '@/utils/string'; +import { createTitle } from '@/utils/string'; type Props = { value: NodeStyle['animated']; @@ -18,7 +18,7 @@ const AnimatedSection = ({ value, isDisabled, onAnimatedChange }: Props) => { {ANIMATED.name} ; @@ -28,7 +28,7 @@ const FillSection = ({ value, onFillChange }: Props) => { { { return ( { onToolSelect(tool.value)} > diff --git a/apps/client/src/components/Panels/ZoomPanel/ZoomPanel.test.tsx b/apps/client/src/components/Panels/ZoomPanel/ZoomPanel.test.tsx index 3c94afd..692e6ea 100644 --- a/apps/client/src/components/Panels/ZoomPanel/ZoomPanel.test.tsx +++ b/apps/client/src/components/Panels/ZoomPanel/ZoomPanel.test.tsx @@ -1,19 +1,34 @@ import { fireEvent, render, screen } from '@testing-library/react'; -import { ZOOM } from '@/constants/panels/zoom'; import ZoomPanel from './ZoomPanel'; describe('ZoomPanel', () => { - it('should call handleZoomChange when clicked', () => { + it('should call handleZoomChange when zoom in clicked', () => { const handleZoomChange = vi.fn(); render(); - const zoomActions = Object.values(ZOOM); + fireEvent.click(screen.getByTestId('zoom-in-button')); - zoomActions.forEach((action) => { - fireEvent.click(screen.getByTitle(new RegExp(action.name))); - }); + expect(handleZoomChange).toHaveBeenCalledTimes(1); + }); + + it('should call handleZoomChange when zoom out clicked', () => { + const handleZoomChange = vi.fn(); + + render(); + + fireEvent.click(screen.getByTestId('zoom-out-button')); + + expect(handleZoomChange).toHaveBeenCalledTimes(1); + }); + + it('should call handleZoomChange when zoom reset clicked', () => { + const handleZoomChange = vi.fn(); + + render(); + + fireEvent.click(screen.getByTestId('zoom-reset-button')); - expect(handleZoomChange).toHaveBeenCalledTimes(zoomActions.length); + expect(handleZoomChange).toHaveBeenCalledTimes(1); }); }); diff --git a/apps/client/src/components/Panels/ZoomPanel/ZoomPanel.tsx b/apps/client/src/components/Panels/ZoomPanel/ZoomPanel.tsx index e722408..573acc6 100644 --- a/apps/client/src/components/Panels/ZoomPanel/ZoomPanel.tsx +++ b/apps/client/src/components/Panels/ZoomPanel/ZoomPanel.tsx @@ -8,6 +8,7 @@ import { ZOOM, type ZoomAction } from '@/constants/panels/zoom'; import * as PanelStyled from '../Panels.styled'; import * as Styled from './ZoomPanel.styled'; import Icon from '@/components/Elements/Icon/Icon'; +import { createKeyTitle } from '@/utils/string'; type Props = { value: number; @@ -40,6 +41,7 @@ const ZoomPanel = ({ value, onZoomChange }: Props) => { handleZoomAction(ZOOM.reset.value)} > @@ -47,14 +49,22 @@ const ZoomPanel = ({ value, onZoomChange }: Props) => { handleZoomAction(ZOOM.in.value)} > handleZoomAction(ZOOM.out.value)} > diff --git a/apps/client/src/constants/index.ts b/apps/client/src/constants/index.ts index 81a9f3e..9e379f5 100644 --- a/apps/client/src/constants/index.ts +++ b/apps/client/src/constants/index.ts @@ -5,3 +5,8 @@ export type Entity = { value: Value; icon: IconName; }; + +export type ShortcutKeyCombo = Entity & { + key: string; + modifierKeys: readonly string[]; +}; diff --git a/apps/client/src/constants/panels/control.ts b/apps/client/src/constants/panels/control.ts index 98e7df4..be7d5ad 100644 --- a/apps/client/src/constants/panels/control.ts +++ b/apps/client/src/constants/panels/control.ts @@ -1,12 +1,8 @@ import type { HistoryActionKey } from '@/stores/reducers/history'; import { type canvasActions } from '@/stores/slices/canvas'; -import { KEYS, type Key } from '../keys'; -import type { Entity } from '@/constants/index'; +import type { ShortcutKeyCombo } from '@/constants/index'; -type Control = Entity & { - key: string; - modifierKeys: readonly Key[]; -}; +type Control = ShortcutKeyCombo; export const CONTROL: readonly Control[] = [ { @@ -14,14 +10,14 @@ export const CONTROL: readonly Control[] = [ value: 'undo', icon: 'arrowBackUp', key: 'Z', - modifierKeys: [KEYS.CTRL], + modifierKeys: ['Ctrl'], }, { name: 'Redo', value: 'redo', icon: 'arrowForwardUp', key: 'Z', - modifierKeys: [KEYS.CTRL, KEYS.SHIFT], + modifierKeys: ['Ctrl', 'Shift'], }, { name: 'Delete', diff --git a/apps/client/src/constants/panels/zoom.ts b/apps/client/src/constants/panels/zoom.ts index 3e95261..3561db1 100644 --- a/apps/client/src/constants/panels/zoom.ts +++ b/apps/client/src/constants/panels/zoom.ts @@ -1,10 +1,10 @@ -import type { Entity } from '../index'; +import type { Entity, ShortcutKeyCombo } from '../index'; export type ZoomAction = (typeof ZOOM)[keyof typeof ZOOM]['value']; type Zoom = { - in: Entity; - out: Entity; + in: ShortcutKeyCombo; + out: ShortcutKeyCombo; reset: Omit; }; @@ -13,11 +13,15 @@ export const ZOOM: Zoom = { name: 'Zoom In', value: 'increase', icon: 'plus', + key: 'Ctrl', + modifierKeys: ['Mouse Wheel++'], }, out: { name: 'Zoom Out', value: 'decrease', icon: 'minus', + key: 'Ctrl', + modifierKeys: ['Mouse Wheel--'], }, reset: { name: 'Reset Zoom', diff --git a/apps/client/src/utils/__tests__/string.test.ts b/apps/client/src/utils/__tests__/string.test.ts index 595434a..d1d31db 100644 --- a/apps/client/src/utils/__tests__/string.test.ts +++ b/apps/client/src/utils/__tests__/string.test.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { capitalizeFirstLetter, - getKeyTitle, - getStyleTitle, + createKeyTitle, + createTitle, hexToRGBa, } from '../string'; @@ -25,27 +25,27 @@ describe('capitalizeFirstLetter', () => { }); }); -describe('getKeyTitle', () => { +describe('createKeyTitle', () => { it('should return a formatted string for a single key', () => { - expect(getKeyTitle('foo', ['bar'])).toBe('Foo — Bar'); + expect(createKeyTitle('foo', ['bar'])).toBe('Foo — Bar'); }); it('should return a formatted string for multiple keys', () => { - expect(getKeyTitle('foo', ['bar', 'foo'])).toBe('Foo — Bar + Foo'); + expect(createKeyTitle('foo', ['bar', 'foo'])).toBe('Foo — Bar + Foo'); }); it('should return empty string if name is an empty string', () => { - expect(getKeyTitle('', ['foo', 'bar'])).toBe(' — Foo + Bar'); + expect(createKeyTitle('', ['foo', 'bar'])).toBe(' — Foo + Bar'); }); it('should return keys as strings when provided with non-string values', () => { - expect(getKeyTitle('foo', [12, null])).toBe('Foo — 12 + Null'); + expect(createKeyTitle('foo', [12, null])).toBe('Foo — 12 + Null'); }); }); -describe('getStyleTitle', () => { +describe('createTitle', () => { it('returns a formatted title', () => { - expect(getStyleTitle('foo', 'bar')).toBe('foo — bar'); + expect(createTitle('foo', 'bar')).toBe('foo — bar'); }); }); diff --git a/apps/client/src/utils/string.ts b/apps/client/src/utils/string.ts index 3bd5f16..3d4e4bf 100644 --- a/apps/client/src/utils/string.ts +++ b/apps/client/src/utils/string.ts @@ -13,24 +13,27 @@ export function capitalizeFirstLetter(string: string) { return string[0].toUpperCase() + string.slice(1).toLowerCase(); } -export const getKeyTitle = (name: string, keys: string[]) => { +export const createTitle = (name: string, value: string) => + `${name} — ${value}`; + +export const createKeyTitle = (name: string, keys: string[]) => { if (typeof name !== 'string') { throw new Error('The provided name must be a string'); } - const capitalizedName = name.length ? capitalizeFirstLetter(name) : ''; + const capitalizedName = capitalizeFirstLetter(name); if (keys.length === 1) { - return `${capitalizedName} — ${capitalizeFirstLetter(`${keys[0]}`)}`; + const capitalizedKey = capitalizeFirstLetter(keys[0]); + + return createTitle(capitalizedName, capitalizedKey); } - return `${capitalizedName} — ${keys + const joinedKeys = keys .map((key) => capitalizeFirstLetter(`${key}`)) - .join(' + ')}`; -}; + .join(' + '); -export const getStyleTitle = (name: string, value: string) => { - return `${name} — ${value}`; + return createTitle(capitalizedName, joinedKeys); }; export const hexToRGBa = (hex: string, alpha = 1) => {