Skip to content

Commit

Permalink
Merge pull request #19 from ryu-man/feat/feaild-comp
Browse files Browse the repository at this point in the history
feat: define new component `Field`
  • Loading branch information
ryu-man committed Jul 23, 2023
2 parents 6afbc36 + 4c51ce1 commit a85069b
Show file tree
Hide file tree
Showing 12 changed files with 333 additions and 54 deletions.
1 change: 1 addition & 0 deletions packages/core/src/context/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './sharedContext'
31 changes: 31 additions & 0 deletions packages/core/src/context/sharedContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { getContext, hasContext, setContext } from 'svelte';
import { type Readable, type Writable, derived, get, readonly, writable } from 'svelte/store';

export const SVELTE_FUI_SHARED_CONTEXT_KEY = 'svelte-fui-shared-context-key';

export type SharedContext<T> = Record<string, Record<string, unknown>> & T;

export function getSharedContext<T>(key?: 'input' | 'label'): Readable<SharedContext<T>> {
const context$ = getContext(SVELTE_FUI_SHARED_CONTEXT_KEY) as Writable<SharedContext<T>>;

if (!context$) {
return readonly(writable({} as SharedContext<T>));
}

if (key) {
return derived(context$, (val) => val[key] as SharedContext<T>);
}

return readonly(context$);
}

export function setSharedContext<T>(context: SharedContext<T>): Writable<SharedContext<T>> {
if (hasContext(SVELTE_FUI_SHARED_CONTEXT_KEY)) {
// extends and overrides the existent context
const existedContext = get(getSharedContext());

setContext(SVELTE_FUI_SHARED_CONTEXT_KEY, writable(structuredClone({ ...existedContext, ...context })));
}

return setContext(SVELTE_FUI_SHARED_CONTEXT_KEY, writable(structuredClone(context)));
}
88 changes: 88 additions & 0 deletions packages/core/src/field/Field.stories.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<script lang="ts">
import { Meta, Story } from '@storybook/addon-svelte-csf';
import type { ArgTypes } from '@storybook/svelte';
import { App, Input } from '@svelte-fui/core';
import { AddCircleFilled } from '@svelte-fui/icons';
import { webDarkTheme, webLightTheme } from '@svelte-fui/themes';
import { onMount } from 'svelte';
import Field from './Field.svelte';
import ValidationMessage from './ValidationMessage.svelte';
const defaultValues = {
size: 'md',
orientation: 'vertical',
};
const argTypes = {
label: {
type: 'string'
},
size: {
type: 'string',
options: ['sm', 'md', 'lg'],
control: 'select'
},
orientation: {
type: 'string',
options: ['horizontal', 'vertical'],
control: 'select'
},
state: {
type: 'string',
options: ['none', 'success', 'warning', 'error'],
control: 'select'
}
} satisfies ArgTypes;
let theme = webLightTheme;
onMount(() => {
function handler(schemeMedia: MediaQueryListEvent) {
theme = schemeMedia.matches ? webLightTheme : webDarkTheme;
}
const schemeMedia = matchMedia('(prefers-color-scheme: light)');
schemeMedia.addEventListener('change', handler);
theme = schemeMedia.matches ? webLightTheme : webDarkTheme;
return () => {
schemeMedia.removeEventListener('change', handler);
};
});
</script>

<Meta title="Components/Field" component={Field} {argTypes} />

<Story id="field" name="Field" args={defaultValues} let:args>
<App {theme}>
<div class="flex h-full w-full flex-col items-center justify-center gap-4">
<div class="flex w-[90%] flex-col gap-4">
<Field {...args} label="Example Field" state="error">
<Input />
<ValidationMessage>This an error message</ValidationMessage>
</Field>

<Field {...args} label="Example Field" state="warning">
<Input />
<ValidationMessage>This a warning message</ValidationMessage>
</Field>

<Field {...args} label="Example Field" state="success">
<Input />
<ValidationMessage>This a success message</ValidationMessage>
</Field>

