Skip to content

Commit

Permalink
improve demo mode
Browse files Browse the repository at this point in the history
  • Loading branch information
yannbf committed Aug 20, 2024
1 parent e7825f6 commit baa92ce
Show file tree
Hide file tree
Showing 6 changed files with 194 additions and 129 deletions.
136 changes: 101 additions & 35 deletions .storybook/interaction.tsx
Original file line number Diff line number Diff line change
@@ -1,65 +1,131 @@
import { userEvent } from '@storybook/test'
import { Loader } from '@storybook/react'

export function delay(ms: number) {
// @ts-expect-error add module augmentation for globalThis
if (!!globalThis.test) {
return new Promise((resolve) => resolve(undefined))
} else {
return new Promise((resolve) => {
// animate mouse pointer from previous to current element
return setTimeout(resolve, ms)
})
}
return new Promise((resolve) => {
// animate mouse pointer from previous to current element
return setTimeout(resolve, ms)
})
}

export async function mouseTo(target: Element, delay = 1700) {
// @ts-expect-error add module augmentation for globalThis
if (!!globalThis.test || !target) {
return new Promise((resolve) => resolve(undefined))
async function mouseTo(
target: Element,
{ cursorStyle = 'hand', delay = 1000 }: { cursorStyle?: 'hand' | 'circle'; delay?: number }
) {
if (!target) {
return new Promise((resolve) => resolve(undefined));
} else {
return new Promise((resolve) => {
// animate mouse pointer from previous to current element
let cursorEl = document.getElementById('demoCursor')
let cursorEl = document.getElementById('sb-demo-cursor');
if (!cursorEl) {
cursorEl = document.createElement('div')
cursorEl.id = 'demoCursor'
cursorEl = document.createElement('div');
cursorEl.id = 'sb-demo-cursor';
cursorEl.dataset.type = cursorStyle;
// Use a hand image if the cursor style is 'hand', else use a circle
if (cursorStyle === 'hand') {
cursorEl.innerHTML = `
<svg width="40" height="40" viewBox="0 0 800 800" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_16_94)">
<path d="M273.5 366L95.5 348L108.5 495.5L301 765L618.5 754L727.5 608L710 348L376.5 220L357 51.5L273.5 38.5V366Z"
fill="white" stroke="black" />
<path
d="M757.266 410.016V401.814C757.266 352.939 717.502 313.179 668.63 313.179C657.592 313.179 647.027 315.223 637.269 318.926C628.977 278.599 593.2 248.179 550.45 248.179C538.841 248.179 527.759 250.444 517.592 254.522C506.865 217.303 471.241 189.089 430.3 189.089C419.277 189.089 408.649 191.22 398.785 195.061V88.6355C398.785 39.7599 359.021 0 310.149 0C261.278 0 221.514 39.7599 221.514 88.6355V349.653C206.001 329.842 187.348 312.576 165.697 303.677C141.537 293.75 116.112 294.935 92.1612 307.112C48.1113 329.511 29.9509 383.492 51.6764 427.451L161.781 650.273C164.893 656.387 239.509 800 374.597 800H532.171C656.418 800 757.498 698.388 757.498 573.464L757.376 410.016H757.266ZM532.175 740.91H374.601C277.291 740.91 216.668 627.701 214.482 623.533L104.653 401.274C97.0735 385.938 103.353 367.715 118.949 359.785C127.544 355.412 135.025 354.967 143.207 358.32C175.829 371.694 207.852 436.146 220.364 473.605L229.857 502.201L280.608 484.241V88.6355C280.608 72.3423 293.86 59.0903 310.153 59.0903C326.446 59.0903 339.698 72.3423 339.698 88.6355V429.39H340.234H398.789H399.324V279.694C399.324 263.2 414.089 248.179 430.303 248.179C447.093 248.179 461.818 261.987 461.818 277.724V336.815V439.238H520.909V336.815C520.909 320.522 534.161 307.27 550.454 307.27C566.747 307.27 579.999 320.522 579.999 336.815V401.814V466.813H639.089V401.814C639.089 385.521 652.341 372.269 668.634 372.269C684.928 372.269 698.179 385.521 698.179 401.814V444.072H698.317L698.416 573.487C698.412 665.806 623.836 740.91 532.175 740.91Z"
fill="black" />
</g>
<defs>
<clipPath id="clip0_16_94">
<rect width="800" height="800" fill="white" />
</clipPath>
</defs>
</svg>`;
}
cursorEl.addEventListener(
'transitionend',
() => {
if (cursorEl) {
cursorEl.className = 'hide'
cursorEl.className = 'sb-cursor-hide';
}
target.dispatchEvent(
new MouseEvent('mouseover', { view: window, bubbles: true, cancelable: true })
)
);
},
{ capture: true }
)
);

document.body.appendChild(cursorEl)
document.body.appendChild(cursorEl);
}

if (!target.getBoundingClientRect) {
console.log('target does not have getBoundingClientRect', target)
return
console.log('target does not have getBoundingClientRect', target);
return;
}

// Function to determine if the element is fully visible in the viewport
const isElementInViewport = (el: Element) => {
const rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
};

// Scroll the element into view if it's not fully visible
if (!isElementInViewport(target)) {
target.scrollIntoView({ block: 'center', inline: 'center' });
}

const { left, top, width, height } = target.getBoundingClientRect()
const sTop = Math.round(top + Math.min(height / 2, 50)) + 'px'
const sLeft = Math.round(left + Math.min(width / 2, 50)) + 'px'
const moveCursor = () => {
const { left, top, width, height } = target.getBoundingClientRect();
const sTop = Math.round(top + Math.min(height / 2, 50)) + 'px';
const sLeft = Math.round(left + Math.min(width / 2, 50)) + 'px';
cursorEl.className = '';

if (cursorStyle === 'circle') {
cursorEl.className = 'sb-cursor-moving';
}
cursorEl.style.top = sTop;
cursorEl.style.left = sLeft;
cursorEl.style.transitionDuration = `${Math.round(delay * 0.9)}ms`;
// ^ bakes in a 10% time delay from movement ending to click event

cursorEl.className = 'moving'
cursorEl.style.top = sTop
cursorEl.style.left = sLeft
cursorEl.style.transitionDuration = `${Math.round(delay * 0.9)}ms`
// ^ bakes in a 10% time delay from movement ending to click event
setTimeout(resolve, delay);
};

return setTimeout(resolve, delay)
})
// Timeout is needed when there are animations on the page e.g. sidebar sliding etc, else the calculation is off and the cursor goes to the wrong place
setTimeout(moveCursor, 300);
});
}
}

