Commit 0aadd4c2 authored by Phil Hughes's avatar Phil Hughes

Merge branch '337890-create-shared-commit-message-field' into 'master'

Create shared commit message field component

See merge request gitlab-org/gitlab!71522
parents afc6cf79 8f10a7db
<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); ...@@ -281,3 +281,12 @@ $gl-line-height-42: px-to-rem(42px);
display: none; 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