@linzjs/windows 8.8.2 → 9.0.0

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.
@@ -1 +1,2 @@
1
1
  export * from './useConstFunction';
2
+ export * from './util';
@@ -0,0 +1,31 @@
1
+ export const tabToNextSiblingElement = (): void => tabToSiblingElement(1);
2
+ export const tabToPreviousSiblingElement = (): void => tabToSiblingElement(-1);
3
+
4
+ export const tabToSiblingElement = (direction: 1 | -1): void => {
5
+ const active = document.activeElement as HTMLElement | null;
6
+ if (!active) {
7
+ return;
8
+ }
9
+
10
+ const selector = [
11
+ 'a[href]',
12
+ 'area[href]',
13
+ 'button:not([disabled])',
14
+ 'input:not([disabled]):not([type="hidden"])',
15
+ 'select:not([disabled])',
16
+ 'textarea:not([disabled])',
17
+ 'iframe',
18
+ '[tabindex]:not([tabindex="-1"])',
19
+ '[contenteditable="true"]',
20
+ ].join(',');
21
+
22
+ const focusables = Array.from(active.parentElement?.querySelectorAll<HTMLElement>(selector) ?? []).filter(
23
+ (el) => el.offsetParent !== null && getComputedStyle(el).visibility !== 'hidden',
24
+ );
25
+
26
+ const next = focusables.indexOf(active) + direction;
27
+
28
+ if (next >= 0 && next < focusables.length) {
29
+ focusables[next]?.focus();
30
+ }
31
+ };
package/dist/index.ts CHANGED
@@ -3,3 +3,4 @@ export * from './components';
3
3
  export * from './LuiModalAsync';
4
4
  export * from './panel';
5
5
  export * from './panel/types';
6
+ export * from './ribbon';
@@ -63,7 +63,7 @@ export const Panel = (props: PanelProps): ReactElement => {
63
63
  }
64
64
 
65
65
  const pos = props.position;
