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

Add non-listbox functionality back to CircularOptionPicker #54290

Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
b976cb3
Refactoring `CircularOptionPicker`
Sep 7, 2023
a5ef383
Updating `CircularOptionPicker` consumers
Sep 8, 2023
4d9c7df
Merge branch 'trunk' into 54239/allow-circularoptionpicker-non-listbox
Sep 8, 2023
158207d
Updating CHANGELOG.md
Sep 8, 2023
8de8875
Fixing circular dependency
Sep 8, 2023
bd811c1
Merge branch 'trunk' into 54239/allow-circularoptionpicker-non-listbox
Sep 12, 2023
d9cac8f
Updating subcomponent filenames
Sep 12, 2023
954932b
Simplifying `selectedIconProps` logic
Sep 13, 2023
e3ab6d2
Refactoring `commonProps`
Sep 13, 2023
61c6762
Refactoring render functions into components
Sep 13, 2023
a65f483
Letting `Button` deal with classes
Sep 13, 2023
b446436
Splitting state props into multiple effects
Sep 13, 2023
33ef3a8
Reverting `disableLooping` prop to `loop`
Sep 13, 2023
77e38f7
Adding note explaining `aria-pressed={null}`
Sep 13, 2023
96acbc5
Fixing some typing, removing `any`
Sep 13, 2023
08dca39
Reverting back to manual `is-pressed` style
Sep 13, 2023
056393f
Merge branch 'trunk' into 54239/allow-circularoptionpicker-non-listbox
Sep 13, 2023
eb916e0
Refactoring `Omit`
Sep 13, 2023
5eb54b2
Updating `color-palette` snapshot
Sep 13, 2023
be7ce54
Wrapping Option implementations with `forwardRef`
Sep 13, 2023
87cafaf
Overriding `WithLoopingDisabled` code sample to show `loop`
Sep 14, 2023
0e4eb2a
Refactoring `asButtons` typing
Sep 15, 2023
880a1a1
Refactoring `compositeState` accessors
Sep 15, 2023
3deef13
Updating various README docs with new props
Sep 15, 2023
6fd9ba6
Adding note to explain class additions
Sep 15, 2023
2f55f86
Refactoring `listbox` implementation to keep `children` out of option…
Sep 15, 2023
a7b82e8
Re-refactoring `listbox` implementation
Sep 15, 2023
87a3b2b
Removing redundant `@default`
Sep 15, 2023
5007d78
Merge branch 'trunk' into 54239/allow-circularoptionpicker-non-listbox
ciampo Sep 15, 2023
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
1 change: 1 addition & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- `ToggleGroupControl`: Rewrite backdrop animation using framer motion shared layout animations, add better support for controlled and uncontrolled modes ([#50278](https://github.com/WordPress/gutenberg/pull/50278)).
- `Popover`: Add the `is-positioned` CSS class only after the popover has finished animating ([#54178](https://github.com/WordPress/gutenberg/pull/54178)).
- `Tooltip`: Replace the existing tooltip to simplify the implementation and improve accessibility while maintaining the same behaviors and API ([#48440](https://github.com/WordPress/gutenberg/pull/48440)).
- `CircularOptionPicker`: Add option to use previous non-listbox behaviour, for contexts where buttons are more appropriate than a list of options ([#54290](https://github.com/WordPress/gutenberg/pull/54290)).

### Bug Fix

Expand Down
262 changes: 4 additions & 258 deletions packages/components/src/circular-option-picker/index.tsx
Original file line number Diff line number Diff line change
@@ -1,264 +1,10 @@
/**
* External dependencies
*/
import classnames from 'classnames';

/**
* WordPress dependencies
*/
import { useInstanceId } from '@wordpress/compose';
import { createContext, useContext, useEffect } from '@wordpress/element';
import { Icon, check } from '@wordpress/icons';
import { isRTL } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import Button from '../button';
import { Composite, CompositeItem, useCompositeState } from '../composite';
import Dropdown from '../dropdown';
import Tooltip from '../tooltip';
import type {
CircularOptionPickerProps,
DropdownLinkActionProps,
OptionGroupProps,
OptionProps,
} from './types';
import type { WordPressComponentProps } from '../ui/context';
import type { ButtonAsButtonProps } from '../button/types';

const CircularOptionPickerContext = createContext( {} );

const hasSelectedOption = new Map();

export function Option( {
className,
isSelected,
selectedIconProps,
tooltipText,
...additionalProps
}: OptionProps ) {
const compositeState = useContext( CircularOptionPickerContext );
const {
baseId = 'option',
currentId,
setCurrentId,
} = compositeState as any;
const id = useInstanceId( Option, baseId );

useEffect( () => {
// If we call `setCurrentId` here, it doesn't update for other
// Option renders in the same pass. So we have to store our own
// map to make sure that we only set the first selected option.
// We still need to check `currentId` because the control will
// update this as the user moves around, and that state should
// be maintained as the group gains and loses focus.
if ( isSelected && ! currentId && ! hasSelectedOption.get( baseId ) ) {
hasSelectedOption.set( baseId, true );
setCurrentId( id );
}
} );

const optionControl = (
<CompositeItem
as={ Button }
className={ classnames(
'components-circular-option-picker__option',
{
'is-pressed': isSelected,
}
) }
id={ id }
{ ...additionalProps }
{ ...compositeState }
role="option"
aria-selected={ !! isSelected }
/>
);

return (
<div
className={ classnames(
className,
'components-circular-option-picker__option-wrapper'
) }
>
{ tooltipText ? (
<Tooltip text={ tooltipText }>{ optionControl }</Tooltip>
) : (
optionControl
) }
{ isSelected && (
<Icon
icon={ check }
{ ...( selectedIconProps ? selectedIconProps : {} ) }
/>
) }
</div>
);
}

export function OptionGroup( {
className,
options,
...additionalProps
}: OptionGroupProps ) {
const { 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby } =
additionalProps as any;
const role = ariaLabel || ariaLabelledby ? 'group' : undefined;

return (
<div
{ ...additionalProps }
role={ role }
className={ classnames(
'components-circular-option-picker__option-group',
'components-circular-option-picker__swatches',
className
) }
>
{ options }
</div>
);
}

export function DropdownLinkAction( {
buttonProps,
className,
dropdownProps,
linkText,
}: DropdownLinkActionProps ) {
return (
<Dropdown
className={ classnames(
'components-circular-option-picker__dropdown-link-action',
className
) }
renderToggle={ ( { isOpen, onToggle } ) => (
<Button
aria-expanded={ isOpen }
aria-haspopup="true"
onClick={ onToggle }
variant="link"
{ ...buttonProps }
>
{ linkText }
</Button>
) }
{ ...dropdownProps }
/>
);
}

export function ButtonAction( {
className,
children,
...additionalProps
}: WordPressComponentProps< ButtonAsButtonProps, 'button', false > ) {
return (
<Button
className={ classnames(
'components-circular-option-picker__clear',
className
) }
variant="tertiary"
{ ...additionalProps }
>
{ children }
</Button>
);
}

/**
*`CircularOptionPicker` is a component that displays a set of options as circular buttons.
*
* ```jsx
* import { CircularOptionPicker } from '../circular-option-picker';
* import { useState } from '@wordpress/element';
*
* const Example = () => {
* const [ currentColor, setCurrentColor ] = useState();
* const colors = [
* { color: '#f00', name: 'Red' },
* { color: '#0f0', name: 'Green' },
* { color: '#00f', name: 'Blue' },
* ];
* const colorOptions = (
* <>
* { colors.map( ( { color, name }, index ) => {
* return (
* <CircularOptionPicker.Option
* key={ `${ color }-${ index }` }
* tooltipText={ name }
* style={ { backgroundColor: color, color } }
* isSelected={ index === currentColor }
* onClick={ () => setCurrentColor( index ) }
* aria-label={ name }
* />
* );
* } ) }
* </>
* );
* return (
* <CircularOptionPicker
* options={ colorOptions }
* actions={
* <CircularOptionPicker.ButtonAction
* onClick={ () => setCurrentColor( undefined ) }
* >
* { 'Clear' }
* </CircularOptionPicker.ButtonAction>
* }
* />
* );
* };
* ```
*/

function CircularOptionPicker( props: CircularOptionPickerProps ) {
const {
actions,
className,
id: idProp,
options,
children,
loop = true,
...additionalProps
} = props;
const id = useInstanceId( CircularOptionPicker, 'option-picker', idProp );
const rtl = isRTL();
const compositeState = useCompositeState( { baseId: id, loop, rtl } );

return (
<Composite
{ ...additionalProps }
{ ...compositeState }
role={ 'listbox' }
className={ classnames(
'components-circular-option-picker',
className
) }
>
<CircularOptionPickerContext.Provider value={ compositeState }>
<div
className={ 'components-circular-option-picker__swatches' }
>
{ options }
</div>
{ children }
{ actions && (
<div className="components-circular-option-picker__custom-clear-wrapper">
{ actions }
</div>
) }
</CircularOptionPickerContext.Provider>
</Composite>
);
}
import CircularOptionPicker from './option-picker';

CircularOptionPicker.Option = Option;
CircularOptionPicker.OptionGroup = OptionGroup;
CircularOptionPicker.ButtonAction = ButtonAction;
CircularOptionPicker.DropdownLinkAction = DropdownLinkAction;
export { Option } from './option-picker-option';
export { OptionGroup } from './option-picker-option-group';
export { ButtonAction, DropdownLinkAction } from './option-picker-actions';

export default CircularOptionPicker;
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* External dependencies
*/
import classnames from 'classnames';

/**
* Internal dependencies
*/
import Button from '../button';
import Dropdown from '../dropdown';
import type { DropdownLinkActionProps } from './types';
import type { WordPressComponentProps } from '../ui/context';
import type { ButtonAsButtonProps } from '../button/types';

export function DropdownLinkAction( {
buttonProps,
className,
dropdownProps,
linkText,
}: DropdownLinkActionProps ) {
return (
<Dropdown
className={ classnames(
'components-circular-option-picker__dropdown-link-action',
className
) }
renderToggle={ ( { isOpen, onToggle } ) => (
<Button
aria-expanded={ isOpen }
aria-haspopup="true"
onClick={ onToggle }
variant="link"
{ ...buttonProps }
>
{ linkText }
</Button>
) }
{ ...dropdownProps }
/>
);
}

export function ButtonAction( {
className,
children,
...additionalProps
}: WordPressComponentProps< ButtonAsButtonProps, 'button', false > ) {
return (
<Button
className={ classnames(
'components-circular-option-picker__clear',
className
) }
variant="tertiary"
{ ...additionalProps }
>
{ children }
</Button>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* WordPress dependencies
*/
import { createContext } from '@wordpress/element';

export const CircularOptionPickerContext = createContext( {} );
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* External dependencies
*/
import classnames from 'classnames';

/**
* Internal dependencies
*/
import type { OptionGroupProps } from './types';

export function OptionGroup( {
className,
options,
...additionalProps
}: OptionGroupProps ) {
const { 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby } =
additionalProps as any;
andrewhayward marked this conversation as resolved.
Show resolved Hide resolved
const role = ariaLabel || ariaLabelledby ? 'group' : undefined;

return (
<div
{ ...additionalProps }
role={ role }
className={ classnames(
'components-circular-option-picker__option-group',
'components-circular-option-picker__swatches',
className
) }
>
{ options }
</div>
);
}
Loading