Skip to content

Commit

Permalink
Merge pull request #321 from 10up/feature/icon-picker-typescript-conv…
Browse files Browse the repository at this point in the history
…ersion

Feature: IconPicker TypeScript Conversion
  • Loading branch information
fabiankaegy authored May 20, 2024
2 parents 0eb1bf5 + 93138c7 commit a2d8101
Show file tree
Hide file tree
Showing 8 changed files with 141 additions and 129 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import PropTypes from 'prop-types';
import { __ } from '@wordpress/i18n';
import { Dropdown, ToolbarButton } from '@wordpress/components';
import styled from '@emotion/styled';

import { IconPicker } from './icon-picker';
import { IconPicker, IconPickerProps } from './icon-picker';
import { Icon } from './icon';

const StyledIconPickerDropdown = styled(IconPicker)`
Expand All @@ -12,17 +11,15 @@ const StyledIconPickerDropdown = styled(IconPicker)`
height: 248px;
`;

/**
* IconPickerToolbarButton
*
* @typedef IconPickerToolbarButtonProps
* @property {string} buttonLabel label
*
* @param {IconPickerToolbarButtonProps} props IconPickerToolbarButtonProps
* @returns {*}
*/
export const IconPickerToolbarButton = (props) => {
const { value, buttonLabel } = props;
interface IconPickerToolbarButtonProps extends IconPickerProps {
/**
* Label for the button
*/
buttonLabel?: string;
}

export const IconPickerToolbarButton: React.FC<IconPickerToolbarButtonProps> = (props) => {
const { value, buttonLabel = __('Select Icon') } = props;

const buttonIcon =
value?.name && value?.iconSet ? <Icon name={value?.name} iconSet={value?.iconSet} /> : null;
Expand All @@ -35,20 +32,18 @@ export const IconPickerToolbarButton = (props) => {
placement: 'bottom-start',
}}
renderToggle={({ isOpen, onToggle }) => (
<ToolbarButton onClick={onToggle} aria-expanded={isOpen} icon={buttonIcon}>
{buttonLabel ?? __('Select Icon')}
<ToolbarButton
onClick={ onToggle }
aria-expanded={ isOpen }
icon={ buttonIcon }
placeholder={ undefined }
onPointerEnterCapture={ undefined }
onPointerLeaveCapture={ undefined }
>
{ buttonLabel }
</ToolbarButton>
)}
renderContent={() => <StyledIconPickerDropdown {...props} />}
/>
);
};

IconPickerToolbarButton.defaultProps = {
buttonLabel: __('Select Icon'),
};

IconPickerToolbarButton.propTypes = {
buttonLabel: PropTypes.string,
value: PropTypes.object.isRequired,
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import PropTypes from 'prop-types';
import styled from '@emotion/styled';
import { __ } from '@wordpress/i18n';
import {
Expand Down Expand Up @@ -29,9 +28,9 @@ const StyledIconGrid = styled(Grid)`
`;

const StyledIconButton = styled(Icon)`
background-color: ${({ selected }) => (selected ? 'black' : 'white')};
color: ${({ selected }) => (selected ? 'white' : 'black')};
fill: ${({ selected }) => (selected ? 'white' : 'black')};
background-color: ${({ selected } : { selected: boolean }) => (selected ? 'black' : 'white')};
color: ${({ selected } : { selected: boolean }) => (selected ? 'white' : 'black')};
fill: ${({ selected } : { selected: boolean }) => (selected ? 'white' : 'black')};
padding: 5px;
border: none;
border-radius: 4px;
Expand All @@ -54,19 +53,19 @@ const StyledIconButton = styled(Icon)`
}
`;

