Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: IconPicker TypeScript Conversion #321

Merged
merged 8 commits into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading