Skip to content

Commit

Permalink
[core] feat: MenuItem small modifier; extend MenuDivider width (#6269)
Browse files Browse the repository at this point in the history
Co-authored-by: Charles <charles.perinet@gmail.com>
  • Loading branch information
adidahiya and CPerinet authored Jul 12, 2023
1 parent 253ad84 commit f0a9b09
Show file tree
Hide file tree
Showing 16 changed files with 210 additions and 126 deletions.
2 changes: 2 additions & 0 deletions packages/core/src/common/classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,8 @@ export const FORM_GROUP_SUB_LABEL = `${NS}-form-group-sub-label`;

export const MENU = `${NS}-menu`;
export const MENU_ITEM = `${MENU}-item`;
export const MENU_ITEM_IS_SELECTABLE = `${MENU_ITEM}-is-selectable`;
export const MENU_ITEM_SELECTED_ICON = `${MENU_ITEM}-selected-icon`;
export const MENU_ITEM_ICON = `${MENU_ITEM}-icon`;
export const MENU_ITEM_LABEL = `${MENU_ITEM}-label`;
export const MENU_SUBMENU = `${NS}-submenu`;
Expand Down
74 changes: 46 additions & 28 deletions packages/core/src/components/menu/_common.scss
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ $menu-item-padding-large: ($pt-button-height-large - $pt-icon-size-large) * 0.5
$menu-item-padding-vertical: ($pt-button-height - $menu-item-line-height) * 0.5 !default;
$menu-item-padding-vertical-large:
($pt-button-height-large - $menu-item-line-height-large) * 0.5 !default;
$menu-item-padding-small: ($pt-button-height-small - $pt-icon-size-standard) * 0.5 !default;
$menu-item-padding-vertical-small:
($pt-button-height-small - $menu-item-line-height) * 0.5 !default;
$menu-item-selected-icon-spacing: 2px;
$menu-item-indent: $pt-icon-size-standard + $menu-item-selected-icon-spacing * 2;

$menu-background-color: $white !default;
$dark-menu-background-color: $dark-gray3 !default;
Expand Down Expand Up @@ -70,6 +75,7 @@ $dark-menu-item-intent-colors: (

&::before,
.#{$ns}-menu-item-icon,
.#{$ns}-menu-item-selected-icon,
.#{$ns}-submenu-icon {
color: $pt-icon-color;
}
Expand All @@ -83,24 +89,35 @@ $dark-menu-item-intent-colors: (
@include menu-item-hover();
}

&:active,
&.#{$ns}-active {
// N.B. mouse interaction :active appearance is different from the .#{$ns}-active modifier class.
// The latter is often used to indicate keyboard navigation, and it's helpful to distinguish between keyboard
// and mouse interactions.
&:active {
background-color: $menu-item-color-active;

.#{$ns}-menu-item-label {
color: $pt-text-color;
}
}

&.#{$ns}-selected {
&,
&:hover,
&:active {
@include menu-item-selected-active(false);
&.#{$ns}-active {
@include menu-item-active(false);
}

&.#{$ns}-menu-item-is-selectable {
padding-left: $menu-item-indent;

&.#{$ns}-selected {
padding-left: 0;
}

.#{$ns}-menu-item-selected-icon {
align-self: center;
margin: 0 $menu-item-selected-icon-spacing;
}
}

// pt-disable always overrides over styles
// disabled class needs to always overrides other styles
/* stylelint-disable declaration-no-important */
&.#{$ns}-disabled {
background-color: inherit !important;
Expand Down Expand Up @@ -131,6 +148,7 @@ $dark-menu-item-intent-colors: (

&::before,
.#{$ns}-menu-item-icon,
.#{$ns}-menu-item-selected-icon,
.#{$ns}-submenu-icon {
color: $pt-dark-icon-color;
}
Expand All @@ -139,19 +157,18 @@ $dark-menu-item-intent-colors: (
@include dark-menu-item-hover();
}

&:active,
&.#{$ns}-active {
// N.B. mouse interaction :active appearance is different from the .#{$ns}-active modifier class.
&:active {
// we can use the same color as light theme due to transparency
background-color: $menu-item-color-active;

.#{$ns}-menu-item-label {
color: $pt-dark-text-color;
}
}

&.#{$ns}-selected {
&,
&:hover,
&:active {
@include menu-item-selected-active(true);
}
&.#{$ns}-active {
@include menu-item-active(true);
}

// pt-disable always overrides over styles
Expand All @@ -177,10 +194,6 @@ $dark-menu-item-intent-colors: (
color: inherit;
cursor: pointer;
text-decoration: none;

&.#{ns}-selected {
@include menu-item-selected-active(false);
}
}

@mixin dark-menu-item-hover() {
Expand All @@ -191,13 +204,9 @@ $dark-menu-item-intent-colors: (
.#{$ns}-submenu-icon {
color: $pt-dark-icon-color;
}

&.#{ns}-selected {
@include menu-item-selected-active(true);
}
}

