diff --git a/app/assets/javascripts/pages/users/show/index.js b/app/assets/javascripts/pages/users/show/index.js new file mode 100644 index 0000000000000000000000000000000000000000..dbb5e29c689ce16b49f530ce039c180ac2c7e2bb --- /dev/null +++ b/app/assets/javascripts/pages/users/show/index.js @@ -0,0 +1,3 @@ +import initReadMore from '~/read_more'; + +initReadMore(); diff --git a/app/assets/javascripts/read_more.js b/app/assets/javascripts/read_more.js index 692f375bb940bcb2abc7b30c6f74dfec23e94ce4..92ceeec9d99e57a38dc84dfafd1bf21b2eeed032 100644 --- a/app/assets/javascripts/read_more.js +++ b/app/assets/javascripts/read_more.js @@ -16,6 +16,8 @@ * * * + * If data-show-button-height is present it will use it to determine if the button should be shown or not. + * */ export default function initReadMore(triggerSelector = '.js-read-more-trigger') { const triggerEls = document.querySelectorAll(triggerSelector); @@ -29,6 +31,14 @@ export default function initReadMore(triggerSelector = '.js-read-more-trigger') return; } + if ( + Object.hasOwn(targetEl.dataset, 'showButtonHeight') && + Number(targetEl.dataset.showButtonHeight) > targetEl.clientHeight + ) { + triggerEl.remove(); + return; + } + triggerEl.addEventListener( 'click', () => { diff --git a/app/assets/stylesheets/page_bundles/profile.scss b/app/assets/stylesheets/page_bundles/profile.scss index 573f9dbc69b968912042389b84dacc8db8a33782..18f33fde2fccd9d1a7ec124f2989f584d165b57f 100644 --- a/app/assets/stylesheets/page_bundles/profile.scss +++ b/app/assets/stylesheets/page_bundles/profile.scss @@ -78,6 +78,39 @@ max-width: 600px; } +.profile-readme-wrapper { + $height: 22rem; + $scrim: 2rem; + $readme-bar-height: 2.5rem; + + .profile-readme:not(.is-expanded) { + max-height: $height; + + // only appears when size is > $height. + &::after { + content: ''; + display: block; + position: absolute; + left: 0; + right: 0; + top: calc(#{$height} - #{$scrim} - #{$readme-bar-height}); + height: $scrim; + background: linear-gradient(180deg, transparent, $white); + + .gl-dark & { + background: linear-gradient(180deg, transparent, $black); + } + } + } + + // only appears when size is > $height. + .read-more-bar { + bottom: 1px; + left: 1px; + right: 1px; + } +} + .user-calendar-activities { direction: ltr; diff --git a/app/views/users/_follow_user.html.haml b/app/views/users/_follow_user.html.haml index 71f8a462cbf7bf881bb45f4a4ade286e7a9be948..953e74c1f27c1698471f8e9f7229991c3f122869 100644 --- a/app/views/users/_follow_user.html.haml +++ b/app/views/users/_follow_user.html.haml @@ -1,11 +1,9 @@ -- link_classes = "flex-grow-1 gl-display-inline-block" - - if current_user&.following_users_allowed?(@user) - if current_user.following?(@user) - = form_tag user_unfollow_path(@user, :json), class: link_classes do - = render Pajamas::ButtonComponent.new(type: :submit, button_options: { class: 'gl-w-full', data: { track_action: 'click_button', track_label: 'unfollow_from_profile' } }) do + = form_tag user_unfollow_path(@user, :json) do + = render Pajamas::ButtonComponent.new(type: :submit, button_options: { data: { track_action: 'click_button', track_label: 'unfollow_from_profile' } }) do = _('Unfollow') - else - = form_tag user_follow_path(@user, :json), class: link_classes do - = render Pajamas::ButtonComponent.new(variant: :confirm, type: :submit, button_options: { class: 'gl-w-full', data: { testid: 'follow-user-link', track_action: 'click_button', track_label: 'follow_from_profile' } }) do + = form_tag user_follow_path(@user, :json) do + = render Pajamas::ButtonComponent.new(variant: :confirm, type: :submit, button_options: { data: { testid: 'follow-user-link', track_action: 'click_button', track_label: 'follow_from_profile' } }) do = _('Follow') diff --git a/app/views/users/_overview.html.haml b/app/views/users/_overview.html.haml index a1043ead49e00c08333aa292abae5d6c6c30c869..5310752e71e5a6286b421cd03d657658e0b0ac2b 100644 --- a/app/views/users/_overview.html.haml +++ b/app/views/users/_overview.html.haml @@ -1,6 +1,6 @@ - if can?(current_user, :read_cross_project) && @user.user_readme&.rich_viewer .profile-readme-wrapper.gl-relative.gl-overflow-hidden.gl-mb-5 - .profile-readme.read-more-container.gl-relative.justify-content-center.gl-border.gl-rounded-base.gl-py-5.gl-px-6.gl-overflow-hidden + .profile-readme.read-more-container.gl-relative.justify-content-center.gl-border.gl-rounded-base.gl-py-5.gl-px-6.gl-overflow-hidden{ data: { 'show-button-height': 320 } } .gl-display-flex %ol.breadcrumb.gl-breadcrumb-list.gl-mb-4 %li.gl-breadcrumb-item @@ -12,16 +12,10 @@ .gl-ml-auto = link_to _('Edit file'), edit_blob_path(@user.user_project, @user.user_project.default_branch, @user.user_readme.path) = render 'projects/blob/viewer', viewer: @user.user_readme.rich_viewer, load_async: false - -- unless Feature.enabled?(:security_auto_fix) && @user.bot? - - if @user.personal_projects.any? - .projects-block - .gl-display-flex.gl-align-items-baseline - %h4.gl-font-lg.gl-flex-grow-1 - = s_('UserProfile|Personal projects') - = link_to s_('UserProfile|View all'), user_projects_path, class: "hide js-view-all" - .overview-content-list{ data: { href: user_projects_path } } - = gl_loading_icon(size: 'md', css_class: 'loading') + .js-read-more-trigger.read-more-bar.gl-h-8.gl-absolute.gl-bottom-0.gl-z-index-2.gl-bg-white.gl-px-6.gl-rounded-bottom-base + = render Pajamas::ButtonComponent.new(variant: :link, button_options: { class: 'gl-mt-4 gl-ml-n1', 'aria-label': _("Expand Readme") }) do + = sprite_icon('chevron-down', size: 14, css_class: 'gl-mr-1 gl-mb-n1') + = _("Read more") - if can?(current_user, :read_cross_project) .gl-align-self-start @@ -41,3 +35,13 @@ .user-calendar-activities .overview-content-list.user-activity-content.gl-mb-5{ data: { href: user_activity_path, testid: 'user-activity-content' } } = gl_loading_icon(size: 'md', css_class: 'loading') + +- unless Feature.enabled?(:security_auto_fix) && @user.bot? + - if @user.personal_projects.any? + .projects-block + .gl-display-flex.gl-align-items-baseline + %h4.gl-font-lg.gl-flex-grow-1 + = s_('UserProfile|Personal projects') + = link_to s_('UserProfile|View all'), user_projects_path, class: "hide js-view-all" + .overview-content-list{ data: { href: user_projects_path } } + = gl_loading_icon(size: 'md', css_class: 'loading') diff --git a/locale/gitlab.pot b/locale/gitlab.pot index ef57ea577cf6d9eccb61448e93fd1c5d219ddb0b..6fd08fce657103280d28eed56f322091e5502f25 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -19829,6 +19829,9 @@ msgstr "" msgid "Expand AI-generated summary" msgstr "" +msgid "Expand Readme" +msgstr "" + msgid "Expand all" msgstr "" diff --git a/spec/frontend/read_more_spec.js b/spec/frontend/read_more_spec.js index 5f7bd32e23101b47d1fc5b9ddd18f371a4bf8853..ab1b9b713d12a43864874bafb32f9a23a680c1bd 100644 --- a/spec/frontend/read_more_spec.js +++ b/spec/frontend/read_more_spec.js @@ -3,6 +3,7 @@ import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import initReadMore from '~/read_more'; describe('Read more click-to-expand functionality', () => { + const findTarget = () => document.querySelector('.read-more-container'); const findTrigger = () => document.querySelector('.js-read-more-trigger'); afterEach(() => { @@ -15,13 +16,11 @@ describe('Read more click-to-expand functionality', () => { }); it('adds "is-expanded" class to target element', () => { - const target = document.querySelector('.read-more-container'); - const trigger = findTrigger(); initReadMore(); - trigger.click(); + findTrigger().click(); - expect(target.classList.contains('is-expanded')).toEqual(true); + expect(findTarget().classList.contains('is-expanded')).toEqual(true); }); }); @@ -34,8 +33,7 @@ describe('Read more click-to-expand functionality', () => { `); - const trigger = findTrigger(); - const nestedElement = trigger.firstElementChild; + const nestedElement = findTrigger().firstElementChild; initReadMore(); nestedElement.click(); @@ -46,3 +44,41 @@ describe('Read more click-to-expand functionality', () => { }); }); }); + +describe('data-show-button-height defines when to show the read-more button', () => { + const findTrigger = () => document.querySelectorAll('.js-read-more-trigger'); + + afterEach(() => { + resetHTMLFixture(); + }); + + it('if not set shows button all the time', () => { + setHTMLFixture(` +
+

Occaecat voluptate exercitation aliqua et duis eiusmod mollit esse ea laborum amet consectetur officia culpa anim. Fugiat laboris eu irure deserunt excepteur laboris irure quis. Occaecat nostrud irure do officia ea laborum velit sunt. Aliqua incididunt non deserunt proident magna aliqua sunt laborum laborum eiusmod ullamco. Et elit commodo irure. Labore eu nisi proident.

+
+ + `); + + initReadMore(); + + expect(findTrigger().length).toBe(1); + }); + + it('if set hides button as threshold is met', () => { + setHTMLFixture(` +
+

Occaecat voluptate exercitation aliqua et duis eiusmod mollit esse ea laborum amet consectetur officia culpa anim. Fugiat laboris eu irure deserunt excepteur laboris irure quis. Occaecat nostrud irure do officia ea laborum velit sunt. Aliqua incididunt non deserunt proident magna aliqua sunt laborum laborum eiusmod ullamco. Et elit commodo irure. Labore eu nisi proident.

+
+ + `); + + initReadMore(); + + expect(findTrigger().length).toBe(0); + }); +});