Compare commits

...

1 Commits

Author SHA1 Message Date
dante01yoon
6174c2fcad fix(keybindings): progressively truncate keybinding column at narrow widths
Container-query-driven tiers in the keybindings settings panel:
- wide (>=16rem):   `Ctrl S , Ctrl Shift S + 1 more`
- medium (>=12rem): `Ctrl S + 2 more`
- compact (>=8rem): `Ctrl S + 2`
- narrowest:         first combo only

Lets the actions column stay fully visible instead of being clipped when the
settings dialog shrinks.

Fixes FE-523
2026-05-12 15:40:13 +09:00
4 changed files with 207 additions and 30 deletions

View File

@@ -106,34 +106,10 @@
:pt="{ bodyCell: 'p-1 min-h-8' }"
>
<template #body="slotProps">
<div
v-if="slotProps.data.keybindings.length > 0"
class="flex items-center gap-1"
>
<template
v-for="(binding, idx) in (
slotProps.data as ICommandData
).keybindings.slice(0, 2)"
:key="binding.combo.serialize()"
>
<span v-if="idx > 0" class="text-muted-foreground">,</span>
<KeyComboDisplay
:key-combo="binding.combo"
:is-modified="slotProps.data.isModified"
/>
</template>
<span
v-if="slotProps.data.keybindings.length > 2"
class="rounded-sm px-1.5 py-0.5 text-xs text-muted-foreground"
>
{{
$t('g.nMoreKeybindings', {
count: slotProps.data.keybindings.length - 2
})
}}
</span>
</div>
<span v-else>-</span>
<KeybindingList
:keybindings="slotProps.data.keybindings"
:is-modified="slotProps.data.isModified"
/>
</template>
</Column>
<Column
@@ -147,9 +123,15 @@
}}</span>
</template>
</Column>
<Column field="actions" header="" :pt="{ bodyCell: 'p-1 min-h-8' }">
<Column
field="actions"
header=""
:pt="{ bodyCell: 'p-1 min-h-8 whitespace-nowrap' }"
>
<template #body="slotProps">
<div class="actions flex flex-row justify-end">
<div
class="actions flex flex-row justify-end whitespace-nowrap"
>
<Button
v-if="slotProps.data.keybindings.length === 1"
v-tooltip="$t('g.edit')"
@@ -330,6 +312,7 @@ import { useCommandStore } from '@/stores/commandStore'
import { useDialogStore } from '@/stores/dialogStore'
import { normalizeI18nKey } from '@/utils/formatUtil'
import KeybindingList from './keybinding/KeybindingList.vue'
import KeybindingPresetToolbar from './keybinding/KeybindingPresetToolbar.vue'
import KeyComboDisplay from './keybinding/KeyComboDisplay.vue'

View File

@@ -0,0 +1,118 @@
import { render, screen } from '@testing-library/vue'
import { describe, expect, it } from 'vitest'
import { createI18n } from 'vue-i18n'
import { KeybindingImpl } from '@/platform/keybindings/keybinding'
import KeybindingList from './KeybindingList.vue'
const i18n = createI18n({
legacy: false,
locale: 'en',
messages: {
en: {
g: {
nMoreKeybindings: '+ {count} more',
nMoreKeybindingsCompact: '+ {count}',
keybindingListAriaLabel: 'Keybindings: {combos}'
}
}
}
})
function makeKeybinding(key: string, ctrl = false, shift = false) {
return new KeybindingImpl({
commandId: 'test.cmd',
combo: { key, ctrl, shift }
})
}
function renderList(props: {
keybindings: KeybindingImpl[]
isModified?: boolean
}) {
return render(KeybindingList, {
props,
global: { plugins: [i18n] }
})
}
describe('KeybindingList', () => {
it('renders "-" placeholder when there are no keybindings', () => {
renderList({ keybindings: [] })
expect(screen.getByText('-')).toBeInTheDocument()
expect(screen.queryByTestId('keybinding-list')).not.toBeInTheDocument()
})
it('renders a single keybinding without any "more" badge', () => {
renderList({ keybindings: [makeKeybinding('A', true)] })
expect(screen.getByTestId('keybinding-list')).toBeInTheDocument()
expect(
screen.queryByTestId('keybinding-list-more-wide')
).not.toBeInTheDocument()
expect(
screen.queryByTestId('keybinding-list-more-medium')
).not.toBeInTheDocument()
expect(
screen.queryByTestId('keybinding-list-more-compact')
).not.toBeInTheDocument()
})
it('with 2 keybindings: omits wide-tier badge, shows medium/compact for narrow widths', () => {
renderList({
keybindings: [makeKeybinding('A', true), makeKeybinding('B', true)]
})
expect(
screen.queryByTestId('keybinding-list-more-wide')
).not.toBeInTheDocument()
expect(screen.getByTestId('keybinding-list-more-medium')).toHaveTextContent(
'+ 1 more'
)
expect(
screen.getByTestId('keybinding-list-more-compact')
).toHaveTextContent('+ 1')
})
it('with 3 keybindings: wide-tier uses count-minus-two, narrower tiers use count-minus-one', () => {
renderList({
keybindings: [
makeKeybinding('A', true),
makeKeybinding('B', true),
makeKeybinding('C', true)
]
})
expect(screen.getByTestId('keybinding-list-more-wide')).toHaveTextContent(
'+ 1 more'
)
expect(screen.getByTestId('keybinding-list-more-medium')).toHaveTextContent(
'+ 2 more'
)
expect(
screen.getByTestId('keybinding-list-more-compact')
).toHaveTextContent('+ 2')
})
it('uses a container query parent so the visible tier can adapt to width', () => {
renderList({
keybindings: [makeKeybinding('A', true), makeKeybinding('B', true)]
})
expect(screen.getByTestId('keybinding-list').className).toContain(
'@container/keybindings'
)
})
it('emits an accessible label listing all combos', () => {
renderList({
keybindings: [makeKeybinding('A', true), makeKeybinding('B', true, true)]
})
const ariaText = screen.getByTestId('keybinding-list-aria').textContent
expect(ariaText).toContain('Keybindings:')
expect(ariaText).toContain('Ctrl')
expect(ariaText).toContain('A')
expect(ariaText).toContain('Shift')
expect(ariaText).toContain('B')
})
})

