feat: remove internal hooks & add performance disclaimer to useBreakpoint
Signed-off-by: Antony Bouyon <antony.bouyon@believe.com>
This commit is contained in:
@@ -8,17 +8,7 @@ Backstage UI custom hooks provide easy access to design system features, style m
|
||||
<ComponentCards>
|
||||
<ComponentCard
|
||||
title="useBreakpoint"
|
||||
description="Hook to detect current responsive breakpoints and manage adaptive behaviors."
|
||||
description="Responsive breakpoint detection hook (performance optimizations in progress)."
|
||||
href="/hooks/use-breakpoint"
|
||||
/>
|
||||
<ComponentCard
|
||||
title="useMediaQuery"
|
||||
description="Hook to evaluate and react to CSS media queries programmatically."
|
||||
href="/hooks/use-media-query"
|
||||
/>
|
||||
<ComponentCard
|
||||
title="useStyles"
|
||||
description="Main hook to access and manipulate design system styles."
|
||||
href="/hooks/use-styles"
|
||||
/>
|
||||
</ComponentCards>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { CodeBlock } from '@/components/CodeBlock';
|
||||
import { PageTitle } from '@/components/PageTitle';
|
||||
import { PropsTable } from '@/components/PropsTable';
|
||||
import { Snippet } from '@/components/Snippet';
|
||||
import { Banner } from '@/components/Banner';
|
||||
import { UseBreakpointExample } from './components';
|
||||
import { useBreakpointReturnDefs } from './props-definition';
|
||||
import { useBreakpointExampleSnippet } from './snippets';
|
||||
@@ -14,6 +15,11 @@ import { useBreakpointExampleSnippet } from './snippets';
|
||||
|
||||
## Usage
|
||||
|
||||
<Banner
|
||||
variant="warning"
|
||||
text="This hook has performance issues and should be used with caution."
|
||||
/>
|
||||
|
||||
The `useBreakpoint` hook returns the active breakpoint and two functions, `up` and `down`, to check if the viewport is above, or below a given breakpoint, letting you adjust your UI responsively.
|
||||
|
||||
<Snippet
|
||||
@@ -39,4 +45,4 @@ The default breakpoints are:
|
||||
|
||||
<PropsTable data={useBreakpointReturnDefs} isHook />
|
||||
|
||||
<ChangelogComponent component="use-breakpoint" />
|
||||
<ChangelogComponent hook="use-breakpoint" />
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
'use client';
|
||||
import { useMediaQuery } from '@backstage/ui/src/hooks/useMediaQuery';
|
||||
import './example.css';
|
||||
|
||||
export function UseMediaQueryThemeExample() {
|
||||
const isDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
|
||||
|
||||
return (
|
||||
<p>{isDarkMode ? 'User prefers Dark mode' : 'User prefers Light mode'}</p>
|
||||
);
|
||||
}
|
||||
|
||||
export function UseMediaQueryResponsiveExample() {
|
||||
const isMobile = useMediaQuery('(max-width: 768px)');
|
||||
const isTablet = useMediaQuery('(min-width: 769px) and (max-width: 1024px)');
|
||||
const isDesktop = useMediaQuery('(min-width: 1025px)');
|
||||
|
||||
return (
|
||||
<p>
|
||||
{isMobile && '<MobileLayout />'}
|
||||
{isTablet && '<TabletLayout />'}
|
||||
{isDesktop && '<DesktopLayout />'}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
export function UseMediaQueryPreferencesExample() {
|
||||
const prefersReducedMotion = useMediaQuery(
|
||||
'(prefers-reduced-motion: reduce)',
|
||||
);
|
||||
|
||||
return (
|
||||
<p className={prefersReducedMotion ? 'no-animations' : 'animated-border'}>
|
||||
Content
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
export function UseMediaQueryOrientationExample() {
|
||||
const isPortrait = useMediaQuery('(orientation: portrait)');
|
||||
|
||||
return <p>{isPortrait ? 'Portrait mode' : 'Landscape mode'}</p>;
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
@keyframes borderSlide {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(-90%);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(190%);
|
||||
}
|
||||
}
|
||||
|
||||
.animated-border {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.animated-border::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 50%;
|
||||
height: 2px;
|
||||
background-color: currentColor;
|
||||
animation: borderSlide 5s ease-in-out infinite;
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
import { ChangelogComponent } from '@/components/ChangelogComponent';
|
||||
import { CodeBlock } from '@/components/CodeBlock';
|
||||
import { PageTitle } from '@/components/PageTitle';
|
||||
import { PropsTable } from '@/components/PropsTable';
|
||||
import { Snippet } from '@/components/Snippet';
|
||||
import { Banner } from '@/components/Banner';
|
||||
import {
|
||||
UseMediaQueryOrientationExample,
|
||||
UseMediaQueryPreferencesExample,
|
||||
UseMediaQueryResponsiveExample,
|
||||
UseMediaQueryThemeExample,
|
||||
} from './components';
|
||||
import {
|
||||
useMediaQueryParamDefs,
|
||||
useMediaQueryReturnDefs,
|
||||
} from './props-definition';
|
||||
import {
|
||||
useMediaQueryOrientationSnippet,
|
||||
useMediaQueryPreferencesSnippet,
|
||||
useMediaQueryResponsiveSnippet,
|
||||
useMediaQueryUsageSnippet,
|
||||
} from './snippets';
|
||||
|
||||
<PageTitle
|
||||
title="useMediaQuery"
|
||||
description="Hook to evaluate and react to CSS media queries programmatically."
|
||||
/>
|
||||
|
||||
<Banner
|
||||
variant="warning"
|
||||
text="This hook is currently intended for internal use within the design system."
|
||||
/>
|
||||
|
||||
## Usage
|
||||
|
||||
The `useMediaQuery` hook allows you to evaluate CSS media queries in your components, enabling responsive design and behavior.
|
||||
|
||||
<Snippet
|
||||
align="center"
|
||||
py={4}
|
||||
height={150}
|
||||
open={true}
|
||||
preview={<UseMediaQueryThemeExample />}
|
||||
code={useMediaQueryUsageSnippet}
|
||||
/>
|
||||
|
||||
## Parameters
|
||||
|
||||
<PropsTable data={useMediaQueryParamDefs} isHook />
|
||||
|
||||
## Return Value
|
||||
|
||||
<PropsTable data={useMediaQueryReturnDefs} isHook />
|
||||
## Examples
|
||||
|
||||
### Responsive Layouts
|
||||
|
||||
Adapt layout based on different screen sizes.
|
||||
|
||||
<Snippet
|
||||
align="center"
|
||||
py={4}
|
||||
height={200}
|
||||
open={true}
|
||||
preview={<UseMediaQueryResponsiveExample />}
|
||||
code={useMediaQueryResponsiveSnippet}
|
||||
/>
|
||||
|
||||
### User Preferences
|
||||
|
||||
Respect user system preferences.
|
||||
|
||||
<Snippet
|
||||
align="center"
|
||||
py={4}
|
||||
height={150}
|
||||
open={true}
|
||||
preview={<UseMediaQueryPreferencesExample />}
|
||||
code={useMediaQueryPreferencesSnippet}
|
||||
/>
|
||||
|
||||
### Device Orientation
|
||||
|
||||
Detect screen orientation.
|
||||
|
||||
<Snippet
|
||||
align="center"
|
||||
py={4}
|
||||
height={150}
|
||||
open={true}
|
||||
preview={<UseMediaQueryOrientationExample />}
|
||||
code={useMediaQueryOrientationSnippet}
|
||||
/>
|
||||
|
||||
<ChangelogComponent component="use-media-query" />
|
||||
@@ -1,36 +0,0 @@
|
||||
import { type PropDef } from '@/utils/propDefs';
|
||||
|
||||
export const useMediaQueryParamDefs: Record<string, PropDef> = {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'The CSS media query to evaluate',
|
||||
},
|
||||
options: {
|
||||
type: 'complex',
|
||||
complexType: {
|
||||
name: 'UseMediaQueryOptions',
|
||||
properties: {
|
||||
defaultValue: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
description:
|
||||
'Default value to use when rendering on the server, defaults to false',
|
||||
},
|
||||
initializeWithValue: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
description:
|
||||
'Whether to initialize with the current value of the media query, or use the default value initially',
|
||||
},
|
||||
},
|
||||
},
|
||||
default: 'defaultValue: false\ninitializeWithValue: true',
|
||||
},
|
||||
};
|
||||
|
||||
export const useMediaQueryReturnDefs: Record<string, PropDef> = {
|
||||
matches: {
|
||||
type: 'boolean',
|
||||
description: 'True if the media query currently matches',
|
||||
},
|
||||
};
|
||||
@@ -1,54 +0,0 @@
|
||||
export const useMediaQueryUsageSnippet = `import { useMediaQuery } from '@backstage/ui';
|
||||
|
||||
function MyComponent() {
|
||||
const isDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
|
||||
|
||||
return (
|
||||
<p>
|
||||
{isDarkMode
|
||||
? 'User prefers Dark mode'
|
||||
: 'User prefers Light mode'
|
||||
}
|
||||
</p>
|
||||
);
|
||||
}`;
|
||||
|
||||
export const useMediaQueryResponsiveSnippet = `import { useMediaQuery } from '@backstage/ui';
|
||||
|
||||
function ResponsiveLayout() {
|
||||
const isMobile = useMediaQuery('(max-width: 768px)');
|
||||
const isTablet = useMediaQuery('(min-width: 769px) and (max-width: 1024px)');
|
||||
const isDesktop = useMediaQuery('(min-width: 1025px)');
|
||||
|
||||
return (
|
||||
<p>
|
||||
{isMobile && <MobileLayout />}
|
||||
{isTablet && <TabletLayout />}
|
||||
{isDesktop && <DesktopLayout />}
|
||||
</p>
|
||||
);
|
||||
}`;
|
||||
|
||||
export const useMediaQueryPreferencesSnippet = `import { useMediaQuery } from '@backstage/ui';
|
||||
|
||||
function AccessibleComponent() {
|
||||
const prefersReducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)');
|
||||
|
||||
return (
|
||||
<p className={prefersReducedMotion ? 'no-animations' : 'animated-border'}>
|
||||
Content
|
||||
</p>
|
||||
);
|
||||
}`;
|
||||
|
||||
export const useMediaQueryOrientationSnippet = `import { useMediaQuery } from '@backstage/ui';
|
||||
|
||||
function OrientationAware() {
|
||||
const isPortrait = useMediaQuery('(orientation: portrait)');
|
||||
|
||||
return (
|
||||
<p>
|
||||
{isPortrait ? 'Portrait mode' : 'Landscape mode'}
|
||||
</p>
|
||||
);
|
||||
}`;
|
||||
@@ -1,62 +0,0 @@
|
||||
import { ChangelogComponent } from '@/components/ChangelogComponent';
|
||||
import { CodeBlock } from '@/components/CodeBlock';
|
||||
import { Banner } from '@/components/Banner';
|
||||
import { PageTitle } from '@/components/PageTitle';
|
||||
import { PropsTable } from '@/components/PropsTable';
|
||||
import {
|
||||
useStylesUsageSnippet,
|
||||
useStylesDefsSnippet,
|
||||
useStylesAttributesSnippet,
|
||||
} from './snippets';
|
||||
import { useStylesPropsDefs, useStylesReturnDefs } from './props-definition';
|
||||
|
||||
<PageTitle
|
||||
title="useStyles"
|
||||
description="React hook to get class names and data attributes for a component with responsive support"
|
||||
/>
|
||||
|
||||
<Banner
|
||||
variant="warning"
|
||||
text="This hook is currently intended for internal use within the design system."
|
||||
/>
|
||||
|
||||
## Usage
|
||||
|
||||
The `useStyles` hook is the core styling utility that processes component definitions and props to generate:
|
||||
|
||||
- **Class names**
|
||||
- **Data attributes** for state-based styling (e.g., `data-disabled`, `data-variant`)
|
||||
- **Utility classes** from utility props like `padding`, `margin`, `display` (with responsive support)
|
||||
- **CSS custom properties** for custom values not in predefined utility lists
|
||||
- **Cleaned props** with utility props removed (safe to spread onto DOM elements)
|
||||
|
||||
<CodeBlock code={useStylesUsageSnippet} />
|
||||
|
||||
## Parameters
|
||||
|
||||
<PropsTable data={useStylesPropsDefs} isHook />
|
||||
|
||||
## Return Value
|
||||
|
||||
<PropsTable data={useStylesReturnDefs} isHook />
|
||||
|
||||
## How It Works
|
||||
|
||||
### Component Definition
|
||||
|
||||
A component definition describes how a component should be styled:
|
||||
|
||||
<CodeBlock code={useStylesDefsSnippet} />
|
||||
|
||||
<Banner
|
||||
variant="info"
|
||||
text="The data-attributes definition should be kebab-case instead of camelCase."
|
||||
/>
|
||||
|
||||
### Data Attributes
|
||||
|
||||
Data attributes enable state-based styling through CSS selectors. The hook automatically adds `data-*` to the attributes based on the defined props:
|
||||
|
||||
<CodeBlock code={useStylesAttributesSnippet} />
|
||||
|
||||
<ChangelogComponent component="use-styles" />
|
||||
@@ -1,58 +0,0 @@
|
||||
import { type PropDef } from '@/utils/propDefs';
|
||||
|
||||
export const useStylesPropsDefs: Record<string, PropDef> = {
|
||||
componentDefinition: {
|
||||
type: 'complex',
|
||||
complexType: {
|
||||
name: 'ComponentDefinition',
|
||||
properties: {
|
||||
classNames: {
|
||||
type: 'Record<string, string>',
|
||||
required: true,
|
||||
},
|
||||
dataAttributes: {
|
||||
type: 'Record<string, readonly (string | number | boolean)[]>',
|
||||
required: false,
|
||||
},
|
||||
utilityProps: {
|
||||
type: 'string[]',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
props: {
|
||||
type: 'enum',
|
||||
description: 'All component props',
|
||||
values: ['{ [key: string]: any; }'],
|
||||
},
|
||||
};
|
||||
|
||||
export const useStylesReturnDefs: Record<string, PropDef> = {
|
||||
classNames: {
|
||||
type: 'enum',
|
||||
values: ['Record<string, string>'],
|
||||
description:
|
||||
"The component's class names mapped to their style definitions",
|
||||
},
|
||||
dataAttributes: {
|
||||
type: 'enum',
|
||||
values: ['Record<string, string>'],
|
||||
description:
|
||||
'Data attributes generated from the component definition and props',
|
||||
},
|
||||
utilityClasses: {
|
||||
type: 'string',
|
||||
description: 'Combined utility classes based on utility props',
|
||||
},
|
||||
style: {
|
||||
type: 'enum',
|
||||
values: ['React.CSSProperties'],
|
||||
description: 'The combined style object for the component',
|
||||
},
|
||||
cleanedProps: {
|
||||
type: 'enum',
|
||||
values: ['Record<string, any>'],
|
||||
description: 'The original props with utility props removed',
|
||||
},
|
||||
};
|
||||
@@ -1,105 +0,0 @@
|
||||
export const useStylesUsageSnippet = `import { useStyles } from '@backstage/ui';
|
||||
import type {
|
||||
ComponentDefinition,
|
||||
SpaceProps,
|
||||
UtilityProps,
|
||||
} from '@backstage/ui';
|
||||
|
||||
// Define your component's styling configuration
|
||||
const buttonDefinition: ComponentDefinition = {
|
||||
classNames: {
|
||||
root: 'bui-button',
|
||||
icon: 'bui-button-icon',
|
||||
},
|
||||
dataAttributes: {
|
||||
variant: ['primary', 'secondary', 'ghost'] as const,
|
||||
size: ['small', 'medium', 'large'] as const,
|
||||
'is-disabled': [true, false] as const,
|
||||
},
|
||||
utilityProps: ['gap', 'mb', 'mt', 'ml', 'mr'],
|
||||
} as const satisfies ComponentDefinition;
|
||||
|
||||
// type the component props
|
||||
interface ButtonProps {
|
||||
variant?: 'primary' | 'secondary' | 'ghost';
|
||||
size?: 'small' | 'medium' | 'large';
|
||||
isDisabled?: boolean;
|
||||
gap?: UtilityProps['gap'];
|
||||
mb?: SpaceProps['mb'];
|
||||
mt?: SpaceProps['mt'];
|
||||
ml?: SpaceProps['ml'];
|
||||
mr?: SpaceProps['mr'];
|
||||
icon?: React.ReactElement;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
// Use the useStyles hook in your component
|
||||
export function Button({
|
||||
variant = 'primary',
|
||||
size = 'medium',
|
||||
isDisabled,
|
||||
...props
|
||||
}: Readonly<ButtonProps>) {
|
||||
const { classNames, dataAttributes, utilityClasses, cleanedProps } =
|
||||
useStyles(buttonDefinition, {
|
||||
variant,
|
||||
size,
|
||||
'is-disabled': isDisabled,
|
||||
...props,
|
||||
});
|
||||
|
||||
return (
|
||||
<button
|
||||
className={\`\${classNames.root} \${utilityClasses}\`}
|
||||
{...dataAttributes}
|
||||
{...cleanedProps}
|
||||
>
|
||||
{props.icon && <span className={classNames.icon}>{props.icon}</span>}
|
||||
{props.children}
|
||||
</button>
|
||||
);
|
||||
}`;
|
||||
|
||||
export const useStylesDefsSnippet = `const componentDefinition = {
|
||||
// CSS class names for component parts
|
||||
classNames: {
|
||||
root: 'bui-button',
|
||||
icon: 'bui-button-icon',
|
||||
},
|
||||
|
||||
// Props that become data attributes for CSS targeting
|
||||
// data-attribute must be kebab-case to respect HTML standards
|
||||
// And casing matters here, lowercase only:
|
||||
// 'variant' not 'Variant', 'is-disabled' not 'isDisabled'..
|
||||
dataAttributes: {
|
||||
variant: ['primary', 'secondary'] as const,
|
||||
size: ['small', 'large'] as const,
|
||||
'is-disabled': [true, false] as const,
|
||||
},
|
||||
|
||||
// Props that become utility classes (spacing, layout, etc.)
|
||||
utilityProps: ['m', 'mb', 'mt', 'ml', 'mr'],
|
||||
};`;
|
||||
|
||||
export const useStylesAttributesSnippet = `/* Component CSS can use data attributes for variants */
|
||||
.bui-button {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
/* Variant styles */
|
||||
.bui-button[data-variant="primary"] {
|
||||
background: blue;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.bui-button[data-variant="secondary"] {
|
||||
background: gray;
|
||||
color: black;
|
||||
}
|
||||
|
||||
/* State styles */
|
||||
.bui-button[data-is-disabled="true"] {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}`;
|
||||
Reference in New Issue
Block a user