Skip to content

Commit 71f787f

Browse files
committed
feat(ui): add checkbox component
1 parent c588e14 commit 71f787f

44 files changed

Lines changed: 1220 additions & 429 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/ui/src/components/anchor/Anchor.tsx

Lines changed: 36 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { DElementSelector } from '../../hooks/element-ref';
2-
import type { DStateBackflowContextData } from '../../hooks/state-backflow';
2+
import type { Draft } from 'immer';
33

44
import { isUndefined } from 'lodash';
55
import React, { useCallback, useEffect, useMemo, useState } from 'react';
@@ -13,11 +13,12 @@ import {
1313
useRefCallback,
1414
useRootContentConfig,
1515
useValueChange,
16-
DStateBackflowContext,
1716
} from '../../hooks';
1817
import { getClassName, CustomScroll } from '../../utils';
1918

2019
export interface DAnchorContextData {
20+
updateLinks: (identity: string, href: string | undefined, el: HTMLLIElement | null) => void;
21+
removeLinks: (identity: string) => void;
2122
anchorActiveHref: string | null;
2223
onLinkClick: (href: string) => void;
2324
}
@@ -55,7 +56,7 @@ export function DAnchor(props: DAnchorProps) {
5556
const asyncCapture = useAsync();
5657
const [customScroll] = useState(() => new CustomScroll());
5758
const [dotStyle, setDotStyle] = useImmer<React.CSSProperties>({});
58-
const [links, setLinks] = useImmer(new Map<string, { href: string; el: HTMLLIElement | null }>());
59+
const [links, setLinks] = useImmer(new Map<string, { href: string; el: HTMLLIElement }>());
5960
const [activeHref, setActiveHref] = useState<string | null>(null);
6061

6162
const pageRef = useRefSelector(dPage ?? null);
@@ -97,8 +98,8 @@ export function DAnchor(props: DAnchorProps) {
9798
if (newHref) {
9899
for (const { href, el } of links.values()) {
99100
if (href === newHref) {
100-
const rect = el?.getBoundingClientRect();
101-
if (rect && anchorEl) {
101+
const rect = el.getBoundingClientRect();
102+
if (anchorEl) {
102103
draft.top = rect.top + rect.height / 2 - anchorEl.getBoundingClientRect().top;
103104
}
104105
break;
@@ -157,51 +158,46 @@ export function DAnchor(props: DAnchorProps) {
157158
}, [asyncCapture, rootContentRef, updateAnchor]);
158159
//#endregion
159160

160-
const contextValue = useMemo<DAnchorContextData>(
161-
() => ({
162-
anchorActiveHref: activeHref,
163-
onLinkClick,
164-
}),
165-
[activeHref, onLinkClick]
166-
);
167-
168-
const stateBackflowContextValue = useMemo<DStateBackflowContextData>(
161+
const stateBackflow = useMemo<Pick<DAnchorContextData, 'updateLinks' | 'removeLinks'>>(
169162
() => ({
170-
addState: (identity, href, el) => {
171-
setLinks((draft) => {
172-
draft.set(identity, { href, el });
173-
});
174-
},
175-
updateState: (identity, href, el) => {
176-
setLinks((draft) => {
177-
draft.set(identity, { href, el });
178-
});
163+
updateLinks: (identity, href, el) => {
164+
if (href && el) {
165+
setLinks((draft) => {
166+
draft.set(identity, { href, el: el as Draft<HTMLLIElement> });
167+
});
168+
}
179169
},
180-
removeState: (identity) => {
170+
removeLinks: (identity) => {
181171
setLinks((draft) => {
182172
draft.delete(identity);
183173
});
184174
},
185175
}),
186176
[setLinks]
187177
);
178+
const contextValue = useMemo<DAnchorContextData>(
179+
() => ({
180+
...stateBackflow,
181+
anchorActiveHref: activeHref,
182+
onLinkClick,
183+
}),
184+
[activeHref, onLinkClick, stateBackflow]
185+
);
188186

189187
return (
190-
<DStateBackflowContext.Provider value={stateBackflowContextValue}>
191-
<DAnchorContext.Provider value={contextValue}>
192-
<ul {...restProps} ref={anchorRef} className={getClassName(className, `${dPrefix}anchor`)}>
193-
<div className={`${dPrefix}anchor__indicator`}>
194-
{dIndicator === 'dot' ? (
195-
<span className={`${dPrefix}anchor__dot-indicator`} style={dotStyle}></span>
196-
) : dIndicator === 'line' ? (
197-
<span className={`${dPrefix}anchor__line-indicator`} style={dotStyle}></span>
198-
) : (
199-
dIndicator
200-
)}
201-
</div>
202-
{children}
203-
</ul>
204-
</DAnchorContext.Provider>
205-
</DStateBackflowContext.Provider>
188+
<DAnchorContext.Provider value={contextValue}>
189+
<ul {...restProps} ref={anchorRef} className={getClassName(className, `${dPrefix}anchor`)}>
190+
<div className={`${dPrefix}anchor__indicator`}>
191+
{dIndicator === 'dot' ? (
192+
<span className={`${dPrefix}anchor__dot-indicator`} style={dotStyle}></span>
193+
) : dIndicator === 'line' ? (
194+
<span className={`${dPrefix}anchor__line-indicator`} style={dotStyle}></span>
195+
) : (
196+
dIndicator
197+
)}
198+
</div>
199+
{children}
200+
</ul>
201+
</DAnchorContext.Provider>
206202
);
207203
}

packages/ui/src/components/anchor/AnchorLink.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@ export function DAnchorLink(props: DAnchorLinkProps) {
1414

1515
//#region Context
1616
const dPrefix = usePrefixConfig();
17-
const [{ anchorActiveHref, onLinkClick }] = useCustomContext(DAnchorContext);
17+
const [{ updateLinks, removeLinks, anchorActiveHref, onLinkClick }] = useCustomContext(DAnchorContext);
1818
//#endregion
1919

2020
//#region Ref
2121
const [linkEl, linkRef] = useRefCallback<HTMLLIElement>();
2222
//#endregion
2323

24-
useStateBackflow(href, linkEl);
24+
useStateBackflow(updateLinks, removeLinks, href, linkEl);
2525

2626
const handleClick = useCallback(
2727
(e) => {

packages/ui/src/components/button/ButtonGroup.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@ export function DButtonGroup(props: DButtonGroupProps) {
3939
const size = dSize ?? gSize;
4040
const disabled = dDisabled || gDisabled;
4141

42+
const generalStateContextValue = useMemo<DGeneralStateContextData>(
43+
() => ({
44+
gSize: size,
45+
gDisabled: disabled,
46+
}),
47+
[disabled, size]
48+
);
49+
4250
const contextValue = useMemo<DButtonGroupContextData>(
4351
() => ({
4452
buttonGroupType: dType,
@@ -48,14 +56,6 @@ export function DButtonGroup(props: DButtonGroupProps) {
4856
[dType, dTheme, dDisabled]
4957
);
5058

51-
const generalStateContextValue = useMemo<DGeneralStateContextData>(
52-
() => ({
53-
gSize: size,
54-
gDisabled: disabled,
55-
}),
56-
[disabled, size]
57-
);
58-
5959
return (
6060
<DGeneralStateContext.Provider value={generalStateContextValue}>
6161
<DButtonGroupContext.Provider value={contextValue}>

packages/ui/src/components/button/README.zh-Hant.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ title: 按钮
2222
| dLoading | 设置按钮载入状态 | boolean | false |
2323
| dBlock | 将按钮宽度调整为其父宽度 | boolean | false |
2424
| dShape | 设置按钮形状 | 'circle' \| 'round' | - |
25-
| dSize | 设置按钮尺寸 | 'smaller' \| 'larger' | - |
25+
| dSize | 设置尺寸 | 'smaller' \| 'larger' | - |
2626
| dIcon | 设置按钮的图标 | React.ReactNode | - |
2727
| dIconRight | 设置图标在右侧 | boolean | false |
2828
<!-- prettier-ignore-end -->

packages/ui/src/components/button/demos/7.Size.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
title:
33
en-US: Size
4-
zh-Hant: 按钮尺寸
4+
zh-Hant: 尺寸
55
---
66

77
# en-US
@@ -10,7 +10,7 @@ Adjust the button size by setting `dSize` to `larger` and `smaller`.
1010

1111
# zh-Hant
1212

13-
通过设置 `dSize``larger` `smaller` 调整按钮尺寸
13+
通过设置 `dSize``larger` `smaller` 调整尺寸
1414

1515
```tsx
1616
import { DButton, DIcon } from '@react-devui/ui';
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import type { Updater } from '../../hooks/two-way-binding';
2+
3+
import React, { useCallback, useId } from 'react';
4+
5+
import { usePrefixConfig, useComponentConfig, useCustomContext, useTwoWayBinding, useGeneralState, useStateBackflow } from '../../hooks';
6+
import { getClassName } from '../../utils';
7+
import { DCheckboxGroupContext } from './CheckboxGroup';
8+
9+
export type DCheckboxRef = HTMLInputElement;
10+
11+
export interface DCheckboxProps extends React.HTMLAttributes<HTMLElement> {
12+
dModel?: [boolean, Updater<boolean>?];
13+
dFormControlName?: string;
14+
dIndeterminate?: boolean;
15+
dAriaControls?: string;
16+
dSize?: 'smaller' | 'larger';
17+
dDisabled?: boolean;
18+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
19+
dValue?: any;
20+
onModelChange?: (checked: boolean) => void;
21+
}
22+
23+
const Checkbox: React.ForwardRefRenderFunction<DCheckboxRef, DCheckboxProps> = (props, ref) => {
24+
const {
25+
dModel,
26+
dFormControlName,
27+
dIndeterminate = false,
28+
dAriaControls,
29+
dSize,
30+
dDisabled = false,
31+
dValue,
32+
onModelChange,
33+
id,
34+
className,
35+
children,
36+
onChange,
37+
...restProps
38+
} = useComponentConfig(DCheckbox.name, props);
39+
40+
//#region Context
41+
const dPrefix = usePrefixConfig();
42+
const { gSize, gDisabled } = useGeneralState();
43+
const [{ updateCheckboxs, removeCheckboxs, checkboxGroupValue, onCheckedChange }, checkboxGroupContext] =
44+
useCustomContext(DCheckboxGroupContext);
45+
//#endregion
46+
47+
const uniqueId = useId();
48+
const _id = id ?? `${dPrefix}checkbox-${uniqueId}`;
49+
50+
useStateBackflow(updateCheckboxs, removeCheckboxs, _id, dValue);
51+
52+
const inGroup = checkboxGroupContext !== null;
53+
54+
const [checked, changeChecked, { validateClassName, ariaAttribute, controlDisabled }] = useTwoWayBinding(
55+
false,
56+
inGroup ? [checkboxGroupValue?.includes(dValue) ?? false] : dModel,
57+
onModelChange,
58+
dFormControlName ? { formControlName: dFormControlName, id: _id } : undefined
59+
);
60+
61+
const size = dSize ?? gSize;
62+
const disabled = dDisabled || gDisabled || controlDisabled;
63+
64+
const handleChange = useCallback<React.ChangeEventHandler<HTMLInputElement>>(
65+
(e) => {
66+
onChange?.(e);
67+
68+
if (!disabled) {
69+
if (inGroup) {
70+
onCheckedChange?.(dValue, !checked);
71+
} else {
72+
changeChecked(!checked);
73+
}
74+
}
75+
},
76+
[onChange, disabled, inGroup, onCheckedChange, dValue, checked, changeChecked]
77+
);
78+
79+
return (
80+
<div
81+
{...restProps}
82+
className={getClassName(className, `${dPrefix}checkbox`, {
83+
[`${dPrefix}checkbox--${size}`]: size,
84+
'is-indeterminate': dIndeterminate,
85+
'is-checked': !dIndeterminate && checked,
86+
'is-disabled': disabled,
87+
})}
88+
>
89+
<div className={`${dPrefix}checkbox__input-wrapper`}>
90+
<input
91+
{...ariaAttribute}
92+
ref={ref}
93+
id={_id}
94+
className={getClassName(`${dPrefix}checkbox__input`, validateClassName)}
95+
type="checkbox"
96+
disabled={disabled}
97+
aria-labelledby={`${dPrefix}checkbox-label-${uniqueId}`}
98+
aria-checked={dIndeterminate ? 'mixed' : checked}
99+
aria-controls={dAriaControls}
100+
onChange={handleChange}
101+
/>
102+
{!dIndeterminate && checked && <div className={`${dPrefix}checkbox__tick`}></div>}
103+
{dIndeterminate && <div className={`${dPrefix}checkbox__indeterminate`}></div>}
104+
</div>
105+
<label id={`${dPrefix}checkbox-label-${uniqueId}`} className={`${dPrefix}checkbox__label`} htmlFor={_id}>
106+
{children}
107+
</label>
108+
</div>
109+
);
110+
};
111+
112+
export const DCheckbox = React.forwardRef(Checkbox);

0 commit comments

Comments
 (0)