better code: IconBtn as special case of Btn
Massimo Melina committed
Mar 2, 2024 at 00:12 UTC
7b09e0fd7e4e76cbab3ae180f27ab07fd49d9abe
2 files changed
+44
-93
admin/src/FileField.ts
-1
@@ -2,7 +2,6 @@
2
3
import { FieldProps, StringField } from '@hfs/mui-grid-form'
4
import { createElement as h } from 'react'
5
-import { InputAdornment } from '@mui/material'
5
import { Eject } from '@mui/icons-material'
6
import { IconBtn, useBreakpoint } from './mui'
7
import { newDialog } from '@hfs/shared'
admin/src/mui.ts
+44
-92
@@ -3,17 +3,15 @@
3
4
import { PauseCircle, PlayCircle, Refresh, SvgIconComponent } from '@mui/icons-material'
5
import { SxProps } from '@mui/system'
6
-import {
7
- createElement as h, FC, forwardRef, Fragment, ReactElement, ReactNode, useCallback, useEffect, useRef,
8
- ForwardedRef, useState, useMemo
9
-} from 'react'
6
+import { createElement as h, forwardRef, Fragment, ReactElement, ReactNode, useCallback, useEffect, useRef,
7
+ ForwardedRef, useState, useMemo } from 'react'
8
import { Box, BoxProps, Breakpoint, ButtonProps, CircularProgress, IconButton, IconButtonProps, Link, LinkProps,
9
Tooltip, TooltipProps, useMediaQuery } from '@mui/material'
10
import { formatPerc, isIpLan, isIpLocalHost, prefix, WIKI_URL } from '../../src/cross'
11
import { dontBotherWithKeys, restartAnimation, useBatch, useStateMounted } from '@hfs/shared'
12
import { Promisable, StringField } from '@hfs/mui-grid-form'
13
import { alertDialog, confirmDialog, toast } from './dialog'
16
-import { LoadingButton, LoadingButtonProps } from '@mui/lab'
14
+import { LoadingButton } from '@mui/lab'
15
import { Link as RouterLink } from 'react-router-dom'
16
import { SvgIconProps } from '@mui/material/SvgIcon/SvgIcon'
17
import _ from 'lodash'
@@ -119,116 +117,70 @@ function useRefPass<T=unknown>(forwarded: ForwardedRef<any>) {
117
})
118
}
119
122
-interface IconBtnProps extends Omit<IconButtonProps, 'disabled'|'title'|'onClick'> {
123
- title?: ReactNode
124
- icon: SvgIconComponent
125
- disabled?: boolean | string
126
- progress?: boolean | number
127
- link?: string
128
- confirm?: string
129
- doneMessage?: boolean | string // displayed only if the result of onClick !== false
130
- tooltipProps?: Partial<TooltipProps>
131
- modified?: boolean
132
- onClick?: (...args: Parameters<NonNullable<IconButtonProps['onClick']>>) => Promisable<any>
133
-}
120
+interface IconBtnProps extends Omit<BtnProps, 'icon' | 'children'> { icon: SvgIconComponent }
121
+export const IconBtn = forwardRef((props: IconBtnProps, ref: ForwardedRef<HTMLButtonElement>) =>
122
+ h(Btn, { ref, ...props }))
123
135
-export const IconBtn = forwardRef(({ title, icon, onClick, disabled, progress, link, tooltipProps, confirm, doneMessage, sx, modified, ...rest }: IconBtnProps, forwarded: ForwardedRef<HTMLButtonElement>) => {
136
- const [loading, setLoading] = useStateMounted(false)
137
- if (typeof disabled === 'string')
138
- title = disabled
139
- if (link)
140
- onClick = () => window.open(link)
141
- disabled = loading || Boolean(progress) || disabled === undefined ? undefined : Boolean(disabled)
142
- const ref = useRefPass<HTMLButtonElement>(forwarded)
143
- let ret: ReturnType<FC> = h(IconButton, {
144
- ref,
145
- 'aria-hidden': disabled,
146
- ..._.merge(modifiedProps(modified),
147
- { disabled, sx: { height: 'fit-content', ...sx } },
148
- rest),
149
- async onClick(...args) {
150
- if (confirm && !await confirmDialog(confirm)) return
151
- const ret = onClick?.apply(this,args)
152
- if (ret && ret instanceof Promise) {
153
- setLoading(true)
154
- ret.then(x => x !== false && execDoneMessage(doneMessage, ref.current), alertDialog)
155
- .finally(()=> setLoading(false))
156
- }
157
- }
158
- },
159
- (progress || loading) && progress !== false // false is also useful to inhibit behavior with loading
160
- && h(CircularProgress, {
161
- ...(typeof progress === 'number' ? { value: progress*100, variant: 'determinate' } : null),
162
- style: { position:'absolute', top: '10%', left: '10%', width: '80%', height: '80%' }
163
- }),
164
- h(icon)
165
- )
166
- const aria = rest['aria-label'] ?? (_.isString(title) ? title : undefined)
167
- if (title) {
168
- if (disabled)
169
- ret = h('span', { role: 'button', 'aria-label': aria, 'aria-disabled': disabled }, ret)
170
- ret = hTooltip(title, aria, ret, tooltipProps)
171
- }
172
- return ret
173
-})
174
-
175
-interface BtnProps extends Omit<LoadingButtonProps,'disabled'|'title'|'onClick'> {
124
+interface BtnProps extends Omit<ButtonProps & IconButtonProps,'disabled'|'title'|'onClick'> {
125
icon?: SvgIconComponent
126
title?: ReactNode
127
disabled?: boolean | string
128
progress?: boolean | number
129
link?: string
130
confirm?: boolean | ReactNode
182
- labelFrom?: Breakpoint
131
+ labelFrom?: Breakpoint | false
132
doneMessage?: boolean | string // displayed only if the result of onClick !== false
184
- tooltipProps?: TooltipProps
133
+ tooltipProps?: Partial<TooltipProps>
134
+ modified?: boolean
135
+ loading?: boolean
136
onClick?: (...args: Parameters<NonNullable<ButtonProps['onClick']>>) => Promisable<any>
137
}
138
188
-export const Btn = forwardRef(({ icon, title, onClick, disabled, progress, link, tooltipProps, confirm, doneMessage, labelFrom, children, ...rest }: BtnProps, ref: any) => {
189
- const [loading, setLoading] = useStateMounted(false)
190
- if (typeof disabled === 'string') {
139
+export const Btn = forwardRef(({ icon, title, onClick, disabled, progress, link, tooltipProps, confirm, doneMessage, labelFrom, children, modified, loading, ...rest }: BtnProps, forwarded: ForwardedRef<HTMLButtonElement>) => {
140
+ const [loadingState, setLoadingState] = useStateMounted(false)
141
+ if (typeof disabled === 'string')
142
title = disabled
192
- disabled = true
193
- }
143
+ disabled = loadingState || Boolean(progress) || disabled === undefined ? undefined : Boolean(disabled)
144
if (link)
145
onClick = () => window.open(link)
146
const showLabel = useBreakpoint(labelFrom || 'xs')
197
- let ret: ReturnType<FC> = h(LoadingButton, {
147
+ const ref = useRefPass<HTMLButtonElement>(forwarded)
148
+ const common = _.merge(modifiedProps(modified), {
149
ref,
199
- variant: 'contained',
200
- startIcon: icon && h(icon),
201
- loading: Boolean(loading || progress),
202
- loadingPosition: icon && 'start',
203
- loadingIndicator: typeof progress !== 'number' ? undefined
204
- : h(CircularProgress, { size: '1rem', value: progress*100, variant: 'determinate' }),
150
disabled,
151
'aria-hidden': disabled,
207
- ...rest,
208
- children: showLabel && children,
209
- sx: {
210
- ...rest.sx,
211
- ...!showLabel && {
212
- minWidth: 'auto',
213
- px: 1,
214
- py: '7px',
215
- '& span': { mx:0 },
216
- }
217
- },
218
- async onClick(...args) {
152
+ async onClick(...args: any[]) {
153
if (confirm && !await confirmDialog(confirm === true ? "Are you sure?" : confirm)) return
220
- const ret = onClick?.apply(this,args)
154
+ const ret = onClick?.apply(this, args as any)
155
if (ret && ret instanceof Promise) {
222
- setLoading(true)
156
+ setLoadingState(true)
157
ret.then(x => x !== false && execDoneMessage(doneMessage), alertDialog)
224
- .finally(()=> setLoading(false))
158
+ .finally(()=> setLoadingState(false))
159
}
226
- }
227
- })
160
+ },
161
+ } as const, rest)
162
+ let ret: ReactElement = children || !icon ? h(LoadingButton, _.merge({
163
+ variant: 'contained',
164
+ startIcon: icon && h(icon),
165
+ loading: Boolean(loading || loadingState || progress),
166
+ loadingPosition: icon && 'start',
167
+ loadingIndicator: typeof progress !== 'number' ? undefined
168
+ : h(CircularProgress, { size: '1rem', value: progress*100, variant: 'determinate' }),
169
+ children: showLabel && children,
170
+ } as const, common, !showLabel && { sx: { minWidth: 'auto', px: 1, py: '7px', '& span': { mx:0 }, } }))
171
+ : h(IconButton, _.merge(common, { sx: { height: 'fit-content' } }),
172
+ (progress || loadingState) && progress !== false // false is also useful to inhibit behavior with loading
173
+ && h(CircularProgress, {
174
+ ...(typeof progress === 'number' ? { value: progress*100, variant: 'determinate' } : null),
175
+ style: { position:'absolute', top: '10%', left: '10%', width: '80%', height: '80%' }
176
+ }),
177
+ h(icon)
178
+ )
179
+
180
const aria = rest['aria-label'] ?? (_.isString(title) ? title : undefined)
181
if (title) {
230
- // having this span-wrapper conditioned by if(disabled) is causing a strange (harmless?) warning by mui-popper as soon as you click, so we don't
231
- ret = h('span', { role: 'button', 'aria-label': aria, 'aria-disabled': disabled }, ret)
182
+ if (disabled) // having this span-wrapper conditioned by if(disabled) is causing a (harmless?) warning by mui-popper if the element becomes disabled after you click (file cut button does), but otherwise we have a bigger problem with a11y, with this being seen as a button
183
+ ret = h('span', { role: 'button', 'aria-label': aria, 'aria-disabled': disabled }, ret)
184
ret = hTooltip(title, aria, ret, tooltipProps)
185
}
186
return ret
@@ -280,7 +232,7 @@ export function useToggleButton(onTitle: string, offTitle: undefined | string, i
232
const props = iconBtn(state)
233
const el = useMemo(() => h(IconBtn, {
234
size: 'small',
283
- color: state ? 'primary' : 'default',
235
+ color: state ? 'primary' : undefined,
236
title: state || offTitle === undefined ? onTitle : offTitle,
237
'aria-label': onTitle, // aria should be steady, and rely on aria-pressed
238
'aria-pressed': state,