Commit 8f10a7db authored by Samantha Ming's avatar Samantha Ming

Create shared commit message field component

This will make this component sharable so it can be easily
used in the ide and delete blob modal.
parent a06553b4
<script>
import { GlIcon, GlPopover } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import { MAX_TITLE_LENGTH, MAX_BODY_LENGTH } from '../../constants';
export default {
components: {
GlIcon,
GlPopover,
},
props: {
text: {
type: String,
required: true,
},
placeholder: {
type: String,
required: true,
},
},
data() {
return {
scrollTop: 0,
isFocused: false,
};
},
computed: {
allLines() {
return this.text.split('\n').map((line, i) => ({
text: line.substr(0, this.getLineLength(i)) || ' ',
highlightedText: line.substr(this.getLineLength(i)),
}));
},
},
methods: {
handleScroll() {
if (this.$refs.textarea) {
this.$nextTick(() => {
this.scrollTop = this.$refs.textarea.scrollTop;
});
}
},
getLineLength(i) {
return i === 0 ? MAX_TITLE_LENGTH : MAX_BODY_LENGTH;
},
onInput(e) {
this.$emit('input', e.target.value);
},
onCtrlEnter() {
if (!this.isFocused) return;
this.$emit('submit');
},
updateIsFocused(isFocused) {
this.isFocused = isFocused;
},
},
popoverOptions: {
triggers: 'hover',
placement: 'top',
content: sprintf(
__(`
The character highlighter helps you keep the subject line to %{titleLength} characters
and wrap the body at %{bodyLength} so they are readable in git.
`),
{ titleLength: MAX_TITLE_LENGTH, bodyLength: MAX_BODY_LENGTH },
),
},
};
</script>
<template>
<fieldset
class="gl-rounded-base gl-inset-border-1-gray-400 gl-py-4 gl-px-5"
:class="{
'gl-outline-none! gl-focus-ring-border-1-gray-900!': isFocused,
}"
>
<div
v-once
class="gl-display-flex gl-align-items-center gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-pb-3 gl-mb-3"
>
<div>{{ __('Commit Message') }}</div>
<div id="commit-message-popover-container">
<span id="commit-message-question" class="gl-gray-700 gl-ml-3">
<gl-icon name="question" />
</span>
<gl-popover
target="commit-message-question"
container="commit-message-popover-container"
v-bind="$options.popoverOptions"
/>
</div>
</div>
<div class="gl-relative gl-w-full gl-h-13 gl-overflow-hidden">
<div class="gl-absolute gl-z-index-1 gl-font-monospace gl-text-transparent">
<div
data-testid="highlights"
:style="{
transform: `translate3d(0, ${-scrollTop}px, 0)`,
}"
>
<div v-for="(line, index) in allLines" :key="index">
<span
data-testid="highlights-text"
class="gl-white-space-pre-wrap gl-word-break-word"
v-text="line.text"
>
</span
><mark
v-show="line.highlightedText"
data-testid="highlights-mark"
class="gl-px-1 gl-py-0 gl-bg-orange-100 gl-text-transparent gl-white-space-pre-wrap gl-word-break-word"
v-text="line.highlightedText"
>
</mark>
</div>
</div>
</div>
<textarea
ref="textarea"
:placeholder="placeholder"
:value="text"
class="gl-absolute gl-w-full gl-h-full gl-z-index-2 gl-font-monospace p-0 gl-outline-0 gl-bg-transparent gl-border-0"
data-qa-selector="ide_commit_message_field"
dir="auto"
name="commit-message"
@scroll="handleScroll"
@input="onInput"
@focus="updateIsFocused(true)"
@blur="updateIsFocused(false)"
@keydown.ctrl.enter="onCtrlEnter"
@keydown.meta.enter="onCtrlEnter"
>
</textarea>
</div>
</fieldset>
</template>
......@@ -281,3 +281,12 @@ $gl-line-height-42: px-to-rem(42px);
display: none;
}
}
// Will both be moved to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1465
.gl-text-transparent {
color: transparent;
}
.gl-focus-ring-border-1-gray-900\! {
@include gl-focus($gl-border-size-1, $gray-900, true);
}
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import CommitMessageField from '~/ide/components/shared/commit_message_field.vue';
const DEFAULT_PROPS = {
text: 'foo text',
placeholder: 'foo placeholder',
};
describe('CommitMessageField', () => {
let wrapper;
const createComponent = (props = {}) => {
wrapper = extendedWrapper(
shallowMount(CommitMessageField, {
propsData: {
...DEFAULT_PROPS,
...props,
},
attachTo: document.body,
}),
);
};
afterEach(() => {
wrapper.destroy();
});
const findTextArea = () => wrapper.find('textarea');
const findHighlights = () => wrapper.findByTestId('highlights');
const findHighlightsText = () => wrapper.findByTestId('highlights-text');
const findHighlightsMark = () => wrapper.findByTestId('highlights-mark');
const findHighlightsTexts = () => wrapper.findAllByTestId('highlights-text');
const findHighlightsMarks = () => wrapper.findAllByTestId('highlights-mark');
const fillText = async (text) => {
wrapper.setProps({ text });
await nextTick();
};
it('emits input event on input', () => {
const value = 'foo';
createComponent();
findTextArea().setValue(value);
expect(wrapper.emitted('input')[0][0]).toEqual(value);
});
describe('focus classes', () => {
beforeEach(async () => {
createComponent();
findTextArea().trigger('focus');
await nextTick();
});
it('is added on textarea focus', async () => {
expect(wrapper.attributes('class')).toEqual(
expect.stringContaining('gl-outline-none! gl-focus-ring-border-1-gray-900!'),
);
});
it('is removed on textarea blur', async () => {
findTextArea().trigger('blur');
await nextTick();
expect(wrapper.attributes('class')).toEqual(
expect.not.stringContaining('gl-outline-none! gl-focus-ring-border-1-gray-900!'),
);
});
});
describe('highlights', () => {
describe('subject line', () => {
it('does not highlight less than 50 characters', async () => {
const text = 'text less than 50 chars';
createComponent();
await fillText(text);
expect(findHighlightsText().text()).toEqual(text);
expect(findHighlightsMark().text()).toBeFalsy();
});
it('highlights characters over 50 length', async () => {
const text =
'text less than 50 chars that should not highlighted. text more than 50 should be highlighted';
createComponent();
await fillText(text);
expect(findHighlightsText().text()).toEqual(text.slice(0, 50));
expect(findHighlightsMark().text()).toEqual(text.slice(50));
});
});
describe('body text', () => {
it('does not highlight body text less tan 72 characters', async () => {
const text = 'subject line\nbody content';
createComponent();
await fillText(text);
expect(findHighlightsTexts()).toHaveLength(2);
expect(findHighlightsMarks().at(1).attributes('style')).toEqual('display: none;');
});
it('highlights body text more than 72 characters', async () => {
const text =
'subject line\nbody content that will be highlighted when it is more than 72 characters in length';
createComponent();
await fillText(text);
expect(findHighlightsTexts()).toHaveLength(2);
expect(findHighlightsMarks().at(1).attributes('style')).not.toEqual('display: none;');
expect(findHighlightsMarks().at(1).element.textContent).toEqual(' in length');
});
it('highlights body text & subject line', async () => {
const text =
'text less than 50 chars that should not highlighted\nbody content that will be highlighted when it is more than 72 characters in length';
createComponent();
await fillText(text);
expect(findHighlightsTexts()).toHaveLength(2);
expect(findHighlightsMarks()).toHaveLength(2);
expect(findHighlightsMarks().at(0).element.textContent).toEqual('d');
expect(findHighlightsMarks().at(1).element.textContent).toEqual(' in length');
});
});
});
describe('scrolling textarea', () => {
it('updates transform of highlights', async () => {
const yCoord = 50;
createComponent();
await fillText('subject line\n\n\n\n\n\n\n\n\n\n\nbody content');
wrapper.vm.$el.querySelector('textarea').scrollTo(0, yCoord);
await nextTick();
expect(wrapper.vm.scrollTop).toEqual(yCoord);
expect(findHighlights().attributes('style')).toEqual('transform: translate3d(0, -50px, 0);');
});
});
});
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