Skip to content

InputTags

Entering and editing tags

Here's an example where tags have a limited width and can be edited by clicking on them.

tsx
import { Flex } from '@semcore/ui/base-components';
import Ellipsis from '@semcore/ui/ellipsis';
import InputTags from '@semcore/ui/input-tags';
import { Text } from '@semcore/ui/typography';
import React from 'react';

const Demo = () => {
  const inputValueRef = React.useRef<HTMLInputElement>(null);
  const [tags, setTags] = React.useState([
    'TikTok',
    'Facebook',
    'LinkedIn',
    'Instagram',
    'Social media with a very long name',
  ]);
  const [value, setValue] = React.useState('');

  const handleAppendTags = (newTags: string[]) => {
    setTags((tags) => [...tags, ...newTags]);
    setValue('');
  };

  const handleRemoveTag = () => {
    if (tags.length === 0) return;
    setTags(tags.slice(0, -1));
    setValue(`${tags.slice(-1)[0]} ${value}`);
  };

  const handleCloseTag = (idx: number) => (e: React.SyntheticEvent) => {
    e.stopPropagation();

    setTags((tags) => tags.filter((_, tagIdx) => idx !== tagIdx));
  };

  const handleTagKeyDown = (e: React.KeyboardEvent<HTMLElement>) => {
    if (e.code === 'Enter' || e.code === 'Space') {
      handleEditTag(e);
    }
    return false;
  };

  const handleEditTag = (
    e: React.SyntheticEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>,
  ) => {
    const { dataset } = e.currentTarget;
    let allTags = [...tags];
    if (value) {
      allTags = [...allTags, value];
    }
    setTags(allTags.filter((tag, ind) => ind !== Number(dataset.id)));
    if (!e.defaultPrevented && dataset.id !== undefined) {
      setValue(tags[Number(dataset.id)]);
      inputValueRef.current?.focus();
    }
    return false;
  };

  const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
    const value = e.target instanceof HTMLInputElement ? e.target.value : null;
    if (e.key === 'Enter' && value) {
      handleAppendTags([value]);

      return false;
    }
  };

  return (
    <Flex direction='column'>
      <Text tag='label' size={300} htmlFor='add-new-social-media'>
        Social media
      </Text>
      <InputTags mt={2} size='l' onAppend={handleAppendTags} onRemove={handleRemoveTag}>
        {tags.map((tag, idx) => (
          <InputTags.Tag
            key={tag}
            tag={InputTags.Tag}
            theme='primary'
            data-id={idx}
            onClick={handleEditTag}
            onKeyDown={handleTagKeyDown}
            active={false}
            editable
          >
            <InputTags.Tag.Text>
              <Ellipsis wMax={100}>{tag}</Ellipsis>
            </InputTags.Tag.Text>
            <InputTags.Tag.Close onClick={handleCloseTag(idx)} />
          </InputTags.Tag>
        ))}
        <InputTags.Value
          value={value}
          onChange={setValue}
          onKeyDown={handleInputKeyDown}
          ref={inputValueRef}
          id='add-new-social-media'
          placeholder='Add social media'
        />
      </InputTags>
    </Flex>
  );
};

export default Demo;

Wrapping email in tag

In this example, emails are wrapped in tags without any width limitation.

tsx
import { Flex } from '@semcore/ui/base-components';
import InputTags from '@semcore/ui/input-tags';
import type { InputTagsTagProps } from '@semcore/ui/input-tags';
import { Text } from '@semcore/ui/typography';
import React from 'react';

const isValidEmail = (value: string) => /.+@.+\..+/i.test(value.toLowerCase());

const defaultTags = ['bob@email.com', 'alice@domain.net', 'mary@website.com', 'steve@company.com'];
type ExampleInputTagsProps = InputTagsTagProps;

const Demo = (props: ExampleInputTagsProps) => {
  const [tags, setTags] = React.useState(defaultTags);
  const [value, setValue] = React.useState('');

  const changeState = (tags?: string[], value?: string) => {
    if (tags !== undefined) {
      setTags(tags);
    }
    if (value !== undefined) {
      setValue(() => value);
    }
  };

  const handleAppendTags = (newTags: string[]) => {
    setTags((tags) => [...tags, ...newTags]);
    setValue(() => '');
  };

  const handleRemoveTag = () => {
    changeState(tags.slice(0, -1), tags.slice(-1)[0]);
  };

  const handleChange = (value: string) => {
    changeState(undefined, value);
  };

  const handleCloseTag = (e: React.SyntheticEvent<HTMLElement>) => {
    const { dataset } = e.currentTarget;
    changeState(
      tags.filter((tag, ind) => ind !== Number(dataset.id)),
      undefined,
    );
  };

  return (
    <Flex direction='column'>
      <Text tag='label' size={300} htmlFor='email'>
        Participants
      </Text>
      <InputTags mt={2} onAppend={handleAppendTags} onRemove={handleRemoveTag}>
        {tags.map((tag, idx) => (
          <InputTags.Tag
            key={idx}
            size={props.size}
            theme={props.theme}
            disabled={props.disabled}
            editable={props.editable}
            color={isValidEmail(tag) ? 'green-500' : 'red-500'}
          >
            <InputTags.Tag.Text>{tag}</InputTags.Tag.Text>
            <InputTags.Tag.Close data-id={idx} onClick={handleCloseTag} />
          </InputTags.Tag>
        ))}
        <InputTags.Value
          id='email'
          placeholder='Add email'
          type='email'
          autoComplete='email'
          value={value}
          onChange={handleChange}
        />
      </InputTags>
    </Flex>
  );
};

