foreman_theme_satellite 16.4.1 → 16.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +12 -5
- data/app/assets/javascripts/foreman_theme_satellite/locale/en_GB/foreman_theme_satellite.js +1 -1
- data/app/assets/javascripts/foreman_theme_satellite/locale/es/foreman_theme_satellite.js +1 -1
- data/app/assets/javascripts/foreman_theme_satellite/locale/fr/foreman_theme_satellite.js +4 -4
- data/app/assets/javascripts/foreman_theme_satellite/locale/ja/foreman_theme_satellite.js +6 -6
- data/app/assets/javascripts/foreman_theme_satellite/locale/ka/foreman_theme_satellite.js +1 -1
- data/app/assets/javascripts/foreman_theme_satellite/locale/ko/foreman_theme_satellite.js +4 -4
- data/app/assets/javascripts/foreman_theme_satellite/locale/pt_BR/foreman_theme_satellite.js +1 -1
- data/app/assets/javascripts/foreman_theme_satellite/locale/zh_CN/foreman_theme_satellite.js +4 -4
- data/app/assets/stylesheets/foreman_theme_satellite/documentation.scss +12 -0
- data/app/assets/stylesheets/foreman_theme_satellite/foreman_theme_satellite.css +1 -0
- data/app/controllers/concerns/documentation_controller_branding.rb +15 -4
- data/app/helpers/documentation_visibility_helper.rb +30 -0
- data/app/overrides/hide_documentation.rb +23 -0
- data/lib/foreman_theme_satellite/engine.rb +1 -0
- data/lib/foreman_theme_satellite/version.rb +1 -1
- data/locale/en_GB/LC_MESSAGES/foreman_theme_satellite.mo +0 -0
- data/locale/en_GB/foreman_theme_satellite.po +1 -1
- data/locale/es/LC_MESSAGES/foreman_theme_satellite.mo +0 -0
- data/locale/es/foreman_theme_satellite.po +1 -1
- data/locale/foreman_theme_satellite.pot +6 -6
- data/locale/fr/LC_MESSAGES/foreman_theme_satellite.mo +0 -0
- data/locale/fr/foreman_theme_satellite.po +5 -5
- data/locale/ja/LC_MESSAGES/foreman_theme_satellite.mo +0 -0
- data/locale/ja/foreman_theme_satellite.po +7 -7
- data/locale/ka/LC_MESSAGES/foreman_theme_satellite.mo +0 -0
- data/locale/ka/foreman_theme_satellite.po +1 -1
- data/locale/ko/LC_MESSAGES/foreman_theme_satellite.mo +0 -0
- data/locale/ko/foreman_theme_satellite.po +5 -5
- data/locale/pt_BR/LC_MESSAGES/foreman_theme_satellite.mo +0 -0
- data/locale/pt_BR/foreman_theme_satellite.po +1 -1
- data/locale/zh_CN/LC_MESSAGES/foreman_theme_satellite.mo +0 -0
- data/locale/zh_CN/foreman_theme_satellite.po +5 -5
- data/webpack/components/SatelliteWelcomeEnv.js +54 -0
- data/webpack/components/fills/UpgradePage/SatelliteUpgradeHelperCard.js +11 -3
- data/webpack/documentation/documentationMenuKeyboard.js +89 -0
- data/webpack/documentation/documentationMenuKeyboard.test.js +229 -0
- data/webpack/documentation/hideDocumentation.js +173 -0
- data/webpack/documentation/hideDocumentation.test.js +294 -0
- data/webpack/index.js +34 -3
- data/webpack/test_setup.js +3 -0
- metadata +10 -1
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { FormattedMessage } from 'react-intl';
|
|
4
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
|
5
|
+
import { foremanUrl, getManualURL } from 'foremanReact/common/helpers';
|
|
6
|
+
import EmptyState from 'foremanReact/components/common/EmptyState';
|
|
7
|
+
|
|
8
|
+
export const SatelliteWelcomeEnv = ({ canCreate }) => {
|
|
9
|
+
const action = canCreate && {
|
|
10
|
+
title: __('Create Puppet Environment'),
|
|
11
|
+
url: foremanUrl('/foreman_puppet/environments/new'),
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const description = (
|
|
15
|
+
<FormattedMessage
|
|
16
|
+
id="puppetenv-welcome"
|
|
17
|
+
defaultMessage={__(
|
|
18
|
+
'If you are planning to use Foreman as an external node classifier you should provide information about one or more environments.{newLine}This information is commonly imported from a pre-existing Puppet configuration by the use of the {puppetClassesLinkToDocs} and environment importer.'
|
|
19
|
+
)}
|
|
20
|
+
values={{
|
|
21
|
+
newLine: <br />,
|
|
22
|
+
puppetClassesLinkToDocs: (
|
|
23
|
+
<a
|
|
24
|
+
target="_blank"
|
|
25
|
+
href={getManualURL('4.2.2Classes')}
|
|
26
|
+
rel="noreferrer"
|
|
27
|
+
>
|
|
28
|
+
{__('Puppet classes')}
|
|
29
|
+
</a>
|
|
30
|
+
),
|
|
31
|
+
}}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<EmptyState
|
|
37
|
+
icon="th"
|
|
38
|
+
iconType="fa"
|
|
39
|
+
header={__('Puppet Environments')}
|
|
40
|
+
description={description}
|
|
41
|
+
action={action}
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
SatelliteWelcomeEnv.propTypes = {
|
|
47
|
+
canCreate: PropTypes.bool,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
SatelliteWelcomeEnv.defaultProps = {
|
|
51
|
+
canCreate: false,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export default SatelliteWelcomeEnv;
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
Card,
|
|
5
|
+
CardBody,
|
|
6
|
+
CardTitle,
|
|
7
|
+
CardFooter,
|
|
8
|
+
Button,
|
|
9
|
+
} from '@patternfly/react-core';
|
|
4
10
|
import { ExternalLinkSquareAltIcon } from '@patternfly/react-icons';
|
|
5
11
|
|
|
6
12
|
import { translate as __ } from 'foremanReact/common/I18n';
|
|
@@ -10,8 +16,10 @@ const SatelliteUpgradeHelperCard = () => (
|
|
|
10
16
|
<Card ouiaId="upgrade-docs-satellite-helper" isFlat>
|
|
11
17
|
<CardTitle component="h4">{__('Satellite upgrade helper')}</CardTitle>
|
|
12
18
|
<CardBody>
|
|
13
|
-
{__(
|
|
14
|
-
|
|
19
|
+
{__(
|
|
20
|
+
'Generate a customized upgrade plan based on your current Satellite environment.' +
|
|
21
|
+
' This tool will guide you through the upgrade process and provide additional steps to avoid known issues specific to your upgrade scenario.'
|
|
22
|
+
)}
|
|
15
23
|
</CardBody>
|
|
16
24
|
<CardFooter>
|
|
17
25
|
<Button
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// PatternFly's legacy dropdown keeps references to CSS-hidden items. Handle
|
|
2
|
+
// navigation in affected menus before its handler attempts to focus those refs.
|
|
3
|
+
const navigationKeys = ['ArrowDown', 'ArrowUp', 'Home', 'End'];
|
|
4
|
+
const openingKeys = [...navigationKeys, 'Enter', ' '];
|
|
5
|
+
const dropdownSelector = '.pf-v5-c-dropdown';
|
|
6
|
+
const menuSelector = '.pf-v5-c-dropdown__menu';
|
|
7
|
+
const itemSelector = '[role="menuitem"]';
|
|
8
|
+
|
|
9
|
+
const isVisible = node => {
|
|
10
|
+
const { defaultView } = node.ownerDocument;
|
|
11
|
+
for (let ancestor = node; ancestor; ancestor = ancestor.parentElement) {
|
|
12
|
+
const style = defaultView.getComputedStyle(ancestor);
|
|
13
|
+
if (
|
|
14
|
+
ancestor.hidden ||
|
|
15
|
+
style.display === 'none' ||
|
|
16
|
+
style.visibility === 'hidden'
|
|
17
|
+
) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return true;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default function installMenuKeyboard(doc, isDocumentationControl) {
|
|
25
|
+
const frames = new Set();
|
|
26
|
+
const navigate = (dropdown, key, current) => {
|
|
27
|
+
const menu = dropdown.querySelector(menuSelector);
|
|
28
|
+
if (!menu || !isVisible(menu)) return false;
|
|
29
|
+
const items = [...menu.querySelectorAll(itemSelector)];
|
|
30
|
+
if (!items.some(isDocumentationControl)) return false;
|
|
31
|
+
const available = items.filter(
|
|
32
|
+
item =>
|
|
33
|
+
!isDocumentationControl(item) &&
|
|
34
|
+
!item.hasAttribute('disabled') &&
|
|
35
|
+
item.getAttribute('aria-disabled') !== 'true' &&
|
|
36
|
+
isVisible(item)
|
|
37
|
+
);
|
|
38
|
+
const lastIndex = available.length - 1;
|
|
39
|
+
const currentIndex = available.indexOf(current);
|
|
40
|
+
let nextIndex = 0;
|
|
41
|
+
if (key === 'End' || (key === 'ArrowUp' && currentIndex <= 0)) {
|
|
42
|
+
nextIndex = lastIndex;
|
|
43
|
+
} else if (key === 'ArrowUp') {
|
|
44
|
+
nextIndex = currentIndex - 1;
|
|
45
|
+
} else if (key === 'ArrowDown' && currentIndex >= 0) {
|
|
46
|
+
nextIndex = (currentIndex + 1) % available.length;
|
|
47
|
+
}
|
|
48
|
+
const target =
|
|
49
|
+
available[nextIndex] || dropdown.querySelector('[aria-expanded]');
|
|
50
|
+
if (target) target.focus();
|
|
51
|
+
return true;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const handleEvent = event => {
|
|
55
|
+
const dropdown = event.target.closest(dropdownSelector);
|
|
56
|
+
if (!dropdown) return;
|
|
57
|
+
const toggle = event.target.closest('[aria-expanded]');
|
|
58
|
+
const key = event.type === 'click' ? 'Enter' : event.key;
|
|
59
|
+
if (event.type === 'keydown' && navigationKeys.includes(key)) {
|
|
60
|
+
const current = event.target.closest(itemSelector);
|
|
61
|
+
if (navigate(dropdown, key, current)) {
|
|
62
|
+
event.preventDefault();
|
|
63
|
+
event.stopImmediatePropagation();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (!toggle || !openingKeys.includes(key)) return;
|
|
68
|
+
// Let React open the menu and PatternFly finish its initial focus first.
|
|
69
|
+
// This also handles menus whose first item is hidden documentation.
|
|
70
|
+
const frame = doc.defaultView.requestAnimationFrame(() => {
|
|
71
|
+
frames.delete(frame);
|
|
72
|
+
if (
|
|
73
|
+
toggle.isConnected &&
|
|
74
|
+
toggle.getAttribute('aria-expanded') === 'true'
|
|
75
|
+
) {
|
|
76
|
+
navigate(dropdown, key, null);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
frames.add(frame);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
doc.addEventListener('keydown', handleEvent, true);
|
|
83
|
+
doc.addEventListener('click', handleEvent, true);
|
|
84
|
+
return () => {
|
|
85
|
+
doc.removeEventListener('keydown', handleEvent, true);
|
|
86
|
+
doc.removeEventListener('click', handleEvent, true);
|
|
87
|
+
frames.forEach(frame => doc.defaultView.cancelAnimationFrame(frame));
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import {
|
|
5
|
+
render,
|
|
6
|
+
fireEvent,
|
|
7
|
+
screen,
|
|
8
|
+
waitFor,
|
|
9
|
+
cleanup,
|
|
10
|
+
} from '@testing-library/react';
|
|
11
|
+
import '@testing-library/jest-dom';
|
|
12
|
+
import {
|
|
13
|
+
Dropdown,
|
|
14
|
+
DropdownItem,
|
|
15
|
+
DropdownToggle,
|
|
16
|
+
DropdownGroup,
|
|
17
|
+
} from '@patternfly/react-core/deprecated';
|
|
18
|
+
import { ActionButtons } from 'foremanReact/components/PF4/TableIndexPage/ActionButtons';
|
|
19
|
+
import DocumentationLink from 'foremanReact/components/PF4/DocumentationLink';
|
|
20
|
+
import { installDocumentationHiding } from './hideDocumentation';
|
|
21
|
+
|
|
22
|
+
const styles = fs
|
|
23
|
+
.readFileSync(
|
|
24
|
+
path.join(
|
|
25
|
+
__dirname,
|
|
26
|
+
'../../app/assets/stylesheets/foreman_theme_satellite/documentation.scss'
|
|
27
|
+
),
|
|
28
|
+
'utf8'
|
|
29
|
+
)
|
|
30
|
+
.replace(/^\/\/.*$/gm, '');
|
|
31
|
+
let installation;
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
const style = document.createElement('style');
|
|
35
|
+
style.textContent = styles;
|
|
36
|
+
style.id = 'documentation-test-style';
|
|
37
|
+
document.head.appendChild(style);
|
|
38
|
+
installation = installDocumentationHiding(document);
|
|
39
|
+
});
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
installation.disconnect();
|
|
42
|
+
cleanup();
|
|
43
|
+
document.getElementById('documentation-test-style').remove();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Use the same grouped PatternFly menu and documentation component as bookmarks.
|
|
47
|
+
const BookmarksMenu = () => {
|
|
48
|
+
const [isOpen, setOpen] = useState(false);
|
|
49
|
+
return (
|
|
50
|
+
<Dropdown
|
|
51
|
+
ouiaId="test-bookmarks"
|
|
52
|
+
isOpen={isOpen}
|
|
53
|
+
isGrouped
|
|
54
|
+
toggle={
|
|
55
|
+
<DropdownToggle onToggle={(_event, open) => setOpen(open)}>
|
|
56
|
+
Bookmarks
|
|
57
|
+
</DropdownToggle>
|
|
58
|
+
}
|
|
59
|
+
>
|
|
60
|
+
<DropdownGroup>
|
|
61
|
+
<DocumentationLink href="/links/manual" />
|
|
62
|
+
<DropdownItem ouiaId="saved-bookmark" href="/hosts?search=saved">
|
|
63
|
+
Saved bookmark
|
|
64
|
+
</DropdownItem>
|
|
65
|
+
<DropdownItem ouiaId="disabled-bookmark" isDisabled>
|
|
66
|
+
Unavailable bookmark
|
|
67
|
+
</DropdownItem>
|
|
68
|
+
<DropdownItem ouiaId="manage-bookmarks">Manage bookmarks</DropdownItem>
|
|
69
|
+
</DropdownGroup>
|
|
70
|
+
</Dropdown>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const press = (element, key) =>
|
|
75
|
+
fireEvent.keyDown(element, {
|
|
76
|
+
key,
|
|
77
|
+
keyCode: {
|
|
78
|
+
ArrowDown: 40,
|
|
79
|
+
ArrowUp: 38,
|
|
80
|
+
Home: 36,
|
|
81
|
+
End: 35,
|
|
82
|
+
Enter: 13,
|
|
83
|
+
Escape: 27,
|
|
84
|
+
}[key],
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('table dropdown skips documentation and wraps between functional actions', async () => {
|
|
88
|
+
const refresh = jest.fn();
|
|
89
|
+
render(
|
|
90
|
+
<ActionButtons
|
|
91
|
+
buttons={[
|
|
92
|
+
{ title: 'Create', action: { href: '/hosts/new' } },
|
|
93
|
+
{ title: 'Export', action: { href: '/hosts.csv' } },
|
|
94
|
+
{ title: 'Documentation', action: { href: '/hosts/help' } },
|
|
95
|
+
{ title: 'Refresh', action: { onClick: refresh } },
|
|
96
|
+
]}
|
|
97
|
+
/>
|
|
98
|
+
);
|
|
99
|
+
const toggle = screen.getByRole('button', { name: 'toggle action dropdown' });
|
|
100
|
+
toggle.focus();
|
|
101
|
+
press(toggle, 'Enter');
|
|
102
|
+
const first = await screen.findByRole('menuitem', { name: 'Export' });
|
|
103
|
+
const last = screen.getByRole('menuitem', { name: 'Refresh' });
|
|
104
|
+
await waitFor(() => expect(first).toHaveFocus());
|
|
105
|
+
await waitFor(() =>
|
|
106
|
+
expect(
|
|
107
|
+
screen.queryByRole('menuitem', { name: 'Documentation' })
|
|
108
|
+
).not.toBeInTheDocument()
|
|
109
|
+
);
|
|
110
|
+
press(first, 'ArrowDown');
|
|
111
|
+
expect(last).toHaveFocus();
|
|
112
|
+
press(last, 'ArrowDown');
|
|
113
|
+
expect(first).toHaveFocus();
|
|
114
|
+
press(first, 'ArrowUp');
|
|
115
|
+
expect(last).toHaveFocus();
|
|
116
|
+
press(last, 'Home');
|
|
117
|
+
expect(first).toHaveFocus();
|
|
118
|
+
press(first, 'End');
|
|
119
|
+
expect(last).toHaveFocus();
|
|
120
|
+
press(last, 'Enter');
|
|
121
|
+
expect(refresh).toHaveBeenCalledTimes(1);
|
|
122
|
+
press(last, 'Escape');
|
|
123
|
+
await waitFor(() => expect(toggle).toHaveFocus());
|
|
124
|
+
expect(toggle).toHaveAttribute('aria-expanded', 'false');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test.each(['Enter', ' '])(
|
|
128
|
+
'opening with %s skips a hidden first item',
|
|
129
|
+
async (key) => {
|
|
130
|
+
render(<BookmarksMenu />);
|
|
131
|
+
const toggle = screen.getByRole('button', { name: 'Bookmarks' });
|
|
132
|
+
toggle.focus();
|
|
133
|
+
press(toggle, key);
|
|
134
|
+
const first = await screen.findByRole('menuitem', {
|
|
135
|
+
name: 'Saved bookmark',
|
|
136
|
+
});
|
|
137
|
+
await waitFor(() => expect(first).toHaveFocus());
|
|
138
|
+
expect(
|
|
139
|
+
screen.queryByRole('menuitem', { name: 'Documentation' })
|
|
140
|
+
).not.toBeInTheDocument();
|
|
141
|
+
press(first, 'ArrowDown');
|
|
142
|
+
expect(
|
|
143
|
+
screen.getByRole('menuitem', { name: 'Manage bookmarks' })
|
|
144
|
+
).toHaveFocus();
|
|
145
|
+
press(document.activeElement, 'ArrowDown');
|
|
146
|
+
expect(first).toHaveFocus();
|
|
147
|
+
}
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
test('ArrowUp from the open toggle focuses the last available action', async () => {
|
|
151
|
+
render(<BookmarksMenu />);
|
|
152
|
+
const toggle = screen.getByRole('button', { name: 'Bookmarks' });
|
|
153
|
+
toggle.focus();
|
|
154
|
+
fireEvent.click(toggle);
|
|
155
|
+
await waitFor(() =>
|
|
156
|
+
expect(
|
|
157
|
+
screen.getByRole('menuitem', { name: 'Saved bookmark' })
|
|
158
|
+
).toHaveFocus()
|
|
159
|
+
);
|
|
160
|
+
toggle.focus();
|
|
161
|
+
press(toggle, 'ArrowUp');
|
|
162
|
+
const last = await screen.findByRole('menuitem', {
|
|
163
|
+
name: 'Manage bookmarks',
|
|
164
|
+
});
|
|
165
|
+
await waitFor(() => expect(last).toHaveFocus());
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('mouse opening also avoids a hidden initial focus target', async () => {
|
|
169
|
+
render(<BookmarksMenu />);
|
|
170
|
+
fireEvent.click(screen.getByRole('button', { name: 'Bookmarks' }));
|
|
171
|
+
await waitFor(() =>
|
|
172
|
+
expect(
|
|
173
|
+
screen.getByRole('menuitem', { name: 'Saved bookmark' })
|
|
174
|
+
).toHaveFocus()
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('menus without documentation retain native keyboard handling', async () => {
|
|
179
|
+
render(
|
|
180
|
+
<ActionButtons
|
|
181
|
+
buttons={[
|
|
182
|
+
{ title: 'Create', action: {} },
|
|
183
|
+
{ title: 'Export', action: { href: '/hosts.csv' } },
|
|
184
|
+
{ title: 'Refresh', action: {} },
|
|
185
|
+
]}
|
|
186
|
+
/>
|
|
187
|
+
);
|
|
188
|
+
const toggle = screen.getByRole('button', { name: 'toggle action dropdown' });
|
|
189
|
+
fireEvent.click(toggle);
|
|
190
|
+
const first = await screen.findByRole('menuitem', { name: 'Export' });
|
|
191
|
+
first.focus();
|
|
192
|
+
press(first, 'ArrowDown');
|
|
193
|
+
expect(screen.getByRole('menuitem', { name: 'Refresh' })).toHaveFocus();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('disconnect removes the theme keyboard listener', async () => {
|
|
197
|
+
render(<BookmarksMenu />);
|
|
198
|
+
fireEvent.click(screen.getByRole('button', { name: 'Bookmarks' }));
|
|
199
|
+
const first = await screen.findByRole('menuitem', { name: 'Saved bookmark' });
|
|
200
|
+
await waitFor(() => expect(first).toHaveFocus());
|
|
201
|
+
installation.disconnect();
|
|
202
|
+
const bubble = jest.fn();
|
|
203
|
+
document.addEventListener('keydown', bubble);
|
|
204
|
+
press(first, 'Home');
|
|
205
|
+
expect(bubble).toHaveBeenCalled();
|
|
206
|
+
document.removeEventListener('keydown', bubble);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('a documentation-only menu keeps focus on its toggle and can close', async () => {
|
|
210
|
+
render(
|
|
211
|
+
<ActionButtons
|
|
212
|
+
buttons={[
|
|
213
|
+
{ title: 'Create', action: {} },
|
|
214
|
+
{ title: 'Documentation', action: { href: '/hosts/help' } },
|
|
215
|
+
]}
|
|
216
|
+
/>
|
|
217
|
+
);
|
|
218
|
+
const toggle = screen.getByRole('button', { name: 'toggle action dropdown' });
|
|
219
|
+
toggle.focus();
|
|
220
|
+
press(toggle, 'Enter');
|
|
221
|
+
await waitFor(() => expect(toggle).toHaveAttribute('aria-expanded', 'true'));
|
|
222
|
+
await waitFor(() =>
|
|
223
|
+
expect(screen.queryByRole('menuitem')).not.toBeInTheDocument()
|
|
224
|
+
);
|
|
225
|
+
press(toggle, 'ArrowDown');
|
|
226
|
+
expect(toggle).toHaveFocus();
|
|
227
|
+
press(toggle, 'Escape');
|
|
228
|
+
expect(toggle).toHaveAttribute('aria-expanded', 'false');
|
|
229
|
+
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import installMenuKeyboard from './documentationMenuKeyboard';
|
|
2
|
+
|
|
3
|
+
// Hide only identified documentation buttons and menu actions. Leave inline
|
|
4
|
+
// links and their surrounding text intact; never remove nodes owned by React.
|
|
5
|
+
export const hiddenClass = 'satellite-documentation-hidden';
|
|
6
|
+
|
|
7
|
+
const controlIds = [
|
|
8
|
+
'doc-url-dropdown',
|
|
9
|
+
'action-buttons-documentation',
|
|
10
|
+
'register-host-documentation-button',
|
|
11
|
+
'audits-documentation-button',
|
|
12
|
+
'inventory-documentation-button',
|
|
13
|
+
'upgrade-docs-button',
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const controlIdSet = new Set(controlIds);
|
|
17
|
+
|
|
18
|
+
const controlSelector = controlIds
|
|
19
|
+
.map(id => `[data-ouia-component-id="${id}"]`)
|
|
20
|
+
.join(',');
|
|
21
|
+
|
|
22
|
+
const dropdownItemSelector =
|
|
23
|
+
'[data-ouia-component-id="action-buttons-dropdown"] [data-ouia-component-id]';
|
|
24
|
+
|
|
25
|
+
const httpHref = /^https?:\/\//i;
|
|
26
|
+
|
|
27
|
+
const satelliteDocsPattern = /\/(?:[a-z]{2}(?:-[a-z]{2})?\/documentation|documentation\/[a-z]{2}(?:-[a-z]{2})?)\/red_hat_satellite(?:\/|$)/i;
|
|
28
|
+
|
|
29
|
+
const isDocumentationControl = (node, documentationLabel) => {
|
|
30
|
+
const id = node.getAttribute('data-ouia-component-id');
|
|
31
|
+
if (!id) return false;
|
|
32
|
+
if (controlIdSet.has(id)) return true;
|
|
33
|
+
return (
|
|
34
|
+
id === `${documentationLabel}-dropdown-item` &&
|
|
35
|
+
node.closest('[data-ouia-component-id="action-buttons-dropdown"]') !== null
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const syncDocumentationControl = (node, documentationLabel) => {
|
|
40
|
+
if (!node || node.nodeType !== 1) return;
|
|
41
|
+
if (isDocumentationControl(node, documentationLabel)) {
|
|
42
|
+
node.classList.add(hiddenClass);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (node.classList.contains(hiddenClass)) node.classList.remove(hiddenClass);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const hideDocumentationIn = (root, documentationLabel) => {
|
|
49
|
+
if (!root || root.nodeType !== 1) return;
|
|
50
|
+
syncDocumentationControl(root, documentationLabel);
|
|
51
|
+
root.querySelectorAll(controlSelector).forEach(node => {
|
|
52
|
+
node.classList.add(hiddenClass);
|
|
53
|
+
});
|
|
54
|
+
const itemId = `${documentationLabel}-dropdown-item`;
|
|
55
|
+
root.querySelectorAll(dropdownItemSelector).forEach(node => {
|
|
56
|
+
if (node.getAttribute('data-ouia-component-id') === itemId) {
|
|
57
|
+
node.classList.add(hiddenClass);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const hideDocumentation = (
|
|
63
|
+
doc,
|
|
64
|
+
documentationLabel = 'Documentation'
|
|
65
|
+
) => {
|
|
66
|
+
// Recalculate so a React node reused for a functional link becomes visible.
|
|
67
|
+
doc
|
|
68
|
+
.querySelectorAll(`.${hiddenClass}`)
|
|
69
|
+
.forEach(node => node.classList.remove(hiddenClass));
|
|
70
|
+
hideDocumentationIn(doc.documentElement || doc, documentationLabel);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const normalizeLink = (link, landingPath) => {
|
|
74
|
+
const href = link.getAttribute('href');
|
|
75
|
+
// A rewrite sets href to the landing path and the observer hears that
|
|
76
|
+
// write. Matching it here makes the follow-up a no-op.
|
|
77
|
+
if (!href || href === landingPath || !httpHref.test(href)) return;
|
|
78
|
+
let url;
|
|
79
|
+
try {
|
|
80
|
+
url = new URL(href);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (satelliteDocsPattern.test(url.pathname)) {
|
|
85
|
+
link.setAttribute('href', landingPath);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const normalizeIn = (root, landingPath) => {
|
|
90
|
+
if (!root || root.nodeType !== 1) return;
|
|
91
|
+
if (typeof root.matches === 'function' && root.matches('a[href]')) {
|
|
92
|
+
normalizeLink(root, landingPath);
|
|
93
|
+
}
|
|
94
|
+
root.querySelectorAll('a[href]').forEach(link => {
|
|
95
|
+
normalizeLink(link, landingPath);
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Direct Satellite documentation links bypass LinksController. Send them
|
|
100
|
+
// through its landing redirect too, including links in stored notifications.
|
|
101
|
+
export const normalizeSatelliteDocumentation = (doc, landingPath) => {
|
|
102
|
+
normalizeIn(doc.documentElement || doc, landingPath);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export const installDocumentationHiding = (
|
|
106
|
+
doc,
|
|
107
|
+
documentationLabel = 'Documentation',
|
|
108
|
+
landingPath
|
|
109
|
+
) => {
|
|
110
|
+
hideDocumentation(doc, documentationLabel);
|
|
111
|
+
if (landingPath) normalizeSatelliteDocumentation(doc, landingPath);
|
|
112
|
+
|
|
113
|
+
const removeMenuKeyboard = installMenuKeyboard(doc, node =>
|
|
114
|
+
isDocumentationControl(node, documentationLabel)
|
|
115
|
+
);
|
|
116
|
+
const pending = [];
|
|
117
|
+
let scheduled = false;
|
|
118
|
+
const flush = () => {
|
|
119
|
+
scheduled = false;
|
|
120
|
+
const batch = pending.splice(0);
|
|
121
|
+
batch.forEach(change => {
|
|
122
|
+
if (change.type === 'childList') {
|
|
123
|
+
change.added.forEach(node => {
|
|
124
|
+
hideDocumentationIn(node, documentationLabel);
|
|
125
|
+
if (landingPath) normalizeIn(node, landingPath);
|
|
126
|
+
});
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (change.name === 'href') {
|
|
130
|
+
if (
|
|
131
|
+
landingPath &&
|
|
132
|
+
change.target.nodeType === 1 &&
|
|
133
|
+
change.target.matches('a[href]')
|
|
134
|
+
) {
|
|
135
|
+
normalizeLink(change.target, landingPath);
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
syncDocumentationControl(change.target, documentationLabel);
|
|
140
|
+
});
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const observer = new doc.defaultView.MutationObserver(mutations => {
|
|
144
|
+
mutations.forEach(mutation => {
|
|
145
|
+
if (mutation.type === 'childList') {
|
|
146
|
+
if (mutation.addedNodes.length === 0) return;
|
|
147
|
+
pending.push({ type: 'childList', added: [...mutation.addedNodes] });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
pending.push({
|
|
151
|
+
type: 'attributes',
|
|
152
|
+
name: mutation.attributeName,
|
|
153
|
+
target: mutation.target,
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
if (scheduled || pending.length === 0) return;
|
|
157
|
+
scheduled = true;
|
|
158
|
+
doc.defaultView.requestAnimationFrame(flush);
|
|
159
|
+
});
|
|
160
|
+
observer.observe(doc.body, {
|
|
161
|
+
subtree: true,
|
|
162
|
+
childList: true,
|
|
163
|
+
attributes: true,
|
|
164
|
+
attributeFilter: ['data-ouia-component-id', 'href'],
|
|
165
|
+
});
|
|
166
|
+
return {
|
|
167
|
+
disconnect: () => {
|
|
168
|
+
observer.disconnect();
|
|
169
|
+
pending.length = 0;
|
|
170
|
+
removeMenuKeyboard();
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
};
|