Skip to content
Merged
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
3 changes: 3 additions & 0 deletions src/molecules/Alert/Alert.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { forwardRef } from 'react';
import { AlertStyled } from './styles';
import { RdAlertComponent, RdAlertCompoundedComponent } from './types';
import { AlertErrorBoundary } from './ErrorBoundary';

export const InternalAlert: RdAlertComponent = forwardRef((props, ref) => {
return <AlertStyled {...props} ref={ref} />;
});

export const Alert: RdAlertCompoundedComponent = InternalAlert as RdAlertCompoundedComponent;

Alert.ErrorBoundary = AlertErrorBoundary;
33 changes: 33 additions & 0 deletions src/molecules/Alert/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react';
import { AlertErrorBoundaryStyled } from './styles';
import {
RdAlertErrorBoundaryComponent,
RdAlertErrorBoundaryProps,
RdAlertErrorBoundaryStates,
} from './types';

export class AlertErrorBoundary
extends React.Component<RdAlertErrorBoundaryProps, RdAlertErrorBoundaryStates>
implements RdAlertErrorBoundaryComponent
{
state: RdAlertErrorBoundaryStates = {
error: undefined,
info: {
componentStack: '',
},
};

componentDidCatch(error: Error | null, info: object): void {
// update state with error + stack trace from Antd
this.setState({
error,
info: {
componentStack: (info as any).componentStack ?? '',
},
});
}

render() {
return <AlertErrorBoundaryStyled {...this.props} />;
}
}
5 changes: 5 additions & 0 deletions src/molecules/Alert/styles.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import styled from '@emotion/styled';
import { Alert } from 'antd';
import { RdAlertErrorBoundaryComponent } from './types';

export const AlertStyled = styled(Alert)``;

export const AlertErrorBoundaryStyled = styled(
Alert.ErrorBoundary as typeof RdAlertErrorBoundaryComponent
)``;
68 changes: 64 additions & 4 deletions src/molecules/Alert/types.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,61 @@
import { Alert, GetProps } from 'antd';
import { AlertRef } from 'antd/es/alert/Alert';
import { ComponentToken as AlertComponentTokenAntd } from 'antd/es/drawer/style';
import { ComponentProps } from 'react';
import React, { ComponentProps } from 'react';

type AlertPropsAntd = GetProps<typeof Alert>;
type AlertErrorBoundaryPropsAntd = GetProps<typeof Alert.ErrorBoundary>;

/**
* 📝 Explanation:
*
* For most Ant Design components, we can simply get the props type using:
* type XxxPropsAntd = GetProps<typeof Xxx>;
*
* However, `Alert.ErrorBoundary` is a special case:
* - Ant Design does NOT export a public props interface for ErrorBoundary.
* - If we directly use `GetProps<typeof Alert.ErrorBoundary>`,
* TypeScript will try to expose internal types and throw an error:
* ts(4023): "Exported variable '...' has or is using name '...' but cannot be named".
*
* 🚑 Workaround:
* - Use `ComponentProps<typeof Alert.ErrorBoundary>` to get the props,
* then clone it via a mapped type to produce a "public" type.
* - This way we avoid referencing Ant Design’s internal types.
*
* 🔮 Note:
* - If Ant Design exports a proper `AlertErrorBoundaryProps` type in the future,
* we can safely remove this workaround and write:
* type AlertErrorBoundaryPropsAntd = GetProps<typeof Alert.ErrorBoundary>;
*/
type AlertErrorBoundaryPropsAntd = {
[K in keyof ComponentProps<typeof Alert.ErrorBoundary>]: ComponentProps<
typeof Alert.ErrorBoundary
>[K];
};

/**
* 📝 Explanation:
*
* Ant Design also does not export the `ErrorBoundaryStates` type.
* Therefore, instead of extracting it automatically, we need to define this interface manually.
*
* The state shape is referenced from the source code:
* state: {
* error: undefined;
* info: {
* componentStack: string;
* };
* }
*
* This is simply a copied definition to ensure type safety for our extended components.
* If Ant Design exports `ErrorBoundaryStates` in the future, we should switch to that instead.
*/
interface AlertErrorBoundaryStatesAntd {
error?: Error | null;
info?: {
componentStack?: string;
};
}
type AlertRefAntd = AlertRef;

//#endregion
Expand All @@ -17,24 +67,34 @@ type AlertComponentTokenExtend = {};
//#region Define extended types
type AlertPropsExtend = {};
type AlertErrorBoundaryPropsExtend = {};
type AlertErrorBoundaryStatesExtend = {};

type AlertRefExtend = {};
//#endregion

