Skip to content

Commit

Permalink
Merge pull request #2905 from serlo/2893-editor-use-autofocus-attribu…
Browse files Browse the repository at this point in the history
…te-in-templates-and-modals

feat(editor): improve autofocus
  • Loading branch information
elbotho authored Sep 21, 2023
2 parents 2a2102f + b27db93 commit f8a882a
Show file tree
Hide file tree
Showing 17 changed files with 109 additions and 28 deletions.
4 changes: 4 additions & 0 deletions src/serlo-editor/core/sub-document/editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ export function SubDocumentEditor({ id, pluginProps }: SubDocumentProps) {
const parent = selectChildTreeOfParent(store.getState(), id)
if (parent) dispatch(focus(parent.id))
} else {
// prevents parents from stealing focus of children
if (document?.plugin === 'exercise') return

// default focus dispatch
dispatch(focus(id))
}
}
Expand Down
1 change: 1 addition & 0 deletions src/serlo-editor/plugins/anchor/editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const AnchorEditor = (props: AnchorProps) => {
<label className="serlo-tooltip-trigger">
<EditorTooltip text={editorStrings.plugins.anchor.anchorId} />
<input
autoFocus
placeholder={editorStrings.plugins.anchor.identifier}
value={state.value}
onChange={(e) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { RefObject } from 'react'

import type { ImageProps } from '..'
import { useEditorStrings } from '@/contexts/logged-in-data-context'
import { tw } from '@/helper/tw'
import { isTempFile } from '@/serlo-editor/plugin'

export function InlineSrcControls({ state }: ImageProps) {
export function InlineSrcControls({
state,
urlInputRef,
}: ImageProps & { urlInputRef: RefObject<HTMLInputElement> }) {
const imageStrings = useEditorStrings().plugins.image
const { src } = state

Expand All @@ -18,6 +23,7 @@ export function InlineSrcControls({ state }: ImageProps) {
<label>
<b>{imageStrings.imageUrl}</b>
<input
ref={urlInputRef}
placeholder={placeholder}
value={!isTempFile(src.value) ? src.value : ''}
disabled={isTempFile(src.value) && !src.value.failed}
Expand Down
27 changes: 20 additions & 7 deletions src/serlo-editor/plugins/image/editor.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { faImages } from '@fortawesome/free-solid-svg-icons'
import { useEffect, useState } from 'react'
import { useEffect, useRef, useState } from 'react'

import type { ImageProps } from '.'
import { InlineSrcControls } from './controls/inline-src-controls'
Expand Down Expand Up @@ -38,6 +38,8 @@ export function ImageEditor(props: ImageProps) {
// eslint-disable-next-line @typescript-eslint/no-base-to-string
const src = state.src.value.toString()

const urlInputRef = useRef<HTMLInputElement>(null)

useEffect(() => {
if (editable && !state.caption.defined) {
state.caption.create({ plugin: EditorPluginType.Text })
Expand All @@ -50,6 +52,17 @@ export function ImageEditor(props: ImageProps) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [editable, focused])

useEffect(() => {
// manually set focus to url after creating plugin
if (editable && focused) {
setTimeout(() => {
urlInputRef.current?.focus()
})
}
// only on first mount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

return (
<>
{hasFocus ? (
Expand All @@ -64,6 +77,12 @@ export function ImageEditor(props: ImageProps) {
className="relative z-[2] [&_img]:min-h-[4rem]"
data-qa="plugin-image-editor"
>
{hasFocus && showInlineImageUrl ? (
<div className="absolute left-side top-side z-[3]">
<InlineSrcControls {...props} urlInputRef={urlInputRef} />
</div>
) : null}

<ImageRenderer
image={{
src,
Expand All @@ -75,12 +94,6 @@ export function ImageEditor(props: ImageProps) {
placeholder={renderPlaceholder()}
forceNewTab
/>

{hasFocus && showInlineImageUrl ? (
<div className="absolute left-side top-side">
<InlineSrcControls {...props} />
</div>
) : null}
</div>
</>
)
Expand Down
1 change: 1 addition & 0 deletions src/serlo-editor/plugins/serlo-template-plugins/applet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ function AppletTypeEditor(props: EditorPluginProps<AppletTypePluginState>) {
<h1 className="serlo-h1 mt-20">
{props.editable ? (
<input
autoFocus
className={headerInputClasses}
placeholder={appletStrings.placeholder}
value={title.value}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ function ArticleTypeEditor(props: EditorPluginProps<ArticleTypePluginState>) {
<h1 className="serlo-h1 mt-20" itemProp="name">
{props.editable ? (
<input
autoFocus
className={headerInputClasses}
placeholder={articleStrings.title}
value={title.value}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect } from 'react'
import { useEffect, useRef } from 'react'

import {
entity,
Expand All @@ -13,6 +13,7 @@ import {
type EditorPluginProps,
string,
} from '@/serlo-editor/plugin'
import { focus, useAppDispatch } from '@/serlo-editor/store'

export const coursePageTypeState = entityType(
{
Expand All @@ -39,12 +40,27 @@ function CoursePageTypeEditor(
props: EditorPluginProps<CoursePageTypePluginState, { skipControls: boolean }>
) {
const { title, content, icon } = props.state
const titleRef = useRef<HTMLInputElement>(null)

const dispatch = useAppDispatch()

useEffect(() => {
// setting not used any more, reset to explanation for now
if (icon.value !== 'explanation') icon.set('explanation')
})

useEffect(() => {
if (props.editable) {
// focus on title, remove focus from content
setTimeout(() => {
dispatch(focus(null))
titleRef.current?.focus()
})
}
// only after creating plugin
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

const placeholder = useEditorStrings().templatePlugins.coursePage.title

return (
Expand All @@ -53,6 +69,8 @@ function CoursePageTypeEditor(
<h1 className="serlo-h1 mt-12">
{props.editable ? (
<input
ref={titleRef}
autoFocus
className={headerInputClasses}
placeholder={placeholder}
value={title.value}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ function CourseTypeEditor(props: EditorPluginProps<CourseTypePluginState>) {
title={
props.editable ? (
<input
autoFocus
className={tw`
-ml-2 mt-1 min-w-[70%] rounded-xl border-2 border-transparent
bg-editor-primary-100 px-2 py-0 focus:border-editor-primary focus:outline-none
Expand Down
1 change: 1 addition & 0 deletions src/serlo-editor/plugins/serlo-template-plugins/event.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ function EventTypeEditor(props: EditorPluginProps<EventTypePluginState>) {
<h1 className="serlo-h1 mt-20">
{props.editable ? (
<input
autoFocus
className={headerInputClasses}
placeholder={placeholder}
value={title.value}
Expand Down
1 change: 1 addition & 0 deletions src/serlo-editor/plugins/serlo-template-plugins/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ function PageTypeEditor(props: EditorPluginProps<PageTypePluginState>) {
<h1 className="serlo-h1" itemProp="name">
{props.editable ? (
<input
autoFocus
className={headerInputClasses}
placeholder={placeholder}
value={title.value}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ function TaxonomyTypeEditor(props: EditorPluginProps<TaxonomyTypePluginState>) {
<h1 className="serlo-h1" itemProp="name">
{props.editable ? (
<input
autoFocus
className={headerInputClasses}
placeholder={editorStrings.taxonomy.title}
value={term.name.value}
Expand Down
1 change: 1 addition & 0 deletions src/serlo-editor/plugins/serlo-template-plugins/video.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ function VideoTypeEditor(props: EditorPluginProps<VideoTypePluginState>) {
<h1 className="serlo-h1 mt-32">
{props.editable ? (
<input
autoFocus
className={headerInputClasses}
placeholder={editorStrings.plugins.video.titlePlaceholder}
value={title.value}
Expand Down
10 changes: 5 additions & 5 deletions src/serlo-editor/store/documents/matchers.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { AnyAction } from '@reduxjs/toolkit'

import { pureInsertDocument } from './slice'
import type { PureInsertDocumentAction } from './types'
import { insertAndFocusDocument } from './slice'
import type { InsertAndFocusDocumentAction } from './types'

export function isPureInsertDocumentAction(
export function isInsertAndFocusDocumentAction(
action: AnyAction
): action is PureInsertDocumentAction {
return action.type === pureInsertDocument.type
): action is InsertAndFocusDocumentAction {
return action.type === insertAndFocusDocument.type
}
33 changes: 22 additions & 11 deletions src/serlo-editor/store/documents/saga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
pureRemoveDocument,
pureReplaceDocument,
runReplaceDocumentSaga,
insertAndFocusDocument,
} from '.'
import type { ReversibleAction } from '..'
import {
Expand Down Expand Up @@ -42,7 +43,9 @@ function* changeDocumentSaga(action: ReturnType<typeof runChangeDocumentSaga>) {
handleRecursiveInserts,
(helpers: StoreDeserializeHelpers) => {
return stateHandler.initial(document.state, helpers)
}
},
[],
true // shouldFocusInsertedDocument
)

const createChange = (state: unknown): ReversibleAction => {
Expand Down Expand Up @@ -118,7 +121,9 @@ function* changeDocumentSaga(action: ReturnType<typeof runChangeDocumentSaga>) {
handleRecursiveInserts,
(helpers: StoreDeserializeHelpers) => {
return updater(currentDocument.state, helpers)
}
},
[],
true // shouldFocusInsertedDocument
)
payload.callback(resolveActions, pureResolveState)
if (payload.resolve || payload.reject) {
Expand Down Expand Up @@ -164,7 +169,8 @@ function* replaceDocumentSaga(
const [actions]: [ReversibleAction[], unknown] = yield call(
handleRecursiveInserts,
() => {},
pendingDocs
pendingDocs,
true // shouldFocusInsertedDocument
)

const reversibleAction: ReversibleAction = {
Expand All @@ -191,7 +197,8 @@ interface ChannelAction {

export function* handleRecursiveInserts(
act: (helpers: StoreDeserializeHelpers) => unknown,
initialDocuments: { id: string; plugin: string; state?: unknown }[] = []
initialDocuments: { id: string; plugin: string; state?: unknown }[] = [],
shouldFocusInsertedDocument: boolean = false
) {
const actions: ReversibleAction[] = []
const pendingDocs: {
Expand All @@ -205,19 +212,20 @@ export function* handleRecursiveInserts(
},
}
const result = act(helpers)

for (let doc; (doc = pendingDocs.pop()); ) {
const plugin = editorPlugins.getByType(doc.plugin)
if (!plugin) {
// eslint-disable-next-line no-console
console.warn(`Invalid plugin '${doc.plugin}'`)
continue
}
let state: unknown
if (doc.state === undefined) {
state = plugin.state.createInitialState(helpers)
} else {
state = plugin.state.deserialize(doc.state, helpers)
}

const state: unknown =
doc.state === undefined
? plugin.state.createInitialState(helpers)
: plugin.state.deserialize(doc.state, helpers)

// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const currentDocument: ReturnType<typeof selectDocument> = yield select(
selectDocument,
Expand All @@ -237,8 +245,11 @@ export function* handleRecursiveInserts(
}),
})
} else {
const action = shouldFocusInsertedDocument
? insertAndFocusDocument
: pureInsertDocument
actions.push({
action: pureInsertDocument({
action: action({
id: doc.id,
plugin: doc.plugin,
state,
Expand Down
12 changes: 12 additions & 0 deletions src/serlo-editor/store/documents/slice.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createSlice } from '@reduxjs/toolkit'

import type {
InsertAndFocusDocumentAction,
PureChangeDocumentAction,
PureInsertDocumentAction,
PureRemoveDocumentAction,
Expand All @@ -14,6 +15,16 @@ export const documentsSlice = createSlice({
name: 'documents',
initialState,
reducers: {
// The insert action with a side effect:
// Focus the newly inserted document (see `focus` slice)
insertAndFocusDocument(state, action: InsertAndFocusDocumentAction) {
const { id, plugin: type, state: pluginState } = action.payload
state[id] = {
plugin: type,
state: pluginState,
}
},
// The pure insert action (no side effects)
pureInsertDocument(state, action: PureInsertDocumentAction) {
const { id, plugin: type, state: pluginState } = action.payload
state[id] = {
Expand All @@ -40,6 +51,7 @@ export const documentsSlice = createSlice({
})

export const {
insertAndFocusDocument,
pureInsertDocument,
pureRemoveDocument,
pureChangeDocument,
Expand Down
4 changes: 4 additions & 0 deletions src/serlo-editor/store/documents/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { PayloadAction } from '@reduxjs/toolkit'

import type { DocumentState } from '../types'

export type InsertAndFocusDocumentAction = PayloadAction<
{ id: string } & DocumentState
>

export type PureInsertDocumentAction = PayloadAction<
{ id: string } & DocumentState
>
Expand Down
11 changes: 8 additions & 3 deletions src/serlo-editor/store/focus/slice.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'

import { findNextChildTreeNode, findPreviousChildTreeNode } from './helpers'
import { type ChildTreeNode, isPureInsertDocumentAction } from '../documents'
import {
type ChildTreeNode,
isInsertAndFocusDocumentAction,
} from '../documents'
import { State } from '../types'

const initialState: State['focus'] = null as State['focus']
Expand Down Expand Up @@ -29,8 +32,10 @@ export const focusSlice = createSlice({
extraReducers: (builder) => {
builder.addMatcher(
// Always focus the newly inserted document
isPureInsertDocumentAction,
(_state, action) => action.payload.id
isInsertAndFocusDocumentAction,
(_state, action) => {
return action.payload.id
}
)
},
})
Expand Down

0 comments on commit f8a882a

Please sign in to comment.