Skip to content

Commit f1d49f9

Browse files
ozkersemihsemih.ozkermuratcorlu
authored
feat: drawer component (#327)
* feat: drawer component * test(drawer): add unit tests * fix(drawer): fix linter check error * fix(drawer): refactor for new rules * feat(drawer): add stories for drawer * fix(drawer): refactor for attribute, css, stories * fix(drawer): update attribute names in stories * fix(drawer): remove unnecessary console log * fix(drawer): add gap between header buttons Co-authored-by: semih.ozker <[email protected]> Co-authored-by: Murat Çorlu <[email protected]>
1 parent fe3eed3 commit f1d49f9

File tree

7 files changed

+488
-0
lines changed

7 files changed

+488
-0
lines changed

.storybook/preview-body.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,7 @@
66
justify-content: center;
77
align-items: center;
88
}
9+
.sb-main-fullscreen #root-inner {
10+
height: 600px;
11+
}
912
</style>

commitlint.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ module.exports = {
2121
'pagination',
2222
'radio',
2323
'dialog',
24+
'drawer',
2425
],
2526
],
2627
},

src/baklava.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export { default as BlBadge } from './components/badge/bl-badge';
33
export { default as BlButton } from './components/button/bl-button';
44
export { default as BlCheckbox } from './components/checkbox/bl-checkbox';
55
export { default as BlDialog } from './components/dialog/bl-dialog';
6+
export { default as BlDrawer } from './components/drawer/bl-drawer';
67
export { default as BlIcon } from './components/icon/bl-icon';
78
export { default as BlInput } from './components/input/bl-input';
89
export { default as BlPagination } from './components/pagination/bl-pagination';

