@vention/machine-ui 6.7.2 → 6.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -111,7 +111,10 @@ Components built on an MUI component (noted as "built on MUI `X`") accept that c
111
111
 
112
112
  ### Navigation and layout
113
113
 
114
- - `VentionTabs`: a row of tabs. `value` is the index of the selected tab, and `onChange(event, index)` receives the new index.
114
+ - `VentionTabs`: a row of tabs. `value` is the index of the selected tab, and `onChange(event, index)` receives the new index. The tabs are controlled, so the app updates `tabOptions` and `value` itself.
115
+ - With `onRename(index, name)`, clicking the selected tab opens an input over its label. Enter or leaving the input saves, Esc cancels. The name is trimmed, and `onRename` isn't called when it's empty or unchanged.
116
+ - With `onRemove(index)`, each tab gets a remove button. It shows while the pointer is over the tab and while the tab has keyboard focus. Touch screens have no hover, so there it shows on the selected tab. It isn't rendered when only one tab is left. Removing doesn't change `value`, so pick the new selected tab in `onRemove`. Clicking it moves focus to that tab's label, or to the previous tab's label for the last tab, so once the parent drops the tab, focus is on the tab that took its place.
117
+ - With `onAdd()`, an add button follows the tabs. Append the new tab to `tabOptions` in `onAdd`.
115
118
  - `VentionSteps`: a vertical list of steps for a guided procedure. Use `size="xx-large"` on touch HMIs; `"small"` always shows dots.
116
119
  - `VentionSidebarItem`: a profile entry in a sidebar.
117
120
  - `VentionTreeItem`: one row of a tree, with nested `VentionTreeItem` children.
package/index.esm.js CHANGED
@@ -10976,57 +10976,270 @@ const useStyles$m = tss.withNestedSelectors().create(({
10976
10976
  };
10977
10977
  });
10978
10978
 
