diff --git a/superset-frontend/spec/javascripts/components/SubMenu_spec.jsx b/superset-frontend/spec/javascripts/components/SubMenu_spec.jsx
new file mode 100644
index 0000000000..b7ec4e2cd3
--- /dev/null
+++ b/superset-frontend/spec/javascripts/components/SubMenu_spec.jsx
@@ -0,0 +1,88 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import React from 'react';
+import { Link } from 'react-router-dom';
+import { shallow } from 'enzyme';
+import { Navbar, MenuItem } from 'react-bootstrap';
+import SubMenu from 'src/components/Menu/SubMenu';
+
+const defaultProps = {
+ name: 'Title',
+ children: [
+ {
+ name: 'Page1',
+ label: 'Page1',
+ url: '/page1',
+ usesRouter: true,
+ },
+ {
+ name: 'Page2',
+ label: 'Page2',
+ url: '/page2',
+ usesRouter: true,
+ },
+ {
+ name: 'Page3',
+ label: 'Page3',
+ url: '/page3',
+ usesRouter: false,
+ },
+ ],
+};
+
+describe('SubMenu', () => {
+ let wrapper;
+
+ const getWrapper = (overrideProps = {}) => {
+ const props = {
+ ...defaultProps,
+ ...overrideProps,
+ };
+ return shallow();
+ };
+
+ beforeEach(() => {
+ wrapper = getWrapper();
+ });
+
+ it('renders a Navbar', () => {
+ expect(wrapper.find(Navbar)).toExist();
+ });
+
+ it('renders 3 MenuItems (when usesRouter === false)', () => {
+ expect(wrapper.find(MenuItem)).toHaveLength(3);
+ });
+
+ it('renders the menu title', () => {
+ expect(wrapper.find(Navbar.Brand)).toExist();
+ expect(wrapper.find(Navbar.Brand).children().text()).toEqual('Title');
+ });
+
+ it('renders Link components when usesRouter === true', () => {
+ const overrideProps = {
+ usesRouter: true,
+ };
+
+ const routerWrapper = getWrapper(overrideProps);
+
+ expect(routerWrapper.find(Link)).toExist();
+ expect(routerWrapper.find(Link)).toHaveLength(2);
+ expect(routerWrapper.find(MenuItem)).toHaveLength(1);
+ });
+});
diff --git a/superset-frontend/src/components/Menu/SubMenu.tsx b/superset-frontend/src/components/Menu/SubMenu.tsx
index 15995ed028..bc965ef8ce 100644
--- a/superset-frontend/src/components/Menu/SubMenu.tsx
+++ b/superset-frontend/src/components/Menu/SubMenu.tsx
@@ -17,6 +17,7 @@
* under the License.
*/
import React from 'react';
+import { Link, useHistory } from 'react-router-dom';
import { styled } from '@superset-ui/core';
import { Nav, Navbar, MenuItem } from 'react-bootstrap';
import Button, { OnClickHandler } from 'src/components/Button';
@@ -31,16 +32,29 @@ const StyledHeader = styled.header`
}
.navbar-nav {
li {
- a {
+ a,
+ div {
font-size: ${({ theme }) => theme.typography.sizes.s}px;
- padding: ${({ theme }) => theme.gridUnit * 2}px;
+ padding: ${({ theme }) => theme.gridUnit * 2}px 0;
margin: ${({ theme }) => theme.gridUnit * 2}px;
color: ${({ theme }) => theme.colors.secondary.dark1};
+
+ a {
+ margin: 0;
+ padding: ${({ theme }) => theme.gridUnit * 4}px;
+ }
+ }
+
+ &.no-router a {
+ padding: ${({ theme }) => theme.gridUnit * 2}px
+ ${({ theme }) => theme.gridUnit * 4}px;
}
}
li.active > a,
- li > a:hover {
+ li.active > div,
+ li > a:hover,
+ li > div:hover {
background-color: ${({ theme }) => theme.colors.secondary.light4};
border-bottom: none;
border-radius: 4px;
@@ -52,6 +66,7 @@ type MenuChild = {
label: string;
name: string;
url: string;
+ usesRouter?: boolean;
};
export interface SubMenuProps {
@@ -66,9 +81,23 @@ export interface SubMenuProps {
name: string;
children?: MenuChild[];
activeChild?: MenuChild['name'];
+ /* If usesRouter is true, a react-router component will be used instead of href.
+ * ONLY set usesRouter to true if SubMenu is wrapped in a react-router ;
+ * otherwise, a 'You should not use outside a ' error will be thrown */
+ usesRouter?: boolean;
}
const SubMenu: React.FunctionComponent = props => {
+ let hasHistory = true;
+
+ // If no parent component exists, useHistory throws an error
+ try {
+ useHistory();
+ } catch (err) {
+ // If error is thrown, we know not to use in render
+ hasHistory = false;
+ }
+
return (
@@ -77,15 +106,31 @@ const SubMenu: React.FunctionComponent = props => {