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

[Joy UI] Add ListSubheader component #34191

Merged
merged 32 commits into from Sep 8, 2022
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
d95645e
fix size bug
siriwatknp Sep 5, 2022
2f36905
fix list item classes
siriwatknp Sep 5, 2022
696f193
Add ListSubheader
siriwatknp Sep 5, 2022
46f6dcd
update demos
siriwatknp Sep 5, 2022
e58a034
add tests
siriwatknp Sep 5, 2022
2749ab0
remove instance size from MenuList
siriwatknp Sep 5, 2022
9dc1b38
fix Menu List integration
siriwatknp Sep 5, 2022
44a7062
remove unnecessary instanceSize
siriwatknp Sep 5, 2022
d2715f7
remove classes from ListItem props
siriwatknp Sep 5, 2022
6742f1d
add classes
siriwatknp Sep 5, 2022
654b31f
fix role calculation
siriwatknp Sep 5, 2022
017648e
unskip theme variant test
siriwatknp Sep 5, 2022
fc128e9
fix List size logic
siriwatknp Sep 5, 2022
841e4fa
update MenuListGroup demo
siriwatknp Sep 5, 2022
ed4e51f
rename playground to exclude it from argos
siriwatknp Sep 5, 2022
6cf0c92
fix Input styles
siriwatknp Sep 5, 2022
5502b2c
add comment
siriwatknp Sep 5, 2022
e1355d1
update demo
siriwatknp Sep 5, 2022
95deebe
update TabsDemo
siriwatknp Sep 5, 2022
a889a0b
fix flex
siriwatknp Sep 5, 2022
797e559
run proptypes
siriwatknp Sep 5, 2022
13ff783
fix proptypes
siriwatknp Sep 5, 2022
dfe2673
Merge branch 'joy/misc-fixes13' into joy/list-subheader2
siriwatknp Sep 5, 2022
e9155f4
run proptypes
siriwatknp Sep 5, 2022
af1466e
add tests
siriwatknp Sep 5, 2022
60f1527
add typescript test
siriwatknp Sep 5, 2022
5fbc5fe
Merge branch 'master' of https://github.com/mui/material-ui into joy/…
siriwatknp Sep 7, 2022
cb4a1d2
minor fixes
siriwatknp Sep 7, 2022
1b311ca
fix content
siriwatknp Sep 7, 2022
d620441
fix aria
siriwatknp Sep 7, 2022
cfefa1f
fix disabled decorator color
siriwatknp Sep 7, 2022
3df88c5
Merge branch 'master' of https://github.com/mui/material-ui into joy/…
siriwatknp Sep 8, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
23 changes: 20 additions & 3 deletions docs/data/joy/components/list/ListUsage.js
Expand Up @@ -6,7 +6,9 @@ import ListItemDecorator from '@mui/joy/ListItemDecorator';
import ListItemButton from '@mui/joy/ListItemButton';
import Home from '@mui/icons-material/Home';
import KeyboardArrowRight from '@mui/icons-material/KeyboardArrowRight';
import JoyUsageDemo from 'docs/src/modules/components/JoyUsageDemo';
import JoyUsageDemo, {
prependLinesSpace,
} from 'docs/src/modules/components/JoyUsageDemo';