66
- if (savedState?.panelPosition) {
66
+ if (props.resizeable !== false && savedState?.panelPosition) {
67
67
  return savedState.panelPosition;
68
68
  }
69
69
 
@@ -100,7 +100,7 @@ export const Panel = (props: PanelProps): ReactElement => {
100
100
  return pick(result, ['width', 'height']);
101
101
  }
102
102
 
103
- if (savedState?.panelSize) {
103
+ if (props.resizeable !== false && savedState?.panelSize) {
104
104
  return cropSizeToWindow(savedState.panelSize);
105
105
  }
106
106
 
@@ -117,13 +117,13 @@ export const Panel = (props: PanelProps): ReactElement => {
117
117
  const setInitialPanelSize = useCallback(
118
118
  (size: PanelSize) => {
119
119
  // If panel was already sized from state, then don't resize
120
- if (savedState?.panelSize) {
120
+ if (props.resizeable !== false && savedState?.panelSize) {
121
121
  return;
122
122
  }
123
123
 
124
124
  setPanelSize(size);
125
125
  },
126
- [savedState?.panelSize, setPanelSize],
126
+ [props.resizeable, savedState?.panelSize, setPanelSize],
127
127
  );
128
128
 
129
129
  const dynamicResize = useCallback(() => {
@@ -0,0 +1,40 @@
1
+ import { PropsWithChildren } from 'react';
2
+ import { v4 } from 'uuid';
3
+
4
+ import { PanelContext } from './PanelContext';
5
+ import { PanelInstanceContext } from './PanelInstanceContext';
6
+
7
+ export interface PanelInlineProps {
8
+ title: string;
9
+ width: number;
10
+ }
11
+
12
+ export const PanelInline = ({ title, width, children }: PropsWithChildren<PanelInlineProps>) => {
13
+ return (
14
+ <PanelContext.Provider value={{ resizeable: false, resizePanel: () => {}, initialResizePanel: () => {} }}>
15
+ <PanelInstanceContext.Provider
16
+ value={{
17
+ setTitle: () => {},
18
+ title,
19
+ zIndex: 1500,
20
+ uniqueId: v4(),
21
+ dock: () => {},
22
+ panelClose: () => {},
23
+ dockId: undefined,
24
+ docked: false,
25
+ bringPanelToFront: () => {},
26
+ hidden: false,
27
+ setPanelWindow: () => {},
28
+ panelTogglePopout: () => {},
29
+ undock: () => {},
30
+ panelPoppedOut: false,
31
+ bounds: undefined,
32
+ }}
33
+ >
34
+ <div className={'WindowPanel'} title={title} style={{ width }}>
35
+ {children}
36
+ </div>
37
+ </PanelInstanceContext.Provider>
38
+ </PanelContext.Provider>
39
+ );
40
+ };
@@ -4,7 +4,6 @@ export interface PanelInstanceContextType {
4
4
  title: string;
5
5
  setTitle: (title: string) => void;
6
6
  uniqueId: string;
7
- panelName: string;
8
7
  bounds: string | Element | undefined;
9
8
  panelTogglePopout: () => void;
10
9
  panelClose: () => void;
@@ -29,7 +28,6 @@ const NoContextError = () => {
29
28
  export const PanelInstanceContext = createContext<PanelInstanceContextType>({
30
29
  title: 'Missing PanelInstanceContext Provider: title',
31
30
  uniqueId: 'Missing PanelInstanceContext Provider: uniqueId',
32
- panelName: 'Missing PanelInstanceContext Provider: panelName',
33
31
  bounds: document.body,
34
32
  setTitle: NoContextError,
35
33
  panelTogglePopout: NoContextError,
@@ -50,7 +50,6 @@ export const PanelInstanceContextProvider = ({
50
50
  setTitle,
51
51
  uniqueId,
52
52
  bounds,
53
- panelName: uniqueId,
54
53
  panelClose: () => {
55
54
  panelInstance.window?.close();
56
55
  panelInstance.window = null;
@@ -51,24 +51,28 @@ export const PanelsContextProvider = ({
51
51
 
52
52
  const bringPanelToFront = useCallback(
53
53
  (panelUniqueId: string) => {
54
- const panelInstance = panelInstances.find((panelInstance) => panelInstance.uniqueId === panelUniqueId);
55
- if (!panelInstance) {
56
- console.warn(`bringPanelToFront cannot find panel with uniqueId: ${panelUniqueId}`);
57
- return;
58
- }
59
- if (panelInstance.window) {
60
- panelInstance.window.focus();
61
- } else {
54
+ setPanelInstances((panelInstances): PanelInstance[] => {
55
+ const panelInstance = panelInstances.find((panelInstance) => panelInstance.uniqueId === panelUniqueId);
56
+ if (!panelInstance) {
57
+ console.warn(`bringPanelToFront cannot find panel with uniqueId: ${panelUniqueId}`);
58
+ return panelInstances;
59
+ }
60
+ if (panelInstance.window) {
61
+ panelInstance.window.focus();
62
+ return panelInstances;
63
+ }
62
64
  const maxZIndexPanelInstance = maxBy(panelInstances, 'zIndex');
63
65
  // Prevent unnecessary state updates
64
- if (maxZIndexPanelInstance === panelInstance) return;
66
+ if (maxZIndexPanelInstance === panelInstance) {
67
+ return panelInstances;
68
+ }
65
69
  sortBy(panelInstances, (pi) => (panelInstance === pi ? 32768 : pi.zIndex)).forEach(
66
70
  (pi, i) => (pi.zIndex = baseZIndex + i),
67
71
  );
68
- setPanelInstances([...panelInstances]);
69
- }
72
+ return [...panelInstances];
73
+ });
70
74
  },
71
- [baseZIndex, panelInstances],
75
+ [baseZIndex],
72
76
  );
73
77
 
74
78
  const showHidePanel = useCallback((hidePanelIds: string | string[], hidden: boolean) => {
@@ -104,39 +108,40 @@ export const PanelsContextProvider = ({
104
108
  const openPanel = useCallback(
105
109
  ({ componentFn, poppedOut = false, uniqueId = v4(), onClose }: OpenPanelOptions): string | null => {
106
110
  try {
107
- const existingPanelInstance = panelInstances.find((pi) => pi.uniqueId === uniqueId);
108
- if (existingPanelInstance) {
109
- if (existingPanelInstance.window) {
110
- existingPanelInstance.window?.focus();
111
- } else {
112
- if (existingPanelInstance.hidden) {
113
- unhidePanel(uniqueId);
111
+ setPanelInstances((panelInstances) => {
112
+ const existingPanelInstance = panelInstances.find((pi) => pi.uniqueId === uniqueId);
113
+ if (existingPanelInstance) {
114
+ if (existingPanelInstance.window) {
115
+ existingPanelInstance.window?.focus();
116
+ } else {
117
+ if (existingPanelInstance.hidden) {
118
+ setTimeout(() => unhidePanel(uniqueId), 0);
119
+ }
120
+ setTimeout(() => bringPanelToFront(uniqueId), 0);
114
121
  }
115
- bringPanelToFront(uniqueId);
122
+ return panelInstances;
116
123
  }
117
- return existingPanelInstance.uniqueId;
118
- }
119
124
 
120
- // If there are any exceptions the modal won't show
121
- setPanelInstances([
122
- ...panelInstances,
123
- {
124
- uniqueId,
125
- componentInstance: componentFn(),
126
- zIndex: baseZIndex + panelInstances.length,
127
- poppedOut,
128
- window: null,
129
- onClose,
130
- hidden: false,
131
- },
132
- ]);
125
+ return [
126
+ ...panelInstances,
127
+ {
128
+ uniqueId,
129
+ componentInstance: componentFn(),
130
+ zIndex: baseZIndex + panelInstances.length,
131
+ poppedOut,
132
+ window: null,
133
+ onClose,
134
+ hidden: false,
135
+ },
136
+ ];
137
+ });
133
138
  return uniqueId;
134
139
  } catch (e) {
135
140
  console.error(e);
136
141
  return null;
137
142
  }
138
143
  },
139
- [baseZIndex, bringPanelToFront, panelInstances, unhidePanel],
144
+ [baseZIndex, bringPanelToFront, unhidePanel],
140
145
  );
141
146
 
142
147
  const closePanel = useCallback((closePanelUniqueIds: string | string[]) => {
@@ -1,10 +1,10 @@
1
1
  export * from './OpenPanelButton';
2
- export * from './OpenPanelIcon';
3
2
  export * from './Panel';
4
3
  export * from './PanelContext';
5
4
  export * from './PanelDock';
6
5
  export * from './PanelHeader';
7
6
  export * from './PanelHeaderButton';
7
+ export * from './PanelInline';
8
8
  export * from './PanelInstanceContext';
9
9
  export * from './PanelInstanceContextProvider';
10
10
  export * from './PanelsContext';
@@ -0,0 +1,146 @@
1
+ @use "@linzjs/lui/dist/scss/Core" as lui;
2
+
3
+ .RibbonButton {
4
+ position: relative;
5
+ background-color: transparent;
6
+ padding: 4px;
7
+ margin: 2px;
8
+ fill: lui.$sea;
9
+ color: lui.$sea;
10
+ width: 40px;
11
+ height: 40px;
12
+ border: 2px solid white;
13
+ border-radius: 5px;
14
+
15
+ &:hover {
16
+ background-color: lui.$polar;
17
+ }
18
+
19
+ span.LuiIcon {
20
+ display: block;
21
+ margin: auto;
22
+ }
23
+ }
24
+
25
+ .RibbonButtonSkeleton {
26
+ padding: 4px;
27
+ margin: 0;
28
+ line-height: 1px;
29
+
30
+ > div {
31
+ padding: 0;
32
+ margin: 0;
33
+ border: 2px solid transparent;
34
+ }
35
+ }
36
+
37
+ .RibbonButton-selected, .RibbonButton:has(+ *:popover-open) {
38
+ cursor: pointer;
39
+ color: lui.$white !important;
40
+ background-color: lui.$sea !important;
41
+ box-shadow: inset 0 2px 4px rgb(41 92 130);
42
+
43
+ svg path {
44
+ color: lui.$white !important;
45
+ fill: lui.$white !important;
46
+ }
47
+ }
48
+
49
+ .RibbonButton-disabled {
50
+ background-color: lui.$white !important;
51
+
52
+ fill: lui.$grey-20 !important;
53
+
54
+ svg * {
55
+ fill: lui.$grey-20 !important;
56
+ }
57
+ }
58
+
59
+ %RibbonButton-Group {
60
+ background-color: white;
61
+ border-radius: 5px;
62
+ padding: 0;
63
+ align-items: center;
64
+ box-shadow: 0 0 10px rgb(0 0 0 / 20%);
65
+ display: inline-flex;
66
+ }
67
+
68
+ .RibbonButtonContent {
69
+ @extend %RibbonButton-Group;
70
+ }
71
+
72
+ .RibbonButton-separator {
73
+ background-color: lui.$grey-10;
74
+ }
75
+
76
+ .RibbonButton-verticalGroup {
77
+ @extend %RibbonButton-Group;
78
+ flex-direction: column;
79
+
80
+ .RibbonButton-separator {
81
+ margin-top: 1px;
82
+ margin-bottom: 1px;
83
+ height: 2px;
84
+ width: 44px;
85
+ }
86
+ }
87
+
88
+ .RibbonButton-horizontalGroup {
89
+ @extend %RibbonButton-Group;
90
+ flex-direction: row;
91
+
92
+ .RibbonButton-separator {
93
+ margin-left: 1px;
94
+ margin-right: 1px;
95
+ height: 44px;
96
+ width: 2px;
97
+ }
98
+ }
99
+
100
+ .RibbonSliderMenu[popover] {
101
+ margin: 0;
102
+ border: 0;
103
+ inset: auto;
104
+ background: transparent;
105
+ position: fixed;
106
+ }
107
+
108
+ .RibbonSliderMenu:popover-open {
109
+ display: flex;
110
+ flex-direction: row;
111
+ }
112
+
113
+ $RibbonSliderMenu-offset1: 2px;
114
+ $RibbonSliderMenu-offset2: 6px;
115
+
116
+ .RibbonSliderMenu-right, .RibbonSliderMenu-right-down {
117
+ translate: ($RibbonSliderMenu-offset1) (-$RibbonSliderMenu-offset2);
118
+ }
119
+
120
+ .RibbonSliderMenu-left, .RibbonSliderMenu-left-down {
121
+ translate: (-$RibbonSliderMenu-offset1) (-$RibbonSliderMenu-offset2);
122
+ }
123
+
124
+ .RibbonSliderMenu-right-up {
125
+ translate: ($RibbonSliderMenu-offset1) ($RibbonSliderMenu-offset2);
126
+ }
127
+
128
+ .RibbonSliderMenu-left-up {
129
+ translate: (-$RibbonSliderMenu-offset1) ($RibbonSliderMenu-offset2);
130
+ }
131
+
132
+ .RibbonSliderMenu-up, .RibbonSliderMenu-up-right {
133
+ translate: (-$RibbonSliderMenu-offset2) (-$RibbonSliderMenu-offset1);
134
+ }
135
+
136
+ .RibbonSliderMenu-down-left {
137
+ translate: ($RibbonSliderMenu-offset2) ($RibbonSliderMenu-offset1);
138
+ }
139
+
140
+ .RibbonSliderMenu-down, .RibbonSliderMenu-down-right {
141
+ translate: (-$RibbonSliderMenu-offset2) ($RibbonSliderMenu-offset1);
142
+ }
143
+
144
+ .RibbonSliderMenu-down-left {
145
+ translate: ($RibbonSliderMenu-offset2) ($RibbonSliderMenu-offset1);
146
+ }
@@ -0,0 +1,95 @@
1
+ import './Ribbon.scss';
2
+
3
+ import { LuiIcon } from '@linzjs/lui';
4
+ import { IconName } from '@linzjs/lui/dist/components/LuiIcon/LuiIcon';
5
+ import clsx from 'clsx';
6
+ import { forwardRef, ReactElement, useContext } from 'react';
7
+ import Skeleton from 'react-loading-skeleton';
8
+
9
+ import { tabToNextSiblingElement, tabToPreviousSiblingElement } from '../common';
10
+ import { RibbonButtonSliderContext } from './RibbonButtonSliderContext';
11
+
12
+ export interface RibbonButtonProps {
13
+ id?: string;
14
+ popoverTarget?: string;
15
+ popoverTargetAction?: 'toggle' | 'show' | 'hide' | undefined;
16
+ anchorName?: string;
17
+ title?: string;
18
+ icon?: IconName;
19
+ content?: ReactElement;
20
+ disabled?: boolean;
21
+ selected?: boolean;
22
+ loading?: boolean;
23
+ className?: string;
24
+ testId?: string;
25
+ onClick?: () => void;
26
+ }
27
+
28
+ export const RibbonButton = forwardRef<HTMLButtonElement, RibbonButtonProps>(function RibbonButton(
29
+ {
30
+ id,
31
+ popoverTarget,
32
+ popoverTargetAction,
33
+ anchorName,
34
+ title,
35
+ icon,
36
+ content,
37
+ className,
38
+ selected,
39
+ loading,
40
+ disabled,
41
+ testId,
42
+ onClick,
43
+ },
44
+ ref,
45
+ ) {
46
+ const { menuUniqueId, autoClose } = useContext(RibbonButtonSliderContext);
47
+
48
+ if (loading) {
49
+ return (
50
+ <div className={'RibbonButtonSkeleton'}>
51
+ <Skeleton
52
+ className={clsx(
53
+ disabled && 'disabled', // disabled to style cursor
54
+ )}
55
+ height={36}
56
+ width={36}
57
+ />
58
+ </div>
59
+ );
60
+ }
61
+
62
+ return (
63
+ <button
64
+ ref={ref}
65
+ id={id}
66
+ style={{ anchorName }}
67
+ {...((popoverTarget || menuUniqueId) && { ['popovertarget' as never]: popoverTarget ?? menuUniqueId })}
68
+ {...((popoverTargetAction || autoClose != null) && {
69
+ ['popovertargetaction' as never]: popoverTargetAction ?? (autoClose ? 'toggle' : 'show'),
70
+ })}
71
+ type="button"
72
+ title={title}
73
+ data-testid={testId}
74
+ disabled={disabled}
75
+ onClick={onClick}
76
+ className={clsx(
77
+ className,
78
+ 'RibbonButton',
79
+ selected && 'RibbonButton-selected',
80
+ disabled && 'RibbonButton-disabled',
81
+ )}
82
+ onKeyDown={({ code }) => {
83
+ if (code === 'ArrowDown' || code === 'ArrowRight') {
84
+ tabToNextSiblingElement();
85
+ }
86
+ if (code === 'ArrowUp' || code === 'ArrowLeft') {
87
+ tabToPreviousSiblingElement();
88
+ }
89
+ }}
90
+ >
91
+ {icon && <LuiIcon name={icon} alt={title ?? ''} size={'md'} />}
92
+ {content}
93
+ </button>
94
+ );
95
+ });
@@ -0,0 +1,26 @@
1
+ import './Ribbon.scss';
2
+
3
+ import { forwardRef } from 'react';
4
+
5
+ import { RibbonButton, RibbonButtonProps } from './RibbonButton';
6
+
7
+ export type RibbonButtonLinkProps = RibbonButtonProps & {
8
+ href: string;
9
+ target?: string;
10
+ };
11
+
12
+ export const RibbonButtonLink = forwardRef<HTMLButtonElement, RibbonButtonLinkProps>(function RibbonButtonLink(
13
+ { onClick, href, target = '_blank', ...props },
14
+ ref,
15
+ ) {
16
+ return (
17
+ <RibbonButton
18
+ ref={ref}
19
+ {...props}
20
+ onClick={() => {
21
+ onClick?.();
22
+ void window.open(href, target);
23
+ }}
24
+ />
25
+ );
26
+ });
@@ -0,0 +1,30 @@
1
+ import './Ribbon.scss';
2
+
3
+ import clsx from 'clsx';
4
+ import { forwardRef, useContext } from 'react';
5
+
6
+ import { OpenPanelOptions, PanelsContext } from '../panel/PanelsContext';
7
+ import { RibbonButton, RibbonButtonProps } from './RibbonButton';
8
+
9
+ export type RibbonButtonOpenPanelProps = Omit<RibbonButtonProps, 'onClick'> & OpenPanelOptions;
10
+ export const RibbonButtonOpenPanel = forwardRef<HTMLButtonElement, RibbonButtonOpenPanelProps>(
11
+ function RibbonButtonOpenPanel(
12
+ { title, icon, className, disabled, testId, uniqueId = title, loading, ...openPanelOptions },
13
+ ref,
14
+ ) {
15
+ const { openPanel, openPanels } = useContext(PanelsContext);
16
+
17
+ return (
18
+ <RibbonButton
19
+ ref={ref}
20
+ testId={testId}
21
+ icon={icon}
22
+ title={title}
23
+ disabled={disabled}
24
+ loading={loading}
25
+ onClick={() => openPanel({ uniqueId, ...openPanelOptions })}
26
+ className={clsx(openPanels.has(uniqueId ?? '') && 'RibbonButton-selected', className)}
27
+ />
28
+ );
29
+ },
30
+ );
@@ -0,0 +1,30 @@
1
+ import { createContext } from 'react';
2
+
3
+ export const ribbonSliderAlignments = {
4
+ right: ['left', 'right', 'top'],
5
+ 'right-down': ['left', 'right', 'top'],
6
+ 'right-up': ['left', 'right', 'bottom'],
7
+ left: ['right', 'left', 'top'],
8
+ 'left-down': ['right', 'left', 'top'],
9
+ 'left-up': ['right', 'left', 'bottom'],
10
+ 'down-left': ['top', 'bottom', 'right'],
11
+ 'down-right': ['top', 'bottom', 'left'],
12
+ down: ['top', 'bottom', 'left'],
13
+ 'up-left': ['bottom', 'top', 'right'],
14
+ 'up-right': ['bottom', 'top', 'left'],
15
+ up: ['bottom', 'top', 'left'],
16
+ };
17
+
18
+ export type RibbonSliderAlignment = keyof typeof ribbonSliderAlignments;
19
+
20
+ export interface RibbonButtonSliderContextType {
21
+ autoClose: boolean | undefined;
22
+ menuUniqueId: string;
23
+ positionAnchor: string;
24
+ }
25
+
26
+ export const RibbonButtonSliderContext = createContext<RibbonButtonSliderContextType>({
27
+ autoClose: undefined,
28
+ menuUniqueId: '',
29
+ positionAnchor: '',
30
+ });
@@ -0,0 +1,20 @@
1
+ import './Ribbon.scss';
2
+
3
+ import { CSSProperties, PropsWithChildren } from 'react';
4
+
5
+ export interface RibbonContainerProps {
6
+ orientation?: 'horizontal' | 'vertical';
7
+ style?: CSSProperties;
8
+ className?: string;
9
+ }
10
+
11
+ export const RibbonContainer = ({
12
+ orientation = 'horizontal',
13
+ style,
14
+ className,
15
+ children,
16
+ }: PropsWithChildren<RibbonContainerProps>) => (
17
+ <div style={style} className={className}>
18
+ <div className={`RibbonButton-${orientation}Group`}>{children}</div>
19
+ </div>
20
+ );
@@ -0,0 +1,27 @@
1
+ import { PropsWithChildren } from 'react';
2
+
3
+ import { RibbonButtonProps } from './RibbonButton';
4
+ import { RibbonButtonOpenPanel, RibbonButtonOpenPanelProps } from './RibbonButtonOpenPanel';
5
+ import { RibbonContainer } from './RibbonContainer';
6
+ import { RibbonSeparator } from './RibbonSeparator';
7
+
8
+ // @Deprecated use RibbonPanelButton
9
+ export type OpenPanelIconProps = RibbonButtonProps;
10
+
11
+ // @Deprecated use RibbonPanelButton
12
+ export const OpenPanelIcon = ({ iconTitle, ...props }: RibbonButtonOpenPanelProps & { iconTitle?: string }) => (
13
+ <RibbonButtonOpenPanel title={iconTitle} {...props} />
14
+ );
15
+
16
+ // @Deprecated use RibbonContainer
17
+ export const ButtonIconHorizontalGroup = (props: PropsWithChildren) => (
18
+ <RibbonContainer orientation={'horizontal'} {...props} />
19
+ );
20
+
21
+ // @Deprecated use RibbonContainer
22
+ export const ButtonIconVerticalGroup = (props: PropsWithChildren) => (
23
+ <RibbonContainer orientation={'vertical'} {...props} />
24
+ );
25
+
26
+ // @Deprecated use ButtonIconSeparator
27
+ export const ButtonIconSeparator = RibbonSeparator;
@@ -0,0 +1,45 @@
1
+ @use "@linzjs/lui/dist/scss/Core" as lui;
2
+
3
+ .RibbonMenuSeparator {
4
+ height: 1px;
5
+ background-color: lui.$dew;
6
+ margin: 4px;
7
+ }
8
+
9
+ .RibbonMenuOption {
10
+ font-family: "Open Sans", system-ui, sans-serif;
11
+ color: lui.$charcoal;
12
+
13
+ path {
14
+ fill: lui.$fuscous;
15
+ }
16
+
17
+ min-width: 180px;
18
+ display: flex;
19
+ gap: 8px;
20
+ background-color: transparent;
21
+ font-weight: 400;
22
+ box-sizing: border-box;
23
+ border: 1px transparent;
24
+ outline: none;
25
+ padding: 6px 12px;
26
+ line-height: 24px;
27
+ font-size: 16px;
28
+ align-items: center;;
29
+
30
+ &:hover {
31
+ background-color: lui.$polar;
32
+ }
33
+
34
+ &:focus {
35
+ background-color: lui.$polar;
36
+ }
37
+
38
+ &:disabled {
39
+ color: lui.$disabled-color;
40
+
41
+ path {
42
+ fill: lui.$disabled-color;
43
+ }
44
+ }
45
+ }
@@ -0,0 +1,25 @@
1
+ import { createContext, PropsWithChildren, useState } from 'react';
2
+
3
+ import { RibbonContainer } from './RibbonContainer';
4
+
5
+ export interface RibbonMenuContext {
6
+ hasIcon: boolean;
7
+ setHasIcon: (hasIcon: boolean) => void;
8
+ }
9
+
10
+ export const RibbonMenuContext = createContext<RibbonMenuContext>({
11
+ hasIcon: false,
12
+ setHasIcon: () => {},
13
+ });
14
+
15
+ export const RibbonMenu = ({ children }: PropsWithChildren) => {
16
+ const [hasIcon, setHasIcon] = useState(false);
17
+
18
+ return (
19
+ <RibbonMenuContext.Provider value={{ hasIcon, setHasIcon }}>
20
+ <RibbonContainer orientation={'vertical'}>
21
+ <div style={{ padding: '4px 0' }}>{children}</div>
22
+ </RibbonContainer>
23
+ </RibbonMenuContext.Provider>
24
+ );
25
+ };
@@ -0,0 +1,67 @@
1
+ import './RibbonMenu.scss';
2
+
3
+ import { LuiIcon } from '@linzjs/lui';
4
+ import { IconName } from '@linzjs/lui/dist/components/LuiIcon/LuiIcon';
5
+ import clsx from 'clsx';
6
+ import { forwardRef, PropsWithChildren, useContext, useEffect } from 'react';
7
+
8
+ import { tabToNextSiblingElement, tabToPreviousSiblingElement } from '../common/util';
9
+ import { RibbonButtonSliderContext } from './RibbonButtonSliderContext';
10
+ import { RibbonMenuContext } from './RibbonMenu';
11
+
12
+ export interface RibbonMenuOption {
13
+ id?: string;
14
+ popoverTarget?: string;
15
+ popoverTargetAction?: 'toggle' | 'show' | 'hide' | undefined;
16
+ anchorName?: string;
17
+ title?: string;
18
+ icon?: IconName;
19
+ disabled?: boolean;
20
+ className?: string;
21
+ testId?: string;
22
+ onClick?: () => void;
23
+ }
24
+
25
+ export const RibbonMenuOption = forwardRef<HTMLButtonElement, PropsWithChildren<RibbonMenuOption>>(
26
+ function RibbonButton(
27
+ { id, popoverTarget, popoverTargetAction, anchorName, title, icon, children, className, disabled, testId, onClick },
28
+ ref,
29
+ ) {
30
+ const { menuUniqueId, autoClose } = useContext(RibbonButtonSliderContext);
31
+ const { hasIcon, setHasIcon } = useContext(RibbonMenuContext);
32
+
33
+ useEffect(() => {
34
+ setHasIcon(!!icon);
35
+ }, [icon, setHasIcon]);
36
+
37
+ return (
38
+ <button
39
+ ref={ref}
40
+ id={id}
41
+ style={{ anchorName }}
42
+ {...{ ['popovertarget' as never]: popoverTarget ?? menuUniqueId }}
43
+ {...{ ['popovertargetaction' as never]: popoverTargetAction ?? (autoClose ? 'toggle' : 'show') }}
44
+ type="button"
45
+ title={title}
46
+ data-testid={testId}
47
+ disabled={disabled}
48
+ onKeyDown={({ code }) => {
49
+ if (code === 'ArrowDown') {
50
+ tabToNextSiblingElement();
51
+ }
52
+ if (code === 'ArrowUp') {
53
+ tabToPreviousSiblingElement();
54
+ }
55
+ }}
56
+ onMouseMove={({ currentTarget }) => {
57
+ currentTarget?.focus?.();
58
+ }}
59
+ onClick={onClick}
60
+ className={clsx(className, 'RibbonMenuOption', disabled && 'RibbonMenuOption-disabled')}
61
+ >
62
+ <div style={{ display: 'flex' }}>{icon && <LuiIcon name={icon} alt={title ?? ''} size={'md'} />}</div>
63
+ <div style={hasIcon && !icon ? { paddingLeft: 24 } : {}}>{children}</div>
64
+ </button>
65
+ );
66
+ },
67
+ );
@@ -0,0 +1,5 @@
1
+ import './Ribbon.scss';
2
+
3
+ export const RibbonMenuSeparator = () => {
4
+ return <div className="RibbonMenuSeparator" />;
5
+ };
@@ -0,0 +1,3 @@
1
+ import './Ribbon.scss';
2
+
3
+ export const RibbonSeparator = () => <div className="RibbonButton-separator">&nbsp;</div>;
@@ -0,0 +1,63 @@
1
+ import './Ribbon.scss';
2
+
3
+ import { PropsWithChildren, ReactElement, useState } from 'react';
4
+ import { v4 } from 'uuid';
5
+
6
+ import { RibbonButton, RibbonButtonProps } from './RibbonButton';
7
+ import { RibbonButtonSliderContext, RibbonSliderAlignment, ribbonSliderAlignments } from './RibbonButtonSliderContext';
8
+
9
+ export interface RibbonSliderProps extends RibbonButtonProps {
10
+ autoClose?: boolean;
11
+ alignment: RibbonSliderAlignment;
12
+ }
13
+
14
+ export const RibbonButtonSlider = ({
15
+ alignment,
16
+ autoClose = false,
17
+ children,
18
+ onClick,
19
+ ...buttonProps
20
+ }: PropsWithChildren<RibbonSliderProps>): ReactElement => {
21
+ const [uniqueId] = useState(v4());
22
+ const [menuUniqueId] = useState(`toolbarSliderMenu-${v4()}`);
23
+ const [positionAnchor] = useState(`--toolbarSliderAnchor-${uniqueId}`);
24
+
25
+ const defaultOnClick = () => setTimeout(focusNextRibbonButton, 0);
26
+
27
+ const alignments = ribbonSliderAlignments[alignment];
28
+
29
+ return (
30
+ <RibbonButtonSliderContext.Provider value={{ autoClose, positionAnchor, menuUniqueId }}>
31
+ <RibbonButton
32
+ popoverTarget={menuUniqueId}
33
+ popoverTargetAction={'toggle'}
34
+ anchorName={positionAnchor}
35
+ {...buttonProps}
36
+ onClick={onClick ?? defaultOnClick}
37
+ />
38
+ <nav
39
+ id={menuUniqueId}
40
+ {...{ ['popover' as never]: 'auto' }}
41
+ role="menu"
42
+ aria-label="Menu"
43
+ className={`RibbonSliderMenu RibbonSliderMenu-${alignment}`}
44
+ style={{
45
+ positionAnchor,
46
+ [alignments[0]]: `anchor(${positionAnchor} ${alignments[1]})`,
47
+ [alignments[2]]: `anchor(${positionAnchor} ${alignments[2]})`,
48
+ }}
49
+ >
50
+ {children}
51
+ </nav>
52
+ </RibbonButtonSliderContext.Provider>
53
+ );
54
+ };
55
+
56
+ const focusNextRibbonButton = () => {
57
+ let button =
58
+ document.activeElement?.nextElementSibling?.querySelectorAll<HTMLButtonElement>('button.RibbonButton-selected')[0];
59
+ if (!button) {
60
+ button = document.activeElement?.nextElementSibling?.querySelectorAll('button')[0];
61
+ }
62
+ button?.focus();
63
+ };
@@ -0,0 +1,11 @@
1
+ export * from './RibbonButton';
2
+ export * from './RibbonButtonLink';
3
+ export * from './RibbonButtonOpenPanel';
4
+ export * from './RibbonButtonSliderContext';
5
+ export * from './RibbonContainer';
6
+ export * from './RibbonDeprecated';
7
+ export * from './RibbonMenu';
8
+ export * from './RibbonMenuOption';
9
+ export * from './RibbonMenuSeparator';
10
+ export * from './RibbonSeparator';
11
+ export * from './RibbonSliderButton';
package/package.json CHANGED
@@ -13,7 +13,7 @@
13
13
  "popout"
14
14
  ],
15
15
  "main": "./dist/index.ts",
16
- "version": "8.8.2",
16
+ "version": "9.0.0",
17
17
  "peerDependencies": {
18
18
  "@linzjs/lui": ">=23",
19
19
  "lodash-es": ">=4",
@@ -52,62 +52,63 @@
52
52
  "@emotion/react": "^11.14.0",
53
53
  "@emotion/styled": "11.14.1",
54
54
  "@types/uuid": "^11.0.0",
55
- "lodash-es": ">=4",
56
- "react-rnd": "^10.5.2",
55
+ "lodash-es": "^4.17.23",
56
+ "react-loading-skeleton": "^3.5.0",
57
+ "react-rnd": "^10.5.3",
57
58
  "usehooks-ts": "^3.1.1",
58
59
  "uuid": "^13.0.0"
59
60
  },
60
61
  "devDependencies": {
61
62
  "@chromatic-com/storybook": "^4.1.3",
62
- "@linzjs/lui": "^24.4.3",
63
+ "@linzjs/lui": "^24.10.1",
63
64
  "@linzjs/step-ag-grid": "^29.14.1",
64
65
  "@linzjs/style": "^5.4.0",
65
66
  "@rollup/plugin-commonjs": "^28.0.9",
66
67
  "@rollup/plugin-json": "^6.1.0",
67
68
  "@rollup/plugin-node-resolve": "^16.0.3",
68
- "@storybook/addon-docs": "^9.1.17",
69
- "@storybook/addon-links": "^9.1.17",
70
- "@storybook/react-vite": "^9.1.17",
69
+ "@storybook/addon-docs": "^9.1.20",
70
+ "@storybook/addon-links": "^9.1.20",
71
+ "@storybook/react-vite": "^9.1.20",
71
72
  "@testing-library/dom": "^10.4.1",
72
- "@testing-library/react": "^16.3.0",
73
+ "@testing-library/react": "^16.3.2",
73
74
  "@testing-library/user-event": "^14.6.1",
74
75
  "@types/lodash-es": "^4.17.12",
75
76
  "@types/node": "^22.19.2",
76
- "@types/react": "^19.2.7",
77
- "@types/react-dom": "^19.2.3",
78
- "@vitejs/plugin-react-swc": "^4.2.2",
79
- "@vitest/ui": "^4.0.15",
77
+ "@types/react": "^18.3.28",
78
+ "@types/react-dom": "^18.3.7",
79
+ "@vitejs/plugin-react-swc": "^4.3.0",
80
+ "@vitest/ui": "^4.1.0",
80
81
  "ag-grid-community": "34.2.0",
81
82
  "ag-grid-react": "34.2.0",
82
83
  "eslint-plugin-react": "^7.37.5",
83
- "eslint-plugin-storybook": "^9.1.17",
84
+ "eslint-plugin-storybook": "^9.1.20",
84
85
  "jsdom": "^27.3.0",
85
86
  "mkdirp": "^3.0.1",
86
87
  "npm-run-all": "^4.1.5",
87
- "react": "18.3.1",
88
+ "react": "^18.3.1",
88
89
  "react-app-polyfill": "^3.0.0",
89
90
  "react-dom": "18.3.1",
90
- "rollup": "^4.53.3",
91
+ "rollup": "^4.59.0",
91
92
  "rollup-plugin-copy": "^3.5.0",
92
- "sass": "^1.96.0",
93
- "sass-loader": "^16.0.6",
94
- "storybook": "^9.1.17",
93
+ "sass": "^1.98.0",
94
+ "sass-loader": "^16.0.7",
95
+ "storybook": "^9.1.19",
95
96
  "style-loader": "^4.0.0",
96
97
  "stylelint": "^16.26.1",
97
98
  "stylelint-config-recommended": "^17.0.0",
98
99
  "stylelint-config-recommended-scss": "^16.0.2",
99
100
  "stylelint-config-standard": "^39.0.1",
100
101
  "stylelint-prettier": "5.0.3",
101
- "stylelint-scss": "6.13.0",
102
+ "stylelint-scss": "6.14.0",
102
103
  "typescript": "^5.9.3",
103
- "vite": "^7.2.7",
104
+ "vite": "^7.3.1",
104
105
  "vite-plugin-html": "^3.2.2",
105
106
  "vite-tsconfig-paths": "^5.1.4",
106
- "vitest": "^4.0.15"
107
+ "vitest": "^4.1.0"
107
108
  },
108
109
  "optionalDependencies": {
109
- "@rollup/rollup-linux-x64-gnu": "^4.53.3",
110
- "@swc/core-linux-x64-gnu": "^1.15.3"
110
+ "@rollup/rollup-linux-x64-gnu": "^4.59.0",
111
+ "@swc/core-linux-x64-gnu": "^1.15.18"
111
112
  },
112
113
  "browserslist": {
113
114
  "production": [
@@ -1,72 +0,0 @@
1
- @use "@linzjs/lui/dist/scss/Core" as lui;
2
-
3
- .lui-button.lui-button-toolbar {
4
- border-color: transparent;
5
- padding: 4px;
6
- line-height: 12px;
7
- margin: 2px;
8
- }
9
-
10
- .OpenPanelIcon-selected {
11
- cursor: pointer;
12
- color: lui.$white !important;
13
- background-color: lui.$blue-75 !important;
14
- box-shadow: inset 0 2px 4px rgb(41 92 130);
15
-
16
- fill: lui.$white !important;
17
-
18
- svg * {
19
- fill: lui.$white !important;
20
- }
21
-
22
- svg * {
23
- color: lui.$white !important;
24
- fill: lui.$white !important;
25
- }
26
- }
27
-
28
- .OpenPanelIcon-disabled {
29
- background-color: lui.$white !important;
30
-
31
- fill: lui.$grey-20 !important;
32
-
33
- svg * {
34
- fill: lui.$grey-20 !important;
35
- }
36
- }
37
-
38
- %OpenPanelIcon-Group {
39
- background-color: white;
40
- border-radius: 4px;
41
- padding: 4px;
42
- align-items: center;
43
- box-shadow: 0 0 10px rgb(0 0 0 / 20%);
44
- display: inline-flex;
45
- }
46
-
47
- .OpenPanelIcon-verticalGroup {
48
- @extend %OpenPanelIcon-Group;
49
- flex-direction: column;
50
-
51
- .OpenPanelIcon-separator {
52
- margin: 6px 0;
53
- height: 2px;
54
- width: 100%;
55
- background-color: lui.$grey-10;
56
- }
57
- }
58
-
59
- .OpenPanelIcon-horizontalGroup {
60
- @extend %OpenPanelIcon-Group;
61
- flex-direction: row;
62
-
63
- .OpenPanelIcon-separator {
64
- height: 100%;
65
- margin-left: 6px;
66
- margin-right: 6px;
67
- margin-top: -3px;
68
- padding-bottom: 8px;
69
- width: 2px;
70
- background-color: lui.$grey-10;
71
- }
72
- }
@@ -1,60 +0,0 @@
1
- import './OpenPanelIcon.scss';
2
-
3
- import { LuiIcon } from '@linzjs/lui';
4
- import { IconName } from '@linzjs/lui/dist/components/LuiIcon/LuiIcon';
5
- import clsx from 'clsx';
6
- import { PropsWithChildren, useContext } from 'react';
7
-
8
- import { OpenPanelOptions, PanelsContext } from './PanelsContext';
9
-
10
- export const ButtonIconHorizontalGroup = ({ children }: PropsWithChildren) => (
11
- <div>
12
- <div className={'OpenPanelIcon-horizontalGroup'}>{children}</div>
13
- </div>
14
- );
15
-
16
- export const ButtonIconVerticalGroup = ({ children }: PropsWithChildren) => (
17
- <div>
18
- <div className={'OpenPanelIcon-verticalGroup'}>{children}</div>
19
- </div>
20
- );
21
-
22
- export const ButtonIconSeparator = () => <div className="OpenPanelIcon-separator">&#160;</div>;
23
-
24
- interface OpenPanelIconProps extends OpenPanelOptions {
25
- iconTitle: string;
26
- icon: IconName;
27
- disabled?: boolean;
28
- className?: string;
29
- testId?: string;
30
- }
31
-
32
- export const OpenPanelIcon = ({
33
- iconTitle,
34
- icon,
35
- className,
36
- disabled,
37
- testId,
38
- uniqueId = iconTitle,
39
- ...openPanelOptions
40
- }: OpenPanelIconProps) => {
41
- const { openPanel, openPanels } = useContext(PanelsContext);
42
-
43
- return (
44
- <button
45
- type="button"
46
- className={clsx(
47
- className,
48
- 'lui-button lui-button-secondary lui-button-toolbar panel-button',
49
- openPanels.has(uniqueId) && 'OpenPanelIcon-selected',
50
- disabled && 'OpenPanelIcon-disabled',
51
- )}
52
- title={iconTitle}
53
- onClick={() => openPanel({ uniqueId, ...openPanelOptions })}
54
- disabled={disabled}
55
- data-testid={testId}
56
- >
57
- <LuiIcon name={icon} alt={iconTitle} size={'md'} />
58
- </button>
59
- );
60
- };