1 /*
2  * ModalDialogBase.java
3  *
4  * Copyright (C) 2021 by RStudio, PBC
5  *
6  * Unless you have received this program directly from RStudio pursuant
7  * to the terms of a commercial license agreement with RStudio, then
8  * this program is licensed to you under the terms of version 3 of the
9  * GNU Affero General Public License. This program is distributed WITHOUT
10  * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
11  * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
12  * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
13  *
14  */
15 package org.rstudio.core.client.widget;
16 
17 import com.google.gwt.animation.client.Animation;
18 import com.google.gwt.aria.client.DialogRole;
19 import com.google.gwt.aria.client.Id;
20 import com.google.gwt.core.client.GWT;
21 import com.google.gwt.core.client.Scheduler;
22 import com.google.gwt.dom.client.Element;
23 import com.google.gwt.dom.client.NativeEvent;
24 import com.google.gwt.dom.client.Style;
25 import com.google.gwt.dom.client.Style.Unit;
26 import com.google.gwt.event.dom.client.KeyCodes;
27 import com.google.gwt.event.dom.client.KeyDownEvent;
28 import com.google.gwt.event.dom.client.MouseDownEvent;
29 import com.google.gwt.resources.client.ClientBundle;
30 import com.google.gwt.resources.client.CssResource;
31 import com.google.gwt.user.client.Command;
32 import com.google.gwt.user.client.DOM;
33 import com.google.gwt.user.client.Event;
34 import com.google.gwt.user.client.ui.DialogBox;
35 import com.google.gwt.user.client.ui.HasHorizontalAlignment;
36 import com.google.gwt.user.client.ui.HasHorizontalAlignment.HorizontalAlignmentConstant;
37 import com.google.gwt.user.client.ui.HorizontalPanel;
38 import com.google.gwt.user.client.ui.SimplePanel;
39 import com.google.gwt.user.client.ui.VerticalPanel;
40 import com.google.gwt.user.client.ui.Widget;
41 
42 import elemental2.dom.DomGlobal;
43 import org.rstudio.core.client.Debug;
44 import org.rstudio.core.client.ElementIds;
45 import org.rstudio.core.client.Point;
46 import org.rstudio.core.client.StringUtil;
47 import org.rstudio.core.client.command.ShortcutManager;
48 import org.rstudio.core.client.command.ShortcutManager.Handle;
49 import org.rstudio.core.client.dom.DomUtils;
50 import org.rstudio.core.client.dom.NativeWindow;
51 import org.rstudio.core.client.theme.res.ThemeStyles;
52 import org.rstudio.studio.client.RStudioGinjector;
53 import org.rstudio.studio.client.application.events.AriaLiveStatusEvent.Severity;
54 import org.rstudio.studio.client.application.ui.RStudioThemes;
55 import org.rstudio.studio.client.common.Timers;
56 
57 import java.util.ArrayList;
58 
59 public abstract class ModalDialogBase extends DialogBox
60                                       implements AriaLiveStatusReporter
61 {
ModalDialogBase(DialogRole role)62    protected ModalDialogBase(DialogRole role)
63    {
64       this(null, role);
65    }
66 
ModalDialogBase(SimplePanel containerPanel, DialogRole role)67    protected ModalDialogBase(SimplePanel containerPanel, DialogRole role)
68    {
69       // core initialization. passing false for modal works around
70       // modal PopupPanel suppressing global keyboard accelerators (like
71       // Ctrl-N or Ctrl-T). modality is achieved via setGlassEnabled(true)
72       super(false, false);
73       setGlassEnabled(true);
74       addStyleDependentName("ModalDialog");
75       addStyleName(RES.styles().modalDialog());
76 
77       // a11y
78       role_ = role;
79       role_.set(getElement());
80       focus_ = new FocusHelper(getElement());
81 
82       // main panel used to host UI
83       mainPanel_ = new VerticalPanel();
84       bottomPanel_ = new HorizontalPanel();
85       bottomPanel_.setStyleName(ThemeStyles.INSTANCE.dialogBottomPanel());
86       bottomPanel_.setWidth("100%");
87       buttonPanel_ = new HorizontalPanel();
88       leftButtonPanel_ = new HorizontalPanel();
89       bottomPanel_.add(leftButtonPanel_);
90       bottomPanel_.add(buttonPanel_);
91 
92       ariaLiveStatusWidget_ = new AriaLiveStatusWidget();
93       bottomPanel_.add(ariaLiveStatusWidget_);
94 
95       setButtonAlignment(HasHorizontalAlignment.ALIGN_RIGHT);
96       mainPanel_.add(bottomPanel_);
97 
98       // embed main panel in a custom container if specified
99       containerPanel_ = containerPanel;
100       if (containerPanel_ != null)
101       {
102          containerPanel_.setWidget(mainPanel_);
103          setWidget(containerPanel_);
104       }
105       else
106       {
107          setWidget(mainPanel_);
108       }
109 
110       addDomHandler(event ->
111       {
112          // Is this too aggressive? Alternatively we could only filter out
113          // keycodes that are known to be problematic (pgup/pgdown)
114          if (handleKeyDownEvent(event)) {
115             event.stopPropagation();
116          }
117       }, KeyDownEvent.getType());
118    }
119 
hideButtons()120    protected void hideButtons()
121    {
122      buttonPanel_.setVisible(false);
123    }
124 
handleKeyDownEvent(KeyDownEvent event)125    protected boolean handleKeyDownEvent(KeyDownEvent event) {
126       return true;
127    }
128 
129    @Override
beginDragging(MouseDownEvent event)130    protected void beginDragging(MouseDownEvent event)
131    {
132       // Prevent text selection from occurring when moving the dialog box
133       event.preventDefault();
134       super.beginDragging(event);
135    }
136 
137    @Override
onLoad()138    protected void onLoad()
139    {
140       super.onLoad();
141       ModalDialogTracker.onShow(this);
142       if (shortcutDisableHandle_ != null)
143          shortcutDisableHandle_.close();
144       shortcutDisableHandle_ = ShortcutManager.INSTANCE.disable();
145 
146       try
147       {
148          // 728: Focus remains in Source view when message dialog pops up over it
149          NativeWindow.get().focus();
150       }
151       catch (Throwable e)
152       {
153       }
154 
155       RStudioThemes.disableDarkMenus();
156    }
157 
158    @Override
onUnload()159    protected void onUnload()
160    {
161       if (shortcutDisableHandle_ != null)
162          shortcutDisableHandle_.close();
163       shortcutDisableHandle_ = null;
164 
165       ModalDialogTracker.onHide(this);
166 
167       RStudioThemes.enableDarkMenus();
168 
169       super.onUnload();
170    }
171 
isEscapeDisabled()172    public boolean isEscapeDisabled()
173    {
174       return escapeDisabled_;
175    }
176 
setEscapeDisabled(boolean escapeDisabled)177    public void setEscapeDisabled(boolean escapeDisabled)
178    {
179       escapeDisabled_ = escapeDisabled;
180    }
181 
isEnterDisabled()182    public boolean isEnterDisabled()
183    {
184       return enterDisabled_;
185    }
186 
setEnterDisabled(boolean enterDisabled)187    public void setEnterDisabled(boolean enterDisabled)
188    {
189       enterDisabled_ = enterDisabled;
190    }
191 
setRestoreFocusOnClose(boolean restoreFocus)192    public void setRestoreFocusOnClose(boolean restoreFocus)
193    {
194       restoreFocus_ = restoreFocus;
195    }
196 
showModal()197    public void showModal()
198    {
199       showModal(true);
200    }
201 
showModal(boolean restoreFocus)202    public void showModal(boolean restoreFocus)
203    {
204       if (mainWidget_ == null)
205       {
206          mainWidget_ = createMainWidget();
207 
208          // get the main widget to line up with the right edge of the buttons.
209          mainWidget_.getElement().getStyle().setMarginRight(2, Unit.PX);
210          mainPanel_.insert(mainWidget_, 0);
211       }
212 
213       restoreFocus_ = restoreFocus;
214 
215       if (restoreFocus)
216       {
217          originallyActiveElement_ = DomUtils.getActiveElement();
218          if (originallyActiveElement_ != null)
219             originallyActiveElement_.blur();
220       }
221 
222       // position the dialog
223       positionAndShowDialog(() ->
224       {
225          // defer shown notification to allow all elements to render
226          // before attempting to interact w/ them programmatically (e.g. setFocus)
227          Timers.singleShot(100, () -> onDialogShown());
228       });
229    }
230 
createMainWidget()231    protected abstract Widget createMainWidget();
232 
positionAndShowDialog(Command onCompleted)233    protected void positionAndShowDialog(Command onCompleted)
234    {
235       super.center();
236       onCompleted.execute();
237 
238       // Force the contents of the modal to be vertically scrollable
239       // if the height of the modal is larger than the app window
240       Element e = this.getElement();
241       Element child = e.getFirstChildElement();
242       if (child != null)
243       {
244          int windowInnerHeight = DomGlobal.window.innerHeight;
245          if (windowInnerHeight <= 10) return; // degenerate property case
246 
247          // snap the top of the modal to the top bounds of the window
248          int eleTop = e.getAbsoluteTop();
249          if (eleTop < 0)
250          {
251             eleTop = 0;
252             e.getStyle().setTop(0, Unit.PX);
253          }
254          int eleHeight = e.getOffsetHeight();
255 
256          if (eleHeight + 30 >= windowInnerHeight)
257          {
258             child.getStyle().setProperty("overflowY", "auto");
259 
260             // don't override overflowX if it's already set
261             String overflowX = child.getStyle().getProperty("overflowX");
262             if (overflowX == null || overflowX.length() < 1)
263             {
264                child.getStyle().setProperty("overflowX", "hidden");
265             }
266             child.getStyle().setPropertyPx("maxHeight", windowInnerHeight - eleTop - 30);
267          }
268       }
269    }
270 
onDialogShown()271    protected void onDialogShown()
272    {
273       refreshFocusableElements();
274       focusInitialControl();
275    }
276 
addOkButton(ThemedButton okButton, String elementId)277    protected void addOkButton(ThemedButton okButton, String elementId)
278    {
279       okButton_ = okButton;
280       okButton_.addStyleDependentName("DialogAction");
281       okButton_.setDefault(defaultOverrideButton_ == null);
282       addButton(okButton_, elementId);
283    }
284 
addOkButton(ThemedButton okButton)285    protected void addOkButton(ThemedButton okButton)
286    {
287       addOkButton(okButton, ElementIds.DIALOG_OK_BUTTON);
288    }
289 
setOkButtonCaption(String caption)290    protected void setOkButtonCaption(String caption)
291    {
292       okButton_.setText(caption);
293    }
294 
setOkButtonId(String id)295    protected void setOkButtonId(String id)
296    {
297       okButton_.getElement().setId(id);
298    }
299 
enableOkButton(boolean enabled)300    protected void enableOkButton(boolean enabled)
301    {
302       okButton_.setEnabled(enabled);
303    }
304 
clickOkButton()305    protected void clickOkButton()
306    {
307       okButton_.click();
308    }
309 
setOkButtonVisible(boolean visible)310    protected void setOkButtonVisible(boolean visible)
311    {
312       okButton_.setVisible(visible);
313    }
314 
315    /**
316     * Set focus on the OK button
317     * @return true if button can receive focus, false if button doesn't exist or was disabled
318     */
focusOkButton()319    protected boolean focusOkButton()
320    {
321       if (okButton_ == null || !okButton_.isEnabled() || !okButton_.isVisible())
322          return false;
323 
324       FocusHelper.setFocusDeferred(okButton_);
325       return true;
326    }
327 
enableCancelButton(boolean enabled)328    protected void enableCancelButton(boolean enabled)
329    {
330       cancelButton_.setEnabled(enabled);
331    }
332 
333     /**
334     * Set focus on the cancel button
335     * @return true if button received focus, false if button doesn't exist or was disabled
336     */
focusCancelButton()337     protected boolean focusCancelButton()
338    {
339       if (cancelButton_ == null || !cancelButton_.isEnabled() || !cancelButton_.isVisible())
340          return false;
341       FocusHelper.setFocusDeferred(cancelButton_);
342       return true;
343    }
344 
setDefaultOverrideButton(ThemedButton button)345    protected void setDefaultOverrideButton(ThemedButton button)
346    {
347       if (button != defaultOverrideButton_)
348       {
349          if (defaultOverrideButton_ != null)
350             defaultOverrideButton_.setDefault(false);
351 
352          defaultOverrideButton_ = button;
353          if (okButton_ != null)
354             okButton_.setDefault(defaultOverrideButton_ == null);
355 
356          if (defaultOverrideButton_ != null)
357             defaultOverrideButton_.setDefault(true);
358       }
359    }
360 
addCancelButton(String elementId)361    protected ThemedButton addCancelButton(String elementId)
362    {
363       ThemedButton cancelButton = createCancelButton(null);
364       addCancelButton(cancelButton);
365       ElementIds.assignElementId(cancelButton.getElement(), elementId);
366       return cancelButton;
367    }
368 
addCancelButton()369    protected ThemedButton addCancelButton()
370    {
371       return addCancelButton(ElementIds.DIALOG_CANCEL_BUTTON);
372    }
373 
createCancelButton(final Operation cancelOperation)374    protected ThemedButton createCancelButton(final Operation cancelOperation)
375    {
376       return new ThemedButton("Cancel", clickEvent ->
377       {
378          if (cancelOperation != null)
379             cancelOperation.execute();
380          closeDialog();
381       });
382    }
383 
addCancelButton(ThemedButton cancelButton, String elementId)384    protected void addCancelButton(ThemedButton cancelButton, String elementId)
385    {
386       cancelButton_ = cancelButton;
387       cancelButton_.addStyleDependentName("DialogAction");
388       addButton(cancelButton_, elementId);
389    }
390 
addCancelButton(ThemedButton cancelButton)391    protected void addCancelButton(ThemedButton cancelButton)
392    {
393       addCancelButton(cancelButton, ElementIds.DIALOG_CANCEL_BUTTON);
394    }
395 
addLeftButton(ThemedButton button, String elementId)396    protected void addLeftButton(ThemedButton button, String elementId)
397    {
398       button.addStyleDependentName("DialogAction");
399       button.addStyleDependentName("DialogActionLeft");
400       ElementIds.assignElementId(button.getElement(), elementId);
401       leftButtonPanel_.add(button);
402       allButtons_.add(button);
403    }
404 
addLeftWidget(Widget widget)405    protected void addLeftWidget(Widget widget)
406    {
407       leftButtonPanel_.add(widget);
408    }
409 
removeLeftWidget(Widget widget)410    protected void removeLeftWidget(Widget widget)
411    {
412       leftButtonPanel_.remove(widget);
413    }
414 
addButton(ThemedButton button, String elementId)415    protected void addButton(ThemedButton button, String elementId)
416    {
417       button.addStyleDependentName("DialogAction");
418       buttonPanel_.add(button);
419       ElementIds.assignElementId(button.getElement(), elementId);
420       allButtons_.add(button);
421    }
422 
423    // inserts an action button--in the same panel as OK/cancel, but preceding
424    // them (presuming they're already present)
addActionButton(ThemedButton button)425    protected void addActionButton(ThemedButton button)
426    {
427       button.addStyleDependentName("DialogAction");
428       buttonPanel_.insert(button, 0);
429       allButtons_.add(button);
430    }
431 
setButtonAlignment(HorizontalAlignmentConstant alignment)432    protected void setButtonAlignment(HorizontalAlignmentConstant alignment)
433    {
434       bottomPanel_.setCellHorizontalAlignment(buttonPanel_, alignment);
435    }
436 
addProgressIndicator()437    protected ProgressIndicator addProgressIndicator()
438    {
439       return addProgressIndicator(true);
440    }
441 
addProgressIndicator(final boolean closeOnCompleted)442    protected ProgressIndicator addProgressIndicator(final boolean closeOnCompleted)
443    {
444       final SlideLabel label = new SlideLabel(true);
445       Element labelEl = label.getElement();
446       Style labelStyle = labelEl.getStyle();
447       labelStyle.setPosition(Style.Position.ABSOLUTE);
448       labelStyle.setLeft(0, Style.Unit.PX);
449       labelStyle.setRight(0, Style.Unit.PX);
450       labelStyle.setTop(-12, Style.Unit.PX);
451       mainPanel_.add(label);
452 
453       return new ProgressIndicator()
454       {
455          public void onProgress(String message)
456          {
457             onProgress(message, null);
458          }
459 
460          public void onProgress(String message, Operation onCancel)
461          {
462             if (message == null)
463             {
464                label.setText("", true);
465                if (showing_)
466                   clearProgress();
467             }
468             else
469             {
470                label.setText(message, false);
471                if (!showing_)
472                {
473                   enableControls(false);
474                   label.show();
475                   showing_ = true;
476                }
477             }
478 
479             label.onCancel(onCancel);
480          }
481 
482          public void onCompleted()
483          {
484             clearProgress();
485             if (closeOnCompleted)
486                closeDialog();
487          }
488 
489          public void onError(String message)
490          {
491             clearProgress();
492             RStudioGinjector.INSTANCE.getGlobalDisplay().showErrorMessage(
493                   "Error", message);
494          }
495 
496          @Override
497          public void clearProgress()
498          {
499             if (showing_)
500             {
501                enableControls(true);
502                label.hide();
503                showing_ = false;
504             }
505 
506          }
507 
508          private boolean showing_;
509       };
510    }
511 
512    public void closeDialog()
513    {
514       hide();
515       removeFromParent();
516 
517       // nothing to do if we don't have an element to return focus to
518       if (originallyActiveElement_ == null)
519          return;
520 
521       try
522       {
523          if (restoreFocus_)
524             restoreFocus();
525       }
526       catch (Exception e)
527       {
528          // intentionally swallow exceptions (as they can occur
529          // for a multitude of reasons and generally are not actionable)
530       }
531       finally
532       {
533          originallyActiveElement_ = null;
534       }
535    }
536 
537    private void restoreFocus()
538    {
539       if(originallyActiveElement_ != null)
540          ModalReturnFocus.returnFocus(originallyActiveElement_);
541    }
542 
543    protected SimplePanel getContainerPanel()
544    {
545       return containerPanel_;
546    }
547 
548    protected void enableControls(boolean enabled)
549    {
550       enableButtons(enabled);
551       onEnableControls(enabled);
552    }
553 
554    protected void onEnableControls(boolean enabled)
555    {
556 
557    }
558 
559    @Override
560    public void onPreviewNativeEvent(Event.NativePreviewEvent event)
561    {
562       if (!ModalDialogTracker.isTopMost(this))
563          return;
564 
565       if (event.getTypeInt() == Event.ONKEYDOWN)
566       {
567          NativeEvent nativeEvent = event.getNativeEvent();
568          switch (nativeEvent.getKeyCode())
569          {
570 
571          case KeyCodes.KEY_ENTER:
572          {
573 
574             if (enterDisabled_)
575                break;
576 
577             // allow Enter on textareas, buttons, or anchors (including custom links)
578             Element e = DomUtils.getActiveElement();
579 
580             boolean enterAllowed =
581                   e.hasTagName("TEXTAREA") ||
582                   e.hasTagName("A") ||
583                   e.hasTagName("BUTTON") ||
584                   e.hasClassName(ALLOW_ENTER_KEY_CLASS) ||
585                   (e.hasAttribute("role") && StringUtil.equals(e.getAttribute("role"), "link"));
586 
587             if (enterAllowed)
588                return;
589 
590             ThemedButton defaultButton = defaultOverrideButton_ == null
591                   ? okButton_
592                   : defaultOverrideButton_;
593 
594             if ((defaultButton != null) && defaultButton.isEnabled())
595             {
596                nativeEvent.preventDefault();
597                nativeEvent.stopPropagation();
598                event.cancel();
599                defaultButton.click();
600             }
601 
602             break;
603          }
604 
605          case KeyCodes.KEY_ESCAPE:
606          {
607 
608             if (escapeDisabled_)
609                break;
610 
611             Element e = DomUtils.getActiveElement();
612             if (e.hasClassName(ALLOW_ESCAPE_KEY_CLASS))
613                break;
614 
615             onEscapeKeyDown(event);
616             break;
617          }
618 
619          case KeyCodes.KEY_TAB:
620          {
621             if (nativeEvent.getShiftKey() && focus_.isFirst(DomUtils.getActiveElement()))
622             {
623                nativeEvent.preventDefault();
624                nativeEvent.stopPropagation();
625                event.cancel();
626                focusLastControl();
627             }
628             else if (!nativeEvent.getShiftKey() && focus_.isLast(DomUtils.getActiveElement()))
629             {
630                nativeEvent.preventDefault();
631                nativeEvent.stopPropagation();
632                event.cancel();
633                focusFirstControl();
634             }
635             break;
636          }
637 
638          }
639       }
640    }
641 
642    protected void onEscapeKeyDown(Event.NativePreviewEvent event)
643    {
644       NativeEvent nativeEvent = event.getNativeEvent();
645       if (cancelButton_ == null)
646       {
647          if ((okButton_ != null) && okButton_.isEnabled())
648          {
649             nativeEvent.preventDefault();
650             nativeEvent.stopPropagation();
651             event.cancel();
652             okButton_.click();
653          }
654       }
655       else if (cancelButton_.isEnabled())
656       {
657          nativeEvent.preventDefault();
658          nativeEvent.stopPropagation();
659          event.cancel();
660          cancelButton_.click();
661       }
662    }
663 
664    private void enableButtons(boolean enabled)
665    {
666       for (ThemedButton allButton : allButtons_)
667          allButton.setEnabled(enabled);
668    }
669 
670    public void move(Point p, boolean allowAnimation)
671    {
672       if (!isShowing() || !allowAnimation)
673       {
674          // Don't animate if not showing
675          setPopupPosition(p.getX(), p.getY());
676          return;
677       }
678 
679       if (currentAnimation_ != null)
680       {
681          currentAnimation_.cancel();
682          currentAnimation_ = null;
683       }
684 
685       final int origLeft = getPopupLeft();
686       final int origTop = getPopupTop();
687       final int deltaX = p.getX() - origLeft;
688       final int deltaY = p.getY() - origTop;
689 
690       currentAnimation_ = new Animation()
691       {
692          @Override
693          protected void onUpdate(double progress)
694          {
695             if (!isShowing())
696                cancel();
697             else
698             {
699                setPopupPosition((int) (origLeft + deltaX * progress),
700                      (int) (origTop + deltaY * progress));
701             }
702          }
703       };
704       currentAnimation_.run(200);
705    }
706 
707    @Override
708    public void setText(String text)
709    {
710       super.setText(text);
711       role_.setAriaLabelProperty(getElement(), text);
712    }
713 
714    /**
715     * Optional description of dialog for accessibility tools
716     * @param element element containing the description
717     */
718    protected void setARIADescribedBy(Element element)
719    {
720       String id = element.getId();
721       if (StringUtil.isNullOrEmpty(id))
722       {
723          id = DOM.createUniqueId();
724          element.setId(id);
725       }
726       role_.setAriaDescribedbyProperty(getElement(), Id.of(element));
727    }
728 
729    /**
730     * Set focus on first keyboard focusable element in dialog, as set by
731     * refreshFocusableElements or setFirstFocusableElement.
732     *
733     * Invoked when Tabbing off the last control in the modal dialog to set focus back to
734     * the first control, and by default to set initial focus when the dialog is shown.
735     *
736     * To set focus on a different control when the dialog is displayed, override
737     * focusInitialControl, instead.
738     */
739    protected void focusFirstControl()
740    {
741       focus_.focusFirstControl();
742    }
743 
744     /**
745     * Set focus on last keyboard focusable element in dialog, as set by
746     * <code>refreshFocusableElements</code> or <code>setLastFocusableElement</code>.
747     */
748    protected void focusLastControl()
749    {
750       focus_.focusLastControl();
751    }
752 
753    /**
754     * Invoked when dialog first loads to set initial focus. By default sets focus on the
755     * first control in the dialog; override to set initial focus elsewhere.
756     */
757    protected void focusInitialControl()
758    {
759       focusFirstControl();
760    }
761 
762    /**
763     * Gets an ordered list of keyboard-focusable elements in the dialog.
764     */
765    public ArrayList<Element> getFocusableElements()
766    {
767       return DomUtils.getFocusableElements(getElement());
768    }
769 
770    /**
771     * Gets a list of keyboard focusable elements in the dialog, and tracks which ones are
772     * first and last. This is used to keep keyboard focus in the dialog when Tabbing and
773     * Shift+Tabbing off end or beginning of dialog.
774     *
775     * If the dialog is dynamic, and the first and/or last focusable elements change over time,
776     * call this function again to update the information; or, if the auto-detection
777     * is not suitable, override focusFirstControl and/or focusLastControl.
778     */
779    public void refreshFocusableElements()
780    {
781       if (!DomUtils.isEffectivelyVisible(getElement()))
782          return;
783 
784       ArrayList<Element> focusable = getFocusableElements();
785       if (focusable.size() == 0)
786       {
787          Debug.logWarning("No potentially focusable controls found in modal dialog");
788          return;
789       }
790       focus_.setFirst(focusable.get(0));
791       focus_.setLast(focusable.get(focusable.size() - 1));
792    }
793 
794    /**
795     * Perform a deferred update of focusable elements, then set focus on the initial control.
796     */
797    public void deferRefreshFocusableElements()
798    {
799       Scheduler.get().scheduleDeferred(() ->
800       {
801          refreshFocusableElements();
802          focusInitialControl();
803       });
804    }
805 
806    public interface Styles extends CssResource
807    {
808       String modalDialog();
809    }
810 
811    public interface Resources extends ClientBundle
812    {
813       @Source("ModalDialogBase.css")
814       Styles styles();
815    }
816 
817    @Override
818    public void reportStatus(String status, int delayMs, Severity severity)
819    {
820       ariaLiveStatusWidget_.reportStatus(status, delayMs, severity);
821    }
822 
823    private static final Resources RES = GWT.create(Resources.class);
824    static
825    {
826       RES.styles().ensureInjected();
827    }
828 
829    private Handle shortcutDisableHandle_;
830 
831    private boolean escapeDisabled_ = false;
832    private boolean enterDisabled_ = false;
833    private final SimplePanel containerPanel_;
834    private final VerticalPanel mainPanel_;
835    private final HorizontalPanel bottomPanel_;
836    private final HorizontalPanel buttonPanel_;
837    private final HorizontalPanel leftButtonPanel_;
838    private ThemedButton okButton_;
839    private ThemedButton cancelButton_;
840    private ThemedButton defaultOverrideButton_;
841    private final ArrayList<ThemedButton> allButtons_ = new ArrayList<>();
842    private Widget mainWidget_;
843    private boolean restoreFocus_;
844    private Element originallyActiveElement_;
845    private Animation currentAnimation_ = null;
846    private final DialogRole role_;
847    private final AriaLiveStatusWidget ariaLiveStatusWidget_;
848    private final FocusHelper focus_;
849 
850    public static final String ALLOW_ENTER_KEY_CLASS = "__rstudio_modal_allow_enter_key";
851    public static final String ALLOW_ESCAPE_KEY_CLASS = "__rstudio_modal_allow_escape_key";
852 }
853