Skip to content

Commit

Permalink
BannerCallout: implement internal IconCompactButton for dismiss butto…
Browse files Browse the repository at this point in the history
…n (VR only) (#3893)
  • Loading branch information
AlbertCarreras authored Nov 26, 2024
1 parent 2c67e9a commit 68c0f00
Show file tree
Hide file tree
Showing 4 changed files with 329 additions and 4 deletions.
6 changes: 3 additions & 3 deletions packages/gestalt/src/BannerCallout/DismissButton.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import classnames from 'classnames';
import styles from '../BannerCallout.css';
import { useDefaultLabelContext } from '../contexts/DefaultLabelProvider';
import IconButton from '../IconButton';
import InternalIconCompactButton from '../IconButton/InternalIconCompactButton';

type Props = {
size?: 'sm' | 'lg';
Expand All @@ -21,9 +21,9 @@ export default function DismissButton({ dismissButton, size = 'lg' }: Props) {
size === 'lg' ? styles.lgRtlVRPos : styles.smRtlVRPos,
)}
>
<IconButton
<InternalIconCompactButton
accessibilityLabel={dismissButton?.accessibilityLabel ?? accessibilityDismissButtonLabel}
icon="cancel"
icon="compact-cancel"
iconColor="darkGray"
onClick={dismissButton?.onDismiss}
size="sm"
Expand Down
2 changes: 1 addition & 1 deletion packages/gestalt/src/BannerCallout/HeaderSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export default function HeaderSection({
size={iconSize}
/>
<Flex.Item flex={fullWidth ? 'grow' : undefined}>
<Box maxWidth={648} width="100%">
<Box width="100%">
{(title || message) && (
<Flex direction="column" gap={2} width="100%">
{title && <Heading size="400">{title}</Heading>}
Expand Down
171 changes: 171 additions & 0 deletions packages/gestalt/src/IconButton/InternalIconCompactButton.tsx
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;
154 changes: 154 additions & 0 deletions packages/gestalt/src/Pog/InternalPogCompact.tsx
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>
);
}

0 comments on commit 68c0f00

Please sign in to comment.