Skip to content

Commit 2b492b0

Browse files
[11.8] [Bugfix] Delay in opening the Tools header in Safari (#11836)
(r11.8 → 11.8)
1 parent 4c74e5f commit 2b492b0

File tree

3 files changed

+290
-9
lines changed

3 files changed

+290
-9
lines changed
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
import App from 'components/App';
2+
import { mockHeadersNormalized, mockModularComponents } from './mockAppState';
3+
import { createTemplate } from 'helpers/storybookHelper';
4+
import { expect, within } from 'storybook/test';
5+
import core from 'core';
6+
import actions from 'actions';
7+
import selectors from 'selectors';
8+
import viewOnlyWhitelist from 'src/redux/viewOnlyWhitelist';
9+
import { panelNames } from 'src/constants/panel';
10+
import { PRESET_BUTTON_TYPES } from 'constants/customizationVariables';
11+
import DataElements from 'constants/dataElement';
12+
import { defaultFlyoutMap } from 'src/redux/modularComponents';
13+
14+
export default {
15+
title: 'ModularComponents/ViewOnly',
16+
component: App,
17+
};
18+
19+
const storeRef = { current: null };
20+
export const ViewOnlyApp = createTemplate({
21+
headers: mockHeadersNormalized,
22+
components: mockModularComponents,
23+
flyoutMap: {
24+
...defaultFlyoutMap,
25+
emptyFlyout: {
26+
dataElement: 'emptyFlyout',
27+
items: [],
28+
},
29+
presetDisabledFlyout: {
30+
dataElement: 'presetDisabledFlyout',
31+
items: [{
32+
'dataElement': DataElements.CREATE_PORTFOLIO_BUTTON,
33+
'presetDataElement': 'createPortfolioPresetButton',
34+
'icon': 'icon-pdf-portfolio',
35+
'label': 'portfolio.createPDFPortfolio',
36+
'title': 'portfolio.createPDFPortfolio',
37+
'isActive': false,
38+
'type': 'presetButton',
39+
'buttonType': 'createPortfolioButton'
40+
}],
41+
}
42+
},
43+
storeRef,
44+
});
45+
46+
ViewOnlyApp.play = async ({ canvasElement }) => {
47+
const originalFFCMode = core.getFormFieldCreationManager().endFormFieldCreationMode;
48+
const originalContentEditMode = core.getContentEditManager().endContentEditMode;
49+
let calls = 0;
50+
core.getFormFieldCreationManager().endFormFieldCreationMode = () => calls++;
51+
core.getContentEditManager().endContentEditMode = () => calls++;
52+
53+
// Should always be false before entering view-only mode
54+
let shouldBeFalse = selectors.isDisabledViewOnly(storeRef.current.getState(), 'rectangleToolButton');
55+
expect(shouldBeFalse).toBe(false);
56+
57+
// These elements should be closed when entering view-only mode
58+
storeRef.current.dispatch(actions.openElements(['mainMenuFlyout', 'searchPanel', 'randomElement']));
59+
window.instance.UI.enableViewOnlyMode();
60+
61+
// Should enable core readOnly
62+
expect(core.getIsReadOnly()).toBe(true);
63+
64+
// Should always be false if dataElement is undefined or not a component type that we have a whitelist for
65+
shouldBeFalse = selectors.isDisabledViewOnly(storeRef.current.getState(), 'randomElement');
66+
expect(shouldBeFalse).toBe(false);
67+
shouldBeFalse = selectors.isDisabledViewOnly(storeRef.current.getState(), undefined);
68+
expect(shouldBeFalse).toBe(false);
69+
shouldBeFalse = selectors.isDisabledViewOnly(storeRef.current.getState(), null);
70+
expect(shouldBeFalse).toBe(false);
71+
// Should be false if it is a modular component type but not in the whitelisted types
72+
shouldBeFalse = selectors.isDisabledViewOnly(storeRef.current.getState(), 'divider-0.1');
73+
expect(shouldBeFalse).toBe(false);
74+
// Should be true if it is a divider and a recursive call (this will cause containers to hide if the only element left is dividers)
75+
let shouldBeTrue = selectors.isDisabledViewOnly(storeRef.current.getState(), 'divider-0.1', true);
76+
expect(shouldBeTrue).toBe(true);
77+
// Should be true for empty flyouts
78+
shouldBeTrue = selectors.isDisabledViewOnly(storeRef.current.getState(), 'emptyFlyout');
79+
expect(shouldBeTrue).toBe(true);
80+
// Should be true for flyouts recursively if all their children are disabled
81+
shouldBeTrue = selectors.isDisabledViewOnly(storeRef.current.getState(), 'presetDisabledFlyout');
82+
expect(shouldBeTrue).toBe(true);
83+
84+
core.getFormFieldCreationManager().endFormFieldCreationMode = originalFFCMode;
85+
core.getContentEditManager().endContentEditMode = originalContentEditMode;
86+
87+
expect(calls >= 2).toBe(true);
88+
const state = storeRef.current.getState();
89+
expect(selectors.isElementOpen(state, 'mainMenuFlyout')).toBe(false);
90+
expect(selectors.isElementOpen(state, 'searchPanel')).toBe(false);
91+
expect(selectors.isElementOpen(state, 'randomElement')).toBe(false);
92+
93+
const panels = Object.values(panelNames);
94+
panels.forEach(async (panel) => {
95+
storeRef.current.dispatch(actions.openElement(panel));
96+
const state = storeRef.current.getState();
97+
const isWhitelisted = viewOnlyWhitelist.panel.includes(panel);
98+
expect(await selectors.isElementOpen(state, panel)).toBe(isWhitelisted);
99+
storeRef.current.dispatch(actions.closeElement(panel));
100+
});
101+
102+
const modalElements = [
103+
DataElements.SCALE_MODAL,
104+
DataElements.CONTENT_EDIT_LINK_MODAL,
105+
DataElements.SIGNATURE_MODAL,
106+
DataElements.PRINT_MODAL,
107+
DataElements.ERROR_MODAL,
108+
DataElements.PASSWORD_MODAL,
109+
DataElements.CUSTOM_STAMP_MODAL,
110+
DataElements.PAGE_REPLACEMENT_MODAL,
111+
DataElements.LINK_MODAL,
112+
DataElements.FILTER_MODAL,
113+
DataElements.PAGE_REDACT_MODAL,
114+
DataElements.CALIBRATION_MODAL,
115+
DataElements.SETTINGS_MODAL,
116+
DataElements.SAVE_MODAL,
117+
DataElements.INSERT_PAGE_MODAL,
118+
DataElements.LOADING_MODAL,
119+
DataElements.WARNING_MODAL,
120+
DataElements.MODEL3D_MODAL,
121+
DataElements.COLOR_PICKER_MODAL,
122+
DataElements.OPEN_FILE_MODAL,
123+
DataElements.CUSTOM_MODAL,
124+
DataElements.SIGNATURE_VALIDATION_MODAL,
125+
DataElements.CREATE_PORTFOLIO_MODAL,
126+
DataElements.HEADER_FOOTER_OPTIONS_MODAL,
127+
DataElements.OFFICE_EDITOR_MARGINS_MODAL,
128+
DataElements.OFFICE_EDITOR_COLUMNS_MODAL,
129+
];
130+
for (let modal of modalElements) {
131+
const shouldBeWhitelisted = viewOnlyWhitelist.modal.includes(modal);
132+
const isDisabled = selectors.isDisabledViewOnly(state, modal);
133+
expect(isDisabled).toBe(!shouldBeWhitelisted);
134+
}
135+
136+
// Should be disabled for headers, ribbons, and GroupedItems by recursively checking their children
137+
const canvas = within(canvasElement);
138+
const headers = await canvas.findAllByRole('toolbar');
139+
// Second header is hidden
140+
expect(headers.length).toBe(1);
141+
const ribbonItems = canvasElement.querySelectorAll('.RibbonItem');
142+
// Ribbons are all hidden (this is a recursive check since ribbons contain GroupedItems which contain toolButtons)
143+
expect(ribbonItems.length).toBe(0);
144+
};
145+
146+
const storeRef2 = { current: null };
147+
export const ViewOnlyWhitelist = createTemplate({
148+
headers: mockHeadersNormalized,
149+
components: mockModularComponents,
150+
flyoutMap: {
151+
...defaultFlyoutMap,
152+
emptyFlyout: {
153+
dataElement: 'emptyFlyout',
154+
items: [],
155+
},
156+
presetDisabledFlyout: {
157+
dataElement: 'presetDisabledFlyout',
158+
items: [{
159+
'dataElement': DataElements.CREATE_PORTFOLIO_BUTTON,
160+
'presetDataElement': 'createPortfolioPresetButton',
161+
'icon': 'icon-pdf-portfolio',
162+
'label': 'portfolio.createPDFPortfolio',
163+
'title': 'portfolio.createPDFPortfolio',
164+
'isActive': false,
165+
'type': 'presetButton',
166+
'buttonType': 'createPortfolioButton'
167+
}],
168+
}
169+
},
170+
storeRef: storeRef2,
171+
});
172+
ViewOnlyWhitelist.play = async ({ canvasElement }) => {
173+
// should be able to whitelist ribbons
174+
window.instance.UI.addToViewOnlyWhitelist(['toolbarGroup-View', 'toolbarGroup-Annotate']);
175+
// Should be able to whitelist buttons and recursively their parents
176+
window.instance.UI.addToViewOnlyWhitelist(['ellipseToolButton']);
177+
// Should be able to blacklist elements
178+
window.instance.UI.removeFromViewOnlyWhitelist(['panToolButton']);
179+
180+
window.instance.UI.enableViewOnlyMode();
181+
182+
const ribbonItems = canvasElement.querySelectorAll('.RibbonItem');
183+
expect(ribbonItems.length).toBe(3);
184+
185+
// Annotate ribbon should not show header since no buttons are whitelisted
186+
const canvas = within(canvasElement);
187+
await canvas.getByRole('button', { name: 'Annotate' }).click();
188+
const headers = await canvas.findAllByRole('toolbar');
189+
expect(headers.length).toBe(1);
190+
191+
await canvas.getByRole('button', { name: 'Shapes' }).click();
192+
await expect(await canvas.findByRole('button', { name: 'Ellipse' })).toBeInTheDocument();
193+
194+
// should be able to update hotkeys whitelist
195+
window.instance.UI.updateViewOnlyShortcuts([window.instance.UI.Shortcuts.RECTANGLE]);
196+
const newShortcuts = window.instance.UI.getViewOnlyShortcuts();
197+
expect(newShortcuts.length).toBe(1);
198+
expect(newShortcuts[0]).toBe(window.instance.UI.Shortcuts.RECTANGLE);
199+
};
200+
201+
const toolComponents = {};
202+
Object.values(window.Core.Tools.ToolNames).forEach((toolName) => {
203+
toolComponents[`${toolName}Button`] = { dataElement: `${toolName}Button`, type: 'toolButton', toolName };
204+
});
205+
export const ToolButtonsHeader = createTemplate({
206+
headers: {
207+
ToolButtonsHeader: {
208+
dataElement: 'ToolButtonsHeader',
209+
placement: 'top',
210+
grow: 0,
211+
gap: 0,
212+
position: 'start',
213+
'float': false,
214+
stroke: true,
215+
dimension: {
216+
paddingTop: 8,
217+
paddingBottom: 8,
218+
borderWidth: 1
219+
},
220+
style: {},
221+
items: Object.keys(toolComponents).reverse(), // Reverse to put whitelisted buttons outside flyout
222+
}
223+
},
224+
components: toolComponents,
225+
});
226+
ToolButtonsHeader.play = async ({ canvasElement }) => {
227+
const canvas = within(canvasElement);
228+
window.instance.UI.enableViewOnlyMode();
229+
const toolButtons = await canvas.findAllByRole('button');
230+
expect(toolButtons.length).toBe(3);
231+
};
232+
ToolButtonsHeader.parameters = window.storybook.disableRtlMode;
233+
234+
const presetComponents = {};
235+
Object.values(PRESET_BUTTON_TYPES).forEach((presetType) => {
236+
presetComponents[presetType] = { dataElement: presetType, type: 'presetButton', buttonType: presetType };
237+
});
238+
export const PresetButtonsHeader = createTemplate({
239+
headers: {
240+
PresetButtonsHeader: {
241+
dataElement: 'PresetButtonsHeader',
242+
placement: 'top',
243+
grow: 0,
244+
gap: 0,
245+
position: 'start',
246+
'float': false,
247+
stroke: true,
248+
dimension: {
249+
paddingTop: 8,
250+
paddingBottom: 8,
251+
borderWidth: 1
252+
},
253+
style: {},
254+
items: Object.keys(presetComponents)
255+
}
256+
},
257+
components: presetComponents,
258+
});
259+
PresetButtonsHeader.play = async ({ canvasElement }) => {
260+
const canvas = within(canvasElement);
261+
window.instance.UI.enableViewOnlyMode();
262+
const presetButtons = await canvas.findAllByRole('button');
263+
expect(presetButtons.length).toBe(6); // Compare and Filepicker are disabled by default
264+
};
265+
PresetButtonsHeader.parameters = window.storybook.disableRtlMode;

src/components/ModularComponents/ModularHeader/ModularHeader.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
z-index: $headers-z-index;
1212
touch-action: none;
1313
outline: 2px solid transparent;
14+
position: relative;
1415

1516
&:has(:focus-visible) {
1617
outline-color: var(--focus-visible-outline) !important;
@@ -20,4 +21,8 @@
2021
&:has(.tab-body.input-mode) {
2122
outline-color: transparent !important;
2223
}
24+
25+
&:has(.ModularHeaderItems.empty-header) {
26+
display: none;
27+
}
2328
}

src/components/ModularHeaderItems/ModularHeaderItems.js

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ import selectors from 'selectors';
1010
import actions from 'actions';
1111
import ToggleElementButton from 'components/ModularComponents/ToggleElementButton';
1212
import PropTypes from 'prop-types';
13+
import classNames from 'classnames';
1314

1415
const ModularHeaderItems = (props) => {
1516
const dispatch = useDispatch();
1617
const { placement, gap, items, justifyContent, className = '', maxWidth, maxHeight, headerId } = props;
1718
const [itemsGap, setItemsGap] = useState(gap);
1819
const elementRef = useRef();
1920
const headerDirection = [PLACEMENT.LEFT, PLACEMENT.RIGHT].includes(placement) ? DIRECTION.COLUMN : DIRECTION.ROW;
21+
const [isHeaderEmpty, setIsHeaderEmpty] = useState(false);
2022

2123
useEffect(() => {
2224
setItemsGap(gap);
@@ -62,6 +64,11 @@ const ModularHeaderItems = (props) => {
6264
}
6365
flyout.items.length > 0 ? dispatch(actions.updateFlyout(flyoutDataElement, flyout)) : dispatch(actions.removeFlyout(flyoutDataElement));
6466
}, [size, items]);
67+
68+
useEffect(() => {
69+
setIsHeaderEmpty(elementRef.current.childElementCount === 0);
70+
}, [items]);
71+
6572
useSizeStore({
6673
elementRef,
6774
dataElement: headerId,
@@ -89,15 +96,19 @@ const ModularHeaderItems = (props) => {
8996
}), [items, size, disabledElements]);
9097

9198
return (
92-
<div className={`ModularHeaderItems ${className}`}
93-
ref={elementRef}
94-
style={{
95-
gap: `${itemsGap}px`,
96-
flexDirection: headerDirection,
97-
justifyContent: justifyContent,
98-
maxWidth: `${maxWidth}px`,
99-
maxHeight: `${maxHeight}px`,
100-
}}>
99+
<div className={classNames({
100+
'ModularHeaderItems': true,
101+
[className]: true,
102+
'empty-header': isHeaderEmpty,
103+
})}
104+
ref={elementRef}
105+
style={{
106+
gap: `${itemsGap}px`,
107+
flexDirection: headerDirection,
108+
justifyContent: justifyContent,
109+
maxWidth: `${maxWidth}px`,
110+
maxHeight: `${maxHeight}px`,
111+
}}>
101112
<ResponsiveContainer headerDirection={headerDirection} elementRef={elementRef} parentDataElement={headerId}
102113
items={items}>
103114
{headerItems}

0 commit comments

Comments
 (0)