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

Focus management #254

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
5 changes: 3 additions & 2 deletions src/electron/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
Menu,
MenuItemConstructorOptions,
MessageBoxOptions,
PopupOptions,
shell,
Tray,
} from 'electron';
Expand Down Expand Up @@ -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() {
Expand Down
8 changes: 7 additions & 1 deletion src/electron/renderer/components/File.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -85,8 +86,13 @@ export const File = ({ accept }: FileProps) => {
) : (
<FilePathEmpty>No file selected</FilePathEmpty>
)}
<VisuallyHiddenInput
type="file"
accept={accept}
id={name}
onChange={onChange}
/>
<FileLabel htmlFor={name}>Change</FileLabel>
<input type="file" accept={accept} id={name} onChange={onChange} hidden />
</FileWrapper>
);
};
4 changes: 3 additions & 1 deletion src/electron/renderer/components/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -144,6 +144,8 @@ export const Field = forwardRef<HTMLDivElement, PropsWithChildren<FieldProps>>(
ref={ref}
onMouseEnter={onHover ? () => onHover(name) : undefined}
onMouseLeave={onHover ? () => onHover(null) : undefined}
onFocus={onHover ? () => onHover(name) : undefined}
onBlur={onHover ? () => onHover(null) : undefined}
>
<FieldContext.Provider
value={{
Expand Down
2 changes: 1 addition & 1 deletion src/electron/renderer/components/FragmentPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,5 @@ export const FragmentPreview = (props: FragmentPreviewProps) => {
});
}, [props.image, props.showBoxes]);

return <canvas ref={ref} style={{ alignSelf: 'flex-start' }} />;
return <canvas ref={ref} style={{ alignSelf: 'flex-start', zIndex: -1 }} />;
};
17 changes: 1 addition & 16 deletions src/electron/renderer/components/KeyBind.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down Expand Up @@ -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);
Expand Down
68 changes: 68 additions & 0 deletions src/electron/renderer/components/List.tsx
Original file line number Diff line number Diff line change
@@ -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);
// }
// `}
// `;
10 changes: 9 additions & 1 deletion src/electron/renderer/components/RangeSlider.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -86,13 +87,20 @@ export function RangeSlider({
}
}

function setValueOnEnter(e: KeyboardEvent) {
if (e.code === VK_ENTER) {
onValueChange(e);
}
}

return (
<RangeWrapper disabled={props.disabled}>
<Range
{...props}
value={displayValue}
onChange={(e) => setDisplayValue(coerceInputValue(e))}
onMouseUp={onValueChange}
onKeyUp={setValueOnEnter}
/>
<RangeValue>{displayValue}</RangeValue>
</RangeWrapper>
Expand Down
6 changes: 5 additions & 1 deletion src/electron/renderer/components/RawDataPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -16,6 +16,10 @@ const RawDataPreviewWrapper = styled.pre`
overflow-y: auto;
margin: 0;
flex-grow: 1;

&:focus-visible {
outline-offset: -2px;
}
`;

// prettier-ignore
Expand Down
71 changes: 71 additions & 0 deletions src/electron/renderer/components/TitleBar.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<TitleBar />, container);
});

const buttons = container.querySelectorAll('button');

expect(buttons.length).toBe(4);
});

it('should send correct events', () => {
act(() => {
render(<TitleBar />, 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);
}
});
});
});
16 changes: 14 additions & 2 deletions src/electron/renderer/components/TitleBar.tsx
Original file line number Diff line number Diff line change
@@ -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 })``;
Expand Down Expand Up @@ -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')};
}
Expand All @@ -71,9 +75,17 @@ const IconButton = styled.button<{ close?: boolean }>`
}
`;

function showHelpMenu(event: MouseEvent<HTMLButtonElement>) {
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(() => (
<StyledTitleBar>
<IconButton onClick={() => api.send('main:show-help-menu')}>
<IconButton onClick={showHelpMenu}>
<MenuIcon></MenuIcon>
</IconButton>
<IconButton onClick={() => api.send('main:minimize')}>
Expand Down
22 changes: 22 additions & 0 deletions src/electron/renderer/components/VisuallyHiddenInput.tsx
Original file line number Diff line number Diff line change
@@ -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;
}
`;
2 changes: 2 additions & 0 deletions src/electron/renderer/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -26,3 +27,4 @@ export * from './Switch';
export * from './ThirdPartyLicensesDialog';
export * from './TitleBar';
export * from './TypesFragmentStatus';
export * from './VisuallyHiddenInput';
2 changes: 1 addition & 1 deletion src/electron/renderer/pages/Calibrate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const Calibrate = () => {

return (
<Col grow>
<Row style={{ gap: '2rem' }}>
<Row style={{ gap: '2rem', margin: '4px 0' }}>
{entry.fragments.map((f) => (
<NavLink key={f.id} to={f.id}>
{fromCamelCase(f.id)}
Expand Down
Loading