Repository: Atypon-OpenSource/manuscripts-body-editor
Pull request #583: CAI-040 - Implementation of the content accessibility elements (LEAN-4433)
Author: login: asouqi
Created: 2025-04-10T19:47:24Z | Merged: 2025-04-29T10:22:14Z

[Design](https://www.figma.com/design/dLmX8y02PYAgEwZRFG8Jip/Designs-for-the-content-accessibility-elements?node-id=16904-11612&t=E1wyS1FTOGyHxI2L-0), and I had to fix the insert of footnote as with new accessibility nodes position will be different



Diff:
diff --git a/package.json b/package.json
index 7c0d4088c..52b5052ca 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "@manuscripts/body-editor",
   "description": "Prosemirror components for editing and viewing manuscripts",
-  "version": "2.8.46",
+  "version": "2.8.47",
   "repository": "github:Atypon-OpenSource/manuscripts-body-editor",
   "license": "Apache-2.0",
   "main": "dist/cjs",
@@ -34,7 +34,7 @@
     "@manuscripts/library": "1.3.13",
     "@manuscripts/style-guide": "2.1.9",
     "@manuscripts/track-changes-plugin": "1.10.4",
-    "@manuscripts/transform": "3.0.55",
+    "@manuscripts/transform": "3.0.56",
     "@popperjs/core": "^2.11.8",
     "astrocite-eutils": "^0.16.4",
     "codemirror": "^5.58.1",
diff --git a/src/commands.ts b/src/commands.ts
index 8412d29e5..366280b34 100644
--- a/src/commands.ts
+++ b/src/commands.ts
@@ -107,7 +107,13 @@ import {
   nearestAncestor,
 } from './lib/helpers'
 import { isDeleted } from './lib/track-changes-utils'
-import { findParentNodeWithId, getChildOfType, isBodyLocked } from './lib/utils'
+import {
+  findParentNodeWithId,
+  getChildOfType,
+  getInsertPos,
+  isBodyLocked,
+} from './lib/utils'
+import { expandAccessibilitySection } from './plugins/accessibility_element'
 import { setCommentSelection } from './plugins/comments'
 import { getEditorProps } from './plugins/editor-props'
 import { searchReplaceKey } from './plugins/search-replace'
@@ -286,40 +292,40 @@ export const createBlock = (
   dispatch?: Dispatch,
   attrs?: Attrs
 ) => {
-  let node
+  let node: ManuscriptNode
   switch (nodeType) {
-    case state.schema.nodes.table_element:
-      node = createAndFillTableElement(state)
+    case schema.nodes.table_element:
+      node = createAndFillTableElement(attrs)
       break
-    case state.schema.nodes.figure_element:
-      node = createAndFillFigureElement(state)
+    case schema.nodes.figure_element:
+      node = createAndFillFigureElement(attrs)
       break
-    case state.schema.nodes.image_element:
-      node = createImageElement(state)
+    case schema.nodes.image_element:
+      node = createImageElement(attrs)
       break
-    case state.schema.nodes.listing_element:
-      node = state.schema.nodes.listing_element.create({}, [
-        state.schema.nodes.listing.create(),
-        createAndFillFigcaptionElement(state),
+    case schema.nodes.listing_element:
+      node = schema.nodes.listing_element.create({}, [
+        schema.nodes.listing.create(),
+        createAndFillFigcaptionElement(),
       ])
       break
-    case state.schema.nodes.equation_element:
-      node = state.schema.nodes.equation_element.create({}, [
-        state.schema.nodes.equation.create(),
+    case schema.nodes.equation_element:
+      node = schema.nodes.equation_element.create({}, [
+        schema.nodes.equation.create(),
       ])
       break
-    case state.schema.nodes.embed:
-      node = state.schema.nodes.embed.create(attrs, [
-        createAndFillFigcaptionElement(state),
-      ])
+    case schema.nodes.embed:
+      node = createEmbedElement(attrs)
       break
     default:
-      node = nodeType.createAndFill(attrs)
+      node = nodeType.createAndFill(attrs) as ManuscriptNode
   }
 
-  const tr = state.tr.insert(position, node as ManuscriptNode)
+  const tr = state.tr.insert(position, node)
+
   if (dispatch) {
     const selection = createSelection(nodeType, position, tr.doc)
+    expandAccessibilitySection(tr, node)
     dispatch(tr.setSelection(selection).scrollIntoView())
   }
 }
@@ -373,7 +379,7 @@ export const insertGeneralTableFootnote = (
   const tr = state.tr
   const footer = insertTableElementFooter(tr, element)
 
-  const pos = footer.pos + 1
+  const pos = footer.pos + 2
   const node = schema.nodes.general_table_footnote.create({}, [
     schema.nodes.paragraph.create(),
   ])
@@ -418,8 +424,9 @@ export const insertTable = (
   if (!pos) {
     return false
   }
-  const node = createAndFillTableElement(state, config)
+  const node = createAndFillTableElement(config)
   const tr = state.tr.insert(pos, node)
+  expandAccessibilitySection(tr, node)
   tr.setSelection(NodeSelection.create(tr.doc, pos)).scrollIntoView()
   dispatch && dispatch(tr)
   return true
@@ -715,7 +722,7 @@ export const insertTableElementFooter = (
     schema.nodes.table_element_footer
   )[0]
   if (footer) {
-    const pos = tr.mapping.map(table[1] + footer.pos + 1)
+    const pos = tr.mapping.map(table[1] + footer.pos)
     if (isDeleted(footer.node)) {
       reinstateNode(tr, footer.node, pos)
     }
@@ -724,7 +731,9 @@ export const insertTableElementFooter = (
       pos,
     }
   }
-  const pos = tr.mapping.map(table[1] + table[0].nodeSize - 2)
+  const pos = tr.mapping.map(
+    getInsertPos(schema.nodes.table_element_footer, table[0], table[1])
+  )
   const node = schema.nodes.table_element_footer.create()
   tr.insert(pos, node)
   return {
@@ -738,6 +747,7 @@ export const insertFootnotesElement = (
   container: [ManuscriptNode, number]
 ) => {
   let pos
+  const node = schema.nodes.footnotes_element.create()
   if (isTableElementNode(container[0])) {
     //table footnote
     const footer = insertTableElementFooter(tr, container)
@@ -746,9 +756,8 @@ export const insertFootnotesElement = (
     //regular footnote
     const section = insertFootnotesSection(tr)
     pos = section.pos + section.node.nodeSize - 1
+    tr.insert(pos, node)
   }
-  const node = schema.nodes.footnotes_element.create()
-  tr.insert(pos, node)
   return [node, pos] as [FootnotesElementNode, number]
 }
 
@@ -1544,7 +1553,7 @@ const DEFAULT_TABLE_CONFIG: TableConfig = {
  * The table can optionally include a header row.
  */
 export const createAndFillTableElement = (
-  state: ManuscriptEditorState,
+  attrs?: Attrs,
   config = DEFAULT_TABLE_CONFIG
 ) => {
   const { numberOfColumns, numberOfRows, includeHeader } = config
@@ -1561,32 +1570,67 @@ export const createAndFillTableElement = (
     tableRows.push(createRow(schema.nodes.table_cell))
   }
 
-  return schema.nodes.table_element.createChecked({}, [
-    createAndFillFigcaptionElement(state),
-    schema.nodes.table.create({}, tableRows),
-    schema.nodes.listing.create(),
-  ])
+  return schema.nodes.table_element.createChecked(
+    {
+      ...attrs,
+      id: generateNodeID(schema.nodes.table_element),
+    },
+    [
+      createAndFillFigcaptionElement(),
+      schema.nodes.table.create({}, tableRows),
+      schema.nodes.alt_text.create(),
+      schema.nodes.long_desc.create(),
+      schema.nodes.listing.create(),
+    ]
+  )
 }
 
-const createAndFillFigureElement = (state: ManuscriptEditorState) =>
-  state.schema.nodes.figure_element.create({}, [
-    state.schema.nodes.figure.create({}, [
-      state.schema.nodes.figcaption.create(),
-    ]),
-    createAndFillFigcaptionElement(state),
-    state.schema.nodes.listing.create(),
-  ])
+const createAndFillFigureElement = (attrs?: Attrs) =>
+  schema.nodes.figure_element.create(
+    {
+      ...attrs,
+      id: generateNodeID(schema.nodes.figure_element),
+    },
+    [
+      schema.nodes.figure.create({}, [schema.nodes.figcaption.create()]),
+      createAndFillFigcaptionElement(),
+      schema.nodes.alt_text.create(),
+      schema.nodes.long_desc.create(),
+      schema.nodes.listing.create(),
+    ]
+  )
 
-const createAndFillFigcaptionElement = (state: ManuscriptEditorState) =>
-  state.schema.nodes.figcaption.create({}, [
-    state.schema.nodes.caption_title.create(),
-    state.schema.nodes.caption.create(),
+const createAndFillFigcaptionElement = () =>
+  schema.nodes.figcaption.create({}, [
+    schema.nodes.caption_title.create(),
+    schema.nodes.caption.create(),
   ])
 
-const createImageElement = (state: ManuscriptEditorState) =>
-  state.schema.nodes.image_element.create({}, [
-    state.schema.nodes.figure.create(),
-  ])
+const createImageElement = (attrs?: Attrs) =>
+  schema.nodes.image_element.create(
+    {
+      ...attrs,
+      id: generateNodeID(schema.nodes.image_element),
+    },
+    [
+      schema.nodes.figure.create(),
+      schema.nodes.alt_text.create(),
+      schema.nodes.long_desc.create(),
+    ]
+  )
+
+const createEmbedElement = (attrs?: Attrs) =>
+  schema.nodes.embed.create(
+    {
+      ...attrs,
+      id: generateNodeID(schema.nodes.embed),
+    },
+    [
+      createAndFillFigcaptionElement(),
+      schema.nodes.alt_text.create(),
+      schema.nodes.long_desc.create(),
+    ]
+  )
 /**
  * This to make sure we get block node
  */
diff --git a/src/configs/editor-plugins.ts b/src/configs/editor-plugins.ts
index 7af85df65..6ebc79191 100644
--- a/src/configs/editor-plugins.ts
+++ b/src/configs/editor-plugins.ts
@@ -27,6 +27,7 @@ import { history } from 'prosemirror-history'
 import { tableEditing } from 'prosemirror-tables'
 
 import keys from '../keys'
+import accessibility_element from '../plugins/accessibility_element'
 import affiliations from '../plugins/affiliations'
 import alt_titles from '../plugins/alt-titles'
 import bibliography from '../plugins/bibliography'
@@ -82,6 +83,7 @@ export default (props: EditorProps) => {
     search_replace(),
     lock_body(),
     alt_titles(),
+    accessibility_element(),
   ]
 
   if (props.collabProvider) {
diff --git a/src/configs/editor-views.ts b/src/configs/editor-views.ts
index 8ad4d2357..b5e76e679 100644
--- a/src/configs/editor-views.ts
+++ b/src/configs/editor-views.ts
@@ -17,6 +17,7 @@ import { ManuscriptNodeView, Nodes } from '@manuscripts/transform'
 
 import { Dispatch } from '../commands'
 import { NodeViewCreator } from '../types'
+import accessibilityElement from '../views/accessibility_element'
 import affiliations from '../views/affiliations'
 import alt_title from '../views/alt_title'
 import alt_titles_section from '../views/alt_titles_section'
@@ -106,7 +107,7 @@ export default (
     author_notes: authorNotes(props, dispatch),
     awards: awards(props, dispatch),
     award: award(props, dispatch),
-    long_desc: empty('long_desc'),
-    alt_text: empty('alt_text'),
+    long_desc: accessibilityElement(props, dispatch),
+    alt_text: accessibilityElement(props, dispatch),
   }
 }
diff --git a/src/lib/__tests__/__fixtures__/doc.json b/src/lib/__tests__/__fixtures__/doc.json
index cc1c814a6..39e476e5a 100644
--- a/src/lib/__tests__/__fixtures__/doc.json
+++ b/src/lib/__tests__/__fixtures__/doc.json
@@ -287,6 +287,14 @@
                     }
                   ]
                 },
+                {
+                  "type": "alt_text",
+                  "attrs": { "id": "" }
+                },
+                {
+                  "type": "long_desc",
+                  "attrs": { "id": "" }
+                },
                 {
                   "type": "listing",
                   "attrs": {
@@ -353,6 +361,14 @@
                     { "type": "table_row", "attrs": { "dataTracked": null } }
                   ]
                 },
+                {
+                  "type": "alt_text",
+                  "attrs": { "id": "" }
+                },
+                {
+                  "type": "long_desc",
+                  "attrs": { "id": "" }
+                },
                 {
                   "type": "listing",
                   "attrs": {
diff --git a/src/lib/__tests__/plugins.test.ts b/src/lib/__tests__/plugins.test.ts
index 3654f5528..00ea192c4 100644
--- a/src/lib/__tests__/plugins.test.ts
+++ b/src/lib/__tests__/plugins.test.ts
@@ -157,7 +157,7 @@ describe('editor view', () => {
     )
 
     const figureElement = sectionWithFigure.content.child(1)
-    expect(figureElement.childCount).toBe(3)
+    expect(figureElement.childCount).toBe(5)
     expect(figureElement.type).toBe(schema.nodes.figure_element)
     expect(figureElement.content.child(0).type).toBe(schema.nodes.figure)
     expect(figureElement.content.child(1).type).toBe(schema.nodes.figcaption)
@@ -170,7 +170,7 @@ describe('editor view', () => {
     )
 
     const tableElement = sectionWithTable.content.child(1)
-    expect(tableElement.childCount).toBe(3)
+    expect(tableElement.childCount).toBe(5)
     expect(tableElement.type).toBe(tableElement.type.schema.nodes.table_element)
     expect(tableElement.content.child(0).type).toBe(
       tableElement.type.schema.nodes.figcaption
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index 2818ffca1..1a3f99ff1 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -25,6 +25,7 @@ import { Node as ProseMirrorNode, NodeType } from 'prosemirror-model'
 import { EditorState, Selection } from 'prosemirror-state'
 import { findChildrenByType, findParentNode } from 'prosemirror-utils'
 
+import { arrowDown } from '../icons'
 import { getEditorProps } from '../plugins/editor-props'
 
 export function* iterateChildren(
@@ -160,3 +161,30 @@ export const isBodyLocked = (state: EditorState) => {
 export const isEditAllowed = (state: EditorState) => {
   return !(isBodyLocked(state) && isSelectionInBody(state))
 }
+
+export const createToggleButton = (listener: () => void) => {
+  const altTitlesButton = document.createElement('button')
+  altTitlesButton.classList.add('toggle-button-open', 'button-reset')
+  altTitlesButton.innerHTML = arrowDown
+  altTitlesButton.addEventListener('click', (e) => {
+    e.preventDefault()
+    listener()
+  })
+  return altTitlesButton
+}
+
+export const getInsertPos = (
+  type: ManuscriptNodeType,
+  parent: ManuscriptNode,
+  pos: number
+) => {
+  let insertPos = pos + parent.nodeSize - 1
+
+  parent.forEach((child, offset, index) => {
+    if (parent.canReplaceWith(index, index, type)) {
+      insertPos = pos + offset
+    }
+  })
+
+  return insertPos
+}
diff --git a/src/plugins/accessibility_element.ts b/src/plugins/accessibility_element.ts
new file mode 100644
index 000000000..872b450e4
--- /dev/null
+++ b/src/plugins/accessibility_element.ts
@@ -0,0 +1,201 @@
+/*!
+ * © 2025 Atypon Systems LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {
+  ManuscriptNode,
+  ManuscriptTransaction,
+  schema,
+} from '@manuscripts/transform'
+import { Plugin, PluginKey, Selection } from 'prosemirror-state'
+import { Decoration, DecorationSet, EditorView } from 'prosemirror-view'
+
+import { createToggleButton } from '../lib/utils'
+
+export interface PluginState {
+  expandButtonDecorations: Decoration[]
+  expandedElementDecorations: Decoration[]
+  expandedElementIDs: Set<string>
+}
+
+export const accessibilityElementKey = new PluginKey<PluginState>(
+  'accessibility-element'
+)
+
+const nodeTypes = [schema.nodes.alt_text, schema.nodes.long_desc]
+
+const handleExpandButtonClick = (view: EditorView, node: ManuscriptNode) => {
+  const tr = view.state.tr
+  toggleAccessibilitySection(tr, node)
+  view.dispatch(tr)
+}
+
+const isSelectionWithin = (
+  node: ManuscriptNode,
+  pos: number,
+  selection?: Selection
+) => {
+  if (!selection || !nodeTypes.includes(node.type)) {
+    return false
+  }
+  const to = pos + node.nodeSize
+  return selection.from >= pos && selection.to <= to
+}
+
+const buildExpandButtonDecorations = (doc: ManuscriptNode) => {
+  const decorations: Decoration[] = []
+  doc.descendants((node, pos) => {
+    if (node.type.spec.content?.includes('alt_text')) {
+      decorations.push(
+        Decoration.widget(
+          pos + node.nodeSize - 1,
+          (view) => {
+            const container = document.createElement('div')
+            container.className =
+              'accessibility_element_expander_button_container'
+            container.appendChild(
+              createToggleButton(() => handleExpandButtonClick(view, node))
+            )
+            return container
+          },
+          {
+            key: node.attrs.id,
+          }
+        )
+      )
+    }
+  })
+
+  return decorations
+}
+
+const buildExpandedElementsDecorations = (
+  doc: ManuscriptNode,
+  expandedElementIDs: Set<string>,
+  selection?: Selection
+) => {
+  const decorations: Decoration[] = []
+  doc.descendants((node, pos) => {
+    if (
+      expandedElementIDs.has(node.attrs.id) ||
+      isSelectionWithin(node, pos, selection)
+    ) {
+      decorations.push(
+        Decoration.node(pos, pos + node.nodeSize, {
+          class: 'show_accessibility_element',
+        })
+      )
+    }
+  })
+
+  return decorations
+}
+
+export default () => {
+  return new Plugin<PluginState>({
+    key: accessibilityElementKey,
+    state: {
+      init(config, instance) {
+        return {
+          expandButtonDecorations: buildExpandButtonDecorations(instance.doc),
+          expandedElementDecorations: [],
+          expandedElementIDs: new Set(),
+        }
+      },
+      apply(tr, value, oldState, newState) {
+        const s = {
+          expandButtonDecorations: value.expandButtonDecorations,
+          expandedElementDecorations: value.expandedElementDecorations,
+          expandedElementIDs: new Set(value.expandedElementIDs),
+        }
+
+        if (tr.docChanged) {
+          s.expandButtonDecorations = buildExpandButtonDecorations(newState.doc)
+        }
+
+        let expandedElementIDsChanged = false
+        const meta = tr.getMeta(accessibilityElementKey)
+        if (meta) {
+          expandedElementIDsChanged = true
+          const isExpanded = s.expandedElementIDs.has(meta.id)
+          if (
+            meta.action === 'add' ||
+            (meta.action === 'toggle' && !isExpanded)
+          ) {
+            s.expandedElementIDs.add(meta.id)
+          } else if (
+            meta.action === 'remove' ||
+            (meta.action === 'toggle' && isExpanded)
+          ) {
+            s.expandedElementIDs.delete(meta.id)
+          }
+        }
+
+        if (tr.selectionSet) {
+          const $pos = tr.selection.$from
+          for (let i = $pos.depth; i >= 0; i--) {
+            const node = $pos.node(i)
+            if (nodeTypes.includes(node.type)) {
+              const parent = $pos.node(i - 1)
+              s.expandedElementIDs.add(parent.attrs.id)
+              expandedElementIDsChanged = true
+              break
+            }
+          }
+        }
+
+        if (expandedElementIDsChanged || tr.docChanged) {
+          s.expandedElementDecorations = buildExpandedElementsDecorations(
+            newState.doc,
+            s.expandedElementIDs
+          )
+        }
+        return s
+      },
+    },
+    props: {
+      decorations: (state) => {
+        const pluginState = accessibilityElementKey.getState(state)
+        if (pluginState) {
+          return DecorationSet.create(state.doc, [
+            ...pluginState.expandButtonDecorations,
+            ...pluginState.expandedElementDecorations,
+          ])
+        }
+      },
+    },
+  })
+}
+
+export const expandAccessibilitySection = (
+  tr: ManuscriptTransaction,
+  node: ManuscriptNode
+) => {
+  if (node.attrs.id && node.type.spec.content?.includes('alt_text')) {
+    tr.setMeta(accessibilityElementKey, {
+      action: 'add',
+      id: node.attrs.id,
+    })
+  }
+}
+
+export const toggleAccessibilitySection = (
+  tr: ManuscriptTransaction,
+  node: ManuscriptNode
+) => {
+  tr.setMeta(accessibilityElementKey, {
+    action: 'toggle',
+    id: node.attrs.id,
+  })
+}
diff --git a/src/plugins/alt-titles.ts b/src/plugins/alt-titles.ts
index 1daa3b87f..85f4c456c 100644
--- a/src/plugins/alt-titles.ts
+++ b/src/plugins/alt-titles.ts
@@ -25,7 +25,7 @@ import { Node as ProseMirrorNode } from 'prosemirror-model'
 import { Plugin, PluginKey } from 'prosemirror-state'
 import { Decoration, DecorationSet } from 'prosemirror-view'
 
-import { arrowDown } from '../icons'
+import { createToggleButton } from '../lib/utils'
 
 export interface PluginState {
   collapsed: boolean
@@ -64,17 +64,6 @@ function getTitlesData(doc: ProseMirrorNode) {
   return { title, runningTitle, shortTitle, altTitlesSection }
 }
 
-function createAltTitlesButton(listener: () => void) {
-  const altTitlesButton = document.createElement('button')
-  altTitlesButton.classList.add('alt-titles-open', 'button-reset')
-  altTitlesButton.innerHTML = arrowDown
-  altTitlesButton.addEventListener('click', (e) => {
-    e.preventDefault()
-    listener()
-  })
-  return altTitlesButton
-}
-
 function selectionInAltTitles(from: number, to: number, state: PluginState) {
   if (state.runningTitle && state.shortTitle) {
     const range = {
@@ -195,7 +184,7 @@ export default () => {
             Decoration.widget(
               titleEnd - 1,
               (view: ManuscriptEditorView) => {
-                return createAltTitlesButton(() => {
+                return createToggleButton(() => {
                   const tr = view.state.tr.setMeta(altTitlesKey, {
                     collapsed: false,
                   })
diff --git a/src/plugins/objects.ts b/src/plugins/objects.ts
index 2dcc6c3ab..7554708a7 100644
--- a/src/plugins/objects.ts
+++ b/src/plugins/objects.ts
@@ -65,7 +65,10 @@ export default () => {
                 if (node.type.name === 'image_element') {
                   labelNode.textContent = target.label
                   decorations.push(
-                    Decoration.widget(pos + node.nodeSize - 1, labelNode)
+                    Decoration.widget(
+                      pos + (node.firstChild?.nodeSize || 0) + 1,
+                      labelNode
+                    )
                   )
                 } else {
                   labelNode.textContent = target.label + ':'
diff --git a/src/views/accessibility_element.ts b/src/views/accessibility_element.ts
new file mode 100644
index 000000000..a957768ae
--- /dev/null
+++ b/src/views/accessibility_element.ts
@@ -0,0 +1,47 @@
+/*!
+ * © 2025 Atypon Systems LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { LongDescNode, schema } from '@manuscripts/transform'
+
+import BlockView from './block_view'
+import { createNodeView } from './creators'
+export class AccessibilityElementView extends BlockView<LongDescNode> {
+  public contentDOM: HTMLElement
+
+  public initialise() {
+    this.createDOM()
+    this.createElement()
+    this.updateContents()
+  }
+
+  public createDOM() {
+    this.dom = document.createElement('div')
+    this.dom.classList.add('accessibility_element')
+    const label = document.createElement('label')
+    label.className = 'accessibility_element_label'
+    label.innerText =
+      this.node.type === schema.nodes.long_desc
+        ? 'Long description'
+        : 'Alt text'
+    this.dom.appendChild(label)
+  }
+
+  public createElement() {
+    super.createElement()
+    this.contentDOM.className = 'accessibility_element_input'
+  }
+}
+
+export default createNodeView(AccessibilityElementView)
diff --git a/src/views/table_element.ts b/src/views/table_element.ts
index 6a3fe02cd..e63e9a40d 100644
--- a/src/views/table_element.ts
+++ b/src/views/table_element.ts
@@ -22,6 +22,8 @@ import { createNodeView } from './creators'
 export class TableElementView extends BlockView<TableElementNode> {
   public elementType = 'figure'
 
+  public ignoreMutation = () => true
+
   public createElement = () => {
     this.contentDOM = document.createElement('figure')
     this.contentDOM.classList.add('block')
diff --git a/styles/AdvancedEditor.css b/styles/AdvancedEditor.css
index 169a7dea5..703d8a8b6 100644
--- a/styles/AdvancedEditor.css
+++ b/styles/AdvancedEditor.css
@@ -124,9 +124,8 @@
 
 .ProseMirror .figure-block {
   width: 100%;
-  border-color: #000 !important;
-  border-style: none !important;
-  border-width: 0pt !important;
+  border: 1px solid #f2f2f2;
+  border-radius: 4px;
 
   padding: 4pt !important;
   gap: 4pt !important;
@@ -143,8 +142,6 @@
 
 .ProseMirror .figure-block > figure {
   width: 100%;
-  border: 1px solid #f2f2f2;
-  border-radius: 4px;
   position: relative;
   grid-column: 1/-1;
 }
@@ -1070,7 +1067,7 @@ figure.block:has(.equation.selected-suggestion) {
   color: #6e6e6e;
   line-height: 1.3;
 }
-.alt-titles-open {
+.toggle-button-open {
   position: absolute;
   right: var(--body-side-margin);
   transform: translateX(100%);
@@ -1206,9 +1203,12 @@ th:hover > .table-context-menu-button,
   > * {
   opacity: 0.3;
 }
+
 .ProseMirror .block-embed .tools-panel {
   position: relative;
+  right: 0;
 }
+
 .ProseMirror .block-embed .block-gutter,
 .block-embed .action-gutter {
   display: none;
@@ -1247,7 +1247,7 @@ th:hover > .table-context-menu-button,
 
 .ProseMirror .non-editable {
   position: relative;
-  pointer-events: none; 
+  pointer-events: none;
   user-select: none;
   box-shadow: inset 49px 0 0 white, inset 53px 0 0 #6E6E6E;
 }
@@ -1280,3 +1280,58 @@ th:hover > .table-context-menu-button,
 .embed-media-preview .tools-panel {
   right: 6px;
 }
+
+.ProseMirror .accessibility_element {
+  margin-top: 12px;
+  grid-column-start: 1;
+  grid-column-end: -1;
+  text-align: start;
+  display: none;
+}
+
+.ProseMirror .show_accessibility_element .accessibility_element {
+  display: block;
+}
+
+.ProseMirror .accessibility_element_label {
+  color: var(--label-color);
+  font-size: 18px;
+  font-style: normal;
+  font-weight: 400;
+  line-height: 24px;
+  letter-spacing: -0.369px;
+  margin-top: 12px;
+  cursor: pointer;
+}
+
+.ProseMirror .accessibility_element_input {
+  border-radius: 4px;
+  border: 1px solid var(--accepted-bg-color);
+  background: #fff;
+  margin-top: 4px;
+  padding: 12px 8px;
+}
+
+.ProseMirror .accessibility_element_expander_button_container {
+  position: absolute;
+  left: 100%;
+  bottom: 0;
+  justify-self: self-end;
+}
+
+.ProseMirror .show_accessibility_element .toggle-button-open svg {
+  transform: rotate(180deg);
+}
+
+.ProseMirror .accessibility_element_expander_button_container .toggle-button-open {
+  right: calc(100% - 6px);
+}
+
+.ProseMirror .block-embed .accessibility_element_expander_button_container {
+  right: 36px;
+  bottom: 36px;
+}
+
+.ProseMirror .block-table_element .accessibility_element_expander_button_container {
+  left: 93%;
+}
diff --git a/yarn.lock b/yarn.lock
index 7fb46cd14..294980ba8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1692,10 +1692,10 @@
     prosemirror-transform "^1.7.0"
     prosemirror-view "^1.29.1"
 
-"@manuscripts/transform@3.0.55":
-  version "3.0.55"
-  resolved "https://registry.yarnpkg.com/@manuscripts/transform/-/transform-3.0.55.tgz#66d6bcd892332705964bb308c859f7b3b8dfbe48"
-  integrity sha512-kU2RhHptf/neBOXJtkKoj7VqFvjkycC+epGwyofwiij7PZlftNK2F2bvct36rNIzCBkO8Z4asMl8WN2P2wrvpw==
+"@manuscripts/transform@3.0.56":
+  version "3.0.56"
+  resolved "https://registry.yarnpkg.com/@manuscripts/transform/-/transform-3.0.56.tgz#20bd7296927467ecd5dcca7da5a777f8f43e86cc"
+  integrity sha512-eu4FCo88HggiJDUfyHDjZRS6T5gLHuldm8Bs4OxVvwLu15MptInnkb+zAdvvN9ZAljZuMxuDTQZ/kop4P8Fq1g==
   dependencies:
     "@manuscripts/json-schema" "2.2.11"
     "@manuscripts/library" "1.3.13"


Reviews:
user: login: mnatsheh
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T16:00:43Z
user: login: asouqi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T20:29:39Z
user: login: mnatsheh
body: In https://github.com/Atypon-OpenSource/manuscripts-body-editor/pull/560/ we're using a plugin to add the icon and manage expanding/collapsing the titles section. I think we should do something similar here.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-13T06:23:59Z
user: login: mnatsheh
body: The implementation feels overly complicated. The plugin state can just be:
```
interface PluginState {
    elementIDs: string[]
    expandedElementIDs: string[]
}
```

And the `apply` implementation can be something like this:
```
apply(...) {
    newState.doc.descendents((node, pos, parent) => {
        if (isAccessibilityElement(node.type)) {
            const elementID = parent.attrs.id
            // add elementID to elementIDs
            if (isWithinSelectedSuggestion(pos)) {
               //add elementID to expandedElementIDs
            }
            
        }
    })
}
```

The decorations should be built in the `decorations` prop.


----

Let's also avoid using React for this, as it's quite simple.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T07:57:42Z
user: login: mnatsheh
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T10:24:36Z
user: login: asouqi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T12:31:19Z

Review comments:
diff_hunk: @@ -0,0 +1,64 @@
+/*!
+ * © 2025 Atypon Systems LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { LongDescNode, schema } from '@manuscripts/transform'
+
+import { selectedSuggestionKey } from '../plugins/selected-suggestion'
+import BlockView from './block_view'
+import { createNodeView } from './creators'
+export class AccessibilityElementView extends BlockView<LongDescNode> {
+  public contentDOM: HTMLElement
+
+  public initialise() {
+    this.createDOM()
+    this.createElement()
+    this.updateContents()
+  }
+
+  public updateContents() {
+    super.updateContents()
+    const state = this.view.state
+    const selection = selectedSuggestionKey.getState(state)?.suggestion
+    if (selection) {
+      const block = this.dom.parentNode?.parentElement?.classList.contains(
+        'block-container'
+      )
+        ? this.dom.parentNode?.parentElement
+        : this.dom.parentNode?.parentElement?.parentElement
+      block &&
+        !block.classList.contains('show_accessibility_element') &&
+        block.classList.toggle('show_accessibility_element')
path: src/views/accessibility_element.ts
user: login: mnatsheh
body: what is the purpose of this?
created_at: 2025-04-11T16:00:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,64 @@
+/*!
+ * © 2025 Atypon Systems LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { LongDescNode, schema } from '@manuscripts/transform'
+
+import { selectedSuggestionKey } from '../plugins/selected-suggestion'
+import BlockView from './block_view'
+import { createNodeView } from './creators'
+export class AccessibilityElementView extends BlockView<LongDescNode> {
+  public contentDOM: HTMLElement
+
+  public initialise() {
+    this.createDOM()
+    this.createElement()
+    this.updateContents()
+  }
+
+  public updateContents() {
+    super.updateContents()
+    const state = this.view.state
+    const selection = selectedSuggestionKey.getState(state)?.suggestion
+    if (selection) {
+      const block = this.dom.parentNode?.parentElement?.classList.contains(
+        'block-container'
+      )
+        ? this.dom.parentNode?.parentElement
+        : this.dom.parentNode?.parentElement?.parentElement
+      block &&
+        !block.classList.contains('show_accessibility_element') &&
+        block.classList.toggle('show_accessibility_element')
path: src/views/accessibility_element.ts
user: login: asouqi
body: that to show the accessibility element view when the user clicks on the suggestion, [design use case](https://www.figma.com/design/dLmX8y02PYAgEwZRFG8Jip/Designs-for-the-content-accessibility-elements?node-id=28920-32668&m=dev)
created_at: 2025-04-11T20:29:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -63,7 +64,7 @@ function getTitlesData(doc: ProseMirrorNode) {
   return { title, runningTitle, shortTitle, altTitlesSection }
 }
 
-function createAltTitlesButton(listener: () => void) {
+export function createAltTitlesButton(listener: () => void) {
path: src/plugins/alt-titles.ts
user: login: mnatsheh
body: Please extract this into its own file, and remove altTitles from the names.
created_at: 2025-04-22T10:24:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -63,7 +64,7 @@ function getTitlesData(doc: ProseMirrorNode) {
   return { title, runningTitle, shortTitle, altTitlesSection }
 }
 
-function createAltTitlesButton(listener: () => void) {
+export function createAltTitlesButton(listener: () => void) {
path: src/plugins/alt-titles.ts
user: login: asouqi
body: updated
created_at: 2025-04-22T12:31:19Z
author_association: CONTRIBUTOR

PR comments:
user: login: asouqi
created_at: 2025-04-13T21:13:55Z
body: > In #560 we're using a plugin to add the icon and manage expanding/collapsing the titles section. I think we should do something similar here.

updated
author_association: CONTRIBUTOR
user: login: asouqi
created_at: 2025-04-16T12:16:33Z
body: > The implementation feels overly complicated. The plugin state can just be:

I simplified it

> The decorations should be built in the decorations prop.

The decorations props will be called for every editor update, so we will go through the editor document to rebuild decorations that will be expensive

> Let's also avoid using React for this, as it's quite simple.

done updated
author_association: CONTRIBUTOR
user: login: mnatsheh
created_at: 2025-04-21T13:04:03Z
body: @asouqi , I made a few changes to the plugin to avoid needing to process individual steps. Please take a look and let me know if you have issues.

@mbetamony it would be great if the transform added the alt_text/long_desc nodes to the doc even if they don't exist in the JATS. Is that simple to implement?
author_association: CONTRIBUTOR
user: login: mnatsheh
created_at: 2025-04-21T13:05:11Z
body: It would also be great if the HTML for the expander widget is shared with the alt_title implementation once this branch is rebased (obviously the onclick implementation will be different). 
author_association: CONTRIBUTOR
user: login: mbetamony
created_at: 2025-04-21T13:48:31Z
body: > @mbetamony it would be great if the transform added the alt_text/long_desc nodes to the doc even if they don't exist in the JATS. Is that simple to implement?

Should be, yes.
author_association: CONTRIBUTOR
user: login: asouqi
created_at: 2025-04-21T14:45:17Z
body: > It would also be great if the HTML for the expander widget is shared with the alt_title implementation once this branch is rebased (obviously the onclick implementation will be different).

done updated
author_association: CONTRIBUTOR
user: login: zax-assistant[bot]
created_at: 2025-04-23T04:37:24Z
body: Published version `2.8.36-LEAN-4433.1`
author_association: CONTRIBUTOR
user: login: zax-assistant[bot]
created_at: 2025-04-23T11:39:14Z
body: Published version `2.8.37-LEAN-4433.0`
author_association: CONTRIBUTOR
user: login: zax-assistant[bot]
created_at: 2025-04-23T11:49:45Z
body: Published version `2.8.37-LEAN-4433.1`
author_association: CONTRIBUTOR
user: login: zax-assistant[bot]
created_at: 2025-04-23T12:33:01Z
body: Published version `2.8.37-LEAN-4433.2`
author_association: CONTRIBUTOR
user: login: zax-assistant[bot]
created_at: 2025-04-23T15:44:25Z
body: Published version `2.8.37-LEAN-4433.2`
author_association: CONTRIBUTOR
user: login: mbetamony
created_at: 2025-04-23T15:57:09Z
body: I've pushed a commit here to update the versions, since zax didn't.
author_association: CONTRIBUTOR