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

feat(content-separator): allow ContentSeparator in Menus #652

Merged
merged 18 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
256f6d7
feat(MenuSeperator): add MenuSeperator component
dominiccarrington Sep 24, 2024
32cac48
test(MenuSeperator): add tests
dominiccarrington Sep 24, 2024
138f05b
test(Menu): add snapshot test with seperators
dominiccarrington Sep 24, 2024
84b6e44
test(MenuSeperator): add stricter props test
dominiccarrington Sep 24, 2024
53e82c4
test(MenuSeperator): add keyboard and render tests
dominiccarrington Sep 24, 2024
ec981d9
fix(MenuSelectionGroup): fix styling
dominiccarrington Sep 24, 2024
008e33b
test(MenuSeperator): simplify test
dominiccarrington Sep 24, 2024
a1fcff0
fix(Menu.stories): remove unnecessary aria-labels
dominiccarrington Sep 24, 2024
620d816
test(MenuSeperator): add to other keyboard tests
dominiccarrington Sep 24, 2024
7eb2f66
feat(MenuSeperator): export component
dominiccarrington Sep 24, 2024
f0576c0
fix(MenuSelectionGroup): remove additional className and update snapshot
dominiccarrington Sep 24, 2024
a25266f
fix(MenuSeparator): fix misspelling of component
dominiccarrington Sep 24, 2024
941fc0c
test(MenuSeparator): fix spelling
dominiccarrington Sep 24, 2024
900b402
feat(ContentSeparator): convert MenuSeparator to use ContentSeparator…
dominiccarrington Sep 25, 2024
cbeeee3
test(Menu): add ContentSeparator to keyboard tests
dominiccarrington Sep 25, 2024
2174e5b
test(ContentSeparator): move menu tests
dominiccarrington Sep 25, 2024
0237268
chore(Menu): cleanup import
dominiccarrington Sep 25, 2024
6507bc3
fix(MenuSelectionGroup): remove font-size style
dominiccarrington Sep 25, 2024
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
15 changes: 14 additions & 1 deletion src/components/ContentSeparator/ContentSeparator.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */

import React from 'react';
import classnames from 'classnames';

import './ContentSeparator.style.scss';
import { Props } from './ContentSeparator.types';
import { STYLE } from './ContentSeparator.constants';
import { useSeparator } from '@react-aria/separator';
import { PartialNode } from '@react-stately/collections';

const ContentSeparator: React.FC<Props> = (props: Props) => {
const { className, children, gradient } = props;
Expand All @@ -24,8 +27,18 @@ const ContentSeparator: React.FC<Props> = (props: Props) => {
);
};

// This allows the ContentSeparator to be used within Menu
// @ts-ignore
ContentSeparator.getCollectionNode = function* getCollectionNode<T>(
props: Props
): Generator<PartialNode<T>> {
yield {
type: 'item',
props: { ...props, _isSeparator: true },
};
};

/**
* ContentSeparator with text/other component in the middle
*/

export default ContentSeparator;
37 changes: 37 additions & 0 deletions src/components/ContentSeparator/ContentSeparator.unit.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,41 @@ describe('ContentSeparator', () => {
expect(container.find('li').prop('data-gradient')).toEqual(true);
});
});

describe('menus', () => {
it('getCollectionNode should yield a single node with type "item" and have prop _isSeparator', () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - We don't export this method in the types
const generator = ContentSeparator.getCollectionNode();
const results = Array.from(generator);

expect(results).toHaveLength(1);
expect(results[0]).toEqual({
type: 'item',
props: {
_isSeparator: true,
},
});
});

it('getCollectionNode should yield a single node with props passed', () => {
const props = {
className: 'main-separator',
};

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - We don't export this method in the types
const generator = ContentSeparator.getCollectionNode(props);
const results = Array.from(generator);

expect(results).toHaveLength(1);
expect(results[0]).toEqual({
type: 'item',
props: {
_isSeparator: true,
...props,
},
});
});
});
});
93 changes: 28 additions & 65 deletions src/components/Menu/Menu.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ import Documentation from './Menu.stories.docs.mdx';
import { action } from '@storybook/addon-actions';
import Flex from '../Flex';
import Avatar from '../Avatar';
import ContentSeparator from '../ContentSeparator';
import { PresenceType } from '../Avatar/Avatar.types';
import { ListHeader, ListItemBaseSection, Icon } from '..';
import { Icon, Text } from '..';
import './Menu.stories.style.scss';