/**
* IconPicker
*
* @typedef IconPickerProps
* @property {object} value value of the selected icon
* @property {Function} onChange change handler for when a new icon is selected
* @property {string} label label of the icon picker
*
* @param {IconPickerProps} props IconPicker Props
* @returns {*} React Element
*/
export const IconPicker = (props) => {
const { value, onChange, label, ...rest } = props;
export type IconPickerProps = Omit<React.ComponentProps<typeof BaseControl>, 'children'> & {
/**
* Value of the selected icon
*/
value: { name: string; iconSet: string };
/**
* Change handler for when a new icon is selected
*/
onChange: (icon: { name: string; iconSet: string }) => void;
}

export const IconPicker: React.FC<IconPickerProps> = (props) => {
const { value, onChange, label = '', ...rest } = props;

const icons = useIcons();

Expand All @@ -90,16 +89,6 @@ export const IconPicker = (props) => {
);
};

IconPicker.defaultProps = {
label: '',
};

IconPicker.propTypes = {
value: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
label: PropTypes.string,
};

/**
* TooltipContent
*
Expand All @@ -109,7 +98,7 @@ IconPicker.propTypes = {
* workaround for that. It will just wrap the children in a div and pass that to the
* Tooltip component.
*/
const TooltipContent = forwardRef(function TooltipContent(props, ref) {
const TooltipContent = forwardRef<HTMLDivElement, React.ComponentPropsWithoutRef<'div'>>(function TooltipContent(props, ref) {
const { children } = props;

return (
Expand All @@ -119,17 +108,18 @@ const TooltipContent = forwardRef(function TooltipContent(props, ref) {
);
});

/**
* IconLabel
*
* @typedef IconLabelProps
* @property {object} icon icon object
* @property {boolean} isChecked whether the icon is checked
*
* @param {IconLabelProps} props IconLabel Props
* @returns {*} React Element
*/
const IconLabel = (props) => {
interface IconLabelProps {
/**
* Icon object
*/
icon: { name: string; iconSet: string; label: string };
/**
* Whether the icon is checked
*/
isChecked: boolean;
}

const IconLabel: React.FC<IconLabelProps> = (props) => {
const { icon, isChecked } = props;
return (
<Tooltip text={icon.label}>
Expand All @@ -145,14 +135,32 @@ const IconLabel = (props) => {
);
};

IconLabel.propTypes = {
icon: PropTypes.object.isRequired,
isChecked: PropTypes.bool.isRequired,
};

const IconGridItem = memo((props) => {
interface IconGridItemProps {
/**
* Column index
*/
columnIndex: number;
/**
* Row index
*/
rowIndex: number;
/**
* Style object
*/
style: React.CSSProperties;
/**
* Data object
*/
data: unknown;
}

const IconGridItem = memo<IconGridItemProps>((props) => {
const { columnIndex, rowIndex, style, data } = props;
const { icons, selectedIcon, onChange } = data;
const { icons, selectedIcon, onChange } = data as {
icons: { name: string; iconSet: string; label: string }[];
selectedIcon: { name: string; iconSet: string };
onChange: (icon: { name: string; iconSet: string }) => void;
};
const index = rowIndex * 5 + columnIndex;
const icon = icons[index];
const isChecked = selectedIcon?.name === icon?.name && selectedIcon?.iconSet === icon?.iconSet;
Expand All @@ -161,11 +169,14 @@ const IconGridItem = memo((props) => {
return null;
}

// We need to cast the IconLabel to a string because types in WP are not correct
const label = <IconLabel isChecked={isChecked} icon={icon} /> as unknown as string;

return (
<div style={style}>
<CheckboxControl
key={icon.name}
label={<IconLabel isChecked={isChecked} icon={icon} />}
label={label}
checked={isChecked}
onChange={() => onChange(icon)}
className="component-icon-picker__checkbox-control"
Expand All @@ -174,7 +185,22 @@ const IconGridItem = memo((props) => {
);
}, areEqual);

const IconGrid = (props) => {
interface IconGridProps {
/**
* List of icons
*/
icons: { name: string; iconSet: string; label: string }[];
/**
* Selected icon
*/
selectedIcon: { name: string; iconSet: string };
/**
* Change handler for when a new icon is selected
*/
onChange: (icon: { name: string; iconSet: string }) => void;
}

const IconGrid: React.FC<IconGridProps> = (props) => {
const { icons, selectedIcon, onChange } = props;

const itemData = useMemo(
Expand All @@ -198,9 +224,3 @@ const IconGrid = (props) => {
</NavigableMenu>
);
};

IconGrid.propTypes = {
icons: PropTypes.array.isRequired,
selectedIcon: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
};
43 changes: 19 additions & 24 deletions components/icon-picker/icon.js → components/icon-picker/icon.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,33 @@
import PropTypes from 'prop-types';
import { Spinner } from '@wordpress/components';
import { forwardRef } from '@wordpress/element';
import { useIcon } from '../../hooks/use-icons';

/**
* Icon
*
* @typedef IconProps
* @property {string } name name of the icon
* @property {string } iconSet name of the icon set
*
* @param {IconProps} props IconProps
* @returns {*}
*/
export const Icon = forwardRef(function Icon(props, ref) {
interface IconProps {
/**
* Name of the icon
*/
name: string;
/**
* Name of the icon set
*/
iconSet: string;
/**
* Click handler
*/
onClick?: () => void;
}

export const Icon: React.FC<IconProps> = forwardRef<HTMLDivElement, IconProps>(function Icon(props, ref) {
const { name, iconSet, onClick, ...rest } = props;
const icon = useIcon(iconSet, name);

if (!icon) {
if (!icon || Array.isArray(icon)) {
// @ts-ignore -- Types on WP seem to require onPointerEnterCapture and onPointerLeaveCapture
return <Spinner />;
}

// only add interactive props to component if a onClick handler was provided
const iconProps = {};
const iconProps: React.JSX.IntrinsicElements['div'] = {};
if (typeof onClick === 'function') {
iconProps.role = 'button';
iconProps.tabIndex = 0;
Expand All @@ -35,13 +40,3 @@ export const Icon = forwardRef(function Icon(props, ref) {
<div {...iconProps} dangerouslySetInnerHTML={{ __html: icon.source }} {...rest} ref={ref} />
);
});

Icon.defaultProps = {
onClick: undefined,
};

Icon.propTypes = {
name: PropTypes.string.isRequired,
onClick: PropTypes.func,
iconSet: PropTypes.string.isRequired,
};
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import PropTypes from 'prop-types';
import styled from '@emotion/styled';
import { Dropdown } from '@wordpress/components';
import { useCallback } from '@wordpress/element';

import { IconPicker } from './icon-picker';
import { IconPicker, IconPickerProps } from './icon-picker';
import { Icon } from './icon';

const StyledIconPickerDropdown = styled(IconPicker)`
Expand All @@ -12,36 +11,29 @@ const StyledIconPickerDropdown = styled(IconPicker)`
height: 248px;
`;

/**
* InlineIconPicker
*
* @typedef InlineIconPickerProps
* @property {string} buttonLabel label
*
* @param {InlineIconPickerProps} props InlineIconPickerProps
* @returns {*}
*/
export const InlineIconPicker = (props) => {
export const InlineIconPicker: React.FC<IconPickerProps> = (props) => {
const { value, ...rest } = props;
const IconButton = useCallback(
({ onToggle }) => (
({ onToggle }: {
onToggle: () => void;
}) => (
<Icon name={value?.name} iconSet={value?.iconSet} onClick={onToggle} {...rest} />
),
[value, rest],
);

IconButton.propTypes = {
onToggle: PropTypes.func.isRequired,
};

return <IconPickerDropdown renderToggle={IconButton} {...props} />;
};

InlineIconPicker.propTypes = {
value: PropTypes.object.isRequired,
};
interface InlineIconPickerProps extends IconPickerProps {
/**
* Render function for the toggle button
* @param props
*/
renderToggle: (props: { onToggle: () => void }) => React.JSX.Element;
}

export const IconPickerDropdown = (props) => {
export const IconPickerDropdown: React.FC<InlineIconPickerProps> = (props) => {
const { renderToggle, ...iconPickerProps } = props;
return (
<Dropdown
Expand All @@ -53,7 +45,3 @@ export const IconPickerDropdown = (props) => {
/>
);
};

IconPickerDropdown.propTypes = {
renderToggle: PropTypes.func.isRequired,
};
Loading

0 comments on commit a2d8101

Please sign in to comment.