Bladeren bron

Attempt to implement tag input

Tag input should derive its functionality from the fallback component.
master
TheoryOfNekomata 2 jaren geleden
bovenliggende
commit
b747abc374
5 gewijzigde bestanden met toevoegingen van 527 en 10 verwijderingen
  1. +1
    -0
      docs/todo.md
  2. +327
    -0
      src/modules/option/components/TagInput/index.tsx
  3. +1
    -0
      src/modules/option/index.ts
  4. +1
    -0
      src/modules/option/web-option-react.test.ts
  5. +197
    -10
      src/pages/categories/option/index.tsx

+ 1
- 0
docs/todo.md Bestand weergeven

@@ -15,6 +15,7 @@

# Formatted

- [ ] `SuggestionInput`
- [ ] `EmailInput`
- [ ] `TelInput`
- [ ] `UrlInput`


+ 327
- 0
src/modules/option/components/TagInput/index.tsx Bestand weergeven

@@ -0,0 +1,327 @@
import * as React from 'react';
import * as TextControlBase from '@tesseract-design/web-base-textcontrol';
import * as BadgeBase from '@tesseract-design/web-base-badge';

export type TagInputProps = Omit<React.HTMLProps<HTMLInputElement>, 'size' | 'style'> & {
/**
* Short textual description indicating the nature of the component's value.
*/
label?: React.ReactNode,
/**
* Short textual description as guidelines for valid input values.
*/
hint?: React.ReactNode,
/**
* Size of the component.
*/
size?: TextControlBase.TextControlSize,
/**
* Additional description, usually graphical, indicating the nature of the component's value.
*/
indicator?: React.ReactNode,
/**
* Should the component display a border?
*/
border?: boolean,
/**
* Should the component occupy the whole width of its parent?
*/
block?: boolean,
/**
* Style of the component.
*/
style?: TextControlBase.TextControlStyle,
/**
* Is the label hidden?
*/
hiddenLabel?: boolean,
enhanced?: boolean,
separator?: string,
}

