Skip to content

Commit

Permalink
Merge pull request #799 from lokesh-sagi125/develop
Browse files Browse the repository at this point in the history
Add KFocusTrap and use it in KModal
  • Loading branch information
AlexVelezLl authored Oct 28, 2024
2 parents 1fe45d3 + 9086148 commit 83d6a3b
Show file tree
Hide file tree
Showing 6 changed files with 228 additions and 84 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ docs/jsdocs.js
# IDE
.idea
kolibri-design-system.iml
docs/pages/playground.vue
20 changes: 20 additions & 0 deletions docs/pages/kfocustrap.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<template>

<DocsPageTemplate apiDocs>

<!-- Overview Section -->
<DocsPageSection title="Overview" anchor="#overview">
<p>
The <code>KFocusTrap</code> component ensures that keyboard focus is trapped within a specific region of the page.
</p>
<p>
When the <code>disabled</code> prop is set to <code>true</code>, focus trapping is disabled, allowing normal tab behavior. The focus trap moves the focus between the first and last elements in the slot content.
</p>

</DocsPageSection>


</DocsPageTemplate>

</template>

5 changes: 5 additions & 0 deletions docs/tableOfContents.js
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,11 @@ export default [
isCode: true,
keywords: cardRelatedKeywords,
}),
new Page({
path: '/kfocustrap',
title: 'KFocusTrap',
isCode: true,
}),
],
}),
];
114 changes: 114 additions & 0 deletions lib/KFocusTrap.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<template>

<div>
<!-- Focus trap starting point. If not disabled, focuses this first when tabbing. -->
<div
v-if="!disabled"
class="focus-trap-first"
tabindex="0"
@focus="handleFirstTrapFocus"
></div>

<!--@slot Default slot where the focusable content will be rendered -->
<slot>

</slot>

<!-- Focus trap ending point. If not disabled, focuses this last when tabbing. -->
<div
v-if="!disabled"
class="focus-trap-last"
tabindex="0"
@focus="handleLastTrapFocus"
></div>
</div>

</template>


<script>
/**
* This component ensures that focus moves between the first element
* and the last element of content provided by the default slot.
* In disabled state, it only renders whatever has been passed
* to the default slot, and doesn't add any focus trap behavior,
* allowing for flexible use from parent components.
*/
export default {
name: 'KFocusTrap',
props: {
/**
* Disables the focus trap when set to `true`. Focus will behave normally.
* @type {Boolean}
* @default false
*/
disabled: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
isTrapActive: false, // Tracks whether the focus trap is currently active
};
},
methods: {
/**
* Called when the first focus trap element receives focus.
* If the trap is not yet active, redirects focus to the first element.
* Otherwise, redirects focus to the last element to enforce the loop.
* @param {Event} e - Focus event
*/
handleFirstTrapFocus(e) {
e.stopPropagation();
if (!this.isTrapActive) {
this.focusFirstEl();
this.isTrapActive = true;
} else {
this.focusLastEl();
}
},
/**
* Called when the last focus trap element receives focus.
* Redirects focus to the first element, ensuring focus remains within the trap.
* @param {Event} e - Focus event
*/
handleLastTrapFocus(e) {
e.stopPropagation();
this.focusFirstEl();
},
focusFirstEl() {
/**
* Emits an event to notify the parent component to focus the first element.
*/
this.$emit('shouldFocusFirstEl');
},
focusLastEl() {
/**
* Emits an event to notify the parent component to focus the last element.
*/
this.$emit('shouldFocusLastEl');
},
/**
* @public
* Reset the next focus to the first focus element
*/
reset() {
this.isTrapActive = false;
},
},
};
</script>


<style scoped>
.focus-trap-first,
.focus-trap-last {
outline: none; /* Prevents focus outline */
}
</style>
170 changes: 86 additions & 84 deletions lib/KModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,98 +3,100 @@
<component :is="wrapper">
<!-- Accessibility properties for the overlay -->
<transition name="modal-fade" appear>
<div
id="modal-window"
ref="modal-overlay"
class="modal-overlay"
@keyup.esc.stop="emitCancelEvent"
@keyup.enter="handleEnter"
>
<!-- KeenUiSelect targets modal by using div.modal selector -->
<KFocusTrap>
<div
ref="modal"
class="modal"
:tabindex="0"
role="dialog"
aria-labelledby="modal-title"
:style="[
modalSizeStyles,
{ background: $themeTokens.surface },
containsKSelect ? { overflowY: 'unset' } : { overflowY: 'auto' }
]"
id="modal-window"
ref="modal-overlay"
class="modal-overlay"
@keyup.esc.stop="emitCancelEvent"
@keyup.enter="handleEnter"
>

