Skip to content

Commit

Permalink
Add breaking improvements page (#86)
Browse files Browse the repository at this point in the history
  • Loading branch information
jribbink committed Apr 26, 2024
1 parent 0774867 commit c8d77a4
Show file tree
Hide file tree
Showing 6 changed files with 2,053 additions and 4 deletions.
162 changes: 162 additions & 0 deletions src/components/Details/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* This file has been copied from the original source code located in
* `@docusaurus/theme-common/src/components/Details/index.tsx` and modified
* to include a workaround for the Details component.
*
* All of the parts of the code that hve been modified are marked with
* comments like so: `// >>>> THIS IS PART OF THE WORKAROUND FOR DETAILS COMPONENT <<<<`.
*
* It is needed to allow the Details component to be searchable using
* the find in page feature of the browser. And also so tht links can be clicked
* inside the summary element.
*/

import React, {
useRef,
useState,
type ComponentProps,
type ReactElement,
} from 'react';
import clsx from 'clsx';
import useIsBrowser from '@docusaurus/useIsBrowser';
import {useCollapsible, Collapsible} from '@docusaurus/theme-common';
import styles from './styles.module.css';

// >>>> THIS IS PART OF THE WORKAROUND FOR DETAILS COMPONENT <<<<
// we don't recurse parents like the original to allow clicking on links
function isTheSummary(node: HTMLElement | null): boolean {
if (!node) {
return false;
}
return node.tagName === 'SUMMARY';
}

function hasParent(node: HTMLElement | null, parent: HTMLElement): boolean {
if (!node) {
return false;
}
return node === parent || hasParent(node.parentElement, parent);
}

export type DetailsProps = {
/**
* Summary is provided as props, optionally including the wrapping
* `<summary>` tag
*/
summary?: ReactElement | string;
} & ComponentProps<'details'>;

/**
* A mostly un-styled `<details>` element with smooth collapsing. Provides some
* very lightweight styles, but you should bring your UI.
*/
export function Details({
summary,
children,
...props
}: DetailsProps): JSX.Element {
const isBrowser = useIsBrowser();
const detailsRef = useRef<HTMLDetailsElement>(null);

const {collapsed, setCollapsed} = useCollapsible({
initialState: !props.open,
});
// Use a separate state for the actual details prop, because it must be set
// only after animation completes, otherwise close animations won't work
const [open, setOpen] = useState(props.open);

const summaryElement = React.isValidElement(summary) ? (
summary
) : (
<summary>{summary ?? 'Details'}</summary>
);

// >>>> THIS IS PART OF THE WORKAROUND FOR DETAILS COMPONENT <<<<
const [skipAnimation, setSkipAnimation] = useState(false);

return (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions
<details
{...props}
ref={detailsRef}
open={open}
data-collapsed={collapsed}
className={clsx(
styles.details,
isBrowser && styles.isBrowser,
props.className,
)}
onMouseDown={(e) => {
const target = e.target as HTMLElement;
// Prevent a double-click to highlight summary text
if (isTheSummary(target) && e.detail > 1) {
e.preventDefault();
}
}}
onClick={(e) => {
console.log(e.target)
e.stopPropagation(); // For isolation of multiple nested details/summary
const target = e.target as HTMLElement;
const shouldToggle =
isTheSummary(target) && hasParent(target, detailsRef.current!);
if (!shouldToggle) {
return;
}
e.preventDefault();

setSkipAnimation(false);
if (collapsed) {
setCollapsed(false);
setOpen(true);
} else {
setCollapsed(true);
// Don't do this, it breaks close animation!
// setOpen(false);
}
}}

// >>>> THIS IS PART OF THE WORKAROUND FOR DETAILS COMPONENT <<<<
onToggle={(e) => {
if (e.target !== detailsRef.current || detailsRef.current === null) return;
const isDOMOpen = detailsRef.current.open;

// May skip closing animation if DOM state is forcefully closed
// But generally this workaround here is needed for triggering open toggle
if (isDOMOpen !== open) {
setSkipAnimation(true);
setOpen(isDOMOpen);
setCollapsed(!isDOMOpen);
}
}}>
{summaryElement}

<Collapsible
lazy={false} // Content might matter for SEO in this case
collapsed={collapsed}
disableSSRStyle // Allows component to work fine even with JS disabled!
onCollapseTransitionEnd={(newCollapsed) => {
setCollapsed(newCollapsed);
setOpen(!newCollapsed);
}}
animation={skipAnimation ? {
duration: 0,
} : undefined}

// >>>> THIS IS PART OF THE WORKAROUND FOR DETAILS COMPONENT <<<<
// 1. Must be displayed to be searchable
// 2. Must have a height to find location of the element
className={!open && collapsed ? clsx(styles.collapsibleContainer, styles.autoHeight) : styles.collapsibleContainer }
>
<div className={styles.collapsibleContent}>{children}</div>
</Collapsible>
</details>
);
}

77 changes: 77 additions & 0 deletions src/components/Details/styles.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/



/* This is used as a workaround to allow the find-in-page feature to work with collapsible content */
.collapsibleContainer {
display: block !important;
}

.autoHeight {
height: auto !important;
}

/*
CSS variables, meant to be overridden by final theme
*/
.details {
--docusaurus-details-summary-arrow-size: 0.38rem;
--docusaurus-details-transition: transform 200ms ease;
--docusaurus-details-decoration-color: grey;
}

.details > summary {
position: relative;
cursor: pointer;
list-style: none;
padding-left: 1rem;
}

/* TODO: deprecation, need to remove this after Safari will support `::marker` */
.details > summary::-webkit-details-marker {
display: none;
}

.details > summary::before {
position: absolute;
top: 0.45rem;
left: 0;

/* CSS-only Arrow */
content: '';
border-width: var(--docusaurus-details-summary-arrow-size);
border-style: solid;
border-color: transparent transparent transparent
var(--docusaurus-details-decoration-color);

/* Arrow rotation anim */
transform: rotate(0deg);
transition: var(--docusaurus-details-transition);
transform-origin: calc(var(--docusaurus-details-summary-arrow-size) / 2) 50%;
}

/* When JS disabled/failed to load: we use the open property for arrow animation: */
.details[open]:not(.isBrowser) > summary::before,
/* When JS works: we use the data-attribute for arrow animation */
.details[data-collapsed='false'].isBrowser > summary::before {
transform: rotate(90deg);
}

.collapsibleContent {
margin-top: 1rem;
border-top: 1px solid var(--docusaurus-details-decoration-color);
padding-top: 1rem;
}

.collapsibleContent p:last-child {
margin-bottom: 0;
}

.details > summary > p:last-child {
margin-bottom: 0;
}
15 changes: 15 additions & 0 deletions src/theme/Details/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';
import clsx from 'clsx';
import {Details as DetailsGeneric} from '@site/src/components/Details';
import styles from './styles.module.css';
// Should we have a custom details/summary comp in Infima instead of reusing
// alert classes?
const InfimaClasses = 'alert alert--info';
export default function Details({...props}) {
return (
<DetailsGeneric
{...props}
className={clsx(InfimaClasses, styles.details, props.className)}
/>
);
}
6 changes: 6 additions & 0 deletions src/theme/Details/styles.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.details {
--docusaurus-details-decoration-color: var(--ifm-alert-border-color);
--docusaurus-details-transition: transform var(--ifm-transition-fast) ease;
margin: 0 0 var(--ifm-spacing-vertical);
border: 1px solid var(--ifm-alert-border-color);
}
Loading

0 comments on commit c8d77a4

Please sign in to comment.