Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
GermanVor committed Feb 8, 2024
1 parent aea9fe5 commit 4d995b1
Show file tree
Hide file tree
Showing 8 changed files with 256 additions and 308 deletions.
3 changes: 3 additions & 0 deletions src/components/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export interface ButtonProps extends DOMProps, QAProps {
onMouseLeave?: React.MouseEventHandler<HTMLButtonElement | HTMLAnchorElement>;
onFocus?: React.FocusEventHandler<HTMLButtonElement | HTMLAnchorElement>;
onBlur?: React.FocusEventHandler<HTMLButtonElement | HTMLAnchorElement>;
onKeyDown?: React.KeyboardEventHandler<HTMLButtonElement>

Check failure on line 81 in src/components/Button/Button.tsx

View workflow job for this annotation

GitHub Actions / Verify Files

Insert `;`
/** Button content. You can mix button text with `<Icon/>` component */
children?: React.ReactNode;
}
Expand Down Expand Up @@ -106,6 +107,7 @@ const ButtonWithHandlers = React.forwardRef<HTMLElement, ButtonProps>(function B
onMouseLeave,
onFocus,
onBlur,
onKeyDown,
children,
id,
style,
Expand Down Expand Up @@ -138,6 +140,7 @@ const ButtonWithHandlers = React.forwardRef<HTMLElement, ButtonProps>(function B
onMouseLeave,
onFocus,
onBlur,
onKeyDown,
id,
style,
className: b(
Expand Down
20 changes: 20 additions & 0 deletions src/components/Table/__stories__/Table.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -178,9 +178,29 @@ const WithTableSettingsTemplate: StoryFn<TableProps<DataItem>> = (args, context)
}
};
export const HOCWithTableSettings = WithTableSettingsTemplate.bind({});
HOCWithTableSettings.parameters = {
// Strict mode ruins sortable list due to this react-beautiful-dnd issue
// https://github.com/atlassian/react-beautiful-dnd/issues/2350
disableStrictMode: true,
};
const columnsWithSettings = _cloneDeep(columns);
const COLUMN_IDX = 2;
columnsWithSettings[COLUMN_IDX].meta = columnsWithSettings[COLUMN_IDX].meta || {};
columnsWithSettings[COLUMN_IDX].meta.selectedAlways = true;

columnsWithSettings[3].meta = columnsWithSettings[3].meta || {};
columnsWithSettings[3].meta.selectedAlways = true;
HOCWithTableSettings.args = {
columns: columnsWithSettings,
};

export const HOCWithTableSettingsFactory = WithTableSettingsTemplate.bind({});
HOCWithTableSettingsFactory.parameters = {
isFactory: true,

// Strict mode ruins sortable list due to this react-beautiful-dnd issue
// https://github.com/atlassian/react-beautiful-dnd/issues/2350
disableStrictMode: true,
};

// ---------------------------------
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import React from 'react';

import {Check, Gear, Lock} from '@gravity-ui/icons';
import {Gear, Lock} from '@gravity-ui/icons';

import type {PopperPlacement} from '../../../../../hooks/private';
import {useActionHandlers} from '../../../../../hooks/useActionHandlers';
import {Button} from '../../../../Button';
import {Icon} from '../../../../Icon';
import {List} from '../../../../List';
import {Popup} from '../../../../Popup';
import type {TreeSelectProps} from '../../../../TreeSelect';
import {DndTreeSelect} from '../../../../TreeSelect/DndTreeSelect';
import type {
DndTreeSelectItemType,
RenderDndContainerType,
} from '../../../../TreeSelect/DndTreeSelect';
import {block} from '../../../../utils/cn';
import type {TableColumnSetupItem} from '../withTableSettings';

Expand All @@ -17,199 +20,130 @@ import './TableColumnSetup.scss';

const b = block('table-column-setup');

type Item = TableColumnSetupItem;

interface SwitcherProps {
onKeyDown: React.KeyboardEventHandler<HTMLElement>;
onClick: React.MouseEventHandler<HTMLElement>;
}

type Item = TableColumnSetupItem & DndTreeSelectItemType;
const prepareItem = (tableColumnItem: TableColumnSetupItem): Item => {
const hasSelectionIcon = tableColumnItem.isRequired === false;

return {
...tableColumnItem,
startSlot: hasSelectionIcon ? undefined : <Icon data={Lock} />,
hasSelectionIcon,
hasSelectionBackground: false,
};
};

const prepareItems = (tableColumnItem: TableColumnSetupItem[]): Item[] => {
return tableColumnItem.map(prepareItem);
};

export interface TableColumnSetupProps {
renderSwitcher?: (props: SwitcherProps) => React.ReactElement | undefined;

// for List
items: Item[];
items: TableColumnSetupItem[];
sortable?: boolean;

onUpdate: (updated: Item[]) => void;
onUpdate: (updated: TableColumnSetupItem[]) => void;
popupWidth?: number | string;
popupPlacement?: PopperPlacement;
}

export const TableColumnSetup = (props: TableColumnSetupProps) => {
const {renderSwitcher, popupWidth, popupPlacement, items: propsItems, sortable = true} = props;

const [focused, setFocused] = React.useState(false);
const [items, setItems] = React.useState<Item[]>([]);
const [currentItems, setCurrentItems] = React.useState<Item[]>([]);
const [requiredItems, setRequiredItems] = React.useState<Item[]>([]);
const {
renderSwitcher,
// popupWidth,
popupPlacement,
items: propsItems,
onUpdate: propsOnUpdate,
} = props;

const refControl = React.useRef(null);
const [open, setOpen] = React.useState(false);

const LIST_ITEM_HEIGHT = 36;

const getRequiredItems = (list: Item[]) =>
list
.filter(({required}) => required)
.map((column) => ({
...column,
disabled: true,
}));

const getConfigurableItems = (list: Item[]) => list.filter(({required}) => !required);
const [items, setItems] = React.useState(prepareItems(propsItems));

React.useEffect(() => {
if (propsItems !== items) {
setItems(propsItems);
setRequiredItems(getRequiredItems(propsItems));
setCurrentItems(getConfigurableItems(propsItems));
}
}, [items, propsItems]);

const setInitialState = () => {
setFocused(false);
setRequiredItems(getRequiredItems(items));
setCurrentItems(getConfigurableItems(items));
};

const getListHeight = (list: Item[]) => {
return Math.min(5, list.length) * LIST_ITEM_HEIGHT + LIST_ITEM_HEIGHT / 2;
};

const getRequiredListHeight = (list: Item[]) => {
return list.length * LIST_ITEM_HEIGHT;
};

const makeOnSortEnd =
(list: Item[]) =>
({oldIndex, newIndex}: {oldIndex: number; newIndex: number}) => {
setCurrentItems(List.moveListElement(list.slice(), oldIndex, newIndex));
};
const newItems = prepareItems(propsItems);
setItems(newItems);
}, [propsItems]);

