Skip to content

Commit c369a2b

Browse files
authored
Merge pull request #332 from igalklebanov/popover-stories
feat(popover): add missing accessibility attributes, cypress/storybook tests and documentation.
2 parents 0b3752b + 4e02395 commit c369a2b

File tree

10 files changed

+298
-69
lines changed

10 files changed

+298
-69
lines changed

apps/website/src/_state/component-statuses.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export const componentsStatuses: ComponentKitsStatuses = {
4141
headless: {
4242
Accordion: 'Planned',
4343
Carousel: 'Planned',
44+
Popover: 'Planned',
4445
Select: 'Draft',
4546
Tabs: 'Planned',
4647
Toggle: 'Planned',
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { Slot, component$ } from '@builder.io/qwik';
2+
import { Popover, PopoverContent, PopoverTrigger } from '@qwik-ui/headless';
3+
import { GitHubIcon } from 'apps/website/src/components/icons/GitHubIcon';
4+
import { PreviewCodeExample } from 'apps/website/src/components/preview-code-example/preview-code-example';
5+
6+
export const MainExample = component$(() => {
7+
return (
8+
<PreviewCodeExample>
9+
<div q:slot="actualComponent">
10+
<Popover>
11+
<PopoverContent>
12+
<div class="p-4 bg-gray-500">Hi, I'm the content</div>
13+
</PopoverContent>
14+
<PopoverTrigger> Click on me </PopoverTrigger>
15+
</Popover>
16+
</div>
17+
<div q:slot="codeExample">
18+
<Slot />
19+
</div>
20+
</PreviewCodeExample>
21+
);
22+
});
23+
24+
export const Example1 = component$(() => {
25+
return (
26+
<PreviewCodeExample>
27+
<div q:slot="actualComponent">
28+
<Popover placement="top">
29+
<PopoverContent>
30+
<div class="p-4 bg-gray-500">
31+
Hi, I'm the content, but now on top
32+
</div>
33+
</PopoverContent>
34+
<PopoverTrigger> Click on me </PopoverTrigger>
35+
</Popover>
36+
</div>
37+
<div q:slot="codeExample">
38+
<Slot />
39+
</div>
40+
</PreviewCodeExample>
41+
);
42+
});
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
---
2+
title: Qwik UI | Popover
3+
---
4+
5+
import { Popover } from '@qwik-ui/headless';
6+
import { MainExample, Example1 } from './examples';
7+
import { CodeExample } from '../../../../../components/code-example/code-example';
8+
import { KeyboardInteractionTable } from '../../../../../components/keyboard-interaction-table/keyboard-interaction-table';
9+
import { APITable } from '../../../../../components/api-table/api-table';
10+
11+
# Popover
12+
13+
#### A popover is an element that displays a modal with content when the user clicks or hovers on a trigger element.
14+
15+
<MainExample>
16+
```tsx
17+
<Popover>
18+
<PopoverContent>
19+
<div class="p-4 bg-gray-500">Hi, I'm the content</div>
20+
</PopoverContent>
21+
<PopoverTrigger> Click on me </PopoverTrigger>
22+
</Popover>
23+
```
24+
</MainExample>
25+
26+
## Building blocks
27+
28+
<CodeExample>
29+
```tsx
30+
import { component$ } from '@builder.io/qwik';
31+
import { Popover } from '@qwik-ui/headless';
32+
33+
export default component$(() => (
34+
<Popover>
35+
<PopoverContent>
36+
<div class="p-4 bg-gray-500">Hi, I'm the content</div>
37+
</PopoverContent>
38+
<PopoverTrigger> Click on me </PopoverTrigger>
39+
</Popover>
40+
));
41+
```
42+
43+
</CodeExample>
44+
45+
## Examples
46+
47+
### EXAMPLE: Positioning the popover's content (on top)
48+
49+
<Example1>
50+
```tsx
51+
<Popover placement="top">
52+
<PopoverContent>
53+
<div class="p-4 bg-gray-500">Hi, I'm the content, but now on top</div>
54+
</PopoverContent>
55+
<PopoverTrigger> Click on me </PopoverTrigger>
56+
</Popover>
57+
```
58+
</Example1>
59+
60+
## API
61+
62+
### Popover
63+
64+
<APITable
65+
propDescriptors={[
66+
{
67+
name: 'placement',
68+
type: '"top" | "right" | "bottom" | "left" | "top-start" | "top-end" | "right-start" | "right-end" | "bottom-start" | "bottom-end" | "left-start" | "left-end"',
69+
description: 'The side where to show the popover.',
70+
},
71+
{
72+
name: 'triggerEvent',
73+
type: '"click" | "mouseOver"',
74+
description: 'Popover is opened when trigger is clicked or mouse overed.',
75+
},
76+
{
77+
name: 'offset',
78+
type: 'number',
79+
description: 'Offset between trigger and content.',
80+
},
81+
{
82+
name: 'isOpen',
83+
type: 'boolean',
84+
description:
85+
'Open or close the popover when popover is controlled by the parent.',
86+
},
87+
{
88+
name: 'disableClickOutSide',
89+
type: 'boolean',
90+
description: 'When true the popover is not closed when click outside.',
91+
},
92+
{
93+
name: 'onUpdate$',
94+
type: 'PropFunction<(isOpen: boolean) => void>',
95+
description: 'Notify a state update to the parent.',
96+
},
97+
]}
98+
/>

apps/website/src/routes/docs/headless/(components)/popover/index.tsx

Lines changed: 0 additions & 67 deletions
This file was deleted.

apps/website/src/routes/docs/headless/menu.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
- [Accordion](/docs/headless/accordion)
1212
- [Carousel](/docs/headless/carousel)
13+
- [Popover](/docs/headless/popover)
1314
- [Select](/docs/headless/select)
1415
- [Tabs](/docs/headless/tabs)
1516
- [Toggle](/docs/headless/toggle)

packages/kit-headless/src/components/popover/popover-content.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,13 @@ export const PopoverContent = component$(() => {
1919
});
2020

2121
return (
22-
<div ref={ref} class="popover-content">
22+
<div
23+
ref={ref}
24+
role="dialog"
25+
aria-modal="true"
26+
aria-label="Popover"
27+
class="popover-content"
28+
>
2329
<Slot />
2430
</div>
2531
);

packages/kit-headless/src/components/popover/popover-trigger.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export const PopoverTrigger = component$(() => {
2626
return (
2727
<span
2828
ref={ref}
29+
role="button"
2930
class="popover-trigger"
3031
onMouseOver$={
3132
contextService.triggerEvent === 'mouseOver'
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { component$ } from '@builder.io/qwik';
2+
import { Popover, PopoverProps } from './popover';
3+
import { PopoverContent } from './popover-content';
4+
import { PopoverTrigger } from './popover-trigger';
5+
6+
const PopoverComponent = component$((props: PopoverProps) => {
7+
return (
8+
<Popover {...props}>
9+
<PopoverContent>popover content</PopoverContent>
10+
<PopoverTrigger>trigger text</PopoverTrigger>
11+
</Popover>
12+
);
13+
});
14+
15+
describe('Popover', () => {
16+
function clickOnTrigger() {
17+
cy.findByRole('button').click();
18+
}
19+
20+
function clickOutside() {
21+
cy.get('body').click('bottomRight');
22+
}
23+
24+
function assertOpen() {
25+
cy.findByRole('dialog')
26+
.should('have.class', 'open')
27+
.should('not.have.class', 'close');
28+
}
29+
30+
function assertClosed() {
31+
cy.findByRole('dialog').should('not.have.class', 'open');
32+
}
33+
34+
function hoverOnTrigger() {
35+
cy.findByRole('button').trigger('mouseover');
36+
}
37+
38+
it('INIT', () => {
39+
cy.mount(<PopoverComponent />);
40+
41+
cy.checkA11yForComponent();
42+
});
43+
44+
it('should render the component', () => {
45+
cy.mount(<PopoverComponent />);
46+
47+
cy.findByRole('button').should('contain', 'trigger text');
48+
cy.findByRole('dialog').should('contain', 'popover content');
49+
assertClosed();
50+
});
51+
52+
it('should render the component with content being open when isOpen is set to true', () => {
53+
cy.mount(<PopoverComponent isOpen />);
54+
55+
assertOpen();
56+
});
57+
58+
it('should open the content when clicking on trigger', () => {
59+
cy.mount(<PopoverComponent />);
60+
61+
assertClosed();
62+
63+
clickOnTrigger();
64+
65+
assertOpen();
66+
});
67+
68+
it('should close the content when clicking outside', () => {
69+
cy.mount(<PopoverComponent />);
70+
71+
clickOnTrigger();
72+
73+
assertOpen();
74+
75+
clickOutside();
76+
77+
assertClosed();
78+
});
79+
80+
it('should not close the content when clicking outside and disabledClickOutside is set to true', () => {
81+
cy.mount(<PopoverComponent disableClickOutSide />);
82+
83+
clickOnTrigger();
84+
85+
assertOpen();
86+
87+
clickOutside();
88+
89+
assertOpen();
90+
});
91+
92+
it('should open the content when hovering over trigger', () => {
93+
cy.mount(<PopoverComponent triggerEvent="mouseOver" />);
94+
95+
assertClosed();
96+
97+
hoverOnTrigger();
98+
99+
assertOpen();
100+
});
101+
102+
it('should close the content when hovering over trigger and exiting', () => {
103+
cy.mount(<PopoverComponent triggerEvent="mouseOver" />);
104+
105+
hoverOnTrigger();
106+
107+
assertOpen();
108+
109+
clickOutside();
110+
111+
assertClosed();
112+
});
113+
});
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { Meta, StoryObj } from 'storybook-framework-qwik';
2+
import { Popover, PopoverProps } from './popover';
3+
import { PopoverTrigger } from './popover-trigger';
4+
import { PopoverContent } from './popover-content';
5+
import { screen, userEvent, within } from '@storybook/testing-library';
6+
import { expect } from '@storybook/jest';
7+
8+
const meta: Meta<PopoverProps> = {
9+
component: Popover,
10+
};
11+
12+
type Story = StoryObj<PopoverProps>;
13+
14+
export default meta;
15+
16+
export const Primary: Story = {
17+
render: () => (
18+
<Popover>
19+
<PopoverContent>Oh hi mark!</PopoverContent>
20+
<PopoverTrigger>Click me please</PopoverTrigger>
21+
</Popover>
22+
),
23+
play: async ({ canvasElement }) => {
24+
const canvas = within(canvasElement);
25+
26+
const button = await canvas.findByRole('button');
27+
28+
await userEvent.click(button);
29+
30+
const popover = await canvas.findByRole('dialog');
31+
32+
await expect(popover).toHaveTextContent('Oh hi mark!');
33+
},
34+
};

0 commit comments

Comments
 (0)