Skip to content

Commit d410271

Browse files
committed
refactor closable handling
1 parent 0204703 commit d410271

File tree

5 files changed

+95
-27
lines changed

5 files changed

+95
-27
lines changed

Agent.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Agent Rules
2+
3+
## Refactor
4+
5+
- Do not modify any files under `src/legacy` when handling refactor requests unless the user explicitly asks for legacy changes.
6+
- For refactor requests, do not run tests by default. Only run tests if the user explicitly asks for them or the task clearly requires verification.

src/Notification.tsx

Lines changed: 17 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import * as React from 'react';
22
import { clsx } from 'clsx';
3-
import pickAttrs from '@rc-component/util/lib/pickAttrs';
43
import useNoticeTimer from './hooks/useNoticeTimer';
54
import { useEvent } from '@rc-component/util';
5+
import useClosable, { type ClosableType } from './hooks/useClosable';
66

77
export interface NotificationClassNames {
88
wrapper?: string;
@@ -32,9 +32,7 @@ export interface NotificationProps {
3232
content?: React.ReactNode;
3333
actions?: React.ReactNode;
3434
close?: React.ReactNode;
35-
closable?:
36-
| boolean
37-
| ({ closeIcon?: React.ReactNode; onClose?: VoidFunction } & React.AriaAttributes);
35+
closable?: ClosableType;
3836
offset?: {
3937
x: number;
4038
y: number;
@@ -90,36 +88,24 @@ const Notification = React.forwardRef<HTMLDivElement, NotificationProps>((props,
9088

9189
// ========================= Close ==========================
9290
const onEventClose = useEvent(onClose);
93-
const offsetRef = React.useRef(offset);
94-
const closableObj = React.useMemo(() => {
95-
if (typeof closable === 'object' && closable !== null) {
96-
return closable;
97-
}
9891

99-
return {};
100-
}, [closable]);
101-
const closeContent = close === undefined ? (closableObj.closeIcon ?? 'x') : close;
102-
const mergedClosable = close !== undefined ? close !== null : !!closable;
103-
const ariaProps = pickAttrs(closableObj, true);
104-
105-
if (offset) {
106-
offsetRef.current = offset;
107-
}
92+
const [closableEnabled, closableConfig, closeBtnAriaProps] = useClosable(closable);
93+
const closeContent = close === undefined ? closableConfig.closeIcon : close;
94+
const mergedClosable = close !== undefined ? close !== null : closableEnabled;
10895

10996
// ======================== Duration ========================
11097
const [hovering, setHovering] = React.useState(false);
11198

11299
const [onResume, onPause] = useNoticeTimer(
113100
duration,
114101
() => {
115-
closableObj.onClose?.();
102+
closableConfig.onClose?.();
116103
onEventClose();
117104
},
118105
setPercent,
119106
!!showProgress,
120107
);
121108

122-
const mergedOffset = offset ?? offsetRef.current;
123109
const validPercent = 100 - Math.min(Math.max(percent * 100, 0), 100);
124110

125111
React.useEffect(() => {
@@ -151,6 +137,14 @@ const Notification = React.forwardRef<HTMLDivElement, NotificationProps>((props,
151137
onMouseLeave?.(event);
152138
}
153139

140+
// ======================== Position ========================
141+
const offsetRef = React.useRef(offset);
142+
if (offset) {
143+
offsetRef.current = offset;
144+
}
145+
146+
const mergedOffset = offset ?? offsetRef.current;
147+
154148
// ========================= Render =========================
155149
return (
156150
<div
@@ -186,18 +180,18 @@ const Notification = React.forwardRef<HTMLDivElement, NotificationProps>((props,
186180
<button
187181
className={clsx(`${noticePrefixCls}-close`, classNames?.close)}
188182
aria-label="Close"
189-
{...ariaProps}
183+
{...closeBtnAriaProps}
190184
style={styles?.close}
191185
onKeyDown={(event) => {
192186
if (event.key === 'Enter' || event.code === 'Enter') {
193-
closableObj.onClose?.();
187+
closableConfig.onClose?.();
194188
onEventClose();
195189
}
196190
}}
197191
onClick={(e) => {
198192
e.preventDefault();
199193
e.stopPropagation();
200-
closableObj.onClose?.();
194+
closableConfig.onClose?.();
201195
onEventClose();
202196
}}
203197
>

src/NotificationList.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,17 @@ const NotificationList: React.FC<NotificationListProps> = (props) => {
119119

120120
return (
121121
<div
122-
className={clsx(prefixCls, `${prefixCls}-${placement}`, contextClassNames?.list, className, {
123-
[`${prefixCls}-stack`]: stackEnabled,
124-
[`${prefixCls}-stack-expanded`]: expanded,
125-
})}
122+
className={clsx(
123+
prefixCls,
124+
listPrefixCls,
125+
`${prefixCls}-${placement}`,
126+
contextClassNames?.list,
127+
className,
128+
{
129+
[`${prefixCls}-stack`]: stackEnabled,
130+
[`${prefixCls}-stack-expanded`]: expanded,
131+
},
132+
)}
126133
onWheel={onWheel}
127134
onMouseEnter={() => {
128135
setListHovering(true);

src/hooks/useClosable.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import pickAttrs from '@rc-component/util/lib/pickAttrs';
2+
import * as React from 'react';
3+
4+
export type ClosableConfig = {
5+
closeIcon?: React.ReactNode;
6+
onClose?: VoidFunction;
7+
} & React.AriaAttributes;
8+
9+
export type ClosableType = boolean | ClosableConfig | null | undefined;
10+
11+
export type MergedClosableConfig = Omit<ClosableConfig, 'closeIcon'> & {
12+
closeIcon: React.ReactNode;
13+
};
14+
15+
const EMPTY_CLOSABLE: ClosableConfig = {};
16+
17+
function normalizeClosable(closable?: ClosableType): ClosableConfig {
18+
if (typeof closable === 'object' && closable !== null) {
19+
return closable;
20+
}
21+
22+
return EMPTY_CLOSABLE;
23+
}
24+
25+
export default function useClosable(
26+
closable?: ClosableType,
27+
): [boolean, MergedClosableConfig, ReturnType<typeof pickAttrs>] {
28+
const closableObj = React.useMemo(() => normalizeClosable(closable), [closable]);
29+
30+
const closableConfig = React.useMemo<MergedClosableConfig>(
31+
() => ({
32+
...closableObj,
33+
closeIcon: closableObj.closeIcon ?? 'x',
34+
}),
35+
[closableObj],
36+
);
37+
38+
const closableAriaProps = React.useMemo(() => pickAttrs(closableConfig, true), [closableConfig]);
39+
40+
return [!!closable, closableConfig, closableAriaProps];
41+
}

tests/index.test.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,26 @@ describe('Notification.Basic', () => {
6868
expect(document.querySelector('.test-icon').textContent).toEqual('test-close-icon');
6969
});
7070

71+
it('works with default close icon and aria props', () => {
72+
const { instance } = renderDemo();
73+
74+
act(() => {
75+
instance.open({
76+
content: <p className="test">1</p>,
77+
closable: {
78+
'aria-describedby': 'custom-close',
79+
},
80+
duration: 0,
81+
});
82+
});
83+
84+
const closeBtn = document.querySelector('.rc-notification-notice-close');
85+
86+
expect(document.querySelectorAll('.test')).toHaveLength(1);
87+
expect(closeBtn?.textContent).toEqual('x');
88+
expect(closeBtn).toHaveAttribute('aria-describedby', 'custom-close');
89+
});
90+
7191
it('works with multi instance', () => {
7292
const { instance } = renderDemo();
7393

0 commit comments

Comments
 (0)