Skip to content

Commit 78055f3

Browse files
committed
Avoid annotation buttons from overflowing the card
1 parent c860138 commit 78055f3

File tree

4 files changed

+139
-2
lines changed

4 files changed

+139
-2
lines changed

src/sidebar/components/Annotation/AnnotationPublishControl.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ import {
77
MenuExpandIcon,
88
} from '@hypothesis/frontend-shared';
99
import classnames from 'classnames';
10+
import { useRef } from 'preact/hooks';
1011

1112
import type { Group } from '../../../types/api';
1213
import type { SidebarSettings } from '../../../types/config';
1314
import { applyTheme } from '../../helpers/theme';
15+
import { useIsContentTruncated } from '../../hooks/use-is-content-truncated';
1416
import { withServices } from '../../service-context';
1517
import Menu from '../Menu';
1618
import MenuItem from '../MenuItem';
@@ -71,22 +73,31 @@ function AnnotationPublishControl({
7173
</div>
7274
);
7375

76+
const postButtonText = `Post to ${isPrivate ? 'Only Me' : group.name}`;
77+
const postButtonRef = useRef<HTMLButtonElement | null>(null);
78+
const isButtonContentTruncated = useIsContentTruncated(postButtonRef);
79+
7480
return (
7581
<div className="flex flex-row gap-x-3">
76-
<div className="flex relative">
82+
<div className="flex relative max-w-full min-w-0">
7783
<Button
7884
classes={classnames(
7985
// Turn off right-side border radius to align with menu-open button
8086
'rounded-r-none',
87+
// Truncate text in this button. It also requires overwriting its
88+
// `display` property from flex to block
89+
'truncate !block',
8190
)}
8291
data-testid="publish-control-button"
8392
style={buttonStyle}
8493
onClick={onSave}
8594
disabled={isDisabled}
8695
size="lg"
8796
variant="primary"
97+
title={isButtonContentTruncated ? postButtonText : undefined}
98+
elementRef={postButtonRef}
8899
>
89-
Post to {isPrivate ? 'Only Me' : group.name}
100+
{postButtonText}
90101
</Button>
91102
{/* This wrapper div is necessary because of peculiarities with
92103
Safari: see https://github.com/hypothesis/client/issues/2302 */}

src/sidebar/components/Annotation/test/AnnotationPublishControl-test.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
mockImportedComponents,
99
} from '@hypothesis/frontend-testing';
1010
import { mount } from '@hypothesis/frontend-testing';
11+
import sinon from 'sinon';
1112

1213
import AnnotationPublishControl, {
1314
$imports,
@@ -17,6 +18,7 @@ describe('AnnotationPublishControl', () => {
1718
let fakeGroup;
1819
let fakeSettings;
1920
let fakeApplyTheme;
21+
let fakeUseIsContentTruncated;
2022

2123
let fakeOnSave;
2224
let fakeOnCancel;
@@ -54,12 +56,16 @@ describe('AnnotationPublishControl', () => {
5456
};
5557

5658
fakeApplyTheme = sinon.stub();
59+
fakeUseIsContentTruncated = sinon.stub().returns(false);
5760

5861
$imports.$mock(mockImportedComponents());
5962
$imports.$mock({
6063
'../../helpers/theme': {
6164
applyTheme: fakeApplyTheme,
6265
},
66+
'../../hooks/use-is-content-truncated': {
67+
useIsContentTruncated: fakeUseIsContentTruncated,
68+
},
6369
});
6470
});
6571

@@ -202,6 +208,16 @@ describe('AnnotationPublishControl', () => {
202208
});
203209
});
204210

211+
[true, false].forEach(isTruncated => {
212+
it('adds title to publish button when its content is truncated', () => {
213+
fakeUseIsContentTruncated.returns(isTruncated);
214+
215+
const wrapper = createAnnotationPublishControl();
216+
217+
assert.equal(!!getPublishButton(wrapper).prop('title'), isTruncated);
218+
});
219+
});
220+
205221
it(
206222
'should pass a11y checks',
207223
checkAccessibility({
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { mount } from '@hypothesis/frontend-testing';
2+
import { useRef } from 'preact/hooks';
3+
4+
import { useIsContentTruncated } from '../use-is-content-truncated';
5+
6+
describe('useIsContentTruncated', () => {
7+
function FakeComponent({ children }) {
8+
const truncatedElementRef = useRef(null);
9+
const isTruncated = useIsContentTruncated(truncatedElementRef);
10+
11+
return (
12+
<div data-testid="container">
13+
<div
14+
style={{ width: '100px', maxWidth: '100%' }}
15+
className="truncate"
16+
ref={truncatedElementRef}
17+
>
18+
{children}
19+
</div>
20+
<div data-testid="is-truncated">{isTruncated ? 'Yes' : 'No'}</div>
21+
</div>
22+
);
23+
}
24+
25+
function createComponent(content) {
26+
return mount(<FakeComponent>{content}</FakeComponent>, { connected: true });
27+
}
28+
29+
function isTruncated(wrapper) {
30+
return wrapper.find('[data-testid="is-truncated"]').text() === 'Yes';
31+
}
32+
33+
function waitForResize(element) {
34+
return new Promise(resolve => {
35+
const observer = new ResizeObserver(() => {
36+
observer.disconnect();
37+
resolve();
38+
});
39+
observer.observe(element);
40+
});
41+
}
42+
43+
[
44+
{
45+
content: 'foo',
46+
shouldBeTruncated: false,
47+
},
48+
{
49+
content: 'foo'.repeat(1000),
50+
shouldBeTruncated: true,
51+
},
52+
].forEach(({ content, shouldBeTruncated }) => {
53+
it('checks if content is truncated once mounted', () => {
54+
const wrapper = createComponent(content);
55+
assert.equal(isTruncated(wrapper), shouldBeTruncated);
56+
});
57+
});
58+
59+
it('checks if content is truncated when resized', async () => {
60+
const wrapper = createComponent('some content');
61+
62+
// Content is initially not truncated
63+
assert.isFalse(isTruncated(wrapper));
64+
65+
// When we resize the container to make it smaller, the content gets
66+
// truncated
67+
const container = wrapper.find('[data-testid="container"]').getDOMNode();
68+
const resizePromise = waitForResize(container);
69+
container.style.width = '10px';
70+
71+
await resizePromise;
72+
wrapper.update();
73+
74+
assert.isTrue(isTruncated(wrapper));
75+
});
76+
});
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type { RefObject } from 'preact';
2+
import { useEffect, useState } from 'preact/hooks';
3+
4+
/**
5+
* Determines if the content of an element is truncated.
6+
*
7+
* Useful to use with elements with hidden overflow and ellipsis text-overflow,
8+
* to know if the content is truncated and the ellipsis is being shown.
9+
*/
10+
export function useIsContentTruncated(
11+
elementRef: RefObject<HTMLElement | null>,
12+
) {
13+
const [isTruncated, setIsTruncated] = useState<boolean>(false);
14+
useEffect(() => {
15+
const buttonEl = elementRef.current;
16+
/* istanbul ignore next */
17+
if (!buttonEl) {
18+
return () => {};
19+
}
20+
21+
const isTruncated = () =>
22+
setIsTruncated(buttonEl.scrollWidth > buttonEl.clientWidth);
23+
24+
// Check immediately if the element's content is truncated, and then again
25+
// every time it is resized
26+
isTruncated();
27+
const observer = new ResizeObserver(isTruncated);
28+
observer.observe(buttonEl);
29+
30+
return () => observer.disconnect();
31+
}, [elementRef]);
32+
33+
return isTruncated;
34+
}

0 commit comments

Comments
 (0)