diff --git a/src/electron/main/main.ts b/src/electron/main/main.ts
index 77dff84c..268717de 100644
--- a/src/electron/main/main.ts
+++ b/src/electron/main/main.ts
@@ -9,6 +9,7 @@ import {
Menu,
MenuItemConstructorOptions,
MessageBoxOptions,
+ PopupOptions,
shell,
Tray,
} from 'electron';
@@ -397,8 +398,8 @@ export class Main {
return dialog.showMessageBox(options);
}
- private onShowHelpMenu() {
- this.menu.popup();
+ private onShowHelpMenu(e: IpcMainEvent, options: PopupOptions) {
+ this.menu.popup(options);
}
private onRendererClosed() {
diff --git a/src/electron/renderer/components/File.tsx b/src/electron/renderer/components/File.tsx
index 0ef3135a..54443435 100644
--- a/src/electron/renderer/components/File.tsx
+++ b/src/electron/renderer/components/File.tsx
@@ -3,6 +3,7 @@ import { ChangeEvent, useState } from 'react';
import styled from 'styled-components';
import { ClearButton } from './Buttons';
import { useField } from './Form';
+import { VisuallyHiddenInput } from './VisuallyHiddenInput';
const FileOutput = styled.output`
flex-grow: 1;
@@ -85,8 +86,13 @@ export const File = ({ accept }: FileProps) => {
) : (
No file selected
)}
+
Change
-
);
};
diff --git a/src/electron/renderer/components/Form.tsx b/src/electron/renderer/components/Form.tsx
index 86ac6965..923a0712 100644
--- a/src/electron/renderer/components/Form.tsx
+++ b/src/electron/renderer/components/Form.tsx
@@ -33,7 +33,7 @@ const StyledField = styled(Row)`
flex-shrink: 0;
}
- &:hover > ${StyledLabel} {
+ &:hover > ${StyledLabel}, &:focus-within > ${StyledLabel} {
color: var(--accent);
background: linear-gradient(
90deg,
@@ -144,6 +144,8 @@ export const Field = forwardRef>(
ref={ref}
onMouseEnter={onHover ? () => onHover(name) : undefined}
onMouseLeave={onHover ? () => onHover(null) : undefined}
+ onFocus={onHover ? () => onHover(name) : undefined}
+ onBlur={onHover ? () => onHover(null) : undefined}
>
{
});
}, [props.image, props.showBoxes]);
- return ;
+ return ;
};
diff --git a/src/electron/renderer/components/KeyBind.tsx b/src/electron/renderer/components/KeyBind.tsx
index 9979e1af..c40d94bd 100644
--- a/src/electron/renderer/components/KeyBind.tsx
+++ b/src/electron/renderer/components/KeyBind.tsx
@@ -11,6 +11,7 @@ import styled from 'styled-components';
import { ClearButton } from './Buttons';
import { Row } from './Flex';
import { OnBeforeValueChange, useField } from './Form';
+import { VisuallyHiddenInput } from './VisuallyHiddenInput';
export interface Transformer {
toUniversal(input: string): string[];
@@ -49,22 +50,6 @@ const KeyBindText = styled.span`
text-transform: uppercase;
`;
-const VisuallyHiddenInput = styled.input`
- border: 0;
- clip: rect(0 0 0 0);
- height: 1px;
- margin: -1px;
- overflow: hidden;
- padding: 0;
- position: absolute;
- width: 1px;
- white-space: nowrap;
- outline: 0;
- appearance: none;
- top: 0;
- left: 0;
-`;
-
const KeyCode = styled.kbd`
border: 1px solid var(--accent);
color: var(--accent);
diff --git a/src/electron/renderer/components/List.tsx b/src/electron/renderer/components/List.tsx
new file mode 100644
index 00000000..1b73599b
--- /dev/null
+++ b/src/electron/renderer/components/List.tsx
@@ -0,0 +1,68 @@
+import { NavLink } from 'react-router-dom';
+import styled from 'styled-components';
+
+export const List = styled.ul`
+ overflow-y: auto;
+ padding: 0;
+ margin: 0;
+ list-style: none;
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ flex: 1 0 250px; // required?
+ align-self: stretch; // required?
+`;
+
+export const ListItem = styled.li``;
+
+export const ListItemLink = styled(NavLink)`
+ border: 1px solid var(--primary);
+ background: var(--background);
+ height: 70px;
+ display: flex;
+ align-items: center;
+ color: var(--accent);
+ flex-shrink: 0;
+ text-decoration: none;
+ gap: 1rem;
+ text-transform: uppercase;
+ font-size: 1.2rem;
+ font-weight: 500;
+ padding: 0 1rem;
+
+ &:focus-visible {
+ outline-offset: -2px;
+ }
+
+ &:hover:not(.active) {
+ border-color: var(--accent);
+ background: var(--primary-darker);
+ }
+
+ &.active {
+ border-color: var(--accent);
+ background: var(--accent-darker);
+ }
+`;
+
+// const Sequence = styled.button<{ active?: boolean }>`
+// background: ${(p) => (p.active ? 'var(--accent-dark)' : 'var(--background)')};
+// color: var(--accent);
+// border: 1px solid;
+// border-color: ${(p) => (p.active ? 'var(--accent)' : 'var(--primary)')};
+// padding: 1rem;
+// font-weight: 500;
+// font-size: 1.3rem;
+// cursor: pointer;
+// display: flex;
+// gap: 0.3rem;
+
+// ${(p) =>
+// !p.active &&
+// css`
+// &:hover {
+// background: var(--primary-darker);
+// border-color: var(--accent);
+// }
+// `}
+// `;
diff --git a/src/electron/renderer/components/RangeSlider.tsx b/src/electron/renderer/components/RangeSlider.tsx
index 52ad0c72..9bd65a05 100644
--- a/src/electron/renderer/components/RangeSlider.tsx
+++ b/src/electron/renderer/components/RangeSlider.tsx
@@ -1,4 +1,5 @@
-import { ChangeEvent, useEffect, useState } from 'react';
+import { VK_ENTER } from '@/common';
+import { ChangeEvent, KeyboardEvent, useEffect, useState } from 'react';
import styled from 'styled-components';
import { OnBeforeValueChange, useField } from './Form';
@@ -86,6 +87,12 @@ export function RangeSlider({
}
}
+ function setValueOnEnter(e: KeyboardEvent) {
+ if (e.code === VK_ENTER) {
+ onValueChange(e);
+ }
+ }
+
return (
setDisplayValue(coerceInputValue(e))}
onMouseUp={onValueChange}
+ onKeyUp={setValueOnEnter}
/>
{displayValue}
diff --git a/src/electron/renderer/components/RawDataPreview.tsx b/src/electron/renderer/components/RawDataPreview.tsx
index 15458335..37bca365 100644
--- a/src/electron/renderer/components/RawDataPreview.tsx
+++ b/src/electron/renderer/components/RawDataPreview.tsx
@@ -5,7 +5,7 @@ import {
import styled from 'styled-components';
import { Col } from './Flex';
-const RawDataPreviewWrapper = styled.pre`
+const RawDataPreviewWrapper = styled.pre.attrs({ tabIndex: 0 })`
border: 1px solid var(--primary);
background: var(--background);
color: var(--accent);
@@ -16,6 +16,10 @@ const RawDataPreviewWrapper = styled.pre`
overflow-y: auto;
margin: 0;
flex-grow: 1;
+
+ &:focus-visible {
+ outline-offset: -2px;
+ }
`;
// prettier-ignore
diff --git a/src/electron/renderer/components/TitleBar.test.tsx b/src/electron/renderer/components/TitleBar.test.tsx
new file mode 100644
index 00000000..9600ab3f
--- /dev/null
+++ b/src/electron/renderer/components/TitleBar.test.tsx
@@ -0,0 +1,71 @@
+/** @jest-environment jsdom */
+import { render, unmountComponentAtNode } from 'react-dom';
+import { act } from 'react-dom/test-utils';
+import { TitleBar } from './TitleBar';
+
+let container: HTMLDivElement = null;
+
+describe('Component: TitleBar', () => {
+ beforeEach(() => {
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ });
+
+ afterEach(() => {
+ unmountComponentAtNode(container);
+
+ document.body.removeChild(container);
+ container = null;
+ (global as any).api = null;
+ });
+
+ it('should render buttons', () => {
+ act(() => {
+ render(, container);
+ });
+
+ const buttons = container.querySelectorAll('button');
+
+ expect(buttons.length).toBe(4);
+ });
+
+ it('should send correct events', () => {
+ act(() => {
+ render(, container);
+ });
+
+ const send = jest.fn();
+ (global as any).api = { send };
+
+ const buttons = container.querySelectorAll('button');
+ const events = [
+ 'main:show-help-menu',
+ 'main:minimize',
+ 'main:maximize',
+ 'main:close',
+ ];
+
+ buttons.forEach((b, i) => {
+ const event = events[i];
+
+ expect(b.childNodes.length).toBe(1);
+ expect(b.firstChild.nodeName).toBe('svg');
+
+ b.click();
+
+ expect(send).toHaveBeenCalledTimes(i + 1);
+
+ if (event === 'main:show-help-menu') {
+ expect(send).toHaveBeenCalledWith(
+ event,
+ expect.objectContaining({
+ x: expect.any(Number),
+ y: expect.any(Number),
+ })
+ );
+ } else {
+ expect(send).toHaveBeenCalledWith(event);
+ }
+ });
+ });
+});
diff --git a/src/electron/renderer/components/TitleBar.tsx b/src/electron/renderer/components/TitleBar.tsx
index ac185222..d158a6ad 100644
--- a/src/electron/renderer/components/TitleBar.tsx
+++ b/src/electron/renderer/components/TitleBar.tsx
@@ -1,4 +1,4 @@
-import { memo } from 'react';
+import { memo, MouseEvent } from 'react';
import styled from 'styled-components';
const StyledSvg = styled.svg.attrs({ width: 20, height: 20 })``;
@@ -62,6 +62,10 @@ const IconButton = styled.button<{ close?: boolean }>`
align-items: center;
-webkit-app-region: no-drag;
+ &:focus-visible {
+ outline-offset: -2px;
+ }
+
&:hover {
background: ${(p) => (p.close ? '#e81123' : '#461f23')};
}
@@ -71,9 +75,17 @@ const IconButton = styled.button<{ close?: boolean }>`
}
`;
+function showHelpMenu(event: MouseEvent) {
+ const target = event.currentTarget as HTMLButtonElement;
+ const x = target.offsetLeft;
+ const y = target.offsetHeight;
+
+ api.send('main:show-help-menu', { x, y });
+}
+
export const TitleBar = memo(() => (
- api.send('main:show-help-menu')}>
+
api.send('main:minimize')}>
diff --git a/src/electron/renderer/components/VisuallyHiddenInput.tsx b/src/electron/renderer/components/VisuallyHiddenInput.tsx
new file mode 100644
index 00000000..5f4e4003
--- /dev/null
+++ b/src/electron/renderer/components/VisuallyHiddenInput.tsx
@@ -0,0 +1,22 @@
+import styled from 'styled-components';
+
+export const VisuallyHiddenInput = styled.input`
+ border: 0;
+ clip: rect(0 0 0 0);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ position: absolute;
+ width: 1px;
+ white-space: nowrap;
+ outline: 0;
+ appearance: none;
+ top: 0;
+ left: 0;
+
+ &:focus-visible + label {
+ outline: 2px solid var(--outline);
+ outline-offset: 2px;
+ }
+`;
diff --git a/src/electron/renderer/components/index.ts b/src/electron/renderer/components/index.ts
index ddbd9a92..41506bc8 100644
--- a/src/electron/renderer/components/index.ts
+++ b/src/electron/renderer/components/index.ts
@@ -12,6 +12,7 @@ export * from './GeneralSettings';
export * from './GridViewer';
export * from './HistoryViewer';
export * from './KeyBindsSettings';
+export * from './List';
export * from './Navigation';
export * from './PerformanceSettings';
export * from './RangeSlider';
@@ -26,3 +27,4 @@ export * from './Switch';
export * from './ThirdPartyLicensesDialog';
export * from './TitleBar';
export * from './TypesFragmentStatus';
+export * from './VisuallyHiddenInput';
diff --git a/src/electron/renderer/pages/Calibrate.tsx b/src/electron/renderer/pages/Calibrate.tsx
index 71d6f9a7..a272ee8a 100644
--- a/src/electron/renderer/pages/Calibrate.tsx
+++ b/src/electron/renderer/pages/Calibrate.tsx
@@ -41,7 +41,7 @@ export const Calibrate = () => {
return (
-
+
{entry.fragments.map((f) => (
{fromCamelCase(f.id)}
diff --git a/src/electron/renderer/pages/CalibrateFragment.tsx b/src/electron/renderer/pages/CalibrateFragment.tsx
index 2bf60cea..806bf3e9 100644
--- a/src/electron/renderer/pages/CalibrateFragment.tsx
+++ b/src/electron/renderer/pages/CalibrateFragment.tsx
@@ -32,6 +32,19 @@ const Title = styled.h3`
letter-spacing: 0;
`;
+const FragmentPreviewContainer = styled(Col).attrs({
+ tabIndex: 0,
+ grow: true,
+ scroll: true,
+})<{ isLoading: boolean }>`
+ justify-content: ${(p) => (p.isLoading ? 'center' : 'flex-start')};
+ align-items: center;
+
+ &:focus-visible {
+ outline-offset: -2px;
+ }
+`;
+
interface CalibrateFormValues {
showBoxes: boolean;
testThreshold: number;
@@ -143,14 +156,7 @@ export const CalibrateFragment = ({ entry }: CalibrateFragmentProps) => {
Fragment preview
-
+
{loading ? (
) : (
@@ -161,7 +167,7 @@ export const CalibrateFragment = ({ entry }: CalibrateFragmentProps) => {
format={entry.settings.format}
/>
)}
-
+
);
diff --git a/src/electron/renderer/pages/History.tsx b/src/electron/renderer/pages/History.tsx
index 83b5a4fb..cf1a89dd 100644
--- a/src/electron/renderer/pages/History.tsx
+++ b/src/electron/renderer/pages/History.tsx
@@ -11,7 +11,7 @@ import {
} from 'react-router-dom';
import styled from 'styled-components';
import { transformTimestamp } from '../common';
-import { Col } from '../components';
+import { Col, List, ListItem, ListItemLink } from '../components';
import { StateContext } from '../state';
import { HistoryDetails } from './HistoryDetails';
@@ -102,23 +102,23 @@ export const History = () => {
return (
-
+
{history.map((e) => {
const { time, distance } = transformTimestamp(e.startedAt);
return (
-
-
+
+
{distance}
{time}
-
-
+
+
);
})}
-
+
diff --git a/src/electron/renderer/styles/global.tsx b/src/electron/renderer/styles/global.tsx
index c19d65cf..f63df516 100644
--- a/src/electron/renderer/styles/global.tsx
+++ b/src/electron/renderer/styles/global.tsx
@@ -13,6 +13,7 @@ export const GlobalStyles = createGlobalStyle`
--accent-darker: #213d3d;
--success: #1bd676;
--disabled: #bb9a95;
+ --outline: #ffb46b;
}
html,
@@ -44,4 +45,9 @@ export const GlobalStyles = createGlobalStyle`
::-webkit-scrollbar-thumb {
background: var(--primary);
}
+
+ :focus-visible {
+ outline: 2px solid var(--outline);
+ outline-offset: 2px;
+ }
`;