Skip to content

Commit c87ee00

Browse files
committed
feat(COffcanvas): add static backdrop support
1 parent 26c0479 commit c87ee00

File tree

2 files changed

+147
-110
lines changed

2 files changed

+147
-110
lines changed

packages/coreui-vue/src/components/offcanvas/COffcanvas.ts

+26-27
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,28 @@ const COffcanvas = defineComponent({
1010
props: {
1111
/**
1212
* Apply a backdrop on body while offcanvas is open.
13+
*
14+
* @values boolean | 'static'
1315
*/
1416
backdrop: {
15-
type: Boolean,
17+
type: [Boolean, String],
1618
default: true,
17-
require: false,
19+
validator: (value: boolean | string) => {
20+
if (typeof value === 'string') {
21+
return ['static'].includes(value)
22+
}
23+
if (typeof value === 'boolean') {
24+
return true
25+
}
26+
return false
27+
},
1828
},
1929
/**
2030
* Closes the offcanvas when escape key is pressed.
2131
*/
2232
keyboard: {
2333
type: Boolean,
2434
default: true,
25-
require: false,
2635
},
2736
/**
2837
* Components placement, there’s no default placement.
@@ -43,15 +52,11 @@ const COffcanvas = defineComponent({
4352
scroll: {
4453
type: Boolean,
4554
default: false,
46-
required: false,
4755
},
4856
/**
4957
* Toggle the visibility of offcanvas component.
5058
*/
51-
visible: {
52-
type: Boolean,
53-
require: false,
54-
},
59+
visible: Boolean,
5560
},
5661
emits: [
5762
/**
@@ -93,44 +98,35 @@ const COffcanvas = defineComponent({
9398
emit('show')
9499
executeAfterTransition(() => done(), el as HTMLElement)
95100
setTimeout(() => {
96-
el.style.visibility = 'visible'
97101
el.classList.add('show')
98102
}, 1)
99103
}
100104
const handleAfterEnter = () => {
101-
window.addEventListener('mousedown', handleMouseDown)
102-
window.addEventListener('keyup', handleKeyUp)
105+
offcanvasRef.value.focus()
103106
}
104107
const handleLeave = (el: RendererElement, done: () => void) => {
105108
executeAfterTransition(() => done(), el as HTMLElement)
106-
window.removeEventListener('mousedown', handleMouseDown)
107-
window.removeEventListener('keyup', handleKeyUp)
108-
el.classList.remove('show')
109+
el.classList.add('hiding')
109110
}
110111
const handleAfterLeave = (el: RendererElement) => {
111-
el.style.visibility = 'hidden'
112+
el.classList.remove('show', 'hiding')
112113
}
113114

114115
const handleDismiss = () => {
115116
visible.value = false
116117
emit('hide')
117118
}
118119

119-
const handleKeyUp = (event: KeyboardEvent) => {
120-
if (offcanvasRef.value && !offcanvasRef.value.contains(event.target as HTMLElement)) {
121-
if (event.key === 'Escape' && props.keyboard && props.backdrop) {
122-
return handleDismiss()
123-
}
120+
const handleBackdropDismiss = () => {
121+
if (props.backdrop !== 'static') {
122+
handleDismiss()
124123
}
125124
}
126125

127-
const handleMouseDown = (event: Event) => {
128-
window.addEventListener('mouseup', () => handleMouseUp(event), { once: true })
129-
}
130-
131-
const handleMouseUp = (event: Event) => {
132-
if (offcanvasRef.value && !offcanvasRef.value.contains(event.target as HTMLElement)) {
133-
props.backdrop && handleDismiss()
126+
const handleKeyDown = (event: KeyboardEvent) => {
127+
console.log('keydown')
128+
if (event.key === 'Escape' && props.keyboard) {
129+
handleDismiss()
134130
}
135131
}
136132

@@ -155,8 +151,10 @@ const COffcanvas = defineComponent({
155151
[`offcanvas-${props.placement}`]: props.placement,
156152
},
157153
],
154+
onKeydown: (event: KeyboardEvent) => handleKeyDown(event),
158155
ref: offcanvasRef,
159156
role: 'dialog',
157+
tabindex: -1,
160158
},
161159
slots.default && slots.default(),
162160
),
@@ -166,6 +164,7 @@ const COffcanvas = defineComponent({
166164
props.backdrop &&
167165
h(CBackdrop, {
168166
class: 'offcanvas-backdrop',
167+
onClick: handleBackdropDismiss,
169168
visible: visible.value,
170169
}),
171170
]

packages/docs/components/offcanvas.md

+121-83
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,126 @@ Use the buttons below to show and hide an offcanvas component.
8585
</script>
8686
```
8787

88+
### Body scrolling
89+
90+
Scrolling the `<body>` element is disabled when an offcanvas and its backdrop are visible. Use the scroll property to toggle `<body>` scrolling and backdrop to toggle the backdrop.
91+
92+
::: demo
93+
<CButton color="primary" @click="() => { visibleScrolling = !visibleScrolling }">Enable body scrolling</CButton>
94+
<COffcanvas :backdrop="false" placement="start" scroll :visible="visibleScrolling" @hide="() => { visibleScrolling = !visibleScrolling }">
95+
<COffcanvasHeader>
96+
<COffcanvasTitle>Offcanvas</COffcanvasTitle>
97+
<CCloseButton class="text-reset" @click="() => { visibleScrolling = false }"/>
98+
</COffcanvasHeader>
99+
<COffcanvasBody>
100+
<p>Try scrolling the rest of the page to see this option in action.</p>
101+
</COffcanvasBody>
102+
</COffcanvas>
103+
:::
104+
```vue
105+
<template>
106+
<CButton color="primary" @click="() => { visibleScrolling = !visibleScrolling }">Enable body scrolling</CButton>
107+
<COffcanvas :backdrop="false" placement="start" scroll :visible="visibleScrolling" @hide="() => { visibleScrolling = !visibleScrolling }">
108+
<COffcanvasHeader>
109+
<COffcanvasTitle>Offcanvas</COffcanvasTitle>
110+
<CCloseButton class="text-reset" @click="() => { visibleScrolling = false }"/>
111+
</COffcanvasHeader>
112+
<COffcanvasBody>
113+
<p>Try scrolling the rest of the page to see this option in action.</p>
114+
</COffcanvasBody>
115+
</COffcanvas>
116+
</template>
117+
<script>
118+
export default {
119+
data() {
120+
return {
121+
visibleScrolling: false,
122+
}
123+
}
124+
}
125+
</script>
126+
```
127+
128+
### Body scrolling and backdrop
129+
130+
You can also enable `<body>` scrolling with a visible backdrop.
131+
132+
::: demo
133+
<CButton color="primary" @click="() => { visibleWithBothOptions = !visibleWithBothOptions }">Enable both scrolling & backdrop</CButton>
134+
<COffcanvas placement="start" scroll :visible="visibleWithBothOptions" @hide="() => { visibleWithBothOptions = !visibleWithBothOptions }">
135+
<COffcanvasHeader>
136+
<COffcanvasTitle>Offcanvas</COffcanvasTitle>
137+
<CCloseButton class="text-reset" @click="() => { visibleWithBothOptions = false }"/>
138+
</COffcanvasHeader>
139+
<COffcanvasBody>
140+
<p>Try scrolling the rest of the page to see this option in action.</p>
141+
</COffcanvasBody>
142+
</COffcanvas>
143+
:::
144+
```vue
145+
<template>
146+
<CButton color="primary" @click="() => { visibleWithBothOptions = !visibleWithBothOptions }">Enable both scrolling &amp; backdrop</CButton>
147+
<COffcanvas placement="start" scroll :visible="visibleWithBothOptions" @hide="() => { visibleWithBothOptions = !visibleWithBothOptions }">
148+
<COffcanvasHeader>
149+
<COffcanvasTitle>Offcanvas</COffcanvasTitle>
150+
<CCloseButton class="text-reset" @click="() => { visibleWithBothOptions = false }"/>
151+
</COffcanvasHeader>
152+
<COffcanvasBody>
153+
<p>Try scrolling the rest of the page to see this option in action.</p>
154+
</COffcanvasBody>
155+
</COffcanvas>
156+
</template>
157+
<script>
158+
export default {
159+
data() {
160+
return {
161+
visibleWithBothOptions: false,
162+
}
163+
}
164+
}
165+
</script>
166+
```
167+
168+
### Static backdrop
169+
170+
If you set a `backdrop` to `static`, your React offcanvas component will not close when clicking outside of it.
171+
172+
::: demo
173+
<CButton color="primary" @click="() => { visibleWithStaticBackdrop = !visibleWithStaticBackdrop }">Toggle static offcanvas</CButton>
174+
<COffcanvas backdrop="static" placement="start" :visible="visibleWithStaticBackdrop" @hide="() => { visibleWithStaticBackdrop = !visibleWithStaticBackdrop }">
175+
<COffcanvasHeader>
176+
<COffcanvasTitle>Offcanvas</COffcanvasTitle>
177+
<CCloseButton class="text-reset" @click="() => { visibleWithStaticBackdrop = false }"/>
178+
</COffcanvasHeader>
179+
<COffcanvasBody>
180+
<p>I will not close if you click outside of me.</p>
181+
</COffcanvasBody>
182+
</COffcanvas>
183+
:::
184+
```vue
185+
<template>
186+
<CButton color="primary" @click="() => { visibleWithStaticBackdrop = !visibleWithStaticBackdrop }">Toggle static offcanvas</CButton>
187+
<COffcanvas backdrop="static" placement="start" :visible="visibleWithStaticBackdrop" @hide="() => { visibleWithStaticBackdrop = !visibleWithStaticBackdrop }">
188+
<COffcanvasHeader>
189+
<COffcanvasTitle>Offcanvas</COffcanvasTitle>
190+
<CCloseButton class="text-reset" @click="() => { visibleWithStaticBackdrop = false }"/>
191+
</COffcanvasHeader>
192+
<COffcanvasBody>
193+
<p>I will not close if you click outside of me.</p>
194+
</COffcanvasBody>
195+
</COffcanvas>
196+
</template>
197+
<script>
198+
export default {
199+
data() {
200+
return {
201+
visibleWithStaticBackdrop: false,
202+
}
203+
}
204+
}
205+
</script>
206+
```
207+
88208
## Placement
89209

90210
There's no default placement for offcanvas components, so you must add one of the modifier classes below;
@@ -210,88 +330,6 @@ Try the top, right, and bottom examples out below.
210330
</script>
211331
```
212332

213-
## Backdrop
214-
215-
Scrolling the `<body>` element is disabled when an offcanvas and its backdrop are visible. Use the `scroll` property to toggle `<body>` scrolling and `backdrop` to toggle the backdrop.
216-
217-
::: demo
218-
<CButton color="primary" @click="() => { visibleScrolling = !visibleScrolling }">Enable body scrolling</CButton>
219-
<CButton color="primary" @click="() => { visibleWithBackdrop = !visibleWithBackdrop }">Enable backdrop (default)</CButton>
220-
<CButton color="primary" @click="() => { visibleWithBothOptions = !visibleWithBothOptions }">Enable both scrolling & backdrop</CButton>
221-
<COffcanvas :backdrop="false" placement="start" scroll :visible="visibleScrolling" @hide="() => { visibleScrolling = !visibleScrolling }">
222-
<COffcanvasHeader>
223-
<COffcanvasTitle>Offcanvas</COffcanvasTitle>
224-
<CCloseButton class="text-reset" @click="() => { visibleScrolling = false }"/>
225-
</COffcanvasHeader>
226-
<COffcanvasBody>
227-
<p>Try scrolling the rest of the page to see this option in action.</p>
228-
</COffcanvasBody>
229-
</COffcanvas>
230-
<COffcanvas placement="start" :visible="visibleWithBackdrop" @hide="() => { visibleWithBackdrop = !visibleWithBackdrop }">
231-
<COffcanvasHeader>
232-
<COffcanvasTitle>Offcanvas</COffcanvasTitle>
233-
<CCloseButton class="text-reset" @click="() => { visibleWithBackdrop = false }"/>
234-
</COffcanvasHeader>
235-
<COffcanvasBody>
236-
<p>.....</p>
237-
</COffcanvasBody>
238-
</COffcanvas>
239-
<COffcanvas placement="start" scroll :visible="visibleWithBothOptions" @hide="() => { visibleWithBothOptions = !visibleWithBothOptions }">
240-
<COffcanvasHeader>
241-
<COffcanvasTitle>Offcanvas</COffcanvasTitle>
242-
<CCloseButton class="text-reset" @click="() => { visibleWithBothOptions = false }"/>
243-
</COffcanvasHeader>
244-
<COffcanvasBody>
245-
<p>Try scrolling the rest of the page to see this option in action.</p>
246-
</COffcanvasBody>
247-
</COffcanvas>
248-
:::
249-
```vue
250-
<template>
251-
<CButton color="primary" @click="() => { visibleScrolling = !visibleScrolling }">Enable body scrolling</CButton>
252-
<CButton color="primary" @click="() => { visibleWithBackdrop = !visibleWithBackdrop }">Enable backdrop (default)</CButton>
253-
<CButton color="primary" @click="() => { visibleWithBothOptions = !visibleWithBothOptions }">Enable both scrolling &amp; backdrop</CButton>
254-
<COffcanvas :backdrop="false" placement="start" scroll :visible="visibleScrolling" @hide="() => { visibleScrolling = !visibleScrolling }">
255-
<COffcanvasHeader>
256-
<COffcanvasTitle>Offcanvas</COffcanvasTitle>
257-
<CCloseButton class="text-reset" @click="() => { visibleScrolling = false }"/>
258-
</COffcanvasHeader>
259-
<COffcanvasBody>
260-
<p>Try scrolling the rest of the page to see this option in action.</p>
261-
</COffcanvasBody>
262-
</COffcanvas>
263-
<COffcanvas placement="start" :visible="visibleWithBackdrop" @hide="() => { visibleWithBackdrop = !visibleWithBackdrop }">
264-
<COffcanvasHeader>
265-
<COffcanvasTitle>Offcanvas</COffcanvasTitle>
266-
<CCloseButton class="text-reset" @click="() => { visibleWithBackdrop = false }"/>
267-
</COffcanvasHeader>
268-
<COffcanvasBody>
269-
<p>.....</p>
270-
</COffcanvasBody>
271-
</COffcanvas>
272-
<COffcanvas placement="start" scroll :visible="visibleWithBothOptions" @hide="() => { visibleWithBothOptions = !visibleWithBothOptions }">
273-
<COffcanvasHeader>
274-
<COffcanvasTitle>Offcanvas</COffcanvasTitle>
275-
<CCloseButton class="text-reset" @click="() => { visibleWithBothOptions = false }"/>
276-
</COffcanvasHeader>
277-
<COffcanvasBody>
278-
<p>Try scrolling the rest of the page to see this option in action.</p>
279-
</COffcanvasBody>
280-
</COffcanvas>
281-
</template>
282-
<script>
283-
export default {
284-
data() {
285-
return {
286-
visibleScrolling: false,
287-
visibleWithBackdrop: false,
288-
visibleWithBothOptions: false,
289-
}
290-
}
291-
}
292-
</script>
293-
```
294-
295333
## Accessibility
296334

297335
Since the offcanvas panel is conceptually a modal dialog, be sure to add `aria-labelledby="..."`—referencing the offcanvas title—to `<COffcanvas>`. Note that you don’t need to add `role="dialog"` since we already add it automatically.
@@ -305,7 +343,7 @@ Since the offcanvas panel is conceptually a modal dialog, be sure to add `aria-l
305343
visibleEnd: false,
306344
visibleBottom: false,
307345
visibleScrolling: false,
308-
visibleWithBackdrop: false,
346+
visibleWithStaticBackdrop: false,
309347
visibleWithBothOptions: false,
310348
}
311349
}

0 commit comments

Comments
 (0)