Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/11-date-formatting.mdx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Meta, Story } from '../.storybook/components';
import * as Stories from './date-formatting.stories';

<Meta title="Date formatter" />
<Meta title="Utilities/DateFormatter" />

# Date formatter
# DateFormatter

The `DateFormatter` is a unified system for formatting dates and times.
It ensures consistent output across the application based on shared product rules.
Expand Down
6 changes: 3 additions & 3 deletions docs/date-formatting.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
import type { StoryObj } from '@storybook/react';

const meta = {
title: 'Date formatter',
title: 'Utilities/DateFormatter',
parameters: {
layout: 'padded',
},
Expand Down Expand Up @@ -41,7 +41,7 @@ function FormatsTable({

return (
<TableContainer>
<Table aria-label="Date formatter">
<Table aria-label="Date formatter" divider="row" fullWidth>
<Table.Header>
<Table.Column>Name</Table.Column>
<Table.Column>Value</Table.Column>
Expand Down Expand Up @@ -71,7 +71,7 @@ function DurationShortestTable({

return (
<TableContainer>
<Table aria-label="Date formatter">
<Table aria-label="Date formatter" divider="row" fullWidth>
<Table.Header>
<Table.Column>Name</Table.Column>
<Table.Column>Seconds</Table.Column>
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ export * from './useScrollPosition';
export * from './useCopyToClipboard';
export * from './useTimer';
export * from './useKeyedRefs';
export * from './useFileSizeFormatter';
1 change: 1 addition & 0 deletions packages/core/src/hooks/useFileSizeFormatter/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useFileSizeFormatter';
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { ReactNode } from 'react';

import { I18nProvider } from '@react-aria/i18n';
import { renderHook } from '@testing-library/react';
import { describe, expect, it } from 'vitest';

import type { FileSizeFormatterConfig } from '../../utils/FileSizeFormatter';

import { useFileSizeFormatter } from './useFileSizeFormatter';

const withLocale = (locale: string) =>
function Wrapper({ children }: { children: ReactNode }) {
return <I18nProvider locale={locale}>{children}</I18nProvider>;
};

describe('useFileSizeFormatter', () => {
it('formats using the current locale', () => {
const { result } = renderHook(() => useFileSizeFormatter(), {
wrapper: withLocale('ru-RU'),
});

expect(result.current.format(1500)).toBe('1,5 КБ');
});

it('applies the provided config', () => {
const { result } = renderHook(
() => useFileSizeFormatter({ defaultUnitSystem: 'IEC' }),
{ wrapper: withLocale('en-US') }
);

expect(result.current.format(1024)).toBe('1 KiB');
});

it('reuses the formatter across renders with an equal config', () => {
const config: FileSizeFormatterConfig = { defaultPrecision: 1 };

const { result, rerender } = renderHook(
() => useFileSizeFormatter(config),
{ wrapper: withLocale('en-US') }
);

const first = result.current;

rerender();

expect(result.current).toBe(first);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useMemo } from 'react';

import { useLocale } from '@react-aria/i18n';

import {
FileSizeFormatter,
type FileSizeFormatterConfig,
} from '../../utils/FileSizeFormatter';

/**
* Returns a memoized {@link FileSizeFormatter} bound to the current locale.
* The formatter is recreated only when the locale or `config` changes.
* @example
* const formatter = useFileSizeFormatter();
* formatter.format(1500); // "1.5 KB"
*/
export function useFileSizeFormatter(
config?: FileSizeFormatterConfig
): FileSizeFormatter {
const { locale } = useLocale();

// `config` is a plain data object, so a stable serialization keeps the
// formatter memoized across renders that pass an equal inline literal.
return useMemo(
() => new FileSizeFormatter(locale, config),
[locale, JSON.stringify(config)]
);
}
92 changes: 92 additions & 0 deletions packages/core/src/utils/FileSizeFormatter/FileSizeFormatter.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { Meta, Story, Status } from '../../../../../.storybook/components';

import * as Stories from './FileSizeFormatter.stories';

<Meta of={Stories} />

# FileSizeFormatter

<Status variant="experimental" />

Formats byte values using SI and IEC measurement systems.

## Import

```tsx
import { FileSizeFormatter } from '@koobiq/react-core';
```

## Usage

Create one formatter for a locale and reuse it.

```tsx
const formatter = new FileSizeFormatter('en-US');

formatter.format(1500);
// 1.5 KB
```

SI is used by default: `1 KB = 1000 bytes`. Use IEC for binary units, where
`1 KiB = 1024 bytes`.

```tsx
const formatter = new FileSizeFormatter('en-US', {
defaultUnitSystem: 'IEC',
});

formatter.format(1536);
// 1.5 KiB
```

<Story of={Stories.Base} />

## Hook

Use `useFileSizeFormatter` inside components. It reads the current locale from
context and memoizes the formatter, so it's only recreated when the locale or
`config` changes.

```tsx
import { useFileSizeFormatter } from '@koobiq/react-core';

function FileSize({ bytes }: { bytes: number }) {
const formatter = useFileSizeFormatter();

return <span>{formatter.format(bytes)}</span>;
}
```

Pass a `config` to switch measurement systems, change precision, or override
unit labels:

```tsx
const formatter = useFileSizeFormatter({
defaultUnitSystem: 'IEC',
defaultPrecision: 1,
});
```

## Precision

The default maximum precision is two fraction digits. Override it for the
formatter or an individual value.

```tsx
const formatter = new FileSizeFormatter('en-US', {
defaultPrecision: 1,
});

formatter.format(1550);
// 1.6 KB

formatter.format(1550, { precision: 2 });
// 1.55 KB
```

## Localization

The locale controls number formatting and unit abbreviations. A non-breaking
space is always used between the value and unit.

<Story of={Stories.Localization} />
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { Table, TableContainer } from '@koobiq/react-components';
import type { Meta, StoryObj } from '@storybook/react';

import { FileSizeFormatter } from './FileSizeFormatter';

const meta = {
title: 'Utilities/FileSizeFormatter',
parameters: { layout: 'padded' },
} satisfies Meta;

export default meta;

type Story = StoryObj;

const values = [512, 1000, 1024, 1_000_000, 1024 ** 2];

type FormatRow = { id: number } & Record<string, string | number>;

const FormatsTable = ({
formatters,
}: {
formatters: Array<{ name: string; formatter: FileSizeFormatter }>;
}) => {
const columns = [
{ id: 'bytes', name: 'Bytes' },
...formatters.map(({ name }, index) => ({
id: `format-${index}`,
name,
})),
];

const items: FormatRow[] = values.map((value) => ({
id: value,
bytes: value,
...Object.fromEntries(
formatters.map(({ formatter }, index) => [
`format-${index}`,
formatter.format(value),
])
),
}));

return (
<TableContainer>
<Table aria-label="File size formatter" divider="row" fullWidth>
<Table.Header columns={columns}>
{(column) => <Table.Column>{column.name}</Table.Column>}
</Table.Header>
<Table.Body items={items}>
{(item) => (
<Table.Row>
{(columnKey) => (
<Table.Cell>{item[String(columnKey)]}</Table.Cell>
)}
</Table.Row>
)}
</Table.Body>
</Table>
</TableContainer>
);
};

export const Base: Story = {
render: () => (
<FormatsTable
formatters={[
{ name: 'SI', formatter: new FileSizeFormatter('en-US') },
{
name: 'IEC',
formatter: new FileSizeFormatter('en-US', {
defaultUnitSystem: 'IEC',
}),
},
]}
/>
),
};

export const Localization: Story = {
render: () => (
<FormatsTable
formatters={[
{ name: 'en-US', formatter: new FileSizeFormatter('en-US') },
{ name: 'ru-RU', formatter: new FileSizeFormatter('ru-RU') },
]}
/>
),
};
Loading
Loading