Skip to content

Commit

Permalink
add basic transition to variable view
Browse files Browse the repository at this point in the history
  • Loading branch information
frostyfan109 committed Sep 11, 2024
1 parent 204d2a5 commit c51c745
Show file tree
Hide file tree
Showing 7 changed files with 230 additions and 60 deletions.
1 change: 1 addition & 0 deletions src/components/search/form/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ export const SearchForm = ({ type=undefined, ...props }) => {
<Form.Item>
<Tooltip title="Change search type">
<Radio.Group
className="search-layout-radio-group"
options={[
{
label: "Concepts",
Expand Down
101 changes: 85 additions & 16 deletions src/contexts/tour-context/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@ import { useEnvironment } from '../environment-context'
import { SearchLayout, useHelxSearch } from '../../components/search'
import { SearchView } from '../../views'
import { useSyntheticDOMMask } from '../../hooks'
import { waitForNoElement } from '../../utils'
import { scrollIntoViewIfNeeded, waitForAttribute, waitForElement, waitForNoElement } from '../../utils'
import 'shepherd.js/dist/css/shepherd.css'
const { useLocation, useNavigate } = require('@gatsbyjs/reach-router')
const waitForElement = require('wait-for-element')

interface ShepherdOptionsWithTypeFixed extends ShepherdOptionsWithType {
when?: any
Expand Down Expand Up @@ -41,7 +40,7 @@ const setNativeValueReact15_16 = (input: HTMLInputElement, value: string) => {
class SelectorTimeoutError extends Error {}
class ErrorSelectorReachedError extends Error {}

const waitForSelector = async (selector: string, errorSelector?: string, timeout: number = 5000) => {
const waitForSelector = async (selector: string, errorSelector?: string, timeout: number | null = 10000) => {
try {
const waitForNormalSelector = waitForElement(selector, timeout)
// Error selector never terminates if none provided.
Expand All @@ -57,6 +56,7 @@ const waitForSelector = async (selector: string, errorSelector?: string, timeout
}

const getExpandButton = () => document.querySelector<HTMLSpanElement>(`.result-card:first-child span.anticon-expand`)
const getVariableViewRadioOption = () => document.querySelector<HTMLLabelElement>(`.search-layout-radio-group > label:nth-child(2)`)

export const TourProvider = ({ children }: ITourProvider ) => {
const { context, routes, basePath} = useEnvironment() as any
Expand All @@ -78,6 +78,7 @@ export const TourProvider = ({ children }: ITourProvider ) => {
const searchBarDomMask = useSyntheticDOMMask(".search-bar, .search-button", { blockClicks: true })
const resultCardDomMask = useSyntheticDOMMask(".result-card:first-child", { blockClicks: false })
const resultModalDomMask = useSyntheticDOMMask(".concept-modal > .ant-modal-content", { blockClicks: false })
const variableRadioOptionDomMask = useSyntheticDOMMask(".search-layout-radio-group > label:nth-child(2)", { blockClicks: false })

const tourOptions = useMemo<Tour.TourOptions>(() => ({
defaultStepOptions: {
Expand Down Expand Up @@ -233,7 +234,7 @@ export const TourProvider = ({ children }: ITourProvider ) => {
element: resultCardDomMask.selector!,
on: "right"
},
scrollTo: false,
scrollToHandler: () => scrollIntoViewIfNeeded(resultCardDomMask.originalSelector),
title: "",
text: renderToStaticMarkup(
<div>
Expand Down Expand Up @@ -265,7 +266,7 @@ export const TourProvider = ({ children }: ITourProvider ) => {
element: resultCardDomMask.selector!,
on: "right"
},
scrollTo: false,
scrollToHandler: () => scrollIntoViewIfNeeded(resultCardDomMask.originalSelector),
title: "",
text: renderToStaticMarkup(
<div>
Expand Down Expand Up @@ -305,7 +306,7 @@ export const TourProvider = ({ children }: ITourProvider ) => {
show: () => {
resultCardDomMask.showMask()

const expandBtn = getExpandButton()!
const expandBtn = getExpandButton()
if (!expandBtn) {
console.log("couldn't find expand button, cancelling tour...")
tour.cancel()
Expand All @@ -317,20 +318,20 @@ export const TourProvider = ({ children }: ITourProvider ) => {
hide: () => {
resultCardDomMask.hideMask()

const expandBtn = getExpandButton()!
expandBtn.removeEventListener("click", listener)
const expandBtn = getExpandButton()
expandBtn?.removeEventListener("click", listener)
},
cancel: () => {
resultCardDomMask.hideMask()

const expandBtn = getExpandButton()!
expandBtn.removeEventListener("click", listener)
const expandBtn = getExpandButton()
expandBtn?.removeEventListener("click", listener)
},
complete: () => {
resultCardDomMask.hideMask()

const expandBtn = getExpandButton()!
expandBtn.removeEventListener("click", listener)
const expandBtn = getExpandButton()
expandBtn?.removeEventListener("click", listener)
}
}
})()
Expand Down Expand Up @@ -425,7 +426,7 @@ export const TourProvider = ({ children }: ITourProvider ) => {
cancel: () => {
stillOnStep = false

const fullscreenBtn = getFullscreenBtn()!
const fullscreenBtn = getFullscreenBtn()
if (fullscreenBtn) fullscreenBtn.disabled = false

resultModalDomMask.hideMask()
Expand All @@ -450,8 +451,8 @@ export const TourProvider = ({ children }: ITourProvider ) => {
text: renderToStaticMarkup(
<div>
Search results are scored based on their search term relevance.
Higher scoring, more relevant results are shown first. Further down
the results, you&apos;ll see concepts less directly relevant to your original
Higher scoring, more relevant results are shown first. Further down,
you&apos;ll see concepts less directly relevant to your original
query but still potentially of unexpected interest.
</div>
),
Expand All @@ -472,6 +473,74 @@ export const TourProvider = ({ children }: ITourProvider ) => {
}
],
},
{
id: "main.search.concept.variable-transition",
attachTo: {
// Antd v4 does not allow id/class identifiers to be set for radio group options.
element: variableRadioOptionDomMask.selector!,
on: "bottom-end"
},
scrollToHandler: () => scrollIntoViewIfNeeded(variableRadioOptionDomMask.originalSelector),
title: "Variable view",
text: renderToStaticMarkup(
<div>
HSS also offers variable-level searching. Click on the Variables button
shows variables containing the search query or synonyms grouped by study.
Similar to concepts, variable results are scored based on the level of the match
between variable information (name, description, related terms) and the search term.
</div>
),
buttons: [
{
classes: 'shepherd-button-secondary',
text: 'Back',
type: 'back'
},
{
classes: 'shepherd-button-primary',
text: 'Next',
type: 'next'
}
],
when: (() => {
const listener = () => {
tour.next()
}
return {
show: () => {
variableRadioOptionDomMask.showMask()

const variableViewOption = getVariableViewRadioOption()
if (!variableViewOption) {
console.log("couldn't find variable view radio option, cancelling tour...")
tour.cancel()
return
}

variableViewOption.addEventListener("click", listener)
},
hide: () => {
variableRadioOptionDomMask.hideMask()

const variableViewOption = getVariableViewRadioOption()
variableViewOption?.removeEventListener("click", listener)
},
cancel: () => {
variableRadioOptionDomMask.hideMask()

const variableViewOption = getVariableViewRadioOption()
variableViewOption?.removeEventListener("click", listener)
},
complete: () => {
variableRadioOptionDomMask.hideMask()

const variableViewOption = getVariableViewRadioOption()
variableViewOption?.removeEventListener("click", listener)
}
}
})()
},

]
return []
}, [searchBarDomMask, basePath, navigate, doSearch])
Expand Down Expand Up @@ -523,7 +592,7 @@ export const TourProvider = ({ children }: ITourProvider ) => {
window.addEventListener("beforeunload", cleanup)
}
const cleanup = () => {
restoreSettings()
// restoreSettings()
window.removeEventListener("beforeunload", cleanup)
}
tour.on("start", setup)
Expand Down
3 changes: 2 additions & 1 deletion src/hooks/use-synthetic-dom-mask.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ export const useSyntheticDOMMask = (
showMask: () => setShow(true),
hideMask: () => setShow(false),
selector: "#" + mask.id,
originalSelector: selector,
element: mask
}) as const, [mask])
}) as const, [mask, selector])

const resize = useCallback((element: HTMLElement, bb: DOMRect) => {
const elBB = element.getBoundingClientRect()
Expand Down
3 changes: 2 additions & 1 deletion src/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export * from './memory-converter';
export * from './update-tab-name';
export * from './call-with-retry'
export * from './palette'
export * from './wait-for-no-element'
export * from './wait-for-element'
export * from './scroll-into-view'
13 changes: 13 additions & 0 deletions src/utils/scroll-into-view.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export function scrollIntoViewIfNeeded(element, scrollOptions) {
if (typeof element === "string") element = document.querySelector(element)
const rect = element.getBoundingClientRect()
const isInViewport = (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
)
if (!isInViewport) {
element.scrollIntoView({ block: "center", inline: "nearest", ...scrollOptions });
}
}
127 changes: 127 additions & 0 deletions src/utils/wait-for-element.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/** matches(attributeValue): boolean */
export function waitForAttribute(node, attributeName, matches, timeout) {
let _resolve, _reject
let promise = new Promise(function (resolve, reject) {
_resolve = resolve
_reject = reject
})

const observer = new MutationObserver(function (mutationList) {
for (const mutation of mutationList) {
if (mutation.type === "attributes" && mutation.attributeName === attributeName) {
if (matches(node.getAttribute(mutation.attributeName))) {
_resolve()
observer.disconnect()
clearTimeout(timerId)
}
}
}
})

// first time check
if (matches(node.getAttribute(attributeName))) {
_resolve()
return promise
}

const timeoutOption = timeout !== undefined ? timeout : 2000

// start
observer.observe(node, {
attributes: true,
attributeFilter: [attributeName]
})

// timeout
var timerId = timeoutOption !== null ? setTimeout(function () {
_reject(new Error("element attribute never matched, timed out:" + selector));
observer.disconnect();
}, timeoutOption) : undefined;

return promise

}

/** Adapted from wait-for-element */
export function waitForElement(selector, timeout) {
var _resolve, _reject;
var promise = new Promise(function (resolve, reject) {
_resolve = resolve;
_reject = reject;
});


var observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
for (var i = 0; i < mutation.addedNodes.length; i++) {
var addedNode = mutation.addedNodes[i];
if (typeof addedNode.matches === "function" && addedNode.matches(selector)) {
_resolve(addedNode);
observer.disconnect();
clearTimeout(timerId);
}
}
});
});
// first time check
var element = document.querySelector(selector);
if (element != null) {
_resolve(element);
return promise;
}
var timeoutOption = timeout !== undefined ? timeout : 2000;// 2s
// start
observer.observe(document.body, {
childList: true, subtree: true
})
// timeout
var timerId = timeoutOption !== null ? setTimeout(function () {
_reject(new Error("Not found element match the selector:" + selector));
observer.disconnect();
}, timeoutOption) : undefined;

return promise;
}

/** Adapted from wait-for-element */
export function waitForNoElement(selector, timeout) {
var _resolve, _reject;
var promise = new Promise(function (resolve, reject) {
_resolve = resolve;
_reject = reject;
});


var observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
for (var i = 0; i < mutation.removedNodes.length; i++) {
var removedNode = mutation.removedNodes[i];
if (typeof removedNode.matches === "function" && removedNode.matches(selector)) {
// Run a check
if (!document.querySelector(selector)) {
_resolve();
observer.disconnect();
clearTimeout(timerId);
}
}
}
});
});
// first time check
if (!document.querySelector(selector)) {
_resolve();
return promise;
}
var timeoutOption = timeout !== undefined ? timeout : 2000;// 2s
// start
observer.observe(document.body, {
childList: true, subtree: true
});
// timeout
var timerId = timeoutOption !== null ? setTimeout(function () {
_reject(new Error("elements matching the selector still exist: " + selector));
observer.disconnect();
}, timeoutOption) : undefined;

return promise;
}
Loading

0 comments on commit c51c745

Please sign in to comment.