]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-16683 [891507] focus first item in menus
authorJeremy Davis <jeremy.davis@sonarsource.com>
Wed, 27 Jul 2022 15:27:40 +0000 (17:27 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 29 Jul 2022 20:03:14 +0000 (20:03 +0000)
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx
server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx
server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/components/embed-docs-modal/EmbedDocsPopup.tsx
server/sonar-web/src/main/js/components/embed-docs-modal/__tests__/EmbedDocsPopup-test.tsx
server/sonar-web/src/main/js/components/embed-docs-modal/__tests__/__snapshots__/EmbedDocsPopup-test.tsx.snap [deleted file]

index 089c8705516b52683a6ebcf3779a4a850a6c5c7b..14c4221719e11a7fc7df1912e7da4505eeecd9ca 100644 (file)
@@ -33,6 +33,12 @@ interface Props {
 }
 
 export class GlobalNavUser extends React.PureComponent<Props> {
+  focusNode = (node: HTMLAnchorElement | null) => {
+    if (node) {
+      node.focus();
+    }
+  };
+
   handleLogin = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
     event.preventDefault();
     const returnTo = encodeURIComponent(window.location.pathname + window.location.search);
@@ -67,7 +73,9 @@ export class GlobalNavUser extends React.PureComponent<Props> {
             </li>
             <li className="divider" />
             <li>
-              <Link to="/account">{translate('my_account.page')}</Link>
+              <Link ref={this.focusNode} to="/account">
+                {translate('my_account.page')}
+              </Link>
             </li>
             <li>
               <a href="#" onClick={this.handleLogout}>
index d683a244ff1fba2f95e13760ba9d25be663ec354..1e4bd02f4c0f80a1343514cb96fdd15e4e573206 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { shallow } from 'enzyme';
+import { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
 import * as React from 'react';
 import { mockCurrentUser, mockLoggedInUser, mockRouter } from '../../../../../helpers/testMocks';
+import { renderComponent } from '../../../../../helpers/testReactTestingUtils';
 import { GlobalNavUser } from '../GlobalNavUser';
 
 it('should render the right interface for anonymous user', () => {
-  expect(shallowRender({ currentUser: mockCurrentUser() })).toMatchSnapshot();
+  renderGlobalNavUser({ currentUser: mockCurrentUser() });
+  expect(screen.getByText('layout.login')).toBeInTheDocument();
 });
 
-it('should render the right interface for logged in user', () => {
-  const wrapper = shallowRender();
-  wrapper.setState({ open: true });
-  expect(wrapper.find('Dropdown')).toMatchSnapshot();
+it('should render the right interface for logged in user', async () => {
+  const user = userEvent.setup();
+  renderGlobalNavUser();
+  await user.click(screen.getByRole('link'));
+
+  expect(screen.getByRole('link', { name: 'my_account.page' })).toHaveFocus();
 });
 
-function shallowRender(overrides: Partial<GlobalNavUser['props']> = {}) {
-  return shallow<GlobalNavUser>(
+function renderGlobalNavUser(overrides: Partial<GlobalNavUser['props']> = {}) {
+  return renderComponent(
     <GlobalNavUser currentUser={mockLoggedInUser()} router={mockRouter()} {...overrides} />
   );
 }
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.tsx.snap
deleted file mode 100644 (file)
index de81a2b..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render the right interface for anonymous user 1`] = `
-<div>
-  <a
-    className="navbar-login"
-    href="/sessions/new"
-    onClick={[Function]}
-  >
-    layout.login
-  </a>
-</div>
-`;
-
-exports[`should render the right interface for logged in user 1`] = `
-<Dropdown
-  className="js-user-authenticated"
-  overlay={
-    <ul
-      className="menu"
-    >
-      <li
-        className="menu-item"
-      >
-        <div
-          className="text-ellipsis text-muted"
-          title="Skywalker"
-        >
-          <strong>
-            Skywalker
-          </strong>
-        </div>
-      </li>
-      <li
-        className="divider"
-      />
-      <li>
-        <Link
-          to="/account"
-        >
-          my_account.page
-        </Link>
-      </li>
-      <li>
-        <a
-          href="#"
-          onClick={[Function]}
-        >
-          layout.logout
-        </a>
-      </li>
-    </ul>
-  }
->
-  <a
-    className="dropdown-toggle navbar-avatar"
-    href="#"
-    title="Skywalker"
-  >
-    <withAppStateContext(Avatar)
-      name="Skywalker"
-      size={32}
-    />
-  </a>
-</Dropdown>
-`;
index 0fd077c50b45f8bdc483412561fbb982c0b794f2..4b063dae646ed220e4e6163812e152758a4db9fd 100644 (file)
@@ -30,6 +30,21 @@ interface Props {
 }
 
 export default class EmbedDocsPopup extends React.PureComponent<Props> {
+  firstItem: HTMLAnchorElement | null = null;
+
+  /*
+   * Will be called by the first suggestion (if any), as well as the first link (documentation)
+   * Since we don't know if we have any suggestions, we need to allow both to make the call.
+   * If we have at least 1 suggestion, it will make the call first, and prevent 'documentation' from
+   * getting the focus.
+   */
+  focusFirstItem = (node: HTMLAnchorElement | null) => {
+    if (node && !this.firstItem) {
+      this.firstItem = node;
+      this.firstItem.focus();
+    }
+  };
+
   renderTitle(text: string) {
     return (
       <li role="presentation" className="menu-header">
@@ -45,9 +60,13 @@ export default class EmbedDocsPopup extends React.PureComponent<Props> {
     return (
       <ul className="menu abs-width-240" role="group">
         {this.renderTitle(translate('embed_docs.suggestion'))}
-        {suggestions.map(suggestion => (
+        {suggestions.map((suggestion, i) => (
           <li key={suggestion.link}>
-            <Link onClick={this.props.onClose} target="_blank" to={suggestion.link}>
+            <Link
+              ref={i === 0 ? this.focusFirstItem : undefined}
+              onClick={this.props.onClose}
+              target="_blank"
+              to={suggestion.link}>
               {suggestion.text}
             </Link>
           </li>
@@ -77,7 +96,11 @@ export default class EmbedDocsPopup extends React.PureComponent<Props> {
         <SuggestionsContext.Consumer>{this.renderSuggestions}</SuggestionsContext.Consumer>
         <ul className="menu abs-width-240" role="group">
           <li>
-            <Link onClick={this.props.onClose} target="_blank" to="/documentation">
+            <Link
+              ref={this.focusFirstItem}
+              onClick={this.props.onClose}
+              target="_blank"
+              to="/documentation">
               {translate('embed_docs.documentation')}
             </Link>
           </li>
index f12348ed5149816a5240b32e43efdca646da2a06..87f7315b3b2a70d18f8a823327fb390cb650c91a 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { shallow } from 'enzyme';
+import { screen } from '@testing-library/react';
 import * as React from 'react';
+import { renderComponent } from '../../../helpers/testReactTestingUtils';
+import { SuggestionLink } from '../../../types/types';
 import EmbedDocsPopup from '../EmbedDocsPopup';
+import { SuggestionsContext } from '../SuggestionsContext';
 
-it('should render', () => {
-  const wrapper = shallow(<EmbedDocsPopup onClose={jest.fn()} />);
-  expect(wrapper).toMatchSnapshot();
+it('should render with no suggestions', () => {
+  renderEmbedDocsPopup();
+
+  expect(screen.getAllByRole('link')).toHaveLength(5);
+  expect(screen.getByText('embed_docs.documentation')).toHaveFocus();
 });
+
+it('should render with suggestions', () => {
+  renderEmbedDocsPopup([
+    { link: '/docs/awesome-doc', text: 'mindblowing' },
+    { link: '/docs/whocares', text: 'boring' }
+  ]);
+
+  expect(screen.getAllByRole('link')).toHaveLength(7);
+  expect(screen.getByText('mindblowing')).toHaveFocus();
+});
+
+function renderEmbedDocsPopup(suggestions: SuggestionLink[] = []) {
+  return renderComponent(
+    <SuggestionsContext.Provider
+      value={{ addSuggestions: jest.fn(), removeSuggestions: jest.fn(), suggestions }}>
+      <EmbedDocsPopup onClose={jest.fn()} />
+    </SuggestionsContext.Provider>
+  );
+}
diff --git a/server/sonar-web/src/main/js/components/embed-docs-modal/__tests__/__snapshots__/EmbedDocsPopup-test.tsx.snap b/server/sonar-web/src/main/js/components/embed-docs-modal/__tests__/__snapshots__/EmbedDocsPopup-test.tsx.snap
deleted file mode 100644 (file)
index edd740b..0000000
+++ /dev/null
@@ -1,88 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render 1`] = `
-<DropdownOverlay>
-  <ContextConsumer>
-    <Component />
-  </ContextConsumer>
-  <ul
-    className="menu abs-width-240"
-    role="group"
-  >
-    <li>
-      <Link
-        onClick={[MockFunction]}
-        target="_blank"
-        to="/documentation"
-      >
-        embed_docs.documentation
-      </Link>
-    </li>
-    <li>
-      <Link
-        onClick={[MockFunction]}
-        to="/web_api"
-      >
-        api_documentation.page
-      </Link>
-    </li>
-  </ul>
-  <ul
-    className="menu abs-width-240"
-    role="group"
-  >
-    <li>
-      <a
-        href="https://community.sonarsource.com/"
-        rel="noopener noreferrer"
-        target="_blank"
-      >
-        embed_docs.get_help
-      </a>
-    </li>
-  </ul>
-  <ul
-    className="menu abs-width-240"
-    role="group"
-  >
-    <li
-      className="menu-header"
-      role="presentation"
-    >
-      embed_docs.stay_connected
-    </li>
-    <li>
-      <a
-        href="https://www.sonarqube.org/whats-new/?referrer=sonarqube"
-        rel="noopener noreferrer"
-        target="_blank"
-      >
-        <img
-          alt="embed_docs.news"
-          className="spacer-right"
-          height="18"
-          src="/images/embed-doc/sq-icon.svg"
-          width="18"
-        />
-        embed_docs.news
-      </a>
-    </li>
-    <li>
-      <a
-        href="https://twitter.com/SonarQube"
-        rel="noopener noreferrer"
-        target="_blank"
-      >
-        <img
-          alt="Twitter"
-          className="spacer-right"
-          height="18"
-          src="/images/embed-doc/twitter-icon.svg"
-          width="18"
-        />
-        Twitter
-      </a>
-    </li>
-  </ul>
-</DropdownOverlay>
-`;