yarn @mantine/core 7.17.0
7.17.0 🌶️

latest release: 8.0.0-alpha.0
3 days ago

View changelog with demos on mantine.dev website

Last 7.x minor release

This is the last minor release in the 7.x series. The next release will be 8.0 with breaking changes and new features.

You are welcome to review the changelog/code and provide feedback and bug reports in Discord or GitHub discussions:

How to update your project dependencies to the new alpha version:

  • Open your package.json
  • Find all @mantine/ packages
  • Update version of all @mantine/ packages to 8.0.0-alpha.0
  • Install dependencies with your package manager, for example, yarn or npm install

Important notes:

  • 8.0 release is scheduled for May 5th. Until then you can influence the included breaking changes by proving feedback using Discord or GitHub discussions.
  • Currently most of planned breaking changes are implemented – it is not planned to introduce other significant breaking changes (unless new versions of recharts or tiptap are released before May 5th).
  • You can find the updated source code in 8.0 branch on GitHub
  • Since the changes to codebase are not that significant compared to previous major releases, it is not planned to deprecate 7.x version as soon as 8.0 is release. 7.17.x patches are planned to be released for some time – if you are not ready to migrate to 8.x, you will still be able to receive bug fixes for 7.x (no new features though).

Portal reuseTargetNode prop

Portal component now supports reuseTargetNode prop which allows to reuse the same target node for all instances.
This option is more performant than the previous behavior, it is recommended to be enabled.
This option will be enabled by default in the 8.0 major release.

To enable reuseTargetNode option in all components that depend on Portal, add the following code
to your theme:

import { createTheme, Portal } from '@mantine/core';

export const theme = createTheme({
  components: {
    Portal: Portal.extend({
      defaultProps: {
        reuseTargetNode: true,
      },
    }),
  },
});

Example usage. In the following example, all three paragraphs will be rendered in the same target node:

import { Portal } from '@mantine/core';

function Demo() {
  return (
    <>
      <Portal reuseTargetNode>
        <p>First</p>
      </Portal>

      <Portal reuseTargetNode>
        <p>Second</p>
      </Portal>

      <Portal reuseTargetNode>
        <p>Third</p>
      </Portal>
    </>
  );
}

use-form formRootRule

formRootRule is a special rule path that can be used to validate objects and arrays
alongside with their nested fields. For example, it is useful when you want to capture
a list of values, validate each value individually and then validate the list itself
to not be empty:

import { IconTrash } from '@tabler/icons-react';
import { ActionIcon, Button, Group, Switch, Text, TextInput } from '@mantine/core';
import { formRootRule, isNotEmpty, useForm } from '@mantine/form';
import { randomId } from '@mantine/hooks';

function Demo() {
  const form = useForm({
    mode: 'uncontrolled',
    initialValues: {
      employees: [{ name: '', active: false, key: randomId() }],
    },
    validate: {
      employees: {
        [formRootRule]: isNotEmpty('At least one employee is required'),
        name: isNotEmpty('Name is required'),
      },
    },
  });

  const fields = form.getValues().employees.map((item, index) => (
    <Group key={item.key} mt="xs">
      <TextInput
        placeholder="John Doe"
        withAsterisk
        style={{ flex: 1 }}
        key={form.key(`employees.${index}.name`)}
        {...form.getInputProps(`employees.${index}.name`)}
      />
      <Switch
        label="Active"
        key={form.key(`employees.${index}.active`)}
        {...form.getInputProps(`employees.${index}.active`, { type: 'checkbox' })}
      />
      <ActionIcon color="red" onClick={() => form.removeListItem('employees', index)}>
        <IconTrash size={16} />
      </ActionIcon>
    </Group>
  ));

  return (
    <form onSubmit={form.onSubmit(() => {})}>
      {fields.length > 0 ? (
        <Group mb="xs">
          <Text fw={500} size="sm" style={{ flex: 1 }}>
            Name
          </Text>
          <Text fw={500} size="sm" pr={90}>
            Status
          </Text>
        </Group>
      ) : (
        <Text c="dimmed" ta="center">
          No one here...
        </Text>
      )}

      {fields}

      {form.errors.employees && (
        <Text c="red" size="sm" mt="sm">
          {form.errors.employees}
        </Text>
      )}

      <Group justify="space-between" mt="md">
        <Button
          variant="default"
          onClick={() => {
            form.insertListItem('employees', { name: '', active: false, key: randomId() });
            form.clearFieldError('employees');
          }}
        >
          Add employee
        </Button>
        <Button type="submit">Submit</Button>
      </Group>
    </form>
  );
}

