@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Replace the "Choose Subtype" radio buttons dialog with a simpler "big stuff you click" sort of UI

Summary:
Ref T13222. Fixes T12588. See PHI683. In several cases, we present the user with a choice between multiple major options: Alamnac service types, Drydock blueprint types, Repository VCS types, Herald rule types, etc.

Today, we generally do this with radio buttons and a "Submit" button. This isn't terrible, but often it means users have to click twice (once on the radio; once on submit) when a single click would be sufficient. The radio click target can also be small.

In other cases, we have a container with a link and we'd like to link the entire container: notifications, the `/drydock/` console, etc. We'd like to just link the entire container, but this causes some problems:

- It's not legal to link block eleements like `<a><div> ... </div></a>` and some browsers actually get upset about it.
- We can `<a><span> ... </span></a>` instead, then turn the `<span>` into a block element with CSS -- and this sometimes works, but also has some drawbacks:
- It's not great to do that for screenreaders, since the readable text in the link isn't necessarily very meaningful.
- We can't have any other links inside the element (e.g., details or documentation).
- We can `<form><button> ... </button></form>` instead, but this has its own set of problems:
- You can't right-click to interact with a button in the same way you can with a link.
- Also not great for screenreaders.

Instead, try adding a `linked-container` behavior which just means "when users click this element, pretend they clicked the first link inside it".

This gives us natural HTML (real, legal HTML with actual `<a>` tags) and good screenreader behavior, but allows the effective link target to be visually larger than just the link.

If no issues crop up with this, I'd plan to eventually use this technique in more places (Repositories, Herald, Almanac, Drydock, Notifications menu, etc).

Test Plan:
{F6053035}

- Left-clicked and command-left-clicked the new JS fanciness, got sensible behaviors.

Reviewers: amckinley

Reviewed By: amckinley

Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam

Maniphest Tasks: T13222, T12588

Differential Revision: https://secure.phabricator.com/D19855

