Skip to content

Commit

Permalink
patch: pr5284 and pr5422 (#5461)
Browse files Browse the repository at this point in the history
* fix(FormItem): add `aria-labelledby` support for `bottom` + `Input` (#5422)

* fix(Math.ts): updated `decimatedClamp` implementation (#5284)

---------

Co-authored-by: Semyon Okulov <67464545+scffs@users.noreply.github.com>
  • Loading branch information
inomdzhon and scffs authored Jul 12, 2023
1 parent 81cd4dd commit e87dd96
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 30 deletions.
17 changes: 16 additions & 1 deletion packages/vkui/src/components/FormItem/FormItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ export interface FormItemProps
RemovableProps {
top?: React.ReactNode;
bottom?: React.ReactNode;
/**
* Передаётся при использовании `bottom`.
*
* Должен совпадать с `aria-describedby`, который передаётся в компонент, отвечающий за пользовательский ввод.
*/
bottomId?: string;
status?: 'default' | 'error' | 'valid';
/**
* Дает возможность удалить `FormItem`. Рекомендуется использовать только для `Input` или `Select`.
Expand All @@ -48,6 +54,7 @@ export const FormItem = ({
getRootRef,
className,
htmlFor,
bottomId,
...restProps
}: FormItemProps) => {
const rootEl = useExternRef(getRootRef);
Expand All @@ -65,7 +72,15 @@ export const FormItem = ({
</Subhead>
)}
{children}
{hasReactNode(bottom) && <Footnote className={styles['FormItem__bottom']}>{bottom}</Footnote>}
{hasReactNode(bottom) && (
<Footnote
className={styles['FormItem__bottom']}
id={bottomId}
role={status === 'error' ? 'alert' : undefined}
>
{bottom}
</Footnote>
)}
</React.Fragment>
);

Expand Down
6 changes: 6 additions & 0 deletions packages/vkui/src/components/FormItem/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,19 @@
- При передаче в `FormItem` компонента, отвечающего за пользовательский ввод (например, `<input type="text" />`),
рекомендуется передавать свойства `top` и `htmlFor`. В компонент пользовательского ввода должно быть передано свойство
`id`, которое соответствует значению `htmlFor` в `FormItem`. <br />
- При использовании `bottom` атрибута рекомендуется также передавать в компонент пользовательского ввода (например,
`<input type="text" />`) атрибут `aria-describedby`, а в сам `FormItem` передать `bottomId`, который соответствует значению `aria-describedby`.

Пример рекомендуемого использования:

```js static
<FormItem top="Имя" htmlFor="name">
<input id="name" type="text" placeholder="Семён" />
</FormItem>

<FormItem top="E-mail" bottom="Например, email@internet.ru" bottomId="emailExample" htmlFor="email">
<input id="email" aria-describedby="emailExample" type="email" placeholder="email@internet.ru" />
</FormItem>
```

```jsx
Expand Down
33 changes: 12 additions & 21 deletions packages/vkui/src/helpers/math.test.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,10 @@
import { clamp, precisionRound, rescale } from './math';
import { clamp, rescale } from './math';

describe(clamp, () => {
it('clamps min', () => expect(clamp(10, 20, 30)).toBe(20));
it('clamps max', () => expect(clamp(40, 20, 30)).toBe(30));
});

describe(precisionRound, () => {
it('rounds to precision', () => {
expect(precisionRound(0.3 + 0.6, 1)).toBe(0.9);
expect(precisionRound(0.88, 1)).toBe(0.9);
expect(precisionRound(0.881, 2)).toBe(0.88);
});
it('can integer-round', () => {
expect(precisionRound(1.1, 0)).toBe(1);
expect(precisionRound(0.9, 0)).toBe(1);
});
});

describe(rescale, () => {
it('scales value', () => {
expect(rescale(0.5, [0, 1], [3, 5])).toBe(4);
Expand All @@ -34,13 +22,16 @@ describe(rescale, () => {
it('rounds precision', () => {
expect(rescale(3.1415926, [3, 4], [3, 4], { step: 0.01 })).toBe(3.14);
});
describe('non-divisor step', () => {
it('rounds to min + k * step when min != n * step', () => {
expect(rescale(0.1, [0.1, 3.1], [0.1, 3.1], { step: 2 })).toBe(0.1);
expect(rescale(3, [1, 5], [1, 5], { step: 2 })).toBe(3);
});
it('clamps to min + min(max, min + k * step) when max != min + n * step', () => {
expect(rescale(3, [0, 3], [0, 3], { step: 2 })).toBe(2);
});
it('rounds to min when min != n * step', () => {
expect(rescale(0.1, [0.1, 3.1], [0.1, 3.1], { step: 2 })).toBe(0.1);
});
it('rounds to max when max != n * step', () => {
expect(rescale(3, [1, 5], [1, 5], { step: 2 })).toBe(3);
expect(rescale(3, [0, 3], [0, 3], { step: 2 })).toBe(3);
});
describe('check use cases', () =>
// см. https://github.com/VKCOM/VKUI/pull/5284
it('should return 20000', () => {
expect(rescale(100, [0, 100], [250, 20000], { step: 115 })).toBe(20000);
}));
});
30 changes: 22 additions & 8 deletions packages/vkui/src/helpers/math.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,34 @@ export function precisionRound(number: number, precision = 1) {
return Math.round(number * factor) / factor;
}

function precision(number: number) {
return (`${number}`.split('.')[1] || '').length;
/**
* Решение скопировано без изменений у MUI:
* https://github.com/mui/material-ui/blob/v5.13.7/packages/mui-base/src/useSlider/useSlider.ts#L89-L105
*/
function getDecimalPrecision(num: number) {
// This handles the case when num is very small (0.00000001), js will turn this into 1e-8.
// When num is bigger than 1 or less than -1 it won't get converted to this notation so it's fine.
if (Math.abs(num) < 1) {
const parts = num.toExponential().split('e-');
const matissaDecimalPart = parts[0].split('.')[1];
return (matissaDecimalPart ? matissaDecimalPart.length : 0) + parseInt(parts[1], 10);
}

const decimalPart = num.toString().split('.')[1];
return decimalPart ? decimalPart.length : 0;
}

function roundValueToStep(value: number, step: number, min: number) {
const nearest = Math.round((value - min) / step) * step + min;
return Number(nearest.toFixed(getDecimalPrecision(step)));
}

function decimatedClamp(val: number, min: number, max: number, step?: number) {
if (step == null || step <= 0) {
return clamp(val, min, max);
}
const prec = precision(step);
// Round value to nearest min + k1 * step
const decimatedOffset = precisionRound(Math.round((val - min) / step) * step, prec);
// Round range length _down_ to nearest min + k2 * step
const decimatedRange = precisionRound(Math.floor((max - min) / step) * step, prec);
return min + clamp(decimatedOffset, 0, decimatedRange);
const roundedValue = roundValueToStep(val, step, min);
return clamp(roundedValue, min, max);
}

export function rescale(
Expand Down

0 comments on commit e87dd96

Please sign in to comment.