Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Broadcast height to parents - useful when storybook is inside an iframe #84

Merged
merged 2 commits into from
Nov 7, 2023

Conversation

jongomez
Copy link
Contributor

@jongomez jongomez commented Nov 6, 2023

This PR adds a script that listens for "requestHeight" messages, and replies with the document's height.

This was done because some pages on the brand guidelines site currently have a lot of empty space on the bottom, for example:

Brand guidelines page - autocomplete component

This is because we're setting the iframe's height to 6000 pixels by default. Why do we do this? The reason is: iframes don't stretch to their children's height. And there's no way currently to know the iframe's height (if we try to access the iframe's document, we get a CORS error). With the script introduced in this PR, it is now possible to know the iframe's height.

How to test locally

Run the logos brand guidelines site locally. And then:

  1. Change one of the component's storyBook prop to this PR's chromebook url: storybookUrl="https://63e4f71c39dc65c5c703c1e8-bzapfmeede.chromatic.com"
  2. Remove the height: 6000px declaration from the StoryBookDemo.module.scss file
  3. On the StorybookDemo component, we need to request the iframe's height. Replace the component's code with the following:
import { Dropdown } from '@acid-info/lsd-react'
import { useColorMode } from '@docusaurus/theme-common'
import clsx from 'clsx'
import React, { useMemo, useRef, useState, useEffect, RefObject } from 'react'
import styles from './StoryBookDemo.module.scss'

const onIframeLoad = (iframe: HTMLIFrameElement) => {
  const handleIframeMessage = (event: MessageEvent) => {
    if (event.data && event.data.type === 'iframeHeightResponse') {
      console.log('iframeHeightResponse', event.data.height)
      iframe.style.height = `${event.data.height}px`
    }
  }

  // Request height from the iframe
  const requestHeightFromIframe = () => {
    if (!iframe || !iframe.contentWindow) {
      console.error('requestHeightFromIframe - iframe is not ready')
      return
    }

    iframe.contentWindow.postMessage(
      {
        type: 'requestHeight',
      },
      '*',
    )
  }

  window.addEventListener('message', handleIframeMessage)

  setInterval(() => {
    requestHeightFromIframe()
  }, 1000)
}

type GlobalType = {
  name: string
  description: string
  defaultValue: string
  toolbar: {
    icon: string
    items: { title: string; value: string }[]
  }
}

type ComponentProperty = {
  name: string
  type: {
    name: 'enum'
    value: string[]
  }
  defaultValue?: string
}

export type StorybookDemoProps = {
  name: string
  docId: string
  storyId: string
  storybookUrl: string
  globalTypes: Record<string, GlobalType>
  componentProperties: ComponentProperty[]
}

export const StorybookDemo: React.FC<StorybookDemoProps> = ({
  name,
  docId,
  storyId,
  storybookUrl,
  globalTypes,
  componentProperties = [],
}) => {
  const colorMode = useColorMode()

  const iframeRef = useRef<HTMLIFrameElement>(null)

  const [globalProps, setGlobalProps] = useState(
    Object.fromEntries(
      Object.entries(globalTypes).map(([name, prop]) => [
        name,
        name === 'themeColor'
          ? colorMode.colorMode.slice(0, 1).toUpperCase() +
            colorMode.colorMode.slice(1)
          : prop.defaultValue,
      ]),
    ),
  )

  const [props, setProps] = useState(
    Object.fromEntries(
      componentProperties.map(prop => [prop.name, prop.defaultValue]),
    ),
  )

  const embedUrl = useMemo(() => {
    const el = iframeRef.current

    const url = el?.src
      ? new URL(el.src)
      : new URL('/iframe.html', storybookUrl as string)

    url.searchParams.set('id', docId)
    storyId && url.searchParams.set('storyId', storyId)
    url.searchParams.set('globals', 'themeColor:Dark;themeFont:sans-serif')
    url.searchParams.set('embedded', 'true')
    url.searchParams.set(
      'hide',
      'title,subtitle,toolbar' +
        (storyId ? ',description,canvas-border,code' : ''),
    )
    url.searchParams.set('globalControls', 'true')

    return url.toString()
  }, [docId, storyId, globalProps, props])

  return (
    <div className={clsx(styles.root, styles[globalProps.themeColor])}>
      <div
        style={{
          display: 'flex',
          flexDirection: 'row',
          gap: '0 8px',
        }}
      >
        {Object.entries(globalTypes).map(([name, prop]) => (
          <Dropdown
            key={name}
            value={globalProps[name]}
            onChange={value =>
              setGlobalProps(state => ({ ...state, [name]: value as string }))
            }
            options={prop.toolbar.items.map(i => ({
              name: i.title,
              value: i.value,
            }))}
            triggerLabel={prop.name}
            label={prop.name}
          />
        ))}
        {componentProperties.map(prop => (
          <Dropdown
            key={prop.name}
            value={props[prop.name]}
            onChange={value =>
              setProps(state => ({ ...state, [prop.name]: value as string }))
            }
            options={prop.type.value.map(i => ({
              name: i,
              value: i,
            }))}
            triggerLabel={prop.name}
            label={prop.name}
          />
        ))}
      </div>
      <div className={styles.iframeContainer}>
        <iframe
          ref={iframeRef}
          src={embedUrl}
          height={0}
          onLoad={() => {
            onIframeLoad(iframeRef.current)
          }}
        />
      </div>
    </div>
  )
}

Finally, we can check out the page with that component: the height of the iframe should be okay, without any scroll bars and without large empty space on the bottom of the page.

@jongomez jongomez marked this pull request as draft November 6, 2023 17:35
@jongomez jongomez requested a review from jeangovil November 6, 2023 18:32
@jongomez jongomez marked this pull request as ready for review November 6, 2023 18:32
@jongomez jongomez merged commit e5c20a8 into main Nov 7, 2023
4 of 6 checks passed
@jongomez jongomez deleted the broadcast-height branch November 7, 2023 13:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants