Navigation Menu

Skip to content

Commit

Permalink
feat: add useNumberFormat hook
Browse files Browse the repository at this point in the history
  • Loading branch information
tujoworker committed Mar 7, 2022
1 parent 904a698 commit b218d27
Show file tree
Hide file tree
Showing 4 changed files with 173 additions and 49 deletions.
Expand Up @@ -33,63 +33,81 @@ It uses the browser APIs `number.toLocaleString` or `Intl.NumberFormat.format` u
- Locale: `nb-NO`
- Currency: `NOK`

### Element and style

The number component is style-independent, so it has no visual styles. By default, a `<span>` is used (with [speak-as: numbers](https://developer.mozilla.org/en-US/docs/Web/CSS/@counter-style/speak-as), even the support is very low). But you can easily change the element type by providing something else to `element="div"` property.

### Sources

Eufemia is basing their number formats on both the [Norwegian authority](https://lovdata.no/forskrift/2004-02-16-426/§16) and [Språkradet](https://www.sprakradet.no/sprakhjelp/Skriveregler/Dato). Wikipedia has more info on world wide [decimal separator](https://en.wikipedia.org/wiki/Decimal_separator) usage.

For international number formatting, we use the [EU Style Guide](https://ec.europa.eu/info/sites/default/files/styleguide_english_dgt_en.pdf).

## Decimals

When the amount of wanted `decimals` is set as a property, but the given value contains decimals that exceeds the wanted `decimals`, the output value will get rounded up or down. Use `omit_rounding` if you need to hard-cut decimals from the displayed value.

## Details

> Screen readers require numbers to be formatted properly in order to be read as numbers. The **NumberFormat** component will help to achieve this requirement.
## Provider

So, numbers are formatted differently for screen readers than the visual number. And numbers also get assigned a `lang` attribute, so the screen reader knows what language (locale) should be used on the particular number, even if the text around does not correspond to the same language.
You can send down the `locale` as an application-wide property (Context). More info about the [provider and locale usage](/uilib/components/number-format/provider).

## Accessibility
```jsx
import Provider from '@dnb/eufemia/shared/Provider'

To enhance the **Copy & Paste** experience of copying numbers into other applications, the NumberFormat component automatically changes the number formatting to be without thousand separators and to have a dot, instead of a comma for the decimal separator.
render(
<Provider locale="en-GB" NumberFormat={{ currency_display: 'code' }}>
<MyApp>
text <NumberFormat>123</NumberFormat> table etc.
</MyApp>
</Provider>
)
```

**NVDA** has also [issues](https://github.com/nvaccess/nvda/issues/8874) on reconciling the `lang` attribute, this makes it hard to have a solid and good solution for reading numbers. VoiceOver on desktop makes a perfect job there.
## NumberFormat Hook

**VoiceOver** on mobile devices (iOS) only supports numbers read out properly to a maximum of `99,999.00`. On amounts above this value, VO reads numbers digit by digit.
**Heads up:** If you do so, keep in mind, you will have to ensure all the accessibility enhancements the component offers. For that you can use the `aria` field:

### Lighthouse and axe-core
```jsx
import Provider from '@dnb/eufemia/shared/Provider'
import useNumberFormat from '@dnb/eufemia/components/number-format/useNumberFormat'

function Component() {
// By using returnAria you get an object
const { number, aria } = useNumberFormat(12345678.9, {
// Props are inherited from the Eufemia Provider and the NumberFormat object
returnAria: true,
})

return (
<span>
<span aria-hidden>{number}</span>
<span className="dnb-sr-only">{aria}</span>
</span>
)
}

In order to enhance the UX while using a screen reader, the NumberFormat component is using a role called `role="text"`. This allows the screen reader to read particular numbers within the context, without interrupting the flow while reading paragraphs.
render(
<Provider locale="en-GB" NumberFormat={{ currency: 'EUR' }}>
<Component />
</Provider>
)
```

[Lighthouse](https://developers.google.com/web/tools/lighthouse) is using [axe-core](https://developers.google.com/web/tools/lighthouse) under the hood. Right now axe-core is allowing us to use `role="text"` only within no focusable descendants. But because the NumberFormat component also includes a **Copy & Paste** feature, it uses `tabindex="-1"` which allows JavaScript to focus the descendant DOM Element. In order to enhance the `role="text"` axe rule, you may follow along and support [this issue](https://github.com/dequelabs/axe-core/issues/2934).
## Formatting only (interceptor)

## Formatting only
You can use the `format` method without using a React Component or React Hook.

You can use the formatting without using the Component. Have a look at the [available properties](/uilib/components/number-format/properties).
**Heads up:** If you do so, keep in mind, you will have to ensure all the accessibility enhancements the component offers. For that you can use the `aria` field:

```js
import { format } from '@dnb/eufemia/components/number-format/NumberUtils'

const value = 12345678.9

// basic formatting
const number = format(value)

// by using returnAria you get an object
const { number, aria } = format(value, {
locale: 'nb-NO', // also inherited from the Provider
// By using returnAria you get an object
const { number, aria } = format(12345678.9, {
locale: 'nb-NO', // not inherited
currency: true,
returnAria: true,
})

// Basic formatting
const number = format(1234)
```

## Using number helpers, like cleanNumber
The `format` method will accept the same [properties](/uilib/components/number-format/properties) as the component.

### Interceptor helpers

You can use the clean helpers without using the Component. Have a look at the [available properties](/uilib/components/number-format/properties). Also, you may check out the related tests **NumberFormat > cleanNumber** in the source code to find more examples.
Also, you may check out the related tests **NumberFormat > cleanNumber** in the source code to find more examples.

```js
import { cleanNumber } from '@dnb/eufemia/components/number-format/NumberUtils'
Expand All @@ -98,6 +116,30 @@ const string = cleanNumber('prefix -12 345,678 suffix') // returns -12345.678
const string = cleanNumber('prefix -12.345,678 suffix') // returns -12345.678
```

### Element and style

The number component is style-independent, so it has no visual styles. By default, a `<span>` is used (with [speak-as: numbers](https://developer.mozilla.org/en-US/docs/Web/CSS/@counter-style/speak-as), even the support is very low). But you can easily change the element type by providing something else to `element="div"` property.

## Accessibility

To enhance the **Copy & Paste** experience of copying numbers into other applications, the NumberFormat component automatically changes the number formatting to be without thousand separators and to have a dot, instead of a comma for the decimal separator.

**NVDA** has also [issues](https://github.com/nvaccess/nvda/issues/8874) on reconciling the `lang` attribute, this makes it hard to have a solid and good solution for reading numbers. VoiceOver on desktop makes a perfect job there.

**VoiceOver** on mobile devices (iOS) only supports numbers read out properly to a maximum of `99,999.00`. On amounts above this value, VO reads numbers digit by digit.

### More details

> Screen readers require numbers to be formatted properly in order to be read as numbers. The **NumberFormat** component will help to achieve this requirement.
So, numbers are formatted differently for screen readers than the visual number. And numbers also get assigned a `lang` attribute, so the screen reader knows what language (locale) should be used on the particular number, even if the text around does not correspond to the same language.

### Sources

Eufemia is basing their number formats on both the [Norwegian authority](https://lovdata.no/forskrift/2004-02-16-426/§16) and [Språkradet](https://www.sprakradet.no/sprakhjelp/Skriveregler/Dato). Wikipedia has more info on world wide [decimal separator](https://en.wikipedia.org/wiki/Decimal_separator) usage.

For international number formatting, we use the [EU Style Guide](https://ec.europa.eu/info/sites/default/files/styleguide_english_dgt_en.pdf).

## Node.js and SSR usage

If you run the component or `format` function in [Node.js](https://nodejs.org) you have to include [ICU](https://nodejs.org/api/intl.html) data in order to display other locales than en-GB. You can do this by:
Expand All @@ -106,22 +148,6 @@ If you run the component or `format` function in [Node.js](https://nodejs.org) y
- and call node (or jest) with an environment variable pointing to the package: `NODE_ICU_DATA=./node_modules/full-icu node ...`
- after a Node.js version upgrade you may have to run `npm rebuild`

## Provider

You can send down the `locale` as an application-wide property (Context). More info about the [provider and locale usage](/uilib/components/number-format/provider).

```jsx
import Provider from '@dnb/eufemia/shared/Provider'

render(
<Provider locale="en-GB" NumberFormat={{ currency_display: 'code' }}>
<MyApp>
text <NumberFormat>123</NumberFormat> table etc.
</MyApp>
</Provider>
)
```

## Known issues

Edge Browser on Windows 10 is converting numbers automatically to followable links. This makes the experience on NVDA bad, as it reads also the new, unformatted link number.
Expand All @@ -133,3 +159,9 @@ You can [disable this behavior](https://developer.mozilla.org/en-US/docs/Web/HTM
...
</html>
```

### Lighthouse and axe-core

In order to enhance the UX while using a screen reader, the NumberFormat component is using a role called `role="text"`. This allows the screen reader to read particular numbers within the context, without interrupting the flow while reading paragraphs.

[Lighthouse](https://developers.google.com/web/tools/lighthouse) is using [axe-core](https://developers.google.com/web/tools/lighthouse) under the hood. Right now axe-core is allowing us to use `role="text"` only within no focusable descendants. But because the NumberFormat component also includes a **Copy & Paste** feature, it uses `tabindex="-1"` which allows JavaScript to focus the descendant DOM Element. In order to enhance the `role="text"` axe rule, you may follow along and support [this issue](https://github.com/dequelabs/axe-core/issues/2934).
@@ -0,0 +1,69 @@
/**
* Hook Test
*
*/

import React from 'react'
import { renderHook } from '@testing-library/react-hooks'
import useNumberFormat from '../useNumberFormat'
import Provider from '../../../shared/Provider'

describe('useNumberFormat', () => {
it('will render without provider', () => {
const { result } = renderHook(() =>
useNumberFormat(1234, { currency: true })
)

expect(result.current).toBe('1 234,00 kr')
})

it('will return object when returnAria is true', () => {
const { result } = renderHook(() =>
useNumberFormat(1234, { currency: true, returnAria: true })
)

expect(result.current).toEqual(
expect.objectContaining({
aria: '1 234,00 norske kroner',
cleanedValue: '1234,00 kr',
locale: 'nb-NO',
number: '1 234,00 kr',
type: 'currency',
value: 1234,
})
)
})

it('will inherit NumberFormat props from provider', () => {
const wrapper = ({ children }) => (
<Provider
locale="nb-NO" // should get overwritte by the NumberFormat props
NumberFormat={{
currency: true,
locale: 'en-GB',
}}
>
{children}
</Provider>
)
const { result } = renderHook(() => useNumberFormat(1234), { wrapper })

expect(result.current).toBe('NOK 1 234.00')
})

it('will inherit locale from provider', () => {
const wrapper = ({ children }) => (
<Provider
locale="en-GB"
NumberFormat={{
currency: true,
}}
>
{children}
</Provider>
)
const { result } = renderHook(() => useNumberFormat(1234), { wrapper })

expect(result.current).toBe('NOK 1 234.00')
})
})
@@ -0,0 +1,21 @@
import { useContext } from 'react'
import { format } from './NumberUtils'
import Context from '../../shared/Context'
import usePropsWithContext from '../../shared/hooks/usePropsWithContext'
import type { formatOptionParams, formatValue } from './NumberUtils'

function useNumberFormat(
value: formatValue,
options: formatOptionParams = {}
) {
const context = useContext(Context)
const params = usePropsWithContext(
options,
{ locale: context.locale },
context.NumberFormat
)

return format(value, params)
}

export default useNumberFormat
2 changes: 2 additions & 0 deletions packages/dnb-eufemia/src/shared/Context.js
Expand Up @@ -45,6 +45,7 @@ export const prepareContext = (props = {}) => {
}
return context.translation
},
locale: null,
locales,
// All eufemia components because of Typescript:
Button: {},
Expand All @@ -60,6 +61,7 @@ export const prepareContext = (props = {}) => {
VisuallyHidden: {},
Drawer: {},
Dialog: {},
NumberFormat: {},

...props,
translation, // make sure we set this after props, since we update this one!
Expand Down

0 comments on commit b218d27

Please sign in to comment.