Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add KFocusTrap and use it in KModal #799

Merged
merged 25 commits into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
18ba260
remove deprecated dropshadows
lokesh-sagi125 Sep 1, 2024
a37e568
add kfocustrap to kds
lokesh-sagi125 Sep 2, 2024
1a30127
Update KFocusTrap.vue
lokesh-sagi125 Sep 2, 2024
dfc92e4
Update definitions.scss
lokesh-sagi125 Sep 2, 2024
d2fe767
Update yarn.lock
lokesh-sagi125 Sep 2, 2024
4ce8852
Update yarn.lock
lokesh-sagi125 Sep 2, 2024
c324310
Fix linting issue
akolson Sep 2, 2024
83e8312
Update lib/KFocusTrap.vue
lokesh-sagi125 Sep 3, 2024
dbec4b6
Make KRadioButton to show a warning for developers when it's not nest…
lokesh-sagi125 Sep 15, 2024
fb45ecc
Merge branch 'learningequality:develop' into develop
lokesh-sagi125 Sep 15, 2024
bf0c943
revert changes to Kmodal
lokesh-sagi125 Sep 15, 2024
02015c7
Merge branch 'develop' of https://github.com/lokesh-sagi125/kolibri-d…
lokesh-sagi125 Sep 15, 2024
2aaed51
Update KModal.vue
lokesh-sagi125 Sep 15, 2024
975b029
Merge branch 'learningequality:develop' into develop
lokesh-sagi125 Oct 11, 2024
1639251
Add KfocusTrap and wrap KModal with it
lokesh-sagi125 Oct 11, 2024
ec0e778
/
lokesh-sagi125 Oct 11, 2024
c142e14
Revert "/"
lokesh-sagi125 Oct 11, 2024
c831113
..
lokesh-sagi125 Oct 11, 2024
ee9c735
..
lokesh-sagi125 Oct 11, 2024
0b16091
add documentation
lokesh-sagi125 Oct 13, 2024
db8facf
Update KFocusTrap.vue
lokesh-sagi125 Oct 13, 2024
d68e59a
changes for the documentation page
lokesh-sagi125 Oct 22, 2024
c7bbe35
Update lib/KFocusTrap.vue
lokesh-sagi125 Oct 24, 2024
46fa2a3
Update lib/KFocusTrap.vue
lokesh-sagi125 Oct 24, 2024
9086148
Update lib/KFocusTrap.vue
lokesh-sagi125 Oct 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
}),
],
}),
];
112 changes: 112 additions & 0 deletions lib/KFocusTrap.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<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>
lokesh-sagi125 marked this conversation as resolved.
Show resolved Hide resolved

<!-- 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>
lokesh-sagi125 marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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>
lokesh-sagi125 marked this conversation as resolved.
Show resolved Hide resolved
.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);
}
Loading