Skip to content

Commit c5dd861

Browse files
committed
Refine token prompt interaction flow
1 parent 1185771 commit c5dd861

4 files changed

Lines changed: 133 additions & 22 deletions

File tree

frontend/src/__tests__/App.test.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ describe('App', () => {
100100
});
101101
});
102102

103-
it('shows inline token prompt when submitting without a token', () => {
103+
it('shows inline token prompt when submitting without a token', async () => {
104104
render(<App />);
105105

106106
fireEvent.input(screen.getByLabelText('Page URL'), {
@@ -109,6 +109,15 @@ describe('App', () => {
109109
fireEvent.click(screen.getByRole('button', { name: 'Generate feed URL' }));
110110

111111
expect(screen.getByText('Add access token')).toBeInTheDocument();
112+
expect(screen.getByLabelText('Page URL')).toBeDisabled();
113+
expect(screen.getByRole('combobox')).toBeDisabled();
114+
expect(screen.queryByRole('button', { name: 'More' })).not.toBeInTheDocument();
115+
expect(screen.getByRole('link', { name: 'Set up your own instance with Docker.' })).toBeInTheDocument();
116+
expect(screen.getByText('This instance needs an access token.')).toBeInTheDocument();
117+
expect(screen.queryByText('Paste an access token to keep going.')).not.toBeInTheDocument();
118+
await waitFor(() => {
119+
expect(document.activeElement).toBe(document.getElementById('access-token'));
120+
});
112121
expect(mockConvertFeed).not.toHaveBeenCalled();
113122
});
114123

frontend/src/components/App.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export function App() {
9393

9494
if (feedCreation.access_token_required && !accessToken) {
9595
setShowTokenPrompt(true);
96-
setTokenError('Paste an access token to keep going.');
96+
setTokenError('');
9797
return false;
9898
}
9999

@@ -197,6 +197,7 @@ export function App() {
197197
strategyHint={strategyHint}
198198
/>
199199
<UtilityStrip
200+
hidden={showTokenPrompt}
200201
hasAccessToken={hasToken}
201202
onClearToken={() => {
202203
clearToken();

frontend/src/components/AppPanels.tsx

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export function CreateFeedPanel({
6565
}: CreateFeedPanelProps) {
6666
const selectedStrategy = strategies.find((strategy) => strategy.id === feedFormData.strategy);
6767
const urlInputRef = useRef<HTMLInputElement | null>(null);
68+
const tokenInputRef = useRef<HTMLInputElement | null>(null);
6869
const strategyOptionLabel = (strategy: Strategy) => {
6970
if (strategy.id === 'ssrf_filter') return 'Standard rendering';
7071
if (strategy.id === 'browserless') return 'JavaScript pages';
@@ -85,9 +86,19 @@ export function CreateFeedPanel({
8586
return () => window.cancelAnimationFrame(focusHandle);
8687
}, [focusComposerKey]);
8788

89+
useLayoutEffect(() => {
90+
if (!showTokenPrompt || !tokenInputRef.current || typeof window === 'undefined') return;
91+
92+
const focusHandle = window.requestAnimationFrame(() => {
93+
tokenInputRef.current?.focus();
94+
});
95+
96+
return () => window.cancelAnimationFrame(focusHandle);
97+
}, [showTokenPrompt]);
98+
8899
return (
89100
<form class="form-shell form-shell--minimal" onSubmit={onFeedSubmit}>
90-
<div class="field-stack field-stack--dense">
101+
<div class={`field-stack field-stack--dense${showTokenPrompt ? ' field-stack--inactive' : ''}`}>
91102
<DominantField
92103
id="url"
93104
label="Page URL"
@@ -98,7 +109,7 @@ export function CreateFeedPanel({
98109
inputRef={urlInputRef}
99110
actionLabel={isConverting ? 'Generating feed URL' : 'Generate feed URL'}
100111
actionText={isConverting ? '...' : '>'}
101-
disabled={isConverting || !feedCreationEnabled}
112+
disabled={isConverting || !feedCreationEnabled || showTokenPrompt}
102113
error={feedFieldErrors.url}
103114
onInput={(event) => onFeedFieldChange('url', (event.target as HTMLInputElement).value)}
104115
/>
@@ -109,7 +120,7 @@ export function CreateFeedPanel({
109120
name="strategy"
110121
class="input input--select input--subtle"
111122
value={feedFormData.strategy}
112-
disabled={strategiesLoading}
123+
disabled={strategiesLoading || showTokenPrompt}
113124
onChange={(event) => onFeedFieldChange('strategy', (event.target as HTMLSelectElement).value)}
114125
>
115126
{strategiesLoading ? (
@@ -137,7 +148,7 @@ export function CreateFeedPanel({
137148
<div class="token-gate" role="group" aria-label="Access token">
138149
<div class="token-gate__copy">
139150
<h2>Add access token</h2>
140-
<p class="field-help">Paste it once and continue.</p>
151+
<p class="token-gate__hint">This instance needs an access token.</p>
141152
</div>
142153
<label class="field-block field-block--token" htmlFor="access-token">
143154
<span class="field-label field-label--ghost">Access token</span>
@@ -149,6 +160,7 @@ export function CreateFeedPanel({
149160
aria-label="Access token"
150161
placeholder="Paste access token"
151162
autocomplete="off"
163+
ref={tokenInputRef}
152164
value={tokenDraft}
153165
onKeyDown={(event) => {
154166
if (event.key !== 'Enter') return;
@@ -160,11 +172,21 @@ export function CreateFeedPanel({
160172
/>
161173
<span class="field-error">{tokenError}</span>
162174
</label>
175+
<a
176+
href="https://html2rss.github.io/web-application/getting-started/"
177+
target="_blank"
178+
rel="noopener noreferrer"
179+
class="token-gate__nudge token-gate__nudge-link"
180+
>
181+
Set up your own instance with Docker.
182+
</a>
163183
<div class="token-gate__actions">
164-
<button type="button" class="btn btn--primary" onClick={onSaveToken}>
184+
<button type="button" class="btn btn--ghost" onClick={onSaveToken}>
165185
Save and continue
166186
</button>
167-
<button type="button" class="btn btn--ghost" onClick={onCancelTokenPrompt}>
187+
</div>
188+
<div class="token-gate__back">
189+
<button type="button" class="btn btn--quiet btn--linkish" onClick={onCancelTokenPrompt}>
168190
Back
169191
</button>
170192
</div>
@@ -188,13 +210,16 @@ export function CreateFeedPanel({
188210
}
189211

190212
interface UtilityStripProps {
213+
hidden?: boolean;
191214
hasAccessToken: boolean;
192215
onClearToken: () => void;
193216
}
194217

195-
export function UtilityStrip({ hasAccessToken, onClearToken }: UtilityStripProps) {
218+
export function UtilityStrip({ hidden = false, hasAccessToken, onClearToken }: UtilityStripProps) {
196219
const [isOpen, setIsOpen] = useState(false);
197220

221+
if (hidden) return null;
222+
198223
return (
199224
<section class="utility-strip" aria-label="Utilities">
200225
<button
@@ -208,14 +233,6 @@ export function UtilityStrip({ hasAccessToken, onClearToken }: UtilityStripProps
208233
{isOpen && (
209234
<div class="utility-strip__items">
210235
<Bookmarklet />
211-
<a
212-
href="https://html2rss.github.io/"
213-
target="_blank"
214-
rel="noopener noreferrer"
215-
class="utility-link"
216-
>
217-
Getting started
218-
</a>
219236
<a
220237
href="https://github.com/html2rss/html2rss-web"
221238
target="_blank"

frontend/src/styles/main.css

Lines changed: 89 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,10 @@ textarea {
198198
gap: var(--space-4);
199199
}
200200

201+
.field-stack--inactive {
202+
opacity: 0.34;
203+
}
204+
201205
.result-shell {
202206
gap: var(--space-3);
203207
}
@@ -320,6 +324,11 @@ a:focus-visible {
320324
padding: 0;
321325
}
322326

327+
.input:disabled,
328+
.input--select:disabled {
329+
cursor: not-allowed;
330+
}
331+
323332
.dominant-field__action {
324333
position: absolute;
325334
right: 0.7rem;
@@ -473,30 +482,98 @@ a:focus-visible {
473482
width: min(100%, 38rem);
474483
margin: 0 auto;
475484
padding-top: var(--space-2);
485+
gap: var(--space-3);
476486
}
477487

478488
.token-gate__copy {
479489
display: grid;
480-
gap: var(--space-1);
481-
justify-items: center;
482-
text-align: center;
490+
gap: 0;
491+
justify-items: start;
492+
text-align: left;
483493
}
484494

485495
.token-gate__copy h2 {
486496
margin: 0;
487497
color: var(--text-strong);
488498
font-family: var(--font-family-display);
489-
font-size: clamp(1.2rem, 3vw, 1.65rem);
499+
font-size: clamp(1.08rem, 2.6vw, 1.32rem);
500+
font-weight: 600;
490501
line-height: var(--line-height-tight);
491502
}
492503

504+
.token-gate__hint {
505+
margin: 0.35rem 0 0;
506+
color: var(--text-muted);
507+
font-size: var(--font-size-0);
508+
line-height: 1.45;
509+
}
510+
493511
.token-gate__actions,
494512
.result-actions {
495513
display: grid;
496514
gap: var(--space-3);
497515
justify-items: center;
498516
}
499517

518+
.token-gate__actions {
519+
justify-items: end;
520+
margin-top: calc(var(--space-1) * -1);
521+
}
522+
523+
.token-gate__actions .btn {
524+
min-height: 2.55rem;
525+
padding-inline: 0.95rem;
526+
border-color: rgba(255, 255, 255, 0.08);
527+
background: rgba(255, 255, 255, 0.025);
528+
color: var(--text-body);
529+
font-size: 0.98rem;
530+
font-weight: 550;
531+
}
532+
533+
.token-gate__back {
534+
margin-top: calc(var(--space-3) * -0.4);
535+
justify-self: start;
536+
}
537+
538+
.token-gate__nudge {
539+
margin: 0;
540+
color: var(--text-muted);
541+
font-size: var(--font-size-0);
542+
line-height: 1.5;
543+
}
544+
545+
.token-gate__nudge-link {
546+
text-decoration: none;
547+
color: rgba(255, 255, 255, 0.72);
548+
}
549+
550+
.token-gate__nudge-link:hover,
551+
.token-gate__nudge-link:focus-visible {
552+
color: var(--text-body);
553+
text-decoration: underline;
554+
text-underline-offset: 0.16em;
555+
}
556+
557+
.field-block--token {
558+
justify-items: stretch;
559+
gap: 0.5rem;
560+
}
561+
562+
.field-block--token .input {
563+
min-height: 3.5rem;
564+
padding-inline: 1.1rem;
565+
border-color: rgba(255, 255, 255, 0.12);
566+
background: rgba(255, 255, 255, 0.03);
567+
}
568+
569+
.field-block--token .input::placeholder {
570+
color: rgba(255, 255, 255, 0.28);
571+
}
572+
573+
.field-block--token .field-error {
574+
text-align: left;
575+
}
576+
500577
.result-copy {
501578
width: min(100%, 40rem);
502579
margin: 0 auto;
@@ -617,7 +694,6 @@ a:focus-visible {
617694
padding-top: clamp(1rem, 6vh, 4rem);
618695
}
619696

620-
.token-gate__actions,
621697
.result-actions {
622698
grid-template-columns: repeat(2, minmax(0, auto));
623699
}
@@ -653,6 +729,14 @@ a:focus-visible {
653729
gap: var(--space-3);
654730
}
655731

732+
.token-gate__actions {
733+
justify-items: stretch;
734+
}
735+
736+
.token-gate__actions .btn {
737+
width: 100%;
738+
}
739+
656740
.field-block--select {
657741
gap: 0;
658742
}

0 commit comments

Comments
 (0)