Skip to content

Commit d969d73

Browse files
authored
feat: S2 Menu design updates (#7580)
* Flip menu link out icon in RTL * Add prop to hide link out icon * Update alignment of multi-section menus
1 parent e7053dd commit d969d73

File tree

4 files changed

+53
-20
lines changed

4 files changed

+53
-20
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,11 +379,12 @@ export function ComboBoxItem(props: ComboBoxItemProps) {
379379

380380
export interface ComboBoxSectionProps<T extends object> extends SectionProps<T> {}
381381
export function ComboBoxSection<T extends object>(props: ComboBoxSectionProps<T>) {
382+
let {size} = useContext(InternalComboboxContext);
382383
return (
383384
<>
384385
<AriaListBoxSection
385386
{...props}
386-
className={section}>
387+
className={section({size})}>
387388
{props.children}
388389
</AriaListBoxSection>
389390
<Divider />

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

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -81,22 +81,26 @@ export interface MenuProps<T> extends Omit<AriaMenuProps<T>, 'children' | 'style
8181
/**
8282
* The contents of the collection.
8383
*/
84-
children?: ReactNode | ((item: T) => ReactNode)
84+
children?: ReactNode | ((item: T) => ReactNode),
85+
/** Hides the default link out icons on menu items that open links in a new tab. */
86+
hideLinkOutIcon?: boolean
8587
}
8688

8789
export const MenuContext = createContext<ContextValue<MenuProps<any>, DOMRefValue<HTMLDivElement>>>(null);
8890

91+
const menuItemGrid = {
92+
size: {
93+
S: [edgeToText(24), 'auto', 'auto', 'minmax(0, 1fr)', 'auto', 'auto', 'auto', edgeToText(24)],
94+
M: [edgeToText(32), 'auto', 'auto', 'minmax(0, 1fr)', 'auto', 'auto', 'auto', edgeToText(32)],
95+
L: [edgeToText(40), 'auto', 'auto', 'minmax(0, 1fr)', 'auto', 'auto', 'auto', edgeToText(40)],
96+
XL: [edgeToText(48), 'auto', 'auto', 'minmax(0, 1fr)', 'auto', 'auto', 'auto', edgeToText(48)]
97+
}
98+
} as const;
99+
89100
export let menu = style({
90101
outlineStyle: 'none',
91102
display: 'grid',
92-
gridTemplateColumns: {
93-
size: {
94-
S: [edgeToText(24), 'auto', 'auto', 'minmax(0, 1fr)', 'auto', 'auto', 'auto', edgeToText(24)],
95-
M: [edgeToText(32), 'auto', 'auto', 'minmax(0, 1fr)', 'auto', 'auto', 'auto', edgeToText(32)],
96-
L: [edgeToText(40), 'auto', 'auto', 'minmax(0, 1fr)', 'auto', 'auto', 'auto', edgeToText(40)],
97-
XL: [edgeToText(48), 'auto', 'auto', 'minmax(0, 1fr)', 'auto', 'auto', 'auto', edgeToText(48)]
98-
}
99-
},
103+
gridTemplateColumns: menuItemGrid,
100104
boxSizing: 'border-box',
101105
maxHeight: '[inherit]',
102106
overflow: {
@@ -121,7 +125,7 @@ export let section = style({
121125
'. checkmark icon label value keyboard descriptor .',
122126
'. . . description . . . .'
123127
],
124-
gridTemplateColumns: 'subgrid'
128+
gridTemplateColumns: menuItemGrid
125129
});
126130

127131
export let sectionHeader = style<{size?: 'S' | 'M' | 'L' | 'XL'}>({
@@ -310,7 +314,12 @@ let descriptor = style({
310314
}
311315
});
312316

313-
let InternalMenuContext = createContext<{size: 'S' | 'M' | 'L' | 'XL', isSubmenu: boolean}>({size: 'M', isSubmenu: false});
317+
let InternalMenuContext = createContext<{size: 'S' | 'M' | 'L' | 'XL', isSubmenu: boolean, hideLinkOutIcon: boolean}>({
318+
size: 'M',
319+
isSubmenu: false,
320+
hideLinkOutIcon: false
321+
});
322+
314323
let InternalMenuTriggerContext = createContext<Omit<MenuTriggerProps, 'children'> | null>(null);
315324

316325
/**
@@ -324,7 +333,8 @@ export const Menu = /*#__PURE__*/ (forwardRef as forwardRefType)(function Menu<T
324333
size = ctxSize,
325334
UNSAFE_style,
326335
UNSAFE_className,
327-
styles
336+
styles,
337+
hideLinkOutIcon = false
328338
} = props;
329339
let ctx = useContext(InternalMenuTriggerContext);
330340
let {align = 'start', direction = 'bottom', shouldFlip} = ctx ?? {};
@@ -349,7 +359,7 @@ export const Menu = /*#__PURE__*/ (forwardRef as forwardRefType)(function Menu<T
349359
}
350360

351361
let content = (
352-
<InternalMenuContext.Provider value={{size, isSubmenu: true}}>
362+
<InternalMenuContext.Provider value={{size, isSubmenu: true, hideLinkOutIcon}}>
353363
<Provider
354364
values={[
355365
[HeaderContext, {styles: sectionHeader({size})}],
@@ -417,11 +427,12 @@ export function Divider(props: SeparatorProps) {
417427
export interface MenuSectionProps<T extends object> extends AriaMenuSectionProps<T> {}
418428
export function MenuSection<T extends object>(props: MenuSectionProps<T>) {
419429
// remember, context doesn't work if it's around Section nor inside
430+
let {size} = useContext(InternalMenuContext);
420431
return (
421432
<>
422433
<AriaMenuSection
423434
{...props}
424-
className={section}>
435+
className={section({size})}>
425436
{props.children}
426437
</AriaMenuSection>
427438
<Divider />
@@ -454,7 +465,7 @@ export function MenuItem(props: MenuItemProps) {
454465
let ref = useRef(null);
455466
let isLink = props.href != null;
456467
let isLinkOut = isLink && props.target === '_blank';
457-
let {size} = useContext(InternalMenuContext);
468+
let {size, hideLinkOutIcon} = useContext(InternalMenuContext);
458469
let textValue = props.textValue || (typeof props.children === 'string' ? props.children : undefined);
459470
let {direction} = useLocale();
460471
return (
@@ -494,13 +505,25 @@ export function MenuItem(props: MenuItemProps) {
494505
</div>
495506
)}
496507
{typeof children === 'string' ? <Text slot="label">{children}</Text> : children}
497-
{isLinkOut && <LinkOutIcon size={linkIconSize[size]} className={descriptor} />}
508+
{isLinkOut && !hideLinkOutIcon && (
509+
<div slot="descriptor" className={descriptor}>
510+
<LinkOutIcon
511+
size={linkIconSize[size]}
512+
className={style({
513+
scaleX: {
514+
direction: {
515+
rtl: -1
516+
}
517+
}
518+
})({direction})} />
519+
</div>
520+
)}
498521
{renderProps.hasSubmenu && (
499522
<div slot="descriptor" className={descriptor}>
500523
<ChevronRightIcon
501524
size={size}
502525
className={style({
503-
scale: {
526+
scaleX: {
504527
direction: {
505528
rtl: -1
506529
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -464,11 +464,12 @@ function DefaultProvider({context, value, children}: {context: React.Context<any
464464

465465
export interface PickerSectionProps<T extends object> extends SectionProps<T> {}
466466
export function PickerSection<T extends object>(props: PickerSectionProps<T>) {
467+
let {size} = useContext(InternalPickerContext);
467468
return (
468469
<>
469470
<AriaListBoxSection
470471
{...props}
471-
className={section}>
472+
className={section({size})}>
472473
{props.children}
473474
</AriaListBoxSection>
474475
<Divider />

packages/@react-spectrum/s2/style/spectrum-theme.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -673,7 +673,14 @@ export const style = createTheme({
673673
translate: 'var(--translateX, 0) var(--translateY, 0)'
674674
}), translate),
675675
rotate: createArbitraryProperty((value: number | `${number}deg` | `${number}rad` | `${number}grad` | `${number}turn`, property) => ({[property]: typeof value === 'number' ? `${value}deg` : value})),
676-
scale: createArbitraryProperty<number>(),
676+
scaleX: createArbitraryProperty<number>(value => ({
677+
'--scaleX': value,
678+
scale: 'var(--scaleX, 1) var(--scaleY, 1)'
679+
})),
680+
scaleY: createArbitraryProperty<number>(value => ({
681+
'--scaleY': value,
682+
scale: 'var(--scaleX, 1) var(--scaleY, 1)'
683+
})),
677684
transform: createArbitraryProperty<string>(),
678685
position: ['absolute', 'fixed', 'relative', 'sticky', 'static'] as const,
679686
insetStart: createRenamedProperty('insetInlineStart', inset),
@@ -934,6 +941,7 @@ export const style = createTheme({
934941
borderStartRadius: ['borderTopStartRadius', 'borderBottomStartRadius'] as const,
935942
borderEndRadius: ['borderTopEndRadius', 'borderBottomEndRadius'] as const,
936943
translate: ['translateX', 'translateY'] as const,
944+
scale: ['scaleX', 'scaleY'] as const,
937945
inset: ['top', 'bottom', 'insetStart', 'insetEnd'] as const,
938946
insetX: ['insetStart', 'insetEnd'] as const,
939947
insetY: ['top', 'bottom'] as const,

0 commit comments

Comments
 (0)