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

Made a new KRadioButtonGroup component to fix Firefox radio button movement with arrow keys issue #650

Merged
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
fbdf0c0
Merge pull request #457 from learningequality/release-v1.5.x
MisRob Sep 28, 2023
2b44dbf
Merge remote-tracking branch 'upstream/develop' into develop
muditchoudhary Mar 29, 2024
7a2c1ad
Merge remote-tracking branch 'upstream/develop' into develop
muditchoudhary Jun 1, 2024
dd6a483
Add new KRadioButtonGroup Component
muditchoudhary Jun 3, 2024
ea5a9f1
Add setTabIndex method to set tab index on each radio component
muditchoudhary Jun 3, 2024
6100a15
Add test for KRadioButtonGroup component
muditchoudhary Jun 4, 2024
eb9c801
Add docs for KRadioButtonGroup
muditchoudhary Jun 4, 2024
fddf31e
Update tableOfContents
muditchoudhary Jun 4, 2024
afcd18f
Merge remote-tracking branch 'upstream/develop' into develop
muditchoudhary Jun 19, 2024
4b0688a
Add new KRadioButtonGroup Component
muditchoudhary Jun 3, 2024
87a3928
Add setTabIndex method to set tab index on each radio component
muditchoudhary Jun 3, 2024
ebaf9cb
Add test for KRadioButtonGroup component
muditchoudhary Jun 4, 2024
a8c1951
Add docs for KRadioButtonGroup
muditchoudhary Jun 4, 2024
3f2d8ea
Update tableOfContents
muditchoudhary Jun 4, 2024
b1d09dc
Fix: fix tab issue for change language model
muditchoudhary Jun 19, 2024
220eacf
Merge remote-tracking branch 'origin/KRadioButtonGroup-Component' int…
muditchoudhary Jun 19, 2024
29f339a
Update KRadioButtonGroup component
muditchoudhary Jun 20, 2024
73e8e76
fix lint
muditchoudhary Jul 10, 2024
df58010
Change keydown event with keyup event
muditchoudhary Jul 10, 2024
0e9be90
fix lint
muditchoudhary Jul 10, 2024
adb42c0
Fix some logics in KRadioButtonGroup, fix tests
muditchoudhary Aug 7, 2024
60d0b3d
Merge branch 'develop' into KRadioButtonGroup-Component
muditchoudhary Aug 8, 2024
3004607
Fix queryAndAddRadioBtns recursive logic and rename methods name
muditchoudhary Aug 17, 2024
c3e9712
Merge remote-tracking branch 'origin/KRadioButtonGroup-Component' int…
muditchoudhary Aug 17, 2024
b1b1816
Merge branch 'develop' into KRadioButtonGroup-Component
MisRob Aug 21, 2024
1fa0a0b
Update CHANGELOG.md
MisRob Aug 21, 2024
46566e4
Update documentation
MisRob Aug 21, 2024
fb4c3cf
Merge branch 'develop' into KRadioButtonGroup-Component
MisRob Aug 21, 2024
37272a8
Update documentation
MisRob Aug 21, 2024
bdea254
Add another example to docs
MisRob Aug 21, 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
55 changes: 55 additions & 0 deletions docs/pages/kradiobuttongroup.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<template>

<DocsPageTemplate :apiDocs="true">

<DocsPageSection title="Overview" anchor="#overview">
<p>
Allows movement between radio buttons inside a radio group using arrow keys in FireFox.
</p>
<DocsShow>
<KRadioButtonGroup>
<KRadioButton
v-model="exampleValue"
label="Option A"
value="val-a"
/>
<KRadioButton
v-model="exampleValue"
label="Option B"
value="val-b"
/>
<KRadioButton
v-model="exampleValue"
label="Option C"
description="This one is special!"
buttonValue="val-c"
/>
<KRadioButton
v-model="exampleValue"
label="Truncated label. Adjusting your browser window size to see this in action."
buttonValue="val-d"
truncateLabel
/>
</KRadioButtonGroup>
<p>
Current value: {{ exampleValue }}
</p>
</DocsShow>
</DocsPageSection>

</DocsPageTemplate>

</template>


<script>

export default {
data() {
return {
exampleValue: 'val-b',
};
},
};

