v2 event editor (#265)

* style: add cursor indicator in all blocks

* style: prevent reflows on width

* style: harmonise white range

* ux: focus elements on cursor nav

* ux: prevent focus on indicators

* refactor: convert to typescript

* fix: missing dependency

* chore: add linter rules for dependency arrays

* style: style editable component

* style: tweak input styles

* style: tweak switch styles

* style: style tweaks
This commit is contained in:
Carlos Valente
2022-12-08 22:22:08 +01:00
committed by GitHub
parent c56c5a636d
commit 6720626bd3
36 changed files with 193 additions and 100 deletions
+2 -1
View File
@@ -11,9 +11,10 @@
"extends": [ "extends": [
"eslint:recommended", "eslint:recommended",
"plugin:react/recommended", "plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended",
"eslint-config-prettier", "eslint-config-prettier",
"plugin:@tanstack/eslint-plugin-query/recommended" "plugin:@tanstack/eslint-plugin-query/recommended"
], ],
"plugins": [ "plugins": [
"react", "react",
+1
View File
@@ -68,6 +68,7 @@
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.5.0",
"eslint-plugin-jest": "^27.0.4", "eslint-plugin-jest": "^27.0.4",
"eslint-plugin-react": "^7.31.8", "eslint-plugin-react": "^7.31.8",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-simple-import-sort": "^7.0.0", "eslint-plugin-simple-import-sort": "^7.0.0",
"eslint-plugin-testing-library": "^5.6.4", "eslint-plugin-testing-library": "^5.6.4",
"jsdom": "^20.0.0", "jsdom": "^20.0.0",
+4 -4
View File
@@ -1,4 +1,4 @@
import { Suspense, useCallback, useEffect } from 'react'; import { Suspense, useEffect } from 'react';
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom';
import { ChakraProvider } from '@chakra-ui/react'; import { ChakraProvider } from '@chakra-ui/react';
import { QueryClientProvider } from '@tanstack/react-query'; import { QueryClientProvider } from '@tanstack/react-query';
@@ -19,7 +19,7 @@ import('typeface-open-sans');
function App() { function App() {
const { isElectron, sendToElectron } = useElectronEvent(); const { isElectron, sendToElectron } = useElectronEvent();
const handleKeyPress = useCallback((event:KeyboardEvent) => { const handleKeyPress = (event:KeyboardEvent) => {
// handle held key // handle held key
if (event.repeat) return; if (event.repeat) return;
// check if the alt key is pressed // check if the alt key is pressed
@@ -29,7 +29,7 @@ function App() {
sendToElectron('set-window', 'show-dev'); sendToElectron('set-window', 'show-dev');
} }
} }
},[]); };
useEffect(() => { useEffect(() => {
if (isElectron) { if (isElectron) {
@@ -40,7 +40,7 @@ function App() {
document.removeEventListener('keydown', handleKeyPress); document.removeEventListener('keydown', handleKeyPress);
} }
}; };
}, [handleKeyPress]); }, []);
return ( return (
<ChakraProvider resetCSS theme={theme}> <ChakraProvider resetCSS theme={theme}>
@@ -1,7 +1,11 @@
@use '../../../../theme/v2Styles' as *;
.delayInput { .delayInput {
display: flex; display: flex;
gap: 8px; gap: $element-spacing;
align-items: center; align-items: center;
color: $ontime-delay-text;
font-size: $text-body-size;
} }
.inputField { .inputField {
@@ -1,7 +1,6 @@
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { Input } from '@chakra-ui/react'; import { Input } from '@chakra-ui/react';
import { clamp } from 'common/utils/math'; import { clamp } from 'common/utils/math';
import PropTypes from 'prop-types';
import style from './DelayInput.module.scss'; import style from './DelayInput.module.scss';
@@ -81,8 +80,3 @@ export default function DelayInput(props: DelayInputProps) {
</label> </label>
); );
} }
DelayInput.propTypes = {
submitHandler: PropTypes.func,
value: PropTypes.number,
};
@@ -1,6 +1,7 @@
import { useCallback, useRef } from 'react'; import { useCallback, useRef } from 'react';
import { Input, Textarea } from '@chakra-ui/react'; import { Input, Textarea } from '@chakra-ui/react';
import { EventEditorSubmitActions } from '../../../../features/event-editor/EventEditor';
import { Size } from '../../../models/UtilTypes'; import { Size } from '../../../models/UtilTypes';
import useReactiveTextInput from './useReactiveTextInput'; import useReactiveTextInput from './useReactiveTextInput';
@@ -9,9 +10,9 @@ interface TextInputProps {
isTextArea?: boolean; isTextArea?: boolean;
isFullHeight?: boolean; isFullHeight?: boolean;
size?: Size; size?: Size;
field: string; field: EventEditorSubmitActions;
initialText?: string; initialText?: string;
submitHandler: (field: string, newValue: string) => void; submitHandler: (field: EventEditorSubmitActions, newValue: string) => void;
} }
export default function TextInput(props: TextInputProps) { export default function TextInput(props: TextInputProps) {
@@ -20,7 +21,7 @@ export default function TextInput(props: TextInputProps) {
const submitCallback = useCallback((newValue: string) => const submitCallback = useCallback((newValue: string) =>
submitHandler(field, newValue) submitHandler(field, newValue)
, [field]); ,[field, submitHandler]);
const textInputProps = useReactiveTextInput(initialText, submitCallback, { submitOnEnter: true }); const textInputProps = useReactiveTextInput(initialText, submitCallback, { submitOnEnter: true });
const textAreaProps = useReactiveTextInput(initialText, submitCallback); const textAreaProps = useReactiveTextInput(initialText, submitCallback);
@@ -76,7 +76,7 @@ export default function useReactiveTextInput(
break; break;
} }
}, },
[initialText, handleSubmit, text], [initialText, options?.submitOnEnter, handleSubmit, text],
); );
return { return {
+2 -2
View File
@@ -61,7 +61,7 @@ export const LoggingProvider = ({ children }: LoggingProviderProps) => {
return () => { return () => {
socket.off('logger'); socket.off('logger');
}; };
}, [socket]); }, []);
/** /**
* Utility function sends message over socket * Utility function sends message over socket
@@ -86,7 +86,7 @@ export const LoggingProvider = ({ children }: LoggingProviderProps) => {
setLogData((currentLog) => currentLog.slice(1)); setLogData((currentLog) => currentLog.slice(1));
} }
}, },
[logData.length, setLogData, socket], [logData.length, setLogData],
); );
/** /**
+1 -1
View File
@@ -60,7 +60,7 @@ export const useEventAction = () => {
emitError(`Error fetching data: ${error.message}`); emitError(`Error fetching data: ${error.message}`);
} }
}, },
[_addEventMutation, emitError], [_addEventMutation, emitError, queryClient],
); );
/** /**
+1 -1
View File
@@ -14,5 +14,5 @@ export const useKeyDown = (callback: () => void, targetKey: string) => {
return () => { return () => {
document.removeEventListener('keydown', onKeyDown); document.removeEventListener('keydown', onKeyDown);
}; };
}, [onKeyDown]); }, []);
}; };
+1 -1
View File
@@ -16,7 +16,7 @@ export default function useSubscription<T>(topic: string, initialState: T, reque
return () => { return () => {
socket.off(topic); socket.off(topic);
}; };
}, [requestString, socket, topic]); }, [requestString, topic]);
return [state, setState] as const; return [state, setState] as const;
}; };
+4 -2
View File
@@ -1,3 +1,5 @@
export type TimeEntryField = 'timeStart' |'timeEnd' | 'durationOverride';
/** /**
* @description Milliseconds in a day * @description Milliseconds in a day
*/ */
@@ -12,7 +14,7 @@ export const calculateDuration = (start: number, end: number): number =>
/** /**
* @description Checks which field the value relates to * @description Checks which field the value relates to
*/ */
export const handleTimeEntry = (field: string, val: number, timeStart: number, timeEnd: number): {start: number, end: number, durationOverride: boolean} => { export const handleTimeEntry = (field: TimeEntryField, val: number, timeStart: number, timeEnd: number): {start: number, end: number, durationOverride: boolean} => {
let start = timeStart; let start = timeStart;
let end = timeEnd; let end = timeEnd;
let durationOverride = false; let durationOverride = false;
@@ -30,7 +32,7 @@ export const handleTimeEntry = (field: string, val: number, timeStart: number, t
/** /**
* @description Validates time entry * @description Validates time entry
*/ */
export const validateEntry = (field: string, value: number, timeStart: number, timeEnd: number): { value: boolean, catch: string } => { export const validateEntry = (field: TimeEntryField, value: number, timeStart: number, timeEnd: number): { value: boolean, catch: string } => {
const validate = { value: true, catch: '' }; const validate = { value: true, catch: '' };
// 1. if one of times is not entered, anything goes // 1. if one of times is not entered, anything goes
@@ -1,12 +1,16 @@
@use '../../theme/ontimeColours' as *; @use '../../theme/ontimeColours' as *;
@use '../../theme/v2Styles' as *; @use '../../theme/v2Styles' as *;
$menu-width: 48px;
$rundown-width: 46em;
$playback-width: 450px;
@mixin absolute-top-right($distance) { @mixin absolute-top-right($distance) {
position: absolute; position: absolute;
top: $distance; top: $distance;
right: $distance; right: $distance;
cursor: pointer; cursor: pointer;
color: #f2f2f2; color: $ui-white;
} }
.corner { .corner {
@@ -26,7 +30,7 @@
display: grid; display: grid;
grid-template-rows: auto 1fr; grid-template-rows: auto 1fr;
grid-template-columns: 48px 46em 450px auto; grid-template-columns: $menu-width $rundown-width $playback-width auto;
grid-template-areas: grid-template-areas:
'sett even play info' 'sett even play info'
'sett even mess info'; 'sett even mess info';
@@ -50,7 +54,7 @@
.mainContainer { .mainContainer {
height: 100%; height: 100%;
grid-template-rows: auto 1fr; grid-template-rows: auto 1fr;
grid-template-columns: 48px 48em auto; grid-template-columns: $menu-width $rundown-width auto;
.info { .info {
visibility: hidden; visibility: hidden;
@@ -63,7 +67,7 @@
.mainContainer { .mainContainer {
height: 100%; height: 100%;
grid-template-rows: 100%; grid-template-rows: 100%;
grid-template-columns: 48px 48em; grid-template-columns: $menu-width $rundown-width;
grid-template-areas: grid-template-areas:
'sett even'; 'sett even';
@@ -45,7 +45,6 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 8px;
margin-top: 4px;
} }
.left { .left {
@@ -10,20 +10,23 @@ import { useEventAction } from 'common/hooks/useEventAction';
import { millisToMinutes } from 'common/utils/dateConfig'; import { millisToMinutes } from 'common/utils/dateConfig';
import getDelayTo from 'common/utils/getDelayTo'; import getDelayTo from 'common/utils/getDelayTo';
import { stringFromMillis } from 'common/utils/time'; import { stringFromMillis } from 'common/utils/time';
import { calculateDuration, validateEntry } from 'common/utils/timesManager'; import { calculateDuration, TimeEntryField, validateEntry } from 'common/utils/timesManager';
import { useAtom } from 'jotai'; import { useAtom } from 'jotai';
import CopyTag from '../../common/components/copy-tag/CopyTag'; import CopyTag from '../../common/components/copy-tag/CopyTag';
import useRundown from '../../common/hooks-query/useRundown'; import useRundown from '../../common/hooks-query/useRundown';
import { OntimeEvent } from '../../common/models/EventTypes';
import style from './EventEditor.module.scss'; import style from './EventEditor.module.scss';
export type EventEditorSubmitActions = keyof OntimeEvent | 'durationOverride';
export default function EventEditor() { export default function EventEditor() {
const [openId] = useAtom(editorEventId); const [openId] = useAtom(editorEventId);
const { data } = useRundown(); const { data } = useRundown();
const { emitWarning, emitError } = useContext(LoggingContext); const { emitWarning, emitError } = useContext(LoggingContext);
const { updateEvent } = useEventAction(); const { updateEvent } = useEventAction();
const [event, setEvent] = useState(null); const [event, setEvent] = useState<OntimeEvent | null>(null);
const [delay, setDelay] = useState(0); const [delay, setDelay] = useState(0);
useEffect(() => { useEffect(() => {
@@ -33,28 +36,34 @@ export default function EventEditor() {
const eventIndex = data.findIndex((event) => event.id === openId); const eventIndex = data.findIndex((event) => event.id === openId);
if (eventIndex > -1) { if (eventIndex > -1) {
setDelay(getDelayTo(data, eventIndex)); const event = data[eventIndex];
setEvent(data[eventIndex]); if (event.type === 'event') {
setDelay(getDelayTo(data, eventIndex));
setEvent(data[eventIndex] as OntimeEvent);
}
} }
}, [data, event, openId]); }, [data, event, openId]);
const handleSubmit = useCallback( const handleSubmit = useCallback(
(field, value) => { (field: EventEditorSubmitActions, value: any) => {
const newEventData = { id: event.id }; if (event === null) {
return;
}
const newEventData: Partial<OntimeEvent> = { id: event.id };
switch (field) { switch (field) {
case 'durationOverride': { case 'durationOverride': {
// duration defines timeEnd // duration defines timeEnd
newEventData.timeEnd = event.timeStart += value; newEventData.timeEnd = event.timeStart += value as number;
break; break;
} }
case 'timeStart': { case 'timeStart': {
newEventData.duration = calculateDuration(value, event.timeEnd); newEventData.duration = calculateDuration(value as number, event.timeEnd);
newEventData.timeStart = value; newEventData.timeStart = value as number;
break; break;
} }
case 'timeEnd': { case 'timeEnd': {
newEventData.duration = calculateDuration(event.timeStart, value); newEventData.duration = calculateDuration(event.timeStart, value as number);
newEventData.timeEnd = value; newEventData.timeEnd = value as number;
break; break;
} }
default: { default: {
@@ -73,22 +82,26 @@ export default function EventEditor() {
[emitError, event, updateEvent], [emitError, event, updateEvent],
); );
const timerValidationHandler = useCallback( const timerValidationHandler = useCallback((entry: TimeEntryField, val: number) => {
(entry, val) => { if (!event) {
return;
}
const valid = validateEntry(entry, val, event.timeStart, event.timeEnd); const valid = validateEntry(entry, val, event.timeStart, event.timeEnd);
if (!valid.value) { if (!valid.value) {
emitWarning(`Time Input Warning: ${valid.catch}`); emitWarning(`Time Input Warning: ${valid.catch}`);
} }
return valid.value; return valid.value;
}, },
[emitWarning, event?.timeStart, event?.timeEnd], [event, emitWarning],
); );
const togglePublic = useCallback( const togglePublic = useCallback((currentValue: boolean) => {
(currentValue) => { if (!event) {
return;
}
updateEvent({ id: event.id, isPublic: !currentValue }); updateEvent({ id: event.id, isPublic: !currentValue });
}, },
[event?.id, updateEvent], [event, updateEvent],
); );
if (!event) { if (!event) {
@@ -197,7 +210,7 @@ export default function EventEditor() {
handleChange={(value) => handleSubmit('colour', value)} handleChange={(value) => handleSubmit('colour', value)}
/> />
<Button <Button
rightIcon={<IoBan />} leftIcon={<IoBan />}
onClick={() => handleSubmit('colour', '')} onClick={() => handleSubmit('colour', '')}
variant='ontime-subtle' variant='ontime-subtle'
size='sm' size='sm'
+2 -2
View File
@@ -81,7 +81,7 @@ export default function MenuBar(props: MenuBarProps) {
} }
} }
}, },
[isElectron, isSettingsOpen, onSettingsClose, onSettingsOpen], [isSettingsOpen, onSettingsClose, onSettingsOpen],
); );
useEffect(() => { useEffect(() => {
@@ -93,7 +93,7 @@ export default function MenuBar(props: MenuBarProps) {
document.removeEventListener('keydown', handleKeyPress); document.removeEventListener('keydown', handleKeyPress);
} }
}; };
}, [handleKeyPress]); }, [handleKeyPress, isElectron]);
return ( return (
<VStack> <VStack>
+1 -1
View File
@@ -41,7 +41,7 @@ const RundownMenu = () => {
break; break;
} }
}, },
[toggleCursorLocked], [addEvent, deleteAllEvents],
); );
return ( return (
+1 -1
View File
@@ -75,7 +75,7 @@ export default function AliasesModal() {
} }
setSubmitting(false); setSubmitting(false);
}, },
[aliases, refetch], [aliases, emitError, refetch],
); );
/** /**
+3 -3
View File
@@ -74,7 +74,7 @@ export default function Rundown(props) {
if (event.altKey && (!event.ctrlKey || !event.shiftKey)) { if (event.altKey && (!event.ctrlKey || !event.shiftKey)) {
// Arrow down // Arrow down
if (event.keyCode === 40) { if (event.keyCode === 40) {
if (cursor < entries.length - 2) moveCursorDown(); if (cursor < entries.length - 1) moveCursorDown();
} }
// Arrow up // Arrow up
if (event.keyCode === 38) { if (event.keyCode === 38) {
@@ -121,7 +121,7 @@ export default function Rundown(props) {
block: 'nearest', block: 'nearest',
inline: 'start', inline: 'start',
}); });
}, [cursor]); }, [cursorRef]);
// if selected event // if selected event
// or cursor settings changed // or cursor settings changed
@@ -143,7 +143,7 @@ export default function Rundown(props) {
// move cursor // move cursor
moveCursorTo(gotoIndex); moveCursorTo(gotoIndex);
} }
}, [data.selectedEventId, isCursorLocked, moveCursorTo]); }, [data.selectedEventId, entries, isCursorLocked, moveCursorTo]);
// DND // DND
const handleOnDragEnd = useCallback( const handleOnDragEnd = useCallback(
+13 -12
View File
@@ -132,16 +132,7 @@ export default function RundownEntry(props: RundownEntryProps) {
break; break;
} }
}, },
[ [addEvent, data, defaultPublic, deleteEvent, emitError, moveCursorTo, openId, setOpenId, startTimeIsLastEnd, updateEvent],
addEvent,
calculateDuration,
data,
defaultPublic,
deleteEvent,
emitError,
startTimeIsLastEnd,
updateEvent,
],
); );
if (data.type === 'event') { if (data.type === 'event') {
@@ -168,9 +159,19 @@ export default function RundownEntry(props: RundownEntryProps) {
/> />
); );
} else if (data.type === 'block') { } else if (data.type === 'block') {
return <BlockBlock index={index} data={data} actionHandler={actionHandler} />; return <BlockBlock
index={index}
data={data}
hasCursor={hasCursor}
actionHandler={actionHandler}
/>;
} else if (data.type === 'delay') { } else if (data.type === 'delay') {
return <DelayBlock index={index} data={data} actionHandler={actionHandler} />; return <DelayBlock
index={index}
data={data}
hasCursor={hasCursor}
actionHandler={actionHandler}
/>;
} }
return null; return null;
}; };
@@ -10,6 +10,7 @@ $block-text-color: $gray-50;
$block-bg: $gray-1200; $block-bg: $gray-1200;
$block-box-shadow: rgba(0, 0, 0, 0.5) 0 0 3px 2px; $block-box-shadow: rgba(0, 0, 0, 0.5) 0 0 3px 2px;
$secondary-block-height: 40px; $secondary-block-height: 40px;
$block-cursor-color: $blue-400;
@mixin block-styling() { @mixin block-styling() {
box-sizing: content-box; box-sizing: content-box;
@@ -21,7 +22,7 @@ $secondary-block-height: 40px;
} }
@mixin block-spacing() { @mixin block-spacing() {
padding: 4px 10px 4px 2px; padding: 4px 8px 4px 2px;
gap: 2px; gap: 2px;
} }
@@ -34,4 +35,8 @@ $secondary-block-height: 40px;
&:hover { &:hover {
opacity: 1; opacity: 1;
} }
&:focus {
box-shadow: none;
outline: none;
}
} }
@@ -8,6 +8,10 @@
grid-template-columns: 32px 1fr auto; grid-template-columns: 32px 1fr auto;
align-items: center; align-items: center;
height: $secondary-block-height; height: $secondary-block-height;
&.hasCursor {
outline: 1px solid $block-cursor-color;
}
} }
.drag { .drag {
@@ -1,7 +1,9 @@
import { useEffect, useRef } from 'react';
import { Draggable } from 'react-beautiful-dnd'; import { Draggable } from 'react-beautiful-dnd';
import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo'; import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo';
import { OntimeBlock, OntimeEvent } from '../../../common/models/EventTypes'; import { OntimeBlock, OntimeEvent } from '../../../common/models/EventTypes';
import { cx } from '../../../common/utils/styleUtils';
import BlockActionMenu from '../event-block/composite/BlockActionMenu'; import BlockActionMenu from '../event-block/composite/BlockActionMenu';
import { EventItemActions } from '../RundownEntry'; import { EventItemActions } from '../RundownEntry';
@@ -10,17 +12,30 @@ import style from './BlockBlock.module.scss';
interface BlockBlockProps { interface BlockBlockProps {
index: number; index: number;
data: OntimeBlock; data: OntimeBlock;
hasCursor: boolean;
actionHandler: (action: EventItemActions, payload?: number | { field: keyof OntimeEvent, value: unknown }) => void; actionHandler: (action: EventItemActions, payload?: number | { field: keyof OntimeEvent, value: unknown }) => void;
} }
export default function BlockBlock(props: BlockBlockProps) { export default function BlockBlock(props: BlockBlockProps) {
const { index, data, actionHandler } = props; const { index, data, hasCursor, actionHandler } = props;
const onFocusRef = useRef<null | HTMLSpanElement>(null);
useEffect(() => {
if (hasCursor) {
onFocusRef?.current?.focus();
}
}, [hasCursor])
const blockClasses = cx([
style.block,
hasCursor ? style.hasCursor : null,
]);
return ( return (
<Draggable key={data.id} draggableId={data.id} index={index}> <Draggable key={data.id} draggableId={data.id} index={index}>
{(provided) => ( {(provided) => (
<div className={style.block} {...provided.draggableProps} ref={provided.innerRef}> <div className={blockClasses} {...provided.draggableProps} ref={provided.innerRef}>
<span className={style.drag} {...provided.dragHandleProps}> <span className={style.drag} {...provided.dragHandleProps} ref={onFocusRef}>
<IoReorderTwo /> <IoReorderTwo />
</span> </span>
<BlockActionMenu <BlockActionMenu
@@ -10,6 +10,10 @@
align-items: center; align-items: center;
height: $secondary-block-height; height: $secondary-block-height;
gap: 8px; gap: 8px;
&.hasCursor {
outline: 1px solid $block-cursor-color;
}
} }
.drag { .drag {
@@ -1,4 +1,4 @@
import { useCallback } from 'react'; import { useCallback, useEffect, useRef } from 'react';
import { Draggable } from 'react-beautiful-dnd'; import { Draggable } from 'react-beautiful-dnd';
import { Button, HStack } from '@chakra-ui/react'; import { Button, HStack } from '@chakra-ui/react';
import { IoCheckmark } from '@react-icons/all-files/io5/IoCheckmark'; import { IoCheckmark } from '@react-icons/all-files/io5/IoCheckmark';
@@ -8,6 +8,7 @@ import { useEventAction } from 'common/hooks/useEventAction';
import { millisToMinutes } from 'common/utils/dateConfig'; import { millisToMinutes } from 'common/utils/dateConfig';
import { OntimeDelay, OntimeEvent } from '../../../common/models/EventTypes'; import { OntimeDelay, OntimeEvent } from '../../../common/models/EventTypes';
import { cx } from '../../../common/utils/styleUtils';
import BlockActionMenu from '../event-block/composite/BlockActionMenu'; import BlockActionMenu from '../event-block/composite/BlockActionMenu';
import { EventItemActions } from '../RundownEntry'; import { EventItemActions } from '../RundownEntry';
@@ -16,12 +17,20 @@ import style from './DelayBlock.module.scss';
interface DelayBlockProps { interface DelayBlockProps {
data: OntimeDelay, data: OntimeDelay,
index: number; index: number;
hasCursor: boolean;
actionHandler: (action: EventItemActions, payload?: number | { field: keyof OntimeEvent, value: unknown }) => void; actionHandler: (action: EventItemActions, payload?: number | { field: keyof OntimeEvent, value: unknown }) => void;
} }
export default function DelayBlock(props: DelayBlockProps) { export default function DelayBlock(props: DelayBlockProps) {
const { data, index, actionHandler } = props; const { data, index, hasCursor, actionHandler } = props;
const { applyDelay, updateEvent } = useEventAction(); const { applyDelay, updateEvent } = useEventAction();
const onFocusRef = useRef<null | HTMLSpanElement>(null);
useEffect(() => {
if (hasCursor) {
onFocusRef?.current?.focus();
}
}, [hasCursor])
const applyDelayHandler = useCallback(() => { const applyDelayHandler = useCallback(() => {
applyDelay(data.id); applyDelay(data.id);
@@ -39,13 +48,18 @@ export default function DelayBlock(props: DelayBlockProps) {
[data.id, updateEvent], [data.id, updateEvent],
); );
const blockClasses = cx([
style.delay,
hasCursor ? style.hasCursor : null,
]);
const delayValue = data.duration != null ? millisToMinutes(data.duration) : undefined; const delayValue = data.duration != null ? millisToMinutes(data.duration) : undefined;
return ( return (
<Draggable key={data.id} draggableId={data.id} index={index}> <Draggable key={data.id} draggableId={data.id} index={index}>
{(provided) => ( {(provided) => (
<div className={style.delay} {...provided.draggableProps} ref={provided.innerRef}> <div className={blockClasses} {...provided.draggableProps} ref={provided.innerRef}>
<span className={style.drag} {...provided.dragHandleProps}> <span className={style.drag} {...provided.dragHandleProps} ref={onFocusRef}>
<IoReorderTwo /> <IoReorderTwo />
</span> </span>
<DelayInput <DelayInput
@@ -24,7 +24,7 @@
} }
&.hasCursor { &.hasCursor {
outline: 1px solid $blue-400; outline: 1px solid $block-cursor-color;
} }
&.skip { &.skip {
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { Draggable } from 'react-beautiful-dnd'; import { Draggable } from 'react-beautiful-dnd';
import { Editable, EditableInput, EditablePreview, Tooltip } from '@chakra-ui/react'; import { Editable, EditableInput, EditablePreview, Tooltip } from '@chakra-ui/react';
import { IoOptions } from '@react-icons/all-files/io5/IoOptions'; import { IoOptions } from '@react-icons/all-files/io5/IoOptions';
@@ -81,6 +81,7 @@ export default function EventBlock(props: EventBlockProps) {
const [openId, setOpenId] = useAtom(editorEventId); const [openId, setOpenId] = useAtom(editorEventId);
const { updateEvent } = useEventAction(); const { updateEvent } = useEventAction();
const [blockTitle, setBlockTitle] = useState<string>(title || ''); const [blockTitle, setBlockTitle] = useState<string>(title || '');
const onFocusRef = useRef<null | HTMLSpanElement>(null);
const binderColours = colour && getAccessibleColour(colour); const binderColours = colour && getAccessibleColour(colour);
@@ -90,6 +91,12 @@ export default function EventBlock(props: EventBlockProps) {
setBlockTitle(title); setBlockTitle(title);
}, [title]); }, [title]);
useEffect(() => {
if (hasCursor) {
onFocusRef?.current?.focus();
}
}, [hasCursor]);
const handleTitle = useCallback( const handleTitle = useCallback(
(text: string) => { (text: string) => {
if (text === title) { if (text === title) {
@@ -101,7 +108,7 @@ export default function EventBlock(props: EventBlockProps) {
updateEvent({ id: eventId, title: cleanVal }); updateEvent({ id: eventId, title: cleanVal });
}, },
[updateEvent, title], [title, updateEvent, eventId],
); );
const eventIsPlaying = selected && playback === 'start'; const eventIsPlaying = selected && playback === 'start';
@@ -133,7 +140,7 @@ export default function EventBlock(props: EventBlockProps) {
tabIndex={-1} tabIndex={-1}
onClick={() => actionHandler('set-cursor', index)} onClick={() => actionHandler('set-cursor', index)}
> >
<span className={style.drag} {...provided.dragHandleProps}> <span className={style.drag} {...provided.dragHandleProps} ref={onFocusRef}>
<IoReorderTwo /> <IoReorderTwo />
</span> </span>
{eventIndex} {eventIndex}
@@ -184,6 +191,7 @@ export default function EventBlock(props: EventBlockProps) {
previousEnd={previousEnd} previousEnd={previousEnd}
/> />
<Editable <Editable
variant='ontime'
value={blockTitle} value={blockTitle}
className={`${style.eventTitle} ${!title || title === '' ? style.noTitle : ''}`} className={`${style.eventTitle} ${!title || title === '' ? style.noTitle : ''}`}
placeholder='Event title' placeholder='Event title'
@@ -198,21 +206,25 @@ export default function EventBlock(props: EventBlockProps) {
<div className={selected ? style.progressBg : `${style.progressBg} ${style.hidden}`}> <div className={selected ? style.progressBg : `${style.progressBg} ${style.hidden}`}>
<EventBlockProgressBar playback={playback} /> <EventBlockProgressBar playback={playback} />
</div> </div>
<div className={style.eventStatus}> <div className={style.eventStatus} tabIndex={-1}
>
<Tooltip <Tooltip
label='Next event' label='Next event'
isDisabled={!next} isDisabled={!next}
shouldWrapChildren {...tooltipProps} {...tooltipProps}
> >
<IoPlaySkipForward <span>
className={`${style.statusIcon} ${next ? style.active : ''}`} /> <IoPlaySkipForward
className={`${style.statusIcon} ${next ? style.active : ''}`} />
</span>
</Tooltip> </Tooltip>
<Tooltip <Tooltip
label={`${isPublic ? 'Event is public' : 'Event is private'}`} label={`${isPublic ? 'Event is public' : 'Event is private'}`}
{...tooltipProps} {...tooltipProps}
shouldWrapChildren
> >
<IoPeople className={`${style.statusIcon} ${isPublic ? style.active : ''}`} /> <span>
<IoPeople className={`${style.statusIcon} ${isPublic ? style.active : ''}`} />
</span>
</Tooltip> </Tooltip>
</div> </div>
</div> </div>
@@ -58,7 +58,7 @@ export default function QuickAddBlock(props: QuickAddBlockProps) {
} }
} }
}, [addEvent, doPublic, doStartTime, emitError, previousId, previousEventId]); }, [defaultPublic, previousId, previousEventId, addEvent, emitError]);
return ( return (
<div className={style.quickAdd}> <div className={style.quickAdd}>
+1 -1
View File
@@ -50,7 +50,7 @@ const withSocket = (Component) => {
socket.on('publicselected-id', (data) => { socket.on('publicselected-id', (data) => {
setPublicSelectedId(data); setPublicSelectedId(data);
}); });
}, [socket]); }, []);
const publicEvents = useMemo(() => { const publicEvents = useMemo(() => {
+1
View File
@@ -20,6 +20,7 @@ $gray-900: #4c4c4c;
$gray-1000: #404040; $gray-1000: #404040;
$gray-1050: #303030; $gray-1050: #303030;
$gray-1100: #2d2d2d; $gray-1100: #2d2d2d;
$gray-1250: #262626;
$gray-1200: #202020; $gray-1200: #202020;
$gray-1300: #1a1a1a; $gray-1300: #1a1a1a;
$gray-1350: #101010; $gray-1350: #101010;
+14
View File
@@ -0,0 +1,14 @@
export const ontimeEditable = {
input: {
borderRadius: '3px',
width: '100%',
_focus: {
border: '1px solid #578AF4', // $blue-500
boxShadow: 'none',
},
},
preview: {
width: '100%',
border: '1px solid transparent', // $blue-500
},
};
+3 -3
View File
@@ -1,9 +1,9 @@
export const ontimeSelect = { export const ontimeSelect = {
field: { field: {
color: '#9d9d9d', // $gray-500 color: '#e2e2e2', // $gray-200
borderRadius: '3px', borderRadius: '3px',
fontWeight: '400', fontWeight: '400',
background: '#2d2d2d', // $gray-1100 background: '#262626', // $gray-1100
border: '1px solid transparent', border: '1px solid transparent',
_hover: { _hover: {
background: '#404040', // $gray-1000 background: '#404040', // $gray-1000
@@ -15,6 +15,6 @@ export const ontimeSelect = {
}, },
}, },
icon: { icon: {
color: '#9d9d9d', // $gray-500 color: '#e2e2e2', // $gray-200
}, },
}; };
+4
View File
@@ -2,9 +2,13 @@ export const ontimeSwitch = {
container: { }, container: { },
track: { track: {
background: '#2d2d2d', // $gray-1100 background: '#2d2d2d', // $gray-1100
border: '1px solid transparent',
_checked: { _checked: {
background: `#2B5ABC`, // $blue-700 background: `#2B5ABC`, // $blue-700
}, },
_focus: {
border: '1px solid #578AF4', // $blue-500
}
}, },
thumb: {}, thumb: {},
}; };
+3 -3
View File
@@ -1,14 +1,14 @@
const commonStyles = { const commonStyles = {
borderRadius: '3px', borderRadius: '3px',
fontWeight: '400', fontWeight: '400',
backgroundColor: '#2d2d2d', // $gray-1100 backgroundColor: '#262626', // $gray-1250
color: '#e2e2e2', // $gray-200 color: '#e2e2e2', // $gray-200
border: '1px solid transparent', border: '1px solid transparent',
_hover: { _hover: {
backgroundColor: '#404040', // $gray-1000 backgroundColor: '#2d2d2d', // $gray-1100
}, },
_focus: { _focus: {
backgroundColor: '#404040', // $gray-1000 backgroundColor: '#2d2d2d', // $gray-1000
color: '#f6f6f6', // $gray-50 color: '#f6f6f6', // $gray-50
border: '1px solid #578AF4', // $blue-500 border: '1px solid #578AF4', // $blue-500
}, },
+4 -9
View File
@@ -7,6 +7,7 @@ import {
ontimeButtonSubtleWhite, ontimeButtonSubtleWhite,
} from './ontimeButton'; } from './ontimeButton';
import { ontimeCheckboxOnDark } from './ontimeCheckbox'; import { ontimeCheckboxOnDark } from './ontimeCheckbox';
import { ontimeEditable } from './ontimeEditable';
import { ontimeMenuOnDark } from './ontimeMenu'; import { ontimeMenuOnDark } from './ontimeMenu';
import { ontimeSelect } from './ontimeSelect'; import { ontimeSelect } from './ontimeSelect';
import { ontimeSwitch } from './ontimeSwitch'; import { ontimeSwitch } from './ontimeSwitch';
@@ -36,19 +37,13 @@ const theme = extendTheme({
}, },
}, },
Editable: { Editable: {
baseStyle: { variants: {
input: { 'ontime': { ...ontimeEditable },
borderRadius: '2px',
width: '100%',
},
preview: {
width: '100%',
},
}, },
}, },
Input: { Input: {
baseStyle: { baseStyle: {
borderRadius: '2px', borderRadius: '3px',
border: '1px', border: '1px',
}, },
variants: { variants: {
+5
View File
@@ -2980,6 +2980,11 @@ eslint-plugin-jest@^27.0.4:
dependencies: dependencies:
"@typescript-eslint/utils" "^5.10.0" "@typescript-eslint/utils" "^5.10.0"
eslint-plugin-react-hooks@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz#4c3e697ad95b77e93f8646aaa1630c1ba607edd3"
integrity sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==
eslint-plugin-react@^7.31.8: eslint-plugin-react@^7.31.8:
version "7.31.8" version "7.31.8"
resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.31.8.tgz#3a4f80c10be1bcbc8197be9e8b641b2a3ef219bf" resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.31.8.tgz#3a4f80c10be1bcbc8197be9e8b641b2a3ef219bf"