Skip to content

Commit 7ace127

Browse files
authored
Merge pull request #3390 from nextcloud/chore/refactor-pillmenu
chore: replace NcCheckboxRadioSwitch with NcRadioGroup in PillMenu
2 parents fb8eeb4 + e04f605 commit 7ace127

5 files changed

Lines changed: 55 additions & 49 deletions

File tree

playwright/support/sections/ResultsSection.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,19 @@ export class ResultsSection {
2424
}
2525

2626
public async switchToSummary(): Promise<void> {
27-
// NcCheckboxRadioSwitch renders a hidden <input type="radio"> with
28-
// v-on="{ change: onToggle }". Dispatch the change event directly.
29-
await this.summaryTab.dispatchEvent('change')
27+
if (await this.summaryTab.isChecked()) {
28+
return
29+
}
30+
// NcRadioGroupButton wraps the hidden radio input in a clickable container.
31+
// Click the parent to trigger the same interaction path as a real user.
32+
await this.summaryTab.locator('xpath=..').click()
3033
}
3134

3235
public async switchToResponses(): Promise<void> {
33-
await this.responsesTab.dispatchEvent('change')
36+
if (await this.responsesTab.isChecked()) {
37+
return
38+
}
39+
await this.responsesTab.locator('xpath=..').click()
3440
}
3541

3642
/**

playwright/support/sections/TopBarSection.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,9 @@ export class TopBarSection {
3737
if (await radio.isChecked()) {
3838
return
3939
}
40-
// NcCheckboxRadioSwitch hides the input inside a label; click the label
41-
// to trigger Vue's event chain rather than force-checking the hidden input
42-
// (which would fail because Vue resets the controlled input state before
43-
// Playwright can verify it)
40+
// The radio input is visually hidden and wrapped in a clickable button-like
41+
// container. Click the parent container to follow the real user interaction
42+
// path and let Vue update the controlled state.
4443
await radio.locator('xpath=..').click()
4544
await this.page.waitForURL(viewRoutes[view])
4645
}

src/components/PillMenu.vue

Lines changed: 40 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,38 +5,39 @@
55

66
<template>
77
<div class="pill-menu">
8-
<NcCheckboxRadioSwitch
9-
v-for="option of options"
10-
:key="option.id"
11-
:aria-label="isMobile ? option.ariaLabel : null"
8+
<NcRadioGroup
9+
:label="groupLabel"
1210
:modelValue="active.id"
13-
:disabled="disabled || option.disabled"
14-
class="pill-menu__toggle"
15-
:class="{ 'pill-menu__toggle--icon-only': isMobile && option.icon }"
16-
buttonVariant
17-
buttonVariantGrouped="horizontal"
18-
type="radio"
19-
:value="option.id"
20-
@update:modelValue="$emit('update:active', option)">
21-
<template v-if="option.icon" #icon>
22-
<NcIconSvgWrapper :svg="option.icon" />
23-
</template>
24-
{{ !isMobile || !option.icon ? option.title : null }}
25-
</NcCheckboxRadioSwitch>
11+
hideLabel
12+
@update:modelValue="onUpdateActive">
13+
<NcRadioGroupButton
14+
v-for="option of options"
15+
:key="option.id"
16+
:value="option.id"
17+
:aria-label="isMobile && option.icon ? option.ariaLabel : undefined"
18+
:label="!isMobile || !option.icon ? option.title : undefined"
19+
:disabled="disabled || option.disabled">
20+
<template v-if="option.icon" #icon>
21+
<NcIconSvgWrapper :svg="option.icon" />
22+
</template>
23+
</NcRadioGroupButton>
24+
</NcRadioGroup>
2625
</div>
2726
</template>
2827

2928
<script>
3029
import { useIsSmallMobile } from '@nextcloud/vue'
31-
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
3230
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
31+
import NcRadioGroup from '@nextcloud/vue/components/NcRadioGroup'
32+
import NcRadioGroupButton from '@nextcloud/vue/components/NcRadioGroupButton'
3333
3434
export default {
3535
name: 'PillMenu',
3636
3737
components: {
38-
NcCheckboxRadioSwitch,
3938
NcIconSvgWrapper,
39+
NcRadioGroup,
40+
NcRadioGroupButton,
4041
},
4142
4243
props: {
@@ -56,6 +57,14 @@ export default {
5657
default: false,
5758
},
5859
60+
/**
61+
* Accessible label for the radio group
62+
*/
63+
groupLabel: {
64+
type: String,
65+
required: true,
66+
},
67+
5968
/**
6069
* List of available options
6170
* `option: {id: string, title: string, ariaLabel: string, icon?: string}`
@@ -73,27 +82,17 @@ export default {
7382
isMobile: useIsSmallMobile(),
7483
}
7584
},
76-
}
77-
</script>
78-
79-
<style lang="scss" scoped>
80-
.pill-menu {
81-
align-items: center;
82-
align-self: flex-end;
83-
display: flex;
84-
justify-content: flex-end;
8585
86-
#{&} &__toggle {
87-
// Make it a bit more condensed
88-
:deep(.checkbox-radio-switch__content) {
89-
flex-direction: row;
90-
padding-block: 0;
91-
}
92-
93-
// Make icon only toggle round intead of elipse
94-
&--icon-only :deep(.checkbox-radio-switch__content) {
95-
padding-inline: 0;
96-
}
97-
}
86+
methods: {
87+
/**
88+
* Emit the full selected option to keep PillMenu API stable
89+
*
90+
* @param {string} optionId The selected option id
91+
*/
92+
onUpdateActive(optionId) {
93+
const option = this.options.find((entry) => entry.id === optionId)
94+
if (option) this.$emit('update:active', option)
95+
},
96+
},
9897
}
99-
</style>
98+
</script>

src/components/TopBar.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
v-if="!canOnlySubmit && currentView"
1515
:active="currentView"
1616
:options="availableViews"
17+
:groupLabel="t('forms', 'View mode')"
1718
@update:active="onChangeView" />
1819
<NcButton
1920
v-if="canShare && !sidebarOpened"

src/views/Results.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
v-model:active="activeResponseView"
4646
:disabled="noSubmissions"
4747
:options="responseViews"
48+
:groupLabel="t('forms', 'View mode')"
4849
class="response-actions__toggle"
4950
@update:active="loadFormResults" />
5051

0 commit comments

Comments
 (0)