-
Notifications
You must be signed in to change notification settings - Fork 356
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
BannerCallout: implement internal IconCompactButton for dismiss butto…
…n (VR only) (#3893)
- Loading branch information
1 parent
2c67e9a
commit 68c0f00
Showing
4 changed files
with
329 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
171 changes: 171 additions & 0 deletions
171
packages/gestalt/src/IconButton/InternalIconCompactButton.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
import { forwardRef, useImperativeHandle, useRef, useState } from 'react'; | ||
import classnames from 'classnames'; | ||
import styles from './InternalIconButton.css'; | ||
import compactIconsVR from '../icons-vr-theme/compact/index'; | ||
import InternalPogCompact from '../Pog/InternalPogCompact'; | ||
import touchableStyles from '../TapArea.css'; | ||
import useFocusVisible from '../useFocusVisible'; | ||
import useInExperiment from '../useInExperiment'; | ||
import useTapFeedback from '../useTapFeedback'; | ||
|
||
type Props = { | ||
accessibilityLabel: string; | ||
accessibilityControls?: string; | ||
accessibilityExpanded?: boolean; | ||
accessibilityHaspopup?: boolean; | ||
accessibilityPopupRole?: 'menu' | 'dialog'; | ||
focusColor?: 'lightBackground' | 'darkBackground'; | ||
bgColor?: | ||
| 'transparent' | ||
| 'transparentDarkBackground' | ||
| 'transparentDarkGray' | ||
| 'gray' | ||
| 'lightGray' | ||
| 'washLight' | ||
| 'white' | ||
| 'red' | ||
| 'elevation'; | ||
dangerouslySetSvgPath?: { | ||
__path: string; | ||
}; | ||
dataTestId?: string; | ||
disabled?: boolean; | ||
icon?: keyof typeof compactIconsVR; | ||
iconColor?: 'gray' | 'darkGray' | 'red' | 'white' | 'brandPrimary' | 'light' | 'dark'; | ||
name?: string; | ||
onClick?: (arg1: { | ||
event: React.MouseEvent<HTMLButtonElement> | React.KeyboardEvent<HTMLButtonElement>; | ||
}) => void; | ||
padding?: 1 | 2 | 3 | 4 | 5; | ||
// eslint-disable-next-line react/no-unused-prop-types | ||
ref?: HTMLButtonElement; | ||
selected?: boolean; | ||
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'; | ||
tabIndex?: -1 | 0; | ||
}; | ||
|
||
const InternalIconButtonWithForwardRef = forwardRef<HTMLButtonElement, Props>(function IconButton( | ||
{ | ||
accessibilityControls, | ||
accessibilityExpanded, | ||
accessibilityHaspopup, | ||
accessibilityLabel, | ||
accessibilityPopupRole, | ||
bgColor, | ||
focusColor = 'lightBackground', | ||
dangerouslySetSvgPath, | ||
dataTestId, | ||
disabled, | ||
icon, | ||
iconColor, | ||
name, | ||
onClick, | ||
padding, | ||
selected, | ||
size = 'sm', | ||
tabIndex = 0, | ||
}: Props, | ||
ref, | ||
) { | ||
const innerRef = useRef<null | HTMLButtonElement>(null); | ||
// When using both forwardRef and innerRef, React.useimperativehandle() allows a parent component | ||
// that renders <IconButton ref={inputRef} /> to call inputRef.current.focus() | ||
// @ts-expect-error - TS2322 - Type 'HTMLButtonElement | null' is not assignable to type 'HTMLButtonElement'. | ||
useImperativeHandle(ref, () => innerRef.current); | ||
const isInVRExperiment = useInExperiment({ | ||
webExperimentName: 'web_gestalt_visualRefresh', | ||
mwebExperimentName: 'web_gestalt_visualRefresh', | ||
}); | ||
|
||
const { | ||
compressStyle, | ||
isTapping, | ||
handleBlur, | ||
handleMouseDown, | ||
handleMouseUp, | ||
handleTouchStart, | ||
handleTouchMove, | ||
handleTouchCancel, | ||
handleTouchEnd, | ||
} = useTapFeedback({ | ||
height: innerRef?.current?.clientHeight, | ||
width: innerRef?.current?.clientWidth, | ||
}); | ||
|
||
const [isActive, setActive] = useState(false); | ||
const [isFocused, setFocused] = useState(false); | ||
const [isHovered, setHovered] = useState(false); | ||
|
||
const { isFocusVisible } = useFocusVisible(); | ||
|
||
const divStyles = classnames(styles.button, touchableStyles.tapTransition, { | ||
[styles.disabled]: disabled && !isInVRExperiment, | ||
[styles.disabledVr]: disabled && isInVRExperiment, | ||
[styles.enabled]: !disabled, | ||
[touchableStyles.tapCompress]: !disabled && isTapping, | ||
}); | ||
|
||
return ( | ||
<button | ||
ref={innerRef} | ||
aria-controls={accessibilityControls} | ||
aria-expanded={accessibilityExpanded} | ||
aria-haspopup={accessibilityPopupRole || accessibilityHaspopup} | ||
aria-label={accessibilityLabel} | ||
className={classnames(styles.parentButton)} | ||
data-test-id={dataTestId} | ||
disabled={disabled} | ||
name={name} | ||
onBlur={() => { | ||
handleBlur(); | ||
setFocused(false); | ||
}} | ||
onClick={(event) => onClick?.({ event })} | ||
onFocus={() => setFocused(true)} | ||
onMouseDown={() => { | ||
handleMouseDown(); | ||
setActive(true); | ||
}} | ||
onMouseEnter={() => setHovered(true)} | ||
onMouseLeave={() => { | ||
setActive(false); | ||
setHovered(false); | ||
}} | ||
onMouseUp={() => { | ||
handleMouseUp(); | ||
setActive(false); | ||
}} | ||
onTouchCancel={handleTouchCancel} | ||
onTouchEnd={handleTouchEnd} | ||
// @ts-expect-error - TS2322 - Type '(arg1: TouchEvent<HTMLDivElement>) => void' is not assignable to type 'TouchEventHandler<HTMLButtonElement>'. | ||
onTouchMove={handleTouchMove} | ||
// @ts-expect-error - TS2322 - Type '(arg1: TouchEvent<HTMLDivElement>) => void' is not assignable to type 'TouchEventHandler<HTMLButtonElement>'. | ||
onTouchStart={handleTouchStart} | ||
// @ts-expect-error - TS2322 - Type '0 | -1 | null' is not assignable to type 'number | undefined'. | ||
tabIndex={disabled ? null : tabIndex} | ||
// react/button-has-type is very particular about this verbose syntax | ||
type="button" | ||
> | ||
<div className={divStyles} style={compressStyle || undefined}> | ||
<InternalPogCompact | ||
active={!disabled && isActive} | ||
bgColor={bgColor} | ||
dangerouslySetSvgPath={dangerouslySetSvgPath} | ||
disabled={disabled} | ||
focusColor={focusColor} | ||
focused={!disabled && isFocusVisible && isFocused} | ||
hovered={!disabled && isHovered} | ||
icon={icon} | ||
iconColor={iconColor} | ||
padding={padding} | ||
selected={selected} | ||
size={size} | ||
/> | ||
</div> | ||
</button> | ||
); | ||
}); | ||
|
||
InternalIconButtonWithForwardRef.displayName = 'IconButton'; | ||
|
||
export default InternalIconButtonWithForwardRef; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
import classnames from 'classnames'; | ||
import styles from './InternalPog.css'; | ||
import IconCompact from '../IconCompact'; | ||
import compactIconsVR from '../icons-vr-theme/compact/index'; | ||
|
||
type Props = { | ||
accessibilityLabel?: string; | ||
active?: boolean; | ||
focusColor?: 'lightBackground' | 'darkBackground'; | ||
bgColor?: | ||
| 'transparent' | ||
| 'transparentDarkBackground' | ||
| 'transparentDarkGray' | ||
| 'gray' | ||
| 'lightGray' | ||
| 'washLight' | ||
| 'white' | ||
| 'red' | ||
| 'elevation'; | ||
disabled?: boolean; | ||
dangerouslySetSvgPath?: { | ||
__path: string; | ||
}; | ||
focused?: boolean; | ||
hovered?: boolean; | ||
icon?: keyof typeof compactIconsVR; | ||
iconColor?: 'gray' | 'darkGray' | 'red' | 'white' | 'brandPrimary' | 'light' | 'dark'; | ||
padding?: 1 | 2 | 3 | 4 | 5; | ||
rounding?: '0' | '100' | '200' | '300' | '400' | '500' | 'circle'; | ||
selected?: boolean; | ||
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'; | ||
}; | ||
|
||
export default function InternalPogCompact({ | ||
accessibilityLabel = '', | ||
active = false, | ||
bgColor = 'transparent', | ||
focusColor = 'lightBackground', | ||
dangerouslySetSvgPath, | ||
disabled, | ||
focused = false, | ||
hovered = false, | ||
icon, | ||
iconColor, | ||
padding, | ||
rounding, | ||
selected = false, | ||
size = 'sm', | ||
}: Props) { | ||
const SIZE_NAME_TO_PADDING_PIXEL = { | ||
xs: 6, | ||
sm: 4, | ||
md: 10, | ||
lg: 12, | ||
xl: 20, | ||
} as const; | ||
|
||
const SIZE_NAME_TO_ICON_SIZE_PIXEL = { | ||
xs: 12, | ||
sm: 16, | ||
md: 16, | ||
lg: 24, | ||
xl: 24, | ||
} as const; | ||
|
||
const OLD_TO_NEW_COLOR_MAP = { | ||
brandPrimary: 'brandPrimary', | ||
dark: 'dark', | ||
darkGray: 'default', | ||
gray: 'subtle', | ||
light: 'light', | ||
red: 'error', | ||
white: 'inverse', | ||
disabled: 'disabled', | ||
} as const; | ||
|
||
const defaultIconButtonIconColors = { | ||
gray: 'white', | ||
lightGray: 'darkGray', | ||
red: 'white', | ||
transparent: 'darkGray', | ||
transparentDarkBackground: 'white', | ||
transparentDarkGray: 'light', | ||
washLight: 'dark', | ||
white: 'darkGray', | ||
elevation: 'darkGray', | ||
} as const; | ||
|
||
const color = (selected && 'white') || iconColor || defaultIconButtonIconColors[bgColor]; | ||
|
||
const iconSizeInPx = SIZE_NAME_TO_ICON_SIZE_PIXEL[size]; | ||
const paddingInPx = padding ? padding * 4 : SIZE_NAME_TO_PADDING_PIXEL[size]; | ||
|
||
const sizeInPx = iconSizeInPx + paddingInPx * 2; | ||
|
||
const inlineStyle = { | ||
height: sizeInPx, | ||
width: sizeInPx, | ||
} as const; | ||
|
||
const vrClasses = classnames(styles.pog, styles[size as keyof typeof styles], { | ||
[styles.rounding0]: rounding === '0', | ||
[styles.rounding100]: (!rounding && size === 'xs') || rounding === '100', | ||
[styles.rounding200]: (!rounding && size === 'sm') || rounding === '200', | ||
[styles.rounding300]: (!rounding && size === 'md') || rounding === '300', | ||
[styles.rounding400]: (!rounding && size === 'lg') || rounding === '400', | ||
[styles.rounding500]: (!rounding && size === 'xl') || rounding === '500', | ||
[styles.roundingCircle]: rounding === 'circle', | ||
[styles[bgColor]]: !selected, | ||
[styles.disabled]: disabled && !selected, | ||
[styles.selected]: selected && !disabled, | ||
[styles.disabledSelected]: selected && disabled, | ||
[styles.active]: active, | ||
[styles.vrFocused]: focused, | ||
[styles.transparentInnerFocus]: focused && bgColor === 'transparent', | ||
[styles.lightOuterFocus]: | ||
focused && (bgColor === 'washLight' || focusColor === 'darkBackground'), | ||
[styles.inverseOuterFocus]: | ||
focused && iconColor === 'white' && bgColor === 'transparentDarkBackground', | ||
[styles.darkInnerFocus]: | ||
focused && (bgColor === 'washLight' || focusColor === 'darkBackground'), | ||
[styles.hovered]: hovered && !active, | ||
// Opacity of 40% should only apply to disabled unselected states, when the icon is white or light | ||
[styles.semitransparent]: | ||
(bgColor === 'transparentDarkGray' || | ||
bgColor === 'transparent' || | ||
bgColor === 'transparentDarkBackground' || | ||
bgColor === 'washLight') && | ||
(color === 'white' || color === 'light') && | ||
disabled && | ||
!selected, | ||
}); | ||
|
||
return ( | ||
<div className={vrClasses} style={inlineStyle}> | ||
<IconCompact | ||
accessibilityLabel={accessibilityLabel || ''} | ||
color={ | ||
// Disabled icons should always use the disabled token, except for washLight and transparentDarkGray with white or light icons when unselected | ||
disabled && | ||
!( | ||
!selected && | ||
(bgColor === 'washLight' || bgColor === 'transparentDarkGray') && | ||
(color === 'white' || color === 'light') | ||
) | ||
? 'disabled' | ||
: OLD_TO_NEW_COLOR_MAP[color] | ||
} | ||
dangerouslySetSvgPath={dangerouslySetSvgPath} | ||
icon={icon} | ||
/> | ||
</div> | ||
); | ||
} |