Skip to content

Commit 532fc27

Browse files
authored
feat: add SfTextarea component (#30)
* WIP: add SfTextarea * fix: change class to wrapper class * fix: remove unused classes * feat: add showcase TextAreaCharacters * feat: add showcase textarea disabled * feat: add showcase textarea Invalid * feat: add showcase textarea readOnly * feat: add showcase textarea autoresize * fix: onInput textarea * chore: delete unused file * chore: delete unused file browser * chore: delete unused file types * chore: delete unused file index
1 parent 9cf058f commit 532fc27

File tree

13 files changed

+449
-2
lines changed

13 files changed

+449
-2
lines changed

apps/docs/utils/components.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export const components = {
2323
// 'SfScrollable',
2424
// 'SfSelect',
2525
'SfSwitch',
26-
// 'SfTextarea',
26+
'SfTextarea',
2727
// 'SfThumbnail',
2828
// 'SfTooltip',
2929
],

apps/docs/utils/hooks.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
export const hooks = [
22
// 'useDisclosure',
33
// 'useDropdown',
4-
// 'useFocusVisible',
4+
'useFocusVisible',
55
// 'usePagination',
66
// 'usePopover',
77
// 'useScrollable',
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import { $, component$, useContext, useTask$ } from '@builder.io/qwik';
2+
import { SfTextarea, SfTextareaSize } from 'qwik-storefront-ui';
3+
import { ComponentExample } from '../../../components/utils/ComponentExample';
4+
import { ControlsType } from '../../../components/utils/types';
5+
import { EXAMPLES_STATE } from '../layout';
6+
7+
export default component$(() => {
8+
const examplesState = useContext(EXAMPLES_STATE);
9+
10+
useTask$(() => {
11+
examplesState.data = {
12+
controls: [
13+
{
14+
type: 'select',
15+
modelName: 'size',
16+
propDefaultValue: 'SfInputSize.base',
17+
propType: 'SfInputSize',
18+
options: Object.keys(SfTextareaSize),
19+
isRequired: false,
20+
},
21+
{
22+
type: 'text',
23+
propType: 'string',
24+
modelName: 'label',
25+
isRequired: false,
26+
},
27+
{
28+
type: 'text',
29+
propType: 'string',
30+
modelName: 'placeholder',
31+
isRequired: false,
32+
},
33+
{
34+
type: 'text',
35+
propType: 'string',
36+
modelName: 'helpText',
37+
isRequired: false,
38+
},
39+
{
40+
type: 'text',
41+
propType: 'string',
42+
modelName: 'requiredText',
43+
isRequired: false,
44+
},
45+
{
46+
type: 'text',
47+
propType: 'string',
48+
modelName: 'errorText',
49+
isRequired: false,
50+
},
51+
{
52+
type: 'text',
53+
propType: 'number',
54+
modelName: 'characterLimit',
55+
isRequired: false,
56+
},
57+
{
58+
type: 'boolean',
59+
propType: 'boolean',
60+
modelName: 'disabled',
61+
isRequired: false,
62+
},
63+
{
64+
type: 'boolean',
65+
propType: 'boolean',
66+
modelName: 'required',
67+
isRequired: false,
68+
},
69+
{
70+
type: 'boolean',
71+
propType: 'boolean',
72+
modelName: 'invalid',
73+
isRequired: false,
74+
},
75+
{
76+
type: 'boolean',
77+
propType: 'boolean',
78+
modelName: 'readonly',
79+
isRequired: false,
80+
},
81+
] satisfies ControlsType,
82+
state: {
83+
size: SfTextareaSize.base,
84+
disabled: false,
85+
required: false,
86+
invalid: false,
87+
readonly: undefined,
88+
placeholder: 'Write something about yourself',
89+
helpText: 'Do not include personal or financial information.',
90+
requiredText: 'Required text',
91+
errorText: 'Error message',
92+
label: 'Description',
93+
characterLimit: 12,
94+
value: '',
95+
},
96+
};
97+
});
98+
99+
const onInput = $((e: Event) => {
100+
const target = e.target as HTMLInputElement;
101+
102+
examplesState.data.state = {
103+
...examplesState.data.state,
104+
value: target.value,
105+
};
106+
});
107+
108+
const isAboveLimit = examplesState.data.state.characterLimit
109+
? examplesState.data.state.value.length >
110+
examplesState.data.state.characterLimit
111+
: false;
112+
113+
const charsCount = examplesState.data.state.characterLimit
114+
? examplesState.data.state.characterLimit -
115+
examplesState.data.state.value.length
116+
: null;
117+
118+
const getCharacterLimitClass = () =>
119+
isAboveLimit ? 'text-negative-700 font-medium' : 'text-neutral-500';
120+
121+
return (
122+
<ComponentExample>
123+
<label>
124+
<span
125+
class={[
126+
'typography-text-sm font-medium',
127+
{
128+
'cursor-not-allowed text-disabled-500':
129+
examplesState.data.state.disabled,
130+
},
131+
]}
132+
>
133+
{examplesState.data.state.label}
134+
</span>
135+
<SfTextarea
136+
name={examplesState.data.state.label}
137+
size={examplesState.data.state.size}
138+
value={examplesState.data.state.value}
139+
invalid={examplesState.data.state.invalid}
140+
placeholder={examplesState.data.state.placeholder}
141+
disabled={examplesState.data.state.disabled}
142+
readOnly={examplesState.data.state.readonly}
143+
onInput$={onInput}
144+
wrapperClass={[
145+
`w-full block h-2/5 ${
146+
examplesState.data.state.disabled
147+
? '!bg-disabled-100 !ring-disabled-300 !ring-1 !text-disabled-500'
148+
: examplesState.data.state.readonly &&
149+
'!bg-disabled-100 !ring-disabled-300 !ring-1 !text-neutral-500'
150+
}`,
151+
]}
152+
/>
153+
</label>
154+
<div class="flex justify-between">
155+
<div>
156+
{examplesState.data.state.invalid &&
157+
!examplesState.data.state.disabled && (
158+
<p class="typography-error-sm text-negative-700 font-medium mt-0.5">
159+
{examplesState.data.state.errorText}
160+
</p>
161+
)}
162+
{examplesState.data.state.helpText && (
163+
<p
164+
class={[
165+
'typography-hint-xs mt-0.5',
166+
examplesState.data.state.disabled
167+
? 'text-disabled-500'
168+
: 'text-neutral-500',
169+
]}
170+
>
171+
{examplesState.data.state.helpText}
172+
</p>
173+
)}
174+
{examplesState.data.state.requiredText &&
175+
examplesState.data.state.required ? (
176+
<p class="mt-1 typography-text-sm font-normal text-neutral-500 before:content-['*']">
177+
{examplesState.data.state.requiredText}
178+
</p>
179+
) : null}
180+
</div>
181+
{examplesState.data.state.characterLimit &&
182+
!examplesState.data.state.readonly ? (
183+
<p
184+
class={[
185+
'typography-error-xs mt-0.5',
186+
examplesState.data.state.disabled
187+
? 'text-disabled-500'
188+
: getCharacterLimitClass(),
189+
]}
190+
>
191+
{charsCount}
192+
</p>
193+
) : null}
194+
</div>
195+
</ComponentExample>
196+
);
197+
});
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { component$, useSignal, useTask$ } from '@builder.io/qwik';
2+
import { attach } from '@frsource/autoresize-textarea';
3+
import { SfTextarea } from 'qwik-storefront-ui';
4+
5+
export default component$(() => {
6+
const textareaRef = useSignal<HTMLTextAreaElement>();
7+
8+
useTask$(() => {
9+
if (textareaRef.value) {
10+
attach(textareaRef.value);
11+
}
12+
});
13+
return (
14+
<>
15+
<label>
16+
<span class="typography-text-sm font-medium">Description</span>
17+
<SfTextarea
18+
ref={textareaRef}
19+
wrapperClass={['w-full h-max-[500px] block']}
20+
size="sm"
21+
aria-label="Label size sm"
22+
/>
23+
</label>
24+
<p class="typography-hint-xs text-neutral-500 mt-0.5">
25+
Do not include personal or financial information.
26+
</p>
27+
</>
28+
);
29+
});
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { $, component$, useComputed$, useSignal } from '@builder.io/qwik';
2+
import { SfTextarea } from 'qwik-storefront-ui';
3+
4+
export default component$(() => {
5+
const characterLimit = 25;
6+
const disabled = false;
7+
const readonly = false;
8+
const invalid = false;
9+
const helpText = 'Help text';
10+
const errorText = 'Error';
11+
12+
const valueSignal = useSignal('');
13+
14+
const isAboveLimitSignal = useComputed$(() =>
15+
characterLimit ? valueSignal.value.length > characterLimit : false
16+
);
17+
18+
const charsCountSignal = useComputed$(() =>
19+
characterLimit ? characterLimit - valueSignal.value.length : null
20+
);
21+
22+
const getCharacterLimitClass = () =>
23+
isAboveLimitSignal.value
24+
? 'text-negative-700 font-medium'
25+
: 'text-neutral-500';
26+
27+
const onInput = $((e: Event) => {
28+
const target = e.target as HTMLInputElement;
29+
valueSignal.value = target.value;
30+
});
31+
32+
return (
33+
<>
34+
<label>
35+
<span class="text-sm font-medium">Description</span>
36+
<SfTextarea
37+
value={valueSignal.value}
38+
placeholder="Write something about yourself..."
39+
invalid={invalid}
40+
disabled={disabled}
41+
onInput$={onInput}
42+
wrapperClass={['w-full mt-0.5 block']}
43+
/>
44+
</label>
45+
<div class="flex justify-between mt-0.5">
46+
<div>
47+
{invalid && !disabled && (
48+
<p class="typography-text-sm text-negative-700 font-medium mt-0.5">
49+
{errorText}
50+
</p>
51+
)}
52+
{helpText && (
53+
<p
54+
class={[
55+
'typography-hint-xs',
56+
disabled ? 'text-disabled-500' : 'text-neutral-500',
57+
]}
58+
>
59+
{helpText}
60+
</p>
61+
)}
62+
</div>
63+
{characterLimit && !readonly ? (
64+
<p
65+
class={[
66+
'typography-error-xs',
67+
disabled ? 'text-disabled-500' : getCharacterLimitClass(),
68+
]}
69+
>
70+
{charsCountSignal.value}
71+
</p>
72+
) : null}
73+
</div>
74+
</>
75+
);
76+
});
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { component$ } from '@builder.io/qwik';
2+
import { SfTextarea } from 'qwik-storefront-ui';
3+
4+
export default component$(() => {
5+
return (
6+
<>
7+
<label>
8+
<span class="typography-text-sm font-medium cursor-not-allowed text-disabled-900">
9+
Description
10+
</span>
11+
<SfTextarea
12+
disabled
13+
placeholder="Write something about yourself..."
14+
wrapperClass={[
15+
'w-full !bg-disabled-100 !ring-disabled-300 !ring-1 block',
16+
]}
17+
/>
18+
</label>
19+
<p class="typography-hint-xs text-disabled-500 mt-0.5">
20+
Do not include personal or financial information.
21+
</p>
22+
</>
23+
);
24+
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { component$ } from '@builder.io/qwik';
2+
import { SfTextarea } from 'qwik-storefront-ui';
3+
4+
export default component$(() => {
5+
return (
6+
<>
7+
<label>
8+
<span class="typography-text-sm font-medium">Description</span>
9+
<SfTextarea
10+
invalid
11+
placeholder="Write something about yourself..."
12+
wrapperClass={['w-full block']}
13+
/>
14+
</label>
15+
<div class="flex justify-between mt-0.5">
16+
<p class="typography-text-sm text-negative-700 font-medium">
17+
The field cannot be empty
18+
</p>
19+
<p class="typography-hint-xs text-neutral-500">
20+
Do not include personal or financial information.
21+
</p>
22+
</div>
23+
</>
24+
);
25+
});
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { component$ } from '@builder.io/qwik';
2+
import { SfTextarea } from 'qwik-storefront-ui';
3+
4+
export default component$(() => {
5+
return (
6+
<>
7+
<label>
8+
<span class="typography-text-sm font-medium">Description</span>
9+
<SfTextarea
10+
value="Hello! I'm a passionate shopper and a regular user of this ecommerce platform."
11+
wrapperClass={[
12+
'w-full !bg-disabled-100 !ring-disabled-300 !ring-1 block',
13+
]}
14+
readOnly
15+
/>
16+
</label>
17+
<p class="typography-hint-xs text-neutral-500 mt-0.5">
18+
Do not include personal or financial information.
19+
</p>
20+
</>
21+
);
22+
});

0 commit comments

Comments
 (0)