Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@netdata/netdata-ui",
"version": "5.6.0",
"version": "5.6.1",
"description": "netdata UI kit",
"main": "dist/index.js",
"module": "dist/es6/index.js",
Expand All @@ -23,6 +23,7 @@
"build": "yarn build:umd && yarn build:cjs && yarn build:es6",
"clean": "rimraf lib dist es coverage",
"test": "jest --config ./jest/config.js",
"lint": "eslint src",
"cp-cloud": "cp -R ./dist ../cloud-frontend/node_modules/@netdata/netdata-ui/",
"to-cloud": "yarn build:cjs && yarn build:es6 && yarn cp-cloud",
"cp-charts": "cp -R ./dist ../charts/node_modules/@netdata/netdata-ui/",
Expand Down Expand Up @@ -73,6 +74,7 @@
"@babel/plugin-transform-runtime": "^7.29.0",
"@babel/preset-env": "^7.29.0",
"@babel/preset-react": "^7.28.5",
"@eslint/js": "^9.39.4",
"@playwright/test": "1.62.1",
"@storybook/addon-docs": "10.2.10",
"@storybook/addon-links": "10.2.10",
Expand All @@ -87,7 +89,7 @@
"babel-plugin-styled-components": "^2.1.4",
"clean-webpack-plugin": "^4.0.0",
"copyfiles": "^2.4.1",
"eslint": "^10.0.0",
"eslint": "^9.39.4",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.0",
"eslint-plugin-storybook": "10.2.10",
Expand Down
40 changes: 40 additions & 0 deletions src/components/button/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,43 @@ const myButton: Button = (
<Button disabled={true} type="default" icon={"plus"} onClick={this.myHandler} label="Press me" />
)
```

### Composed controls and exact geometry (local branch additions)

`Button` accepts `height`, `width`, `fontSize`, `fontWeight`, `gap`, and `justifyContent` in addition to its existing padding/radius props. Numeric geometry, text size and gap values use the theme's 4px subunit, including fractions. String height/width/gap values pass through. An explicit height also sets the minimum height; an icon-only button's explicit width sets its minimum width. `underline` keeps link-style text underlined at rest and on hover. `fontWeight` overrides the default medium (500) weight; existing `strong` behavior is unchanged. The standard compact/default/large ladder remains the default.

`wrapChildren={false}` renders custom child composition directly, preserving sibling icons, labels and descriptions. Its default is `true`, preserving the existing label span. `aria-pressed="true"` uses the same selected state grammar as `active` and `aria-expanded="true"`.

`IconButton` accepts `buttonSize` and `buttonHeight` in 4px subunits for square or rectangular controls. Its historical `width`/`height` props still size glyphs. `TooltipComponent` and `tooltipProps` optionally supply a tooltip adapter; the tooltip must support a child render function. Button refs and pre-existing `aria-describedby` IDs are merged with the tooltip's refs and IDs.

```jsx
<Button neutral flavour="hollow" height={6.5} fontSize={3} padding={[0, 2.5]} wrapChildren={false}>
<span>Filter</span>
<span>3</span>
</Button>
<IconButton type="button" icon="chevron_down" aria-label="Move up" buttonSize={7} buttonHeight={3.5} />
```

### Surface appearance

`<Button neutral flavour="hollow" appearance="surface" />` uses `controlBackground` and the softer `border` at rest. Hover uses `controlHoverBackground`, `controlBorder`, and `controlHoverText`; active, expanded, and pressed controls use `controlPressedBackground`, `controlBorder`, and `controlPressedText`. Resting label ink uses `controlText`. All three text roles default to the existing `textFocus` value in each theme. Resting icons follow the label unless `iconColor` is set; hover follows the existing label-ink behavior. `colorPalette` can remap these theme roles, including token arrays such as `colorPalette={{ controlBackground: ["transparent", "full"] }}` to preserve an originally transparent control.

Use `colorPalette={{ controlText: "statusMutedText", controlHoverText: "textFocus", controlPressedText: "textFocus" }}` for a muted trigger that gains emphasis on interaction.

The appearance applies to neutral hollow controls without `danger`, `warning`, or an explicit `color`. Other flavours and existing defaults are unchanged. Radius stays independent: `round={1}` gives 4px. Native `disabled`, `aria-disabled`, and `aria-pressed` retain their existing behavior; an aria-disabled surface uses its resting colors on hover.

### Inline links

`<Button flavour="link" appearance="inline" />` participates in text flow with inline-flex display, baseline alignment, inherited line height, zero default padding, no border width, and no minimum control height. Existing geometry props still apply; other flavours ignore this appearance.

Use `monospace` for the theme's `FONT_MONOSPACE` face. `underlineColor` accepts a theme token or token path; hover restores `currentColor`. `underlineOffset` uses 4px units for numbers (`0.75` gives 3px) or passes strings through. Existing `underline` still controls the resting underline. Inline keyboard focus uses the existing 2px ring with `segmentedFocus` ink and a -2px inset. Other appearances retain their focus styling.

Button also supports the existing Flex `flex`/`basis` contract, CSS `lineHeight` (numbers are unitless, strings pass through), and `disabledOpacity` (default0.4) for native and aria-disabled controls. Defaults remain unchanged; use these props instead of inline styles when preserving compact controls.

With `wrapChildren={false}`, direct child spans retain their own margins; only the default wrapped label receives the existing icon-to-label margin. Text transformation retains its existing behavior.

## Keyboard focus

Use `useRovingFocus({ count })` for a single tab stop in an action group. Spread `getItemProps(index)` on each item. Pass existing handlers and refs as the second argument to keep them composed.

For a controlled tab or radio group, pass `activeIndex` and `onNavigate`. Set `orientation` to `"horizontal"`, `"vertical"`, or `"both"` to match the group. For a list that moves focus without selection or wrapping, call `focusInList(event, elements)` from the list’s key handler.
54 changes: 30 additions & 24 deletions src/components/button/__snapshots__/button.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ exports[`Borderless Button renders 1`] = `
font-weight: 500;
font-size: 13px;
line-height: 16px;
min-height: 28px;
min-height: 30px;
white-space: nowrap;
word-break: keep-all;
cursor: pointer;
pointer-events: auto;
padding: 0 12px;
padding: 0 14px;
transition: background-color 140ms ease-out,border-color 140ms ease-out,color 140ms ease-out;
background-color: rgba(255,255,255,0.0);
color: #007830;
Expand Down Expand Up @@ -48,7 +48,8 @@ exports[`Borderless Button renders 1`] = `
}

.c0.c0:active,
.c0.c0[aria-expanded="true"] {
.c0.c0[aria-expanded="true"],
.c0.c0[aria-pressed="true"] {
border-color: rgba(255,255,255,0.0);
background-color: rgba(255,255,255,0.0);
color: #005622;
Expand Down Expand Up @@ -118,7 +119,7 @@ exports[`Borderless Button renders 1`] = `
class="c0"
colors="[object Object]"
flavour="borderless"
padding="0,3"
padding="0,3.5"
round="1"
texttransform="firstLetter"
>
Expand All @@ -138,12 +139,12 @@ exports[`Button states renders loading icon 1`] = `
font-weight: 500;
font-size: 13px;
line-height: 16px;
min-height: 28px;
min-height: 30px;
white-space: nowrap;
word-break: keep-all;
cursor: pointer;
pointer-events: auto;
padding: 0 12px;
padding: 0 14px;
transition: background-color 140ms ease-out,border-color 140ms ease-out,color 140ms ease-out;
background-color: #007830;
color: #FFFFFF;
Expand Down Expand Up @@ -176,7 +177,8 @@ exports[`Button states renders loading icon 1`] = `
}

