Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tag component + InputField story using it #28

Merged
merged 21 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/forms/controls/Radio/Radio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export type RadioProps = ComponentProps<'input'> & {
unstyled?: undefined | boolean,
};
/**
* A simple Radio control, just the &lt;input type="radio"&gt; and nothing else..
* A simple Radio control, just the &lt;input type="radio"&gt; and nothing else.
*/
export const Radio = (props: RadioProps) => {
const {
Expand Down
27 changes: 27 additions & 0 deletions src/components/forms/controls/Tag/Tag.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/* Copyright (c) Fortanix, Inc.
|* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of
|* the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */

@use '../../../../styling/defs.scss' as bk;

@layer baklava.components {
.bk-tag {
@include bk.component-base(bk-tag);

background: bk.$theme-tag-background-default;
border-radius: 2px;
color: bk.$theme-tag-text-default;
display: flex;
align-items: center;
font-size: bk.$font-size-xs;
padding: 2px 0 3px bk.$spacing-2;

.bk-tag__icon {
width: 7px;
height: 7px;
color: bk.$theme-tag-icon-default;
cursor: pointer;
padding: 2px bk.$spacing-2 2px bk.$spacing-2;
}
}
nighto marked this conversation as resolved.
Show resolved Hide resolved
}
32 changes: 32 additions & 0 deletions src/components/forms/controls/Tag/Tag.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/* Copyright (c) Fortanix, Inc.
|* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of
|* the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import type { Meta, StoryObj } from '@storybook/react';

import * as React from 'react';

import { Tag } from './Tag.tsx';


type TagArgs = React.ComponentProps<typeof Tag>;
type Story = StoryObj<TagArgs>;

export default {
component: Tag,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
},
args: {
value: 'Tag Title',
},
render: (args) => <Tag {...args}/>,
} satisfies Meta<TagArgs>;


export const TagStory: Story = {
name: 'Tag',
};
46 changes: 46 additions & 0 deletions src/components/forms/controls/Tag/Tag.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/* Copyright (c) Fortanix, Inc.
|* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of
|* the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import { classNames as cx, type ComponentProps } from '../../../../util/componentUtil.ts';
import * as React from 'react';

import { Icon } from '../../../graphics/Icon/Icon.tsx';

import cl from './Tag.module.scss';


export { cl as TagClassNames };

export type TagProps = ComponentProps<'div'> & {
/** Whether this component should be unstyled. */
unstyled?: undefined | boolean,

/** The text displayed inside the tag. */
value: string,
nighto marked this conversation as resolved.
Show resolved Hide resolved
};

/**
* A tag component, meant to be used within Input fields.
nighto marked this conversation as resolved.
Show resolved Hide resolved
*/
export const Tag = (props: TagProps) => {
const {
unstyled = false,
value = '',
...propsRest
} = props;

return (
<div
{...propsRest}
className={cx(
'bk',
{ [cl['bk-tag']]: !unstyled },
propsRest.className,
)}
>
{value}
<Icon icon="cross" className={cl['bk-tag__icon']} />
</div>
);
};
24 changes: 21 additions & 3 deletions src/components/forms/fields/InputField/InputField.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,36 @@
@layer baklava.components {
.bk-input-field {
@include bk.component-base(bk-input-field);

display: flex;
flex-direction: column;
gap: 6px;

.bk-input-field__label {
@include bk.font(bk.$font-family-body, bk.$font-weight-semibold);
cursor: default;
}

.bk-input-field__control {
--empty: ; // Prevent empty class from being removed
}
}

.bk-input-field__tags-and-input {
display: flex;
flex-direction: row;
gap: bk.$spacing-2;
}

.bk-input-field--with-tags {
border-bottom: 1px solid bk.$theme-form-rule-default;

&:focus-within {
border-bottom-color: bk.$theme-form-rule-focused;

input {
outline: none !important;
}
}
}
}
31 changes: 31 additions & 0 deletions src/components/forms/fields/InputField/InputField.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,34 @@ export const InvalidInput: Story = {
await fireEvent.submit(input.closest('form')!);
},
};

export const InputWithTags: Story = {
name: 'Input with tags (enter creates new tag, backspace erases tags)',
render: () => {
const [tags, setTags] = React.useState<Array<string>>(['Tag Title', 'Tag Title 2']);
const [inputText, setInputText] = React.useState<string>('Example');

const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInputText(e.target.value);
};
const onKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Backspace' && inputText === '') {
setTags(tags.slice(0,-1));
}
if (e.key === 'Enter' && inputText !== '') {
setTags([...tags, inputText]);
setInputText('');
}
};

return (
<InputField
tags={tags}
value={inputText}
onKeyUp={onKeyUp}
onChange={onChange}
placeholder={''}
nighto marked this conversation as resolved.
Show resolved Hide resolved
/>
);
}
};
40 changes: 27 additions & 13 deletions src/components/forms/fields/InputField/InputField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

import { classNames as cx, type ComponentProps } from '../../../../util/componentUtil.ts';
import * as React from 'react';
import { useFormStatus } from 'react-dom';

import { useFormContext } from '../../context/Form/Form.tsx';
import { Input } from '../../controls/Input/Input.tsx';
import { Tag } from '../../controls/Tag/Tag.tsx';

import cl from './InputField.module.scss';

Expand All @@ -17,15 +17,18 @@ export { cl as InputFieldClassNames };
export type InputFieldProps = ComponentProps<'input'> & {
/** Whether this component should be unstyled. */
unstyled?: undefined | boolean,

/** Label for the input. */
label?: undefined | React.ReactNode,

/** Props for the `<label>` element, if `label` is defined. */
labelProps?: undefined | ComponentProps<'label'>,

/** Props for the wrapper element. */
wrapperProps?: undefined | ComponentProps<'div'>,

/** Tags to be displayed inside the input field */
tags?: undefined | string[],
};
/**
* Input field.
Expand All @@ -36,19 +39,25 @@ export const InputField = (props: InputFieldProps) => {
label,
labelProps = {},
wrapperProps = {},
tags = [],
...inputProps
} = props;

const controlId = React.useId();
const formContext = useFormContext();
//const formStatus = useFormStatus();


const injectedInputProps = {
...inputProps,
unstyled: tags && tags.length > 0,
};

return (
<div
{...wrapperProps}
className={cx(
'bk',
{ [cl['bk-input-field']]: !unstyled },
{ [cl['bk-input-field--with-tags']]: tags && tags.length > 0 },
wrapperProps.className,
)}
>
Expand All @@ -61,12 +70,17 @@ export const InputField = (props: InputFieldProps) => {
{label}
</label>
}
<Input
{...inputProps}
id={controlId}
form={formContext.formId}
className={cx(cl['bk-input-field__control'], inputProps.className)}
/>
<div className={cl['bk-input-field__tags-and-input']}>
{tags && (
tags.map((tag, idx) => <Tag key={idx} value={tag}/>)
)}
<Input
{...injectedInputProps}
id={controlId}
form={formContext.formId}
className={cx(cl['bk-input-field__control'], inputProps.className)}
/>
</div>
</div>
);
};