@@ -7,11 +7,28 @@ import { getDescriptionProgress, getProgressColor } from "@yoast/search-metadata
77import { ContentPlannerError } from "./content-planner-error" ;
88import classNames from "classnames" ;
99import { IntentCallout } from "./intent-callout" ;
10- import { StructureRow } from "./structure-row" ;
10+ import { StructureRow , StructureRowSkeleton } from "./structure-row" ;
1111import { CategorySection } from "./category-section" ;
1212import { ASYNC_ACTION_STATUS } from "../../shared-admin/constants" ;
1313import { useDraggableStructure , useFetchContentOutline } from "../hooks" ;
1414import { Transition } from "@headlessui/react" ;
15+ import { SKELETON_ROW_COUNT } from "../constants" ;
16+
17+ /**
18+ * Section header for the Blog post structure list, shared by the loading and loaded states.
19+ *
20+ * @returns {JSX.Element } The StructureSectionHeader component.
21+ */
22+ const StructureSectionHeader = ( ) => (
23+ < div className = "yst-flex yst-items-end yst-justify-between yst-mb-2" >
24+ < span className = "yst-font-medium yst-text-sm yst-text-slate-800" >
25+ { __ ( "Blog post structure" , "wordpress-seo" ) }
26+ </ span >
27+ < span className = "yst-text-xs yst-text-slate-500" >
28+ { __ ( "Drag to reorder" , "wordpress-seo" ) }
29+ </ span >
30+ </ div >
31+ ) ;
1532
1633/**
1734 * @typedef {import( "../constants" ).Suggestion } Suggestion
@@ -82,10 +99,23 @@ const LoadingOutlineModalContent = () => {
8299 < CategorySection
83100 isLoading = { true }
84101 />
85- < div className = "yst-flex yst-flex-col yst-gap-4" >
86- < SkeletonFormField label = { __ ( "Focus Keyphrase" , "wordpress-seo" ) } />
87- < SkeletonFormField label = { __ ( "Title" , "wordpress-seo" ) } />
88- < SkeletonFormField label = { __ ( "Meta description" , "wordpress-seo" ) } multiline = { true } />
102+ < div className = "yst-flex yst-flex-col" >
103+ < div className = "yst-flex yst-flex-col yst-gap-4" >
104+ < SkeletonFormField label = { __ ( "Focus Keyphrase" , "wordpress-seo" ) } />
105+ < SkeletonFormField label = { __ ( "Title" , "wordpress-seo" ) } />
106+ < SkeletonFormField label = { __ ( "Meta description" , "wordpress-seo" ) } multiline = { true } />
107+ </ div >
108+ < hr className = "yst-border-slate-200 yst-my-6" />
109+ < StructureSectionHeader />
110+ < ul
111+ aria-label = { __ ( "Blog post structure" , "wordpress-seo" ) }
112+ aria-busy = { true }
113+ className = "yst-list-none yst-p-0 yst-m-0 yst-flex yst-flex-col yst-gap-2"
114+ >
115+ { Array . from ( { length : SKELETON_ROW_COUNT } ) . map ( ( _ , index ) => (
116+ < StructureRowSkeleton key = { `structure-row-skeleton-${ index } ` } />
117+ ) ) }
118+ </ ul >
89119 </ div >
90120 </ > ;
91121} ;
@@ -128,6 +158,8 @@ export const OutlineModalContent = ( {
128158 const {
129159 structure,
130160 dragOverIndex,
161+ reorderMessage,
162+ handleAnnounce,
131163 handleDragStart,
132164 handleDragOver,
133165 handleDrop,
@@ -136,6 +168,9 @@ export const OutlineModalContent = ( {
136168 handleMoveDown,
137169 } = useDraggableStructure ( ) ;
138170
171+ const handleSentinelDragOver = useCallback ( ( e ) => handleDragOver ( e , structure . length ) , [ handleDragOver , structure . length ] ) ;
172+ const handleSentinelDrop = useCallback ( ( e ) => handleDrop ( e , structure . length ) , [ handleDrop , structure . length ] ) ;
173+
139174 useEffect ( ( ) => {
140175 setFocusKeyphrase ( suggestion . keyphrase ) ;
141176 setTitle ( suggestion . title ) ;
@@ -180,6 +215,7 @@ export const OutlineModalContent = ( {
180215 errorCode = { error . errorCode }
181216 errorIdentifier = { error . errorIdentifier }
182217 errorMessage = { error . errorMessage }
218+ missingLicenses = { error . missingLicenses }
183219 onRetry = { handleRetry }
184220 />
185221 </ Modal . Container . Content >
@@ -189,7 +225,7 @@ export const OutlineModalContent = ( {
189225 return (
190226 < >
191227 < Modal . Container . Content className = "yst-overflow-y-auto yst-pt-6 yst-px-6 yst-pb-0 yst-m-0 yst-relative" aria-busy = { isLoading } >
192- < div className = "yst-flex yst-flex-col yst-gap-6 yst-pb-4 " >
228+ < div className = "yst-flex yst-flex-col yst-gap-6" >
193229 < IntentCallout
194230 intent = { suggestion . intent }
195231 description = { suggestion . explanation }
@@ -228,7 +264,7 @@ export const OutlineModalContent = ( {
228264 onToggle = { handleCategoryToggle }
229265 />
230266 ) }
231- < div className = "yst-flex yst-flex-col yst-gap-6 " >
267+ < div className = "yst-flex yst-flex-col" >
232268 < div className = "yst-flex yst-flex-col yst-gap-4" >
233269 < TextField
234270 id = "content-outline-focus-keyphrase"
@@ -259,16 +295,11 @@ export const OutlineModalContent = ( {
259295
260296 </ div >
261297 </ div >
262- < hr className = "yst-border-slate-200" />
263- < div className = "yst-flex yst-items-end yst-justify-between" style = { { marginBottom : "-16px" } } >
264- < span className = "yst-font-medium yst-text-sm yst-text-slate-800" >
265- { __ ( "Blog post structure" , "wordpress-seo" ) }
266- </ span >
267- < span className = "yst-text-xs yst-text-slate-500" >
268- { __ ( "Drag to reorder" , "wordpress-seo" ) }
269- </ span >
270- </ div >
271- < div role = "listbox" aria-label = { __ ( "Blog post structure" , "wordpress-seo" ) } className = "yst-flex yst-flex-col yst-gap-2" >
298+ < hr className = "yst-border-slate-200 yst-my-6" />
299+ < StructureSectionHeader />
300+ { /* Live region announces keyboard reorder results to screen readers. */ }
301+ < div aria-live = "assertive" aria-atomic = "true" className = "yst-sr-only" > { reorderMessage } </ div >
302+ < ul aria-label = { __ ( "Blog post structure" , "wordpress-seo" ) } className = "yst-flex yst-flex-col yst-gap-2 yst-list-none yst-p-0 yst-m-0" >
272303 { structure . map ( ( item , index ) => (
273304 < StructureRow
274305 key = { item . id }
@@ -282,14 +313,24 @@ export const OutlineModalContent = ( {
282313 onMoveUp = { handleMoveUp }
283314 onMoveDown = { handleMoveDown }
284315 totalItems = { structure . length }
316+ onAnnounce = { handleAnnounce }
285317 />
286318 ) ) }
287- </ div >
319+ </ ul >
320+ { /* Sentinel drop zone: allows dropping an item into the last position. */ }
321+ < div
322+ className = "yst-h-8"
323+ onDragOver = { handleSentinelDragOver }
324+ onDrop = { handleSentinelDrop }
325+ />
288326 </ div >
289327 </ Transition >
290328 </ div >
291329 < div
292- className = "yst-sticky -yst-left-6 -yst-right-6 yst-bottom-0 yst-h-10 yst-pointer-events-none yst-bg-gradient-to-t yst-from-white yst-to-transparent yst-transition-opacity"
330+ className = { classNames (
331+ isLoading ? "yst-sticky" : "yst-hidden" ,
332+ "-yst-left-6 -yst-right-6 yst-bottom-0 yst-h-10 yst-pointer-events-none yst-bg-gradient-to-t yst-from-white yst-to-transparent yst-transition-opacity"
333+ ) }
293334 aria-hidden = "true"
294335 />
295336 </ Modal . Container . Content >
0 commit comments