<!-- Modal Title -->
<h1
id="modal-title"
ref="title"
class="title"
<!-- KeenUiSelect targets modal by using div.modal selector -->
<div
ref="modal"
class="modal"
:tabindex="0"
role="dialog"
aria-labelledby="modal-title"
:style="[
modalSizeStyles,
{ background: $themeTokens.surface },
containsKSelect ? { overflowY: 'unset' } : { overflowY: 'auto' }
]"
>
{{ title }}
<!-- Accessible error reporting per @radina -->
<span
v-if="hasError"
class="visuallyhidden"

<!-- Modal Title -->
<h1
id="modal-title"
ref="title"
class="title"
>
{{ errorMessage }}
</span>
</h1>

<!-- Stop propagation of enter key to prevent the submit event from being emitted twice -->
<form
class="form"
@submit.prevent="emitSubmitEvent"
@keyup.enter.stop
>
<!-- Wrapper for main content -->
<div
ref="content"
class="content"
:style="[ contentSectionMaxHeight, scrollShadow ? {
borderTop: `1px solid ${$themeTokens.fineLine}`,
borderBottom: `1px solid ${$themeTokens.fineLine}`,
} : {} ]"
:class="{
'scroll-shadow': scrollShadow,
'contains-kselect': containsKSelect
}"
{{ title }}
<!-- Accessible error reporting per @radina -->
<span
v-if="hasError"
class="visuallyhidden"
>
{{ errorMessage }}
</span>
</h1>

<!-- Stop propagation of enter key to prevent the submit event from being emitted twice -->
<form
class="form"
@submit.prevent="emitSubmitEvent"
@keyup.enter.stop
>
<!-- @slot Main content of modal -->
<slot></slot>
</div>
<!-- Wrapper for main content -->
<div
ref="content"
class="content"
:style="[ contentSectionMaxHeight, scrollShadow ? {
borderTop: `1px solid ${$themeTokens.fineLine}`,
borderBottom: `1px solid ${$themeTokens.fineLine}`,
} : {} ]"
:class="{
'scroll-shadow': scrollShadow,
'contains-kselect': containsKSelect
}"
>
<!-- @slot Main content of modal -->
<slot></slot>
</div>

<div
ref="actions"
class="actions"
>
<!-- @slot Alternative buttons and actions below main content -->
<slot
v-if="$slots.actions"
name="actions"
<div
ref="actions"
class="actions"
>
</slot>
<template v-else>
<KButton
v-if="cancelText"
name="cancel"
:text="cancelText"
appearance="flat-button"
:disabled="cancelDisabled || $attrs.disabled"
@click="emitCancelEvent"
/>
<KButton
v-if="submitText"
name="submit"
:text="submitText"
:primary="true"
:disabled="submitDisabled || $attrs.disabled"
type="submit"
/>
</template>
</div>
</form>
<!-- @slot Alternative buttons and actions below main content -->
<slot
v-if="$slots.actions"
name="actions"
>
</slot>
<template v-else>
<KButton
v-if="cancelText"
name="cancel"
:text="cancelText"
appearance="flat-button"
:disabled="cancelDisabled || $attrs.disabled"
@click="emitCancelEvent"
/>
<KButton
v-if="submitText"
name="submit"
:text="submitText"
:primary="true"
:disabled="submitDisabled || $attrs.disabled"
type="submit"
/>
</template>
</div>
</form>
</div>
</div>
</div>
</KFocusTrap>
</transition>
</component>

Expand Down
2 changes: 2 additions & 0 deletions lib/KThemePlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import KTransition from './KTransition';
import KTextTruncator from './KTextTruncator';
import KLogo from './KLogo';
import KRadioButtonGroup from './KRadioButtonGroup.vue';
import KFocusTrap from './KFocusTrap.vue';

import { themeTokens, themeBrand, themePalette, themeOutlineStyle } from './styles/theme';
import globalThemeState from './styles/globalThemeState';
Expand Down Expand Up @@ -163,4 +164,5 @@ export default function KThemePlugin(Vue) {
Vue.component('KTransition', KTransition);
Vue.component('KTextTruncator', KTextTruncator);
Vue.component('KRadioButtonGroup', KRadioButtonGroup);
Vue.component('KFocusTrap', KFocusTrap);
}

0 comments on commit 83d6a3b

Please sign in to comment.