Skip to content
代码片段 群组 项目
未验证 提交 f669b3a6 编辑于 作者: Nick Leonard's avatar Nick Leonard 提交者: GitLab
浏览文件

Add shortcut tooltips

Adds tooltips with keys for
work item fields with shortcuts.

Changelog: added
上级 b2a7ebcc
No related branches found
No related tags found
2 合并请求!3031Merge per-main-jh to main-jh by luzhiyuan,!3030Merge per-main-jh to main-jh
显示
127 个添加1 个删除
<script> <script>
import { GlButton, GlForm, GlLoadingIcon, GlCollapsibleListbox } from '@gitlab/ui'; import {
GlButton,
GlForm,
GlLoadingIcon,
GlCollapsibleListbox,
GlTooltipDirective,
} from '@gitlab/ui';
import { isEmpty, debounce } from 'lodash'; import { isEmpty, debounce } from 'lodash';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { sanitize } from '~/lib/dompurify';
import { shouldDisableShortcuts } from '~/behaviors/shortcuts/shortcuts_toggle';
import { keysFor } from '~/behaviors/shortcuts/keybindings';
import { s__, __, sprintf } from '~/locale'; import { s__, __, sprintf } from '~/locale';
...@@ -19,6 +28,9 @@ export default { ...@@ -19,6 +28,9 @@ export default {
GlForm, GlForm,
GlCollapsibleListbox, GlCollapsibleListbox,
}, },
directives: {
GlTooltip: GlTooltipDirective,
},
props: { props: {
canUpdate: { canUpdate: {
type: Boolean, type: Boolean,
...@@ -108,6 +120,11 @@ export default { ...@@ -108,6 +120,11 @@ export default {
required: false, required: false,
default: __('Search'), default: __('Search'),
}, },
shortcut: {
type: Object,
required: false,
default: () => ({}),
},
}, },
data() { data() {
return { return {
...@@ -131,6 +148,22 @@ export default { ...@@ -131,6 +148,22 @@ export default {
? sprintf(__(`No %{label}`), { label: this.dropdownLabel.toLowerCase() }) ? sprintf(__(`No %{label}`), { label: this.dropdownLabel.toLowerCase() })
: this.toggleDropdownText; : this.toggleDropdownText;
}, },
disableShortcuts() {
return shouldDisableShortcuts() || Object.keys(this.shortcut).length === 0;
},
shortcutDescription() {
return this.disableShortcuts ? null : this.shortcut.description;
},
shortcutKey() {
return this.disableShortcuts ? null : keysFor(this.shortcut)[0];
},
tooltipText() {
const description = this.shortcutDescription;
const key = this.shortcutKey;
return this.disableShortcuts
? null
: sanitize(`${description} <kbd class="flat gl-ml-1" aria-hidden=true>${key}</kbd>`);
},
}, },
watch: { watch: {
itemValue: { itemValue: {
...@@ -198,6 +231,8 @@ export default { ...@@ -198,6 +231,8 @@ export default {
<gl-loading-icon v-if="updateInProgress" /> <gl-loading-icon v-if="updateInProgress" />
<gl-button <gl-button
v-if="canUpdate && !isEditing" v-if="canUpdate && !isEditing"
v-gl-tooltip.viewport.html
:title="tooltipText"
data-testid="edit-button" data-testid="edit-button"
category="tertiary" category="tertiary"
size="small" size="small"
......
...@@ -10,6 +10,7 @@ import UncollapsedAssigneeList from '~/sidebar/components/assignees/uncollapsed_ ...@@ -10,6 +10,7 @@ import UncollapsedAssigneeList from '~/sidebar/components/assignees/uncollapsed_
import WorkItemSidebarDropdownWidget from '~/work_items/components/shared/work_item_sidebar_dropdown_widget.vue'; import WorkItemSidebarDropdownWidget from '~/work_items/components/shared/work_item_sidebar_dropdown_widget.vue';
import { s__, sprintf, __ } from '~/locale'; import { s__, sprintf, __ } from '~/locale';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import { ISSUE_MR_CHANGE_ASSIGNEE } from '~/behaviors/shortcuts/keybindings';
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql'; import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
import updateNewWorkItemMutation from '../graphql/update_new_work_item.mutation.graphql'; import updateNewWorkItemMutation from '../graphql/update_new_work_item.mutation.graphql';
import { i18n, TRACKING_CATEGORY_SHOW } from '../constants'; import { i18n, TRACKING_CATEGORY_SHOW } from '../constants';
...@@ -79,6 +80,7 @@ export default { ...@@ -79,6 +80,7 @@ export default {
currentUser: null, currentUser: null,
updateInProgress: false, updateInProgress: false,
localUsers: [], localUsers: [],
shortcut: ISSUE_MR_CHANGE_ASSIGNEE,
}; };
}, },
apollo: { apollo: {
...@@ -337,6 +339,7 @@ export default { ...@@ -337,6 +339,7 @@ export default {
:header-text="headerText" :header-text="headerText"
:update-in-progress="updateInProgress" :update-in-progress="updateInProgress"
:reset-button-label="__('Clear')" :reset-button-label="__('Clear')"
:shortcut="shortcut"
clear-search-on-item-select clear-search-on-item-select
data-testid="work-item-assignees" data-testid="work-item-assignees"
@dropdownShown="onDropdownShown" @dropdownShown="onDropdownShown"
......
...@@ -18,6 +18,9 @@ import { TYPENAME_GROUP } from '~/graphql_shared/constants'; ...@@ -18,6 +18,9 @@ import { TYPENAME_GROUP } from '~/graphql_shared/constants';
import { isLoggedIn } from '~/lib/utils/common_utils'; import { isLoggedIn } from '~/lib/utils/common_utils';
import { WORKSPACE_PROJECT } from '~/issues/constants'; import { WORKSPACE_PROJECT } from '~/issues/constants';
import { addShortcutsExtension } from '~/behaviors/shortcuts'; import { addShortcutsExtension } from '~/behaviors/shortcuts';
import { sanitize } from '~/lib/dompurify';
import { shouldDisableShortcuts } from '~/behaviors/shortcuts/shortcuts_toggle';
import { keysFor, ISSUABLE_EDIT_DESCRIPTION } from '~/behaviors/shortcuts/keybindings';
import ShortcutsWorkItems from '~/behaviors/shortcuts/shortcuts_work_items'; import ShortcutsWorkItems from '~/behaviors/shortcuts/shortcuts_work_items';
import { import {
i18n, i18n,
...@@ -462,6 +465,16 @@ export default { ...@@ -462,6 +465,16 @@ export default {
shouldShowEditButton() { shouldShowEditButton() {
return !this.editMode && this.canUpdate; return !this.editMode && this.canUpdate;
}, },
editShortcutKey() {
return shouldDisableShortcuts() ? null : keysFor(ISSUABLE_EDIT_DESCRIPTION)[0];
},
editTooltip() {
const description = __('Edit title and description');
const key = this.editShortcutKey;
return shouldDisableShortcuts()
? description
: sanitize(`${description} <kbd class="flat gl-ml-1" aria-hidden=true>${key}</kbd>`);
},
modalCloseButtonClass() { modalCloseButtonClass() {
return { return {
'sm:gl-hidden': !this.error, 'sm:gl-hidden': !this.error,
...@@ -933,6 +946,8 @@ export default { ...@@ -933,6 +946,8 @@ export default {
<div class="gl-ml-auto gl-mt-1 gl-flex gl-gap-3 gl-self-start"> <div class="gl-ml-auto gl-mt-1 gl-flex gl-gap-3 gl-self-start">
<gl-button <gl-button
v-if="shouldShowEditButton" v-if="shouldShowEditButton"
v-gl-tooltip.bottom.html
:title="editTooltip"
category="secondary" category="secondary"
data-testid="work-item-edit-form-button" data-testid="work-item-edit-form-button"
class="shortcut-edit-wi-description" class="shortcut-edit-wi-description"
......
...@@ -10,6 +10,7 @@ import groupLabelsQuery from '~/sidebar/components/labels/labels_select_widget/g ...@@ -10,6 +10,7 @@ import groupLabelsQuery from '~/sidebar/components/labels/labels_select_widget/g
import projectLabelsQuery from '~/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql'; import projectLabelsQuery from '~/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql';
import { isScopedLabel } from '~/lib/utils/common_utils'; import { isScopedLabel } from '~/lib/utils/common_utils';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import { ISSUABLE_CHANGE_LABEL } from '~/behaviors/shortcuts/keybindings';
import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql'; import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql'; import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
import updateNewWorkItemMutation from '../graphql/update_new_work_item.mutation.graphql'; import updateNewWorkItemMutation from '../graphql/update_new_work_item.mutation.graphql';
...@@ -72,6 +73,7 @@ export default { ...@@ -72,6 +73,7 @@ export default {
addLabelIds: [], addLabelIds: [],
labelsCache: [], labelsCache: [],
labelsToShowAtTopOfTheListbox: [], labelsToShowAtTopOfTheListbox: [],
shortcut: ISSUABLE_CHANGE_LABEL,
}; };
}, },
computed: { computed: {
...@@ -334,6 +336,7 @@ export default { ...@@ -334,6 +336,7 @@ export default {
:toggle-dropdown-text="dropdownText" :toggle-dropdown-text="dropdownText"
:header-text="__('Select labels')" :header-text="__('Select labels')"
:reset-button-label="__('Clear')" :reset-button-label="__('Clear')"
:shortcut="shortcut"
show-footer show-footer
multi-select multi-select
clear-search-on-item-select clear-search-on-item-select
......
...@@ -7,6 +7,7 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils'; ...@@ -7,6 +7,7 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import { MILESTONE_STATE } from '~/sidebar/constants'; import { MILESTONE_STATE } from '~/sidebar/constants';
import WorkItemSidebarDropdownWidget from '~/work_items/components/shared/work_item_sidebar_dropdown_widget.vue'; import WorkItemSidebarDropdownWidget from '~/work_items/components/shared/work_item_sidebar_dropdown_widget.vue';
import { ISSUE_MR_CHANGE_MILESTONE } from '~/behaviors/shortcuts/keybindings';
import projectMilestonesQuery from '~/sidebar/queries/project_milestones.query.graphql'; import projectMilestonesQuery from '~/sidebar/queries/project_milestones.query.graphql';
import groupMilestonesQuery from '~/sidebar/queries/group_milestones.query.graphql'; import groupMilestonesQuery from '~/sidebar/queries/group_milestones.query.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
...@@ -68,6 +69,7 @@ export default { ...@@ -68,6 +69,7 @@ export default {
updateInProgress: false, updateInProgress: false,
milestones: [], milestones: [],
localMilestone: this.workItemMilestone, localMilestone: this.workItemMilestone,
shortcut: ISSUE_MR_CHANGE_MILESTONE,
}; };
}, },
computed: { computed: {
...@@ -235,6 +237,7 @@ export default { ...@@ -235,6 +237,7 @@ export default {
:toggle-dropdown-text="dropdownText" :toggle-dropdown-text="dropdownText"
:header-text="__('Select milestone')" :header-text="__('Select milestone')"
:reset-button-label="__('Clear')" :reset-button-label="__('Clear')"
:shortcut="shortcut"
data-testid="work-item-milestone" data-testid="work-item-milestone"
@dropdownShown="onDropdownShown" @dropdownShown="onDropdownShown"
@searchStarted="search" @searchStarted="search"
......
...@@ -2,8 +2,13 @@ import { GlForm, GlCollapsibleListbox, GlLoadingIcon } from '@gitlab/ui'; ...@@ -2,8 +2,13 @@ import { GlForm, GlCollapsibleListbox, GlLoadingIcon } from '@gitlab/ui';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper';
import { groupIterationsResponse } from 'jest/work_items/mock_data'; import { groupIterationsResponse } from 'jest/work_items/mock_data';
import { shouldDisableShortcuts } from '~/behaviors/shortcuts/shortcuts_toggle';
import { keysFor } from '~/behaviors/shortcuts/keybindings';
import WorkItemSidebarDropdownWidget from '~/work_items/components/shared/work_item_sidebar_dropdown_widget.vue'; import WorkItemSidebarDropdownWidget from '~/work_items/components/shared/work_item_sidebar_dropdown_widget.vue';
jest.mock('~/behaviors/shortcuts/shortcuts_toggle');
jest.mock('~/behaviors/shortcuts/keybindings');
describe('WorkItemSidebarDropdownWidget component', () => { describe('WorkItemSidebarDropdownWidget component', () => {
let wrapper; let wrapper;
...@@ -28,6 +33,7 @@ describe('WorkItemSidebarDropdownWidget component', () => { ...@@ -28,6 +33,7 @@ describe('WorkItemSidebarDropdownWidget component', () => {
infiniteScrollLoading = false, infiniteScrollLoading = false,
clearSearchOnItemSelect = false, clearSearchOnItemSelect = false,
listItems = [], listItems = [],
shortcut = undefined,
} = {}) => { } = {}) => {
wrapper = mountExtended(WorkItemSidebarDropdownWidget, { wrapper = mountExtended(WorkItemSidebarDropdownWidget, {
propsData: { propsData: {
...@@ -43,6 +49,7 @@ describe('WorkItemSidebarDropdownWidget component', () => { ...@@ -43,6 +49,7 @@ describe('WorkItemSidebarDropdownWidget component', () => {
infiniteScroll, infiniteScroll,
infiniteScrollLoading, infiniteScrollLoading,
clearSearchOnItemSelect, clearSearchOnItemSelect,
shortcut,
}, },
slots, slots,
}); });
...@@ -257,4 +264,47 @@ describe('WorkItemSidebarDropdownWidget component', () => { ...@@ -257,4 +264,47 @@ describe('WorkItemSidebarDropdownWidget component', () => {
}); });
}); });
}); });
describe('shortcut tooltip', () => {
const shortcut = {
description: 'Edit dropdown',
};
beforeEach(() => {
shouldDisableShortcuts.mockReturnValue(false);
keysFor.mockReturnValue(['e']);
});
afterEach(() => {
jest.clearAllMocks();
});
it('shows tooltip with key when shortcut is provided', () => {
createComponent({
canUpdate: true,
shortcut,
});
const expectedTooltip = 'Edit dropdown <kbd aria-hidden="true" class="flat gl-ml-1">e</kbd>';
expect(findEditButton().attributes('title')).toContain(expectedTooltip);
});
it('does not show tooltip when shortcut is not provided', () => {
createComponent({
canUpdate: true,
});
expect(findEditButton().attributes('title')).toBeUndefined();
});
it('does not show tooltip when shortcuts are disabled', () => {
shouldDisableShortcuts.mockReturnValue(true);
createComponent({
canUpdate: true,
shortcut,
});
expect(findEditButton().attributes('title')).toBeUndefined();
});
});
}); });
...@@ -26,6 +26,7 @@ import { ...@@ -26,6 +26,7 @@ import {
workItemResponseFactory, workItemResponseFactory,
} from 'jest/work_items/mock_data'; } from 'jest/work_items/mock_data';
import { i18n, TRACKING_CATEGORY_SHOW, NEW_WORK_ITEM_IID } from '~/work_items/constants'; import { i18n, TRACKING_CATEGORY_SHOW, NEW_WORK_ITEM_IID } from '~/work_items/constants';
import { ISSUE_MR_CHANGE_ASSIGNEE } from '~/behaviors/shortcuts/keybindings';
describe('WorkItemAssignees component', () => { describe('WorkItemAssignees component', () => {
Vue.use(VueApollo); Vue.use(VueApollo);
...@@ -126,6 +127,12 @@ describe('WorkItemAssignees component', () => { ...@@ -126,6 +127,12 @@ describe('WorkItemAssignees component', () => {
expect(findSidebarDropdownWidget().props('dropdownLabel')).toBe('Assignee'); expect(findSidebarDropdownWidget().props('dropdownLabel')).toBe('Assignee');
}); });
it('has key shortcut tooltip', () => {
createComponent();
expect(findSidebarDropdownWidget().props('shortcut')).toBe(ISSUE_MR_CHANGE_ASSIGNEE);
});
describe('Dropdown search', () => { describe('Dropdown search', () => {
it('shows no items in the dropdown when no results matching', async () => { it('shows no items in the dropdown when no results matching', async () => {
createComponent({ searchQueryHandler: successSearchWithNoMatchingUsers }); createComponent({ searchQueryHandler: successSearchWithNoMatchingUsers });
......
...@@ -1141,6 +1141,7 @@ describe('WorkItemDetail component', () => { ...@@ -1141,6 +1141,7 @@ describe('WorkItemDetail component', () => {
it('shows the edit button', () => { it('shows the edit button', () => {
expect(findEditButton().exists()).toBe(true); expect(findEditButton().exists()).toBe(true);
expect(findEditButton().attributes('title')).toContain('Edit title and description');
}); });
it('renders the work item title with edit component', () => { it('renders the work item title with edit component', () => {
......
...@@ -13,6 +13,7 @@ import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutati ...@@ -13,6 +13,7 @@ import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutati
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql'; import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import WorkItemLabels from '~/work_items/components/work_item_labels.vue'; import WorkItemLabels from '~/work_items/components/work_item_labels.vue';
import WorkItemSidebarDropdownWidget from '~/work_items/components/shared/work_item_sidebar_dropdown_widget.vue'; import WorkItemSidebarDropdownWidget from '~/work_items/components/shared/work_item_sidebar_dropdown_widget.vue';
import { ISSUABLE_CHANGE_LABEL } from '~/behaviors/shortcuts/keybindings';
import { import {
projectLabelsResponse, projectLabelsResponse,
groupLabelsResponse, groupLabelsResponse,
...@@ -145,6 +146,7 @@ describe('WorkItemLabels component', () => { ...@@ -145,6 +146,7 @@ describe('WorkItemLabels component', () => {
multiSelect: true, multiSelect: true,
showFooter: true, showFooter: true,
itemValue: [], itemValue: [],
shortcut: ISSUABLE_CHANGE_LABEL,
}); });
expect(findAllLabels()).toHaveLength(0); expect(findAllLabels()).toHaveLength(0);
}); });
......
...@@ -10,6 +10,7 @@ import waitForPromises from 'helpers/wait_for_promises'; ...@@ -10,6 +10,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants'; import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import projectMilestonesQuery from '~/sidebar/queries/project_milestones.query.graphql'; import projectMilestonesQuery from '~/sidebar/queries/project_milestones.query.graphql';
import { ISSUE_MR_CHANGE_MILESTONE } from '~/behaviors/shortcuts/keybindings';
import { import {
projectMilestonesResponse, projectMilestonesResponse,
projectMilestonesResponseWithNoMilestones, projectMilestonesResponseWithNoMilestones,
...@@ -68,6 +69,12 @@ describe('WorkItemMilestone component', () => { ...@@ -68,6 +69,12 @@ describe('WorkItemMilestone component', () => {
expect(findSidebarDropdownWidget().props('dropdownLabel')).toBe('Milestone'); expect(findSidebarDropdownWidget().props('dropdownLabel')).toBe('Milestone');
}); });
it('has key shortcut tooltip', () => {
createComponent();
expect(findSidebarDropdownWidget().props('shortcut')).toBe(ISSUE_MR_CHANGE_MILESTONE);
});
describe('Default text with canUpdate false and milestone value', () => { describe('Default text with canUpdate false and milestone value', () => {
describe.each` describe.each`
description | milestone | value description | milestone | value
......
0% 加载中 .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册