@ably/ui 7.10.0 → 8.0.0-dev.1715d4e
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.
- package/core/Meganav/component.css +5 -1
- package/core/Meganav/component.js +2 -1
- package/core/Meganav/component.js.LICENSE.txt +7 -0
- package/core/Meganav.jsx +8071 -4709
- package/core/MeganavContentWhyAbly.jsx +1 -1
- package/core/MeganavControl/component.js +1 -1
- package/core/MeganavControl.jsx +5 -3
- package/core/MeganavItemsDesktop.jsx +5 -3
- package/core/MeganavItemsMobile.jsx +188 -11
- package/core/MeganavItemsSignedIn.jsx +259 -7
- package/core/MeganavSearch/component.js +1 -0
- package/core/MeganavSearch.jsx +504 -0
- package/core/MeganavSearchAutocomplete/component.js +2 -0
- package/core/MeganavSearchAutocomplete/component.js.LICENSE.txt +7 -0
- package/core/MeganavSearchAutocomplete.jsx +117 -0
- package/core/MeganavSearchPanel/component.js +1 -0
- package/core/MeganavSearchPanel.jsx +455 -0
- package/core/MeganavSearchSuggestions/component.js +1 -0
- package/core/MeganavSearchSuggestions.jsx +366 -0
- package/core/fonts/.DS_Store +0 -0
- package/core/fonts/source-code-pro.css +3 -0
- package/core/images/.DS_Store +0 -0
- package/core/scripts.js +1 -1
- package/core/styles.css +18 -36
- package/package.json +2 -1
- package/preview/vendor/bundle/ruby/3.0.0/bundler/gems/ably-ui-abffd210ec0f/preview/log/.keep +0 -0
- package/preview/vendor/bundle/ruby/3.0.0/bundler/gems/ably-ui-abffd210ec0f/preview/tmp/.keep +0 -0
- package/preview/vendor/bundle/ruby/3.0.0/bundler/gems/ably-ui-abffd210ec0f/preview/tmp/pids/.keep +0 -0
- package/src/.DS_Store +0 -0
- package/src/core/.DS_Store +0 -0
- package/src/core/Code/component.css +1 -3
- package/src/core/Meganav/component.css +5 -1
- package/src/core/Meganav/component.html.erb +10 -5
- package/src/core/Meganav/component.js +11 -1
- package/src/core/Meganav/component.jsx +15 -7
- package/src/core/MeganavControl/component.html.erb +2 -2
- package/src/core/MeganavControl/component.js +37 -12
- package/src/core/MeganavControl/component.jsx +3 -2
- package/src/core/MeganavControl/component.rb +3 -1
- package/src/core/MeganavItemsMobile/component.html.erb +30 -2
- package/src/core/MeganavItemsMobile/component.jsx +33 -2
- package/src/core/MeganavItemsSignedIn/component.html.erb +6 -2
- package/src/core/MeganavItemsSignedIn/component.jsx +7 -2
- package/src/core/MeganavSearch/component.html.erb +15 -0
- package/src/core/MeganavSearch/component.js +0 -0
- package/src/core/MeganavSearch/component.jsx +33 -0
- package/src/core/MeganavSearch/component.rb +13 -0
- package/src/core/MeganavSearchAutocomplete/component.html.erb +6 -0
- package/src/core/MeganavSearchAutocomplete/component.js +177 -0
- package/src/core/MeganavSearchAutocomplete/component.jsx +14 -0
- package/src/core/MeganavSearchAutocomplete/component.rb +6 -0
- package/src/core/MeganavSearchPanel/component.html.erb +22 -0
- package/src/core/MeganavSearchPanel/component.js +0 -0
- package/src/core/MeganavSearchPanel/component.jsx +39 -0
- package/src/core/MeganavSearchPanel/component.rb +13 -0
- package/src/core/MeganavSearchSuggestions/component.html.erb +22 -0
- package/src/core/MeganavSearchSuggestions/component.js +123 -0
- package/src/core/MeganavSearchSuggestions/component.jsx +49 -0
- package/src/core/MeganavSearchSuggestions/component.rb +18 -0
- package/src/core/fonts/.DS_Store +0 -0
- package/src/core/fonts/source-code-pro.css +3 -0
- package/src/core/images/.DS_Store +0 -0
- package/src/core/remote-blogs-posts.js +1 -1
- package/src/core/remote-session-data.js +1 -1
- package/src/core/styles/properties.css +6 -3
- package/src/core/styles/text.css +12 -16
- package/src/core/styles.components.css +0 -15
- package/src/reset/.DS_Store +0 -0
- package/tailwind.config.js +14 -4
|
@@ -17,6 +17,8 @@ import MeganavControl from "../MeganavControl/component";
|
|
|
17
17
|
import MeganavControlMobileDropdown from "../MeganavControlMobileDropdown/component";
|
|
18
18
|
import MobilePanelOpenClick from "../MeganavControlMobilePanelOpen/component";
|
|
19
19
|
import MobilePanelCloseClick from "../MeganavControlMobilePanelClose/component";
|
|
20
|
+
import MeganavSearchAutocomplete from "../MeganavSearchAutocomplete/component";
|
|
21
|
+
import MeganavSearchSuggestions from "../MeganavSearchSuggestions/component";
|
|
20
22
|
|
|
21
23
|
// Close menu when user clicks outside of viewport
|
|
22
24
|
const windowOnBlur = (closeAll) => {
|
|
@@ -111,10 +113,14 @@ const documentScroll = (themeName) => {
|
|
|
111
113
|
};
|
|
112
114
|
};
|
|
113
115
|
|
|
114
|
-
export default function Meganav(
|
|
116
|
+
export default function Meganav(
|
|
117
|
+
{ themeName, addSearchApiKey } = { themeName: null }
|
|
118
|
+
) {
|
|
115
119
|
const controls = MeganavControl();
|
|
116
120
|
const panelOpenControls = MobilePanelOpenClick();
|
|
117
121
|
const panelCloseControls = MobilePanelCloseClick();
|
|
122
|
+
const search = MeganavSearchAutocomplete(addSearchApiKey);
|
|
123
|
+
const searchSuggestions = MeganavSearchSuggestions();
|
|
118
124
|
|
|
119
125
|
const mobileDropdownControl = MeganavControlMobileDropdown({
|
|
120
126
|
clearPanels: () =>
|
|
@@ -124,9 +130,11 @@ export default function Meganav({ themeName } = { themeName: null }) {
|
|
|
124
130
|
const closeAll = () =>
|
|
125
131
|
[
|
|
126
132
|
mobileDropdownControl,
|
|
133
|
+
searchSuggestions,
|
|
127
134
|
...panelOpenControls,
|
|
128
135
|
...panelCloseControls,
|
|
129
136
|
...controls,
|
|
137
|
+
...search,
|
|
130
138
|
].forEach((i) => i.clear());
|
|
131
139
|
|
|
132
140
|
const teardowns = [
|
|
@@ -134,9 +142,11 @@ export default function Meganav({ themeName } = { themeName: null }) {
|
|
|
134
142
|
documentClick(closeAll),
|
|
135
143
|
windowOnBlur(closeAll),
|
|
136
144
|
mobileDropdownControl,
|
|
145
|
+
searchSuggestions,
|
|
137
146
|
...controls,
|
|
138
147
|
...panelOpenControls,
|
|
139
148
|
...panelCloseControls,
|
|
149
|
+
...search,
|
|
140
150
|
].map((i) => i.teardown);
|
|
141
151
|
|
|
142
152
|
return () => teardowns.forEach((teardown) => teardown());
|
|
@@ -18,23 +18,27 @@ import MeganavContentPlatform from "../MeganavContentPlatform/component.jsx";
|
|
|
18
18
|
import MeganavContentUseCases from "../MeganavContentUseCases/component.jsx";
|
|
19
19
|
import MeganavContentWhyAbly from "../MeganavContentWhyAbly/component.jsx";
|
|
20
20
|
import MeganavContentDevelopers from "../MeganavContentDevelopers/component.jsx";
|
|
21
|
+
import MeganavSearch from "../MeganavSearch/component.jsx";
|
|
21
22
|
|
|
22
23
|
const SignIn = ({ sessionState, theme, loginLink, absUrl }) => {
|
|
23
24
|
return sessionState.signedIn ? (
|
|
24
25
|
<MeganavItemsSignedIn absUrl={absUrl} sessionState={sessionState} theme={theme} />
|
|
25
26
|
) : (
|
|
26
27
|
<ul className="hidden md:flex items-center">
|
|
27
|
-
<li>
|
|
28
|
+
<li className="ui-meganav-item">
|
|
28
29
|
<a href={absUrl("/contact")} className={`ui-meganav-link ${theme.textColor}`} data-id="meganav-link">
|
|
29
30
|
Contact us
|
|
30
31
|
</a>
|
|
31
32
|
</li>
|
|
32
|
-
<li>
|
|
33
|
-
<a href={absUrl(loginLink)} className={`ui-meganav-link ${theme.textColor}`} data-id="meganav-link">
|
|
33
|
+
<li className="ui-meganav-item">
|
|
34
|
+
<a href={absUrl(loginLink)} className={`ui-meganav-link mr-0 ${theme.textColor}`} data-id="meganav-link">
|
|
34
35
|
Login
|
|
35
36
|
</a>
|
|
36
37
|
</li>
|
|
37
|
-
<li className="
|
|
38
|
+
<li className="ui-meganav-item">
|
|
39
|
+
<MeganavSearch absUrl={absUrl} />
|
|
40
|
+
</li>
|
|
41
|
+
<li className="ui-meganav-item">
|
|
38
42
|
<a href={absUrl("/sign-up")} data-id="meganav-sign-up-btn" className={`ui-btn p-btn-small ${theme.buttonBackgroundColor} ${theme.buttonTextColor}`}>
|
|
39
43
|
Sign up free
|
|
40
44
|
</a>
|
|
@@ -60,7 +64,7 @@ const panels = {
|
|
|
60
64
|
MeganavContentDevelopers: MeganavContentDevelopers,
|
|
61
65
|
};
|
|
62
66
|
|
|
63
|
-
export default function Meganav({ paths, themeName = "white", notice, loginLink = "/login", urlBase }) {
|
|
67
|
+
export default function Meganav({ paths, themeName = "white", notice, loginLink = "/login", urlBase, addSearchApiKey }) {
|
|
64
68
|
const [sessionState, setSessionState] = useState(null);
|
|
65
69
|
|
|
66
70
|
useEffect(() => {
|
|
@@ -70,7 +74,7 @@ export default function Meganav({ paths, themeName = "white", notice, loginLink
|
|
|
70
74
|
}, []);
|
|
71
75
|
|
|
72
76
|
useEffect(() => {
|
|
73
|
-
const teardown = MeganavScripts({ themeName });
|
|
77
|
+
const teardown = MeganavScripts({ themeName, addSearchApiKey });
|
|
74
78
|
return () => teardown();
|
|
75
79
|
}, [sessionState]);
|
|
76
80
|
|
|
@@ -81,7 +85,10 @@ export default function Meganav({ paths, themeName = "white", notice, loginLink
|
|
|
81
85
|
<nav className={`ui-meganav-wrapper ${theme.backgroundColor} ${theme.barShadow}`} data-id="meganav" aria-label="Main">
|
|
82
86
|
{notice && <Notice {...notice.props} config={notice.config} />}
|
|
83
87
|
<div className="ui-meganav ui-grid-px">
|
|
84
|
-
<
|
|
88
|
+
<div className="mr-24">
|
|
89
|
+
<Logo dataId="meganav-logo" href={urlBase} />
|
|
90
|
+
</div>
|
|
91
|
+
|
|
85
92
|
<MeganavItemsDesktop panels={panels} paths={paths} theme={theme} absUrl={absUrl} />
|
|
86
93
|
|
|
87
94
|
{/* Because we load the session state through fetch, we display a placeholder until fetch returns */}
|
|
@@ -112,4 +119,5 @@ Meganav.propTypes = {
|
|
|
112
119
|
}),
|
|
113
120
|
loginLink: T.string,
|
|
114
121
|
urlBase: T.string,
|
|
122
|
+
addSearchApiKey: T.string,
|
|
115
123
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<%= button_tag(type: "button",
|
|
2
|
-
class: ["ui-meganav-link", "h-64", "flex", "items-center", "group", theme(:text_color)],
|
|
2
|
+
class: ["ui-meganav-link", "h-64", "flex", "items-center", "group", theme(:text_color), additional_css],
|
|
3
3
|
data: { id: "meganav-control" },
|
|
4
|
-
aria: { expanded: false, controls:
|
|
4
|
+
aria: { expanded: false, controls: aria_controls, label: "Show #{content}" }) do -%>
|
|
5
5
|
<%= content -%><%= render(AblyUi::Core::Icon.new(name: "icon-gui-disclosure-arrow", size: "1.5rem", color: "text-cool-black", additional_css: "transform rotate-90 group-hover:text-gui-hover group-focus:text-gui-focus")) %>
|
|
6
6
|
<% end %>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { queryIdAll } from "../dom-query";
|
|
1
|
+
import { queryId, queryIdAll } from "../dom-query";
|
|
2
2
|
|
|
3
3
|
const MeganavControl = () => {
|
|
4
4
|
const controls = Array.from(queryIdAll("meganav-control"));
|
|
@@ -12,11 +12,21 @@ const MeganavControl = () => {
|
|
|
12
12
|
`(hover: hover) and (pointer: fine) and (min-width: ${mdBreakpoint})`
|
|
13
13
|
).matches;
|
|
14
14
|
|
|
15
|
+
const isSearchControl = (node) => node.dataset.control === "search";
|
|
16
|
+
|
|
17
|
+
const isSearchPanelOpen = () => {
|
|
18
|
+
const searchPanel = document.querySelector(
|
|
19
|
+
'[data-id="meganav-panel"]#panel-search'
|
|
20
|
+
);
|
|
21
|
+
if (!searchPanel) return;
|
|
22
|
+
return !searchPanel.classList.contains("invisible");
|
|
23
|
+
};
|
|
24
|
+
|
|
15
25
|
const controlsHaveFocus = () =>
|
|
16
26
|
controls.some((control) => control === document.activeElement);
|
|
17
27
|
|
|
18
28
|
const hover = (control, panel, open) => {
|
|
19
|
-
if (hoverEnabled() && !controlsHaveFocus()) {
|
|
29
|
+
if (hoverEnabled() && !controlsHaveFocus() && !isSearchPanelOpen()) {
|
|
20
30
|
const classes = ["invisible", "visible"];
|
|
21
31
|
panel.classList.replace(...(open ? classes : classes.reverse()));
|
|
22
32
|
control.setAttribute("aria-expanded", open);
|
|
@@ -40,12 +50,20 @@ const MeganavControl = () => {
|
|
|
40
50
|
|
|
41
51
|
const ariaExpanded = control.getAttribute("aria-expanded");
|
|
42
52
|
|
|
43
|
-
if (ariaExpanded) {
|
|
53
|
+
if (ariaExpanded === "true") {
|
|
54
|
+
control.setAttribute("aria-expanded", false);
|
|
55
|
+
panel.classList.replace("visible", "invisible");
|
|
56
|
+
} else {
|
|
44
57
|
control.setAttribute("aria-expanded", true);
|
|
45
58
|
panel.classList.replace("invisible", "visible");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (isSearchControl(control)) {
|
|
62
|
+
const searchInput = queryId("meganav-search-input", panel);
|
|
63
|
+
if (!searchInput) return;
|
|
64
|
+
searchInput.focus();
|
|
46
65
|
} else {
|
|
47
|
-
control.
|
|
48
|
-
panel.classList.replace("visible", "invisible");
|
|
66
|
+
control.focus();
|
|
49
67
|
}
|
|
50
68
|
};
|
|
51
69
|
|
|
@@ -56,18 +74,25 @@ const MeganavControl = () => {
|
|
|
56
74
|
`#${control.getAttribute("aria-controls")}`
|
|
57
75
|
);
|
|
58
76
|
const click = clickHandler(control, panel);
|
|
59
|
-
const mouseenter = mouseenterHandler(control, panel);
|
|
60
|
-
const mouseleave = mouseleaveHandler(control, panel);
|
|
61
|
-
|
|
62
|
-
item.addEventListener("mouseenter", mouseenter);
|
|
63
|
-
item.addEventListener("mouseleave", mouseleave);
|
|
64
77
|
control.addEventListener("click", click);
|
|
78
|
+
let mouseenter, mouseleave;
|
|
79
|
+
|
|
80
|
+
if (!isSearchControl(control)) {
|
|
81
|
+
mouseenter = mouseenterHandler(control, panel);
|
|
82
|
+
mouseleave = mouseleaveHandler(control, panel);
|
|
83
|
+
|
|
84
|
+
item.addEventListener("mouseenter", mouseenter);
|
|
85
|
+
item.addEventListener("mouseleave", mouseleave);
|
|
86
|
+
}
|
|
65
87
|
|
|
66
88
|
return [
|
|
67
89
|
{
|
|
68
90
|
teardown: () => {
|
|
69
|
-
|
|
70
|
-
|
|
91
|
+
if (mouseenter && mouseleave) {
|
|
92
|
+
item.removeEventListener("mouseenter", mouseenter);
|
|
93
|
+
item.removeEventListener("mouseleave", mouseleave);
|
|
94
|
+
}
|
|
95
|
+
|
|
71
96
|
control.removeEventListener("click", click);
|
|
72
97
|
},
|
|
73
98
|
clear: () => {
|
|
@@ -3,11 +3,11 @@ import T from "prop-types";
|
|
|
3
3
|
|
|
4
4
|
import Icon from "../Icon/component.jsx";
|
|
5
5
|
|
|
6
|
-
const MeganavControl = ({ ariaControls, children, theme }) => (
|
|
6
|
+
const MeganavControl = ({ ariaControls, children, theme, additionalCSS }) => (
|
|
7
7
|
<button
|
|
8
8
|
type="button"
|
|
9
9
|
data-id="meganav-control"
|
|
10
|
-
className={`ui-meganav-link h-64 flex items-center group ${theme.textColor}`}
|
|
10
|
+
className={`ui-meganav-link h-64 flex items-center group ${additionalCSS} ${theme.textColor}`}
|
|
11
11
|
aria-expanded="false"
|
|
12
12
|
aria-controls={ariaControls}
|
|
13
13
|
aria-label={`Show ${children}`}
|
|
@@ -26,6 +26,7 @@ MeganavControl.propTypes = {
|
|
|
26
26
|
ariaControls: T.string,
|
|
27
27
|
children: T.node,
|
|
28
28
|
theme: T.object,
|
|
29
|
+
additionalCSS: T.string,
|
|
29
30
|
};
|
|
30
31
|
|
|
31
32
|
export default MeganavControl;
|
|
@@ -2,9 +2,11 @@ module AblyUi
|
|
|
2
2
|
module Core
|
|
3
3
|
class MeganavControl < ViewComponent::Base
|
|
4
4
|
include AblyUi::Core::MeganavConfig
|
|
5
|
+
attr_reader :aria_controls, :additional_css
|
|
5
6
|
|
|
6
|
-
def initialize(aria_controls:, theme_name:)
|
|
7
|
+
def initialize(aria_controls:, theme_name:, additional_css: '')
|
|
7
8
|
@aria_controls = aria_controls
|
|
9
|
+
@additional_css = additional_css
|
|
8
10
|
theme_setup(theme_name)
|
|
9
11
|
end
|
|
10
12
|
end
|
|
@@ -11,8 +11,36 @@
|
|
|
11
11
|
<%= render(AblyUi::Core::MeganavControlMobileDropdown.new(theme_name: @theme_name)) %>
|
|
12
12
|
|
|
13
13
|
<div class="ui-meganav-mobile-dropdown invisible" id="meganav-mobile-dropdown" data-id="meganav-mobile-dropdown">
|
|
14
|
-
<div class="
|
|
15
|
-
|
|
14
|
+
<div class="pt-24 pb-16 ui-grid-px bg-white">
|
|
15
|
+
<%= tag.form class: "mb-16", action: abs_url("/search"), method: "get" do %>
|
|
16
|
+
<div class="relative w-full">
|
|
17
|
+
<%= render(AblyUi::Core::Icon.new(name: "icon-gui-search", size: "1.5rem", color: "text-cool-black", additional_css: "absolute top-12 left-16 hover:text-gui-hover")) %>
|
|
18
|
+
<button
|
|
19
|
+
type="button"
|
|
20
|
+
class="absolute top-12 right-16 p-0 focus:outline-gui-focus m-0 md:hidden invisible"
|
|
21
|
+
data-id="meganav-search-input-clear"
|
|
22
|
+
>
|
|
23
|
+
<%= render(AblyUi::Core::Icon.new(name: "icon-gui-cross-circled-fill", size: "1.5rem", color: "text-dark-grey")) %>
|
|
24
|
+
</button>
|
|
25
|
+
<input
|
|
26
|
+
type="search"
|
|
27
|
+
name="q"
|
|
28
|
+
class="ui-input px-48 h-48"
|
|
29
|
+
style={{ maxWidth: "none" }}
|
|
30
|
+
placeholder="Search"
|
|
31
|
+
autocomplete="off"
|
|
32
|
+
data-id="meganav-mobile-search-input"
|
|
33
|
+
/>
|
|
34
|
+
|
|
35
|
+
<%= render(AblyUi::Core::MeganavSearchAutocomplete.new) %>
|
|
36
|
+
</div>
|
|
37
|
+
<% end %>
|
|
38
|
+
|
|
39
|
+
<div class="max-h-0 overflow-hidden transition-all" data-id="meganav-mobile-search-suggestions">
|
|
40
|
+
<%= render(AblyUi::Core::MeganavSearchSuggestions.new(url_base: url_base, display_support_link: false)) %>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<ul class="mb-16" data-id="meganav-mobile-panel-controls">
|
|
16
44
|
<% panels.each do |panel| %>
|
|
17
45
|
<li class="ui-meganav-mobile-item">
|
|
18
46
|
<%= render(AblyUi::Core::MeganavControlMobilePanelOpen.new(aria_controls: "#{panel[:id]}-mobile")) do %>
|
|
@@ -2,11 +2,14 @@ import React from "react";
|
|
|
2
2
|
import T from "prop-types";
|
|
3
3
|
|
|
4
4
|
import SignOutLink from "../SignOutLink/component.jsx";
|
|
5
|
+
import MeganavSearchSuggestions from "../MeganavSearchSuggestions/component.jsx";
|
|
6
|
+
import Icon from "../Icon/component.jsx";
|
|
5
7
|
|
|
6
8
|
import MeganavData from "../Meganav/component.json";
|
|
7
9
|
import MeganavControlMobileDropdown from "../MeganavControlMobileDropdown/component.jsx";
|
|
8
10
|
import MeganavControlMobilePanelClose from "../MeganavControlMobilePanelClose/component.jsx";
|
|
9
11
|
import MeganavControlMobilePanelOpen from "../MeganavControlMobilePanelOpen/component.jsx";
|
|
12
|
+
import MeganavSearchAutocomplete from "../MeganavSearchAutocomplete/component.jsx";
|
|
10
13
|
|
|
11
14
|
const MeganavItemsMobile = ({ panels, paths, sessionState, theme, loginLink, absUrl }) => {
|
|
12
15
|
const classNames = `ui-meganav-link ${theme.textColor}`;
|
|
@@ -33,8 +36,36 @@ const MeganavItemsMobile = ({ panels, paths, sessionState, theme, loginLink, abs
|
|
|
33
36
|
<MeganavControlMobileDropdown theme={theme} />
|
|
34
37
|
|
|
35
38
|
<div className="ui-meganav-mobile-dropdown invisible" id="meganav-mobile-dropdown" data-id="meganav-mobile-dropdown">
|
|
36
|
-
<div className="
|
|
37
|
-
<
|
|
39
|
+
<div className="pt-24 pb-16 ui-grid-px bg-white">
|
|
40
|
+
<form className="mb-16" action={absUrl("/search")} method="get">
|
|
41
|
+
<div className="relative w-full">
|
|
42
|
+
<Icon name="icon-gui-search" color="text-cool-black" size="1.5rem" additionalCSS="absolute top-12 left-16 hover:text-gui-hover" />
|
|
43
|
+
<button
|
|
44
|
+
type="button"
|
|
45
|
+
className="absolute top-12 right-16 p-0 focus:outline-gui-focus m-0 md:hidden invisible"
|
|
46
|
+
data-id="meganav-search-input-clear"
|
|
47
|
+
>
|
|
48
|
+
<Icon name="icon-gui-cross-circled-fill" color="text-dark-grey" size="1.5rem" additionalCSS="" />
|
|
49
|
+
</button>
|
|
50
|
+
<input
|
|
51
|
+
type="search"
|
|
52
|
+
name="q"
|
|
53
|
+
className="ui-input px-48 h-48"
|
|
54
|
+
style={{ maxWidth: "none" }}
|
|
55
|
+
placeholder="Search"
|
|
56
|
+
autoComplete="off"
|
|
57
|
+
data-id="meganav-mobile-search-input"
|
|
58
|
+
/>
|
|
59
|
+
|
|
60
|
+
<MeganavSearchAutocomplete />
|
|
61
|
+
</div>
|
|
62
|
+
</form>
|
|
63
|
+
|
|
64
|
+
<div className="max-h-0 overflow-hidden transition-all" data-id="meganav-mobile-search-suggestions">
|
|
65
|
+
<MeganavSearchSuggestions absUrl={absUrl} displaySupportLink={false} />
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<ul className="mb-16" data-id="meganav-mobile-panel-controls">
|
|
38
69
|
{MeganavData.panels.map((panel) => {
|
|
39
70
|
const PanelComponent = panels[panel.component];
|
|
40
71
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<ul class="hidden md:flex items-center">
|
|
2
2
|
<li class="ui-meganav-item relative">
|
|
3
|
-
<%= render(AblyUi::Core::MeganavControl.new(aria_controls: "account-panel", theme_name: @theme_name)) do %>
|
|
3
|
+
<%= render(AblyUi::Core::MeganavControl.new(aria_controls: "account-panel", theme_name: @theme_name, additional_css: "mr-0")) do %>
|
|
4
4
|
<%= account_name %>
|
|
5
5
|
<% end %>
|
|
6
6
|
|
|
@@ -41,8 +41,12 @@
|
|
|
41
41
|
</div>
|
|
42
42
|
</li>
|
|
43
43
|
|
|
44
|
+
<li>
|
|
45
|
+
<%= render(AblyUi::Core::MeganavSearch.new(url_base: url_base)) %>
|
|
46
|
+
</li>
|
|
47
|
+
|
|
44
48
|
<% if account? %>
|
|
45
|
-
<li
|
|
49
|
+
<li>
|
|
46
50
|
<%= link_to "Dashboard", @session_data[:account][:links][:dashboard][:href], class: "ui-btn-secondary p-btn-small" %>
|
|
47
51
|
</li>
|
|
48
52
|
<% end %>
|
|
@@ -3,6 +3,7 @@ import T from "prop-types";
|
|
|
3
3
|
|
|
4
4
|
import MeganavControl from "../MeganavControl/component.jsx";
|
|
5
5
|
import SignOutLink from "../SignOutLink/component.jsx";
|
|
6
|
+
import MeganavSearch from "../MeganavSearch/component.jsx";
|
|
6
7
|
|
|
7
8
|
const truncate = (string, length) => {
|
|
8
9
|
return string?.length && string.length > length ? `${string.slice(0, length - 1)}…` : string;
|
|
@@ -16,7 +17,7 @@ const MeganavItemsSignedIn = ({ sessionState, theme, absUrl }) => {
|
|
|
16
17
|
return (
|
|
17
18
|
<ul className="hidden md:flex items-center">
|
|
18
19
|
<li className="ui-meganav-item relative">
|
|
19
|
-
<MeganavControl ariaControls="account-panel" theme={theme}>
|
|
20
|
+
<MeganavControl ariaControls="account-panel" theme={theme} additionalCSS="mr-0">
|
|
20
21
|
{accountName}
|
|
21
22
|
</MeganavControl>
|
|
22
23
|
|
|
@@ -70,8 +71,12 @@ const MeganavItemsSignedIn = ({ sessionState, theme, absUrl }) => {
|
|
|
70
71
|
</div>
|
|
71
72
|
</li>
|
|
72
73
|
|
|
74
|
+
<li>
|
|
75
|
+
<MeganavSearch absUrl={absUrl} />
|
|
76
|
+
</li>
|
|
77
|
+
|
|
73
78
|
{sessionState.account && (
|
|
74
|
-
<li
|
|
79
|
+
<li>
|
|
75
80
|
<a href={absUrl(sessionState.account.links.dashboard.href)} className="ui-btn-secondary p-btn-small">
|
|
76
81
|
Dashboard
|
|
77
82
|
</a>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<button
|
|
2
|
+
type="button"
|
|
3
|
+
data-id="meganav-control"
|
|
4
|
+
data-control="search"
|
|
5
|
+
class="h-64 w-24 px-24 pr-48 py-20 group focus:outline-none"
|
|
6
|
+
aria-expanded="false"
|
|
7
|
+
aria-controls="panel-search"
|
|
8
|
+
aria-label="Show Search Panel"
|
|
9
|
+
>
|
|
10
|
+
<%= render(AblyUi::Core::Icon.new(name: "icon-gui-search", size: "1.5rem", color: "text-cool-black", additional_css: "group-hover:text-gui-hover group-focus:text-gui-focus")) %>
|
|
11
|
+
</button>
|
|
12
|
+
|
|
13
|
+
<div class="ui-meganav-panel invisible" id="panel-search" data-id="meganav-panel">
|
|
14
|
+
<%= render(AblyUi::Core::MeganavSearchPanel.new(url_base: url_base)) %>
|
|
15
|
+
</div>
|
|
File without changes
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import T from "prop-types";
|
|
3
|
+
|
|
4
|
+
import Icon from "../Icon/component.jsx";
|
|
5
|
+
import MeganavSearchPanel from "../MeganavSearchPanel/component.jsx";
|
|
6
|
+
|
|
7
|
+
const MeganavSearch = ({ absUrl }) => {
|
|
8
|
+
return (
|
|
9
|
+
<>
|
|
10
|
+
<button
|
|
11
|
+
type="button"
|
|
12
|
+
data-id="meganav-control"
|
|
13
|
+
data-control="search"
|
|
14
|
+
className="h-64 w-24 px-24 pr-48 py-20 group focus:outline-none"
|
|
15
|
+
aria-expanded="false"
|
|
16
|
+
aria-controls="panel-search"
|
|
17
|
+
aria-label={`Show Search Panel`}
|
|
18
|
+
>
|
|
19
|
+
<Icon name="icon-gui-search" color="text-cool-black" size="1.5rem" additionalCSS="group-hover:text-gui-hover group-focus:text-gui-focus" />
|
|
20
|
+
</button>
|
|
21
|
+
|
|
22
|
+
<div className="ui-meganav-panel invisible" id="panel-search" data-id="meganav-panel">
|
|
23
|
+
<MeganavSearchPanel absUrl={absUrl} />
|
|
24
|
+
</div>
|
|
25
|
+
</>
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
MeganavSearch.propTypes = {
|
|
30
|
+
absUrl: T.func,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default MeganavSearch;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { queryId } from "../dom-query";
|
|
2
|
+
import AddSearchClient from "addsearch-js-client";
|
|
3
|
+
|
|
4
|
+
const init = ({ input, container, listContainer, clear, client }) => {
|
|
5
|
+
client.setAnalyticsTag("Meganav autocomplete");
|
|
6
|
+
client.setThrottleTime(400);
|
|
7
|
+
|
|
8
|
+
const clearResults = () => {
|
|
9
|
+
container.classList.add("hidden");
|
|
10
|
+
listContainer.innerHTML = "";
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const toggleClearBtn = (query) => {
|
|
14
|
+
if ((query || "").length > 0 && clear) {
|
|
15
|
+
clear.classList.remove("invisible");
|
|
16
|
+
} else if (clear) {
|
|
17
|
+
clear.classList.add("invisible");
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const markQueryInSuggestion = (suggestionValue, query) => {
|
|
22
|
+
return suggestionValue.replace(
|
|
23
|
+
query.toLowerCase(),
|
|
24
|
+
`<span class="font-light">${query}</span>`
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const navigateToUrl = (q) => (window.location = `/search?q=${q}`);
|
|
29
|
+
|
|
30
|
+
const focusNext = (index) => {
|
|
31
|
+
const nextSuggestion = listContainer.querySelector(
|
|
32
|
+
`[data-suggestion-index="${index + 1}"]`
|
|
33
|
+
);
|
|
34
|
+
if (!nextSuggestion) return;
|
|
35
|
+
nextSuggestion.focus();
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const focusPrevious = (index) => {
|
|
39
|
+
const previousIndex = index - 1;
|
|
40
|
+
|
|
41
|
+
const previousSuggestion = listContainer.querySelector(
|
|
42
|
+
`[data-suggestion-index="${previousIndex}"]`
|
|
43
|
+
);
|
|
44
|
+
if (!previousSuggestion) return;
|
|
45
|
+
previousSuggestion.focus();
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const renderResults =
|
|
49
|
+
(query) =>
|
|
50
|
+
(results = {}) => {
|
|
51
|
+
toggleClearBtn(query);
|
|
52
|
+
|
|
53
|
+
// Prevent invalid access error when key is invalid
|
|
54
|
+
if (!Array.isArray(results.suggestions)) {
|
|
55
|
+
clearResults();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Prevent key error from invalid key
|
|
60
|
+
if (results.suggestions.length === 0) {
|
|
61
|
+
clearResults();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const items = results.suggestions.map((suggestion, index) => {
|
|
66
|
+
const li = document.createElement("li");
|
|
67
|
+
const button = document.createElement("button");
|
|
68
|
+
button.type = "button";
|
|
69
|
+
|
|
70
|
+
button.classList.add(
|
|
71
|
+
"ui-text-menu2",
|
|
72
|
+
"font-medium",
|
|
73
|
+
"p-8",
|
|
74
|
+
"w-full",
|
|
75
|
+
"text-left",
|
|
76
|
+
"rounded",
|
|
77
|
+
"hover:text-gui-hover",
|
|
78
|
+
"focus:outline-gui-focus",
|
|
79
|
+
"hover:bg-light-grey"
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
button.innerHTML = markQueryInSuggestion(suggestion.value, query);
|
|
83
|
+
|
|
84
|
+
button.dataset.suggestionIndex = index;
|
|
85
|
+
|
|
86
|
+
button.addEventListener("click", () => {
|
|
87
|
+
navigateToUrl(suggestion.value);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
button.addEventListener("keydown", (e) => {
|
|
91
|
+
const key = e.key;
|
|
92
|
+
|
|
93
|
+
if (key === "ArrowDown") {
|
|
94
|
+
focusNext(index);
|
|
95
|
+
} else if (key === "ArrowUp" && index - 1 < 0) {
|
|
96
|
+
input.focus();
|
|
97
|
+
} else if (key === "ArrowUp" && index - 1 >= 0) {
|
|
98
|
+
focusPrevious(index);
|
|
99
|
+
} else if (key === "Enter" || key === "Space") {
|
|
100
|
+
navigateToUrl(suggestion.value);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
li.appendChild(button);
|
|
105
|
+
return li;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
listContainer.innerHTML = "";
|
|
109
|
+
items.forEach((item) => listContainer.appendChild(item));
|
|
110
|
+
container.classList.remove("hidden");
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const keyupHandler = (e) => {
|
|
114
|
+
const query = e.target.value;
|
|
115
|
+
const key = e.key;
|
|
116
|
+
|
|
117
|
+
if (key === "ArrowDown") {
|
|
118
|
+
focusNext(0);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!query) {
|
|
123
|
+
clearResults();
|
|
124
|
+
} else {
|
|
125
|
+
client.suggestions(query, renderResults(query));
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
let clearHandler;
|
|
130
|
+
if (clear) {
|
|
131
|
+
clearHandler = () => {
|
|
132
|
+
input.value = "";
|
|
133
|
+
clear.classList.add("invisible");
|
|
134
|
+
clearResults();
|
|
135
|
+
};
|
|
136
|
+
clear.addEventListener("click", clearHandler);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
input.addEventListener("keyup", keyupHandler);
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
teardown: () => {
|
|
143
|
+
input.removeEventListener("keyup", keyupHandler);
|
|
144
|
+
if (clear) clear.removeEventListener("click", clearHandler);
|
|
145
|
+
},
|
|
146
|
+
clear: () => {
|
|
147
|
+
input.value = "";
|
|
148
|
+
clearResults();
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
export default (apiKey) => {
|
|
154
|
+
if (!apiKey) {
|
|
155
|
+
console.log(`No AddSearch API key provided, skipping search suggestions.`);
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const client = new AddSearchClient(apiKey);
|
|
160
|
+
|
|
161
|
+
return [
|
|
162
|
+
queryId("meganav-search-input"),
|
|
163
|
+
queryId("meganav-mobile-search-input"),
|
|
164
|
+
]
|
|
165
|
+
.filter((i) => i)
|
|
166
|
+
.map((input) => {
|
|
167
|
+
const parent = input.parentNode;
|
|
168
|
+
const container = queryId(
|
|
169
|
+
"meganav-search-autocomplete-container",
|
|
170
|
+
parent
|
|
171
|
+
);
|
|
172
|
+
const listContainer = queryId("meganav-search-autocomplete-list", parent);
|
|
173
|
+
const clear = queryId("meganav-search-input-clear", parent);
|
|
174
|
+
|
|
175
|
+
return init({ input, container, listContainer, client, clear });
|
|
176
|
+
});
|
|
177
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
const MeganavSearchAutocomplete = () => {
|
|
4
|
+
return (
|
|
5
|
+
<div
|
|
6
|
+
className="absolute w-full mt-8 z-10 hidden shadow-container rounded-lg bg-white border border-mid-grey"
|
|
7
|
+
data-id="meganav-search-autocomplete-container"
|
|
8
|
+
>
|
|
9
|
+
<ol className="m-16" data-id="meganav-search-autocomplete-list"></ol>
|
|
10
|
+
</div>
|
|
11
|
+
);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default MeganavSearchAutocomplete;
|