Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
612 changes: 611 additions & 1 deletion package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@
"@hashicorp/react-tabs": "^8.2.0",
"@hashicorp/remark-plugins": "^4.1.1",
"@hashicorp/sentinel-embedded": "^1.0.1",
"@hashicorp/react-mds": "*",
"@mapbox/rehype-prism": "^0.8.0",
"@mdx-js/mdx": "^1.6.22",
"@mdx-js/react": "^1.6.22",
Expand Down Expand Up @@ -232,6 +233,7 @@
"minimumChangeThreshold": 300
},
"workspaces": [
"./scripts/docs-content-link-rewrites"
"./scripts/docs-content-link-rewrites",
"packages/*"
]
}
4 changes: 4 additions & 0 deletions packages/react-mds/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
root: true,
extends: require.resolve('@hashicorp/platform-cli/config/.eslintrc.js'),
}
3 changes: 3 additions & 0 deletions packages/react-mds/.stylelintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
extends: ['@hashicorp/platform-cli/config/stylelint.config']
}
1 change: 1 addition & 0 deletions packages/react-mds/.test/setup-vitest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import '@testing-library/jest-dom/vitest'
3 changes: 3 additions & 0 deletions packages/react-mds/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# `@hashicorp/react-mds`

This package contains React implementations of MDS components. This package is maintained by the Web Presence team at HashiCorp.
8 changes: 8 additions & 0 deletions packages/react-mds/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/// <reference types="@hashicorp/platform-types" />

declare module '*.scss'

declare module '*.svg' {
const content: { src: string }
export default content
}
37 changes: 37 additions & 0 deletions packages/react-mds/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "@hashicorp/react-mds",
"scripts": {
"lint": "next-hashicorp lint",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"test:watch": "vitest watch"
},
"dependencies": {
"@hashicorp/flight-icons": "^3.7.0",
"@nivo/pie": "^0.87.0",
"@react-spring/web": "^9.7.3",
"@web/utils": "*",
"classnames": "^2.3.2",
"react-focus-lock": "^2.13.6",
"react-player": "2.10.1",
"react-wrap-balancer": "^0.4.0",
"use-resize-observer": "^9.1.0"
},
"peerDependencies": {
"@tippyjs/react": "^4.2.6",
"@types/react": "*",
"@types/react-dom": "*",
"next": ">=12.0.0",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0",
"tippy.js": "^6.3.1"
},
"devDependencies": {
"@hashicorp/platform-cli": "^2.8.0",
"@hashicorp/platform-types": "^0.4.0",
"@testing-library/jest-dom": "^6.4.2",
"@vitejs/plugin-react": "^4.2.1",
"typescript": "^5.5.3",
"vitest": "^1.4.0"
}
}
21 changes: 21 additions & 0 deletions packages/react-mds/postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Next.js allows for plugins to be specified as an array of arrays, but this
// isn't supported by Vitest, so use a helper function to convert the array of
// arrays to an object.
function standardSyntax(plugins) {
const _plugins = {}
plugins.forEach((p) => {
if (Array.isArray(p)) {
_plugins[p[0]] = p[1]
} else {
_plugins[p] = {}
}
})
return _plugins
}

const baseConfig = require('@hashicorp/platform-postcss-config')

module.exports = {
...baseConfig,
plugins: standardSyntax(baseConfig.plugins),
}
5 changes: 5 additions & 0 deletions packages/react-mds/prettier.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
...require('@hashicorp/platform-cli/config/prettier.config'),
useTabs: true,
tabWidth: 2,
}
22 changes: 22 additions & 0 deletions packages/react-mds/src/components/accordion/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { HTMLAttributes } from 'react'
import classNames from 'classnames'
import s from './style.module.scss'

interface AccordionProps extends HTMLAttributes<HTMLDivElement> {
/**
* optional class name to add to the accordion
*/
className?: string
}

const Accordion = ({ children, className, ...rest }: AccordionProps) => {
return (
<div className={classNames(s.accordion, className)} {...rest}>
{children}
</div>
)
}

export { Accordion }
export type { AccordionProps }
export { AccordionContent, AccordionItem, AccordionToggle } from './item'
39 changes: 39 additions & 0 deletions packages/react-mds/src/components/accordion/item/button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import classNames from 'classnames'
import { FlightIcon } from '../../flight-icon'
import { useDisclosurePrimitive } from '../../disclosure-primitive/use-disclosure-primitive'
import s from '../style.module.scss'

