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 all 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
4 changes: 4 additions & 0 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ const preview = {
'Icon',
'Spinner',
],
'text',
[
'Tag',
],
'containers',
[
'Panel',
Expand Down
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
6 changes: 3 additions & 3 deletions src/components/forms/fields/InputField/InputField.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@
@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
}
Expand Down
7 changes: 3 additions & 4 deletions src/components/forms/fields/InputField/InputField.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,7 @@ export default {
render: (args) => <InputField {...args}/>,
} satisfies Meta<InputArgs>;


export const Standard: Story = {
};
export const Standard: Story = {};

export const InvalidInput: Story = {
args: {
Expand All @@ -52,6 +50,7 @@ export const InvalidInput: Story = {
await userEvent.type(input, 'invalid');
await delay(100);
await userEvent.keyboard('{Enter}');
// biome-ignore lint/style/noNonNullAssertion: we know there is a form on this story
await fireEvent.submit(input.closest('form')!);
},
},
};
15 changes: 7 additions & 8 deletions src/components/forms/fields/InputField/InputField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,26 @@

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 '../../../text/Tag/Tag.tsx';

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


export { cl as InputFieldClassNames };

export type InputFieldProps = ComponentProps<'input'> & {
export type InputFieldProps = Omit<ComponentProps<'input'>, 'value'> & {
/** 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'>,
};
Expand All @@ -38,11 +38,10 @@ export const InputField = (props: InputFieldProps) => {
wrapperProps = {},
...inputProps
} = props;

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


return (
<div
{...wrapperProps}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/* 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-input-field-with-tags {
@include bk.component-base(input-field-with-tags);

display: flex;
flex-direction: column;
gap: 6px;
padding-bottom: 1px;
border-bottom: 1px solid bk.$theme-form-rule-default;

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

.bk-input-field-with-tags__control {
--empty: ; // Prevent empty class from being removed
}

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

input {
outline: none !important;
}
}
}

.bk-input-field-with-tags__container {
display: flex;
flex-flow: row wrap;
gap: bk.$spacing-2;
}

.bk-input-field-with-tags__input-container {
flex-grow: 1;

input {
width: 100%;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/* 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 { Form } from '../../context/Form/Form.tsx';
import { Card } from '../../../containers/Card/Card.tsx';

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


type InputArgs = React.ComponentProps<typeof InputFieldWithTags>;
type Story = StoryObj<InputArgs>;

export default {
component: InputFieldWithTags,
parameters: {
layout: 'centered',
design: {
type: 'figma',
url: 'https://www.figma.com/design/ymWCnsGfIsC2zCz17Ur11Z/Design-System-UX?node-id=3606-101183&node-type=instance&m=dev',
}
},
tags: ['autodocs'],
argTypes: {
},
args: {
label: 'Test',
placeholder: 'Example',
},
decorators: [
Story => <Form><Story/></Form>,
],
} satisfies Meta<InputArgs>;

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 handleUpdate = (newInputText: string) => {
setInputText(newInputText);
};
const handleUpdateTags = (newTags: string[]) => {
setTags(newTags);
};

return (
<Card>
<InputFieldWithTags
tags={tags}
value={inputText}
onUpdate={handleUpdate}
onUpdateTags={handleUpdateTags}
placeholder=""
/>
</Card>
);
}
};
133 changes: 133 additions & 0 deletions src/components/forms/fields/InputFieldWithTags/InputFieldWithTags.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/* 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 { useFormContext } from '../../context/Form/Form.tsx';
import { Input } from '../../controls/Input/Input.tsx';
import { Tag } from '../../../text/Tag/Tag.tsx';

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


export { cl as InputFieldWithTagsClassNames };

export type InputFieldWithTagsProps = Omit<ComponentProps<'input'>, 'value'> & {
/** 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'>,

/** Value of the input field */
value?: undefined | string,

/** Tags to be displayed inside the input field */
tags?: undefined | string[],

/** Callback to update the input value. Internally hooks to onChange */
onUpdate?: undefined | ((value: string) => void),

/** Callback to update the tags. Internally hooks to onKeyDown */
onUpdateTags?: undefined | ((tags: string[]) => void),
};
/**
* Input field with tags. Enter creates a new tag, backspace erases last tag.
*/
export const InputFieldWithTags = (props: InputFieldWithTagsProps) => {
const {
unstyled = false,
label,
labelProps = {},
wrapperProps = {},
value = '',
tags = [],
onUpdate,
onUpdateTags,
...inputProps
} = props;

const controlId = React.useId();
const formContext = useFormContext();

const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// first handle supplied onChange, if exists
if (inputProps.onChange) {
inputProps.onChange(e);
}
// then return value to onUpdate
if (onUpdate) {
onUpdate(e.target.value);
}
};

const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
// first handle supplied onKeyDown, if exists
if (inputProps.onKeyDown) {
inputProps.onKeyDown(e);
}
// then return value to onUpdateTags
if (onUpdateTags && onUpdate) {
if (e.key === 'Backspace' && value === '') {
onUpdateTags(tags.slice(0,-1));
}
if (e.key === 'Enter' && value !== '') {
onUpdateTags([...tags, value.trim()]);
onUpdate('');
}
}
};

const onRemoveTag = (index: number) => {
if (onUpdateTags) {
onUpdateTags(tags.filter((_, idx) => idx !== index));
}
};

return (
<div
{...wrapperProps}
className={cx(
'bk',
{ [cl['bk-input-field-with-tags']]: !unstyled },
wrapperProps.className,
)}
>
{label &&
<label
htmlFor={controlId}
{...labelProps}
className={cx(cl['bk-input-field-with-tags__label'], labelProps.className)}
>
{label}
</label>
}
<div className={cl['bk-input-field-with-tags__container']}>
{tags && (
// biome-ignore lint/suspicious/noArrayIndexKey: no other unique identifier available
tags.map((tag, idx) => <Tag key={idx} content={tag} onRemove={() => onRemoveTag(idx)}/>)
)}
<div className={cl['bk-input-field-with-tags__input-container']}>
<Input
{...inputProps}
unstyled={true}
id={controlId}
form={formContext.formId}
className={cx(cl['bk-input-field-with-tags__control'], inputProps.className)}
onChange={onChange}
onKeyDown={onKeyDown}
value={value}
/>
</div>
</div>
</div>
);
};
33 changes: 33 additions & 0 deletions src/components/text/Tag/Tag.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/* 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: bk.$size-2;
color: bk.$theme-tag-text-default;
display: flex;
align-items: center;
font-size: bk.$font-size-xs;
padding: bk.$size-2 bk.$spacing-2 bk.$size-3;

&.bk-tag--with-close-button {
padding-right: 0;
}

.bk-tag__icon {
--icon-size: 7px;

width: var(--icon-size);
height: var(--icon-size);
color: bk.$theme-tag-icon-default;
cursor: pointer;
padding: bk.$size-2 bk.$spacing-2 bk.$size-2 bk.$spacing-2;
}
}
}
Loading