const handleUpdate = (value: Item[]) => setCurrentItems(value);

const handleClosePopup = () => setInitialState();

const handleControlClick = React.useCallback(() => {
setFocused(!focused);
setRequiredItems(getRequiredItems(items));
setCurrentItems(getConfigurableItems(items));
}, [focused, items]);

const handleApplyClick = () => {
setInitialState();

const newItems = requiredItems.concat(currentItems);

if (items !== newItems) {
props.onUpdate(newItems);
}
const onApply = () => {
propsOnUpdate(items);
setOpen(false);
};

const handleItemClick = (value: Item) => {
const newItems = currentItems.map((item) =>
item === value ? {...item, selected: !item.selected} : item,
const renderContainer: RenderDndContainerType = ({renderList}) => {
return (
<React.Fragment>
{renderList()}
<Button view="action" width="max" onClick={onApply}>
{i18n('button_apply')}
</Button>
</React.Fragment>
);
handleUpdate(newItems);
};

const renderItem = (item: Item) => {
const renderControl: TreeSelectProps<unknown>['renderControl'] = ({toggleOpen}) => {
return (
<div className={b('item-content')}>
{item.required ? (
<div className={b('lock-wrap', {visible: item.selected})}>
<Icon data={Lock} />
</div>
) : (
<div className={b('tick-wrap', {visible: item.selected})}>
<Icon data={Check} className={b('tick')} width={10} height={10} />
</div>
)}
<div className={b('title')}>{item.title}</div>
</div>
renderSwitcher?.({onClick: toggleOpen, onKeyDown: toggleOpen}) || (
<Button onClick={toggleOpen}>
<Icon data={Gear} />
{i18n('button_switcher')}
</Button>
)
);
};

const renderRequiredColumns = () => {
const hasRequiredColumns = requiredItems.length;
const onOpenChange = (open: boolean) => {
setOpen(open);

if (!hasRequiredColumns) {
return null;
if (open === false) {
const initialItems = prepareItems(propsItems);
setItems(initialItems);
}

return (
<List
items={requiredItems}
itemHeight={LIST_ITEM_HEIGHT}
itemsHeight={getRequiredListHeight}
renderItem={renderItem}
itemsClassName={b('items')}
itemClassName={b('item')}
virtualized={false}
/>
);
};

const renderConfigurableColumns = () => {
return (
<List
items={currentItems}
itemHeight={LIST_ITEM_HEIGHT}
itemsHeight={getListHeight}
sortable={sortable}
sortHandleAlign={'right'}
onSortEnd={makeOnSortEnd(currentItems)}
onItemClick={handleItemClick}
renderItem={renderItem}
itemsClassName={b('items')}
itemClassName={b('item')}
virtualized={false}
/>
);
};
const onUpdate = React.useCallback((selectedItemsIds: string[]) => {
setItems((prevItems) => {
return prevItems.map((item) => ({
...item,
isSelected: selectedItemsIds.includes(item.id),
}));
});
}, []);

const {onKeyDown: handleControlKeyDown} = useActionHandlers(handleControlClick);
const value = React.useMemo(() => {
const selectedIds: string[] = [];

const switcherProps = React.useMemo<SwitcherProps>(
() => ({
onClick: handleControlClick,
onKeyDown: handleControlKeyDown,
}),
[handleControlClick, handleControlKeyDown],
);
items.forEach(({id, isSelected}) => {
if (isSelected) {
selectedIds.push(id);
}
});

return selectedIds;
}, [items]);

return (
<div className={b(null)}>
{/* FIXME remove switcher prop and this wrapper */}
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<div
className={b('control')}
ref={refControl}
{...(renderSwitcher ? {} : switcherProps)}
>
{renderSwitcher?.(switcherProps) || (
<Button>
<Icon data={Gear} />
{i18n('button_switcher')}
</Button>
)}
</div>
<Popup
anchorRef={refControl}
placement={popupPlacement || ['bottom-start', 'bottom-end', 'top-start', 'top-end']}
open={focused}
onClose={handleClosePopup}
className={b('popup')}
style={{width: popupWidth}}
>
{renderRequiredColumns()}
{renderConfigurableColumns()}
<div className={b('controls')}>
<Button view="action" width="max" onClick={handleApplyClick}>
{i18n('button_apply')}
</Button>
</div>
</Popup>
<DndTreeSelect
multiple
size="l"
open={open}
value={value}
onUpdate={onUpdate}
// popupWidth={popupWidth}
onOpenChange={onOpenChange}
placement={popupPlacement}
renderContainer={renderContainer}
renderControl={renderControl}
items={items}
setItems={setItems}
/>
</div>
);
};
Loading

0 comments on commit 4d995b1

Please sign in to comment.