Skip to content

Commit 84bda22

Browse files
committed
Copy side panel modal to Studio
1 parent 4863c93 commit 84bda22

3 files changed

Lines changed: 362 additions & 0 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { createTranslator } from 'shared/i18n';
2+
3+
export const commonStrings = createTranslator('CommonStrings', {
4+
backAction: {
5+
message: 'Back',
6+
context:
7+
'Indicates going back to a previous step in multi-step workflows. It can be used as a label of the back button that is displayed next to the continue button.',
8+
},
9+
closeAction: {
10+
message: 'Close',
11+
context: 'A label for an action that closes a dialog or window',
12+
},
13+
});
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<template>
2+
3+
<transition
4+
name="backdrop"
5+
appear
6+
>
7+
<div
8+
class="backdrop"
9+
:class="{ 'has-transitions': transitions }"
10+
@click="$emit('click')"
11+
>
12+
<slot></slot>
13+
</div>
14+
</transition>
15+
16+
</template>
17+
18+
19+
<script>
20+
21+
export default {
22+
name: 'Backdrop',
23+
props: {
24+
transitions: {
25+
// If true, backdrop will have enter/leave transitions
26+
type: Boolean,
27+
default: false,
28+
},
29+
},
30+
};
31+
32+
</script>
33+
34+
35+
<style lang="scss" scoped>
36+
37+
.backdrop {
38+
position: fixed;
39+
top: 0;
40+
right: 0;
41+
bottom: 0;
42+
left: 0;
43+
// Overlays everything except the sidepanel and KModals
44+
z-index: 11;
45+
width: 100%;
46+
height: 100%;
47+
background: rgba(0, 0, 0, 0.7);
48+
background-attachment: fixed;
49+
}
50+
51+
.has-transitions.backdrop-enter {
52+
opacity: 0;
53+
}
54+
55+
.has-transitions.backdrop-enter-to {
56+
opacity: 1;
57+
}
58+
59+
.has-transitions.backdrop-enter-active {
60+
transition: opacity 0.2s ease-in-out;
61+
}
62+
63+
.has-transitions.backdrop-leave {
64+
opacity: 1;
65+
}
66+
67+
.has-transitions.backdrop-leave-to {
68+
opacity: 0;
69+
}
70+
71+
.has-transitions.backdrop-leave-active {
72+
transition: opacity 0.2s ease-in-out;
73+
}
74+
75+
</style>
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
<template>
2+
3+
<div
4+
ref="sidePanel"
5+
:tabindex="0"
6+
:class="{ 'is-rtl': isRtl, 'is-mobile': isMobile }"
7+
@keyup.esc="closePanel"
8+
>
9+
<transition name="side-panel">
10+
<KFocusTrap
11+
@shouldFocusFirstEl="focusFirstEl"
12+
@shouldFocusLastEl="focusLastEl"
13+
>
14+
<section
15+
class="side-panel"
16+
role="region"
17+
:style="sidePanelStyles"
18+
:aria-label="ariaLabel"
19+
>
20+
<!-- Fixed header -->
21+
<div
22+
ref="fixedHeader"
23+
:class="{ 'side-panel-header': true, immersive: immersive }"
24+
:style="headerStyles"
25+
>
26+
<div
27+
class="header-content"
28+
:style="{
29+
flexDirection: closeButtonIconType === 'close' ? 'row' : 'row-reverse',
30+
}"
31+
>
32+
<div style="overflow: hidden">
33+
<slot name="header"> </slot>
34+
</div>
35+
<KIconButton
36+
v-if="closeButtonIconType"
37+
:icon="closeButtonIconType"
38+
class="close-button"
39+
:ariaLabel="closeButtonMessage"
40+
:tooltip="closeButtonMessage"
41+
@click="closePanel"
42+
/>
43+
</div>
44+
</div>
45+
46+
<!-- Default slot for inserting content which will scroll on overflow -->
47+
<div
48+
class="side-panel-content"
49+
@scroll="isScrolled = $event.target.scrollTop > 0"
50+
>
51+
<slot :isScrolled="isScrolled"></slot>
52+
</div>
53+
<div
54+
v-if="$slots.bottomNavigation"
55+
ref="fixedBottombar"
56+
class="bottom-navigation"
57+
:style="{ backgroundColor: $themeTokens.surface }"
58+
>
59+
<slot name="bottomNavigation"></slot>
60+
</div>
61+
</section>
62+
</KFocusTrap>
63+
</transition>
64+
65+
<Backdrop
66+
:transitions="true"
67+
class="backdrop"
68+
@click="closePanel"
69+
/>
70+
</div>
71+
72+
</template>
73+
74+
75+
<script>
76+
77+
import { ref } from 'vue';
78+
import useKResponsiveWindow from 'kolibri-design-system/lib/composables/useKResponsiveWindow';
79+
import Backdrop from './Backdrop.vue';
80+
import { commonStrings } from 'shared/strings/commonStrings';
81+
82+
export default {
83+
name: 'SidePanelModal',
84+
components: {
85+
Backdrop,
86+
},
87+
setup() {
88+
const { windowBreakpoint } = useKResponsiveWindow();
89+
return {
90+
/* Will be calculated in mounted() as it will get the height of the fixedHeader then */
91+
// @type {RefImpl<number>}
92+
windowBreakpoint,
93+
lastFocus: null,
94+
isScrolled: ref(false),
95+
};
96+
},
97+
props: {
98+
/* CloseButtonIconType icon from parent component */
99+
closeButtonIconType: {
100+
type: String,
101+
required: false,
102+
default: 'close',
103+
validator: value => {
104+
return ['close', 'back'].includes(value);
105+
},
106+
},
107+
/* Optionally override the default width of the side panel with valid CSS value */
108+
sidePanelWidth: {
109+
type: String,
110+
required: false,
111+
default: '436px',
112+
},
113+
/* Which side of the screen should the panel be fixed? Reverses the value when isRtl */
114+
alignment: {
115+
type: String,
116+
required: true,
117+
validator(value) {
118+
return ['right', 'left'].includes(value);
119+
},
120+
},
121+
ariaLabel: {
122+
type: String,
123+
required: false,
124+
default: null,
125+
},
126+
immersive: {
127+
type: Boolean,
128+
required: false,
129+
default: false,
130+
},
131+
},
132+
computed: {
133+
isMobile() {
134+
// This should be suitable for any mobile/tablet
135+
return this.windowBreakpoint <= 2;
136+
},
137+
/* Returns an object with properties left or right set to the appropriate value
138+
depending on isRtl and this.alignment */
139+
rtlAlignment() {
140+
if (this.isRtl && this.alignment === 'left') {
141+
return 'right';
142+
} else if (this.isRtl && this.alignment === 'right') {
143+
return 'left';
144+
} else {
145+
return this.alignment;
146+
}
147+
},
148+
/* Returns an object with this.rtlAlignment set to 0 */
149+
langDirStyles() {
150+
return {
151+
[this.rtlAlignment]: 0,
152+
};
153+
},
154+
responsiveWidth() {
155+
return this.isMobile ? '100vw' : this.sidePanelWidth;
156+
},
157+
/** Styling Properties */
158+
headerStyles() {
159+
return {
160+
backgroundColor: this.immersive ? this.$themeTokens.appBar : this.$themeTokens.surface,
161+
borderBottom: `1px solid ${this.$themePalette.grey.v_400}`,
162+
};
163+
},
164+
sidePanelStyles() {
165+
return {
166+
...this.langDirStyles,
167+
width: this.responsiveWidth,
168+
top: 0,
169+
position: 'fixed',
170+
color: this.$themeTokens.text,
171+
backgroundColor: this.$themeTokens.surface,
172+
'z-index': 12,
173+
};
174+
},
175+
closeButtonMessage() {
176+
const { backAction, closeAction } = commonStrings;
177+
return this.closeButtonIconType === 'back' ? backAction : closeAction;
178+
},
179+
},
180+
beforeMount() {
181+
this.lastFocus = document.activeElement;
182+
},
183+
/* this is the easiest way I could think to avoid having dual scroll bars and to avoid
184+
strange screen-squeezing behavior noted here:
185+
https://user-images.githubusercontent.com/79847249/164241012-b161bad7-8a46-4221-a391-a375899ed9a9.mp4 */
186+
mounted() {
187+
const htmlTag = window.document.getElementsByTagName('html')[0];
188+
htmlTag.style['overflow-y'] = 'hidden';
189+
this.$nextTick(() => {
190+
this.focusFirstEl();
191+
this.$emit('shouldFocusFirstEl');
192+
});
193+
},
194+
beforeDestroy() {
195+
const htmlTag = window.document.getElementsByTagName('html')[0];
196+
htmlTag.style['overflow-y'] = 'auto';
197+
},
198+
destroyed() {
199+
window.setTimeout(() => this.lastFocus.focus());
200+
},
201+
methods: {
202+
closePanel() {
203+
this.$emit('closePanel');
204+
},
205+
focusLastEl() {
206+
this.$el.querySelector('.close-button').focus();
207+
},
208+
/**
209+
* @public
210+
* Reset the next focus to the first focus element
211+
*/
212+
focusFirstEl() {
213+
this.$el.querySelector('.close-button').focus();
214+
},
215+
},
216+
};
217+
218+
</script>
219+
220+
221+
<style lang="scss" scoped>
222+
223+
@import '~kolibri-design-system/lib/styles/definitions';
224+
225+
.header-content {
226+
display: flex;
227+
align-items: center;
228+
justify-content: space-between;
229+
width: 100%;
230+
height: 100%;
231+
}
232+
233+
/** Need to be sure a KDropdownMenu shows up on the Side Panel */
234+
/deep/ .tippy-popper {
235+
z-index: 24;
236+
}
237+
238+
.side-panel {
239+
display: flex;
240+
flex-direction: column;
241+
height: 100%;
242+
243+
.side-panel-header {
244+
z-index: 1;
245+
width: 100%;
246+
min-height: 60px;
247+
padding: 0 1em;
248+
249+
&.immersive {
250+
@extend %dropshadow-2dp;
251+
}
252+
}
253+
254+
.side-panel-content {
255+
flex-grow: 1;
256+
padding: 24px 32px 16px;
257+
overflow-x: hidden;
258+
overflow-y: auto;
259+
}
260+
261+
.bottom-navigation {
262+
@extend %dropshadow-2dp;
263+
264+
z-index: 1;
265+
display: flex;
266+
justify-content: space-between;
267+
width: 100%;
268+
padding: 1em;
269+
line-height: 2.5em;
270+
text-align: center;
271+
}
272+
}
273+
274+
</style>

0 commit comments

Comments
 (0)