diff --git a/resources/celerity/map.php b/resources/celerity/map.php --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,8 +7,8 @@ return array( 'names' => array( - 'core.pkg.css' => 'fcca0300', - 'core.pkg.js' => 'e60bc550', + 'core.pkg.css' => '2a1577d2', + 'core.pkg.js' => 'ad837c4b', 'darkconsole.pkg.js' => 'ca8671ce', 'differential.pkg.css' => '8a064eb7', 'differential.pkg.js' => 'a2f45b5f', @@ -105,7 +105,7 @@ 'rsrc/css/application/subscriptions/subscribers-list.css' => '5bb30c78', 'rsrc/css/application/tokens/tokens.css' => '5f7bca25', 'rsrc/css/application/uiexample/example.css' => '528b19de', - 'rsrc/css/core/core.css' => '7dff07c3', + 'rsrc/css/core/core.css' => 'ef1b7892', 'rsrc/css/core/remarkup.css' => '80c3a48c', 'rsrc/css/core/syntax.css' => '3c18c1cb', 'rsrc/css/core/z-index.css' => 'efb673ac', @@ -126,7 +126,7 @@ 'rsrc/css/phui/calendar/phui-calendar-month.css' => 'a92e47d2', 'rsrc/css/phui/calendar/phui-calendar.css' => '5e1ad989', 'rsrc/css/phui/phui-box.css' => '7b3a2eed', - 'rsrc/css/phui/phui-button.css' => '73b33963', + 'rsrc/css/phui/phui-button.css' => '3dbdbf0d', 'rsrc/css/phui/phui-document.css' => '3b078dc0', 'rsrc/css/phui/phui-feed-story.css' => '3a59c2cf', 'rsrc/css/phui/phui-fontkit.css' => 'de84aa4a', @@ -145,7 +145,7 @@ 'rsrc/css/phui/phui-status.css' => '2f562399', 'rsrc/css/phui/phui-tag-view.css' => '295d81c4', 'rsrc/css/phui/phui-text.css' => '23e9b4b7', - 'rsrc/css/phui/phui-timeline-view.css' => '27b280ca', + 'rsrc/css/phui/phui-timeline-view.css' => 'e9efa60a', 'rsrc/css/phui/phui-workboard-view.css' => '84f2c272', 'rsrc/css/phui/phui-workpanel-view.css' => '97b69459', 'rsrc/css/sprite-actions.css' => '969ad0e5', @@ -414,7 +414,7 @@ 'rsrc/js/application/search/behavior-reorder-queries.js' => '37871df4', 'rsrc/js/application/slowvote/behavior-slowvote-embed.js' => 'a51fdb2e', 'rsrc/js/application/transactions/behavior-transaction-comment-form.js' => '9084a36f', - 'rsrc/js/application/transactions/behavior-transaction-list.js' => 'b8e6b76f', + 'rsrc/js/application/transactions/behavior-transaction-list.js' => 'cf656c84', 'rsrc/js/application/uiexample/JavelinViewExample.js' => 'd4a14807', 'rsrc/js/application/uiexample/ReactorButtonExample.js' => '44524435', 'rsrc/js/application/uiexample/ReactorCheckboxExample.js' => '7ba325ee', @@ -479,9 +479,10 @@ 'rsrc/js/core/behavior-workflow.js' => 'fee00761', 'rsrc/js/core/phtize.js' => 'd254d646', 'rsrc/js/phui/behavior-phui-object-box-tabs.js' => 'a3e2244e', + 'rsrc/js/phui/behavior-phui-timeline-dropdown-menu.js' => '4d94d9c3', 'rsrc/js/phuix/PHUIXActionListView.js' => 'b5c256b8', 'rsrc/js/phuix/PHUIXActionView.js' => '19a0b148', - 'rsrc/js/phuix/PHUIXDropdownMenu.js' => 'bc3bd59c', + 'rsrc/js/phuix/PHUIXDropdownMenu.js' => 'bd4c8dca', 'rsrc/swf/aphlict.swf' => 'abac967d', ), 'symbols' => @@ -610,12 +611,13 @@ 'javelin-behavior-phabricator-show-all-transactions' => '7c273581', 'javelin-behavior-phabricator-tooltips' => '48db4145', 'javelin-behavior-phabricator-transaction-comment-form' => '9084a36f', - 'javelin-behavior-phabricator-transaction-list' => 'b8e6b76f', + 'javelin-behavior-phabricator-transaction-list' => 'cf656c84', 'javelin-behavior-phabricator-watch-anchor' => '06e05112', 'javelin-behavior-phame-post-preview' => '61d927ec', 'javelin-behavior-pholio-mock-edit' => '1e1e8bb0', 'javelin-behavior-pholio-mock-view' => '28497740', 'javelin-behavior-phui-object-box-tabs' => 'a3e2244e', + 'javelin-behavior-phui-timeline-dropdown-menu' => '4d94d9c3', 'javelin-behavior-policy-control' => 'bc99b0f2', 'javelin-behavior-policy-rule-editor' => '263aeb8c', 'javelin-behavior-ponder-votebox' => '327dbe61', @@ -687,7 +689,7 @@ 'phabricator-busy' => '6453c869', 'phabricator-chatlog-css' => '852140ff', 'phabricator-content-source-view-css' => '4b8b05d4', - 'phabricator-core-css' => '7dff07c3', + 'phabricator-core-css' => 'ef1b7892', 'phabricator-countdown-css' => '86b7b0a0', 'phabricator-crumbs-view-css' => '0222cbe0', 'phabricator-drag-and-drop-file-upload' => 'ae6abfba', @@ -744,7 +746,7 @@ 'phrequent-css' => 'ffc185ad', 'phriction-document-css' => '7d7f0071', 'phui-box-css' => '7b3a2eed', - 'phui-button-css' => '73b33963', + 'phui-button-css' => '3dbdbf0d', 'phui-calendar-css' => '5e1ad989', 'phui-calendar-day-css' => 'de035c8a', 'phui-calendar-list-css' => 'c1d0ca59', @@ -768,12 +770,12 @@ 'phui-status-list-view-css' => '2f562399', 'phui-tag-view-css' => '295d81c4', 'phui-text-css' => '23e9b4b7', - 'phui-timeline-view-css' => '27b280ca', + 'phui-timeline-view-css' => 'e9efa60a', 'phui-workboard-view-css' => '84f2c272', 'phui-workpanel-view-css' => '97b69459', 'phuix-action-list-view' => 'b5c256b8', 'phuix-action-view' => '19a0b148', - 'phuix-dropdown-menu' => 'bc3bd59c', + 'phuix-dropdown-menu' => 'bd4c8dca', 'policy-css' => '957ea14c', 'policy-edit-css' => '05cca26a', 'policy-transaction-detail-css' => '82100a43', @@ -1131,6 +1133,13 @@ 2 => 'phabricator-drag-and-drop-file-upload', 3 => 'phabricator-textareautils', ), + '4d94d9c3' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-stratcom', + 2 => 'javelin-dom', + 3 => 'phuix-dropdown-menu', + ), '4e37e4de' => array( 0 => 'javelin-install', @@ -1595,16 +1604,6 @@ 4 => 'javelin-install', 5 => 'javelin-util', ), - 'b8e6b76f' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-stratcom', - 2 => 'javelin-workflow', - 3 => 'javelin-dom', - 4 => 'javelin-fx', - 5 => 'javelin-util', - 6 => 'phabricator-textareautils', - ), 'b98fc918' => array( 0 => 'javelin-behavior', @@ -1617,14 +1616,6 @@ 0 => 'javelin-install', 1 => 'javelin-dom', ), - 'bc3bd59c' => - array( - 0 => 'javelin-install', - 1 => 'javelin-util', - 2 => 'javelin-dom', - 3 => 'javelin-vector', - 4 => 'javelin-stratcom', - ), 'bc778103' => array( 0 => 'javelin-behavior', @@ -1642,6 +1633,14 @@ 5 => 'phuix-action-view', 6 => 'javelin-workflow', ), + 'bd4c8dca' => + array( + 0 => 'javelin-install', + 1 => 'javelin-util', + 2 => 'javelin-dom', + 3 => 'javelin-vector', + 4 => 'javelin-stratcom', + ), 'bdaf4d04' => array( 0 => 'javelin-behavior', @@ -1756,6 +1755,16 @@ 6 => 'javelin-vector', 7 => 'phabricator-tooltip', ), + 'cf656c84' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-stratcom', + 2 => 'javelin-workflow', + 3 => 'javelin-dom', + 4 => 'javelin-fx', + 5 => 'javelin-uri', + 6 => 'phabricator-textareautils', + ), 'cf76cfd5' => array( 0 => 'javelin-behavior', diff --git a/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentEditController.php b/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentEditController.php --- a/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentEditController.php +++ b/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentEditController.php @@ -54,10 +54,7 @@ ->applyEdit($xaction, $comment); if ($request->isAjax()) { - return id(new PhabricatorApplicationTransactionResponse()) - ->setViewer($user) - ->setTransactions(array($xaction)) - ->setAnchorOffset($request->getStr('anchor')); + return id(new AphrontAjaxResponse())->setContent(array()); } else { return id(new AphrontReloadResponse())->setURI($obj_handle->getURI()); } diff --git a/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentRemoveController.php b/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentRemoveController.php --- a/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentRemoveController.php +++ b/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentRemoveController.php @@ -47,10 +47,7 @@ ->applyEdit($xaction, $comment); if ($request->isAjax()) { - return id(new PhabricatorApplicationTransactionResponse()) - ->setViewer($viewer) - ->setTransactions(array($xaction)) - ->setAnchorOffset($request->getStr('anchor')); + return id(new AphrontAjaxResponse())->setContent(array()); } else { return id(new AphrontReloadResponse())->setURI($obj_handle->getURI()); } diff --git a/src/applications/transactions/view/PhabricatorApplicationTransactionView.php b/src/applications/transactions/view/PhabricatorApplicationTransactionView.php --- a/src/applications/transactions/view/PhabricatorApplicationTransactionView.php +++ b/src/applications/transactions/view/PhabricatorApplicationTransactionView.php @@ -169,9 +169,6 @@ 'listID' => $list_id, 'objectPHID' => $this->getObjectPHID(), 'nextAnchor' => $this->anchorOffset + count($events), - 'historyLink' => '/transactions/history/', - 'historyLinkText' => pht('Edited'), - 'linkDelimiter' => PHUITimelineEventView::DELIMITER, )); } diff --git a/src/view/layout/PhabricatorActionView.php b/src/view/layout/PhabricatorActionView.php --- a/src/view/layout/PhabricatorActionView.php +++ b/src/view/layout/PhabricatorActionView.php @@ -12,6 +12,16 @@ private $download; private $objectURI; private $sigils = array(); + private $metadata; + + public function setMetadata($metadata) { + $this->metadata = $metadata; + return $this; + } + + public function getMetadata() { + return $this->metadata; + } public function setObjectURI($object_uri) { $this->objectURI = $object_uri; @@ -138,6 +148,7 @@ 'action' => $this->getHref(), 'method' => 'POST', 'sigil' => $sigils, + 'meta' => $this->metadata, ), $item); } else { @@ -147,6 +158,7 @@ 'href' => $this->getHref(), 'class' => 'phabricator-action-view-item', 'sigil' => $sigils, + 'meta' => $this->metadata, ), $this->name); } diff --git a/src/view/phui/PHUITimelineEventView.php b/src/view/phui/PHUITimelineEventView.php --- a/src/view/phui/PHUITimelineEventView.php +++ b/src/view/phui/PHUITimelineEventView.php @@ -163,7 +163,7 @@ return $this; } - protected function renderEventTitle($is_first_event, $force_icon) { + protected function renderEventTitle($is_first_event, $force_icon, $has_menu) { $title = $this->title; if (($title === null) && !$this->hasChildren()) { $title = ''; @@ -181,7 +181,6 @@ $extra = javelin_tag( 'span', array( - 'sigil' => 'timeline-extra', 'class' => 'phui-timeline-extra', ), phutil_implode_html(self::DELIMITER, $extra)); @@ -198,6 +197,10 @@ $title_classes[] = 'phui-timeline-title-with-icon'; } + if ($has_menu) { + $title_classes[] = 'phui-timeline-title-with-menu'; + } + if ($this->icon) { $fill_classes = array(); $fill_classes[] = 'phui-timeline-icon-fill'; @@ -253,11 +256,71 @@ $events = array_select_keys($events, $icon_keys) + $events; $force_icon = (bool)$icon_keys; + $menu = null; + $items = array(); + $has_menu = false; + if (!$this->getIsPreview()) { + foreach ($this->getEventGroup() as $event) { + $items[] = $event->getMenuItems($this->anchor); + if ($event->hasChildren()) { + $has_menu = true; + } + } + $items = array_mergev($items); + } + + if ($items || $has_menu) { + $icon = id(new PHUIIconView()) + ->setIconFont('fa-cog'); + $aural = javelin_tag( + 'span', + array( + 'aural' => true, + ), + pht('Comment Actions')); + + if ($items) { + $sigil = 'phui-timeline-menu'; + Javelin::initBehavior('phui-timeline-dropdown-menu'); + } else { + $sigil = null; + } + + $action_list = id(new PhabricatorActionListView()) + ->setUser($this->getUser()); + foreach ($items as $item) { + $action_list->addAction($item); + } + + $menu = javelin_tag( + $items ? 'a' : 'span', + array( + 'href' => '#', + 'class' => 'phui-timeline-menu', + 'sigil' => $sigil, + 'aria-haspopup' => 'true', + 'aria-expanded' => 'false', + 'meta' => array( + 'items' => hsprintf('%s', $action_list), + ), + ), + array( + $aural, + $icon, + )); + + $has_menu = true; + } + $group_titles = array(); + $group_items = array(); $group_children = array(); $is_first_event = true; foreach ($events as $event) { - $group_titles[] = $event->renderEventTitle($is_first_event, $force_icon); + $group_titles[] = $event->renderEventTitle( + $is_first_event, + $force_icon, + $has_menu); $is_first_event = false; if ($event->hasChildren()) { $group_children[] = $event->renderChildren(); @@ -296,6 +359,7 @@ ), array( $group_titles, + $menu, phutil_tag( 'div', array( @@ -368,61 +432,9 @@ if ($this->getIsPreview()) { $extra[] = pht('PREVIEW'); } else { - $xaction_phid = $this->getTransactionPHID(); - - if ($this->getQuoteTargetID()) { - - $ref = null; - if ($this->getQuoteRef()) { - $ref = $this->getQuoteRef(); - if ($this->anchor) { - $ref = $ref.'#'.$this->anchor; - } - } - - $extra[] = javelin_tag( - 'a', - array( - 'href' => '#', - 'sigil' => 'transaction-quote', - 'mustcapture' => true, - 'meta' => array( - 'targetID' => $this->getQuoteTargetID(), - 'uri' => '/transactions/quote/'.$xaction_phid.'/', - 'ref' => $ref, - ), - ), - pht('Quote')); - } if ($this->getIsEdited()) { - $extra[] = javelin_tag( - 'a', - array( - 'href' => '/transactions/history/'.$xaction_phid.'/', - 'sigil' => 'workflow transaction-edit-history', - ), - pht('Edited')); - } - - if ($this->getIsEditable()) { - $extra[] = javelin_tag( - 'a', - array( - 'href' => '/transactions/edit/'.$xaction_phid.'/', - 'sigil' => 'workflow transaction-edit', - ), - pht('Edit')); - } - - if ($this->getIsRemovable()) { - $extra[] = javelin_tag( - 'a', - array( - 'href' => '/transactions/remove/'.$xaction_phid.'/', - 'sigil' => 'workflow transaction-remove', - ), - pht('Remove')); + $extra[] = pht('Edited'); } if ($is_first_extra) { @@ -469,9 +481,73 @@ $extra[] = $date; } } + } return $extra; } + private function getMenuItems($anchor) { + $xaction_phid = $this->getTransactionPHID(); + + $items = array(); + if ($this->getQuoteTargetID()) { + + $ref = null; + if ($this->getQuoteRef()) { + $ref = $this->getQuoteRef(); + if ($anchor) { + $ref = $ref.'#'.$anchor; + } + } + + $items[] = id(new PhabricatorActionView()) + ->setIcon('comment') + ->setHref('#') + ->setName(pht('Quote')) + ->addSigil('transaction-quote') + ->setMetadata( + array( + 'targetID' => $this->getQuoteTargetID(), + 'uri' => '/transactions/quote/'.$xaction_phid.'/', + 'ref' => $ref, + )); + } + + if ($this->getIsEditable()) { + $items[] = id(new PhabricatorActionView()) + ->setIcon('edit') + ->setHref('/transactions/edit/'.$xaction_phid.'/') + ->setName(pht('Edit Comment')) + ->addSigil('transaction-edit') + ->setMetadata( + array( + 'anchor' => $anchor, + )); + } + + if ($this->getIsRemovable()) { + $items[] = id(new PhabricatorActionView()) + ->setIcon('delete') + ->setHref('/transactions/remove/'.$xaction_phid.'/') + ->setName(pht('Remove Comment')) + ->addSigil('transaction-remove') + ->setMetadata( + array( + 'anchor' => $anchor, + )); + + } + + if ($this->getIsEdited()) { + $items[] = id(new PhabricatorActionView()) + ->setIcon('transcript') + ->setHref('/transactions/history/'.$xaction_phid.'/') + ->setName(pht('View Edit History')) + ->setWorkflow(true); + } + + return $items; + } + } diff --git a/webroot/rsrc/css/core/core.css b/webroot/rsrc/css/core/core.css --- a/webroot/rsrc/css/core/core.css +++ b/webroot/rsrc/css/core/core.css @@ -127,6 +127,13 @@ .aural-only { position: absolute !important; clip: rect(1px, 1px, 1px, 1px); + + /* NOTE: Without this, Safari sometimes lays these elements out at normal + size. An example is the label on the comment action menu on timelines. */ + + width: 0; + height: 0; + overflow: hidden; } .visual-only { @@ -135,6 +142,9 @@ .audible .aural-only { clip: auto; + width: auto; + height: auto; + overflow: auto; background: #006699; color: #ffffff; } diff --git a/webroot/rsrc/css/phui/phui-button.css b/webroot/rsrc/css/phui/phui-button.css --- a/webroot/rsrc/css/phui/phui-button.css +++ b/webroot/rsrc/css/phui/phui-button.css @@ -98,12 +98,12 @@ button.grey:active, a.grey:active, button.grey_active, -a.button.dropdown-open { +a.button.phuix-dropdown-open { background-color: #7d7d7d; box-shadow: inset 0 0 4px rgba(0,0,0,.2); } -a.dropdown-open { +a.phuix-dropdown-open { color: {$greytext}; } @@ -177,6 +177,12 @@ border-bottom-color: {$greyborder}; } +.phuix-dropdown-menu a:focus { + /* We automatically focus links in dropdown menus for assistive devices, but + this is distracting for visual user agents. */ + outline: none; +} + a.policy-control { width: 240px; text-align: left; diff --git a/webroot/rsrc/css/phui/phui-timeline-view.css b/webroot/rsrc/css/phui/phui-timeline-view.css --- a/webroot/rsrc/css/phui/phui-timeline-view.css +++ b/webroot/rsrc/css/phui/phui-timeline-view.css @@ -125,6 +125,10 @@ padding-left: 38px; } +.phui-timeline-title-with-menu { + padding-right: 36px; +} + .phui-timeline-view .phui-icon-view.phui-timeline-token { vertical-align: middle; margin-right: 4px; @@ -315,3 +319,48 @@ font-weight: normal; color: {$bluetext}; } + +.phui-timeline-comment-actions .phui-icon-view { + width: 16px; + height: 16px; + font-size: 16px; + text-align: center; + overflow: hidden; +} + +.phui-timeline-menu { + position: absolute; + right: 3px; + top: 4px; + width: 28px; + height: 22px; + text-align: center; + line-height: 22px; + font-size: 15px; + border-left: 1px solid {$lightblueborder}; +} + +.phui-timeline-menu:focus { + outline: none; +} + +.phui-timeline-menu .phui-icon-view { + color: {$lightgreytext}; +} + +a.phui-timeline-menu .phui-icon-view { + color: {$bluetext}; +} + +.device-desktop a.phui-timeline-menu:hover .phui-icon-view { + color: {$darkgreytext}; +} + +.phui-timeline-menu.phuix-dropdown-open { + background: {$blue}; +} + +.phui-timeline-menu.phuix-dropdown-open .phui-icon-view, +.device-desktop a.phuix-dropdown-open:hover .phui-icon-view { + color: #ffffff; +} diff --git a/webroot/rsrc/js/application/transactions/behavior-transaction-list.js b/webroot/rsrc/js/application/transactions/behavior-transaction-list.js --- a/webroot/rsrc/js/application/transactions/behavior-transaction-list.js +++ b/webroot/rsrc/js/application/transactions/behavior-transaction-list.js @@ -5,7 +5,7 @@ * javelin-workflow * javelin-dom * javelin-fx - * javelin-util + * javelin-uri * phabricator-textareautils */ @@ -64,64 +64,37 @@ } } - function edittransaction(transaction, response) { - // NOTE: this is for 1 transaction only - for (var phid in response.xactions) { - var new_node = JX.$H(response.xactions[phid]).getFragment().firstChild; - var new_comment = JX.DOM.find(new_node, 'span', 'transaction-comment'); - var old_comments = JX.DOM.scry( - transaction, - 'span', - 'transaction-comment'); - var old_comment = old_comments[0]; - JX.DOM.replace(old_comment, new_comment); - var edit_history = JX.DOM.scry( - transaction, - 'a', - 'transaction-edit-history'); - if (!edit_history.length) { - var transaction_phid = JX.Stratcom.getData(new_comment).phid; - var history_link = JX.$N( - 'a', - { sigil : 'transaction-edit-history', - href : config.historyLink + transaction_phid + '/' }, - config.historyLinkText); - JX.Stratcom.addSigil(history_link, 'workflow'); - var timeline_extra = JX.DOM.find(transaction, 'span', 'timeline-extra'); - var old_content = JX.$H(timeline_extra.innerHTML); - JX.DOM.setContent( - timeline_extra, - [history_link, config.linkDelimiter, old_content]); - } - new JX.FX(transaction).setDuration(500).start({opacity: [0, 1]}); - } - } - - JX.DOM.listen( - list, + JX.Stratcom.listen( 'click', - ['transaction-edit', 'transaction-remove'], + [['transaction-edit'], ['transaction-remove']], function(e) { if (!e.isNormalClick()) { return; } - var transaction = e.getNode('transaction'); + e.prevent(); - JX.Workflow.newFromLink(e.getTarget()) - .setData({anchor: e.getNodeData('transaction').anchor}) - .setHandler(JX.bind(null, edittransaction, transaction)) - .start(); + var anchor = e.getNodeData('tag:a').anchor; + var uri = JX.$U(window.location).setFragment(anchor); - e.kill(); + JX.Workflow.newFromLink(e.getNode('tag:a')) + .setHandler(function() { + // In most cases, `uri` is on the same page (just at a new anchor), + // so we have to call reload() explicitly to get the browser to + // refresh the page. It would be nice to just issue a server-side + // redirect instead, but there isn't currently an easy way to do + // that without complexity and/or a semi-open redirect. + uri.go(); + window.location.reload(); + }) + .start(); }); - JX.DOM.listen( - list, + JX.Stratcom.listen( 'click', 'transaction-quote', function(e) { - e.kill(); + e.prevent(); var data = e.getNodeData('transaction-quote'); new JX.Workflow(data.uri) diff --git a/webroot/rsrc/js/phui/behavior-phui-timeline-dropdown-menu.js b/webroot/rsrc/js/phui/behavior-phui-timeline-dropdown-menu.js new file mode 100644 --- /dev/null +++ b/webroot/rsrc/js/phui/behavior-phui-timeline-dropdown-menu.js @@ -0,0 +1,34 @@ +/** + * @provides javelin-behavior-phui-timeline-dropdown-menu + * @requires javelin-behavior + * javelin-stratcom + * javelin-dom + * phuix-dropdown-menu + */ + +JX.behavior('phui-timeline-dropdown-menu', function() { + + JX.Stratcom.listen('click', 'phui-timeline-menu', function(e) { + var data = e.getNodeData('phui-timeline-menu'); + if (data.menu) { + return; + } + + e.kill(); + + var list = JX.$H(data.items).getFragment().firstChild; + + var icon = e.getNode('phui-timeline-menu'); + data.menu = new JX.PHUIXDropdownMenu(icon); + data.menu.setContent(list); + data.menu.open(); + + JX.DOM.listen(list, 'click', 'tag:a', function(e) { + if (!e.isNormalClick()) { + return; + } + data.menu.close(); + }); + }); + +}); diff --git a/webroot/rsrc/js/phuix/PHUIXDropdownMenu.js b/webroot/rsrc/js/phuix/PHUIXDropdownMenu.js --- a/webroot/rsrc/js/phuix/PHUIXDropdownMenu.js +++ b/webroot/rsrc/js/phuix/PHUIXDropdownMenu.js @@ -38,6 +38,8 @@ JX.bind(this, this._adjustposition)); JX.Stratcom.listen('phuix.dropdown.open', null, JX.bind(this, this.close)); + + JX.Stratcom.listen('keydown', null, JX.bind(this, this._onkey)); }, events: ['open'], @@ -93,9 +95,6 @@ var menu = JX.$N('div', attrs); - this._node.setAttribute('aria-haspopup', 'true'); - this._node.setAttribute('aria-expanded', 'false'); - this._menu = menu; } @@ -143,6 +142,12 @@ JX.DOM.alterClass(this._node, 'phuix-dropdown-open', true); this._node.setAttribute('aria-expanded', 'true'); + + // Try to highlight the first link in the menu for assistive technologies. + var links = JX.DOM.scry(this._menu, 'a'); + if (links[0]) { + JX.DOM.focus(links[0]); + } }, _hide : function() { @@ -176,6 +181,26 @@ v = v.add(this.getOffsetX(), this.getOffsetY()); v.setPos(this._menu); + }, + + _onkey: function(e) { + // When the user presses escape with a menu open, close the menu and + // refocus the button which activates the menu. In particular, this makes + // popups more usable with assistive technologies. + + if (!this._open) { + return; + } + + if (e.getSpecialKey() != 'esc') { + return; + } + + this.close(); + JX.DOM.focus(this._node); + + e.prevent(); } + } });