+121 -43
+12 -6
resources/celerity/map.php
··· 9 9 'names' => array( 10 10 'conpherence.pkg.css' => 'e68cf1fa', 11 11 'conpherence.pkg.js' => '15191c65', 12 - 'core.pkg.css' => 'cff4ff6f', 12 + 'core.pkg.css' => '9d1148a4', 13 13 'core.pkg.js' => '4bde473b', 14 14 'differential.pkg.css' => '06dc617c', 15 15 'differential.pkg.js' => 'ef0b989b', ··· 127 127 'rsrc/css/phui/calendar/phui-calendar-list.css' => '576be600', 128 128 'rsrc/css/phui/calendar/phui-calendar-month.css' => '21154caf', 129 129 'rsrc/css/phui/calendar/phui-calendar.css' => 'f1ddf11c', 130 - 'rsrc/css/phui/object-item/phui-oi-big-ui.css' => '628f59de', 130 + 'rsrc/css/phui/object-item/phui-oi-big-ui.css' => '7a7c22af', 131 131 'rsrc/css/phui/object-item/phui-oi-color.css' => 'cd2b9b77', 132 132 'rsrc/css/phui/object-item/phui-oi-drag-ui.css' => '08f4ccc3', 133 133 'rsrc/css/phui/object-item/phui-oi-flush-ui.css' => '9d9685d6', ··· 469 469 'rsrc/js/core/behavior-keyboard-shortcuts.js' => '01fca1f0', 470 470 'rsrc/js/core/behavior-lightbox-attachments.js' => '6b31879a', 471 471 'rsrc/js/core/behavior-line-linker.js' => '66a62306', 472 + 'rsrc/js/core/behavior-linked-container.js' => '291da458', 472 473 'rsrc/js/core/behavior-more.js' => 'a80d0378', 473 474 'rsrc/js/core/behavior-object-selector.js' => '77c1f0b0', 474 475 'rsrc/js/core/behavior-oncopy.js' => '2926fff2', ··· 616 617 'javelin-behavior-launch-icon-composer' => '48086888', 617 618 'javelin-behavior-lightbox-attachments' => '6b31879a', 618 619 'javelin-behavior-line-chart' => 'e4232876', 620 + 'javelin-behavior-linked-container' => '291da458', 619 621 'javelin-behavior-maniphest-batch-selector' => 'ad54037e', 620 622 'javelin-behavior-maniphest-list-editor' => 'a9f88de2', 621 623 'javelin-behavior-maniphest-subpriority-editor' => '71237763', ··· 832 834 'phui-lightbox-css' => '0a035e40', 833 835 'phui-list-view-css' => '38f8c9bd', 834 836 'phui-object-box-css' => '9cff003c', 835 - 'phui-oi-big-ui-css' => '628f59de', 837 + 'phui-oi-big-ui-css' => '7a7c22af', 836 838 'phui-oi-color-css' => 'cd2b9b77', 837 839 'phui-oi-drag-ui-css' => '08f4ccc3', 838 840 'phui-oi-flush-ui-css' => '9d9685d6', ··· 1027 1029 'javelin-uri', 1028 1030 'phabricator-notification', 1029 1031 ), 1032 + '291da458' => array( 1033 + 'javelin-behavior', 1034 + 'javelin-dom', 1035 + ), 1030 1036 '2926fff2' => array( 1031 1037 'javelin-behavior', 1032 1038 'javelin-dom', ··· 1348 1354 'javelin-magical-init', 1349 1355 'javelin-util', 1350 1356 ), 1351 - '628f59de' => array( 1352 - 'phui-oi-list-view-css', 1353 - ), 1354 1357 '62dfea03' => array( 1355 1358 'javelin-install', 1356 1359 'javelin-util', ··· 1507 1510 '7a68dda3' => array( 1508 1511 'owners-path-editor', 1509 1512 'javelin-behavior', 1513 + ), 1514 + '7a7c22af' => array( 1515 + 'phui-oi-list-view-css', 1510 1516 ), 1511 1517 '7cbe244b' => array( 1512 1518 'javelin-install',
+4
src/applications/drydock/controller/DrydockConsoleController.php
··· 34 34 ->setHeader(pht('Blueprints')) 35 35 ->setImageIcon('fa-map-o') 36 36 ->setHref($this->getApplicationURI('blueprint/')) 37 + ->setClickable(true) 37 38 ->addAttribute( 38 39 pht( 39 40 'Configure blueprints so Drydock can build resources, like '. ··· 44 45 ->setHeader(pht('Resources')) 45 46 ->setImageIcon('fa-map') 46 47 ->setHref($this->getApplicationURI('resource/')) 48 + ->setClickable(true) 47 49 ->addAttribute( 48 50 pht('View and manage resources Drydock has built, like hosts.'))); 49 51 ··· 52 54 ->setHeader(pht('Leases')) 53 55 ->setImageIcon('fa-link') 54 56 ->setHref($this->getApplicationURI('lease/')) 57 + ->setClickable(true) 55 58 ->addAttribute(pht('Manage leases on resources.'))); 56 59 57 60 $menu->addItem( ··· 59 62 ->setHeader(pht('Repository Operations')) 60 63 ->setImageIcon('fa-fighter-jet') 61 64 ->setHref($this->getApplicationURI('operation/')) 65 + ->setClickable(true) 62 66 ->addAttribute(pht('Review the repository operation queue.'))); 63 67 64 68 $crumbs = $this->buildApplicationCrumbs();
+21 -26
src/applications/maniphest/controller/ManiphestTaskSubtaskController.php
··· 37 37 ->addCancelButton($cancel_uri, pht('Close')); 38 38 } 39 39 40 - if ($request->isFormPost()) { 41 - $form_key = $request->getStr('formKey'); 42 - if (isset($subtype_options[$form_key])) { 43 - $subtask_uri = id(new PhutilURI("/task/edit/form/{$form_key}/")) 44 - ->setQueryParam('parent', $id) 45 - ->setQueryParam('template', $id) 46 - ->setQueryParam('status', ManiphestTaskStatus::getDefaultStatus()); 47 - $subtask_uri = $this->getApplicationURI($subtask_uri); 40 + $menu = id(new PHUIObjectItemListView()) 41 + ->setUser($viewer) 42 + ->setBig(true) 43 + ->setFlush(true); 48 44 49 - return id(new AphrontRedirectResponse()) 50 - ->setURI($subtask_uri); 51 - } 52 - } 45 + foreach ($subtype_options as $form_key => $subtype_form) { 46 + $subtype_key = $subtype_form->getSubtype(); 47 + $subtype = $subtype_map->getSubtype($subtype_key); 48 + 49 + $subtask_uri = id(new PhutilURI("/task/edit/form/{$form_key}/")) 50 + ->setQueryParam('parent', $id) 51 + ->setQueryParam('template', $id) 52 + ->setQueryParam('status', ManiphestTaskStatus::getDefaultStatus()); 53 + $subtask_uri = $this->getApplicationURI($subtask_uri); 53 54 54 - $control = id(new AphrontFormRadioButtonControl()) 55 - ->setName('formKey') 56 - ->setLabel(pht('Subtype')); 55 + $item = id(new PHUIObjectItemView()) 56 + ->setHeader($subtype_form->getDisplayName()) 57 + ->setHref($subtask_uri) 58 + ->setClickable(true) 59 + ->setImageIcon($subtype->newIconView()) 60 + ->addAttribute($subtype->getName()); 57 61 58 - foreach ($subtype_options as $key => $subtype_form) { 59 - $control->addButton( 60 - $key, 61 - $subtype_form->getDisplayName(), 62 - null); 62 + $menu->addItem($item); 63 63 } 64 64 65 - $form = id(new AphrontFormView()) 66 - ->setViewer($viewer) 67 - ->appendControl($control); 68 - 69 65 return $this->newDialog() 70 66 ->setTitle(pht('Choose Subtype')) 71 - ->appendForm($form) 72 - ->addSubmitButton(pht('Continue')) 67 + ->appendChild($menu) 73 68 ->addCancelButton($cancel_uri); 74 69 } 75 70
+5
src/applications/transactions/editengine/PhabricatorEditEngineSubtype.php
··· 239 239 return new PhabricatorEditEngineSubtypeMap($map); 240 240 } 241 241 242 + public function newIconView() { 243 + return id(new PHUIIconView()) 244 + ->setIcon($this->getIcon(), $this->getColor()); 245 + } 246 + 242 247 }
+1 -1
src/view/AphrontDialogView.php
··· 404 404 $header), 405 405 phutil_tag('div', 406 406 array( 407 - 'class' => 'aphront-dialog-body phabricator-remarkup grouped', 407 + 'class' => 'aphront-dialog-body grouped', 408 408 ), 409 409 $children), 410 410 $tail,
+18 -10
src/view/phui/PHUIObjectItemView.php
··· 28 28 private $sideColumn; 29 29 private $coverImage; 30 30 private $description; 31 + private $clickable; 31 32 32 33 private $selectableName; 33 34 private $selectableValue; ··· 179 180 return $this; 180 181 } 181 182 183 + public function setClickable($clickable) { 184 + $this->clickable = $clickable; 185 + return $this; 186 + } 187 + 188 + public function getClickable() { 189 + return $this->clickable; 190 + } 191 + 182 192 public function setEpoch($epoch) { 183 193 $date = phabricator_datetime($epoch, $this->getUser()); 184 194 $this->addIcon('none', $date); ··· 332 342 $item_classes[] = 'phui-oi-with-image-icon'; 333 343 } 334 344 345 + if ($this->getClickable()) { 346 + Javelin::initBehavior('linked-container'); 347 + 348 + $item_classes[] = 'phui-oi-linked-container'; 349 + $sigils[] = 'linked-container'; 350 + } 351 + 335 352 return array( 336 353 'class' => $item_classes, 337 354 'sigil' => $sigils, ··· 443 460 ), 444 461 $spec['label']); 445 462 446 - if (isset($spec['attributes']['href'])) { 447 - $icon_href = phutil_tag( 448 - 'a', 449 - array('href' => $spec['attributes']['href']), 450 - array($icon, $label)); 451 - } else { 452 - $icon_href = array($icon, $label); 453 - } 454 - 455 463 $classes = array(); 456 464 $classes[] = 'phui-oi-icon'; 457 465 if (isset($spec['attributes']['class'])) { ··· 463 471 array( 464 472 'class' => implode(' ', $classes), 465 473 ), 466 - $icon_href); 474 + $icon); 467 475 } 468 476 469 477 $icons[] = phutil_tag(
+13
webroot/rsrc/css/phui/object-item/phui-oi-big-ui.css
··· 59 59 background-color: {$hoverblue}; 60 60 border-radius: 3px; 61 61 } 62 + 63 + .device-desktop .phui-oi-linked-container { 64 + cursor: pointer; 65 + } 66 + 67 + .device-desktop .phui-oi-linked-container:hover { 68 + background-color: {$hoverblue}; 69 + border-radius: 3px; 70 + } 71 + 72 + .device-desktop .phui-oi-linked-container a:hover { 73 + text-decoration: none; 74 + }
+47
webroot/rsrc/js/core/behavior-linked-container.js
··· 1 + /** 2 + * @provides javelin-behavior-linked-container 3 + * @requires javelin-behavior javelin-dom 4 + */ 5 + 6 + JX.behavior('linked-container', function() { 7 + 8 + JX.Stratcom.listen( 9 + 'click', 10 + 'linked-container', 11 + function(e) { 12 + 13 + // If the user clicked some link inside the container, bail out and just 14 + // click the link. 15 + if (e.getNode('tag:a')) { 16 + return; 17 + } 18 + 19 + // If this is some sort of unusual click, bail out. Note that we'll 20 + // handle "Left Click" and "Command + Left Click" differently, below. 21 + if (!e.isLeftButton()) { 22 + return; 23 + } 24 + 25 + var container = e.getNode('linked-container'); 26 + 27 + // Find the first link in the container. We're going to pretend the user 28 + // clicked it. 29 + var link = JX.DOM.scry(container, 'a')[0]; 30 + if (!link) { 31 + return; 32 + } 33 + 34 + // If the click is a "Command + Left Click", change the target of the 35 + // link so we open it in a new tab. 36 + var is_command = !!e.getRawEvent().metaKey; 37 + if (is_command) { 38 + var old_target = link.target; 39 + link.target = '_blank'; 40 + link.click(); 41 + link.target = old_target; 42 + } else { 43 + link.click(); 44 + } 45 + }); 46 + 47 + });