Skip to content

Commit 01a0713

Browse files
feat(unordered-list): add support for marker types
1 parent 2436a22 commit 01a0713

File tree

15 files changed

+3680
-17
lines changed

15 files changed

+3680
-17
lines changed

IMPLEMENTATION_PLAN.md

Lines changed: 1002 additions & 0 deletions
Large diffs are not rendered by default.

MARKER_TYPE_VERIFICATION_REPORT.md

Lines changed: 810 additions & 0 deletions
Large diffs are not rendered by default.

PR_PREPARATION_GUIDE.md

Lines changed: 620 additions & 0 deletions
Large diffs are not rendered by default.

TESTING_GUIDE.md

Lines changed: 501 additions & 0 deletions
Large diffs are not rendered by default.

packages/react/src/components/UnorderedList/UnorderedList-test.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,91 @@ describe('UnorderedList', () => {
5757
expect(container.firstChild).toHaveClass(`${prefix}--list--unordered`);
5858
expect(container.firstChild).toHaveClass(`${prefix}--list--expressive`);
5959
});
60+
61+
it('should render with disc marker type', () => {
62+
const { container } = render(
63+
<UnorderedList type="disc" data-testid="list">
64+
<ListItem>Item</ListItem>
65+
</UnorderedList>
66+
);
67+
68+
expect(screen.getByTestId('list')).toHaveClass(
69+
`${prefix}--list--marker-disc`
70+
);
71+
});
72+
73+
it('should render with circle marker type', () => {
74+
const { container } = render(
75+
<UnorderedList type="circle" data-testid="list">
76+
<ListItem>Item</ListItem>
77+
</UnorderedList>
78+
);
79+
80+
expect(screen.getByTestId('list')).toHaveClass(
81+
`${prefix}--list--marker-circle`
82+
);
83+
});
84+
85+
it('should render with square marker type', () => {
86+
const { container } = render(
87+
<UnorderedList type="square" data-testid="list">
88+
<ListItem>Item</ListItem>
89+
</UnorderedList>
90+
);
91+
92+
expect(screen.getByTestId('list')).toHaveClass(
93+
`${prefix}--list--marker-square`
94+
);
95+
});
96+
97+
it('should render with hyphen marker type', () => {
98+
const { container } = render(
99+
<UnorderedList type="hyphen" data-testid="list">
100+
<ListItem>Item</ListItem>
101+
</UnorderedList>
102+
);
103+
104+
expect(screen.getByTestId('list')).toHaveClass(
105+
`${prefix}--list--marker-hyphen`
106+
);
107+
});
108+
109+
it('should render with custom marker type', () => {
110+
const { container } = render(
111+
<UnorderedList type="custom" customMarker="→" data-testid="list">
112+
<ListItem>Item</ListItem>
113+
</UnorderedList>
114+
);
115+
116+
expect(screen.getByTestId('list')).toHaveClass(
117+
`${prefix}--list--marker-custom`
118+
);
119+
expect(screen.getByTestId('list')).toHaveStyle({
120+
'--cds--list--marker-content': "'→'",
121+
});
122+
});
123+
124+
it('should default to hyphen marker for top-level lists', () => {
125+
const { container } = render(
126+
<UnorderedList data-testid="list">
127+
<ListItem>Item</ListItem>
128+
</UnorderedList>
129+
);
130+
131+
expect(screen.getByTestId('list')).toHaveClass(
132+
`${prefix}--list--marker-hyphen`
133+
);
134+
});
135+
136+
it('should default to square marker for nested lists', () => {
137+
const { container } = render(
138+
<UnorderedList nested data-testid="list">
139+
<ListItem>Item</ListItem>
140+
</UnorderedList>
141+
);
142+
143+
expect(screen.getByTestId('list')).toHaveClass(
144+
`${prefix}--list--marker-square`
145+
);
146+
});
60147
});

packages/react/src/components/UnorderedList/UnorderedList.mdx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { stackblitzPrefillConfig } from '../../../previewer/codePreviewer';
1717
## Table of Contents
1818

