21
21
import org .apache .wicket .Component ;
22
22
import org .apache .wicket .ajax .AjaxRequestTarget ;
23
23
import org .apache .wicket .extensions .ajax .markup .html .modal .ModalWindow ;
24
+ import lombok .extern .slf4j .Slf4j ;
24
25
25
26
/**
26
27
* A custom ModalWindow that adds behaviours specific to our tool
27
28
*/
29
+ @ Slf4j
28
30
public class GbModalWindow extends ModalWindow {
29
31
30
32
private static final long serialVersionUID = 1L ;
@@ -34,7 +36,7 @@ public class GbModalWindow extends ModalWindow {
34
36
private String studentUuidToReturnFocusTo ;
35
37
private boolean returnFocusToCourseGrade = false ;
36
38
private List <WindowClosedCallback > closeCallbacks ;
37
- private boolean positionAtTop = false ;
39
+ private Component initialFocusComponent ;
38
40
39
41
public GbModalWindow (final String id ) {
40
42
super (id );
@@ -66,20 +68,73 @@ public void onClose(final AjaxRequestTarget target) {
66
68
67
69
@ Override
68
70
protected CharSequence getShowJavaScript () {
69
- StringBuilder extraJavascript = new StringBuilder ();
71
+ if (getContent () == null ) {
72
+ log .warn ("ModalWindow content is null, cannot generate show JavaScript reliably." );
73
+ return super .getShowJavaScript ();
74
+ }
75
+ getContent ().setOutputMarkupId (true );
76
+
77
+ StringBuilder js = new StringBuilder (super .getShowJavaScript ().toString ());
70
78
71
- // focus the first input field in the content pane
72
- extraJavascript .append (String .format ("setTimeout(function() {$('#%s :input:visible:first').focus();});" ,
73
- getContent ().getMarkupId ()));
79
+ js .append (String .format ("$('#%s').attr('tabindex', '-1');" , getContent ().getMarkupId ()));
74
80
75
- // position at the top of the page
76
- if (this .positionAtTop ) {
77
- extraJavascript .append (
78
- String .format ("setTimeout(function() {GbGradeTable.positionModalAtTop($('#%s').closest('.wicket-modal'));});" ,
79
- getContent ().getMarkupId ()));
81
+ js .append ("setTimeout(function() {" );
82
+ if (this .initialFocusComponent != null && this .initialFocusComponent .getOutputMarkupId ()) {
83
+ js .append (String .format ("try { $('#%s').focus(); } catch(e) { console.error('Failed to focus initial component:', e); }" ,
84
+ this .initialFocusComponent .getMarkupId ()));
85
+ } else {
86
+ js .append (String .format ("try { $('#%s').focus(); } catch(e) { console.error('Failed to focus modal content:', e); }" ,
87
+ getContent ().getMarkupId ()));
80
88
}
89
+ js .append ("}, 50);" );
90
+
91
+ js .append ("function GbModalWindow_trapFocus(event, modalContentId) {" );
92
+ js .append (" if (!modalContentId || event.key !== 'Tab' && event.keyCode !== 9) return;" );
93
+ js .append (" const modalContent = document.getElementById(modalContentId);" );
94
+ js .append (" if (!modalContent) return;" );
95
+ js .append (" const modalWindowEl = modalContent.closest('.wicket-modal');" );
96
+ js .append (" if (!modalWindowEl) return;" );
97
+
98
+ js .append (" const focusableElements = modalWindowEl.querySelectorAll(" );
99
+ js .append (" 'a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex=\" -1\" ])'" );
100
+ js .append (" );" );
101
+ js .append (" if (focusableElements.length === 0) return;" );
102
+ js .append (" const firstFocusableElement = focusableElements[0];" );
103
+ js .append (" const lastFocusableElement = focusableElements[focusableElements.length - 1];" );
81
104
82
- return super .getShowJavaScript ().toString () + extraJavascript .toString ();
105
+ js .append (" if (event.shiftKey) {" );
106
+ js .append (" if (document.activeElement === firstFocusableElement) {" );
107
+ js .append (" lastFocusableElement.focus();" );
108
+ js .append (" event.preventDefault();" );
109
+ js .append (" }" );
110
+ js .append (" } else {" );
111
+ js .append (" if (document.activeElement === lastFocusableElement) {" );
112
+ js .append (" firstFocusableElement.focus();" );
113
+ js .append (" event.preventDefault();" );
114
+ js .append (" }" );
115
+ js .append (" }" );
116
+ js .append ("}" );
117
+
118
+ // Attach the event listener to the document, namespaced per modal instance
119
+ this .setOutputMarkupId (true ); // Ensure modal window itself has an ID for namespacing
120
+ js .append (String .format (
121
+ "$(document).on('keydown.gbTrapFocus_%s', function(e) { " +
122
+ " if (e.key !== 'Tab' && e.keyCode !== 9) return; " + // Early exit
123
+ " const modalContent = document.getElementById('%s'); " +
124
+ " if (!modalContent) { $(document).off('keydown.gbTrapFocus_%s'); return; } " + // Cleanup if content disappears
125
+ " const modalWindow = $(modalContent).closest('.wicket-modal'); " +
126
+ " if (!modalWindow || modalWindow.is(':hidden')) return; " + // Check if *this* modal is visible
127
+ " if (modalContent.contains(document.activeElement)) { " + // Check if focus is currently inside
128
+ " GbModalWindow_trapFocus(e, '%s'); " +
129
+ " } " +
130
+ "});" ,
131
+ this .getMarkupId (), // Namespace for document listener
132
+ getContent ().getMarkupId (),
133
+ this .getMarkupId (), // Namespace for cleanup inside listener
134
+ getContent ().getMarkupId ()
135
+ ));
136
+
137
+ return js ;
83
138
}
84
139
85
140
@ Override
@@ -137,8 +192,18 @@ public void clearWindowClosedCallbacks() {
137
192
setDefaultWindowClosedCallback ();
138
193
}
139
194
140
- public void setPositionAtTop (final boolean positionAtTop ) {
141
- this .positionAtTop = positionAtTop ;
195
+ /**
196
+ * Set the component to focus when the modal is first opened.
197
+ * The component MUST have its output markup ID set via setOutputMarkupId(true).
198
+ * If null or the component doesn't have an output markup id, the modal content panel will be focused.
199
+ *
200
+ * @param component The component to focus initially, or null to focus the content panel.
201
+ */
202
+ public void setInitialFocusComponent (final Component component ) {
203
+ this .initialFocusComponent = component ;
204
+ if (this .initialFocusComponent != null ) {
205
+ this .initialFocusComponent .setOutputMarkupId (true );
206
+ }
142
207
}
143
208
144
209
private void setDefaultWindowClosedCallback () {
@@ -147,39 +212,54 @@ private void setDefaultWindowClosedCallback() {
147
212
148
213
@ Override
149
214
public void onClose (final AjaxRequestTarget target ) {
150
- // Disable all buttons with in the modal in case it takes a moment to close
151
215
target .appendJavaScript (
152
- String .format ("$('#%s :input').prop('disabled', true);" ,
216
+ String .format ("try { $('#%s :input').prop('disabled', true); } catch(e) { console.error('Failed to disable inputs on close:', e); } " ,
153
217
GbModalWindow .this .getContent ().getMarkupId ()));
154
218
155
- // Ensure the date picker is hidden
156
- target .appendJavaScript ("$('#ui-datepicker-div').hide();" );
219
+ target .appendJavaScript ("try { $('#ui-datepicker-div').hide(); } catch(e) { console.error('Failed to hide datepicker:', e); }" );
220
+
221
+ target .appendJavaScript ("try { GradebookGradeSummaryUtils.clearBlur(); } catch(e) { console.error('Failed to clear blur:', e); }" );
157
222
158
- // Ensure any mask is hidden
159
- target .appendJavaScript ("GradebookGradeSummaryUtils.clearBlur();" );
223
+ // Remove the focus trap listener from the document using the correct namespace
224
+ target .appendJavaScript (String .format ("try { $(document).off('keydown.gbTrapFocus_%s'); } catch(e) { console.error('Failed to remove focus trap listener:', e); }" , GbModalWindow .this .getMarkupId ()));
225
+
226
+ String focusScript = "setTimeout(function() { try { " ;
227
+ boolean focusSet = false ;
160
228
161
- // Return focus to defined component
162
229
if (GbModalWindow .this .componentToReturnFocusTo != null ) {
163
- target .appendJavaScript (String .format ("setTimeout(function() {$('#%s').focus();});" ,
164
- GbModalWindow .this .componentToReturnFocusTo .getMarkupId ()));
230
+ focusScript += String .format ("$('#%s').focus();" ,
231
+ GbModalWindow .this .componentToReturnFocusTo .getMarkupId ());
232
+ focusSet = true ;
165
233
} else if (GbModalWindow .this .assignmentIdToReturnFocusTo != null &&
166
234
GbModalWindow .this .studentUuidToReturnFocusTo != null ) {
167
- target . appendJavaScript ( String .format ("setTimeout(function() { GbGradeTable.selectCell('%s', '%s');} );" ,
235
+ focusScript += String .format ("GbGradeTable.selectCell('%s', '%s');" ,
168
236
GbModalWindow .this .assignmentIdToReturnFocusTo ,
169
- GbModalWindow .this .studentUuidToReturnFocusTo ));
237
+ GbModalWindow .this .studentUuidToReturnFocusTo );
238
+ focusSet = true ;
170
239
} else if (GbModalWindow .this .assignmentIdToReturnFocusTo != null ) {
171
- target .appendJavaScript (String .format ("setTimeout(function() {GbGradeTable.selectCell('%s', null);});" ,
172
- GbModalWindow .this .assignmentIdToReturnFocusTo ));
240
+ focusScript += String .format ("GbGradeTable.selectCell('%s', null);" ,
241
+ GbModalWindow .this .assignmentIdToReturnFocusTo );
242
+ focusSet = true ;
173
243
} else if (GbModalWindow .this .studentUuidToReturnFocusTo != null ) {
174
244
if (GbModalWindow .this .returnFocusToCourseGrade ) {
175
- target . appendJavaScript ( String .format ("setTimeout(function() { GbGradeTable.selectCourseGradeCell('%s');} );" ,
176
- GbModalWindow .this .studentUuidToReturnFocusTo )) ;
245
+ focusScript += String .format ("GbGradeTable.selectCourseGradeCell('%s');" ,
246
+ GbModalWindow .this .studentUuidToReturnFocusTo );
177
247
} else {
178
- target . appendJavaScript ( String .format ("setTimeout(function() { GbGradeTable.selectCell(null, '%s');} );" ,
179
- GbModalWindow .this .studentUuidToReturnFocusTo )) ;
248
+ focusScript += String .format ("GbGradeTable.selectCell(null, '%s');" ,
249
+ GbModalWindow .this .studentUuidToReturnFocusTo );
180
250
}
251
+ focusSet = true ;
181
252
} else if (GbModalWindow .this .returnFocusToCourseGrade ) {
182
- target .appendJavaScript ("setTimeout(function() {GbGradeTable.selectCourseGradeCell();});" );
253
+ focusScript += "GbGradeTable.selectCourseGradeCell();" ;
254
+ focusSet = true ;
255
+ }
256
+
257
+ if (focusSet ) {
258
+ focusScript += " } catch(e) { console.error('Error returning focus:', e); } }, 50);" ;
259
+ target .appendJavaScript (focusScript );
260
+ } else {
261
+ // Fallback focus if nothing specific is set? Maybe focus body or a known static element?
262
+ // For now, do nothing if no specific return focus is set.
183
263
}
184
264
}
185
265
});
0 commit comments