<Field {...args} label="Example Field" state="none">
<Input />
<ValidationMessage>This a simple message</ValidationMessage>
</Field>
<Field {...args} label="Example Field" state="none">
<Input />
<ValidationMessage icon={AddCircleFilled}>This a simple message</ValidationMessage>
</Field>
</div>
</div>
</App>
</Story>
69 changes: 69 additions & 0 deletions packages/core/src/field/Field.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<script lang="ts">
import { CheckmarkCircleFilled, ErrorCircleFilled, WarningFilled } from '@svelte-fui/icons';
import { setSharedContext } from '../context';
import { classnames } from '../internal';
import { Label } from '../label';
import ValidationMessage from './ValidationMessage.svelte';
import { setFieldContext } from './context';
import type { State } from './types,';
export let label: string = '';
export let orientation: 'horizontal' | 'vertical' = 'vertical';
export let size: 'sm' | 'md' | 'lg' = 'md';
export let validationMessage = '';
export let state: State = validationMessage ? 'error' : 'none';
const validationMessageIcons = {
error: ErrorCircleFilled,
warning: WarningFilled,
success: CheckmarkCircleFilled,
none: undefined
};
const sharedContext$ = setSharedContext({ input: { invalid: state === 'error', size }, label: { size } });
$: sharedContext$.set({ input: { invalid: state === 'error', size }, label: { size } });
const { icon$, state$ } = setFieldContext({ state, icon: validationMessageIcons[state || 'none'] });
$: icon$.set(validationMessageIcons[state || 'none']);
$: state$.set(state);
</script>

<div class={classnames('fui-field', orientation, state, size, { 'no-label': !label })}>
<Label>{label}</Label>
<slot />
</div>

<style lang="postcss">
.fui-field {
display: grid;
&.horizontal {
grid-template-columns: 33% 1fr;
grid-template-rows: auto auto auto 1fr;
&.no-label {
padding-left: 33%;
grid-template-columns: 1fr;
}
}
}
.fui-field :global(.fui-label) {
@apply py-xxs;
}
.fui-field :global(.fui-label.lg) {
@apply py-[1px];
}
.fui-field.vertical :global(.fui-label) {
@apply mb-xxs;
}
.fui-field.vertical :global(.fui-label.lg) {
@apply pb-xs;
}
.fui-field.horizontal :global(.fui-label) {
@apply mr-m;
grid-row-start: 1;
grid-row-end: -1;
}
</style>
65 changes: 65 additions & 0 deletions packages/core/src/field/ValidationMessage.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<script lang="ts">
import type { ComponentType } from 'svelte/internal';
import { Icon } from '../icon';
import { classnames } from '../internal';
import { getFieldContext } from './context';
const { state$, icon$ } = getFieldContext();
export let icon: ComponentType | undefined = $icon$;
export let id: string | undefined = undefined;
let klass = '';
export { klass as class };
let iconSize = '12px';
</script>