export default {
Expand Down Expand Up @@ -82,39 +83,21 @@ Sections.parameters = {
onSelectionChange: menuOnSelectionChange,
onAction: menuOnAction,
children: [
<Section
key="0"
title={
<ListHeader outline={false}>
<ListItemBaseSection position="fill">Europe</ListItemBaseSection>
</ListHeader>
}
>
<Section key="0" title="Europe">
<Item key="00">Spain</Item>
<Item key="01">France</Item>
<Item key="02">Italy</Item>
</Section>,
<Section
key="1"
title={
<ListHeader outline={true} outlinePosition="top" outlineColor="secondary">
<ListItemBaseSection position="fill">Asia</ListItemBaseSection>
</ListHeader>
}
>
<ContentSeparator key="sep-0" />,
<Section key="1" title="Asia">
<Item key="10">India</Item>
<Item key="11">China</Item>
<Item key="12">Japan</Item>
</Section>,
<Section
key="2"
title={
<ListHeader outline={true} outlinePosition="top" outlineColor="secondary">
<ListItemBaseSection position="fill">America</ListItemBaseSection>
</ListHeader>
}
>
<ContentSeparator key="sep-1" />,
<Section key="2" title="America">
<Item key="13">USA</Item>
<ContentSeparator key="sep-21" />
<Item key="14">Mexico</Item>
<Item key="15">Canada</Item>
</Section>,
Expand Down Expand Up @@ -146,52 +129,42 @@ SelectionGroups.parameters = {
<SelectionGroup
key="0"
selectionMode="multiple"
aria-label="First group"
onSelectionChange={(...rest) => {
console.log('singleselection1', rest);
}}
onAction={(...rest) => {
console.log('selectionOnAction1', rest);
}}
title={
<ListHeader outline={false}>
<ListItemBaseSection position="start">
<Icon scale={16} name="speaker" strokeColor="none" />
</ListItemBaseSection>
<ListItemBaseSection position="fill">
Speaker (you can choose many)
</ListItemBaseSection>
</ListHeader>
<Flex direction="row" alignItems="center" xgap="0.25rem">
<Icon scale={16} name="speaker" strokeColor="none" />
<Text type="body-secondary">Speaker (you can choose many)</Text>
</Flex>
}
>
<Item key="00">System default speaker</Item>
<Item key="01">Default - External Headphones (Built-in)</Item>
<ContentSeparator key="sep-21" />
<Item key="02">Desk Pro Web Camera</Item>
<Item key="03">MacBook Pro Speakers</Item>
<Item key="04">Webex Media Audio Device</Item>
</SelectionGroup>,
<ContentSeparator key="sep-0" />,
<SelectionGroup
key="1"
selectionMode="single"
tickPosition="right"
aria-label="Second group"
onSelectionChange={(...rest) => {
console.log('singleselection2', rest);
}}
onAction={(...rest) => {
console.log('selectionOnAction2', rest);
}}
title={
<>
<ListHeader outline={true} outlinePosition="top" outlineColor="secondary">
<ListItemBaseSection position="start">
<Icon scale={16} name="microphone" strokeColor="none" />
</ListItemBaseSection>
<ListItemBaseSection position="fill">
Microphone (you can choose one)
</ListItemBaseSection>
</ListHeader>
</>
<Flex direction="row" alignItems="center" xgap="0.25rem">
<Icon scale={16} name="microphone" strokeColor="none" />
<Text type="body-secondary">Microphone (you can choose one)</Text>
</Flex>
}
>
<Item key="10">No Microphone</Item>
Expand All @@ -200,6 +173,7 @@ SelectionGroups.parameters = {
<Item key="13">MacBook Pro Microphone</Item>
<Item key="14">Webex Media Audio Device</Item>
</SelectionGroup>,
<ContentSeparator key="sep-1" />,
<SelectionGroup
key="2"
tickPosition="none"
Expand All @@ -210,24 +184,17 @@ SelectionGroups.parameters = {
{ key: '22', value: 'Music mode' },
]}
selectionMode="single"
aria-label="Second group"
onSelectionChange={(...rest) => {
console.log('singleselection3', rest);
}}
onAction={(...rest) => {
console.log('selectionOnAction3', rest);
}}
title={
<>
<ListHeader outline={true} outlinePosition="top" outlineColor="secondary">
<ListItemBaseSection position="start">
<Icon scale={16} name="adjust-microphone" strokeColor="none" />
</ListItemBaseSection>
<ListItemBaseSection position="fill">
Webex smart audio (You can choose one)
</ListItemBaseSection>
</ListHeader>
</>
<Flex direction="row" alignItems="center" xgap="0.25rem">
<Icon scale={16} name="adjust-microphone" strokeColor="none" />
<Text type="body-secondary">Webex smart audio (You can choose one)</Text>
</Flex>
}
>
{(item) => (
Expand All @@ -236,6 +203,7 @@ SelectionGroups.parameters = {
</Item>
)}
</SelectionGroup>,
<ContentSeparator key="sep-2" />,
<SelectionGroup
key="3"
tickPosition="none"
Expand All @@ -248,22 +216,17 @@ SelectionGroups.parameters = {
{ key: '32', value: 'Side by side' },
]}
selectionMode="single"
aria-label="Third group"
onSelectionChange={(...rest) => {
console.log('singleselection4', rest);
}}
onAction={(...rest) => {
console.log('selectionOnAction4', rest);
}}
title={
<>
<ListHeader outline={true} outlinePosition="top" outlineColor="secondary">
<ListItemBaseSection position="start">
<Icon scale={16} name="accessibility" strokeColor="none" />
</ListItemBaseSection>
<ListItemBaseSection position="fill">Layout</ListItemBaseSection>
</ListHeader>
</>
<Flex direction="row" alignItems="center" xgap="0.25rem">
nataliadelmar marked this conversation as resolved.
Show resolved Hide resolved
<Icon scale={16} name="accessibility" strokeColor="none" />
<Text type="body-secondary">Layout</Text>
</Flex>
}
>
{(item) => (
Expand Down
20 changes: 18 additions & 2 deletions src/components/Menu/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import MenuItem from '../MenuItem';
import { mergeProps } from '@react-aria/utils';
import MenuSection from '../MenuSection';
import MenuSelectionGroup from '../MenuSelectionGroup';
import ContentSeparator from '../ContentSeparator';

export const MenuContext = React.createContext<MenuContextValue>({});

Expand Down Expand Up @@ -63,10 +64,18 @@ const Menu = <T extends object>(props: Props<T>, providedRef: RefObject<HTMLDivE

const internalRef = useRef();
const ref = providedRef || internalRef;
const itemArray = Array.from(state.collection.getKeys());

const { menuProps } = useMenu(_props, state, ref);
// Ensure all separators are in the disabledKeys set so they are not interactable
// This may need copying instead of modifying due to how React works
itemArray.forEach((key) => {
const item = state.collection.getItem(key);
if (item.props?._isSeparator) {
state.disabledKeys.add(key);
}
});

const itemArray = Array.from(state.collection.getKeys());
const { menuProps } = useMenu(_props, state, ref);

const renderItem = useCallback(
<T extends object>(item: Node<T>, state: TreeState<T>) => {
Expand All @@ -90,6 +99,13 @@ const Menu = <T extends object>(props: Props<T>, providedRef: RefObject<HTMLDivE
return;
}

if (item.props?._isSeparator) {
const props = { ...item.props };
delete props._isSeparator;

return <ContentSeparator {...props} />;
}

let menuItem = (
<MenuItem key={item.key} item={item} state={state} onAction={_props.onAction} />
);
Expand Down
Loading
Loading