Skip to content

Commit ad7e38e

Browse files
Reid BarberreidbarberLFDanLu
authored andcommitted
Breadcrumbs (#148)
* initial Breadcrumbs work * improve link styles * simplify styles * add flex to macro * fix multiline * more style improvements * allow more link props * pass down link props * remove s2 link context and styles added * remove underline for disabled link * review follow-up * use marginStart: text-to-visual for chevron * remove design link * remove shorthand flex from theme * remove nav wrapper * update styles * spread props onto RAC breadcrumbs * add onAction story * add styles to Heading * ad Heading story to Breadcrumbs * fixing breadcrumb vertical padding when multiline only had to change the height, the gap between the top and current breadcrumb is already provided by the row gap of 4 which equals the breadcrumbs-start-edge-to-truncated-menu value in the spec. This value is also moreaccurate than the 9px they have for top-text-to-to-bottom-text since it starts at the bottom of the breadcrumb box * fix active style, chevron centering, and tentative styles for multiline see comments for uncertainties with margins and multiline in design * update default, wrapper padding, and isMultiLine sizing as per design feedback * review comments * fix Safari focus ring cut off * get rid of default heading styling for now --------- Co-authored-by: Reid Barber <[email protected]> Co-authored-by: danilu <[email protected]>
1 parent 34e3fa0 commit ad7e38e

File tree

3 files changed

+346
-10
lines changed

3 files changed

+346
-10
lines changed

packages/@react-spectrum/s2/src/Breadcrumbs.tsx

Lines changed: 295 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,301 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13-
import {Breadcrumbs as RACBreadcrumbs, BreadcrumbsProps} from 'react-aria-components';
13+
import {Breadcrumbs as RACBreadcrumbs, BreadcrumbsProps as AriaBreadcrumbsProps, Breadcrumb as AriaBreadcrumb, Provider, Link, HeadingContext} from 'react-aria-components';
14+
import {StyleProps, focusRing, getAllowedOverrides} from './style-utils' with {type: 'macro'};
15+
import {size, style} from '../style/spectrum-theme' with { type: 'macro' };
16+
import {forwardRefType} from './types';
17+
import {Children, ReactElement, ReactNode, cloneElement, createContext, forwardRef, isValidElement, useContext, useRef} from 'react';
18+
import {AriaBreadcrumbItemProps} from 'react-aria';
19+
import ChevronIcon from '../ui-icons/Chevron';
20+
import {LinkDOMProps} from '@react-types/shared';
1421

22+
interface BreadcrumbsStyleProps {
23+
/**
24+
* Size of the Breadcrumbs including spacing and layout.
25+
*
26+
* @default 'M'
27+
*/
28+
size?: 'M' | 'L',
29+
/** Whether the breadcrumbs are disabled. */
30+
isDisabled?: boolean,
31+
/**
32+
* Whether to place the last Breadcrumb item onto a new line.
33+
*/
34+
isMultiline?: boolean
35+
/** Whether to always show the root item if the items are collapsed. */
36+
// TODO: showRoot?: boolean,
37+
}
38+
39+
export interface BreadcrumbsProps<T> extends Omit<AriaBreadcrumbsProps<T>, 'children' | 'style' | 'className'>, BreadcrumbsStyleProps, StyleProps {
40+
/** The children of the Breadcrumbs. */
41+
children?: ReactNode
42+
}
43+
44+
const wrapper = style<BreadcrumbsStyleProps>({
45+
display: 'flex',
46+
justifyContent: 'start',
47+
listStyleType: 'none',
48+
flexWrap: {
49+
default: 'nowrap',
50+
isMultiline: 'wrap'
51+
},
52+
flexGrow: 1,
53+
flexShrink: 0,
54+
flexBasis: 0,
55+
gap: {
56+
size: {
57+
M: size(6), // breadcrumbs-text-to-separator-medium
58+
L: size(9) // breadcrumbs-text-to-separator-large
59+
},
60+
isMultiline: 4
61+
},
62+
padding: 0,
63+
transition: 'default',
64+
marginTop: 0,
65+
marginBottom: 0,
66+
marginStart: {
67+
size: {
68+
M: size(6),
69+
L: size(9)
70+
},
71+
isMultiline: 4
72+
}
73+
}, getAllowedOverrides());
74+
75+
const BreadcrumbsInternalContext = createContext<BreadcrumbsProps<any> & {length: number}>({length: 0});
76+
77+
function Breadcrumbs<T extends object>({
78+
UNSAFE_className = '',
79+
UNSAFE_style,
80+
styles,
81+
...props
82+
}: BreadcrumbsProps<T>) {
83+
let {size = 'M', isMultiline, isDisabled} = props;
84+
let ref = useRef(null);
85+
// TODO: Remove when https://github.com/adobe/react-spectrum/pull/6440 is released
86+
let childArray: ReactElement[] = [];
87+
Children.forEach(props.children, (child, index) => {
88+
if (isValidElement<{index: number}>(child)) {
89+
child = cloneElement(child, {key: index, index});
90+
childArray.push(child);
91+
}
92+
});
93+
return (
94+
<RACBreadcrumbs
95+
{...props}
96+
ref={ref}
97+
style={UNSAFE_style}
98+
className={UNSAFE_className + wrapper({
99+
size,
100+
isMultiline
101+
}, styles)}>
102+
<Provider
103+
values={[
104+
[BreadcrumbsInternalContext, {size, isDisabled, isMultiline, length: childArray.length}]
105+
]}>
106+
{childArray}
107+
</Provider>
108+
</RACBreadcrumbs>
109+
);
110+
}
111+
112+
/** Breadcrumbs show hierarchy and navigational context for a user’s location within an application. */
113+
let _Breadcrumbs = /*#__PURE__*/ (forwardRef as forwardRefType)(Breadcrumbs);
114+
export {_Breadcrumbs as Breadcrumbs};
115+
116+
const breadcrumbStyles = style({
117+
display: 'inline-flex',
118+
flexShrink: {
119+
isMultiline: {
120+
isCurrent: 0
121+
}
122+
},
123+
flexBasis: {
124+
isMultiline: {
125+
isCurrent: 'full'
126+
}
127+
},
128+
alignItems: 'center',
129+
justifyContent: 'start',
130+
height: {
131+
default: 'control',
132+
isMultiline: {
133+
default: 24,
134+
isCurrent: 35
135+
}
136+
},
137+
transition: 'default',
138+
position: 'relative',
139+
color: {
140+
default: 'neutral',
141+
isDisabled: 'disabled',
142+
forcedColors: {
143+
default: 'ButtonText',
144+
isDisabled: 'GrayText'
145+
}
146+
},
147+
borderStyle: 'none'
148+
});
149+
150+
const chevronStyles = style({
151+
marginStart: 'text-to-visual',
152+
'--iconPrimary': {
153+
type: 'fill',
154+
value: 'currentColor'
155+
}
156+
});
157+
158+
const linkStyles = style({
159+
...focusRing(),
160+
borderRadius: 'sm',
161+
color: {
162+
default: 'neutral-subdued',
163+
isDisabled: 'disabled',
164+
isCurrent: 'neutral',
165+
forcedColors: {
166+
default: 'LinkText',
167+
isDisabled: 'GrayText'
168+
}
169+
},
170+
transition: 'default',
171+
fontFamily: 'sans',
172+
fontSize: {
173+
default: 'control',
174+
isMultiline: {
175+
default: 'ui-sm'
176+
}
177+
},
178+
fontWeight: {
179+
default: 'normal',
180+
isCurrent: 'bold',
181+
isMultiline: {
182+
isCurrent: 'extra-bold'
183+
}
184+
},
185+
textDecoration: {
186+
default: 'none',
187+
isHovered: 'underline',
188+
isFocusVisible: 'underline',
189+
isDisabled: 'none'
190+
},
191+
cursor: {
192+
default: 'pointer',
193+
isDisabled: 'default'
194+
},
195+
outlineColor: {
196+
default: 'focus-ring',
197+
forcedColors: 'Highlight'
198+
},
199+
disableTapHighlight: true,
200+
marginTop: {
201+
default: {
202+
size: {
203+
M: size(6), // component-top-to-text-100
204+
L: size(9) // component-top-to-text-200
205+
}
206+
},
207+
isMultiline: size(4)
208+
},
209+
marginBottom: {
210+
default: {
211+
size: {
212+
M: size(8), // component-bottom-to-text-100
213+
L: size(11) // component-bottom-to-text-200
214+
}
215+
},
216+
isMultiline: size(5)
217+
}
218+
});
219+
220+
const currentStyles = style({
221+
color: {
222+
default: 'neutral',
223+
forcedColors: 'ButtonText'
224+
},
225+
transition: 'default',
226+
fontFamily: 'sans',
227+
fontSize: {
228+
default: 'control',
229+
isMultiline: 'heading-lg' // TODO: Customizable, but it’s preferred to use: heading-size-s, heading-size-m, heading-size-l (default), and heading-size-xl
230+
},
231+
fontWeight: {
232+
default: 'bold',
233+
isMultiline: 'extra-bold'
234+
},
235+
marginTop: {
236+
default: {
237+
size: {
238+
M: size(6), // component-top-to-text-100
239+
L: size(9) // component-top-to-text-200
240+
}
241+
},
242+
isMultiline: 0
243+
},
244+
marginBottom: {
245+
default: {
246+
size: {
247+
M: size(9), // component-bottom-to-text-100
248+
L: size(11) // component-bottom-to-text-200
249+
}
250+
},
251+
isMultiline: size(9)
252+
}
253+
});
254+
255+
// TODO: support user heading size customization, for now just set it to large
256+
const heading = style({
257+
margin: 0,
258+
fontFamily: 'sans',
259+
fontSize: 'heading-lg',
260+
fontWeight: 'extra-bold'
261+
});
262+
263+
export interface BreadcrumbProps extends Omit<AriaBreadcrumbItemProps, 'children' | 'style' | 'className'>, LinkDOMProps {
264+
/** The children of the breadcrumb item. */
265+
children?: ReactNode
266+
}
15267

16-
export function Breadcrumbs<T extends object>(props: BreadcrumbsProps<T>) {
17-
return <RACBreadcrumbs {...props} />;
268+
export function Breadcrumb({children, ...props}: BreadcrumbProps) {
269+
let {href, target, rel, download, ping, referrerPolicy, ...other} = props;
270+
let {size = 'M', isMultiline, length, isDisabled} = useContext(BreadcrumbsInternalContext);
271+
let ref = useRef(null);
272+
// TODO: use isCurrent render prop when https://github.com/adobe/react-spectrum/pull/6440 is released
273+
let isCurrent = (props as BreadcrumbProps & {index: number}).index === length - 1;
274+
return (
275+
<AriaBreadcrumb
276+
{...other}
277+
ref={ref}
278+
className={breadcrumbStyles({size, isMultiline, isCurrent})} >
279+
{isCurrent ?
280+
<span
281+
className={currentStyles({size, isMultiline, isCurrent})}>
282+
<Provider
283+
values={[
284+
[HeadingContext, {className: heading}]
285+
]}>
286+
{children}
287+
</Provider>
288+
</span>
289+
: (
290+
<>
291+
<Link
292+
style={({isFocusVisible}) => ({clipPath: isFocusVisible ? 'none' : 'margin-box'})}
293+
href={href}
294+
target={target}
295+
rel={rel}
296+
download={download}
297+
ping={ping}
298+
referrerPolicy={referrerPolicy}
299+
isDisabled={isDisabled || isCurrent}
300+
className={({isFocused, isFocusVisible, isHovered, isDisabled, isPressed}) => linkStyles({isFocused, isFocusVisible, isHovered, isDisabled, size, isCurrent, isMultiline, isPressed})}>
301+
{children}
302+
</Link>
303+
<ChevronIcon
304+
size={isMultiline ? 'S' : 'M'}
305+
className={chevronStyles} />
306+
</>
307+
)}
308+
</AriaBreadcrumb>
309+
);
18310
}

packages/@react-spectrum/s2/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export {ActionButton} from './ActionButton';
1414
export {ActionMenu} from './ActionMenu';
1515
export {Avatar} from './Avatar';
1616
export {Badge} from './Badge';
17+
export {Breadcrumbs, Breadcrumb} from './Breadcrumbs';
1718
export {Button, LinkButton} from './Button';
1819
export {ButtonGroup} from './ButtonGroup';
1920
export {Checkbox} from './Checkbox';
@@ -60,6 +61,7 @@ export {FileTrigger} from 'react-aria-components';
6061
export type {ActionButtonProps} from './ActionButton';
6162
export type {ActionMenuProps} from './ActionMenu';
6263
export type {AvatarProps} from './Avatar';
64+
export type {BreadcrumbsProps, BreadcrumbProps} from './Breadcrumbs';
6365
export type {BadgeProps} from './Badge';
6466
export type {ButtonProps, LinkButtonProps} from './Button';
6567
export type {ButtonGroupProps} from './ButtonGroup';

0 commit comments

Comments
 (0)