export const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(((
{
label = '',
hint = '',
indicator = null,
size = TextControlBase.TextControlSize.MEDIUM,
border = false,
block = false,
style = TextControlBase.TextControlStyle.DEFAULT,
hiddenLabel = false,
onInput,
enhanced = false,
defaultValue = '',
separator = ',',
onFocus,
onBlur,
onSelect,
className: _className,
placeholder: _placeholder,
as: _as,
...etcProps
}: TagInputProps,
forwardedRef,
) => {
const [hydrated, setHydrated] = React.useState(false);
const [focused, setFocused] = React.useState(false);
const [selectionStart, setSelectionStart] = React.useState(0);
const [selectionEnd, setSelectionEnd] = React.useState(0);
const [viewValue, setViewValue] = React.useState<string[]>(() => {
const theDefaultValue = !Array.isArray(defaultValue) ? [defaultValue.toString(), ''] : [...defaultValue, ''];
return theDefaultValue.filter(v => v.length > 0);
});
const defaultRef = React.useRef<HTMLInputElement>(null);
const effectiveRef = forwardedRef ?? defaultRef;

const styleArgs = React.useMemo<TextControlBase.TextControlBaseArgs>(() => ({
block,
border,
size,
indicator: Boolean(indicator),
style,
resizable: true,
predefinedValues: false,
}), [block, border, size, indicator, style]);

const renderEnhanced = React.useMemo(() => enhanced && hydrated, [enhanced, hydrated]);

const handleInput: React.FormEventHandler<HTMLInputElement> = (e) => {
const target = e.target as HTMLInputElement;
setViewValue(target.value.split(separator))

if (onInput) {
onInput(e)
}
}

const handleFocus: React.FocusEventHandler<HTMLInputElement> = (e) => {
setFocused(true);
if (onFocus) {
onFocus(e);
}
}

const handleBlur: React.FocusEventHandler<HTMLInputElement> = (e) => {
setFocused(false);
if (onBlur) {
onBlur(e);
}
}

const handleSelect: React.ReactEventHandler<HTMLInputElement> = (e) => {
const target = e.target as HTMLInputElement;
const newSelectionStart = target.selectionStart ?? 0;
const newSelectionEnd = target.selectionEnd ?? 0;
const newDirection = Math.sign(newSelectionStart - selectionStart) > 0 ? 'forward' : 'backward'
setSelectionStart(newSelectionStart);
setSelectionEnd(newSelectionEnd);

console.log(newDirection);

const lastSeparatorIndex = target.value.lastIndexOf(separator);
const separatorStartRaw = newSelectionStart > lastSeparatorIndex ? newSelectionStart : target.value.slice(0, newSelectionEnd).lastIndexOf(separator) + separator?.length;
const separatorEndRaw = newSelectionEnd > lastSeparatorIndex ? newSelectionEnd : target.value.slice(0, newSelectionEnd).length + target.value.slice(newSelectionEnd).indexOf(separator);

let separatorStart = 0;
let separatorEnd;
if (lastSeparatorIndex > -1) {
separatorStart = separatorStartRaw > -1 ? separatorStartRaw : 0;
}
separatorEnd = separatorEndRaw;
if (newSelectionStart <= target.value.lastIndexOf(separator)) {
if (newSelectionStart === newSelectionEnd && newSelectionStart === separatorStart && newDirection === 'backward') {
target.selectionStart = newSelectionStart - separator?.length;
target.selectionEnd = newSelectionStart - separator?.length;
target.selectionDirection = newDirection;
} else if (newSelectionStart === newSelectionEnd && newSelectionEnd === separatorEnd && newDirection === 'forward') {
target.selectionStart = newSelectionEnd + separator?.length;
target.selectionEnd = newSelectionEnd + separator?.length;
target.selectionDirection = newDirection;
} else {
target.selectionStart = separatorStart;
target.selectionEnd = separatorEnd;
target.selectionDirection = 'backward';
}
}
if (onSelect) {
onSelect(e);
}
}

const focusOnInput: React.MouseEventHandler<HTMLDivElement> = (e) => {
e.preventDefault();
if (typeof effectiveRef === 'function') {
return;
}
if (effectiveRef !== null && effectiveRef.current) {
effectiveRef.current.focus();
}
}

const tags = React.useMemo(() => viewValue.slice(0, -1), [viewValue])
const inputText = React.useMemo(() => viewValue.slice(-1)[0] ?? '', [viewValue])

React.useEffect(() => {
setHydrated(true)
}, []);

return (
<div
className={TextControlBase.Root(styleArgs)}
>
<input
{...etcProps}
className={TextControlBase.Input(styleArgs)}
ref={effectiveRef}
aria-label={label}
style={{
height: TextControlBase.MIN_HEIGHTS[size],
// position: renderEnhanced ? 'absolute' : undefined,
// left: renderEnhanced ? -999999 : undefined,
}}
data-testid="input"
onInput={handleInput}
onFocus={handleFocus}
onBlur={handleBlur}
onSelect={handleSelect}
/>
<div
className={TextControlBase.Input(styleArgs)}
onClick={focusOnInput}
style={{
cursor: 'text',
}}
>
<div
style={{
margin: '-0.125rem',
}}
>
{tags.map(v => (
<div
style={{
padding: '0.125rem',
display: 'inline-block',
}}
key={v}
>
<button
className={BadgeBase.Root({ rounded: false })}
style={{
border: 0,
font: 'inherit',
lineHeight: 0,
paddingTop: 0,
paddingBottom: 0,
color: 'inherit',
backgroundColor: 'transparent',
}}
>
<div
className={BadgeBase.Content()}
>
{v}
{' '}
&times;
</div>
</button>
</div>
))}
{
inputText.lastIndexOf(separator) < 0
&& (
<>
{
inputText.slice(0, selectionStart - tags.join(separator).length - separator?.length)
}
<div
style={{
display: 'inline-block',
verticalAlign: 'middle',
backgroundColor: focused ? 'Highlight' : undefined,
color: focused ? 'HighlightText' : undefined,
height: '1.25em',
minWidth: 1,
}}
>
{
inputText.slice(selectionStart - tags.join(separator).length - separator?.length, selectionEnd - tags.join(separator).length - separator?.length)
}
</div>
{
inputText.slice(selectionEnd - tags.join(separator).length - separator?.length)
}
</>
)
}
{
inputText.lastIndexOf(separator) >= 0
&& (
<>
{
inputText.slice(0, selectionStart - tags.join(separator).length)
}
<div
style={{
display: 'inline-block',
verticalAlign: 'middle',
backgroundColor: focused ? 'Highlight' : undefined,
color: focused ? 'HighlightText' : undefined,
height: '1.25em',
minWidth: 1,
}}
>
{
inputText.slice(selectionStart - tags.join(separator).length - 1, selectionEnd - tags.join(separator).length - 1)
}
</div>
{
inputText.slice(selectionEnd - tags.join(separator).length - 1)
}
</>
)
}
</div>
</div>
{
border && (
<span
data-testid="border"
/>
)
}
{
label && !hiddenLabel && (
<div
data-testid="label"
className={TextControlBase.LabelWrapper(styleArgs)}
>
{label}
</div>
)
}
{hint && (
<div
className={TextControlBase.HintWrapper(styleArgs)}
data-testid="hint"
>
<div
className={TextControlBase.Hint()}
>
{hint}
</div>
</div>
)}
{indicator && (
<div
className={TextControlBase.IndicatorWrapper(styleArgs)}
>
{indicator}
</div>
)}
</div>
);
}));

TagInput.displayName = 'TagInput';