const AccordionButton = () => {
const {
onClickToggle,
isOpen,
contentId,
containsInteractive: parentContainsInteractive,
ariaLabel,
} = useDisclosurePrimitive()

console.log(s)

return (
<button
type="button"
className={classNames(s['button'], {
[s['parent-contains-interactive']]: parentContainsInteractive,
[s['parent-does-not-contain-interactive']]: !parentContainsInteractive,
})}
onClick={onClickToggle}
aria-controls={contentId}
aria-expanded={isOpen}
aria-label={ariaLabel}
>
<FlightIcon
name="chevron-down"
size={24}
isInlineBlock={false}
className={classNames(s.icon, { [s['icon-rotate']]: isOpen })}
/>
</button>
)
}

export { AccordionButton }
73 changes: 73 additions & 0 deletions packages/react-mds/src/components/accordion/item/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
'use client'

import classNames from 'classnames'
import { DisclosurePrimitive } from '../../disclosure-primitive'
import { useDisclosurePrimitive } from '../../disclosure-primitive/use-disclosure-primitive'
import type { HTMLAttributes } from 'react'
import { AccordionButton } from './button'
import s from '../style.module.scss'

interface AccordionItemProps extends HTMLAttributes<HTMLDivElement> {
ariaLabel?: string
isOpen?: boolean
containsInteractive?: boolean
}

const AccordionItem = ({
children,
containsInteractive,
isOpen,
ariaLabel = 'Toggle display',
...rest
}: AccordionItemProps) => {
return (
<DisclosurePrimitive.Provider
className={classNames(s.item, {
[s['contains-interactive']]: containsInteractive,
[s['does-not-contain-interactive']]: !containsInteractive,
})}
trackedIsOpen={isOpen}
containsInteractive={containsInteractive}
ariaLabel={ariaLabel}
{...rest}
>
{children}
</DisclosurePrimitive.Provider>
)
}

const AccordionToggle = ({ children }: React.PropsWithChildren) => {
return (
<div className={s.toggle}>
<AccordionButton />
<div
className={classNames(
s['toggle-content'],
'mds-typography-display-200 token-foreground-strong mds-typography-font-weight-semibold'
)}
>
{children}
</div>
</div>
)
}

const AccordionContent = ({ children }: React.PropsWithChildren) => {
const { contentId } = useDisclosurePrimitive()

return (
<DisclosurePrimitive.Content>
<div
className={classNames(
s['content'],
'token-typography-body-200 token-foreground-primary'
)}
id={contentId}
>
{children}
</div>
</DisclosurePrimitive.Content>
)
}

export { AccordionItem, AccordionToggle, AccordionContent }
112 changes: 112 additions & 0 deletions packages/react-mds/src/components/accordion/style.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
//
// ACCORDION
//

@use "../../styles/mixins/button" as *;
@use "../../styles/mixins/focus-ring" as *;

$item-padding: 16px;
$item-border-radius: 6px;

// ACCORDION COMPONENT (wrapper)

.accordion {
display: flex;
flex-direction: column;
gap: 12px;
}

// ACCORDION ITEM COMPONENT (nested child)

.item {
background: var(--token-color-surface-primary);
border-radius: $item-border-radius;

&.does-not-contain-interactive {
box-shadow: var(--token-surface-mid-box-shadow);

&:hover,
&:global(.mock-hover) {
box-shadow: var(--token-surface-high-box-shadow);
}
}

&.contains-interactive {
box-shadow: var(--token-surface-base-box-shadow);
}
}

// TOGGLE BLOCK

.toggle {
position: relative;
display: flex;
gap: 12px;
align-items: center;
padding:
$item-padding
$item-padding
$item-padding
12px; // by design
}

.button {
padding: 0;

&:hover { cursor: pointer; }

& .icon {
@media (prefers-reduced-motion: no-preference) {
transition: transform 0.3s;
}

&.icon-rotate {
transform: rotate(-180deg);
}
}

// entire toggle area is interactive
&.parent-does-not-contain-interactive {
@include hds-focus-ring-with-pseudo-element();
position: static;
margin: -1px 0;
color: var(--token-color-foreground-primary);
background: transparent;
border: 1px solid transparent;

// expand button target to cover entire AccordionItem Toggle block (depending on the `@containsInteractive/@parentContainsInteractive` argument)
&::after {
position: absolute;
display: block;
border-radius: $item-border-radius;
content: "";
inset: 0;
}
}

// only chevron button area is interactive
&.parent-contains-interactive {
@include hds-button();
width: 24px;
height: 24px;

&:focus,
&:global(.mock-focus) {
@include hds-button-state-focus();
}

// `hds-button-color-secondary` determines the focus color and needs to be placed after `hds-button-state-focus`
@include hds-button-color-secondary();
}
}

// Consumer added content that appears next to the chevron button:
.toggle-content {
flex: 1;
}

// CONTENT BLOCK

.content {
padding: 4px $item-padding $item-padding $item-padding;
}
Loading
Loading