Merge pull request #28570 from backstage/canon-fix-responsive

Canon - Fix responsive capacity
This commit is contained in:
Charles de Dreuille
2025-01-21 13:20:46 +00:00
committed by GitHub
10 changed files with 124 additions and 94 deletions
+12 -12
View File
@@ -29,43 +29,43 @@ const preview: Preview = {
},
viewport: {
viewports: {
xs: {
name: 'XSmall',
initial: {
name: 'Initial',
styles: {
width: '320px',
height: '100%',
},
},
small: {
name: 'Small',
xs: {
name: 'Extra Small',
styles: {
width: '640px',
height: '100%',
},
},
medium: {
name: 'Medium',
sm: {
name: 'Small',
styles: {
width: '768px',
height: '100%',
},
},
large: {
name: 'Large',
md: {
name: 'Medium',
styles: {
width: '1024px',
height: '100%',
},
},
xlarge: {
name: 'XLarge',
lg: {
name: 'Large',
styles: {
width: '1280px',
height: '100%',
},
},
'2xl': {
name: '2XL',
xl: {
name: 'Extra Large',
styles: {
width: '1536px',
height: '100%',
+1 -7
View File
@@ -68,7 +68,7 @@ export interface BoxProps extends UtilityProps {
}
// @public (undocumented)
export type Breakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';
export type Breakpoint = 'initial' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
// @public (undocumented)
export const Button: React_2.ForwardRefExoticComponent<
@@ -101,12 +101,6 @@ export interface ButtonProps {
// @public (undocumented)
export interface CanonContextProps {
// (undocumented)
breakpoint: Breakpoint;
// (undocumented)
getResponsiveValue: (
value: string | Partial<Record<Breakpoint, string>>,
) => string;
// (undocumented)
icons: IconMap;
}
@@ -114,7 +114,7 @@ export const Responsive: Story = {
args: {
children: 'Button',
variant: {
xs: 'primary',
initial: 'primary',
sm: 'secondary',
md: 'tertiary',
},
@@ -18,9 +18,10 @@
import React, { forwardRef } from 'react';
import { Icon } from '../Icon';
import { ButtonProps } from './types';
import { useCanon } from '../../contexts/canon';
import clsx from 'clsx';
import { useResponsiveValue } from '../../hooks/useResponsiveValue';
import type { ButtonProps } from './types';
/** @public */
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
@@ -37,11 +38,9 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
...rest
} = props;
const { getResponsiveValue } = useCanon();
// Get the responsive value for the variant
const responsiveSize = getResponsiveValue(size);
const responsiveVariant = getResponsiveValue(variant);
const responsiveSize = useResponsiveValue(size);
const responsiveVariant = useResponsiveValue(variant);
return (
<button
@@ -17,9 +17,10 @@
'use client';
import React, { forwardRef } from 'react';
import { HeadingProps } from './types';
import { useCanon } from '../../contexts/canon';
import clsx from 'clsx';
import { useResponsiveValue } from '../../hooks/useResponsiveValue';
import type { HeadingProps } from './types';
/** @public */
export const Heading = forwardRef<HTMLHeadingElement, HeadingProps>(
@@ -31,10 +32,9 @@ export const Heading = forwardRef<HTMLHeadingElement, HeadingProps>(
className,
...restProps
} = props;
const { getResponsiveValue } = useCanon();
// Get the responsive value for the variant
const responsiveVariant = getResponsiveValue(variant);
const responsiveVariant = useResponsiveValue(variant);
// Determine the component to render based on the variant
let Component = as;
+6 -6
View File
@@ -17,9 +17,11 @@
'use client';
import React, { forwardRef } from 'react';
import { TextProps } from './types';
import { useCanon } from '../../contexts/canon';
import { useResponsiveValue } from '../../hooks/useResponsiveValue';
import clsx from 'clsx';
import type { TextProps } from './types';
/** @public */
export const Text = forwardRef<HTMLParagraphElement, TextProps>(
(props, ref) => {
@@ -32,11 +34,9 @@ export const Text = forwardRef<HTMLParagraphElement, TextProps>(
...restProps
} = props;
const { getResponsiveValue } = useCanon();
// Get the responsive values for the variant and weight
const responsiveVariant = getResponsiveValue(variant);
const responsiveWeight = getResponsiveValue(weight);
const responsiveVariant = useResponsiveValue(variant);
const responsiveWeight = useResponsiveValue(weight);
return (
<p
+7 -57
View File
@@ -16,33 +16,25 @@
'use client';
import React, { createContext, useContext, ReactNode } from 'react';
import { IconMap, IconNames } from '../components/Icon/types';
import React, { createContext, ReactNode, useContext } from 'react';
import { icons } from '../components/Icon/icons';
import { useMediaQuery } from '../hooks/useMediaQuery';
import type { Breakpoint } from '../types';
import { IconMap, IconNames } from '../components/Icon/types';
/** @public */
export interface CanonContextProps {
icons: IconMap;
breakpoint: Breakpoint;
getResponsiveValue: (
value: string | Partial<Record<Breakpoint, string>>,
) => string;
}
const CanonContext = createContext<CanonContextProps>({
icons,
breakpoint: 'md',
getResponsiveValue: () => '',
});
/** @public */
export interface CanonProviderProps {
children?: ReactNode;
overrides?: Partial<Record<IconNames, React.ComponentType>>;
}
const CanonContext = createContext<CanonContextProps>({
icons,
});
/** @public */
export const CanonProvider = (props: CanonProviderProps) => {
const { children, overrides } = props;
@@ -50,50 +42,8 @@ export const CanonProvider = (props: CanonProviderProps) => {
// Merge provided overrides with default icons
const combinedIcons = { ...icons, ...overrides };
const isBreakpointSm = useMediaQuery(`(min-width: 640px)`);
const isBreakpointMd = useMediaQuery(`(min-width: 768px)`);
const isBreakpointLg = useMediaQuery(`(min-width: 1024px)`);
const isBreakpointXl = useMediaQuery(`(min-width: 1280px)`);
const isBreakpoint2xl = useMediaQuery(`(min-width: 1536px)`);
// Determine the current breakpoint
const breakpoint = (() => {
if (isBreakpoint2xl) return '2xl';
if (isBreakpointXl) return 'xl';
if (isBreakpointLg) return 'lg';
if (isBreakpointMd) return 'md';
if (isBreakpointSm) return 'sm';
return 'xs';
})();
const getResponsiveValue = (
value: string | Partial<Record<Breakpoint, string>>,
) => {
if (typeof value === 'object') {
const breakpointsOrder: Breakpoint[] = [
'xs',
'sm',
'md',
'lg',
'xl',
'2xl',
];
const index = breakpointsOrder.indexOf(breakpoint);
for (let i = index; i >= 0; i--) {
if (value[breakpointsOrder[i]]) {
return value[breakpointsOrder[i]] as string;
}
}
return value['xs'] as string;
}
return value;
};
return (
<CanonContext.Provider
value={{ icons: combinedIcons, breakpoint, getResponsiveValue }}
>
<CanonContext.Provider value={{ icons: combinedIcons }}>
{children}
</CanonContext.Provider>
);
+42
View File
@@ -0,0 +1,42 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useMediaQuery } from './useMediaQuery';
import type { Breakpoint } from '../types';
export const breakpoints: { name: string; id: Breakpoint; value: number }[] = [
{ name: 'Initial', id: 'initial', value: 0 },
{ name: 'Extra Small', id: 'xs', value: 640 },
{ name: 'Small', id: 'sm', value: 768 },
{ name: 'Medium', id: 'md', value: 1024 },
{ name: 'Large', id: 'lg', value: 1280 },
{ name: 'Extra Large', id: 'xl', value: 1536 },
];
export const useBreakpoint = (): Breakpoint => {
// TODO: Perhaps refactor for useMediaQuery to accept an array of queries
const matches = breakpoints.map(breakpoint => {
const match = useMediaQuery(`(min-width: ${breakpoint.value}px)`);
return match;
});
for (let i = matches.length - 1; i >= 0; i--) {
if (matches[i]) {
return breakpoints[i].id;
}
}
return breakpoints[0].id;
};
@@ -0,0 +1,45 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Breakpoint } from '../types';
import { useBreakpoint, breakpoints } from './useBreakpoint';
type ResponsiveValue = string | Partial<Record<Breakpoint, string>>;
export const useResponsiveValue = (value: ResponsiveValue) => {
const currentBreakpoint = useBreakpoint();
if (typeof value === 'object') {
const index = breakpoints.findIndex(
breakpoint => breakpoint.id === currentBreakpoint,
);
for (let i = index; i >= 0; i--) {
if (value[breakpoints[i].id]) {
return value[breakpoints[i].id] as string;
}
}
// If no value is found for the current or smaller breakpoints, check from the smallest
for (let i = 0; i < breakpoints.length; i++) {
if (value[breakpoints[i].id]) {
return value[breakpoints[i].id] as string;
}
}
}
return value;
};
+1 -1
View File
@@ -34,7 +34,7 @@ export type AsProps =
| 'dt';
/** @public */
export type Breakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';
export type Breakpoint = 'initial' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
/** @public */
export type Space = 'none' | '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';