//#region Export types
export type RdAlertProps = AlertPropsAntd & AlertPropsExtend;
export type RdAlertErrorBoundaryProps = AlertErrorBoundaryPropsAntd & AlertErrorBoundaryPropsExtend;
export type RdAlertErrorBoundaryStates = AlertErrorBoundaryStatesAntd &
AlertErrorBoundaryStatesExtend;
export type RdAlertRef = AlertRefAntd & AlertRefExtend;
export type RdAlertComponentToken = AlertComponentTokenAntd & AlertComponentTokenExtend;
//#endregion

//#region Define component types
// export type RdAlertComponent = React.FC<RdAlertProps>;
export type RdAlertComponent = React.ForwardRefExoticComponent<
RdAlertProps & React.RefAttributes<RdAlertRef>
>;
export declare class RdAlertErrorBoundaryComponent extends React.Component<
RdAlertErrorBoundaryProps,
RdAlertErrorBoundaryStates
> {
state: RdAlertErrorBoundaryStates;
componentDidCatch(error: Error | null, info: object): void;
render(): React.ReactNode;
}

export type RdAlertCompoundedComponent = RdAlertComponent & {
// Timer: RdAlertErrorBoundaryComponent;
ErrorBoundary: typeof RdAlertErrorBoundaryComponent;
};
//#endregion
15 changes: 15 additions & 0 deletions src/molecules/Segmented/Segmented.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useMemo } from 'react';
import { SegmentedStyledFunc } from './styles';
import { RdSegmentedComponent, RdSegmentedProps } from './types';

export const InternalSegmented = <ValueType extends string | number>(
props: RdSegmentedProps<ValueType>
) => {
const SegmentedStyled = useMemo(() => {
return SegmentedStyledFunc<ValueType>();
}, [SegmentedStyledFunc]);

return <SegmentedStyled {...props} />;
};

export const Segmented = InternalSegmented as RdSegmentedComponent;
2 changes: 2 additions & 0 deletions src/molecules/Segmented/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { Segmented } from './Segmented';
export * from './types';
7 changes: 7 additions & 0 deletions src/molecules/Segmented/styles.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import styled from '@emotion/styled';
import { Segmented } from 'antd';

export const SegmentedStyles = styled(Segmented)``;

export const SegmentedStyledFunc = <ValueType extends string | number>() =>
styled(Segmented<ValueType>)``;
30 changes: 30 additions & 0 deletions src/molecules/Segmented/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { GetProps, Segmented } from 'antd';
import { ComponentToken as SegmentedComponentTokenAntd } from 'antd/es/affix/style';
import { InternalSegmented } from './Segmented';

//#region Define Ant Design types
type SegmentedPropsAntd<ValueType> = GetProps<typeof Segmented<ValueType>>;
//#endregion

//#region Define extended component tokens
type SegmentedComponentTokenExtend = {};
//#endregion

//#region Define extended types
type SegmentedPropsExtend = {};

type SegmentedRefExtend = {};
//#endregion

//#region Export types
export type RdSegmentedProps<ValueType> = SegmentedPropsAntd<ValueType> & SegmentedPropsExtend;

export type RdSegmentedComponentToken = SegmentedComponentTokenAntd & SegmentedComponentTokenExtend;
//#endregion