src/components/drawer/bl-drawer.css

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
@keyframes slide-from-right{
2+
0% {
3+
transform: translateX(+100%);
4+
}
5+
6+
100% {
7+
transform: translateX(0);
8+
}
9+
}
10+
11+
.drawer{
12+
position: fixed;
13+
top:0;
14+
right: 0;
15+
width: 424px;
16+
height: 100%;
17+
background: var(--bl-color-primary-background);
18+
box-shadow: var(--bl-size-xs) 0 var(--bl-size-2xl) rgba(0 0 0 / 50%);
19+
animation: 0.25s slide-from-right;
20+
21+
/* FIXME: Use z-index variable */
22+
z-index: 999;
23+
}
24+
25+
iframe{
26+
height: 100%;
27+
width: 100%;
28+
border: none;
29+
}
30+
31+
.container{
32+
display: flex;
33+
flex-direction: column;
34+
width: 100%;
35+
height: 100%;
36+
}
37+
38+
header {
39+
display: flex;
40+
justify-content: space-between;
41+
align-items: baseline;
42+
gap: var(--bl-size-2xs);
43+
padding: var(--bl-size-xl) var(--bl-size-xl) 0 var(--bl-size-xl);
44+
background-color: white;
45+
}
46+
47+
header .header-buttons {
48+
display: flex;
49+
gap:24px;
50+
margin-left: auto;
51+
}
52+
53+
header h2 {
54+
font: var(--bl-font-title-1-medium);
55+
color: var(--bl-color-secondary);
56+
overflow: hidden;
57+
margin: 0;
58+
padding: 0;
59+
}
60+
61+
section {
62+
padding: var(--bl-size-xl) var(--bl-size-xl) var(--bl-size-m) var(--bl-size-xl);
63+
}
64+
65+
.content {
66+
overflow-y: scroll;
67+
}
68+
69+
.iframe-content {
70+
height: 100%;
71+
}
72+
73+
@media only screen and (max-width: 424px) {
74+
:host([open]) .drawer {
75+
width: calc(100vw - 24px);
76+
}
77+
}
78+
79+
80+
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { html } from 'lit';
2+
import { ifDefined } from 'lit/directives/if-defined.js';
3+
import { Meta, Canvas, ArgsTable, Story, Preview, Source } from '@storybook/addon-docs';
4+
5+
<Meta
6+
title="Components/Drawer"
7+
component="bl-drawer"
8+
parameters={{
9+
layout: 'fullscreen',
10+
chromatic: { viewports: [1000]},
11+
}}
12+
argTypes={{
13+
open: {
14+
control: "boolean",
15+
default: false
16+
},
17+
caption: {
18+
control: "text",
19+
default: "",
20+
},
21+
externalLink: {
22+
control: "text",
23+
default: "",
24+
},
25+
embedUrl: {
26+
control: "text",
27+
default: "",
28+
},
29+
}}
30+
/>
31+
32+
export const openDialog = async (event,args) => {
33+
const insideCanvas = !event.target || event.target.parentNode.parentNode.getAttribute("id") === "root";
34+
let selector= insideCanvas ? `#root #${args.id}`: `#docs-root #${args.id}`;
35+
const drawer = document.querySelector(selector);
36+
drawer.open = true;
37+
await new Promise(resolve => setTimeout(resolve,1000));
38+
}
39+
40+
export const DummyContent = () => html`
41+
<div style="font: var(--bl-font-body-text-2)">
42+
<p>
43+
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
44+
</p>
45+
<p>
46+
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
47+
</p>
48+
<p>
49+
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
50+
</p>
51+
<p>
52+
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
53+
</p>
54+
<p>
55+
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
56+
</p>
57+
</div>
58+
`
59+
60+
export const DrawerTemplate = (args) => html`
61+
<bl-drawer id=${ifDefined(args.id)}
62+
caption=${ifDefined(args.caption)}
63+
embed-url=${ifDefined(args.embedUrl)}
64+
external-link=${ifDefined(args.externalLink)}>
65+
${ifDefined(args.content)}
66+
</bl-drawer>
67+
`
68+
69+
export const StoryTemplate = (args) => html`
70+
${DrawerTemplate(args)}
71+
<bl-button @click="${(event)=>openDialog(event,args)}">${ifDefined(args.buttonText)}</bl-button>
72+
`
73+
74+
# Drawer
75+
76+
A drawer presents context-specific information and/or actions without leaving the current page
77+
78+
<bl-alert variant="warning" icon caption="Note">Inline styles in examples are only for **demo purposes**. Use regular CSS classes or tag selectors to set styles.</bl-alert>
79+
80+
### Design Rules
81+
82+
* Title and external link displayed on header section in drawer. Both of them are optional.
83+
* Close button always displayed on header section and drawer can be closed by clicking close button.
84+
* By default, Drawer can not close by clicking somewhere outside drawer.
85+
* Drawer appears right side on the page with full height expanded.
86+
* Title can be multiline automatically if it does not fit one line.
87+
* When drawer does not fit in its fixed size, it switches to mobile view.
88+
* There is an attribute about iframe and drawer component handle iframe rendering itself.
89+
* Only one drawer can display at the same time. When one drawer opens others will be closed.
90+
91+
## Usage
92+
93+
<Canvas>
94+
<Story name="Default Values"
95+
play={(event) => openDialog(event,{id:'drawer-1'})}
96+
args={{id:"drawer-1", buttonText:"default drawer", content:DummyContent()}}>
97+
{StoryTemplate.bind({})}
98+
</Story>
99+
<Story name="With caption"
100+
play={(event)=> openDialog(event,{id:'drawer-2'})}
101+
args={{id:"drawer-2", buttonText:"with caption", caption: "Caption", content:'example content'}}>
102+
{StoryTemplate.bind({})}
103+
</Story>
104+
<Story name="With long caption"
105+
play={(event) => openDialog(event,{id:'drawer-3'})}
106+
args={{id:"drawer-3", buttonText:"with long caption", caption: "This drawer has a long caption and automatically handle it", content:'example content'}}>
107+
{StoryTemplate.bind({})}
108+
</Story>
109+
<Story name="With caption and externalLink"
110+
play={(event) => openDialog(event,{id:'drawer-4'})}
111+
args={{id:"drawer-4", buttonText:"with caption and external", caption: "Caption", externalLink:"some-url", content:'example content'}}>
112+
{StoryTemplate.bind({})}
113+
</Story>
114+
<Story name="With caption and embedUrl"
115+
play={(event) => openDialog(event,{id:'drawer-5'})}
116+
args={{id:"drawer-5", buttonText:"with caption, embedUrl", caption: "Caption", embedUrl:"some-url"}}>
117+
{StoryTemplate.bind({})}
118+
</Story>
119+
</Canvas>
120+
121+
<ArgsTable of="bl-drawer" />
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import {assert, elementUpdated, expect, fixture, html, oneEvent} from '@open-wc/testing';
2+
import BlDrawer from "./bl-drawer";
3+
import type typeOfBlDrawer from './bl-drawer';
4+
5+
describe('bl-drawer',() => {
6+
it('is defined', () => {
7+
const el = document.createElement('bl-drawer');
8+
assert.instanceOf(el, BlDrawer);
9+
});
10+
11+
describe('render tests',()=>{
12+
it('should render drawer component with default values', async ()=>{
13+
const el = await fixture<typeOfBlDrawer>(html`<bl-drawer open></bl-drawer>`);
14+
assert.shadowDom.equal(
15+
el,
16+
`
17+
<div class="drawer">
18+
<div class="container">
19+
<header>
20+
<div class="header-buttons">
21+
<bl-button
22+
icon="close"
23+
variant="tertiary"
24+
size="small"
25+
kind="neutral"
26+
></bl-button>
27+
</div>
28+
</header>
29+
<section class="content">
30+
<slot></slot>
31+
</section>
32+
</div>
33+
</div>
34+
`
35+
);
36+
});
37+
it('should render the caption, externalLink and content if provided', async ()=>{
38+
const el = await fixture<typeOfBlDrawer>(html`<bl-drawer caption="My Caption" external-link="some-url" open>
39+
<div>example content</div>
40+
</bl-drawer>`);
41+
42+
assert.shadowDom.equal(
43+
el,
44+
`
45+
<div class="drawer">
46+
<div class="container">
47+
<header>
48+
<h2 id="drawer-caption">
49+
My Caption
50+
</h2>
51+
<div class="header-buttons">
52+
<bl-button
53+
href="some-url"
54+
icon="external_link"
55+
variant="tertiary"
56+
size="small"
57+
target="_blank"
58+
kind="neutral"
59+
></bl-button>
60+
<bl-button
61+
icon="close"
62+
variant="tertiary"
63+
size="small"
64+
kind="neutral"
65+
></bl-button>
66+
</div>
67+
</header>
68+
<section class="content">
69+
<slot></slot>
70+
</section>
71+
</div>
72+
</div>
73+
`
74+
);
75+
});
76+
77+
it('should render the caption, embedUrl if provided', async ()=>{
78+
const el = await fixture<typeOfBlDrawer>(html`<bl-drawer caption="My Caption" embed-url="some-url" open><div>example content</div></bl-drawer>`);
79+
80+
const caption = el.shadowRoot?.querySelector('#drawer-caption') as HTMLElement;
81+
const iframeEl = el.shadowRoot?.querySelector('iframe') as HTMLElement;
82+
83+
expect(iframeEl).to.exist;
84+
expect(iframeEl.attributes.getNamedItem('src')?.value).to.contain("some-url");
85+
86+
expect(caption).to.exist;
87+
expect(caption.innerText).to.equal('My Caption');
88+
});
89+
90+
it('should open the drawer when change open attribute as true', async () => {
91+
const el = await fixture<typeOfBlDrawer>(html`<bl-drawer caption="Drawer Title">
92+
<div>Drawer Content</div>
93+
</bl-drawer>`);
94+
95+
expect(el.open).to.equal(false);
96+
97+
el.open = true;
98+
await elementUpdated(el);
99+
100+
setTimeout(() => {
101+
expect(el.open).to.equal(true);
102+
});
103+
});
104+
it('should close the drawer when change click close button', async () => {
105+
const el = await fixture<typeOfBlDrawer>(html`<bl-drawer open caption="Drawer Title">
106+
<div>Drawer Content</div>
107+
</bl-drawer>`);
108+
109+
const closeBtn = el?.shadowRoot?.querySelector('bl-button');
110+
111+
expect(closeBtn).to.exist;
112+
expect(el.open).to.equal(true);
113+
closeBtn?.click();
114+
115+
setTimeout(()=>{
116+
expect(el.open).to.equal(false);
117+
expect(el.offsetWidth).to.equal(0);
118+
});
119+
});
120+
});
121+
describe('event tests',()=>{
122+
it('should fire bl-drawer-open when dialog opens',async ()=>{
123+
const el = await fixture<typeOfBlDrawer>(html`<bl-drawer caption="My Drawer"></bl-drawer>`)
124+
125+
setTimeout(async () => {
126+
const openEvent = await oneEvent(el,'bl-drawer-open');
127+
expect(openEvent).to.exist;
128+
expect(openEvent.detail.isOpen).to.equal(true);
129+
});
130+
});
131+
it('should fire bl-drawer-close when dialog closes',async ()=>{
132+
const el = await fixture<typeOfBlDrawer>(html`<bl-drawer open caption="My Drawer"></bl-drawer>`)
133+
134+
const closeBtn = el?.shadowRoot?.querySelector('bl-button');
135+
136+
setTimeout(async () => {
137+
closeBtn?.click();
138+
const openEvent = await oneEvent(el,'bl-drawer-close');
139+
expect(openEvent).to.exist;
140+
expect(openEvent.detail.isOpen).to.equal(false);
141+
});
142+
});
143+
})
144+
});

0 commit comments

Comments
 (0)