+ 1
- 0
src/modules/option/index.ts Bestand weergeven

@@ -2,6 +2,7 @@ export * from './components/DropdownSelect';
export * from './components/MenuSelect';
export * from './components/RadioButton';
export * from './components/RadioTickBox';
export * from './components/TagInput';
export * from './components/ToggleButton';
export * from './components/ToggleSwitch';
export * from './components/ToggleTickBox';

+ 1
- 0
src/modules/option/web-option-react.test.ts Bestand weergeven

@@ -6,6 +6,7 @@ describe('web-option-react', () => {
'MenuSelect',
'RadioButton',
'RadioTickBox',
'TagInput',
'ToggleButton',
'ToggleSwitch',
'ToggleTickBox',


+ 197
- 10
src/pages/categories/option/index.tsx Bestand weergeven

@@ -872,6 +872,203 @@ const OptionPage: NextPage<Props> = ({
</div>
</div>
</section>
<section>
<div className="container mx-auto px-4">
<h1>
TagInput
</h1>
<div>
<section>
<h2>
Default
</h2>
<div>
<div className="grid md:grid-cols-2 gap-4">
<div>
<Option.TagInput
enhanced
size={TextControlSize.SMALL}
label="TagInput"
hint="Type anything here&hellip;"
indicator="A"
block
/>
</div>
<div>
<Option.TagInput
enhanced
border
size={TextControlSize.SMALL}
label="TagInput"
hint="Type anything here&hellip;"
indicator="A"
block
/>
</div>
<div>
<Option.TagInput
enhanced
label="TagInput"
hint="Type anything here&hellip;"
indicator="A"
block
/>
</div>
<div>
<Option.TagInput
enhanced
border
label="TagInput"
hint="Type anything here&hellip;"
indicator="A"
block
/>
</div>
<div>
<Option.TagInput
enhanced
size={TextControlSize.LARGE}
label="TagInput"
hint="Type anything here&hellip;"
indicator="A"
block
/>
</div>
<div>
<Option.TagInput
enhanced
border
size={TextControlSize.LARGE}
label="TagInput"
hint="Type anything here&hellip;"
indicator="A"
block
/>
</div>
<div>
<Option.TagInput
enhanced
label="TagInput"
hint="Type anything here&hellip;"
indicator="A"
block
disabled
/>
</div>
<div>
<Option.TagInput
enhanced
border
label="TagInput"
hint="Type anything here&hellip;"
indicator="A"
block
disabled
/>
</div>
</div>
</div>
</section>
<section>
<h2>Alternate</h2>
<div>
<div className="grid md:grid-cols-2 gap-4">
<div>
<Option.TagInput
enhanced
style={TextControlStyle.ALTERNATE}
size={TextControlSize.SMALL}
label="TagInput"
hint="Type anything here&hellip;"
indicator="A"
block
/>
</div>
<div>
<Option.TagInput
enhanced
border
style={TextControlStyle.ALTERNATE}
size={TextControlSize.SMALL}
label="TagInput"
hint="Type anything here&hellip;"
indicator="A"
block
/>
</div>
<div>
<Option.TagInput
enhanced
style={TextControlStyle.ALTERNATE}
label="TagInput"
hint="Type anything here&hellip;"
indicator="A"
block
/>
</div>
<div>
<Option.TagInput
enhanced
border
style={TextControlStyle.ALTERNATE}
label="TagInput"
hint="Type anything here&hellip;"
indicator="A"
block
/>
</div>
<div>
<Option.TagInput
enhanced
style={TextControlStyle.ALTERNATE}
size={TextControlSize.LARGE}
label="TagInput"
hint="Type anything here&hellip;"
indicator="A"
block
/>
</div>
<div>
<Option.TagInput
enhanced
border
block
style={TextControlStyle.ALTERNATE}
size={TextControlSize.LARGE}
label="TagInput"
hint="Type anything here&hellip;"
indicator="A"
/>
</div>
<div>
<Option.TagInput
enhanced
style={TextControlStyle.ALTERNATE}
label="TagInput"
hint="Type anything here&hellip;"
indicator="A"
block
disabled
/>
</div>
<div>
<Option.TagInput
enhanced
style={TextControlStyle.ALTERNATE}
border
label="TagInput"
hint="Type anything here&hellip;"
indicator="A"
block
disabled
/>
</div>
</div>
</div>
</section>
</div>
</div>
</section>
<section>
<div className="container mx-auto px-4">
<h1>
@@ -1211,16 +1408,6 @@ const OptionPage: NextPage<Props> = ({
</div>
</div>
</section>
<section>
<div className="container mx-auto px-4">
<h1>
TagList
</h1>
<div>
TODO
</div>
</div>
</section>
<section>
<div className="container mx-auto px-4">
<h1>


Laden…
Annuleren
Opslaan