Skip to content

Commit 1dac7eb

Browse files
committed
IBX-7905: AltRadio
1 parent 0e1569a commit 1dac7eb

File tree

10 files changed

+286
-35
lines changed

10 files changed

+286
-35
lines changed
Lines changed: 2 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
@use 'sass:map';
22
@use 'functions' as *;
33
@use 'variables' as *;
4-
@use 'mixins/icons' as mixins;
4+
@use 'mixins/inputs' as inputs-mixins;
55

66
$input-sizes: (
77
'small': (
@@ -15,45 +15,12 @@ $input-sizes: (
1515
);
1616

1717
.ids-input {
18-
border-radius: $border-radius-medium;
19-
background: var(--ids-input-default-bg-color, #{$color-neutral-10});
20-
border: calculateRem(1px) solid var(--ids-input-default-border-color, #{$color-neutral-80});
21-
color: var(--ids-input-default-text-color, #{$color-neutral-240});
22-
outline: 0;
18+
@include inputs-mixins.input-base;
2319

2420
@each $name, $properties in $input-sizes {
2521
&--#{$name} {
2622
font-size: map.get($properties, 'font-size');
2723
padding: map.get($properties, 'padding');
2824
}
2925
}
30-
31-
&.ids-input--error {
32-
background: var(--ids-input-error-bg-color, #{$color-error-20});
33-
border-color: var(--ids-input-error-border-color, #{$color-error-80});
34-
color: var(--ids-input-error-text-color, #{$color-error-90});
35-
}
36-
37-
&.ids-input--disabled,
38-
&.disabled,
39-
&[disabled],
40-
&:disabled {
41-
color: var(--ids-input-disabled-text-color, #{$color-neutral-150});
42-
background: var(--ids-input-disabled-bg-color, #{$color-neutral-40});
43-
border-color: var(--ids-input-disabled-border-color, #{$color-neutral-80});
44-
pointer-events: none;
45-
}
46-
47-
&:hover {
48-
border-color: var(--ids-input-hover-border-color, #{$color-primary-80});
49-
}
50-
51-
&:active {
52-
border-color: var(--ids-input-active-border-color, #{$color-primary-90});
53-
}
54-
55-
&:focus {
56-
border-color: var(--ids-input-focus-border-color, #{$color-primary-80});
57-
box-shadow: var(--ids-input-focus-box-shadow, #{$box-shadow-focus-primary});
58-
}
5926
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
@use '../functions' as *;
2+
@use '../variables' as *;
3+
@use '../mixins/inputs' as inputs-mixins;
4+
@use '../mixins/utils' as utils-mixins;
5+
6+
.ids-alt-radio {
7+
&__source {
8+
@include utils-mixins.hidden;
9+
}
10+
11+
&__tile {
12+
@include inputs-mixins.input-base;
13+
14+
& {
15+
cursor: pointer;
16+
padding: calculateRem(8px) calculateRem(16px);
17+
}
18+
19+
&--checked {
20+
background-color: var(--ids-input-checked-bg-color, #{$color-neutral-50});
21+
}
22+
23+
&--focused {
24+
@include inputs-mixins.input-focus;
25+
}
26+
}
27+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
@use '../functions' as *;
2+
@use '../variables' as *;
3+
4+
@mixin input-focus {
5+
border-color: var(--ids-input-focus-border-color, #{$color-primary-80});
6+
box-shadow: var(--ids-input-focus-box-shadow, #{$box-shadow-focus-primary});
7+
}
8+
9+
@mixin input-base {
10+
border-radius: $border-radius-medium;
11+
background: var(--ids-input-default-bg-color, #{$color-neutral-10});
12+
border: calculateRem(1px) solid var(--ids-input-default-border-color, #{$color-neutral-80});
13+
color: var(--ids-input-default-text-color, #{$color-neutral-240});
14+
outline: 0;
15+
16+
&--error {
17+
background: var(--ids-input-error-bg-color, #{$color-error-20});
18+
border-color: var(--ids-input-error-border-color, #{$color-error-80});
19+
color: var(--ids-input-error-text-color, #{$color-error-90});
20+
}
21+
22+
&--disabled,
23+
&.disabled,
24+
&[disabled],
25+
&:disabled {
26+
color: var(--ids-input-disabled-text-color, #{$color-neutral-150});
27+
background: var(--ids-input-disabled-bg-color, #{$color-neutral-40});
28+
border-color: var(--ids-input-disabled-border-color, #{$color-neutral-80});
29+
pointer-events: none;
30+
}
31+
32+
&:hover {
33+
border-color: var(--ids-input-hover-border-color, #{$color-primary-80});
34+
}
35+
36+
&:active {
37+
border-color: var(--ids-input-active-border-color, #{$color-primary-90});
38+
}
39+
40+
&:focus {
41+
@include input-focus;
42+
}
43+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
@use '../functions' as *;
2+
3+
@mixin hidden {
4+
width: 0;
5+
height: 0;
6+
overflow: hidden;
7+
opacity: 0;
8+
}

packages/assets/src/scss/styles.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
@use 'input';
1313
@use 'label';
1414

15+
@use 'inputs/alt-radio';
1516
@use 'inputs/checkbox';
1617
@use 'inputs/input-text';
1718
@use 'inputs/radio-button';
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import React from 'react';
2+
3+
import type { Meta, StoryObj } from '@storybook/react';
4+
import { action } from 'storybook/actions';
5+
6+
import { AltRadioStateful } from './AltRadio';
7+
8+
const meta: Meta<typeof AltRadioStateful> = {
9+
component: AltRadioStateful,
10+
parameters: {
11+
layout: 'centered',
12+
},
13+
tags: ['autodocs', 'foundation', 'inputs'],
14+
argTypes: {
15+
className: {
16+
control: 'text',
17+
},
18+
title: {
19+
control: 'text',
20+
},
21+
checked: {
22+
control: 'boolean',
23+
},
24+
},
25+
args: {
26+
name: 'default-input',
27+
label: '1:1',
28+
onBlur: action('on-blur'),
29+
onChange: action('on-change'),
30+
onFocus: action('on-focus'),
31+
onInput: action('on-input'),
32+
},
33+
decorators: [
34+
(Story) => {
35+
return (
36+
<form name="default-form">
37+
<Story />
38+
</form>
39+
);
40+
},
41+
],
42+
};
43+
44+
export default meta;
45+
46+
type Story = StoryObj<typeof AltRadioStateful>;
47+
48+
export const Empty: Story = {
49+
name: 'Empty',
50+
};
51+
52+
export const EmptyDisabled: Story = {
53+
name: 'Empty (Disabled)',
54+
args: {
55+
disabled: true,
56+
},
57+
};
58+
59+
export const EmptyError: Story = {
60+
name: 'Empty (Error)',
61+
args: {
62+
error: true,
63+
},
64+
};
65+
66+
export const Checked: Story = {
67+
name: 'Checked',
68+
args: {
69+
checked: true,
70+
},
71+
};
72+
73+
export const CheckedDisabled: Story = {
74+
name: 'Checked (Disabled)',
75+
args: {
76+
disabled: true,
77+
checked: true,
78+
},
79+
};
80+
81+
export const CheckedError: Story = {
82+
name: 'Checked (Error)',
83+
args: {
84+
error: true,
85+
checked: true,
86+
},
87+
};
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import type { Meta, StoryObj } from '@storybook/react';
2+
import { expect, fn, userEvent, within } from 'storybook/test';
3+
4+
import { AltRadioStateful } from './AltRadio';
5+
6+
const meta: Meta<typeof AltRadioStateful> = {
7+
component: AltRadioStateful,
8+
parameters: {
9+
layout: 'centered',
10+
},
11+
tags: ['!dev'],
12+
args: {
13+
name: 'default-input',
14+
label: '1:1',
15+
onBlur: fn(),
16+
onChange: fn(),
17+
onFocus: fn(),
18+
onInput: fn(),
19+
},
20+
};
21+
22+
export default meta;
23+
24+
type Story = StoryObj<typeof AltRadioStateful>;
25+
26+
export const Default: Story = {
27+
name: 'Default',
28+
play: async ({ canvasElement, step, args }) => {
29+
const canvas = within(canvasElement);
30+
const input = canvas.getByRole('button');
31+
32+
await step('Radio Button handles focus event', async () => {
33+
await expect(args.onFocus).not.toHaveBeenCalled();
34+
35+
await userEvent.click(input);
36+
37+
await expect(args.onFocus).toHaveBeenCalledOnce();
38+
await expect(args.onChange).toHaveBeenCalledOnce();
39+
await expect(args.onInput).toHaveBeenCalledOnce();
40+
});
41+
42+
await step('Radio Button handles blur event', async () => {
43+
await expect(args.onBlur).not.toHaveBeenCalled();
44+
45+
await userEvent.click(canvasElement);
46+
47+
await expect(args.onBlur).toHaveBeenCalledOnce();
48+
});
49+
},
50+
};
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import React, { useRef, useState } from 'react';
2+
3+
import BaseChoiceInput from '@ids-internal/partials/BaseChoiceInput';
4+
import { createCssClassNames } from '@ibexa/ids-core/helpers/cssClassNames';
5+
import withStateChecked from '@ids-internal/hoc/withStateChecked';
6+
7+
import { AltRadioProps } from './AltRadio.types';
8+
9+
const AltRadio = ({ className = '', label, ...restProps }: AltRadioProps) => {
10+
const { checked = false, disabled = false, error = false, onBlur, onChange, onFocus, onInput } = restProps;
11+
const inputRef = useRef<HTMLInputElement>(null);
12+
const [isFocused, setIsFocused] = useState(false);
13+
const altRadioClassName = createCssClassNames({
14+
'ids-alt-radio': true,
15+
[className]: true,
16+
});
17+
const altRadioTileClassName = createCssClassNames({
18+
'ids-alt-radio__tile': true,
19+
'ids-alt-radio__tile--checked': checked,
20+
'ids-alt-radio__tile--disabled': disabled,
21+
'ids-alt-radio__tile--error': error,
22+
'ids-alt-radio__tile--focused': isFocused,
23+
});
24+
const onTileClick = () => {
25+
inputRef.current?.focus();
26+
27+
if (!checked) {
28+
onChange?.(true);
29+
onInput?.(true);
30+
}
31+
};
32+
const onInputFocus = (event: React.FocusEvent<HTMLInputElement>) => {
33+
setIsFocused(true);
34+
onFocus?.(event);
35+
};
36+
const onInputBlur = (event: React.FocusEvent<HTMLInputElement>) => {
37+
setIsFocused(false);
38+
onBlur?.(event);
39+
};
40+
41+
return (
42+
<div className={altRadioClassName}>
43+
<div className="ids-alt-radio__source">
44+
<BaseChoiceInput {...restProps} onBlur={onInputBlur} onFocus={onInputFocus} ref={inputRef} type="radio" />
45+
</div>
46+
<div className={altRadioTileClassName} onClick={onTileClick} role="button">
47+
{label}
48+
</div>
49+
</div>
50+
);
51+
};
52+
53+
export default AltRadio;
54+
55+
export const AltRadioStateful = withStateChecked(AltRadio);
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { ReactNode } from 'react';
2+
3+
import { BaseChoiceInputProps } from '@ids-internal/partials/BaseChoiceInput';
4+
5+
export interface AltRadioProps extends Omit<BaseChoiceInputProps, 'type'> {
6+
label: ReactNode;
7+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import AltRadio, { AltRadioStateful } from './AltRadio';
2+
import { AltRadioProps } from './AltRadio.types';
3+
4+
export default AltRadio;
5+
export { AltRadioStateful };
6+
export type { AltRadioProps };

0 commit comments

Comments
 (0)