export async function animatedUserEventClick(target: Element) {
await mouseTo(target)
return userEvent.click(target)
// @ts-expect-error add module augmentation for types
const isInStorybook = import.meta.env.STORYBOOK && !globalThis.test

export const demoModeLoader: Loader = async (context) => {
if (isInStorybook && context.args.demoMode || context.parameters.test?.demoMode || context.globals.interactionsDemoMode) {
const user = userEvent.setup();

context.userEvent = {
...user,
type: async (...args: any[]) => {
const [target, text, options] = args;
const userSession = userEvent.setup({
// make the typing take .5 seconds
delay: Math.floor(Math.max(500 / text.length, 0)),
});
return userSession.type(target, text, options);
},
click: async (target: Element) => {
await mouseTo(target, {
cursorStyle: context.parameters.test?.cursorStyle,
delay: context.parameters.test?.demoModeDelay,
});
return user.click(target);
},
};
} else {
context.userEvent = userEvent.setup();
}
}
98 changes: 48 additions & 50 deletions .storybook/preview-head.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@
font-weight: 400;
font-display: swap;
src: local('Montserrat Regular'), local('Montserrat-Regular'),
url(https://fonts.gstatic.com/s/montserrat/v15/JTUSjIg1_i6t8kCHKm459WlhyyTh89Y.woff2)
format('woff2');
url(https://fonts.gstatic.com/s/montserrat/v15/JTUSjIg1_i6t8kCHKm459WlhyyTh89Y.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
Expand All @@ -30,8 +29,7 @@
font-weight: 500;
font-display: swap;
src: local('Montserrat Medium'), local('Montserrat-Medium'),
url(https://fonts.gstatic.com/s/montserrat/v15/JTURjIg1_i6t8kCHKm45_ZpC3gnD_vx3rCs.woff2)
format('woff2');
url(https://fonts.gstatic.com/s/montserrat/v15/JTURjIg1_i6t8kCHKm45_ZpC3gnD_vx3rCs.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
Expand All @@ -42,8 +40,7 @@
font-weight: 700;
font-display: swap;
src: local('Montserrat Bold'), local('Montserrat-Bold'),
url(https://fonts.gstatic.com/s/montserrat/v15/JTURjIg1_i6t8kCHKm45_dJE3gnD_vx3rCs.woff2)
format('woff2');
url(https://fonts.gstatic.com/s/montserrat/v15/JTURjIg1_i6t8kCHKm45_dJE3gnD_vx3rCs.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
Expand All @@ -54,8 +51,7 @@
font-style: normal;
font-weight: 900;
src: local('Montserrat Black'), local('Montserrat-Black'),
url(https://fonts.gstatic.com/s/montserrat/v15/JTURjIg1_i6t8kCHKm45_epG3gnD_vx3rCs.woff2)
format('woff2');
url(https://fonts.gstatic.com/s/montserrat/v15/JTURjIg1_i6t8kCHKm45_epG3gnD_vx3rCs.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
Expand All @@ -82,47 +78,15 @@
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* storybook present interaction */
.clickEffect {
width: 20px;
height: 20px;
margin: -10px 0 0 -10px;
/*background: red;*/
position: fixed;
box-sizing: border-box;
border-style: solid;
border-color: #ffc400d6;
border-radius: 50%;
animation: clickEffect 1s ease-out;
z-index: 99999;
}

@keyframes clickEffect {
0% {
opacity: 1;
width: 0.5em;
height: 0.5em;
margin: -0.25em;
border-width: 0.5em;
}

100% {
opacity: 0.2;
width: 15em;
height: 15em;
margin: -7.5em;
border-width: 0.03em;
}
}

#demoCursor {
z-index: 999;
/* storybook interactions demo mode */
#sb-demo-cursor {
z-index: 9999;
transition-duration: 1000ms;
transition-timing-function: cubic-bezier(0.2, 1, 0.2, 1);
transition-property: top, left;
position: fixed;
width: 40px;
height: 40px;
width: 30px;
height: 30px;
border-radius: 20px;
margin: -20px 0 0 -20px;
background: rgba(0, 0, 0, 0);
Expand All @@ -131,22 +95,56 @@
pointer-events: none;
}

#demoCursor.moving {
#sb-demo-cursor[data-type='circle'].sb-cursor-moving {
background: rgba(0, 0, 0, 0.5);
border: 2px solid rgba(255, 255, 255, 0.5);
}

#demoCursor.hide {
animation: cursorHide 1000ms linear;
#sb-demo-cursor[data-type='circle'].sb-cursor-hide {
animation:
fadeCursor 1000ms linear,
press 300ms ease-in;
}

#sb-demo-cursor[data-type='hand'].sb-cursor-hide {
animation:
hideCursor 1000ms linear forwards,
press 300ms ease-in;
}

@keyframes cursorHide {
@keyframes fadeCursor {
0% {
background: rgba(0, 0, 0, 0.1);
background: rgba(0, 0, 0, 0.8);
opacity: 1;
}

100% {
background: rgba(0, 0, 0, 0);
opacity: 0;
}
}

@keyframes hideCursor {
0% {
opacity: 1;
}

100% {
opacity: 0;
}
}

@keyframes press {
0% {
transform: scale(1);
}

50% {
transform: scale(0.45);
}

100% {
transform: scale(1);
}
}
</style>
10 changes: 9 additions & 1 deletion .storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import type { Preview } from '@storybook/react'
import { INITIAL_VIEWPORTS } from '@storybook/addon-viewport'
import { userEvent } from '@storybook/test'

import { globalDecorators } from './decorators'
import { viewports as breakpoints } from '../src/styles/breakpoints'
import { DocsContainer, DocsContainerProps } from '@storybook/blocks'
import { ThemeProvider } from 'styled-components'
import { lightTheme } from '../src/styles/theme'
import { demoModeLoader } from './interaction'
import { mswLoader } from 'msw-storybook-addon'

// Create custom viewports using widths defined in design tokens
Expand Down Expand Up @@ -65,6 +67,12 @@ const preview: Preview = {
},
},
decorators: globalDecorators,
loaders: [mswLoader]
loaders: [mswLoader, demoModeLoader]
}

declare module '@storybook/csf' {
interface StoryContext {
userEvent: ReturnType<typeof userEvent.setup>;
}
}
export default preview
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Meta, StoryObj } from '@storybook/react'
import { http, HttpResponse, delay } from 'msw'
import { within, userEvent } from '@storybook/test'
import { expect } from '@storybook/test'

import { BASE_URL } from '../../api'
Expand Down Expand Up @@ -29,6 +28,11 @@ const meta = {
</>
)
},
argTypes: {
demoMode: {
control: { type: 'boolean' },
},
},
} satisfies Meta<typeof RestaurantDetailPage>
export default meta

Expand Down Expand Up @@ -59,7 +63,7 @@ export const WithModalOpen: Story = {
play: async (context) => {
await Success.play(context)
const item = await context.canvas.findByText(/Cheeseburger/i)
await userEvent.click(item)
await context.userEvent.click(item)
await expect(context.canvas.getByTestId('modal')).toBeInTheDocument()
},
}
Expand Down
Loading

0 comments on commit baa92ce

Please sign in to comment.