</script>
5 changes: 5 additions & 0 deletions docs/tableOfContents.js
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,11 @@ export default [
title: 'KListWithOverflow',
isCode: true,
}),
new Page({
path: '/kradiobuttongroup',
title: 'KRadioButtonGroup',
isCode: true,
}),
],
}),
];
9 changes: 9 additions & 0 deletions lib/KRadioButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
ref="input"
v-autofocus="autofocus"
type="radio"
:tabindex="tabIndex"
class="k-radio-button-input"
:checked="isChecked"
:value="buttonValue !== null ? buttonValue : value"
Expand Down Expand Up @@ -148,6 +149,7 @@
},
data: () => ({
active: false,
tabIndex: 0,
}),
computed: {
isChecked() {
Expand Down Expand Up @@ -220,6 +222,13 @@
*/
this.$emit('blur');
},
/**
* @public
* Set the tabIndex value
MisRob marked this conversation as resolved.
Show resolved Hide resolved
*/
setTabIndex(val) {
this.tabIndex = val;
},
},
};

Expand Down
133 changes: 133 additions & 0 deletions lib/KRadioButtonGroup.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
<template>

<div class="k-radio-button-group">
<!-- @slot To render the KRadioButtons -->
<slot></slot>
</div>

</template>


<script>

export default {
name: 'KRadioButtonGroup',
props: {
/**
* Specifies whether the radio button group is enabled or disabled.
* Used to make the first radio button active when the radio group becomes enabled again after being disabled.
*/
enable: {
MisRob marked this conversation as resolved.
Show resolved Hide resolved
type: Boolean,
default: true,
},
},
data() {
return {
radioButtons: [],
focusedRadioIdx: 0,
firstRadioIdx: 0,
lastRadioIdx: 0,
};
},
computed: {
isFirefox() {
return navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
},
},
watch: {
// To focus on the first radio button when radio group enables again
enable(newVal, oldVal) {
MisRob marked this conversation as resolved.
Show resolved Hide resolved
if (newVal && !oldVal && this.radioButtons.length > 0) {
this.setChecked(0);
this.focusedRadioIdx = 0;
}
},
},
mounted() {
this.$nextTick(() => {
if (this.isFirefox) {
MisRob marked this conversation as resolved.
Show resolved Hide resolved
if (this.$children[0].$options._componentTag === 'KGridItem') {
MisRob marked this conversation as resolved.
Show resolved Hide resolved
this.$children.forEach(gridItem => {
gridItem.$children.forEach(fixedGridItem => {
fixedGridItem.$children.forEach(radioBtn => {
this.radioButtons.push(radioBtn);
radioBtn.$el.addEventListener('keyup', this.onKeyUp);
MisRob marked this conversation as resolved.
Show resolved Hide resolved
radioBtn.setTabIndex(-1);
});
});
});
} else {
this.$children.forEach(radioBtn => {
this.radioButtons.push(radioBtn);
radioBtn.$el.addEventListener('keyup', this.onKeyUp);
radioBtn.setTabIndex(-1);
});
}

this.lastRadioIdx = this.radioButtons.length - 1;
this.radioButtons[this.focusedRadioIdx].setTabIndex(0);
this.radioButtons.forEach((radioBtn, index) => {
radioBtn.$on('input', () => {
this.handleInputChange(index);
});
});
}
});
},
beforeDestroy() {
for (let radioBtn in this.radioButtons) {
radioBtn.$el.removeEventListener('keyup', this.onKeyUp);
}
},
methods: {
handleInputChange(idx) {
if (this.isFirefox) {
this.setChecked(idx);
this.focusedRadioIdx = idx;
}
},
onKeyUp(event) {
const handlers = {
ArrowLeft: this.focusPreviousRadio,
ArrowRight: this.focusNextRadio,
ArrowUp: this.focusPreviousRadio,
ArrowDown: this.focusNextRadio,
};
const handler = handlers[event.key];
if (handler) {
event.preventDefault();
event.stopPropagation();
handler(event);
}
},
focusPreviousRadio(event) {
let newFocusedRadioIdx =
this.focusedRadioIdx === this.firstRadioIdx
? this.lastRadioIdx
: this.focusedRadioIdx - 1;
this.focusRadio(newFocusedRadioIdx, event);
},
focusNextRadio(event) {
const newFocusedRadioIdx =
this.focusedRadioIdx === this.lastRadioIdx
? this.firstRadioIdx
: this.focusedRadioIdx + 1;
this.focusRadio(newFocusedRadioIdx, event);
},
setChecked(radioIdx) {
muditchoudhary marked this conversation as resolved.
Show resolved Hide resolved
for (let i = 0; i < this.radioButtons.length; i++) {
const radioBtn = this.radioButtons[i];
radioBtn.setTabIndex(-1);
}
this.radioButtons[radioIdx].setTabIndex(0);
},
focusRadio(radioIdx, event) {
if (radioIdx !== undefined && radioIdx !== null) {
this.radioButtons[radioIdx].toggleCheck(event);
MisRob marked this conversation as resolved.
Show resolved Hide resolved
}
},
},
};

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

import { themeTokens, themeBrand, themePalette, themeOutlineStyle } from './styles/theme';
import globalThemeState from './styles/globalThemeState';
Expand Down Expand Up @@ -127,4 +128,5 @@ export default function KThemePlugin(Vue) {
Vue.component('KTooltip', KTooltip);
Vue.component('KTransition', KTransition);
Vue.component('KTextTruncator', KTextTruncator);
Vue.component('KRadioButtonGroup', KRadioButtonGroup);
}
96 changes: 96 additions & 0 deletions lib/__tests__/KRadioButtonGroup.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { mount } from '@vue/test-utils';
import KRadioButtonGroup from '../KRadioButtonGroup.vue';
import KRadioButton from '../KRadioButton.vue';

describe('KRadioButtonGroup component', () => {
beforeEach(() => {
// Mocked the userAgent because KRadioButtionGroup implements roving tabIndex only for firefox
// So these tests are testing for firefox client only
Object.defineProperty(window.navigator, 'userAgent', {
value: 'mozilla/5.0 (x11; ubuntu; linux x86_64; rv:126.0) gecko/20100101 firefox/126.0',
writable: true,
});
});
describe('slot', () => {
it('renders two KRadioButton', () => {
const wrapper = mount(KRadioButtonGroup, {
slots: {
default: [
'<KRadioButton label="Option A" buttonValue="val-a" />',
'<KRadioButton label="Option B" buttonValue="val-b" />',
],
},
});
expect(wrapper.findAllComponents(KRadioButton).length).toBe(2);
});
});
describe('Behavior Tests', () => {
it('handles keyboard navigation (focus previous/next radio)', async () => {
const wrapper = mount(KRadioButtonGroup, {
slots: {
default: [
'<KRadioButton label="Option A" buttonValue="val-a" />',
'<KRadioButton label="Option B" buttonValue="val-b" />',
],
},
});
await wrapper.vm.$nextTick();

const radioButtons = wrapper.findAllComponents(KRadioButton);
expect(radioButtons.length).toBe(2);

await radioButtons.at(0).trigger('keyup', { key: 'ArrowDown' });
expect(wrapper.vm.focusedRadioIdx).toBe(1);
expect(radioButtons.at(0).vm.tabIndex).toBe(-1);
expect(radioButtons.at(1).vm.tabIndex).toBe(0);

await radioButtons.at(1).trigger('keyup', { key: 'ArrowUp' });
expect(wrapper.vm.focusedRadioIdx).toBe(0);
expect(radioButtons.at(0).vm.tabIndex).toBe(0);
expect(radioButtons.at(1).vm.tabIndex).toBe(-1);
});
it('handles click on radio correctly', async () => {
const wrapper = mount(KRadioButtonGroup, {
slots: {
default: [
'<KRadioButton label="Option A" buttonValue="val-a" />',
'<KRadioButton label="Option B" buttonValue="val-b" />',
'<KRadioButton label="Option C" buttonValue="val-c" />',
],
},
});
await wrapper.vm.$nextTick();
const radioButtons = wrapper.findAllComponents(KRadioButton);
await radioButtons.at(2).trigger('click');
expect(wrapper.vm.focusedRadioIdx).toBe(2);
MisRob marked this conversation as resolved.
Show resolved Hide resolved
expect(radioButtons.at(2).vm.tabIndex).toBe(0);
expect(radioButtons.at(1).vm.tabIndex).toBe(-1);
MisRob marked this conversation as resolved.
Show resolved Hide resolved
});
it('handles enabling and disabling correctly', async () => {
const wrapper = mount(KRadioButtonGroup, {
props: {
enable: true,
},
slots: {
default: [
'<KRadioButton label="Option A" buttonValue="val-a" />',
'<KRadioButton label="Option B" buttonValue="val-b" />',
'<KRadioButton label="Option C" buttonValue="val-c" />',
],
},
});
await wrapper.vm.$nextTick();
const radioButtons = wrapper.findAllComponents(KRadioButton);
await radioButtons.at(2).trigger('click');
expect(wrapper.vm.focusedRadioIdx).toBe(2);
expect(radioButtons.at(2).vm.tabIndex).toBe(0);
expect(radioButtons.at(1).vm.tabIndex).toBe(-1);

await wrapper.setProps({ enable: false });
await wrapper.setProps({ enable: true });
expect(wrapper.vm.focusedRadioIdx).toBe(0);
expect(radioButtons.at(0).vm.tabIndex).toBe(0);
expect(radioButtons.at(1).vm.tabIndex).toBe(-1);
});
});
});
Loading