Skip to content

Commit f74b46a

Browse files
authored
Merge pull request #862 from jsonwebtoken/auto-focus-token-input
Add autofocus for jwt token input
2 parents ca5d46a + 7fc54f4 commit f74b46a

File tree

7 files changed

+178
-6
lines changed

7 files changed

+178
-6
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Checkbox, type CheckboxProps } from "react-aria-components";
2+
import styles from './checkbox.module.scss'
3+
4+
export function CheckboxComponent({
5+
children,
6+
...props
7+
}: Omit<CheckboxProps, "children"> & {
8+
children?: React.ReactNode;
9+
}) {
10+
return (
11+
<Checkbox {...props} className={styles.checkbox__component}>
12+
{({ isIndeterminate }) => (
13+
<>
14+
<div className={styles.checkbox}>
15+
<svg viewBox="0 0 18 18" aria-hidden="true">
16+
{isIndeterminate ? (
17+
<rect x={1} y={7.5} width={15} height={3} />
18+
) : (
19+
<polyline points="1 9 7 14 15 4" />
20+
)}
21+
</svg>
22+
</div>
23+
{children}
24+
</>
25+
)}
26+
</Checkbox>
27+
);
28+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
.checkbox__component {
2+
--selected-color: var(--color_bg_state_success);
3+
--selected-color-pressed: var(--color_fg_on_state_success_subtle);
4+
--checkmark-color: var(--color_border_state_success);
5+
6+
display: flex;
7+
/* This is needed so the HiddenInput is positioned correctly */
8+
position: relative;
9+
align-items: center;
10+
gap: 0.571rem;
11+
font-size: 1.143rem;
12+
color: white;
13+
forced-color-adjust: none;
14+
15+
.checkbox {
16+
width: 1.143rem;
17+
height: 1.143rem;
18+
border: 2px solid var(--color_fg_default);
19+
border-radius: 4px;
20+
transition: all 200ms;
21+
display: flex;
22+
align-items: center;
23+
justify-content: center;
24+
flex-shrink: 0;
25+
}
26+
27+
svg {
28+
width: 1rem;
29+
height: 1rem;
30+
fill: none;
31+
stroke: var(--functional-gray-0);
32+
stroke-width: 3px;
33+
stroke-dasharray: 22px;
34+
stroke-dashoffset: 66;
35+
transition: all 200ms;
36+
}
37+
38+
&[data-focus-visible] .checkbox {
39+
outline: 2px solid var(--color_fg_selected);
40+
outline-offset: 2px;
41+
}
42+
43+
&[data-selected],
44+
&[data-indeterminate] {
45+
.checkbox {
46+
border-color: var(--selected-color);
47+
background: var(--selected-color);
48+
}
49+
50+
&[data-pressed] .checkbox {
51+
border-color: var(--selected-color-pressed);
52+
background: var(--selected-color-pressed);
53+
}
54+
55+
svg {
56+
stroke-dashoffset: 44;
57+
}
58+
}
59+
60+
&[data-indeterminate] {
61+
& svg {
62+
stroke: none;
63+
fill: var(--checkmark-color);
64+
}
65+
}
66+
}

src/features/common/components/code-editor/editor.component.tsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ type Props = React.HTMLAttributes<HTMLDivElement> & {
1818
textareaId?: string;
1919
textareaClassName?: string;
2020
autoFocus?: boolean;
21+
focusOnWindowFocus?: boolean;
2122
disabled?: boolean;
2223
form?: string;
2324
maxLength?: number;
@@ -87,6 +88,21 @@ export class EditorComponent extends React.Component<Props, State> {
8788

8889
componentDidMount() {
8990
this._recordCurrentState();
91+
if (this.props.focusOnWindowFocus) {
92+
window.addEventListener("focus", this._focusInput);
93+
}
94+
}
95+
96+
componentDidUpdate(prevProps: Readonly<Props>): void {
97+
if (prevProps.focusOnWindowFocus !== this.props.focusOnWindowFocus) {
98+
this.props.focusOnWindowFocus
99+
? window.addEventListener("focus", this._focusInput)
100+
: window.removeEventListener("focus", this._focusInput);
101+
}
102+
}
103+
104+
componentWillUnmount() {
105+
window.removeEventListener("focus", this._focusInput);
90106
}
91107

92108
private _recordCurrentState = () => {
@@ -161,6 +177,13 @@ export class EditorComponent extends React.Component<Props, State> {
161177
this._history.offset++;
162178
};
163179

180+
private _focusInput = () => {
181+
const input = this._input;
182+
183+
if (!input) return;
184+
input.focus();
185+
};
186+
164187
private _updateInput = (record: Record) => {
165188
const input = this._input;
166189

@@ -466,7 +489,7 @@ export class EditorComponent extends React.Component<Props, State> {
466489
selectionStart,
467490
selectionEnd,
468491
},
469-
true,
492+
true
470493
);
471494

472495
this.props.onValueChange(value);
@@ -516,6 +539,7 @@ export class EditorComponent extends React.Component<Props, State> {
516539
insertSpaces,
517540
ignoreTabKey,
518541
preClassName,
542+
focusOnWindowFocus,
519543
...rest
520544
} = this.props;
521545

src/features/debugger/components/debugger-toolbar/debugger-toolbar.component.tsx

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from "react";
1+
import React, { useEffect, useRef } from "react";
22
import styles from "./debugger-toolbar.module.scss";
33
import { BoxComponent } from "@/features/common/components/box/box.component";
44
import { useDebuggerStore } from "@/features/debugger/services/debugger.store";
@@ -18,9 +18,22 @@ interface DebuggerToolbarComponentProps {
1818
export const DebuggerToolbarComponent: React.FC<
1919
DebuggerToolbarComponentProps
2020
> = ({ decoderDictionary, encoderDictionary, mode }) => {
21+
const tabRefs = useRef<Array<HTMLLIElement | null>>([]);
2122
const activeWidget$ = useDebuggerStore((state) => state.activeWidget$);
22-
2323
const setActiveWidget$ = useDebuggerStore((state) => state.setActiveWidget$);
24+
const isDecoder = activeWidget$ === DebuggerWidgetValues.DECODER;
25+
26+
const handleKeyDown = (e: React.KeyboardEvent<HTMLLIElement>) => {
27+
const { key } = e;
28+
29+
if (key == "ArrowRight" || key == "ArrowLeft") {
30+
setActiveWidget$(
31+
isDecoder ? DebuggerWidgetValues.ENCODER : DebuggerWidgetValues.DECODER
32+
);
33+
e.preventDefault();
34+
}
35+
tabRefs.current[isDecoder ? 0 : 1]?.focus();
36+
};
2437

2538
if (mode === DebuggerModeValues.UNIFIED) {
2639
return (
@@ -40,8 +53,15 @@ export const DebuggerToolbarComponent: React.FC<
4053
onClick={() => {
4154
setActiveWidget$(DebuggerWidgetValues.DECODER);
4255
}}
56+
onKeyDown={handleKeyDown}
4357
data-active={activeWidget$ === DebuggerWidgetValues.DECODER}
4458
data-testid={dataTestidDictionary.debugger.decoderTab.id}
59+
aria-selected={activeWidget$ === DebuggerWidgetValues.DECODER}
60+
aria-controls={`${DebuggerWidgetValues.DECODER}-panel`}
61+
ref={(el) => {
62+
tabRefs.current[0] = el;
63+
}}
64+
tabIndex={activeWidget$ === DebuggerWidgetValues.DECODER ? 0 : -1}
4565
>
4666
<span className={styles.titleTab__compactLabel}>
4767
{decoderDictionary.compactTitle}
@@ -53,11 +73,18 @@ export const DebuggerToolbarComponent: React.FC<
5373
<li
5474
role="tab"
5575
className={styles.titleTab}
76+
onKeyDown={handleKeyDown}
5677
onClick={() => {
5778
setActiveWidget$(DebuggerWidgetValues.ENCODER);
5879
}}
5980
data-active={activeWidget$ === DebuggerWidgetValues.ENCODER}
6081
data-testid={dataTestidDictionary.debugger.encoderTab.id}
82+
aria-selected={activeWidget$ === DebuggerWidgetValues.ENCODER}
83+
aria-controls={`${DebuggerWidgetValues.ENCODER}-panel`}
84+
ref={(el) => {
85+
tabRefs.current[1] = el;
86+
}}
87+
tabIndex={activeWidget$ === DebuggerWidgetValues.ENCODER ? 0 : -1}
6188
>
6289
<span className={styles.titleTab__compactLabel}>
6390
{encoderDictionary.compactTitle}

src/features/decoder/components/jwt-editor.component.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,20 @@ import { EditorComponent } from "@/features/common/components/code-editor/editor
44
interface JwtEditorComponentProps {
55
token: string;
66
handleJwtChange: (value: string) => void;
7+
autoFocus: boolean
78
}
89

910
export const JwtEditorComponent: React.FC<JwtEditorComponentProps> = ({
1011
token,
12+
autoFocus,
1113
handleJwtChange,
1214
}) => {
1315
return (
1416
<EditorComponent
1517
aria-label="JWT editor input"
1618
value={token}
19+
autoFocus={autoFocus}
20+
focusOnWindowFocus={autoFocus}
1721
onValueChange={(code) => handleJwtChange(code)}
1822
highlight={(code) => {
1923
if (!code) {

src/features/decoder/components/jwt-input.component.tsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { CardToolbarComponent } from "@/features/common/components/card-toolbar/
1414
import { CardToolbarCopyButtonComponent } from "@/features/common/components/card-toolbar-buttons/card-toolbar-copy-button/card-toolbar-copy-button.component";
1515
import { CardToolbarClearButtonComponent } from "@/features/common/components/card-toolbar-buttons/card-toolbar-clear-button/card-toolbar-clear-button.component";
1616
import styles from "./jwt-input.module.scss";
17+
import { CheckboxComponent } from "@/features/common/components/checkbox/checkbox.component";
1718

1819
type JwtInputComponentProps = {
1920
languageCode: string;
@@ -24,14 +25,18 @@ export const JwtInputComponent: React.FC<JwtInputComponentProps> = ({
2425
languageCode,
2526
dictionary,
2627
}) => {
28+
const [autoFocusEnabled, setAutofocusEnabled] = useState(() => {
29+
const saved = localStorage.getItem("autofocus-enabled");
30+
return saved ? !!JSON.parse(saved) : false
31+
});
2732
const handleJwtChange$ = useDecoderStore((state) => state.handleJwtChange);
2833
const jwt$ = useDecoderStore((state) => state.jwt);
2934
const decodeErrors$ = useDecoderStore((state) => state.decodingErrors);
3035

3136
const decoderInputs$ = useDebuggerStore((state) => state.decoderInputs$);
3237

3338
const [token, setToken] = useState<string>(
34-
decoderInputs$.jwt || DEFAULT_JWT.token,
39+
decoderInputs$.jwt || DEFAULT_JWT.token
3540
);
3641

3742
const clearValue = async () => {
@@ -46,13 +51,23 @@ export const JwtInputComponent: React.FC<JwtInputComponentProps> = ({
4651
handleJwtChange$(cleanValue);
4752
};
4853

54+
const handleCheckboxChange = (selected: boolean) => {
55+
localStorage.setItem("autofocus-enabled", JSON.stringify(selected))
56+
setAutofocusEnabled(selected)
57+
}
58+
4959
useEffect(() => {
5060
setToken(jwt$);
5161
}, [jwt$]);
5262

5363
return (
5464
<>
55-
<span className={styles.headline}>{dictionary.headline}</span>
65+
<div style={{ display: "flex", justifyContent: "space-between" }}>
66+
<span className={styles.headline}>{dictionary.headline}</span>
67+
<CheckboxComponent isSelected={autoFocusEnabled} onChange={e => handleCheckboxChange(e)}>
68+
<span className={styles.checkbox__label}>Enable auto-focus</span>
69+
</CheckboxComponent>
70+
</div>
5671
<CardComponent
5772
id={dataTestidDictionary.decoder.jwtEditor.id}
5873
languageCode={languageCode}
@@ -84,7 +99,7 @@ export const JwtInputComponent: React.FC<JwtInputComponentProps> = ({
8499
),
85100
}}
86101
>
87-
<JwtEditorComponent token={token} handleJwtChange={handleJwtChange} />
102+
<JwtEditorComponent token={token} handleJwtChange={handleJwtChange} autoFocus={autoFocusEnabled}/>
88103
</CardComponent>
89104
</>
90105
);

src/features/decoder/components/jwt-input.module.scss

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,11 @@
1010
font-weight: 500;
1111
letter-spacing: 0.24px;
1212
}
13+
14+
.checkbox__label {
15+
color: var(--color_fg_default);
16+
font-size: 0.875rem;
17+
line-height: 1.375rem;
18+
font-weight: 500;
19+
letter-spacing: 0.24px;
20+
}

0 commit comments

Comments
 (0)