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