Skip to content

Commit

Permalink
docs: make control tabs linkable (#1423)
Browse files Browse the repository at this point in the history
Co-authored-by: Andreas Berliner <andreas.berliner@siemens.com>
  • Loading branch information
danielleroux and AndreasBerliner authored Aug 22, 2024
1 parent 0d1233d commit fbd0b25
Show file tree
Hide file tree
Showing 13 changed files with 485 additions and 18 deletions.
8 changes: 4 additions & 4 deletions packages/documentation/docs/controls/_avatar_code.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Events from './../auto-generated/ix-avatar/events.md';

import Playground from '@site/src/components/PlaygroundV2';

## Examples
## Development

### Basic

Expand Down Expand Up @@ -37,12 +37,12 @@ You can also add the avatar to the header, which will turn it into a clickable b
height="21rem">
</Playground>

## API
### API

### Properties
#### Properties

<Props />

### Events
#### Events

<Events />
24 changes: 13 additions & 11 deletions packages/documentation/docs/controls/_avatar_styleguide.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
Avatars are visual or textual representations of individual identities and they are most often used to represent users logged into a system. Identity providers or user management systems usually provide identity information, and the amount of information provided varies from system to system. The avatar component offers different options to handle this.
## Guidelines

Avatars are visual or textual representations of individual identities, most often used to represent users logged into a system. Identity providers or user management systems usually provide identity information, and the amount of information provided varies from system to system. The avatar component offers different options to handle this.

## Options
### Options

![Avatar overview](https://www.figma.com/design/wEptRgAezDU1z80Cn3eZ0o/iX-Pattern-Illustrations?type=design&node-id=963-565&mode=design&t=M9CowfOcGyqnSycV-4)

| Option | Description and usage |
| -------------------------- | ------------------------------------------------------------------------------------------------------------ |
| Default (1) | Without any set option the visual is just a predefined placeholder graphic, it can be used when identity information is unavailable or cannot be used for other reasons.|
| Initials (2) | Shows a string of one or two characters, can be used when only textual information is available. Examples: a user’s initials (JD for John Doe), the first character from the username (J for johndoe)|
| Image (3) | Shows an image, can be used when identity information includes an image|

## Behavior
| Option | Description and usage |
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Default (1) | Without any option set, the visual is just a predefined placeholder graphic. It can be used when identity information is unavailable or cannot be used for other reasons. |
| Initials (2) | Shows a string of one or two characters. Can be used when only textual information is available. Examples: A user’s initials (JD for John Doe) or the first character from the username (J for johndoe) |
| Image (3) | Shows an image. Can be used when identity information includes an image |

### Behavior

The avatar is a display-only component with no further interactions. Images provided are proportionally scaled to fill the content. A circle shape clips the image. All image formats that browser engines support can be used.

## Dos and Don’ts
### Dos and Don’ts

![Avatar dos and Don‘ts](https://www.figma.com/design/wEptRgAezDU1z80Cn3eZ0o/iX-Pattern-Illustrations?type=design&node-id=975-13&mode=design&t=SxUA6AcHswBAiIzi-4)
![Avatar dos and don‘ts](https://www.figma.com/design/wEptRgAezDU1z80Cn3eZ0o/iX-Pattern-Illustrations?type=design&node-id=975-13&mode=design&t=SxUA6AcHswBAiIzi-4)

- Don't use more than 2 characters when using the "Initials" option
7 changes: 5 additions & 2 deletions packages/documentation/docs/controls/avatar.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import DocsTabs from '@site/src/components/DocsTabs';
import LinkableDocsTabs from '@site/src/components/LinkableDocsTabs';

import DocsUx from './\_avatar_styleguide.md'
import DocsCode from './\_avatar_code.md'
Expand All @@ -12,4 +12,7 @@ import Tags from './../auto-generated/ix-avatar/tags.md';
<br/>
<br/>

<DocsTabs styleguide={DocsUx} code={DocsCode} />
<LinkableDocsTabs>
<DocsUx />
<DocsCode />
</LinkableDocsTabs>
2 changes: 2 additions & 0 deletions packages/documentation/docs/controls/messagebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import Events from './../auto-generated/ix-message-bar/events.md';

import Playground from '@site/src/components/PlaygroundV2';

# Message-bar

## Examples

<Playground
Expand Down
2 changes: 1 addition & 1 deletion packages/documentation/src/components/DocsTabs/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export default (props) => {
return <BrowserOnly>{() => <DocsTabs {...props} />}</BrowserOnly>;
};

function DocTab(
export function DocTab(
props: React.PropsWithChildren<{
name: string;
active: boolean;
Expand Down
41 changes: 41 additions & 0 deletions packages/documentation/src/components/LinkableDocsTabs/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* SPDX-FileCopyrightText: 2024 Siemens AG
*
* SPDX-License-Identifier: MIT
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import { iconCode, iconNavigation } from '@siemens/ix-icons/icons';

export const docsTabQueryString = 'current-tab';
export const guidelinesTabValue = 'guidelines';
export const developmentTabValue = 'development';

export default function LinkableDocsTabs(props: { children: [any, any] }) {
return (
<Tabs queryString={docsTabQueryString}>
<TabItem
value={guidelinesTabValue}
label="Guidelines"
attributes={{
icon: iconNavigation,
}}
>
{props.children[0]}
</TabItem>

<TabItem
value={developmentTabValue}
label="Development"
attributes={{
icon: iconCode,
}}
>
{props.children[1]}
</TabItem>
</Tabs>
);
}
43 changes: 43 additions & 0 deletions packages/documentation/src/css/custom.scss
Original file line number Diff line number Diff line change
Expand Up @@ -415,3 +415,46 @@ p > img {
.Accordion__Last {
margin-block-end: 2rem;
}

.tabs__item {
display: flex;
justify-content: center;
align-items: center;
flex-grow: 1;
background-color: var(--theme-color-component-1);
border-radius: 0px;
color: var(--theme-color-std-text);
height: 2.75rem;
max-height: 2.75rem;
padding: 0;
border: none;

&:hover {
background-color: var(--theme-color-component-1);
}
}

.tabs__item--active {
background-color: var(--theme-color-dynamic);
color: var(--theme-color-primary--contrast);
border: none;

&:hover {
background-color: var(--theme-color-dynamic);
}
}

[id]::before {
content: '';
display: block;
height: 1rem;
margin-top: -1rem;
visibility: hidden;
}

// Hide the development and guidelines sections because they are shown via tabs
h2#development,
h2#guidelines {
visibility: hidden;
height: 0px;
}
25 changes: 25 additions & 0 deletions packages/documentation/src/theme/DocsRoot/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React, { useLayoutEffect } from 'react';
import DocsRoot from '@theme-original/DocsRoot';
import type DocsRootType from '@theme/DocsRoot';
import type { WrapperProps } from '@docusaurus/types';

type Props = WrapperProps<typeof DocsRootType>;

export default function DocsRootWrapper(props: Props): JSX.Element {
const { history, location } = props;

useLayoutEffect(() => {
setTimeout(() => {
history.push({
search: location.search,
hash: location.hash,
});
});
}, [location.search, location.hash]);

return (
<>
<DocsRoot {...props} />
</>
);
}
70 changes: 70 additions & 0 deletions packages/documentation/src/theme/Heading/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React from 'react';
import clsx from 'clsx';
import { translate } from '@docusaurus/Translate';
import { useThemeConfig } from '@docusaurus/theme-common';
import Link from '@docusaurus/Link';
import useBrokenLinks from '@docusaurus/useBrokenLinks';
import type { Props } from '@theme/Heading';

import styles from './styles.module.css';
import { useLocation } from '@docusaurus/router';
import { docsTabQueryString } from '@site/src/components/LinkableDocsTabs';

export default function Heading({ as: As, id, ...props }: Props): JSX.Element {
const location = useLocation();
const brokenLinks = useBrokenLinks();
const {
navbar: { hideOnScroll },
} = useThemeConfig();

const searchParams = new URLSearchParams(location.search);
const currentTab = searchParams.get(docsTabQueryString);

// H1 headings do not need an id because they don't appear in the TOC.
if (As === 'h1' || !id) {
return <As {...props} id={undefined} />;
}

brokenLinks.collectAnchor(id);

const anchorTitle = translate(
{
id: 'theme.common.headingLinkTitle',
message: 'Direct link to {heading}',
description: 'Title for link to heading',
},
{
heading: typeof props.children === 'string' ? props.children : id,
}
);

let link = `#${id}`;

if (currentTab === 'development' || currentTab === 'guidelines') {
link = `?${docsTabQueryString}=${currentTab}${link}`;
}

return (
<As
{...props}
className={clsx(
'anchor',
hideOnScroll
? styles.anchorWithHideOnScrollNavbar
: styles.anchorWithStickyNavbar,
props.className
)}
id={id}
>
{props.children}
<Link
className="hash-link"
to={link}
aria-label={anchorTitle}
title={anchorTitle}
>
&#8203;
</Link>
</As>
);
}
28 changes: 28 additions & 0 deletions packages/documentation/src/theme/Heading/styles.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
When the navbar is sticky, ensure that on anchor click,
the browser does not scroll that anchor behind the navbar
See https://twitter.com/JoshWComeau/status/1332015868725891076
*/
.anchorWithStickyNavbar {
scroll-margin-top: calc(var(--ifm-navbar-height) + 0.5rem);
}

.anchorWithHideOnScrollNavbar {
scroll-margin-top: 0.5rem;
}

:global(.hash-link) {
opacity: 0;
padding-left: 0.5rem;
transition: opacity var(--ifm-transition-fast);
user-select: none;
}

:global(.hash-link::before) {
content: '#';
}

:global(.hash-link:focus),
:global(*:hover > .hash-link) {
opacity: 1;
}
79 changes: 79 additions & 0 deletions packages/documentation/src/theme/TOCItems/Tree.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React, { useMemo } from 'react';
import Link from '@docusaurus/Link';
import type { Props } from '@theme/TOCItems/Tree';
import type { TOCTreeNode } from '@docusaurus/theme-common/internal';
import { useLocation } from '@docusaurus/router';
import {
developmentTabValue,
docsTabQueryString,
guidelinesTabValue,
} from '@site/src/components/LinkableDocsTabs';

type IxProps = Props & {
parentTocItems: readonly TOCTreeNode[];
};

// Recursive component rendering the toc tree
function TOCItemTree({
toc,
className,
linkClassName,
isChild,
parentTocItems,
}: IxProps): JSX.Element | null {
const _location = useLocation();
if (!toc.length) {
return null;
}

return (
<ul className={isChild ? undefined : className}>
{toc.map((heading) => {
const searchParams = new URLSearchParams(_location.search);

let tabId = '';
parentTocItems?.forEach((tab) => {
tab.children.forEach((child) => {
if (child.id === heading.id) {
searchParams.set(docsTabQueryString, tab.id);
tabId = tab.id;
}
});
});

if (tabId === '' && !parentTocItems) {
const tab = toc.find(
(firstLevelHeading) => firstLevelHeading.id === heading.id
);
tabId = tab?.id ?? '';
}

let queryParam = '';
if (tabId === guidelinesTabValue || tabId === developmentTabValue) {
queryParam = `?${docsTabQueryString}=${tabId}`;
}

return (
<li key={heading.id}>
<Link
to={`${queryParam}#${heading.id}`}
className={linkClassName ?? undefined}
// Developer provided the HTML, so assume it's safe.
dangerouslySetInnerHTML={{ __html: heading.value }}
/>
<TOCItemTree
isChild
toc={heading.children}
parentTocItems={isChild ? parentTocItems : toc}
className={className}
linkClassName={linkClassName}
/>
</li>
);
})}
</ul>
);
}

// Memo only the tree root is enough
export default React.memo(TOCItemTree);
Loading

0 comments on commit fbd0b25

Please sign in to comment.