1919
- [Overview](#overview)
20+
- [Marker Types](#marker-types)
2021
- [Nested](#nested)
2122
- [Component API](#component-api)
2223
- [Feedback](#feedback)
@@ -35,6 +36,44 @@ import { stackblitzPrefillConfig } from '../../../previewer/codePreviewer';
3536
]}
3637
/>
3738

39+
## Marker Types
40+
41+
The `UnorderedList` component supports different marker types for list items. You can specify the marker type using the `type` prop:
42+
43+
- **`disc`**: Filled circle (•) - uses native CSS `list-style-type: disc`
44+
- **`circle`**: Hollow circle (○) - uses native CSS `list-style-type: circle`
45+
- **`square`**: Filled square (▪) - uses native CSS `list-style-type: square`
46+
- **`hyphen`**: En dash (–) - uses a custom `::before` pseudo-element (default for top-level lists)
47+
- **`custom`**: Custom marker - uses a custom `::before` pseudo-element with content from the `customMarker` prop
48+
49+
When no `type` is specified:
50+
- Top-level lists default to `hyphen`
51+
- Nested lists default to `square` (deprecated - will inherit parent type in next major release)
52+
53+
<Canvas
54+
of={UnorderedList.MarkerTypes}
55+
additionalActions={[
56+
{
57+
title: 'Open in Stackblitz',
58+
onClick: () => stackblitzPrefillConfig(UnorderedList.MarkerTypes),
59+
},
60+
]}
61+
/>
62+
63+
### Nested Lists with Marker Types
64+
65+
You can specify different marker types for nested lists. In the next major release, nested lists without an explicit `type` will inherit the parent list's marker type.
66+
67+
<Canvas
68+
of={UnorderedList.NestedWithMarkerTypes}
69+
additionalActions={[
70+
{
71+
title: 'Open in Stackblitz',
72+
onClick: () => stackblitzPrefillConfig(UnorderedList.NestedWithMarkerTypes),
73+
},
74+
]}
75+
/>
76+
3877
## Nested
3978

4079
<Canvas

packages/react/src/components/UnorderedList/UnorderedList.stories.js

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,18 @@ Default.argTypes = {
4444
type: 'boolean',
4545
},
4646
},
47+
type: {
48+
control: {
49+
type: 'select',
50+
},
51+
options: ['disc', 'circle', 'square', 'hyphen', 'custom'],
52+
},
53+
customMarker: {
54+
control: {
55+
type: 'text',
56+
},
57+
if: { arg: 'type', eq: 'custom' },
58+
},
4759
};
4860

4961
export const Nested = () => {
@@ -78,3 +90,76 @@ export const Nested = () => {
7890
};
7991

8092
Nested.storyName = 'nested';
93+
94+
export const MarkerTypes = () => {
95+
return (
96+
<div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
97+
<div>
98+
<h3>Disc (default filled circle)</h3>
99+
<UnorderedList type="disc">
100+
<ListItem>Item with disc marker</ListItem>
101+
<ListItem>Item with disc marker</ListItem>
102+
<ListItem>Item with disc marker</ListItem>
103+
</UnorderedList>
104+
</div>
105+
<div>
106+
<h3>Circle (hollow circle)</h3>
107+
<UnorderedList type="circle">
108+
<ListItem>Item with circle marker</ListItem>
109+
<ListItem>Item with circle marker</ListItem>
110+
<ListItem>Item with circle marker</ListItem>
111+
</UnorderedList>
112+
</div>
113+
<div>
114+
<h3>Square</h3>
115+
<UnorderedList type="square">
116+
<ListItem>Item with square marker</ListItem>
117+
<ListItem>Item with square marker</ListItem>
118+
<ListItem>Item with square marker</ListItem>
119+
</UnorderedList>
120+
</div>
121+
<div>
122+
<h3>Hyphen (default for top-level)</h3>
123+
<UnorderedList type="hyphen">
124+
<ListItem>Item with hyphen marker</ListItem>
125+
<ListItem>Item with hyphen marker</ListItem>
126+
<ListItem>Item with hyphen marker</ListItem>
127+
</UnorderedList>
128+
</div>
129+
<div>
130+
<h3>Custom marker</h3>
131+
<UnorderedList type="custom" customMarker="→">
132+
<ListItem>Item with custom arrow marker</ListItem>
133+
<ListItem>Item with custom arrow marker</ListItem>
134+
<ListItem>Item with custom arrow marker</ListItem>
135+
</UnorderedList>
136+
</div>
137+
</div>
138+
);
139+
};
140+
141+
MarkerTypes.storyName = 'marker types';
142+
143+
export const NestedWithMarkerTypes = () => {
144+
return (
145+
<UnorderedList type="disc">
146+
<ListItem>
147+
Level 1 with disc
148+
<UnorderedList nested type="circle">
149+
<ListItem>Level 2 with circle</ListItem>
150+
<ListItem>
151+
Level 2 with circle
152+
<UnorderedList nested type="square">
153+
<ListItem>Level 3 with square</ListItem>
154+
<ListItem>Level 3 with square</ListItem>
155+
</UnorderedList>
156+
</ListItem>
157+
</UnorderedList>
158+
</ListItem>
159+
<ListItem>Level 1 with disc</ListItem>
160+
<ListItem>Level 1 with disc</ListItem>
161+
</UnorderedList>
162+
);
163+
};
164+
165+
NestedWithMarkerTypes.storyName = 'nested with marker types';

packages/react/src/components/UnorderedList/UnorderedList.tsx

Lines changed: 85 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,85 @@
66
*/
77

88
import PropTypes from 'prop-types';
9-
import React, { type ComponentProps } from 'react';
9+
import React, { type ComponentProps, useEffect, useRef } from 'react';
1010
import classnames from 'classnames';
1111
import { usePrefix } from '../../internal/usePrefix';
12+
import { warning } from '../../internal/warning';
13+
14+
export type UnorderedListMarkerType =
15+
| 'disc'
16+
| 'circle'
17+
| 'square'
18+
| 'hyphen'
19+
| 'custom';
1220

1321
export interface UnorderedListProps extends ComponentProps<'ul'> {
1422
nested?: boolean | undefined;
1523
isExpressive?: boolean | undefined;
24+
type?: UnorderedListMarkerType | undefined;
25+
customMarker?: string | undefined;
1626
}
1727

1828
export default function UnorderedList({
1929
className,
2030
nested = false,
2131
isExpressive = false,
32+
type,
33+
customMarker,
34+
style,
2235
...other
2336
}: UnorderedListProps) {
2437
const prefix = usePrefix();
25-
const classNames = classnames(`${prefix}--list--unordered`, className, {
26-
[`${prefix}--list--nested`]: nested,
27-
[`${prefix}--list--expressive`]: isExpressive,
28-
});
29-
return <ul className={classNames} {...other} />;
38+
const hasWarnedRef = useRef(false);
39+
40+
// Determine marker type: use provided type, or default based on nesting
41+
const markerType: UnorderedListMarkerType | undefined =
42+
type ||
43+
(nested ? 'square' : 'hyphen');
44+
45+
// Show deprecation warning for nested lists without explicit type
46+
useEffect(() => {
47+
if (
48+
nested &&
49+
!type &&
50+
!hasWarnedRef.current &&
51+
process.env.NODE_ENV !== 'production'
52+
) {
53+
warning(
54+
false,
55+
'Nested unordered lists without an explicit `type` prop will default to ' +
56+
'square markers. This behavior is deprecated. Please explicitly set ' +
57+
'`type="square"` (or another marker type) for nested lists. ' +
58+
'In the next major release, nested lists will inherit the parent list\'s marker type.'
59+
);
60+
hasWarnedRef.current = true;
61+
}
62+
}, [nested, type]);
63+
64+
// Build class names
65+
const classNames = classnames(
66+
`${prefix}--list--unordered`,
67+
className,
68+
{
69+
[`${prefix}--list--nested`]: nested,
70+
[`${prefix}--list--expressive`]: isExpressive,
71+
[`${prefix}--list--marker-${markerType}`]: markerType,
72+
}
73+
);
74+
75+
// Build styles for custom marker
76+
const customStyles: React.CSSProperties = {
77+
...style,
78+
...(markerType === 'custom' && customMarker
79+
? {
80+
[`--${prefix}--list--marker-content`]: `'${customMarker}'`,
81+
}
82+
: {}),
83+
};
84+
85+
return (
86+
<ul className={classNames} style={customStyles} {...other} />
87+
);
3088
}
3189

3290
UnorderedList.propTypes = {
@@ -49,4 +107,25 @@ UnorderedList.propTypes = {
49107
* Specify whether the list is nested, or not
50108
*/
51109
nested: PropTypes.bool,
110+
111+
/**
112+
* Specify the marker type for the list items.
113+
* - `disc`: filled circle (•)
114+
* - `circle`: hollow circle (○)
115+
* - `square`: filled square (▪)
116+
* - `hyphen`: en dash (default for top-level lists) (–)
117+
* - `custom`: custom marker (requires `customMarker` prop)
118+
*
119+
* When not specified:
120+
* - Top-level lists default to `hyphen`
121+
* - Nested lists default to `square` (deprecated - will inherit parent type in next major release)
122+
*/
123+
type: PropTypes.oneOf(['disc', 'circle', 'square', 'hyphen', 'custom']),
124+
125+
/**
126+
* Specify a custom marker character/content.
127+
* Only used when `type="custom"`.
128+
* The value will be used as the CSS content for the marker.
129+
*/
130+
customMarker: PropTypes.string,
52131
};

0 commit comments

Comments
 (0)