export default function ListUsage() {
return (
Expand All @@ -27,10 +29,25 @@ export default function ListUsage() {
{
propName: 'selected',
knob: 'switch',
defaultValue: true,
codeBlockDisplay: true,
defaultValue: false,
},
{
propName: 'disabled',
knob: 'switch',
defaultValue: false,
},
{
propName: 'children',
defaultValue: `<ListItemDecorator><Home /></ListItemDecorator>
Home
<KeyboardArrowRight />`,
},
]}
getCodeBlock={(code) => `<List>
<ListItem>
${prependLinesSpace(code, 3)}
</ListItem>
</List>`}
renderDemo={(props) => (
<List sx={{ width: 240, my: 5 }}>
<ListItem>
Expand Down
28 changes: 6 additions & 22 deletions docs/data/joy/components/list/NestedList.js
Expand Up @@ -2,6 +2,7 @@ import * as React from 'react';
import Box from '@mui/joy/Box';
import List from '@mui/joy/List';
import ListItem from '@mui/joy/ListItem';
import ListSubheader from '@mui/joy/ListSubheader';
import ListItemButton from '@mui/joy/ListItemButton';
import Typography from '@mui/joy/Typography';
import Switch from '@mui/joy/Switch';
Expand All @@ -21,23 +22,15 @@ export default function NestedList() {
/>
<List
variant="outlined"
size={small ? 'sm' : undefined}
sx={{
width: 200,
borderRadius: 'sm',
}}
>
<ListItem nested>
<ListItem component="div">
<Typography
id="nested-list-demo-1"
level="body3"
textTransform="uppercase"
fontWeight="lg"
>
Category 1
</Typography>
</ListItem>
<List aria-labelledby="nested-list-demo-1" size={small ? 'sm' : undefined}>
<ListSubheader>Category 1</ListSubheader>
<List>
<ListItem>
<ListItemButton>Subitem 1</ListItemButton>
</ListItem>
Expand All @@ -47,17 +40,8 @@ export default function NestedList() {
</List>
</ListItem>
<ListItem nested>
<ListItem component="div">
<Typography
id="nested-list-demo-2"
level="body3"
textTransform="uppercase"
fontWeight="lg"
>
Category 2
</Typography>
</ListItem>
<List aria-labelledby="nested-list-demo-2" size={small ? 'sm' : undefined}>
<ListSubheader>Category 2</ListSubheader>
<List>
<ListItem>
<ListItemButton>Subitem 1</ListItemButton>
</ListItem>
Expand Down
15 changes: 3 additions & 12 deletions docs/data/joy/components/list/StickyList.js
@@ -1,8 +1,8 @@
import * as React from 'react';
import List from '@mui/joy/List';
import ListItem from '@mui/joy/ListItem';
import ListSubheader from '@mui/joy/ListSubheader';
import ListItemButton from '@mui/joy/ListItemButton';
import Typography from '@mui/joy/Typography';
import Sheet from '@mui/joy/Sheet';

export default function StickyList() {
Expand All @@ -17,17 +17,8 @@ export default function StickyList() {
<List>
{[...Array(5)].map((_, categoryIndex) => (
<ListItem nested key={categoryIndex}>
<ListItem component="div" sticky>
<Typography
id={`sticky-list-demo-${categoryIndex}`}
level="body3"
textTransform="uppercase"
fontWeight="lg"
>
Category {categoryIndex + 1}
</Typography>
</ListItem>
<List aria-labelledby={`sticky-list-demo-${categoryIndex}`}>
<ListSubheader sticky>Category {categoryIndex + 1}</ListSubheader>
<List>
{[...Array(10)].map((__, index) => (
<ListItem key={index}>
<ListItemButton>Subitem {index + 1}</ListItemButton>
Expand Down
3 changes: 2 additions & 1 deletion docs/data/joy/components/list/list.md
Expand Up @@ -18,6 +18,7 @@ Joy UI provides four list-related components:
- [`ListItemDecorator`](#decorator): A decorator of a list item, usually used to display an icon.
- [`ListItemContent`](#ellipsis-content): A container inside a list item, used to display text content.
- [`ListDivider`](#divider): A separator between list items.
- [`ListSubheader`](#nested-list): A label for a nested list.

{{"demo": "ListUsage.js", "hideToolbar": true}}

Expand All @@ -31,7 +32,7 @@ import ListItem from '@mui/joy/ListItem';

export default function MyApp() {
return (
<List aria-labelledby="basic-list-demo">
<List aria-label="basic-list">
<ListItem>Hello, world!</ListItem>
<ListItem>Bye bye, world!</ListItem>
</List>
Expand Down
1 change: 1 addition & 0 deletions packages/mui-joy/src/List/List.tsx
Expand Up @@ -183,6 +183,7 @@ const List = React.forwardRef(function List(inProps, ref) {
className={clsx(classes.root, className)}
ownerState={ownerState}
role={role}
aria-labelledby={typeof nesting === 'string' ? nesting : undefined}
{...other}
>
<ComponentListContext.Provider
Expand Down
2 changes: 1 addition & 1 deletion packages/mui-joy/src/List/ListProps.ts
Expand Up @@ -68,5 +68,5 @@ export interface ListOwnerState extends ListProps {
* @internal
* If `true`, the element is rendered in a nested list item.
*/
nesting: boolean;
nesting: boolean | string;
}
2 changes: 1 addition & 1 deletion packages/mui-joy/src/List/NestedListContext.ts
@@ -1,5 +1,5 @@
import * as React from 'react';

const NestedListContext = React.createContext(false);
const NestedListContext = React.createContext<boolean | string>(false);

export default NestedListContext;
37 changes: 37 additions & 0 deletions packages/mui-joy/src/ListItem/ListItem.test.js
Expand Up @@ -5,6 +5,7 @@ import { ThemeProvider } from '@mui/joy/styles';
import MenuList from '@mui/joy/MenuList';
import List from '@mui/joy/List';
import ListItem, { listItemClasses as classes } from '@mui/joy/ListItem';
import ListSubheader from '@mui/joy/ListSubheader';

describe('Joy <ListItem />', () => {
const { render } = createRenderer();
Expand Down Expand Up @@ -164,4 +165,40 @@ describe('Joy <ListItem />', () => {
expect(screen.getByText('Foo')).to.have.attribute('role', 'menuitem');
});
});

describe('NestedList', () => {
it('the nested list should be labelledby the subheader', () => {
const { getByRole, getByTestId } = render(
<ListItem nested>
<ListSubheader data-testid="subheader">Subheader</ListSubheader>
<List />
</ListItem>,
);

const subheader = getByTestId('subheader');

expect(getByRole('list')).to.have.attribute('aria-labelledby', subheader.id);
});

it('the aria-labelledby can be overridden', () => {
const { getByRole } = render(
<ListItem nested>
<ListSubheader data-testid="subheader">Subheader</ListSubheader>
<List aria-labelledby={undefined} />
</ListItem>,
);

expect(getByRole('list')).not.to.have.attribute('aria-labelledby');
});

it('the nested list should not be labelled without the subheader', () => {
const { getByRole } = render(
<ListItem nested>
<List />
</ListItem>,
);

expect(getByRole('list')).not.to.have.attribute('aria-labelledby');
});
});
});
72 changes: 39 additions & 33 deletions packages/mui-joy/src/ListItem/ListItem.tsx
Expand Up @@ -15,6 +15,7 @@ import NestedListContext from '../List/NestedListContext';
import RowListContext from '../List/RowListContext';
import WrapListContext from '../List/WrapListContext';
import ComponentListContext from '../List/ComponentListContext';
import ListSubheaderDispatch from '../ListSubheader/ListSubheaderContext';

const useUtilityClasses = (ownerState: ListItemOwnerState) => {
const { sticky, nested, nesting, variant, color } = ownerState;
Expand Down Expand Up @@ -96,6 +97,7 @@ const ListItemRoot = styled('li', {
fontSize: 'var(--List-item-fontSize)',
fontFamily: theme.vars.fontFamily.body,
...(ownerState.sticky && {
// sticky in list item can be found in grouped options
position: 'sticky',
top: 'var(--List-item-stickyTop, 0px)', // integration with Menu and Select.
zIndex: 1,
Expand Down Expand Up @@ -157,6 +159,8 @@ const ListItem = React.forwardRef(function ListItem(inProps, ref) {
...other
} = props;

const [subheaderId, setSubheaderId] = React.useState('');

const [listElement, listRole] = listComponent?.split(':') || ['', ''];
const component =
componentProp || (listElement && !listElement.match(/^(ul|ol|menu)$/) ? 'div' : undefined);
Expand Down Expand Up @@ -189,41 +193,43 @@ const ListItem = React.forwardRef(function ListItem(inProps, ref) {

const classes = useUtilityClasses(ownerState);
return (
<NestedListContext.Provider value={nested}>
<ListItemRoot
ref={ref}
as={component}
className={clsx(classes.root, className)}
ownerState={ownerState}
role={role}
{...other}
>
{startAction && (
<ListItemStartAction className={classes.startAction} ownerState={ownerState}>
{startAction}
</ListItemStartAction>
)}
<ListSubheaderDispatch.Provider value={setSubheaderId}>
<NestedListContext.Provider value={nested ? subheaderId || true : false}>
<ListItemRoot
ref={ref}
as={component}
className={clsx(classes.root, className)}
ownerState={ownerState}
role={role}
{...other}
>
{startAction && (
<ListItemStartAction className={classes.startAction} ownerState={ownerState}>
{startAction}
</ListItemStartAction>
)}

{React.Children.map(children, (child, index) =>
React.isValidElement(child)
? React.cloneElement(child, {
// to let ListItem knows when to apply margin(Inline|Block)Start
...(index === 0 && { 'data-first-child': '' }),
...(isMuiElement(child, ['ListItem']) && {
// The ListItem of ListItem should not be 'li'
component: child.props.component || 'div',
}),
})
: child,
)}
{React.Children.map(children, (child, index) =>
React.isValidElement(child)
? React.cloneElement(child, {
// to let ListItem knows when to apply margin(Inline|Block)Start
...(index === 0 && { 'data-first-child': '' }),
...(isMuiElement(child, ['ListItem']) && {
// The ListItem of ListItem should not be 'li'
component: child.props.component || 'div',
}),
})
: child,
)}

{endAction && (
<ListItemEndAction className={classes.endAction} ownerState={ownerState}>
{endAction}
</ListItemEndAction>
)}
</ListItemRoot>
</NestedListContext.Provider>
{endAction && (
<ListItemEndAction className={classes.endAction} ownerState={ownerState}>
{endAction}
</ListItemEndAction>
)}
</ListItemRoot>
</NestedListContext.Provider>
</ListSubheaderDispatch.Provider>
);
}) as OverridableComponent<ListItemTypeMap>;

Expand Down
2 changes: 1 addition & 1 deletion packages/mui-joy/src/ListItem/ListItemProps.ts
Expand Up @@ -71,7 +71,7 @@ export interface ListItemOwnerState extends ListItemProps {
/**
* If `true`, the element is rendered in a nested list item.
*/
nesting: boolean;
nesting: boolean | string;
/**
* @internal
* The internal prop for controlling CSS margin of the element.
Expand Down
4 changes: 4 additions & 0 deletions packages/mui-joy/src/ListItemButton/ListItemButton.tsx
Expand Up @@ -47,6 +47,10 @@ export const ListItemButtonRoot = styled('div', {
...(ownerState.selected && {
'--List-decorator-color': 'initial',
}),
...(ownerState.disabled && {
'--List-decorator-color':
theme.vars.palette[ownerState.color!]?.[`${ownerState.variant!}DisabledColor`],
}),
boxSizing: 'border-box',
position: 'relative',
display: 'flex',
Expand Down
55 changes: 55 additions & 0 deletions packages/mui-joy/src/ListSubheader/ListSubheader.test.tsx
@@ -0,0 +1,55 @@
import * as React from 'react';
import { expect } from 'chai';
import { spy } from 'sinon';
import { describeConformance, createRenderer } from 'test/utils';
import { ThemeProvider } from '@mui/joy/styles';
import ListSubheader, { listSubheaderClasses as classes } from '@mui/joy/ListSubheader';
import ListSubheaderDispatch from './ListSubheaderContext';

describe('Joy <ListSubheader />', () => {
const { render } = createRenderer();

describeConformance(<ListSubheader />, () => ({
classes,
inheritComponent: 'div',
render,
ThemeProvider,
muiName: 'JoyListSubheader',
refInstanceof: window.HTMLDivElement,
testVariantProps: { variant: 'solid' },
testCustomVariant: true,
skip: ['componentsProp', 'classesRoot'],
}));

it('should have root className', () => {
const { container } = render(<ListSubheader />);
expect(container.firstChild).to.have.class(classes.root);
});

it('should accept className prop', () => {
const { container } = render(<ListSubheader className="foo-bar" />);
expect(container.firstChild).to.have.class('foo-bar');
});

it('should have variant class', () => {
const { container } = render(<ListSubheader variant="soft" />);
expect(container.firstChild).to.have.class(classes.variantSoft);
});

it('should have color class', () => {
const { container } = render(<ListSubheader color="success" />);
expect(container.firstChild).to.have.class(classes.colorSuccess);
});

it('should call dispatch context with the generated id', () => {
const dispatch = spy();
const { container } = render(
<ListSubheaderDispatch.Provider value={dispatch}>
<ListSubheader />
</ListSubheaderDispatch.Provider>,
);

// @ts-ignore
expect(dispatch.firstCall.calledWith(container.firstChild?.id)).to.equal(true);
});
});