From e871e843655617ae75eb5bb688b42b331f1660e2 Mon Sep 17 00:00:00 2001 From: Andrew Goldstein Date: Thu, 8 Apr 2021 12:50:31 -0600 Subject: [PATCH] [Security Solution] Fixes the `Read Less` button rendering below the fold (#96524) ## [Security Solution] Fixes the `Read Less` button rendering below the fold Fixes issue where the `Read Less` button in the Event Details flyout is rendered below the fold when an event's `message` field is too large, per the `Before` and `After` screenshots below: ### Before ![before](https://user-images.githubusercontent.com/4459398/113962310-aa463e80-97e4-11eb-93f9-f4a90bd250f6.png) _Before: The `Read Less` button is not visible in the `Event details` flyout above_ ### After ![after](https://user-images.githubusercontent.com/4459398/113962433-e9748f80-97e4-11eb-8f46-835eb12ea09d.png) _After: The `Read Less` button is visible in the `Event details` flyout above_ In the _After_ screenshot above, the long `message` is rendered in a vertically scrollable view that occupies ~ one third of the vertical height of the viewport. The `Read Less` button is visible below the message. ### Desk Testing Desk tested on a 16" MBP, and at larger desktop resolutions in: - Chrome `89.0.4389.114` - Firefox `87.0` - Safari `14.0.3` --- .../components/line_clamp/index.test.tsx | 163 ++++++++++++++++++ .../common/components/line_clamp/index.tsx | 19 +- 2 files changed, 178 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/line_clamp/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/line_clamp/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/line_clamp/index.test.tsx new file mode 100644 index 0000000000000..a1547940765c9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/line_clamp/index.test.tsx @@ -0,0 +1,163 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mount } from 'enzyme'; +import { repeat } from 'lodash/fp'; +import React from 'react'; + +import { LineClamp } from '.'; + +describe('LineClamp', () => { + const message = repeat(1000, 'abcdefghij '); // 10 characters, with a trailing space + + describe('no overflow', () => { + test('it does NOT render the expanded line clamp when isOverflow is falsy', () => { + const wrapper = mount(); + + expect(wrapper.find('[data-test-subj="expanded-line-clamp"]').exists()).toBe(false); + }); + + test('it does NOT render the styled line clamp expanded when isOverflow is falsy', () => { + const wrapper = mount(); + + expect(wrapper.find('[data-test-subj="styled-line-clamp"]').exists()).toBe(false); + }); + + test('it renders the default line clamp when isOverflow is falsy', () => { + const wrapper = mount(); + + expect(wrapper.find('[data-test-subj="default-line-clamp"]').first().text()).toBe(message); + }); + + test('it does NOT render the `Read More` button when isOverflow is falsy', () => { + const wrapper = mount(); + + expect(wrapper.find('[data-test-subj="summary-view-readmore"]').exists()).toBe(false); + }); + }); + + describe('overflow', () => { + const clientHeight = 400; + const scrollHeight = clientHeight + 100; // scrollHeight is > clientHeight + + beforeAll(() => { + Object.defineProperty(HTMLElement.prototype, 'clientHeight', { + configurable: true, + value: clientHeight, + }); + + Object.defineProperty(HTMLElement.prototype, 'scrollHeight', { + configurable: true, + value: scrollHeight, + }); + }); + + test('it does NOT render the expanded line clamp by default when isOverflow is true', () => { + const wrapper = mount(); + + expect(wrapper.find('[data-test-subj="expanded-line-clamp"]').exists()).toBe(false); + }); + + test('it renders the styled line clamp when isOverflow is true', () => { + const wrapper = mount(); + + expect(wrapper.find('[data-test-subj="styled-line-clamp"]').first().text()).toBe(message); + }); + + test('it does NOT render the default line clamp when isOverflow is true', () => { + const wrapper = mount(); + + expect(wrapper.find('[data-test-subj="default-line-clamp"]').exists()).toBe(false); + }); + + test('it renders the `Read More` button with the expected (default) text when isOverflow is true', () => { + const wrapper = mount(); + + expect(wrapper.find('[data-test-subj="summary-view-readmore"]').first().text()).toBe( + 'Read More' + ); + }); + + describe('clicking the Read More button', () => { + test('it displays the `Read Less` button text after the user clicks the `Read More` button when isOverflow is true', () => { + const wrapper = mount(); + + wrapper.find('[data-test-subj="summary-view-readmore"]').first().simulate('click'); + wrapper.update(); + + expect(wrapper.find('[data-test-subj="summary-view-readmore"]').first().text()).toBe( + 'Read Less' + ); + }); + + test('it renders the expanded content after the user clicks the `Read More` button when isOverflow is true', () => { + const wrapper = mount(); + + wrapper.find('[data-test-subj="summary-view-readmore"]').first().simulate('click'); + wrapper.update(); + + expect(wrapper.find('[data-test-subj="expanded-line-clamp"]').first().text()).toBe(message); + }); + }); + + test('it renders the expanded content with a max-height of one third the view height when isOverflow is true', () => { + const wrapper = mount(); + + wrapper.find('[data-test-subj="summary-view-readmore"]').first().simulate('click'); + wrapper.update(); + + expect(wrapper.find('[data-test-subj="expanded-line-clamp"]').first()).toHaveStyleRule( + 'max-height', + '33vh' + ); + }); + + test('it automatically vertically scrolls the content when isOverflow is true', () => { + const wrapper = mount(); + + wrapper.find('[data-test-subj="summary-view-readmore"]').first().simulate('click'); + wrapper.update(); + + expect(wrapper.find('[data-test-subj="expanded-line-clamp"]').first()).toHaveStyleRule( + 'overflow-y', + 'auto' + ); + }); + + test('it does NOT render the styled line clamp after the user clicks the `Read More` button when isOverflow is true', () => { + const wrapper = mount(); + + wrapper.find('[data-test-subj="summary-view-readmore"]').first().simulate('click'); + wrapper.update(); + + expect(wrapper.find('[data-test-subj="styled-line-clamp"]').exists()).toBe(false); + }); + + test('it does NOT render the default line clamp after the user clicks the `Read More` button when isOverflow is true', () => { + const wrapper = mount(); + + wrapper.find('[data-test-subj="summary-view-readmore"]').first().simulate('click'); + wrapper.update(); + + expect(wrapper.find('[data-test-subj="default-line-clamp"]').exists()).toBe(false); + }); + + test('it once again displays the `Read More` button text after the user clicks the `Read Less` when isOverflow is true', () => { + const wrapper = mount(); + + wrapper.find('[data-test-subj="summary-view-readmore"]').first().simulate('click'); + wrapper.update(); // 1st toggle + + wrapper.find('[data-test-subj="summary-view-readmore"]').first().simulate('click'); + wrapper.update(); // 2nd toggle + + expect(wrapper.find('[data-test-subj="summary-view-readmore"]').first().text()).toBe( + 'Read More' // after the 2nd toggle, the button once-again says `Read More` + ); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/line_clamp/index.tsx b/x-pack/plugins/security_solution/public/common/components/line_clamp/index.tsx index d3bc4c2d50f98..896b0ec5fd8df 100644 --- a/x-pack/plugins/security_solution/public/common/components/line_clamp/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/line_clamp/index.tsx @@ -11,7 +11,7 @@ import styled from 'styled-components'; import * as i18n from './translations'; const LINE_CLAMP = 3; -const LINE_CLAMP_HEIGHT = 4.5; +const LINE_CLAMP_HEIGHT = 5.5; const StyledLineClamp = styled.div` display: -webkit-box; @@ -28,6 +28,13 @@ const ReadMore = styled(EuiButtonEmpty)` } `; +const ExpandedContent = styled.div` + max-height: 33vh; + overflow-wrap: break-word; + overflow-x: hidden; + overflow-y: auto; +`; + const LineClampComponent: React.FC<{ content?: string | null }> = ({ content }) => { const [isOverflow, setIsOverflow] = useState(null); const [isExpanded, setIsExpanded] = useState(null); @@ -60,11 +67,15 @@ const LineClampComponent: React.FC<{ content?: string | null }> = ({ content }) return ( <> {isExpanded ? ( -

{content}

+ +

{content}

+
) : isOverflow == null || isOverflow === true ? ( - {content} + + {content} + ) : ( - {content} + {content} )} {isOverflow && (