@mixin menu-item-selected-active($is-dark) {
@mixin menu-item-active($is-dark) {
$intent-colors: $menu-item-intent-colors;
$background-alpha: 0.1;

Expand All @@ -215,6 +224,7 @@ $dark-menu-item-intent-colors: (

&::before,
.#{$ns}-menu-item-icon,
.#{$ns}-menu-item-selected-icon,
.#{$ns}-submenu-icon {
color: nth(map-get($intent-colors, "primary"), 2);
}
Expand Down Expand Up @@ -244,6 +254,7 @@ $dark-menu-item-intent-colors: (

&::before,
.#{$ns}-menu-item-icon,
.#{$ns}-menu-item-selected-icon,
.#{$ns}-submenu-icon,
.#{$ns}-menu-item-label {
color: inherit;
Expand Down Expand Up @@ -272,7 +283,8 @@ $dark-menu-item-intent-colors: (
@mixin menu-item-large() {
font-size: $pt-font-size-large;
line-height: $menu-item-line-height-large;
padding: $menu-item-padding-vertical-large $menu-item-padding;
padding-bottom: $menu-item-padding-vertical-large;
padding-top: $menu-item-padding-vertical-large;

.#{$ns}-menu-item-icon {
height: $menu-item-line-height-large;
Expand All @@ -285,10 +297,16 @@ $dark-menu-item-intent-colors: (
}
}

@mixin menu-item-small() {
padding-bottom: $menu-item-padding-vertical-small;
padding-top: $menu-item-padding-vertical-small;
}

@mixin menu-divider() {
border-top: 1px solid $pt-divider-black;
display: block;
margin: $half-grid-size;
// use negative margin to make dividers take up full menu width
margin: $half-grid-size (-$half-grid-size);

.#{$ns}-dark & {
border-top-color: $pt-dark-divider-white;
Expand Down Expand Up @@ -321,7 +339,7 @@ $dark-menu-item-intent-colors: (
// a little extra space to avoid clipping descenders (because overflow hidden)
line-height: $pt-icon-size-standard + 1px;
margin: 0;
padding: $pt-grid-size $menu-item-padding 0 1px;
padding: $pt-grid-size $menu-item-padding 0 (1px + $half-grid-size);
}

@mixin menu-header-large($heading-selector) {
Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/components/menu/_menu.scss
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ Markup:
</li>
</ul>
.#{$ns}-large - Large size (only supported on <code>.#{$ns}-menu</code>)
.#{$ns}-large - Large size
.#{$ns}-small - Small size
Styleguide menu
*/
Expand Down Expand Up @@ -69,6 +70,10 @@ Styleguide menu
margin-right: $menu-item-padding-large;
}
}

.#{$ns}-small & {
@include menu-item-small();
}
}

button.#{$ns}-menu-item {
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/components/menu/_submenu.scss
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@
&,
&:hover,
&:active {
@include menu-item-selected-active(false);
@include menu-item-active(false);

.#{$ns}-dark & {
@include menu-item-selected-active(true);
@include menu-item-active(true);
}
}
}
Expand Down
18 changes: 14 additions & 4 deletions packages/core/src/components/menu/menu.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,21 @@ there is not enough room to the right.
</Menu>
```

@## CSS
@## CSS API

Menus can be constructed manually using the HTML markup and `@ns-menu-*` classes below. However, you
should use the menu [React components](#core/components/menu.javscript-api) instead wherever possible,
as they abstract away the tedious parts of implementing a menu.
<div class="@ns-callout @ns-intent-warning @ns-icon-warning-sign">
<h5 class="@ns-heading">

Deprecated API: use [`<Menu>` and `<MenuItem>`](#core/components/menu)

</h5>

CSS APIs for Blueprint components are considered deprecated, as they are verbose, error-prone, and they
often fall out of sync as the design system is updated. You should use the React component APIs instead.

</div>

Menus can be constructed manually using the following HTML markup and `@ns-menu-*` classes (available in JS/TS as `Classes.MENU_*`):

* Begin with a `ul.@ns-menu`. Each `li` child denotes a single entry in the menu.

Expand Down
10 changes: 8 additions & 2 deletions packages/core/src/components/menu/menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ export interface MenuProps extends Props, React.HTMLAttributes<HTMLUListElement>
/** Whether the menu items in this menu should use a large appearance. */
large?: boolean;

/** Whether the menu items in this menu should use a small appearance. */
small?: boolean;

/** Ref handler that receives the HTML `<ul>` element backing this component. */
ulRef?: React.Ref<HTMLUListElement>;
}
Expand All @@ -40,8 +43,11 @@ export class Menu extends AbstractPureComponent<MenuProps> {
public static displayName = `${DISPLAYNAME_PREFIX}.Menu`;

public render() {
const { className, children, large, ulRef, ...htmlProps } = this.props;
const classes = classNames(Classes.MENU, { [Classes.LARGE]: large }, className);
const { className, children, large, small, ulRef, ...htmlProps } = this.props;
const classes = classNames(className, Classes.MENU, {
[Classes.LARGE]: large,
[Classes.SMALL]: small,
});
return (
<ul role="menu" {...htmlProps} className={classes} ref={ulRef}>
{children}
Expand Down
27 changes: 12 additions & 15 deletions packages/core/src/components/menu/menuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import classNames from "classnames";
import * as React from "react";

import { CaretRight } from "@blueprintjs/icons";
import { CaretRight, SmallTick } from "@blueprintjs/icons";

import { Classes } from "../../common";
import { ActionProps, DISPLAYNAME_PREFIX, removeNonHTMLProps } from "../../common/props";
Expand Down Expand Up @@ -84,29 +84,25 @@ export interface MenuItemProps
*
* If `menuitem`, role structure becomes:
*
* `<li role="none"`
* `<a role="menuitem"`
* `<li role="none"><a role="menuitem" /></li>`
*
* which is proper role structure for a `<ul role="menu"` parent (this is the default `role` of a `Menu`).
*
* If `listoption`, role structure becomes:
*
* `<li role="option"`
* `<a role=undefined`
* `<li role="option"><a role={undefined} /></li>`
*
* which is proper role structure for a `<ul role="listbox"` parent, or a `<select>` parent.
*
* If `listitem`, role structure becomes:
*
* `<li role=undefined`
* `<a role=undefined`
* `<li role={undefined}><a role={undefined} /></li>`
*
* which can be used if this item is within a basic `<ul/>` (or `role="list"`) parent.
*
* If `none`, role structure becomes:
*
* `<li role="none"`
* `<a role=undefined`
* `<li role="none"><a role={undefined} /></li>`
*
* which can be used if wrapping this item in a custom `<li>` parent.
*
Expand Down Expand Up @@ -180,6 +176,7 @@ export const MenuItem: React.FC<MenuItemProps> = React.forwardRef<HTMLLIElement,
className,
children,
disabled,
icon,
intent,
labelClassName,
labelElement,
Expand All @@ -196,36 +193,34 @@ export const MenuItem: React.FC<MenuItemProps> = React.forwardRef<HTMLLIElement,
...htmlProps
} = props;

const [liRole, targetRole, icon, ariaSelected] =
const [liRole, targetRole, ariaSelected] =
roleStructure === "listoption" // "listoption": parent has listbox role, or is a <select>
? [
"option",
undefined, // target should have no role
props.icon ?? (selected === undefined ? undefined : selected ? "small-tick" : "blank"),
Boolean(selected), // aria-selected prop
]
: roleStructure === "menuitem" // "menuitem": parent has menu role
? [
"none",
"menuitem",
props.icon,
undefined, // don't set aria-selected prop
]
: roleStructure === "none" // "none": allows wrapping MenuItem in custom <li>
? [
"none",
undefined, // target should have no role
props.icon,
undefined, // don't set aria-selected prop
]
: // roleStructure === "listitem"
[
undefined, // needs no role prop, li is listitem by default
undefined,
props.icon,
undefined, // don't set aria-selected prop
];

const isSelectable = roleStructure === "listoption";
const isSelected = isSelectable && selected;
const hasIcon = icon != null;
const hasSubmenu = children != null;

Expand All @@ -238,7 +233,8 @@ export const MenuItem: React.FC<MenuItemProps> = React.forwardRef<HTMLLIElement,
[Classes.DISABLED]: disabled,
// prevent popover from closing when clicking on submenu trigger or disabled item
[Classes.POPOVER_DISMISS]: shouldDismissPopover && !disabled && !hasSubmenu,
[Classes.SELECTED]: active && intentClass === undefined,
[Classes.MENU_ITEM_IS_SELECTABLE]: isSelectable,
[Classes.SELECTED]: isSelected,
},
className,
);
Expand All @@ -263,6 +259,7 @@ export const MenuItem: React.FC<MenuItemProps> = React.forwardRef<HTMLLIElement,
...(disabled ? DISABLED_PROPS : {}),
className: anchorClasses,
},
isSelected ? <SmallTick className={Classes.MENU_ITEM_SELECTED_ICON} /> : undefined,
hasIcon ? (
// wrap icon in a <span> in case `icon` is a custom element rather than a built-in icon identifier,
// so that we always render this class
Expand Down
12 changes: 12 additions & 0 deletions packages/docs-app/src/examples/core-examples/common/sizeSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,15 @@ SizeSelect.defaultProps = {
label: "Size",
optionLabels: ["Small", "Regular", "Large"],
};

export function getSizeProp(size: Size) {
switch (size) {
case "large":
return { large: true };
case "small":
return { small: true };
default:
// regular is the default
return {};
}
}
Loading

1 comment on commit f0a9b09

@adidahiya
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[core] feat: MenuItem small modifier; extend MenuDivider width (#6269)

Build artifact links for this commit: documentation | landing | table | demo

This is an automated comment from the deploy-preview CircleCI job.

Please sign in to comment.