Skip to content

Instantly share code, notes, and snippets.

@jomido
Created February 2, 2023 21:23
Show Gist options
  • Save jomido/e095468c0ab07ace970d971f1f9109bf to your computer and use it in GitHub Desktop.
Save jomido/e095468c0ab07ace970d971f1f9109bf to your computer and use it in GitHub Desktop.
Focused Tour

Focused Tour

A component to automate focused tours.

The framework does not matter (it's incidental), but this demo is in Vue 3.

  • handle window resize
  • add UI for text/help
const SomeView = () => {
return <div class='some view container'>
<FocusedTour
show={state.showTour.value}
interactive={false}
steps={[
{
target: () => dom.buttons.register.value,
text: 'do the thing with the site, yes it is very good',
title: 'register to site',
},
{
target: () => dom.buttons.deliver.value,
text: 'do the thing with the delivery, yes it is very good',
title: 'start a delivery',
},
{
target: () => dom.buttons.emailReceipt.value,
text: 'do the thing with the receipt, yes it is very good',
title: 'email receipt',
},
]}
/>
</div>
}
.ccs-focused-tour {
--clip-background-color: rgba(0, 0, 0, 0.8);
position: absolute;
top: 0;
left: 0;
transition: all 0.2s ease-in-out;
opacity: 1;
pointer-events: none;
width: 100%;
height: 100%;
&.hide {
opacity: 0;
pointer-events: none
}
>.background {
position: absolute;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.0);
z-index: 100;
pointer-events: all;
&.interactive {
pointer-events: none;
}
&>* {
position: absolute;
top: 0;
left: 0;
width: inherit;
height: inherit;
}
>.focus {
background: var(--clip-background-color);
clip-path: polygon(0% 0%, 0% 0%, 0% 0%, 0% 0%);
transition: clip-path 0.6s ease-in-out;
}
}
}
import cc from 'classcat'
import { computed } from 'vue'
import { defineComponent } from 'vue'
import { ref } from 'vue'
import { Teleport } from 'vue'
import type { ComputedRef } from 'vue'
import type { Ref } from 'vue'
import type { PropType } from 'vue'
import './FocusedTour.scss'
interface State {
currentStep: Ref<number>
isReady: ComputedRef<boolean>
}
interface Step {
target: () => HTMLElement | null
text: string
title: string
}
interface Box {
left: number
right: number
top: number
bottom: number
}
interface Props {
/**
* Default `false`. If `true`, allows the focused region to accept pointer
* events.
*/
interactive: boolean
/**
* Default `false`. If `true`, will show the component.
*/
show: boolean
/**
* An array of `Step` objects.
*/
steps: Step[]
}
const props = {
interactive: {
type: Boolean,
default: false,
},
show: {
type: Boolean,
default: false,
},
steps: {
type: Array as PropType<Props['steps']>,
},
}
const zeroBox = () => ({
bottom: 0,
left: 0,
right: 0,
top: 0,
})
const getBox = (step: Step): Box => {
const element = step.target()
if (!element) {
return zeroBox()
}
// @ts-ignore
const rect = element.$el.getBoundingClientRect()
return rect
}
const getFocusStyles = (step: Step | undefined) => {
const box: Box = step === undefined ? zeroBox() : getBox(step)
const clipPathValue = `
polygon(
0% 0%, 100% 0%, 100% 100%, 0% 100%, 0% 0%,
${box.left}px 0%,
${box.left}px ${box.bottom}px,
${box.right}px ${box.bottom}px,
${box.right}px ${box.top}px,
${box.left}px ${box.top}px
)`
return {
'clip-path': clipPathValue,
}
}
const render = (state: State) => (props: Props) => {
if (props.show && !state.isReady.value) {
console.warn('Cannot show focused-tour, as there are still null targets.')
}
const classes = {
component: cc({
'ccs-focused-tour': true,
hide: !props.show || !state.isReady.value,
}),
background: cc({
background: true,
interactive: props.interactive,
}),
}
const step = props.steps[state.currentStep.value]
const styles = {
focus: getFocusStyles(step),
}
return (
<Teleport to="body">
<div
class={classes.component}
onClick={() =>
(state.currentStep.value = state.currentStep.value === 0 ? 1 : 0)
}
>
<div class={classes.background}>
<div class="focus" style={styles.focus} />
</div>
</div>
</Teleport>
)
}
const Component = defineComponent({
name: 'FocusedTour',
props,
setup: (props) => {
const state: State = {
currentStep: ref(0),
isReady: computed(() => {
const noNullElements = (props.steps ?? [])
.map((step) => step.target())
.every((target) => target !== null)
return (props.steps ?? []).length > 0 && noNullElements
}),
}
return render(state)
},
})
export { Component as FocusedTour, type Props as FocusedTourProps }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment