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

Composite: add Hover and Typeahead subcomponents #64399

Merged
merged 8 commits into from
Aug 9, 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
1 change: 1 addition & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### New Features

- `Composite`: add stable version of the component ([#63564](https://github.com/WordPress/gutenberg/pull/63564)).
- `Composite`: add `Hover` and `Typeahead` subcomponents ([#64399](https://github.com/WordPress/gutenberg/pull/64399)).

### Enhancements

Expand Down
32 changes: 32 additions & 0 deletions packages/components/src/composite/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,3 +216,35 @@ Allows the component to be rendered as a different HTML element or React compone
The contents of the component.

- Required: no

### `Composite.Hover`

Renders an element in a composite widget that receives focus on mouse move and loses focus to the composite base element on mouse leave. This should be combined with the `Composite.Item` component.

##### `render`: `RenderProp<React.HTMLAttributes<any> & { ref?: React.Ref<any> | undefined; }> | React.ReactElement<any, string | React.JSXElementConstructor<any>>`

Allows the component to be rendered as a different HTML element or React component. The value can be a React element or a function that takes in the original component props and gives back a React element with the props merged.

- Required: no

##### `children`: `React.ReactNode`

The contents of the component.

- Required: no

### `Composite.Typeahead`

Renders a component that adds typeahead functionality to composite components. Hitting printable character keys will move focus to the next composite item that begins with the input characters.

##### `render`: `RenderProp<React.HTMLAttributes<any> & { ref?: React.Ref<any> | undefined; }> | React.ReactElement<any, string | React.JSXElementConstructor<any>>`

Allows the component to be rendered as a different HTML element or React component. The value can be a React element or a function that takes in the original component props and gives back a React element with the props merged.

- Required: no

##### `children`: `React.ReactNode`

The contents of the component.

- Required: no
56 changes: 56 additions & 0 deletions packages/components/src/composite/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import type {
CompositeGroupLabelProps,
CompositeItemProps,
CompositeRowProps,
CompositeHoverProps,
CompositeTypeaheadProps,
} from './types';

/**
Expand Down Expand Up @@ -98,6 +100,22 @@ const Row = forwardRef<
} );
Row.displayName = 'Composite.Row';

const Hover = forwardRef<
HTMLDivElement,
WordPressComponentProps< CompositeHoverProps, 'div', false >
>( function CompositeHover( props, ref ) {
return <Ariakit.CompositeHover { ...props } ref={ ref } />;
} );
Hover.displayName = 'Composite.Hover';

const Typeahead = forwardRef<
HTMLDivElement,
WordPressComponentProps< CompositeTypeaheadProps, 'div', false >
>( function CompositeTypeahead( props, ref ) {
return <Ariakit.CompositeTypeahead { ...props } ref={ ref } />;
} );
Typeahead.displayName = 'Composite.Typeahead';

/**
* Renders a widget based on the WAI-ARIA [`composite`](https://w3c.github.io/aria/#composite)
* role, which provides a single tab stop on the page and arrow key navigation
Expand Down Expand Up @@ -202,5 +220,43 @@ export const Composite = Object.assign(
* ```
*/
Row,
/**
* Renders an element in a composite widget that receives focus on mouse move
* and loses focus to the composite base element on mouse leave. This should
* be combined with the `Composite.Item` component.
*
* @example
* ```jsx
* import { Composite, useCompositeStore } from '@wordpress/components';
*
* const store = useCompositeStore();
* <Composite store={store}>
* <Composite.Hover render={ <Composite.Item /> }>
* Item 1
* </Composite.Hover>
* <Composite.Hover render={ <Composite.Item /> }>
* Item 2
* </Composite.Hover>
* </Composite>
* ```
*/
Hover,
/**
* Renders a component that adds typeahead functionality to composite
* components. Hitting printable character keys will move focus to the next
* composite item that begins with the input characters.
*
* @example
* ```jsx
* import { Composite, useCompositeStore } from '@wordpress/components';
*
* const store = useCompositeStore();
* <Composite store={store} render={ <CompositeTypeahead /> }>
* <Composite.Item>Item 1</Composite.Item>
* <Composite.Item>Item 2</Composite.Item>
* </Composite>
* ```
*/
Typeahead,
}
);
91 changes: 91 additions & 0 deletions packages/components/src/composite/stories/index.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ const meta: Meta< typeof UseCompositeStorePlaceholder > = {
'Composite.Row': Composite.Row,
// @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170
'Composite.Item': Composite.Item,
// @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170
'Composite.Hover': Composite.Hover,
// @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170
'Composite.Typeahead': Composite.Typeahead,
},
argTypes: {
activeId: { control: 'text' },
Expand Down Expand Up @@ -227,6 +231,8 @@ This only affects the composite widget behavior. You still need to set \`dir="rt
'Composite.GroupLabel': commonArgTypes,
'Composite.Row': commonArgTypes,
'Composite.Item': commonArgTypes,
'Composite.Hover': commonArgTypes,
'Composite.Typeahead': commonArgTypes,
};

const name = component.displayName ?? '';
Expand All @@ -237,6 +243,41 @@ This only affects the composite widget behavior. You still need to set \`dir="rt
},
},
},
decorators: [
( Story ) => {
return (
<>
{ /* Visually style the active composite item */ }
<style>{ `
[data-active-item] {
background-color: #ffc0b5;
}
` }</style>
<Story />
<div
style={ {
marginTop: '2em',
fontSize: '12px',
fontStyle: 'italic',
} }
>
{ /* eslint-disable-next-line no-restricted-syntax */ }
<p id="list-title">Notes</p>
<ul aria-labelledby="list-title">
<li>
The active composite item is highlighted with a
different background color;
</li>
<li>
A composite item can be the active item even
when it doesn&apos;t have keyboard focus.
</li>
</ul>
</div>
</>
);
},
],
Comment on lines +246 to +280
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This decorator helps to visually identify the active composite item. It is particularly useful in the Hover example, where composite items become "active" on hover without receiving keyboard focus.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooo, helpful!

};
export default meta;

Expand Down Expand Up @@ -303,3 +344,53 @@ export const Grid: StoryFn< typeof UseCompositeStorePlaceholder > = (
</Composite>
);
};

export const Hover: StoryFn< typeof UseCompositeStorePlaceholder > = (
storeProps
) => {
const rtl = isRTL();
const store = useCompositeStore( { rtl, ...storeProps } );

return (
<Composite store={ store }>
<Composite.Hover render={ <Composite.Item /> }>
Hover item one
</Composite.Hover>
<Composite.Hover render={ <Composite.Item /> }>
Hover item two
</Composite.Hover>
<Composite.Hover render={ <Composite.Item /> }>
Hover item three
</Composite.Hover>
</Composite>
);
};
Hover.parameters = {
docs: {
description: {
story: 'Elements in the composite widget will receive focus on mouse move and lose focus to the composite base element on mouse leave.',
},
},
};

export const Typeahead: StoryFn< typeof UseCompositeStorePlaceholder > = (
storeProps
) => {
const rtl = isRTL();
const store = useCompositeStore( { rtl, ...storeProps } );

return (
<Composite store={ store } render={ <Composite.Typeahead /> }>
<Composite.Item>Apple</Composite.Item>
<Composite.Item>Banana</Composite.Item>
<Composite.Item>Peach</Composite.Item>
</Composite>
);
};
Typeahead.parameters = {
docs: {
description: {
story: 'When focus in on the composite widget, hitting printable character keys will move focus to the next composite item that begins with the input characters.',
},
},
};
28 changes: 28 additions & 0 deletions packages/components/src/composite/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,3 +192,31 @@ export type CompositeRowProps = {
*/
children?: Ariakit.CompositeRowProps[ 'children' ];
};

export type CompositeHoverProps = {
/**
* Allows the component to be rendered as a different HTML element or React
* component. The value can be a React element or a function that takes in the
* original component props and gives back a React element with the props
* merged.
*/
render?: Ariakit.CompositeHoverProps[ 'render' ];
/**
* The contents of the component.
*/
children?: Ariakit.CompositeHoverProps[ 'children' ];
};

export type CompositeTypeaheadProps = {
/**
* Allows the component to be rendered as a different HTML element or React
* component. The value can be a React element or a function that takes in the
* original component props and gives back a React element with the props
* merged.
*/
render?: Ariakit.CompositeTypeaheadProps[ 'render' ];
/**
* The contents of the component.
*/
children?: Ariakit.CompositeTypeaheadProps[ 'children' ];
};
Loading