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

Anchored Headings for Portable Text #52

Open
58bits opened this issue Mar 13, 2023 · 2 comments
Open

Anchored Headings for Portable Text #52

58bits opened this issue Mar 13, 2023 · 2 comments
Labels

Comments

@58bits
Copy link

58bits commented Mar 13, 2023

Wondering how we would do something similar to this...

https://www.sanity.io/schemas/anchored-headings-for-portable-text-a6c70b6e - created by @kmelve

I think I understand how to create the new block definitions for h1, h2 etc..., or a complete handler like in the example above, but in the code below, I don't believe node is available from props? And I'm not sure what to return instead of return PortableText.defaultSerializers.types.block(props) (since defaultSerializers is no longer on PortableText)

const components: PortableTextComponents = {
  block: props => {
    const { node, children } = props
    const { style, _key } = node

    if (/^h\d/.test(style)) {
      const HeadingTag = style
      // Even though HTML5 allows id to start with a digit, we append it with a letter to avoid various JS methods to act up and make problems
      const headingId = `h${_key}`
      return (
        <HeadingTag id={headingId}>
          <a href={`#${headingId}`} aria-hidden="true" tabIndex={-1}>
            #
          </a>
          <span>{children}</span>
        </HeadingTag>
      )
    }
    // ... you can put in other overrides here

    // or return the default ones 👇
    return PortableText.defaultSerializers.types.block(props)
  }
}

Any thoughts or suggestions greatly appreciated.

@58bits
Copy link
Author

58bits commented Mar 13, 2023

Ah okay I've just ready the migration docs here...

https://github.com/portabletext/react-portabletext/blob/main/MIGRATING.md

and so...

 const { node, children } = props
 const { style, _key } = node

becomes...

 const { value, children } = props
 const { style, _key } = value

What about the return defaultSerializers? (or the 'components') equivalent?

@58bits
Copy link
Author

58bits commented Mar 14, 2023

In case anyone else finds this helpful - here's how I did this in the end.... creating a url-safe kebab-case ID for the heading element and anchor ref using slugify and a helper function to extract the text from the heading element.

import * as React from 'react'
import { PortableText } from '@portabletext/react'
import slugify from 'slugify'

const getTextFromChildren = (children: React.ReactNode) => {
  let text = ''
  React.Children.map(children, child => {
    if (typeof child === 'string') {
      text += child
    }
  })
  return text
}

type HeadingWithAnchorProps = {
  heading: string
  children: React.ReactNode
}

function HeadingWithAnchor({ heading, children }: HeadingWithAnchorProps) {
  const text = getTextFromChildren(children)
  const headingId = slugify(text, { lower: true })
  const Element = heading as keyof JSX.IntrinsicElements
  return (
    <Element id={headingId}>
      <a href={`#${headingId}`} aria-hidden="true" tabIndex={-1}>
        #
      </a>
      <span>{children}</span>
    </Element>
  )
}

const components: PortableTextComponents = {
  block: {
    h2: ({ children }) => {
      return <HeadingWithAnchor heading="h2">{children}</HeadingWithAnchor>
    },
    h3: ({ children }) => {
      return <HeadingWithAnchor heading="h3">{children}</HeadingWithAnchor>
    },
    h4: ({ children }) => {
      return <HeadingWithAnchor heading="h4">{children}</HeadingWithAnchor>
    },
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants