Commit 842e1d4f authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'nfriend-allow-release-page-anchor-scrolling' into 'master'

Allow release blocks to be scrolled to using URL anchor tags

See merge request gitlab-org/gitlab!17150
parents ac226c69 dc55a4d5
...@@ -6,6 +6,9 @@ import Icon from '~/vue_shared/components/icon.vue'; ...@@ -6,6 +6,9 @@ import Icon from '~/vue_shared/components/icon.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago'; import timeagoMixin from '~/vue_shared/mixins/timeago';
import { __, n__, sprintf } from '../../locale'; import { __, n__, sprintf } from '../../locale';
import { slugify } from '~/lib/utils/text_utility';
import { getLocationHash } from '~/lib/utils/url_utility';
import { scrollToElement } from '~/lib/utils/common_utils';
export default { export default {
name: 'ReleaseBlock', name: 'ReleaseBlock',
...@@ -26,7 +29,15 @@ export default { ...@@ -26,7 +29,15 @@ export default {
default: () => ({}), default: () => ({}),
}, },
}, },
data() {
return {
isHighlighted: false,
};
},
computed: { computed: {
id() {
return slugify(this.release.tag_name);
},
releasedTimeAgo() { releasedTimeAgo() {
return sprintf(__('released %{time}'), { return sprintf(__('released %{time}'), {
time: this.timeFormated(this.release.released_at), time: this.timeFormated(this.release.released_at),
...@@ -62,10 +73,21 @@ export default { ...@@ -62,10 +73,21 @@ export default {
return n__('Milestone', 'Milestones', this.release.milestones.length); return n__('Milestone', 'Milestones', this.release.milestones.length);
}, },
}, },
mounted() {
const hash = getLocationHash();
if (hash && slugify(hash) === this.id) {
this.isHighlighted = true;
setTimeout(() => {
this.isHighlighted = false;
}, 2000);
scrollToElement(this.$el);
}
},
}; };
</script> </script>
<template> <template>
<div :id="release.tag_name" class="card"> <div :id="id" :class="{ 'bg-line-target-blue': isHighlighted }" class="card release-block">
<div class="card-body"> <div class="card-body">
<h2 class="card-title mt-0"> <h2 class="card-title mt-0">
{{ release.name }} {{ release.name }}
......
.release-block {
transition: background-color 1s linear;
}
...@@ -55,6 +55,10 @@ ...@@ -55,6 +55,10 @@
background-color: $gray-light; background-color: $gray-light;
} }
.bg-line-target-blue {
background: $line-target-blue;
}
.text-break-word { .text-break-word {
word-break: break-all; word-break: break-all;
} }
......
---
title: Allow releases to be targeted by URL anchor links on the Releases page
merge_request: 17150
author:
type: added
...@@ -4,6 +4,18 @@ import timeagoMixin from '~/vue_shared/mixins/timeago'; ...@@ -4,6 +4,18 @@ import timeagoMixin from '~/vue_shared/mixins/timeago';
import { first } from 'underscore'; import { first } from 'underscore';
import { release } from '../mock_data'; import { release } from '../mock_data';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import { scrollToElement } from '~/lib/utils/common_utils';
let mockLocationHash;
jest.mock('~/lib/utils/url_utility', () => ({
__esModule: true,
getLocationHash: jest.fn().mockImplementation(() => mockLocationHash),
}));
jest.mock('~/lib/utils/common_utils', () => ({
__esModule: true,
scrollToElement: jest.fn(),
}));
describe('Release block', () => { describe('Release block', () => {
let wrapper; let wrapper;
...@@ -159,4 +171,61 @@ describe('Release block', () => { ...@@ -159,4 +171,61 @@ describe('Release block', () => {
expect(wrapper.text()).toContain('Upcoming Release'); expect(wrapper.text()).toContain('Upcoming Release');
}); });
it('slugifies the tag_name before setting it as the elements ID', () => {
const releaseClone = JSON.parse(JSON.stringify(release));
releaseClone.tag_name = 'a dangerous tag name <script>alert("hello")</script>';
factory(releaseClone);
expect(wrapper.attributes().id).toBe('a-dangerous-tag-name-script-alert-hello-script-');
});
describe('anchor scrolling', () => {
beforeEach(() => {
scrollToElement.mockClear();
});
const hasTargetBlueBackground = () => wrapper.classes('bg-line-target-blue');
it('does not attempt to scroll the page if no anchor tag is included in the URL', () => {
mockLocationHash = '';
factory(release);
expect(scrollToElement).not.toHaveBeenCalled();
});
it("does not attempt to scroll the page if the anchor tag doesn't match the release's tag name", () => {
mockLocationHash = 'v0.4';
factory(release);
expect(scrollToElement).not.toHaveBeenCalled();
});
it("attempts to scroll itself into view if the anchor tag matches the release's tag name", () => {
mockLocationHash = release.tag_name;
factory(release);
expect(scrollToElement).toHaveBeenCalledTimes(1);
expect(scrollToElement).toHaveBeenCalledWith(wrapper.element);
});
it('renders with a light blue background if it is the target of the anchor', () => {
mockLocationHash = release.tag_name;
factory(release);
return wrapper.vm.$nextTick().then(() => {
expect(hasTargetBlueBackground()).toBe(true);
});
});
it('does not render with a light blue background if it is not the target of the anchor', () => {
mockLocationHash = '';
factory(release);
return wrapper.vm.$nextTick().then(() => {
expect(hasTargetBlueBackground()).toBe(false);
});
});
});
}); });
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