//#region Define component types
export type RdSegmentedComponent = (<ValueType>(
props: RdSegmentedProps<ValueType> & React.RefAttributes<HTMLDivElement>
) => ReturnType<typeof InternalSegmented>) &
Pick<React.FC, 'displayName'>;
//#endregion
13 changes: 12 additions & 1 deletion src/molecules/Select/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,18 @@ type SelectPropsExtend = {
type SelectOptionPropsExtend = {};
type SelectOptionGroupPropsExtend = {};
type BaseOptionTypeExtend = {};
type DefaultOptionTypeExtend = {};
type DefaultOptionTypeExtend = {
/**
* Extends Ant Design's DefaultOptionType.
*
* Reason:
* - Ant Design's <Select> supports nested `options`, but the official `DefaultOptionType`
* type does not include this property.
* - Added here for type-safety when building hierarchical option trees.
* - Temporary workaround: can be removed if Ant Design updates DefaultOptionType to include it.
*/
options?: RdDefaultOptionType[];
};
//#endregion

//#region Export types
Expand Down
1 change: 1 addition & 0 deletions src/molecules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export * from './Popover';
export * from './Progress';
export * from './Radio';
export * from './Result';
export * from './Segmented';
export * from './Select';
export * from './Skeleton';
export * from './Slider';
Expand Down
2 changes: 1 addition & 1 deletion src/templates/DashboardTemplate/DashboardTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const DashboardTemplateInternal: RdDashboardTemplateComponent = forwardRef((prop
</Layout>
</DashboardTemplateStyles>
);

const renderContentHeightLayout = () => (
<DashboardTemplateStyles ref={ref} fitScreen={fitScreen} {...restProps}>
{headerProps && (
Expand Down
68 changes: 50 additions & 18 deletions src/templates/DashboardTemplate/Sider/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const DashboardTemplateSider: RdDashboardTemplateSiderComponent = forward
const {
children,
breakpoint = 'lg',
fixedSiderBreakpoint = 'md',
sidebarMode = 'fullHeight',
headerSidebar,
collapsed,
Expand All @@ -23,33 +24,56 @@ export const DashboardTemplateSider: RdDashboardTemplateSiderComponent = forward
toggleSider,
...restProps
} = props;
const [fixed, setFixed] = useState<boolean>(false);

/**
* `fixedSider` state:
* - true → sidebar is displayed as an overlay (does not take layout width).
* - false → sidebar is displayed normally and occupies width.
*/
const [fixedSider, setFixedSider] = useState<boolean>(false);
const siderWidth = getComponentToken('DashboardTemplate', 'siderWidth');

/**
* collapsedWidth:
* - When collapsed in "normal mode" → width = collapsedWidth (e.g. 80px).
* - When collapsed in "fixed overlay mode" → width = 0 (sider overlays content).
*/
const [collapsedWith, setCollapsedWidth] = useState<number | string>(
DEFAULT_COLLAPSED_WIDTH
);

/**
* Two behaviors for the sidebar:
*
* 1. Collapse at breakpoint:
* - Handled by Ant Design using the `breakpoint` prop (e.g., "lg").
* - When screen < breakpoint, the sidebar collapses but still occupies width.
*
* 2. Fixed overlay at fixedSiderBreakpoint:
* - Custom logic in this component.
* - When screen < fixedSiderBreakpoint (e.g., "md"), the sidebar becomes fixed overlay.
*
* => Summary:
* - Antd handles collapsing into a narrow-width sidebar.
* - This component handles switching to fixed overlay on smaller screens.
*/
if (render) {
return render({ children, ...restProps });
return render({ children, fixedSiderBreakpoint, ...restProps });
}

const screens = Grid.useBreakpoint();

useEffect(() => {
// On smaller screens, we want the sidebar to be fixed and overlay the content
if (!screens.lg && sidebarMode === 'fullHeight') {
setFixed(true);
// On smaller screens: switch to fixed overlay mode
if (!screens[fixedSiderBreakpoint]) {
setFixedSider(true);

if (sidebarMode === 'fullHeight') {
setCollapsedWidth(0);
} else {
setCollapsedWidth(DEFAULT_COLLAPSED_WIDTH);
}
setCollapsedWidth(0);
} else {
setFixed(false);
setFixedSider(false);
setCollapsedWidth(DEFAULT_COLLAPSED_WIDTH);
}
}, [screens.lg]);
}, [screens[fixedSiderBreakpoint]]);

return (
<DashboardTemplateSiderStyles
Expand All @@ -60,16 +84,24 @@ export const DashboardTemplateSider: RdDashboardTemplateSiderComponent = forward
collapsed={collapsed}
onCollapse={toggleSider}
breakpoint={breakpoint}
fixed={fixed}
fixed={fixedSider}
fixedSiderBreakpoint={fixedSiderBreakpoint}
sidebarMode={sidebarMode}
fixedOnScroll={fixedOnScroll}
{...restProps}
>
{sidebarMode === 'fullHeight' && (
<DashboardTemplateHeaderSidebarStyled fixedOnScroll={fixedHeaderOnScroll}>
{headerSidebar?.(collapsed ?? false)}
</DashboardTemplateHeaderSidebarStyled>
)}
{/*
Sidebar Header:

- Always renders `headerSidebar` (for both "contentHeight" and "fullHeight" modes).
- The `headerSidebar` function receives the `collapsed` state
so the parent can decide whether to render Logo, Favicon, or other variations.
- For "contentHeight": when the screen shrinks into fixed overlay mode,
the headerSidebar still renders (at this point, the sider behaves like fullHeight).
*/}
<DashboardTemplateHeaderSidebarStyled fixedOnScroll={fixedHeaderOnScroll}>
{headerSidebar?.(collapsed ?? false)}
</DashboardTemplateHeaderSidebarStyled>

<DashboardTemplateSiderContentStyled>
{children}
Expand Down
Loading