Commit e5232748 authored by Andrew Fontaine's avatar Andrew Fontaine

Merge branch '344429-switch-to-gltabs-for-markdown-header-component' into 'master'

Switch to GlTabs for markdown header component

See merge request gitlab-org/gitlab!80153
parents 45211e88 51c05183
<script> <script>
import { GlPopover, GlButton, GlTooltipDirective } from '@gitlab/ui'; import { GlPopover, GlButton, GlTooltipDirective, GlTabs, GlTab } from '@gitlab/ui';
import $ from 'jquery'; import $ from 'jquery';
import { keysFor, BOLD_TEXT, ITALIC_TEXT, LINK_TEXT } from '~/behaviors/shortcuts/keybindings'; import { keysFor, BOLD_TEXT, ITALIC_TEXT, LINK_TEXT } from '~/behaviors/shortcuts/keybindings';
import { getSelectedFragment } from '~/lib/utils/common_utils'; import { getSelectedFragment } from '~/lib/utils/common_utils';
import { s__ } from '~/locale'; import { s__, __ } from '~/locale';
import { CopyAsGFM } from '../../../behaviors/markdown/copy_as_gfm'; import { CopyAsGFM } from '../../../behaviors/markdown/copy_as_gfm';
import ToolbarButton from './toolbar_button.vue'; import ToolbarButton from './toolbar_button.vue';
...@@ -12,6 +12,8 @@ export default { ...@@ -12,6 +12,8 @@ export default {
ToolbarButton, ToolbarButton,
GlPopover, GlPopover,
GlButton, GlButton,
GlTabs,
GlTab,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
...@@ -144,27 +146,33 @@ export default { ...@@ -144,27 +146,33 @@ export default {
italic: keysFor(ITALIC_TEXT), italic: keysFor(ITALIC_TEXT),
link: keysFor(LINK_TEXT), link: keysFor(LINK_TEXT),
}, },
i18n: {
writeTabTitle: __('Write'),
previewTabTitle: __('Preview'),
},
}; };
</script> </script>
<template> <template>
<div class="md-header"> <div class="md-header">
<ul class="nav-links clearfix"> <gl-tabs content-class="gl-display-none">
<li :class="{ active: !previewMarkdown }" class="md-header-tab"> <gl-tab
<button class="js-write-link" type="button" @click="writeMarkdownTab($event)"> title-link-class="gl-pt-3 gl-px-3 js-md-write-button"
{{ __('Write') }} :title="$options.i18n.writeTabTitle"
</button> :active="!previewMarkdown"
</li> data-testid="write-tab"
<li :class="{ active: previewMarkdown }" class="md-header-tab"> @click="writeMarkdownTab($event)"
<button />
class="js-preview-link js-md-preview-button" <gl-tab
type="button" title-link-class="gl-pt-3 gl-px-3 js-md-preview-button"
:title="$options.i18n.previewTabTitle"
:active="previewMarkdown"
data-testid="preview-tab"
@click="previewMarkdownTab($event)" @click="previewMarkdownTab($event)"
> />
{{ __('Preview') }}
</button> <template v-if="!previewMarkdown" #tabs-end>
</li> <div class="md-header-toolbar gl-ml-auto gl-pb-3 gl-justify-content-center">
<li :class="{ active: !previewMarkdown }" class="md-header-toolbar">
<toolbar-button <toolbar-button
tag="**" tag="**"
:button-title=" :button-title="
...@@ -273,7 +281,8 @@ export default { ...@@ -273,7 +281,8 @@ export default {
:button-title="__('Go full screen')" :button-title="__('Go full screen')"
icon="maximize" icon="maximize"
/> />
</li> </div>
</ul> </template>
</gl-tabs>
</div> </div>
</template> </template>
...@@ -67,6 +67,27 @@ ...@@ -67,6 +67,27 @@
} }
} }
} }
.gl-tabs-nav {
@include media-breakpoint-down(xs) {
.nav-item {
flex: 1;
border-bottom: 1px solid $border-color;
}
.gl-tab-nav-item {
padding-top: $gl-padding-4;
padding-bottom: $gl-padding-8;
}
.md-header-toolbar {
width: 100%;
display: flex;
flex-wrap: wrap;
margin-top: $gl-padding-8;
}
}
}
} }
.md-header-tab { .md-header-tab {
......
...@@ -49,7 +49,7 @@ RSpec.describe 'Update Epic', :js do ...@@ -49,7 +49,7 @@ RSpec.describe 'Update Epic', :js do
fill_in 'issue-description', with: 'New epic description' fill_in 'issue-description', with: 'New epic description'
page.within('.detail-page-description') do page.within('.detail-page-description') do
click_button('Preview') click_link('Preview')
expect(find('.md-preview-holder')).to have_content('New epic description') expect(find('.md-preview-holder')).to have_content('New epic description')
end end
...@@ -63,7 +63,7 @@ RSpec.describe 'Update Epic', :js do ...@@ -63,7 +63,7 @@ RSpec.describe 'Update Epic', :js do
fill_in 'issue-description', with: 'New epic description' fill_in 'issue-description', with: 'New epic description'
page.within('.detail-page-description') do page.within('.detail-page-description') do
click_button('Preview') click_link('Preview')
expect(find('.md-preview-holder')).to have_content('New epic description') expect(find('.md-preview-holder')).to have_content('New epic description')
end end
...@@ -76,7 +76,7 @@ RSpec.describe 'Update Epic', :js do ...@@ -76,7 +76,7 @@ RSpec.describe 'Update Epic', :js do
find('.btn-edit').click find('.btn-edit').click
page.within('.detail-page-description') do page.within('.detail-page-description') do
click_button('Preview') click_link('Preview')
expect(find('.md-preview-holder')).to have_content('New epic description') expect(find('.md-preview-holder')).to have_content('New epic description')
end end
end end
...@@ -122,7 +122,7 @@ RSpec.describe 'Update Epic', :js do ...@@ -122,7 +122,7 @@ RSpec.describe 'Update Epic', :js do
expect(page.find_field("issue-description").value).to have_content('banana_sample') expect(page.find_field("issue-description").value).to have_content('banana_sample')
page.within('.detail-page-description') do page.within('.detail-page-description') do
click_button('Preview') click_link('Preview')
wait_for_requests wait_for_requests
within('.md-preview-holder') do within('.md-preview-holder') do
......
...@@ -28,14 +28,14 @@ RSpec.describe 'Group iterations' do ...@@ -28,14 +28,14 @@ RSpec.describe 'Group iterations' do
it 'renders description preview' do it 'renders description preview' do
description = find(description_selector) description = find(description_selector)
description.native.send_keys('') description.native.send_keys('')
click_button('Preview') click_link('Preview')
preview = find('.js-vue-md-preview') preview = find('.js-vue-md-preview')
expect(preview).to have_content('Nothing to preview.') expect(preview).to have_content('Nothing to preview.')
click_button('Write') click_link('Write')
description.native.send_keys(':+1: Nice') description.native.send_keys(':+1: Nice')
click_button('Preview') click_link('Preview')
expect(preview).to have_css('gl-emoji') expect(preview).to have_css('gl-emoji')
expect(find('#iteration-description', visible: false)).not_to be_visible expect(find('#iteration-description', visible: false)).not_to be_visible
......
...@@ -133,7 +133,7 @@ RSpec.describe 'Merge request > User posts notes', :js do ...@@ -133,7 +133,7 @@ RSpec.describe 'Merge request > User posts notes', :js do
describe 'when previewing a note' do describe 'when previewing a note' do
it 'shows the toolbar buttons when editing a note' do it 'shows the toolbar buttons when editing a note' do
page.within('.js-main-target-form') do page.within('.js-main-target-form') do
expect(page).to have_css('.md-header-toolbar.active') expect(page).to have_css('.md-header-toolbar')
end end
end end
...@@ -141,7 +141,7 @@ RSpec.describe 'Merge request > User posts notes', :js do ...@@ -141,7 +141,7 @@ RSpec.describe 'Merge request > User posts notes', :js do
wait_for_requests wait_for_requests
find('.js-md-preview-button').click find('.js-md-preview-button').click
page.within('.js-main-target-form') do page.within('.js-main-target-form') do
expect(page).not_to have_css('.md-header-toolbar.active') expect(page).not_to have_css('.md-header-toolbar')
end end
end end
end end
......
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import AxiosMockAdapter from 'axios-mock-adapter'; import AxiosMockAdapter from 'axios-mock-adapter';
import $ from 'jquery'; import $ from 'jquery';
import { TEST_HOST, FIXTURES_PATH } from 'spec/test_constants'; import { TEST_HOST, FIXTURES_PATH } from 'spec/test_constants';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import MarkdownField from '~/vue_shared/components/markdown/field.vue'; import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
const markdownPreviewPath = `${TEST_HOST}/preview`; const markdownPreviewPath = `${TEST_HOST}/preview`;
const markdownDocsPath = `${TEST_HOST}/docs`; const markdownDocsPath = `${TEST_HOST}/docs`;
...@@ -12,8 +12,8 @@ const textareaValue = 'testing\n123'; ...@@ -12,8 +12,8 @@ const textareaValue = 'testing\n123';
const uploadsPath = 'test/uploads'; const uploadsPath = 'test/uploads';
function assertMarkdownTabs(isWrite, writeLink, previewLink, wrapper) { function assertMarkdownTabs(isWrite, writeLink, previewLink, wrapper) {
expect(writeLink.element.parentNode.classList.contains('active')).toBe(isWrite); expect(writeLink.element.children[0].classList.contains('active')).toBe(isWrite);
expect(previewLink.element.parentNode.classList.contains('active')).toBe(!isWrite); expect(previewLink.element.children[0].classList.contains('active')).toBe(!isWrite);
expect(wrapper.find('.md-preview-holder').element.style.display).toBe(isWrite ? 'none' : ''); expect(wrapper.find('.md-preview-holder').element.style.display).toBe(isWrite ? 'none' : '');
} }
...@@ -29,14 +29,13 @@ describe('Markdown field component', () => { ...@@ -29,14 +29,13 @@ describe('Markdown field component', () => {
afterEach(() => { afterEach(() => {
subject.destroy(); subject.destroy();
subject = null;
axiosMock.restore(); axiosMock.restore();
}); });
function createSubject(lines = []) { function createSubject(lines = []) {
// We actually mount a wrapper component so that we can force Vue to rerender classes in order to test a regression // We actually mount a wrapper component so that we can force Vue to rerender classes in order to test a regression
// caused by mixing Vanilla JS and Vue. // caused by mixing Vanilla JS and Vue.
subject = mount( subject = mountExtended(
{ {
components: { components: {
MarkdownField, MarkdownField,
...@@ -72,8 +71,8 @@ describe('Markdown field component', () => { ...@@ -72,8 +71,8 @@ describe('Markdown field component', () => {
); );
} }
const getPreviewLink = () => subject.find('.nav-links .js-preview-link'); const getPreviewLink = () => subject.findByTestId('preview-tab');
const getWriteLink = () => subject.find('.nav-links .js-write-link'); const getWriteLink = () => subject.findByTestId('write-tab');
const getMarkdownButton = () => subject.find('.js-md'); const getMarkdownButton = () => subject.find('.js-md');
const getAllMarkdownButtons = () => subject.findAll('.js-md'); const getAllMarkdownButtons = () => subject.findAll('.js-md');
const getVideo = () => subject.find('video'); const getVideo = () => subject.find('video');
...@@ -107,15 +106,15 @@ describe('Markdown field component', () => { ...@@ -107,15 +106,15 @@ describe('Markdown field component', () => {
it('sets preview link as active', async () => { it('sets preview link as active', async () => {
previewLink = getPreviewLink(); previewLink = getPreviewLink();
previewLink.trigger('click'); previewLink.vm.$emit('click', { target: {} });
await nextTick(); await nextTick();
expect(previewLink.element.parentNode.classList.contains('active')).toBeTruthy(); expect(previewLink.element.children[0].classList.contains('active')).toBe(true);
}); });
it('shows preview loading text', async () => { it('shows preview loading text', async () => {
previewLink = getPreviewLink(); previewLink = getPreviewLink();
previewLink.trigger('click'); previewLink.vm.$emit('click', { target: {} });
await nextTick(); await nextTick();
expect(subject.find('.md-preview-holder').element.textContent.trim()).toContain('Loading…'); expect(subject.find('.md-preview-holder').element.textContent.trim()).toContain('Loading…');
...@@ -126,7 +125,7 @@ describe('Markdown field component', () => { ...@@ -126,7 +125,7 @@ describe('Markdown field component', () => {
previewLink = getPreviewLink(); previewLink = getPreviewLink();
previewLink.trigger('click'); previewLink.vm.$emit('click', { target: {} });
await axios.waitFor(markdownPreviewPath); await axios.waitFor(markdownPreviewPath);
expect(subject.find('.md-preview-holder').element.innerHTML).toContain(previewHTML); expect(subject.find('.md-preview-holder').element.innerHTML).toContain(previewHTML);
...@@ -135,7 +134,7 @@ describe('Markdown field component', () => { ...@@ -135,7 +134,7 @@ describe('Markdown field component', () => {
it('calls video.pause() on comment input when isSubmitting is changed to true', async () => { it('calls video.pause() on comment input when isSubmitting is changed to true', async () => {
previewLink = getPreviewLink(); previewLink = getPreviewLink();
previewLink.trigger('click'); previewLink.vm.$emit('click', { target: {} });
await axios.waitFor(markdownPreviewPath); await axios.waitFor(markdownPreviewPath);
const video = getVideo(); const video = getVideo();
...@@ -151,19 +150,19 @@ describe('Markdown field component', () => { ...@@ -151,19 +150,19 @@ describe('Markdown field component', () => {
writeLink = getWriteLink(); writeLink = getWriteLink();
previewLink = getPreviewLink(); previewLink = getPreviewLink();
writeLink.trigger('click'); writeLink.vm.$emit('click', { target: {} });
await nextTick(); await nextTick();
assertMarkdownTabs(true, writeLink, previewLink, subject); assertMarkdownTabs(true, writeLink, previewLink, subject);
writeLink.trigger('click'); writeLink.vm.$emit('click', { target: {} });
await nextTick(); await nextTick();
assertMarkdownTabs(true, writeLink, previewLink, subject); assertMarkdownTabs(true, writeLink, previewLink, subject);
previewLink.trigger('click'); previewLink.vm.$emit('click', { target: {} });
await nextTick(); await nextTick();
assertMarkdownTabs(false, writeLink, previewLink, subject); assertMarkdownTabs(false, writeLink, previewLink, subject);
previewLink.trigger('click'); previewLink.vm.$emit('click', { target: {} });
await nextTick(); await nextTick();
assertMarkdownTabs(false, writeLink, previewLink, subject); assertMarkdownTabs(false, writeLink, previewLink, subject);
......
import { shallowMount } from '@vue/test-utils';
import $ from 'jquery'; import $ from 'jquery';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import { GlTabs } from '@gitlab/ui';
import HeaderComponent from '~/vue_shared/components/markdown/header.vue'; import HeaderComponent from '~/vue_shared/components/markdown/header.vue';
import ToolbarButton from '~/vue_shared/components/markdown/toolbar_button.vue'; import ToolbarButton from '~/vue_shared/components/markdown/toolbar_button.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
describe('Markdown field header component', () => { describe('Markdown field header component', () => {
let wrapper; let wrapper;
const createWrapper = (props) => { const createWrapper = (props) => {
wrapper = shallowMount(HeaderComponent, { wrapper = shallowMountExtended(HeaderComponent, {
propsData: { propsData: {
previewMarkdown: false, previewMarkdown: false,
...props, ...props,
}, },
stubs: { GlTabs },
}); });
}; };
const findWriteTab = () => wrapper.findByTestId('write-tab');
const findPreviewTab = () => wrapper.findByTestId('preview-tab');
const findToolbarButtons = () => wrapper.findAll(ToolbarButton); const findToolbarButtons = () => wrapper.findAll(ToolbarButton);
const findToolbarButtonByProp = (prop, value) => const findToolbarButtonByProp = (prop, value) =>
findToolbarButtons() findToolbarButtons()
...@@ -34,7 +38,6 @@ describe('Markdown field header component', () => { ...@@ -34,7 +38,6 @@ describe('Markdown field header component', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
describe('markdown header buttons', () => { describe('markdown header buttons', () => {
...@@ -75,23 +78,26 @@ describe('Markdown field header component', () => { ...@@ -75,23 +78,26 @@ describe('Markdown field header component', () => {
}); });
}); });
it('renders `write` link as active when previewMarkdown is false', () => { it('activates `write` tab when previewMarkdown is false', () => {
expect(wrapper.find('li:nth-child(1)').classes()).toContain('active'); expect(findWriteTab().attributes('active')).toBe('true');
expect(findPreviewTab().attributes('active')).toBeUndefined();
}); });
it('renders `preview` link as active when previewMarkdown is true', () => { it('activates `preview` tab when previewMarkdown is true', () => {
createWrapper({ previewMarkdown: true }); createWrapper({ previewMarkdown: true });
expect(wrapper.find('li:nth-child(2)').classes()).toContain('active'); expect(findWriteTab().attributes('active')).toBeUndefined();
expect(findPreviewTab().attributes('active')).toBe('true');
}); });
it('emits toggle markdown event when clicking preview', async () => { it('emits toggle markdown event when clicking preview tab', async () => {
wrapper.find('.js-preview-link').trigger('click'); const eventData = { target: {} };
findPreviewTab().vm.$emit('click', eventData);
await nextTick(); await nextTick();
expect(wrapper.emitted('preview-markdown').length).toEqual(1); expect(wrapper.emitted('preview-markdown').length).toEqual(1);
wrapper.find('.js-write-link').trigger('click'); findWriteTab().vm.$emit('click', eventData);
await nextTick(); await nextTick();
expect(wrapper.emitted('write-markdown').length).toEqual(1); expect(wrapper.emitted('write-markdown').length).toEqual(1);
...@@ -109,12 +115,10 @@ describe('Markdown field header component', () => { ...@@ -109,12 +115,10 @@ describe('Markdown field header component', () => {
}); });
it('blurs preview link after click', () => { it('blurs preview link after click', () => {
const link = wrapper.find('li:nth-child(2) button'); const target = { blur: jest.fn() };
jest.spyOn(HTMLElement.prototype, 'blur').mockImplementation(); findPreviewTab().vm.$emit('click', { target });
link.trigger('click'); expect(target.blur).toHaveBeenCalled();
expect(link.element.blur).toHaveBeenCalled();
}); });
it('renders markdown table template', () => { it('renders markdown table template', () => {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment