Skip to content

Drag and drop

tsx
import React from 'react';
import Button, { ButtonLink } from '@semcore/button';
import Counter from '@semcore/counter';
import SettingsM from '@semcore/icon/Settings/m';
import DropdownMenu from '@semcore/dropdown-menu';
import { Text } from '@semcore/typography';
import { Flex } from '@semcore/flex-box';
import DnD from '@semcore/drag-and-drop';

const defeaultColumns = [
  { id: 'uniquePageviews', label: 'Unique Pageviews' },
  { id: 'uniqueVisitors', label: 'Unique Visitors' },
  { id: 'entranceSources', label: 'Entrance Sources' },
  { id: 'desktop', label: 'Desktop' },
  { id: 'mobile', label: 'Mobile' },
];
const defaultSelectedColumns = ['uniquePageviews', 'entranceSources'];

const Demo = () => {
  const [highlightedIndex, setHighlightedIndex] = React.useState<number | null>(null);
  const [columns, setColumns] = React.useState(defeaultColumns);
  const handleDnD = React.useCallback(
    ({ fromIndex, toIndex }: { fromIndex: number; toIndex: number }) => {
      setColumns((columns) => {
        const newColumns = [...columns];
        const shift = fromIndex < toIndex ? 1 : -1;
        for (let i = fromIndex; i !== toIndex; i += shift) {
          newColumns[i] = columns[i + shift];
        }
        newColumns[toIndex] = columns[fromIndex];
        return newColumns;
      });
      setHighlightedIndex(toIndex);
    },
    [],
  );
  const [selectedColumns, setSelectedColumns] = React.useState<string[]>(defaultSelectedColumns);

  const resetToDefault = React.useCallback(() => {
    setSelectedColumns(defaultSelectedColumns);
  }, []);
  const toggleAll = React.useCallback(() => {
    const allSelected = selectedColumns.length === columns.length;
    const allColumns = columns.map((column) => column.id);
    if (allSelected) {
      setSelectedColumns([]);
    } else {
      setSelectedColumns(allColumns);
    }
  }, [selectedColumns, columns]);

  return (
    <DropdownMenu
      selectable
      multiselect
      highlightedIndex={highlightedIndex}
      onHighlightedIndexChange={setHighlightedIndex}
    >
      <DropdownMenu.Trigger mt={2} mr='auto' id='dropdown-menu-basic' tag={Button}>
        <Button.Addon>
          <SettingsM />
        </Button.Addon>
        <Button.Text>Manage columns</Button.Text>
        <Button.Addon>
          <Counter>
            {selectedColumns.length}/{columns.length}
          </Counter>
        </Button.Addon>
      </DropdownMenu.Trigger>
      <DropdownMenu.Popper hMax={800} aria-labelledby={'popper_id'}>
        <Flex direction='column' alignItems='flex-start' p={2} gap={2}>
          <Text bold id={'popper_id'}>
            Show table columns
          </Text>
          <ButtonLink onClick={resetToDefault}>Reset to default</ButtonLink>
          <ButtonLink onClick={toggleAll}>
            {selectedColumns.length === columns.length ? 'Deselect' : 'Select'} all
          </ButtonLink>
        </Flex>
        <DropdownMenu.List hMax={800}>
          <DnD onDnD={handleDnD} aria-label={'drag-and-drop container'}>
            {columns.map((column, index) => (
              <DropdownMenu.Item
                tag={DnD.Draggable}
                isCustomFocus={true}
                key={column.id}
                selected={selectedColumns.includes(column.id)}
                onClick={(e) => {
                  if (
                    e.target instanceof HTMLElement &&
                    e.target.getAttribute('role') === 'menuitemcheckbox'
                  ) {
                    if (!selectedColumns.includes(column.id)) {
                      setSelectedColumns([...selectedColumns, column.id]);
                    } else {
                      setSelectedColumns(selectedColumns.filter((i) => i !== column.id));
                    }
                  }
                }}
              >
                {column.label}
              </DropdownMenu.Item>
            ))}
          </DnD>
        </DropdownMenu.List>
      </DropdownMenu.Popper>
    </DropdownMenu>
  );
};

export default Demo;

Cards with drag & drop

Place widget hereChange the order of the widgets!
Market traffic
0
Press "Tab" to enable graphical charts accessibility module.
Backlinks
0
Press "Tab" to enable graphical charts accessibility module.
Place widget hereChange the order of the widgets!
tsx
import React from 'react';
import DnD from '@semcore/drag-and-drop';
import Card from '@semcore/card';
import { Flex } from '@semcore/flex-box';
import { Chart } from '@semcore/d3-chart';
import MathPlusL from '@semcore/icon/MathPlus/l';
import { Text } from '@semcore/typography';

const stableRandom = (seed: number) => {
  let randomIndex = seed;
  return () => {
    if (randomIndex > 20) randomIndex = 1;
    return Math.abs(Math.sin(Math.PI * randomIndex * Math.cos(100 - randomIndex++)));
  };
};
const Widget: React.FC<{ title: string }> = ({ title }) => {
  const data = React.useMemo(() => {
    const random = stableRandom(title.length);
    const dateFormatter = new Intl.DateTimeFormat('en', { month: 'numeric', day: 'numeric' });
    return Array(3)
      .fill(0)
      .map((_, i) => ({
        date: dateFormatter.format(new Date(Date.now() - 1000 * 60 * 60 * 24 * 3 * i)),
        value: Math.round(random() * 10),
      }));
  }, [title]);

  return (
    <Card w={240} h={280}>
      <Card.Header>
        <Card.Title>{title}</Card.Title>
      </Card.Header>
      <Card.Body>
        <Chart.Bar
          duration={0}
          groupKey={'date'}
          data={data}
          plotWidth={200}
          plotHeight={200}
          aria-label={`${title} chart`}
        />
      </Card.Body>
    </Card>
  );
};

const widgetsSetup = [
  {
    title: 'Market traffic',
    id: 'market-traffic',
  },
  {
    title: 'Backlinks',
    id: 'backlinks',
  },
];
const defaultWidgets = [null, 'market-traffic', 'backlinks', null];

const Demo = () => {
  const [widgets, setWidgets] = React.useState(defaultWidgets);

  const handleDnD = React.useCallback(
    ({ fromIndex, toIndex }: { fromIndex: number; toIndex: number }) => {
      setWidgets((widgets) => {
        const newWidgets = [...widgets];
        const shift = fromIndex < toIndex ? 1 : -1;
        for (let i = fromIndex; i !== toIndex; i += shift) {
          newWidgets[i] = widgets[i + shift];
        }
        newWidgets[toIndex] = widgets[fromIndex];
        return newWidgets;
      });
    },
    [],
  );

  return (
    <DnD tag={Flex} flexWrap gap={4} onDnD={handleDnD} aria-label={'Draggable charts'}>
      {widgets.map((id, index) => {
        if (!id) {
          return (
            <DnD.DropZone key={index} aria-label={`Drop zone ${index + 1}`}>
              <Flex
                alignItems='center'
                gap={1}
                justifyContent='center'
                w={240}
                h={280}
                direction='column'
                p={5}
                style={{
                  border: '1px dashed var(--intergalactic-border-primary, #c4c7cf)',
                  borderRadius: '6px',
                }}
              >
                <Text color='text-secondary'>
                  <MathPlusL />
                </Text>
                <Text color='text-secondary' bold size={200}>
                  Place widget here
                </Text>
                <Text color='text-secondary' textAlign='center' size={200}>
                  Change the order of the widgets!
                </Text>
              </Flex>
            </DnD.DropZone>
          );
        }

        const widget = widgetsSetup.find((widget) => widget.id === id)!;

        return (
          <DnD.Draggable placement='top' key={id} aria-label={`${widget.title} widget`} h='100%'>
            <Widget title={widget.title} />
          </DnD.Draggable>
        );
      })}
    </DnD>
  );
};

export default Demo;

Last updated:

Released under the MIT License.

Released under the MIT License.