Skip to content

Commit d6e6ab0

Browse files
authored
[refactor] rewrite Button & Form components (#16)
1 parent 3b5be3b commit d6e6ab0

File tree

14 files changed

+283
-150
lines changed

14 files changed

+283
-150
lines changed

.husky/pre-commit

100644100755
File mode changed.

.husky/pre-push

100644100755
File mode changed.

.npmignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
.parcelrc
12
.parcel-cache/
23
.prettierrc.yml
4+
v1/
35
test/
4-
jest.config.js
6+
jest.config.ts
57
docs/
68
.husky/
79
.github/

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@
2626
"dependencies": {
2727
"@swc/helpers": "^0.5.3",
2828
"classnames": "^2.5.1",
29-
"dom-renderer": "^2.0.4",
29+
"dom-renderer": "^2.0.5",
3030
"mobx": "^6.12.0",
3131
"regenerator-runtime": "^0.14.1",
32-
"web-cell": "^3.0.0-rc.4",
32+
"web-cell": "^3.0.0-rc.6",
3333
"web-utility": "^4.1.3"
3434
},
3535
"peerDependencies": {

pnpm-lock.yaml

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

source/Button.tsx

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import classNames from 'classnames';
2+
import { JsxChildren, VNode } from 'dom-renderer';
3+
import { FC, WebCellProps } from 'web-cell';
4+
5+
import { FormControlProps } from './Form';
6+
import { Icon, IconProps } from './Icon';
7+
import { Color } from './type';
8+
9+
export interface ButtonProps
10+
extends WebCellProps<HTMLButtonElement>,
11+
Omit<WebCellProps<HTMLAnchorElement>, 'type'>,
12+
Pick<FormControlProps<'input'>, 'size'> {
13+
variant?: Color | `outline-${Color}` | 'link';
14+
active?: boolean;
15+
}
16+
17+
export const Button: FC<ButtonProps> = ({
18+
className,
19+
href,
20+
variant = 'primary',
21+
active,
22+
children,
23+
...props
24+
}) => {
25+
const { disabled, tabIndex } = props,
26+
Class = classNames('btn', variant && `btn-${variant}`, className);
27+
28+
return href ? (
29+
<a
30+
role="button"
31+
className={classNames(Class, { disabled, active })}
32+
tabIndex={disabled ? -1 : tabIndex}
33+
ariaDisabled={disabled + ''}
34+
ariaPressed={active + ''}
35+
{...props}
36+
>
37+
{children}
38+
</a>
39+
) : (
40+
<button className={Class} {...props} ariaPressed={active + ''}>
41+
{children}
42+
</button>
43+
);
44+
};
45+
46+
export function isButton(node: JsxChildren): node is VNode {
47+
const { selector, props } = node as VNode;
48+
49+
return /^(a|input|button)/.test(selector) && props?.className?.btn;
50+
}
51+
52+
export type IconButtonProps = IconProps & ButtonProps;
53+
54+
export const IconButton: FC<IconButtonProps> = ({
55+
className,
56+
name,
57+
...button
58+
}) => (
59+
<Button
60+
className={classNames('p-1', className)}
61+
style={{ lineHeight: '0.8' }}
62+
{...button}
63+
>
64+
<Icon name={name} />
65+
</Button>
66+
);
67+
68+
export const CloseButton: FC<WebCellProps<HTMLButtonElement>> = ({
69+
className = '',
70+
...props
71+
}) => (
72+
<button
73+
className={`btn-close ${className}`}
74+
type="button"
75+
ariaLabel="Close"
76+
{...props}
77+
/>
78+
);

source/Form.tsx

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import classNames from 'classnames';
2+
import { VNode } from 'dom-renderer';
3+
import { FC, WebCellProps } from 'web-cell';
4+
import { uniqueID } from 'web-utility';
5+
6+
export type FormGroupProps = WebCellProps<HTMLDivElement>;
7+
8+
export const FormGroup: FC<FormGroupProps> = ({ children, ...props }) => (
9+
<div {...props}>{children}</div>
10+
);
11+
12+
export type FormLabelProps = WebCellProps<HTMLLabelElement>;
13+
14+
export const FormLabel: FC<FormLabelProps> = ({
15+
className = '',
16+
children,
17+
...props
18+
}) => (
19+
<label className={`form-label ${className}`} {...props}>
20+
{children}
21+
</label>
22+
);
23+
24+
export interface FloatingLabelProps extends FormLabelProps {
25+
label: string;
26+
}
27+
28+
export const FloatingLabel: FC<FloatingLabelProps> = ({
29+
className = '',
30+
style,
31+
label,
32+
children,
33+
...props
34+
}) => (
35+
<div className={`form-floating ${className}`} style={style}>
36+
{children}
37+
<label {...props}>{label}</label>
38+
</div>
39+
);
40+
41+
export type FormControlTag = 'input' | 'textarea' | 'select';
42+
43+
export type FormControlProps<T extends FormControlTag> = WebCellProps &
44+
Omit<JSX.IntrinsicElements[T], 'size'> & {
45+
as?: T;
46+
htmlSize?: number;
47+
size?: 'sm' | 'lg';
48+
plaintext?: boolean;
49+
};
50+
51+
export const FormControl = <T extends FormControlTag = 'input'>({
52+
as: Tag,
53+
className = '',
54+
htmlSize,
55+
size,
56+
plaintext,
57+
...props
58+
}: FormControlProps<T>) => (
59+
<Tag
60+
className={classNames(
61+
'form-control',
62+
size && `form-control-${size}`,
63+
(props as FormControlProps<'input'>).readOnly &&
64+
plaintext &&
65+
`form-control-plaintext`,
66+
(props as FormControlProps<'input'>).type === 'color' &&
67+
`form-control-color`,
68+
className
69+
)}
70+
{...props}
71+
size={htmlSize}
72+
/>
73+
);
74+
75+
export interface FormCheckProps extends WebCellProps<HTMLInputElement> {
76+
type: 'radio' | 'checkbox' | 'switch';
77+
inline?: boolean;
78+
reverse?: boolean;
79+
label?: VNode;
80+
}
81+
82+
export const FormCheck: FC<FormCheckProps> = ({
83+
id = uniqueID(),
84+
className = '',
85+
style,
86+
title,
87+
type,
88+
inline,
89+
reverse,
90+
label,
91+
...props
92+
}) => (
93+
<div
94+
className={classNames(
95+
label && 'form-check',
96+
inline && 'form-check-inline',
97+
reverse && 'form-check-reverse',
98+
type === 'switch' && 'form-switch',
99+
className
100+
)}
101+
style={style}
102+
>
103+
<input
104+
className="form-check-input"
105+
type={type === 'switch' ? 'checkbox' : type}
106+
role={type === 'switch' ? 'switch' : undefined}
107+
id={id}
108+
{...props}
109+
/>
110+
{label && (
111+
<label className="form-check-label" htmlFor={id} title={title}>
112+
{label}
113+
</label>
114+
)}
115+
</div>
116+
);
117+
118+
export type FormFieldProps<T extends FormControlTag> = FormGroupProps &
119+
FormLabelProps &
120+
FormControlProps<T> & {
121+
label?: string;
122+
labelFloat?: boolean;
123+
};
124+
125+
export const FormField = <T extends FormControlTag = 'input'>({
126+
className,
127+
label,
128+
labelFloat,
129+
...props
130+
}: FormFieldProps<T>) => {
131+
label ||= props.title || (props as FormControlProps<'input'>).placeholder;
132+
133+
const field = <FormControl {...(props as FormControlProps<T>)} />;
134+
135+
return labelFloat ? (
136+
<FloatingLabel {...{ className, label }}>{field}</FloatingLabel>
137+
) : (
138+
<FormGroup className={className}>
139+
<FormLabel>{label}</FormLabel>
140+
{field}
141+
</FormGroup>
142+
);
143+
};
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { WebCellProps, createCell } from 'web-cell';
21
import classNames from 'classnames';
2+
import { WebCellProps } from 'web-cell';
33

4-
import { TextColors } from '../utility/constant';
4+
import { Color } from './type';
55

66
export interface IconProps extends WebCellProps {
77
name: string;
8-
color?: TextColors;
8+
color?: Color;
99
size?: number;
1010
}
1111

@@ -15,7 +15,7 @@ export function Icon({
1515
color,
1616
name,
1717
size,
18-
defaultSlot,
18+
children,
1919
...rest
2020
}: IconProps) {
2121
return (

source/Nav.tsx

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { JsxProps } from 'dom-renderer';
2-
import { FC } from 'web-cell';
2+
import { FC, WebCell, WebCellProps, component } from 'web-cell';
3+
4+
import { OffcanvasNavbar } from './Navbar';
35

46
export interface NavLinkProps extends JsxProps<HTMLAnchorElement> {
57
active?: boolean;
@@ -20,3 +22,36 @@ export const NavLink: FC<NavLinkProps> = ({
2022
</a>
2123
</li>
2224
);
25+
26+
export interface Nav extends WebCell {}
27+
28+
@component({
29+
tagName: 'bs-nav',
30+
mode: 'open'
31+
})
32+
export class Nav extends HTMLElement {
33+
declare props: WebCellProps;
34+
35+
connectedCallback() {
36+
const navBar = this.closest<OffcanvasNavbar>(
37+
'offcanvas-navbar, .navbar'
38+
);
39+
40+
if (!navBar) return this.classList.add('nav');
41+
42+
const expand =
43+
navBar.expand ||
44+
navBar.className.match(/navbar-expand(-(\S+))?/)?.[2];
45+
46+
this.classList.add(
47+
'navbar-nav',
48+
'align-items-center',
49+
expand && 'flex-column',
50+
expand && `flex-${expand}-row`
51+
);
52+
}
53+
54+
render() {
55+
return <slot />;
56+
}
57+
}

source/Navbar.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { JsxProps, VNode } from 'dom-renderer';
22
import { observable } from 'mobx';
3-
import { FC, attribute, component, observer } from 'web-cell';
3+
import { FC, WebCellProps, attribute, component, observer } from 'web-cell';
44
import { uniqueID } from 'web-utility';
55

66
import { Container, ContainerProps } from './Grid';
@@ -38,7 +38,7 @@ export const NavbarToggle: FC<NavbarToggleProps> = ({
3838
</button>
3939
);
4040

41-
export interface NavbarProps extends JsxProps<HTMLDivElement> {
41+
export interface NavbarProps extends WebCellProps {
4242
variant?: 'light' | 'dark';
4343
bg?: BackgroundColor;
4444
expand?: boolean | Size;

0 commit comments

Comments
 (0)