diff --git a/superset-frontend/src/components/Popover/Popover.stories.tsx b/superset-frontend/src/components/Popover/Popover.stories.tsx
new file mode 100644
index 0000000000..929651cec3
--- /dev/null
+++ b/superset-frontend/src/components/Popover/Popover.stories.tsx
@@ -0,0 +1,81 @@
+/**
+ * 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 Button from 'src/components/Button';
+import { PopoverProps } from 'antd/lib/popover';
+import React from 'react';
+import Popover from '.';
+
+export default {
+ title: 'Popover',
+ component: Popover,
+};
+
+export const InteractivePopover = (args: PopoverProps) => (
+
+
+
+);
+
+const PLACEMENTS = {
+ label: 'placement',
+ options: [
+ 'topLeft',
+ 'top',
+ 'topRight',
+ 'leftTop',
+ 'left',
+ 'leftBottom',
+ 'rightTop',
+ 'right',
+ 'rightBottom',
+ 'bottomLeft',
+ 'bottom',
+ 'bottomRight',
+ ],
+ defaultValue: null,
+};
+
+const TRIGGERS = {
+ label: 'trigger',
+ options: ['hover', 'click', 'focus'],
+ defaultValue: null,
+};
+
+InteractivePopover.args = {
+ content: 'Popover sample content',
+ title: 'Popover title',
+};
+
+InteractivePopover.argTypes = {
+ placement: {
+ name: PLACEMENTS.label,
+ control: { type: 'select', options: PLACEMENTS.options },
+ },
+ trigger: {
+ name: TRIGGERS.label,
+ control: { type: 'select', options: TRIGGERS.options },
+ },
+};
diff --git a/superset-frontend/src/components/Popover/Popover.test.tsx b/superset-frontend/src/components/Popover/Popover.test.tsx
new file mode 100644
index 0000000000..88e24c1d68
--- /dev/null
+++ b/superset-frontend/src/components/Popover/Popover.test.tsx
@@ -0,0 +1,82 @@
+/**
+ * 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 { render, screen, waitFor } from 'spec/helpers/testing-library';
+import userEvent from '@testing-library/user-event';
+import { supersetTheme } from '@superset-ui/core';
+import Icon from 'src/components/Icon';
+import Button from 'src/components/Button';
+import Popover from '.';
+
+test('should render', () => {
+ const { container } = render();
+ expect(container).toBeInTheDocument();
+});
+
+test('should render a title when visible', () => {
+ render();
+ expect(screen.getByText('Popover title')).toBeTruthy();
+});
+
+test('should render some content when visible', () => {
+ render();
+ expect(screen.getByText('Content sample')).toBeTruthy();
+});
+
+test('it should not render a title or content when not visible', () => {
+ render();
+ const content = screen.queryByText('Content sample');
+ const title = screen.queryByText('Popover title');
+ expect(content).not.toBeInTheDocument();
+ expect(title).not.toBeInTheDocument();
+});
+
+test('renders with icon child', () => {
+ render(
+
+
+ Click me
+
+ ,
+ );
+ expect(screen.getByRole('img')).toBeInTheDocument();
+});
+
+test('fires an event when visibility is changed', async () => {
+ const onVisibleChange = jest.fn();
+ render(
+
+
+ ,
+ );
+ userEvent.hover(screen.getByRole('button'));
+ await waitFor(() => expect(onVisibleChange).toHaveBeenCalledTimes(1));
+});
+
+test('renders with theme', () => {
+ render();
+ const title = screen.getByText('Popover title');
+ expect(title).toHaveStyle({
+ fontSize: supersetTheme.gridUnit * 3.5,
+ });
+});