Skip to content

Commit 285a9cd

Browse files
committed
Initial Input styling implementation
1 parent 28476bf commit 285a9cd

File tree

7 files changed

+435
-29
lines changed

7 files changed

+435
-29
lines changed

packages/react-input/Spec.md

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,123 @@ Base accessibility information is included in the design document. After the spe
6161
- Identify UI parts that appear on **hover or focus** and specify keyboard and screen reader interaction with them
6262
- List cases when **focus** needs to be **trapped** in sections of the UI (for dialogs and popups or for hierarchical navigation)
6363
- List cases when **focus** needs to be **moved programatically** (if parts of the UI are appearing/disappearing or other cases)
64+
65+
## Styling implementation notes
66+
67+
Input has multiple size and appearance variants. These tables were created based on the design spec to assist with initial implementation and help ensure everything is handled. (They're not exactly part of the input spec, but this seemed like a reasonable place to put them.)
68+
69+
General abbreviations used:
70+
71+
- " = inherit from left
72+
- ^ = inherit from above
73+
- ^^ = inherit from 2 lines above
74+
75+
## Sizes
76+
77+
- padding and gap values are from (theoretical) `spacing.horizontal` unless otherwise specified
78+
- bookend-related sizes are from separate bookends page (everything except L/R padding and spacing within inherits from default)
79+
- interpretation:
80+
- "spacing between icon before and content"/"spacing between content and icon after 1" => "spacing start/end to content"
81+
- "spacing between icon after 1 and icon after 2" => "spacing within insideEnd" because we're not going to have two icon slots
82+
- bookend "spacing between content and icon" => "spacing within bookend"
83+
- omitted "focus indicator" b/c that's handled elsewhere
84+
85+
| | All |
86+
| ------------- | ------------------- |
87+
| v-align | vertically centered |
88+
| border radius | medium |
89+
90+
| | medium | small | large |
91+
| ----------------------------- | ---------------- | ------------------- | --------- |
92+
| height | 32px | 24px | 40px |
93+
| left/right padding | mNudge | sNudge | m |
94+
| left/right padding in content | xxs | " | sNudge |
95+
| bookends left/right padding | s | sNudge | m |
96+
| content size | body1 (base.300) | caption1 (base.200) | base.400 |
97+
| "icon" size | 20Regular | 16Regular | 24Regular |
98+
| spacing start/end to content | xxs | " | sNudge |
99+
| spacing within insideEnd | xs | " | " |
100+
| spacing within bookend | xs | " | " |
101+
102+
### Sizes application
103+
104+
| thing | slot | notes |
105+
| ----------------------------- | ---------------------------- | ---------------------------------------------------------------- |
106+
| v-align | root, inputWrapper | ??? |
107+
| height | root | ? as minHeight or height ? |
108+
| border radius | bookends, inputWrapper, root | set where borders or shadow are defined; don't use if underlined |
109+
| left/right padding | inputWrapper | padding |
110+
| left/right padding in content | input | padding |
111+
| bookends left/right padding | bookends | padding |
112+
| content size | root, input | fontSize; doesn't inherit to input |
113+
| "icon" size | n/a | no icons built in |
114+
| spacing start/end to content | inputWrapper | display: flex (also to grow input), flex gap |
115+
| spacing within insideEnd | insideEnd | display: flex, flex gap |
116+
| spacing within bookends | bookends | display flex, flex gap |
117+
118+
## Appearance colors and strokes
119+
120+
- italics = thick border
121+
- interpreting "compound brand stroke 1 pressed" as compoundBrandStrokePressed
122+
- appears that focus and keyboard focus styles are the same
123+
124+
| | All |
125+
| ------------------------------------------ | ------------------------- |
126+
| content | neutralForeground1 |
127+
| content disabled | neutralForegroundDisabled |
128+
| placeholder | neutralForeground4 |
129+
| placeholder disabled | neutralForegroundDisabled |
130+
| "icon" color | neutralForeground3 |
131+
| "icon" color disabled | neutralForegroundDisabled |
132+
| background disabled | transparentBackground |
133+
| border disabled | neutralStrokeDisabled |
134+
| in focus indicator (bottom border) | _compoundBrandStroke_ |
135+
| in focus indicator (bottom border) pressed | _^Pressed_ |
136+
| cursor disabled | not-allowed |
137+
138+
| | filledDarker | filledLighter | underline | outline |
139+
| -------------------- | ------------------ | ------------------ | ------------------------ | -------------------- |
140+
| shadow | shadow2 | " | none | " |
141+
| background | neutralBackground3 | neutralBackground1 | transparentBackground | neutralBackground1 |
142+
| border | transparentStroke | " | none | neutralStroke1 |
143+
| border hover | ^Interactive | " | n/a | ^Hover |
144+
| border pressed | ^ | " | n/a | ^^Pressed |
145+
| border focused | ^ | " | n/a | n/a (neutralStroke1) |
146+
| borderBottom | n/a | n/a | neutralStrokeAccessible | " |
147+
| borderBottom hover | n/a | n/a | ^Hover | " |
148+
| borderBottom pressed | n/a | n/a | _^^Pressed_ | " |
149+
| borderBottom focused | n/a | n/a | n/a (in focus indicator) | " |
150+
151+
### Appearance application
152+
153+
| thing | slot | notes |
154+
| -------------------------- | ------------------- | -------------------------------------------------------- |
155+
| content color | input | other things have their own colors |
156+
| placeholder color | input | `::placeholder` |
157+
| "icon" color | insideStart/End | |
158+
| shadow | root | encompasses bookends; requires rounding root corners |
159+
| background | inputWrapper, input | bookends have separate background; input doesn't inherit |
160+
| border | inputWrapper | |
161+
| border hover | TODO inputWrapper | `:hover` |
162+
| border pressed | TODO | |
163+
| border focused | TODO inputWrapper | `:focus-within` |
164+
| borderBottom | inputWrapper | |
165+
| borderBottom hover | TODO inputWrapper | `:hover` |
166+
| borderBottom pressed | TODO | |
167+
| borderBottom focused | n/a | handled by focus indicator |
168+
| in focus indicator | TODO | |
169+
| in focus indicator pressed | TODO | |
170+
| cursor | root, input | |
171+
172+
## Bookend appearance (TODO)
173+
174+
| | filled | brand | transparent |
175+
| --------------- | ------------------ | ------------------------ | --------------------- |
176+
| background | neutralBackground6 | brandBackground | transparentBackground |
177+
| content (+icon) | neutralForeground2 | neutralForegroundOnBrand | neutralForeground2 |
178+
| border | transparentStroke | none | transparentStroke |
179+
| inner border | n/a | n/a | neutralStroke3 |
180+
181+
- Inner border ("border right") color is applied separately to before/after bookends.
182+
- Others are applied in obvious way to both bookends.
183+
- All borders are thin (1px).

packages/react-input/etc/react-input.api.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ export const Input: React_2.ForwardRefExoticComponent<InputProps & React_2.RefAt
1313

1414
// @public (undocumented)
1515
export interface InputCommons extends Omit<React_2.HTMLAttributes<HTMLElement>, 'children'> {
16+
appearance?: 'filledDarker' | 'filledLighter' | 'underline' | 'outline';
17+
// (undocumented)
18+
inline?: boolean;
19+
size?: 'small' | 'medium' | 'large';
1620
}
1721

1822
// @public
Lines changed: 73 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,98 @@
11
import * as React from 'react';
2-
import { makeStyles } from '@fluentui/react-make-styles';
2+
import { makeStyles, mergeClasses } from '@fluentui/react-make-styles';
33
import { Input } from './Input';
4-
import { useId } from '@fluentui/react-utilities';
4+
import { getNativeElementProps, useId } from '@fluentui/react-utilities';
5+
import { InputProps } from './Input.types';
6+
import { ArgTypes } from '@storybook/react';
7+
// prevent terrible reload times by using deep imports :(
8+
import Search16Regular from '@fluentui/react-icons/lib/esm/components/Search16Regular';
9+
import Search20Regular from '@fluentui/react-icons/lib/esm/components/Search20Regular';
10+
import Search24Regular from '@fluentui/react-icons/lib/esm/components/Search24Regular';
11+
import Dismiss16Regular from '@fluentui/react-icons/lib/esm/components/Dismiss16Regular';
12+
import Dismiss20Regular from '@fluentui/react-icons/lib/esm/components/Dismiss20Regular';
13+
import Dismiss24Regular from '@fluentui/react-icons/lib/esm/components/Dismiss24Regular';
514

615
const useStyles = makeStyles({
716
container: {
817
display: 'flex',
918
flexDirection: 'column',
1019
gap: '20px',
11-
width: '300px',
20+
padding: '20px',
1221
},
22+
storyFilledBackground: theme => ({ background: theme.alias.color.neutral.neutralBackground3 }),
1323
});
1424

15-
export const InputExamples = () => {
25+
const icons = {
26+
search: { small: Search16Regular, medium: Search20Regular, large: Search24Regular },
27+
dismiss: { small: Dismiss16Regular, medium: Dismiss20Regular, large: Dismiss24Regular },
28+
};
29+
30+
export const InputExamples = (
31+
args: Partial<InputProps> & React.InputHTMLAttributes<HTMLInputElement> & { storyFilledBackground: boolean },
32+
) => {
1633
const styles = useStyles();
1734
const inputId1 = useId();
35+
// pass native input props to the internal input element and custom props to the root
36+
const { storyFilledBackground, ...rest } = args;
37+
const inputProps = getNativeElementProps('input', rest, ['size']);
38+
const props: Partial<InputProps> = { input: inputProps };
39+
for (const prop of Object.keys(rest) as (keyof InputProps)[]) {
40+
if (!(inputProps as Partial<InputProps>)[prop]) {
41+
props[prop] = rest[prop];
42+
}
43+
}
44+
const SearchIcon = icons.search[props.size!];
45+
const DismissIcon = icons.dismiss[props.size!];
46+
1847
return (
19-
<div className={styles.container}>
20-
<Input />
48+
<div className={mergeClasses(styles.container, storyFilledBackground && styles.storyFilledBackground)}>
49+
<Input {...props} />
2150
<div>
2251
<label htmlFor={inputId1}>with a label</label>
23-
<Input id={inputId1} />
52+
<Input {...props} id={inputId1} />
2453
</div>
54+
<Input {...props} insideStart={<SearchIcon />} insideEnd={<DismissIcon />} />
55+
<p>
56+
Some text with <Input {...props} inline /> inline input
57+
</p>
58+
<Input
59+
{...props}
60+
input={{
61+
...(props.input as React.HTMLAttributes<HTMLInputElement>),
62+
disabled: true,
63+
placeholder: 'disabled',
64+
}}
65+
insideStart={<SearchIcon />}
66+
insideEnd={<DismissIcon />}
67+
/>
68+
<Input
69+
{...props}
70+
style={{ width: '300px' }}
71+
input={{
72+
...(props.input as React.HTMLAttributes<HTMLInputElement>),
73+
placeholder: '300px width',
74+
}}
75+
/>
2576
</div>
2677
);
2778
};
2879

80+
const argTypes: ArgTypes = {
81+
size: { defaultValue: 'medium', control: { type: 'radio', options: ['small', 'medium', 'large'] } },
82+
appearance: {
83+
defaultValue: 'outline',
84+
control: { type: 'radio', options: ['filledDarker', 'filledLighter', 'underline', 'outline'] },
85+
},
86+
// this one is for the example
87+
storyFilledBackground: { defaultValue: false, control: { type: 'boolean' } },
88+
// NOTE: these are not actually top-level props right now until RFC is resolved,
89+
// so they get passed through in the example via the input slot
90+
placeholder: { defaultValue: 'placeholder', control: { type: 'text' } },
91+
value: { control: { type: 'text' } },
92+
};
93+
2994
export default {
3095
title: 'Components/Input',
3196
component: Input,
97+
argTypes,
3298
};

packages/react-input/src/components/Input/Input.types.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,16 @@ export type InputSlots = {
1919
insideEnd?: React.HTMLAttributes<HTMLElement>;
2020
};
2121

22-
export interface InputCommons extends Omit<React.HTMLAttributes<HTMLElement>, 'children'> {}
22+
export interface InputCommons extends Omit<React.HTMLAttributes<HTMLElement>, 'children'> {
23+
/** @default 'medium' */
24+
// TODO this overlaps with a native input prop
25+
size?: 'small' | 'medium' | 'large';
26+
// TODO(design) not clear on what block should do or if it's separate from inline
27+
// block?: boolean;
28+
inline?: boolean;
29+
/** @default 'outline' */
30+
appearance?: 'filledDarker' | 'filledLighter' | 'underline' | 'outline';
31+
}
2332

2433
/**
2534
* Input Props

packages/react-input/src/components/Input/__snapshots__/Input.test.tsx.snap

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@
22

33
exports[`Input renders a default state 1`] = `
44
<div>
5-
<div
5+
<span
66
class=""
77
>
8-
<div>
9-
<input />
10-
</div>
11-
</div>
8+
<span
9+
class=""
10+
>
11+
<input
12+
class=""
13+
/>
14+
</span>
15+
</span>
1216
</div>
1317
`;

packages/react-input/src/components/Input/useInput.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,17 @@ export const inputShorthandProps: (keyof InputSlots)[] = [
2525
*/
2626
export const useInput = (props: InputProps, ref: React.Ref<HTMLElement>): InputState => {
2727
return {
28+
// use span for the slots to avoid nesting errors in case the field is inline and nested within a <p> or something
29+
as: 'span',
2830
...props,
2931
components: {
3032
input: 'input',
33+
inputWrapper: 'span',
34+
bookendBefore: 'span',
35+
bookendAfter: 'span',
36+
insideStart: 'span',
37+
insideEnd: 'span',
3138
},
32-
// temporarily must add fake children to prevent getSlots from substituting nullRender
3339
input: resolveShorthand(props.input, { required: true }),
3440
inputWrapper: resolveShorthand(props.inputWrapper, { required: true }),
3541
bookendAfter: resolveShorthand(props.bookendAfter),

0 commit comments

Comments
 (0)