Another example is to validate an object fields combination:

import { Button, Text, TextInput } from '@mantine/core';
import { formRootRule, isNotEmpty, useForm } from '@mantine/form';

function Demo() {
  const form = useForm({
    mode: 'uncontrolled',
    initialValues: {
      user: {
        firstName: '',
        lastName: '',
      },
    },

    validate: {
      user: {
        [formRootRule]: (value) =>
          value.firstName.trim().length > 0 && value.firstName === value.lastName
            ? 'First name and last name cannot be the same'
            : null,
        firstName: isNotEmpty('First name is required'),
        lastName: isNotEmpty('Last name is required'),
      },
    },
  });

  return (
    <form onSubmit={form.onSubmit(() => {})}>
      <TextInput
        label="First name"
        placeholder="First name"
        {...form.getInputProps('user.firstName')}
      />
      <TextInput
        label="Last name"
        placeholder="Last name"
        mt="md"
        {...form.getInputProps('user.lastName')}
      />
      {form.errors.user && (
        <Text c="red" mt={5} fz="sm">
          {form.errors.user}
        </Text>
      )}
      <Button type="submit" mt="lg">
        Submit
      </Button>
    </form>
  );
}

isJSONString and isNotEmptyHTML form validators

New isJSONString and isNotEmptyHTML form validators:

  • isNotEmptyHTML checks that form value is not an empty HTML string. Empty string, string with only HTML tags and whitespace are considered to be empty.
  • isJSONString checks that form value is a valid JSON string.
import { isJSONString, useForm } from '@mantine/form';

const form = useForm({
  mode: 'uncontrolled',
  initialValues: {
    json: '',
    html: '',
  },

  validate: {
    json: isJSONString('Invalid JSON string'),
    html: isNotEmptyHTML('HTML cannot be empty'),
  },
});

Popover onDismiss

Popover now supports onDismiss prop, which makes it easier
to subscribe to outside clicks and escape key presses to close popover:

import { useState } from 'react';
import { Button, Popover } from '@mantine/core';

function Demo() {
  const [opened, setOpened] = useState(false);
  return (
    <Popover opened={opened} onDismiss={() => setOpened(false)}>
      <Popover.Target>
        <Button onClick={() => setOpened((o) => !o)}>Toggle popover</Button>
      </Popover.Target>

      <Popover.Dropdown>Dropdown</Popover.Dropdown>
    </Popover>
  );
}

MantineProvider env

MantineProvider component now supports env prop.
It can be used in test environment to disable some features that
might impact tests and/or make it harder to test components:

  • transitions that mount/unmount child component with delay
  • portals that render child component in a different part of the DOM

To enable test environment, set env to test:

import { MantineProvider } from '@mantine/core';

function Demo() {
  return <MantineProvider env="test">{/* Your app here */}</MantineProvider>;
}

use-file-dialog hook

New use-file-dialog allows capturing one or more files from the user
without file input element:

import { Button, Group, List } from '@mantine/core';
import { useFileDialog } from '@mantine/hooks';

function Demo() {
  const fileDialog = useFileDialog();

  const pickedFiles = Array.from(fileDialog.files || []).map((file) => (
    <List.Item key={file.name}>{file.name}</List.Item>
  ));

  return (
    <div>
      <Group>
        <Button onClick={fileDialog.open}>Pick files</Button>
        {pickedFiles.length > 0 && (
          <Button variant="default" onClick={fileDialog.reset}>
            Reset
          </Button>
        )}
      </Group>
      {pickedFiles.length > 0 && <List mt="lg">{pickedFiles}</List>}
    </div>
  );
}

Remix deprecation

Remix is deprecated, the documentation related to Remix integration
was removed, use React Router instead. To simplify maintenance,
Remix/React Router templates were archived and will not be updated.

Help center updates

Other changes

  • Tooltip now supports customizing middlewares
  • ScrollArea now supports overscrollBehavior prop
  • Affix now supports theme.spacing values for position prop
  • Anchor now supports underline="not-hover" option to display underline only when the link is not hovered

Don't miss a new core release

NewReleases is sending notifications on new releases.