<div
{id}
class={classnames('fui-field-validation-message', $state$, { icon: !!icon, 'secondary-text': !!$$slots.default }, klass)}
style:--icon-size={iconSize}
>
{#if icon}
<!-- content here -->
<Icon class={classnames('fui-field-validation-message-icon', $state$)} src={icon} size={iconSize} ariaHidden />
{/if}
<slot><!-- optional fallback --></slot>
</div>

<style lang="postcss">
.fui-field-validation-message {
@apply font-regular text-base-200 flex items-center text-left;
}
.fui-field-validation-message.secondary-text {
@apply mt-xxs text-neutral-foreground-3 caption-1;
&.error {
@apply text-palette-red-foreground-1;
}
&.icon {
/* Add a gutter for the icon, to allow multiple lines of text to line up to the right of the icon. */
padding-left: calc(var(--icon-size) + theme(spacing.xs));
}
}
.fui-field-validation-message :global(.fui-field-validation-message-icon) {
@apply inline-block;
font-size: var(--icon-size);
margin-left: calc(-1 * var(--icon-size) - theme(spacing.xs));
margin-right: theme(spacing.xs);
line-height: 0;
vertical-align: -1px;
}
.fui-field-validation-message :global(.fui-field-validation-message-icon.error) {
@apply !text-palette-red-foreground-1;
}
.fui-field-validation-message :global(.fui-field-validation-message-icon.warning) {
@apply !text-palette-dark-orange-foreground-1;
}
.fui-field-validation-message :global(.fui-field-validation-message-icon.success) {
@apply !text-palette-green-foreground-1;
}
</style>
24 changes: 24 additions & 0 deletions packages/core/src/field/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { getContext, setContext } from 'svelte';
import type { ComponentType } from 'svelte/internal';
import { type Writable, writable } from 'svelte/store';
import type { State } from './types,';

export const SVELTE_FUI_FIELD_CONTEXT_KEY = 'svelte-fui-field-context-key';

export type FieldContext = {
state$: Writable<State>;
icon$: Writable<ComponentType | undefined>;
};

export function getFieldContext(): FieldContext {
return getContext(SVELTE_FUI_FIELD_CONTEXT_KEY);
}

export function setFieldContext(values: { state: State; icon: ComponentType | undefined }) {
const context: FieldContext = {
state$: writable(values.state),
icon$: writable(values.icon)
};

return setContext(SVELTE_FUI_FIELD_CONTEXT_KEY, context);
}
2 changes: 2 additions & 0 deletions packages/core/src/field/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as Field } from './Field.svelte';
export { default as ValidationMessage } from './ValidationMessage.svelte'
1 change: 1 addition & 0 deletions packages/core/src/field/types,.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type State = 'none' | 'error' | 'warning' | 'success' | undefined;
35 changes: 14 additions & 21 deletions packages/core/src/input/Input.svelte
Original file line number Diff line number Diff line change
@@ -1,23 +1,14 @@
<script lang="ts">
import { getSharedContext } from '../context';
import { classnames } from '../internal';
type InputEvent = Event & {
currentTarget: EventTarget & HTMLInputElement;
};
type InputTypes =
| 'text'
| 'number'
| 'search'
| 'password'
| 'email'
| 'tel'
| 'url'
| 'date'
| 'datetime-local'
| 'month'
| 'time'
| 'week';
type InputTypes = 'text' | 'number' | 'search' | 'password' | 'email' | 'tel' | 'url' | 'date' | 'datetime-local' | 'month' | 'time' | 'week';
const sharedContext$ = getSharedContext<{ invalid: boolean; size: 'sm' | 'md' | 'lg' }>('input') || {};
/** The input's current value. */
export let value: any = '';
Expand All @@ -36,14 +27,17 @@
/** Controls whether the TextBox is intended for user interaction, and styles it accordingly. */
export let disabled = false;
export let size: 'sm' | 'md' | 'lg' = 'md';
export let size: 'sm' | 'md' | 'lg' = $sharedContext$.size || 'md';
export let underline = false;
export let name : string | undefined = undefined;
export let name: string | undefined = undefined;
export let ariaLabel: string | undefined = undefined;
export let ariaDescribedby: string | undefined = undefined;
export let ariaInvalid = $sharedContext$.invalid || false;
/** Specifies a custom class name for the TextBox container. */
let klass = '';
export { klass as class };
Expand All @@ -61,11 +55,7 @@
}
</script>

<span
class={classnames('fui-input', { size, underline, disabled }, klass)}
{id}

>
<span class={classnames('fui-input', { size, underline, disabled, invalid: ariaInvalid && !disabled }, klass)} {id}>
<slot name="before" />
<input
use:setInputType={type}
Expand All @@ -76,6 +66,8 @@
{disabled}
{readonly}
aria-label={ariaLabel}
aria-describedby={ariaDescribedby}
aria-invalid={ariaInvalid}
on:input={onInputHandler}
on:input
on:blur
Expand Down Expand Up @@ -234,7 +226,8 @@
/* ...shorthands.borderColor(tokens.colorNeutralStrokeDisabled); */
@media (forced-colors: active) {
color: graytext;
/* color: graytext; */
border-color: graytext;
/* ...shorthands.borderColor('GrayText'); */
}
Expand Down
9 changes: 6 additions & 3 deletions packages/core/src/label/Label.svelte
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
<script lang="ts">
import { getSharedContext } from '../context';
import { classnames } from '../internal';
const sharedContext$ = getSharedContext<{ size: 'sm' | 'md' | 'lg' }>('label') || {};
export let disabled = false;
export let required = false;
export let size: 'sm' | 'md' | 'lg' = 'md';
export let size: 'sm' | 'md' | 'lg' = $sharedContext$.size || 'md';
export let weight: 'regular' | 'semibold' = 'regular';
let klass = ''
export { klass as class }
let klass = '';
export { klass as class };
</script>

<label class={classnames('fui-label', { disabled, required }, size, weight, klass)} {...$$restProps}>
Expand Down
Loading

1 comment on commit a85069b

@vercel
Copy link

@vercel vercel bot commented on a85069b Jul 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

svelte-fui – ./

svelte-fui-ryu-man.vercel.app
svelte-fui.vercel.app
svelte-fui-git-main-ryu-man.vercel.app

Please sign in to comment.