.c0.c0:active,
.c0.c0[aria-expanded="true"] {
.c0.c0[aria-expanded="true"],
.c0.c0[aria-pressed="true"] {
border-color: #005622;
background-color: #005622;
color: #FFFFFF;
Expand Down Expand Up @@ -266,7 +268,7 @@ exports[`Button states renders loading icon 1`] = `
<button
class="c0"
colors="[object Object]"
padding="0,3"
padding="0,3.5"
round="1"
texttransform="firstLetter"
>
Expand Down Expand Up @@ -306,8 +308,8 @@ exports[`Button states renders only icon 1`] = `
font-weight: 500;
font-size: 13px;
line-height: 16px;
min-height: 28px;
min-width: 28px;
min-height: 30px;
min-width: 30px;
white-space: nowrap;
word-break: keep-all;
cursor: pointer;
Expand Down Expand Up @@ -345,7 +347,8 @@ exports[`Button states renders only icon 1`] = `
}

.c0.c0:active,
.c0.c0[aria-expanded="true"] {
.c0.c0[aria-expanded="true"],
.c0.c0[aria-pressed="true"] {
border-color: #005622;
background-color: #DDFFEB;
color: #007830;
Expand Down Expand Up @@ -414,7 +417,7 @@ exports[`Button states renders only icon 1`] = `
justify-content: center;
width: auto;
height: 100%;
color: #526161;
color: #2D3E33;
}

@media (prefers-reduced-motion:reduce) {
Expand Down Expand Up @@ -468,8 +471,8 @@ exports[`Button states renders smaller only icon 1`] = `
font-weight: 500;
font-size: 13px;
line-height: 16px;
min-height: 28px;
min-width: 28px;
min-height: 30px;
min-width: 30px;
white-space: nowrap;
word-break: keep-all;
cursor: pointer;
Expand Down Expand Up @@ -507,7 +510,8 @@ exports[`Button states renders smaller only icon 1`] = `
}

.c0.c0:active,
.c0.c0[aria-expanded="true"] {
.c0.c0[aria-expanded="true"],
.c0.c0[aria-pressed="true"] {
border-color: #005622;
background-color: #DDFFEB;
color: #007830;
Expand Down Expand Up @@ -576,7 +580,7 @@ exports[`Button states renders smaller only icon 1`] = `
justify-content: center;
width: auto;
height: 100%;
color: #526161;
color: #2D3E33;
}

@media (prefers-reduced-motion:reduce) {
Expand Down Expand Up @@ -630,12 +634,12 @@ exports[`Default Button renders 1`] = `
font-weight: 500;
font-size: 13px;
line-height: 16px;
min-height: 28px;
min-height: 30px;
white-space: nowrap;
word-break: keep-all;
cursor: pointer;
pointer-events: auto;
padding: 0 12px;
padding: 0 14px;
transition: background-color 140ms ease-out,border-color 140ms ease-out,color 140ms ease-out;
background-color: #007830;
color: #FFFFFF;
Expand Down Expand Up @@ -664,7 +668,8 @@ exports[`Default Button renders 1`] = `
}

.c0.c0:active,
.c0.c0[aria-expanded="true"] {
.c0.c0[aria-expanded="true"],
.c0.c0[aria-pressed="true"] {
border-color: #005622;
background-color: #005622;
color: #FFFFFF;
Expand Down Expand Up @@ -733,7 +738,7 @@ exports[`Default Button renders 1`] = `
<button
class="c0"
colors="[object Object]"
padding="0,3"
padding="0,3.5"
round="1"
texttransform="fullWidth"
>
Expand All @@ -753,12 +758,12 @@ exports[`Hollow Button renders 1`] = `
font-weight: 500;
font-size: 13px;
line-height: 16px;
min-height: 28px;
min-height: 30px;
white-space: nowrap;
word-break: keep-all;
cursor: pointer;
pointer-events: auto;
padding: 0 12px;
padding: 0 14px;
transition: background-color 140ms ease-out,border-color 140ms ease-out,color 140ms ease-out;
background-color: rgba(255,255,255,0.0);
color: #007830;
Expand Down Expand Up @@ -791,7 +796,8 @@ exports[`Hollow Button renders 1`] = `
}

.c0.c0:active,
.c0.c0[aria-expanded="true"] {
.c0.c0[aria-expanded="true"],
.c0.c0[aria-pressed="true"] {
border-color: #005622;
background-color: #DDFFEB;
color: #007830;
Expand Down Expand Up @@ -861,7 +867,7 @@ exports[`Hollow Button renders 1`] = `
class="c0"
colors="[object Object]"
flavour="hollow"
padding="0,3"
padding="0,3.5"
round="1"
texttransform="firstLetter"
>
Expand Down
81 changes: 51 additions & 30 deletions src/components/button/button.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,36 +18,57 @@ export const Button = ({
iconHeight,
iconRotate = 0,
children = label,
wrapChildren = true,
ref,
...rest
}) => (
<StyledButton
flavour={flavour}
textTransform={textTransform}
hasIcon={!!icon || isLoading}
hasLabel={!!children}
onClick={isLoading ? undefined : onClick}
ref={ref}
iconColor={iconColor}
iconWidth={iconWidth}
iconHeight={iconHeight}
{...rest}
>
{isLoading && <LoaderIcon className="button-icon" />}
{icon && !isLoading && (
<Flex justifyContent="center" alignItems="center" width="auto" height="100%">
<Icon
size={iconSize}
className="button-icon"
title={icon}
name={icon}
width={iconWidth}
height={iconHeight}
rotate={iconRotate}
/>
</Flex>
)}
}) => {
const labelContent =
!!children &&
(wrapChildren ? (
<span>{(isLoading && loadingLabel) || children}</span>
) : (
(isLoading && loadingLabel) || children
))
const iconContent = isLoading ? (
<LoaderIcon className="button-icon" />
) : icon ? (
<Flex justifyContent="center" alignItems="center" width="auto" height="100%">
<Icon
size={iconSize}
className="button-icon"
title={icon}
name={icon}
width={iconWidth}
height={iconHeight}
rotate={iconRotate}
/>
</Flex>
) : null
const bareText =
!iconContent && (typeof labelContent === "string" || typeof labelContent === "number")

{!!children && <span>{(isLoading && loadingLabel) || children}</span>}
</StyledButton>
)
return (
<StyledButton
flavour={flavour}
textTransform={textTransform}
hasIcon={!!icon || isLoading}
hasLabel={!!children}
data-unwrapped-children={wrapChildren ? undefined : ""}
onClick={isLoading ? undefined : onClick}
ref={ref}
iconColor={iconColor}
iconWidth={iconWidth}
iconHeight={iconHeight}
{...rest}
>
{bareText ? (
labelContent
) : (
<>
{iconContent}
{labelContent}
</>
)}
</StyledButton>
)
}
4 changes: 2 additions & 2 deletions src/components/button/button.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ const statusOptions = [
// One control ladder: every row lines up at 24 (compact), 28 (default) or 32 (large).
const ladderRows = [
{ name: "compact · 24px · 12px text", button: { compact: true }, field: { size: "compact" } },
{ name: "default · 28px · 13px text", button: {}, field: {} },
{ name: "default · 30px · 13px text", button: {}, field: {} },
{ name: "large · 32px · 14px text", button: { large: true }, field: { height: 8 } },
]

Expand Down Expand Up @@ -151,7 +151,7 @@ export const IconButtonSizes = () => (
<Cell caption="compact · 24px">
<IconButton icon="gear" tooltip="Settings" compact />
</Cell>
<Cell caption="default · 28px">
<Cell caption="default · 30px">
<IconButton icon="gear" tooltip="Settings" />
</Cell>
<Cell caption="large · 32px">
Expand Down
Loading
Loading