10979
+ const ICON_BUTTON_WIDTH = 32;
10980
+ const HOVER_MEDIA_QUERY = "@media (hover: hover) and (pointer: fine)";
10981
+ const TOUCH_MEDIA_QUERY = "@media not all and (hover: hover) and (pointer: fine)";
10979
10982
  const VentionTabs = ({
10980
10983
  tabOptions,
10981
10984
  onChange,
10985
+ onRename,
10986
+ onRemove,
10987
+ onAdd,
10982
10988
  value
10983
10989
  }) => {
10990
+ const theme = useTheme();
10984
10991
  const {
10985
10992
  cx,
10986
10993
  classes
10987
10994
  } = useStyles$l();
10988
- return jsx(Box$1, {
10995
+ const canRemove = onRemove !== undefined && tabOptions.length > 1;
10996
+ const [editingIndex, setEditingIndex] = useState(null);
10997
+ const [draft, setDraft] = useState("");
10998
+ const isCancellingRef = useRef(false);
10999
+ const refocusIndexRef = useRef(null);
11000
+ const labelButtonsRef = useRef([]);
11001
+ // The edit is tied to a tab position, so it closes when the parent changes the selection or the tab count.
11002
+ useEffect(() => {
11003
+ setEditingIndex(null);
11004
+ refocusIndexRef.current = null;
11005
+ }, [value, tabOptions.length]);
11006
+ const focusInputOnMount = useCallback(input => {
11007
+ if (input) {
11008
+ input.focus();
11009
+ input.select();
11010
+ }
11011
+ }, []);
11012
+ const handleTabClick = (event, index) => {
11013
+ if (onRename && index === value) {
11014
+ setDraft(tabOptions[index]);
11015
+ setEditingIndex(index);
11016
+ return;
11017
+ }
11018
+ onChange(event, index);
11019
+ };
11020
+ // Enter and Esc both blur the input, so blur is the only place the edit closes.
11021
+ const handleInputKeyDown = (event, index) => {
11022
+ // Safari ends the composition before the Enter that confirms it, and only flags that Enter with keyCode 229.
11023
+ if (event.nativeEvent.isComposing || event.keyCode === 229) {
11024
+ return;
11025
+ }
11026
+ if (event.key === "Enter" || event.key === "Escape") {
11027
+ event.preventDefault();
11028
+ event.stopPropagation();
11029
+ isCancellingRef.current = event.key === "Escape";
11030
+ refocusIndexRef.current = index;
11031
+ event.currentTarget.blur();
11032
+ }
11033
+ };
11034
+ // The label is hidden while the input is open, so it can only take focus back once it renders visible again.
11035
+ const attachLabelButton = (labelButton, index) => {
11036
+ labelButtonsRef.current[index] = labelButton;
11037
+ if (labelButton && refocusIndexRef.current === index) {
11038
+ refocusIndexRef.current = null;
11039
+ labelButton.focus();
11040
+ }
11041
+ };
11042
+ const handleInputBlur = index => {
11043
+ const name = draft.trim();
11044
+ if (!isCancellingRef.current && name.length > 0 && name !== tabOptions[index]) {
11045
+ onRename === null || onRename === void 0 ? void 0 : onRename(index, name);
11046
+ }
11047
+ isCancellingRef.current = false;
11048
+ setEditingIndex(null);
11049
+ };
11050
+ // Tabs are keyed by index, so once the parent drops this tab, the label at this index shows the tab that took its place.
11051
+ const handleRemoveClick = index => {
11052
+ var _a;
11053
+ const isLastTab = index === tabOptions.length - 1;
11054
+ (_a = labelButtonsRef.current[isLastTab ? index - 1 : index]) === null || _a === void 0 ? void 0 : _a.focus();
11055
+ onRemove === null || onRemove === void 0 ? void 0 : onRemove(index);
11056
+ };
11057
+ return jsxs(Box$1, {
10989
11058
  className: classes.root,
10990
- children: tabOptions.map((buttonLabel, index) => jsx(Button, {
10991
- className: index === value ? cx(classes.button, classes.selectedButton) : classes.button,
10992
- onClick: event => onChange(event, index),
10993
- disableRipple: true,
10994
- children: jsx(Typography, {
10995
- variant: "heading18SemiBold",
10996
- children: buttonLabel
11059
+ role: "tablist",
11060
+ children: [tabOptions.map((tabLabel, index) => {
11061
+ const isSelected = index === value;
11062
+ const isEditing = index === editingIndex;
11063
+ return jsxs("div", {
11064
+ className: cx(classes.tab, isSelected && classes.selectedTab),
11065
+ children: [jsx("button", {
11066
+ ref: labelButton => attachLabelButton(labelButton, index),
11067
+ className: cx(classes.label, isEditing && classes.hiddenLabel),
11068
+ type: "button",
11069
+ role: "tab",
11070
+ "aria-selected": isSelected,
11071
+ onClick: event => handleTabClick(event, index),
11072
+ children: jsx(Typography, {
11073
+ variant: "heading18SemiBold",
11074
+ noWrap: true,
11075
+ children: tabLabel
11076
+ })
11077
+ }), isEditing && jsx("input", {
11078
+ ref: focusInputOnMount,
11079
+ className: classes.input,
11080
+ type: "text",
11081
+ "aria-label": "Tab name",
11082
+ value: draft,
11083
+ onChange: event => setDraft(event.target.value),
11084
+ onKeyDown: event => handleInputKeyDown(event, index),
11085
+ onBlur: () => handleInputBlur(index)
11086
+ }), canRemove && !isEditing && jsx("span", {
11087
+ className: classes.removeControls,
11088
+ children: jsx("button", {
11089
+ className: classes.removeButton,
11090
+ type: "button",
11091
+ "aria-label": `Remove tab ${tabLabel}`,
11092
+ onClick: () => handleRemoveClick(index),
11093
+ children: jsx(VentionIcon, {
11094
+ type: "close",
11095
+ size: 16,
11096
+ color: theme.palette.icon.secondary
11097
+ })
11098
+ })
11099
+ })]
11100
+ }, index);
11101
+ }), onAdd && jsx("button", {
11102
+ className: classes.addButton,
11103
+ type: "button",
11104
+ "aria-label": "Add tab",
11105
+ onClick: () => onAdd(),
11106
+ children: jsx(VentionIcon, {
11107
+ type: "plus",
11108
+ size: 16,
11109
+ color: theme.palette.icon.secondary
10997
11110
  })
10998
- }, index))
11111
+ })]
10999
11112
  });
11000
11113
  };
11001
- const useStyles$l = tss.create(({
11002
- theme
11114
+ const useStyles$l = tss.withNestedSelectors().create(({
11115
+ theme,
11116
+ classes
11003
11117
  }) => {
11118
+ const iconButton = {
11119
+ flexShrink: 0,
11120
+ display: "inline-flex",
11121
+ alignItems: "center",
11122
+ justifyContent: "center",
11123
+ width: `${ICON_BUTTON_WIDTH}px`,
11124
+ padding: 0,
11125
+ border: "none",
11126
+ backgroundColor: "transparent",
11127
+ cursor: "pointer",
11128
+ transition: "background-color 120ms",
11129
+ "&:focus-visible": {
11130
+ outline: `2px solid ${theme.palette.border.blue}`,
11131
+ outlineOffset: "-2px"
11132
+ }
11133
+ };
11134
+ const shownRemoveControls = {
11135
+ opacity: 1,
11136
+ pointerEvents: "auto"
11137
+ };
11004
11138
  return {
11005
11139
  root: {
11140
+ display: "inline-flex",
11141
+ flexWrap: "wrap",
11142
+ alignItems: "center",
11143
+ gap: "1px",
11144
+ padding: "1px",
11006
11145
  borderRadius: theme.spacing(1),
11007
11146
  borderWidth: "2px",
11008
11147
  borderColor: theme.palette.misc.stepper.border,
11009
11148
  borderStyle: "solid",
11010
11149
  backgroundColor: theme.palette.background.surface2
11011
11150
  },
11012
- button: {
11013
- color: theme.palette.text.primary,
11014
- borderWidth: theme.spacing(2),
11151
+ tab: {
11152
+ position: "relative",
11153
+ display: "inline-flex",
11154
+ alignItems: "center",
11155
+ height: "36px",
11156
+ minWidth: "150px",
11015
11157
  margin: "1px",
11016
- minWidth: "120px",
11017
- textTransform: "none",
11018
- "&:hover": {
11019
- backgroundColor: theme.palette.background.default
11020
- },
11021
- "@media (hover: none)": {
11158
+ paddingLeft: theme.spacing(1),
11159
+ borderRadius: "2px",
11160
+ transition: "background-color 120ms",
11161
+ [`&:has(:focus-visible) .${classes.removeControls}`]: shownRemoveControls,
11162
+ [HOVER_MEDIA_QUERY]: {
11163
+ "&:hover": {
11164
+ backgroundColor: alpha(theme.palette.common.white, 0.6)
11165
+ },
11166
+ [`&:hover .${classes.removeControls}`]: shownRemoveControls
11167
+ }
11168
+ },
11169
+ selectedTab: {
11170
+ backgroundColor: theme.palette.background.default,
11171
+ boxShadow: theme.shadows[1],
11172
+ [HOVER_MEDIA_QUERY]: {
11022
11173
  "&:hover": {
11023
- backgroundColor: "initial"
11174
+ backgroundColor: theme.palette.background.default
11024
11175
  }
11176
+ },
11177
+ [TOUCH_MEDIA_QUERY]: {
11178
+ [`& .${classes.removeControls}`]: shownRemoveControls
11025
11179
  }
11026
11180
  },
11027
- selectedButton: {
11028
- background: theme.palette.background.default
11029
- }
11181
+ label: {
11182
+ flexGrow: 1,
11183
+ minWidth: 0,
11184
+ height: "100%",
11185
+ padding: theme.spacing(0, 2),
11186
+ border: "none",
11187
+ background: "transparent",
11188
+ color: theme.palette.text.primary,
11189
+ cursor: "pointer",
11190
+ textAlign: "left",
11191
+ "&:focus-visible": {
11192
+ outline: `2px solid ${theme.palette.border.blue}`,
11193
+ outlineOffset: "-2px",
11194
+ borderRadius: "2px"
11195
+ }
11196
+ },
11197
+ hiddenLabel: {
11198
+ visibility: "hidden"
11199
+ },
11200
+ input: Object.assign(Object.assign({}, theme.typography.heading18SemiBold), {
11201
+ position: "absolute",
11202
+ top: theme.spacing(1),
11203
+ bottom: theme.spacing(1),
11204
+ left: "6px",
11205
+ right: theme.spacing(1),
11206
+ padding: "0 5px",
11207
+ border: `1px solid ${theme.palette.border.blue}`,
11208
+ borderRadius: "3px",
11209
+ backgroundColor: theme.palette.background.default,
11210
+ color: theme.palette.text.primary,
11211
+ outline: "none",
11212
+ boxShadow: `0 0 0 3px ${alpha(theme.palette.border.blue, 0.18)}`
11213
+ }),
11214
+ removeControls: {
11215
+ flexShrink: 0,
11216
+ alignSelf: "stretch",
11217
+ display: "inline-flex",
11218
+ opacity: 0,
11219
+ pointerEvents: "none",
11220
+ transition: "opacity 120ms"
11221
+ },
11222
+ removeButton: Object.assign(Object.assign({}, iconButton), {
11223
+ borderRadius: "0 2px 2px 0",
11224
+ [HOVER_MEDIA_QUERY]: {
11225
+ "&:hover": {
11226
+ backgroundColor: theme.palette.background.surface3
11227
+ }
11228
+ },
11229
+ "&:active": {
11230
+ backgroundColor: theme.palette.background.surface4
11231
+ }
11232
+ }),
11233
+ addButton: Object.assign(Object.assign({}, iconButton), {
11234
+ height: "36px",
11235
+ margin: "1px",
11236
+ borderRadius: "2px",
11237
+ [HOVER_MEDIA_QUERY]: {
11238
+ "&:hover": {
11239
+ backgroundColor: alpha(theme.palette.common.white, 0.6)
11240
+ }
11241
+ }
11242
+ })
11030
11243
  };
11031
11244
  });
11032
11245
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vention/machine-ui",
3
- "version": "6.7.2",
3
+ "version": "6.8.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/VentionCo/machine-cloud.git"
@@ -3,6 +3,9 @@ type VentionTabsProps = {
3
3
  tabOptions: string[];
4
4
  value: number;
5
5
  onChange: (event: React.MouseEvent<HTMLElement>, arg: number) => void;
6
+ onRename?: (index: number, name: string) => void;
7
+ onRemove?: (index: number) => void;
8
+ onAdd?: () => void;
6
9
  };
7
- export declare const VentionTabs: ({ tabOptions, onChange, value }: VentionTabsProps) => import("react/jsx-runtime").JSX.Element;
10
+ export declare const VentionTabs: ({ tabOptions, onChange, onRename, onRemove, onAdd, value }: VentionTabsProps) => import("react/jsx-runtime").JSX.Element;
8
11
  export {};