View File

@@ -0,0 +1,74 @@
<template>
<span
v-if="keybindings.length > 0"
class="@container/keybindings flex min-w-0 items-center gap-1"
data-testid="keybinding-list"
>
<KeyComboDisplay
:key-combo="keybindings[0].combo"
:is-modified="isModified"
/>
<template v-if="keybindings.length >= 2">
<span
class="hidden text-muted-foreground @[16rem]/keybindings:inline"
aria-hidden="true"
>
,
</span>
<KeyComboDisplay
class="hidden @[16rem]/keybindings:inline-flex"
:key-combo="keybindings[1].combo"
:is-modified="isModified"
/>
</template>
<span
v-if="keybindings.length > 2"
class="hidden rounded-sm px-1.5 py-0.5 text-xs text-muted-foreground @[16rem]/keybindings:inline"
data-testid="keybinding-list-more-wide"
>
{{ $t('g.nMoreKeybindings', { count: keybindings.length - 2 }) }}
</span>
<span
v-if="keybindings.length >= 2"
class="hidden rounded-sm px-1.5 py-0.5 text-xs text-muted-foreground @[12rem]/keybindings:inline @[16rem]/keybindings:hidden"
data-testid="keybinding-list-more-medium"
>
{{ $t('g.nMoreKeybindings', { count: keybindings.length - 1 }) }}
</span>
<span
v-if="keybindings.length >= 2"
class="hidden rounded-sm px-1.5 py-0.5 text-xs text-muted-foreground @[8rem]/keybindings:inline @[12rem]/keybindings:hidden"
data-testid="keybinding-list-more-compact"
>
{{ $t('g.nMoreKeybindingsCompact', { count: keybindings.length - 1 }) }}
</span>
<span class="sr-only" data-testid="keybinding-list-aria">
{{ ariaLabel }}
</span>
</span>
<span v-else>-</span>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import type { KeybindingImpl } from '@/platform/keybindings/keybinding'
import KeyComboDisplay from './KeyComboDisplay.vue'
const { keybindings, isModified = false } = defineProps<{
keybindings: KeybindingImpl[]
isModified?: boolean
}>()
const { t } = useI18n()
const ariaLabel = computed(() => {
if (keybindings.length === 0) return ''
const combos = keybindings
.map((binding) => binding.combo.toString())
.join(', ')
return t('g.keybindingListAriaLabel', { combos })
})
</script>

View File

@@ -83,6 +83,8 @@
"resetToDefault": "Reset to default",
"removeKeybinding": "Remove keybinding",
"nMoreKeybindings": "+ {count} more",
"nMoreKeybindingsCompact": "+ {count}",
"keybindingListAriaLabel": "Keybindings: {combos}",
"customizeFolder": "Customize Folder",
"icon": "Icon",
"color": "Color",