export const defaultPropsEmail: ExampleInputTagsProps = {
  size: 'l',
  theme: 'primary',
  disabled: false,
  editable: undefined,
};

Demo.defaultProps = defaultPropsEmail;

export default Demo;

Select for tag filtering

In this example, selected options are wrapped in tags within the input field.

tsx
import { Flex, ScreenReaderOnly } from '@semcore/ui/base-components';
import InputTags from '@semcore/ui/input-tags';
import Select from '@semcore/ui/select';
import { Text } from '@semcore/ui/typography';
import React from 'react';

const tagsSelect = ['LinkedIn', 'Facebook', 'TikTok', 'Instagram'];

const Demo = () => {
  const selectTriggerRef = React.useRef<HTMLInputElement>(null);
  const [tags, setTags] = React.useState<string[]>([]);
  const [valueInput, setValueInput] = React.useState('');
  const [visible, setVisible] = React.useState(false);

  function onRemoveLastTag() {
    if (tags.length) {
      setValueInput(tags[tags.length - 1]);
      setTags(tags.slice(0, -1));
    }
  }

  function onRemoveTag(index: number, e: React.SyntheticEvent<HTMLElement>) {
    e.stopPropagation();
    const newTags = tags.filter((tag, i) => i !== index);
    setTags(tags.filter((tag, i) => i !== index));
    if (newTags.length === index) {
      selectTriggerRef.current?.focus();
    }
  }

  function onChangeValue(value: string) {
    setValueInput(value);
    setVisible(true);
  }

  function onChange(value: string[]) {
    setTags(value);
    setValueInput('');
  }

  function onBlurValue() {
    setValueInput('');
  }

  const tagsFilter = tagsSelect.filter((tag) => {
    return tag.toLowerCase().includes(valueInput.toLowerCase()) && !tags.includes(tag);
  });

  return (
    <Flex direction='column'>
      <Text tag='label' size={300} htmlFor='secondary-social-medias'>
        Social media
      </Text>
      <Select
        interaction='focus'
        size='l'
        visible={visible && tags.length < 4}
        onVisibleChange={(visible) => setVisible(visible)}
        multiselect={true}
        value={tags}
        onChange={onChange}
      >
        <Select.Trigger
          tag={InputTags}
          mt={2}
          w={300}
          size='l'
          onRemove={onRemoveLastTag}
          delimiters={[]}
        >
          {tags.map((tag, i) => (
            <InputTags.Tag key={i} theme='primary'>
              <InputTags.Tag.Text>{tag}</InputTags.Tag.Text>
              <InputTags.Tag.Close onClick={(e) => onRemoveTag(i, e)} />
            </InputTags.Tag>
          ))}
          <InputTags.Value
            ref={selectTriggerRef}
            value={valueInput}
            onChange={onChangeValue}
            id='secondary-social-medias'
            placeholder='Select social media'
            onBlur={onBlurValue}
            aria-describedby={valueInput ? 'search-result' : undefined}
          />
        </Select.Trigger>
        <Select.Menu>
          {tagsFilter.map((tag, i) => (
            <Select.Option value={tag} key={i}>
              {tag}
            </Select.Option>
          ))}
          {tagsFilter.length !== 0 && valueInput !== '' && (
            <ScreenReaderOnly id='search-result' aria-hidden='true'>
              {tagsFilter.length}
              {' '}
              result
              {tagsFilter.length > 1 && 's'}
              {' '}
              found
            </ScreenReaderOnly>
          )}
          {tagsFilter.length === 0 && valueInput !== '' && (
            <Text
              tag='div'
              id='search-result'
              key='Nothing'
              p='6px 8px'
              size={200}
              use='secondary'
            >
              Nothing found
            </Text>
          )}
        </Select.Menu>
      </Select>
    </Flex>
  );
};

export default Demo;

Released under the MIT License.

Released under the MIT License.