@bspk/ui 1.3.10 → 1.3.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,16 +1,15 @@
1
1
  import './chip-group.scss';
2
- import { ReactNode } from 'react';
3
2
  import { ChipProps } from '-/components/Chip';
4
- export type ChipGroupItem = Pick<ChipProps, 'disabled' | 'flat' | 'label' | 'leadingIcon' | 'onClick' | 'selected' | 'trailingBadge' | 'trailingIcon'>;
5
3
  export type ChipGroupProps = {
6
4
  /**
7
- * To allow chips to wrap. If set to false chips will scroll.
5
+ * Controls the overflow behavior of the chip group. If set to `scroll`, the chip group will be scrollable
6
+ * horizontally. If set to `wrap`, the chip group will wrap to multiple lines as needed.
8
7
  *
9
- * @default true
8
+ * @default wrap
10
9
  */
11
- wrap?: boolean;
12
- /** Only Chip components should be used as children. */
13
- children?: ReactNode;
10
+ overflow?: 'scroll' | 'wrap';
11
+ /** Only Chip components should be used as items. */
12
+ items?: ChipProps[];
14
13
  };
15
14
  /**
16
15
  * A component that manages the layout of a group of chips.
@@ -19,29 +18,17 @@ export type ChipGroupProps = {
19
18
  * import { Chip } from '@bspk/ui/Chip';
20
19
  * import { ChipGroup } from '@bspk/ui/ChipGroup';
21
20
  *
22
- * <ChipGroup wrap={false}>
23
- * <Chip
24
- * label="chip 1"
25
- * leadingIcon={<SvgLightbulb />}
26
- * onClick={() => action('Chip clicked!')}
27
- * trailingIcon={<SvgChevronRight />}
28
- * />
29
- * <Chip
30
- * label="chip 2"
31
- * leadingIcon={<SvgIcecream />}
32
- * onClick={() => action('Chip clicked!')}
33
- * trailingIcon={<SvgChevronRight />}
34
- * />
35
- * <Chip
36
- * label="chip 3"
37
- * leadingIcon={<SvgSignLanguage />}
38
- * onClick={() => action('Chip clicked!')}
39
- * trailingIcon={<SvgClose />}
40
- * />
41
- * </ChipGroup>;
21
+ * <ChipGroup
22
+ * overflow="scroll"
23
+ * items={[
24
+ * { label: 'chip 1', leadingIcon: <SvgLightbulb />, onClick: () => {}, trailingIcon: <SvgChevronRight /> },
25
+ * { label: 'chip 2', leadingIcon: <SvgIcecream />, onClick: () => {}, trailingIcon: <SvgChevronRight /> },
26
+ * { label: 'chip 3', leadingIcon: <SvgSignLanguage />, onClick: () => {}, trailingIcon: <SvgClose /> },
27
+ * ]}
28
+ * />;
42
29
  *
43
30
  * @name ChipGroup
44
31
  * @phase UXReview
45
32
  */
46
- export declare function ChipGroup({ children, wrap }: ChipGroupProps): import("react/jsx-runtime").JSX.Element;
33
+ export declare function ChipGroup({ overflow, items }: ChipGroupProps): import("react/jsx-runtime").JSX.Element;
47
34
  /** Copyright 2025 Anywhere Real Estate - CC BY 4.0 */
@@ -1,5 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createElement as _createElement } from "react";
2
3
  import './chip-group.css.js';
4
+ import { Chip } from '../Chip';
3
5
  /**
4
6
  * A component that manages the layout of a group of chips.
5
7
  *
@@ -7,31 +9,19 @@ import './chip-group.css.js';
7
9
  * import { Chip } from '@bspk/ui/Chip';
8
10
  * import { ChipGroup } from '@bspk/ui/ChipGroup';
9
11
  *
10
- * <ChipGroup wrap={false}>
11
- * <Chip
12
- * label="chip 1"
13
- * leadingIcon={<SvgLightbulb />}
14
- * onClick={() => action('Chip clicked!')}
15
- * trailingIcon={<SvgChevronRight />}
16
- * />
17
- * <Chip
18
- * label="chip 2"
19
- * leadingIcon={<SvgIcecream />}
20
- * onClick={() => action('Chip clicked!')}
21
- * trailingIcon={<SvgChevronRight />}
22
- * />
23
- * <Chip
24
- * label="chip 3"
25
- * leadingIcon={<SvgSignLanguage />}
26
- * onClick={() => action('Chip clicked!')}
27
- * trailingIcon={<SvgClose />}
28
- * />
29
- * </ChipGroup>;
12
+ * <ChipGroup
13
+ * overflow="scroll"
14
+ * items={[
15
+ * { label: 'chip 1', leadingIcon: <SvgLightbulb />, onClick: () => {}, trailingIcon: <SvgChevronRight /> },
16
+ * { label: 'chip 2', leadingIcon: <SvgIcecream />, onClick: () => {}, trailingIcon: <SvgChevronRight /> },
17
+ * { label: 'chip 3', leadingIcon: <SvgSignLanguage />, onClick: () => {}, trailingIcon: <SvgClose /> },
18
+ * ]}
19
+ * />;
30
20
  *
31
21
  * @name ChipGroup
32
22
  * @phase UXReview
33
23
  */
34
- export function ChipGroup({ children, wrap = true }) {
35
- return (_jsx("div", { "data-bspk": "chip-group", "data-wrap": wrap || undefined, children: children }));
24
+ export function ChipGroup({ overflow = 'wrap', items }) {
25
+ return (_jsx("div", { "data-bspk": "chip-group", "data-scroll": overflow === 'scroll' || undefined, children: items?.length ? items.map((item, idx) => _createElement(Chip, { ...item, key: item.label ?? idx })) : null }));
36
26
  }
37
27
  //# sourceMappingURL=ChipGroup.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ChipGroup.js","sourceRoot":"","sources":["../../../src/components/ChipGroup/ChipGroup.tsx"],"names":[],"mappings":";AAAA,OAAO,mBAAmB,CAAC;AAqB3B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,UAAU,SAAS,CAAC,EAAE,QAAQ,EAAE,IAAI,GAAG,IAAI,EAAkB;IAC/D,OAAO,CACH,2BAAe,YAAY,eAAY,IAAI,IAAI,SAAS,YACnD,QAAQ,GACP,CACT,CAAC;AACN,CAAC"}
1
+ {"version":3,"file":"ChipGroup.js","sourceRoot":"","sources":["../../../src/components/ChipGroup/ChipGroup.tsx"],"names":[],"mappings":";;AAAA,OAAO,mBAAmB,CAAC;AAE3B,OAAO,EAAE,IAAI,EAAa,MAAM,mBAAmB,CAAC;AAapD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,SAAS,CAAC,EAAE,QAAQ,GAAG,MAAM,EAAE,KAAK,EAAkB;IAClE,OAAO,CACH,2BAAe,YAAY,iBAAc,QAAQ,KAAK,QAAQ,IAAI,SAAS,YACtE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE,CAAC,eAAC,IAAI,OAAK,IAAI,EAAE,GAAG,EAAE,IAAI,CAAC,KAAK,IAAI,GAAG,GAAI,CAAC,CAAC,CAAC,CAAC,IAAI,GAC1F,CACT,CAAC;AACN,CAAC"}
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { SvgChevronRight } from '@bspk/icons/ChevronRight';
3
3
  import { SvgClose } from '@bspk/icons/Close';
4
4
  import { SvgCloud } from '@bspk/icons/Cloud';
@@ -7,17 +7,72 @@ import { SvgKeyboardArrowDown } from '@bspk/icons/KeyboardArrowDown';
7
7
  import { SvgLightbulb } from '@bspk/icons/Lightbulb';
8
8
  import { SvgOpportunities } from '@bspk/icons/Opportunities';
9
9
  import { SvgSignLanguage } from '@bspk/icons/SignLanguage';
10
- import { Chip } from '../Chip';
11
10
  export const presets = [
12
- { label: 'scroll', propState: { wrap: false } },
13
- { label: 'wrap', propState: { wrap: true } },
11
+ {
12
+ label: 'Scroll',
13
+ propState: {
14
+ overflow: 'scroll',
15
+ items: [
16
+ { label: 'chip 1', leadingIcon: _jsx(SvgLightbulb, {}), trailingIcon: _jsx(SvgChevronRight, {}) },
17
+ { label: 'chip 2', leadingIcon: _jsx(SvgIcecream, {}), trailingIcon: _jsx(SvgChevronRight, {}) },
18
+ { label: 'chip 3', leadingIcon: _jsx(SvgSignLanguage, {}), trailingIcon: _jsx(SvgClose, {}) },
19
+ { label: 'chip 4', leadingIcon: _jsx(SvgOpportunities, {}), trailingIcon: _jsx(SvgClose, {}) },
20
+ { label: 'chip 5', leadingIcon: _jsx(SvgCloud, {}), trailingIcon: _jsx(SvgKeyboardArrowDown, {}) },
21
+ ],
22
+ },
23
+ },
24
+ {
25
+ label: 'Scroll: Flat chips',
26
+ propState: {
27
+ overflow: 'scroll',
28
+ items: [
29
+ {
30
+ flat: true,
31
+ label: 'chip 1',
32
+ leadingIcon: _jsx(SvgLightbulb, {}),
33
+ trailingBadge: { count: 9, size: 'x-small' },
34
+ },
35
+ {
36
+ flat: true,
37
+ label: 'chip 2',
38
+ leadingIcon: _jsx(SvgIcecream, {}),
39
+ trailingBadge: { count: 2, size: 'x-small' },
40
+ },
41
+ { flat: true, label: 'chip 3', leadingIcon: _jsx(SvgSignLanguage, {}), trailingIcon: _jsx(SvgClose, {}) },
42
+ {
43
+ flat: true,
44
+ label: 'chip 4',
45
+ leadingIcon: _jsx(SvgOpportunities, {}),
46
+ trailingBadge: { count: 5, size: 'x-small' },
47
+ },
48
+ {
49
+ flat: true,
50
+ label: 'chip 5',
51
+ leadingIcon: _jsx(SvgCloud, {}),
52
+ trailingBadge: { count: 3, size: 'x-small' },
53
+ },
54
+ ],
55
+ },
56
+ },
14
57
  ];
15
58
  export const ChipGroupExample = ({ action }) => ({
16
59
  containerStyle: { width: '600px' },
17
60
  presets,
61
+ defaultState: {
62
+ overflow: 'wrap',
63
+ items: [
64
+ { label: 'chip 1', leadingIcon: _jsx(SvgLightbulb, {}), trailingIcon: _jsx(SvgChevronRight, {}) },
65
+ { label: 'chip 2', leadingIcon: _jsx(SvgIcecream, {}), trailingIcon: _jsx(SvgChevronRight, {}) },
66
+ { label: 'chip 3', leadingIcon: _jsx(SvgSignLanguage, {}), trailingIcon: _jsx(SvgClose, {}) },
67
+ { label: 'chip 4', leadingIcon: _jsx(SvgOpportunities, {}), trailingIcon: _jsx(SvgClose, {}) },
68
+ { label: 'chip 5', leadingIcon: _jsx(SvgCloud, {}), trailingIcon: _jsx(SvgKeyboardArrowDown, {}) },
69
+ ],
70
+ },
18
71
  render: ({ props, Component }) => {
19
- const handleChipInputClick = () => action('Chip clicked!');
20
- return (_jsxs(Component, { ...props, children: [_jsx(Chip, { label: "chip 1", leadingIcon: _jsx(SvgLightbulb, {}), onClick: handleChipInputClick, trailingIcon: _jsx(SvgChevronRight, {}) }), _jsx(Chip, { label: "chip 2", leadingIcon: _jsx(SvgIcecream, {}), onClick: handleChipInputClick, trailingIcon: _jsx(SvgChevronRight, {}) }), _jsx(Chip, { label: "chip 3", leadingIcon: _jsx(SvgSignLanguage, {}), onClick: handleChipInputClick, trailingIcon: _jsx(SvgClose, {}) }), _jsx(Chip, { label: "chip 4", leadingIcon: _jsx(SvgOpportunities, {}), onClick: handleChipInputClick, trailingIcon: _jsx(SvgClose, {}) }), _jsx(Chip, { label: "chip 5", leadingIcon: _jsx(SvgCloud, {}), onClick: handleChipInputClick, trailingIcon: _jsx(SvgKeyboardArrowDown, {}) })] }));
72
+ return (_jsx(Component, { ...props, items: props.items?.map((item) => ({
73
+ ...item,
74
+ onClick: () => action('Chip clicked!'),
75
+ })) }));
21
76
  },
22
77
  });
23
78
  //# sourceMappingURL=ChipGroupExample.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ChipGroupExample.js","sourceRoot":"","sources":["../../../src/components/ChipGroup/ChipGroupExample.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAE3D,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAIzC,MAAM,CAAC,MAAM,OAAO,GAA6B;IAC7C,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE;IAC/C,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE;CAC/C,CAAC;AAEF,MAAM,CAAC,MAAM,gBAAgB,GAAuC,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC;IACjF,cAAc,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE;IAClC,OAAO;IACP,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE,EAAE;QAC7B,MAAM,oBAAoB,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC;QAC3D,OAAO,CACH,MAAC,SAAS,OAAK,KAAK,aAChB,KAAC,IAAI,IACD,KAAK,EAAC,QAAQ,EACd,WAAW,EAAE,KAAC,YAAY,KAAG,EAC7B,OAAO,EAAE,oBAAoB,EAC7B,YAAY,EAAE,KAAC,eAAe,KAAG,GACnC,EACF,KAAC,IAAI,IACD,KAAK,EAAC,QAAQ,EACd,WAAW,EAAE,KAAC,WAAW,KAAG,EAC5B,OAAO,EAAE,oBAAoB,EAC7B,YAAY,EAAE,KAAC,eAAe,KAAG,GACnC,EACF,KAAC,IAAI,IACD,KAAK,EAAC,QAAQ,EACd,WAAW,EAAE,KAAC,eAAe,KAAG,EAChC,OAAO,EAAE,oBAAoB,EAC7B,YAAY,EAAE,KAAC,QAAQ,KAAG,GAC5B,EACF,KAAC,IAAI,IACD,KAAK,EAAC,QAAQ,EACd,WAAW,EAAE,KAAC,gBAAgB,KAAG,EACjC,OAAO,EAAE,oBAAoB,EAC7B,YAAY,EAAE,KAAC,QAAQ,KAAG,GAC5B,EACF,KAAC,IAAI,IACD,KAAK,EAAC,QAAQ,EACd,WAAW,EAAE,KAAC,QAAQ,KAAG,EACzB,OAAO,EAAE,oBAAoB,EAC7B,YAAY,EAAE,KAAC,oBAAoB,KAAG,GACxC,IACM,CACf,CAAC;IACN,CAAC;CACJ,CAAC,CAAC"}
1
+ {"version":3,"file":"ChipGroupExample.js","sourceRoot":"","sources":["../../../src/components/ChipGroup/ChipGroupExample.tsx"],"names":[],"mappings":"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"}
@@ -2,10 +2,12 @@
2
2
  display: flex;
3
3
  gap: var(--spacing-sizing-02);
4
4
  width: 100%;
5
- overflow: auto;
6
- }
7
- [data-bspk=chip-group][data-wrap] {
8
5
  flex-flow: row wrap;
6
+ padding-bottom: var(--spacing-sizing-01);
7
+ }
8
+ [data-bspk=chip-group][data-scroll] {
9
+ overflow: auto;
10
+ flex-wrap: nowrap;
9
11
  }
10
12
 
11
13
  /** Copyright 2025 Anywhere Real Estate - CC BY 4.0 */
@@ -5,10 +5,12 @@ style.appendChild(document.createTextNode(`[data-bspk=chip-group] {
5
5
  display: flex;
6
6
  gap: var(--spacing-sizing-02);
7
7
  width: 100%;
8
- overflow: auto;
9
- }
10
- [data-bspk=chip-group][data-wrap] {
11
8
  flex-flow: row wrap;
9
+ padding-bottom: var(--spacing-sizing-01);
10
+ }
11
+ [data-bspk=chip-group][data-scroll] {
12
+ overflow: auto;
13
+ flex-wrap: nowrap;
12
14
  }
13
15
 
14
16
  /** Copyright 2025 Anywhere Real Estate - CC BY 4.0 */
@@ -1,6 +1,6 @@
1
1
  import './otp-input.scss';
2
2
  import { CommonProps } from '-/types/common';
3
- export type OTPInputProps = CommonProps<'id' | 'invalid' | 'name' | 'size'> & {
3
+ export type OTPInputProps = CommonProps<'aria-label' | 'id' | 'invalid' | 'name' | 'size'> & {
4
4
  /**
5
5
  * The value of the otp-input.
6
6
  *
@@ -23,6 +23,12 @@ export type OTPInputProps = CommonProps<'id' | 'invalid' | 'name' | 'size'> & {
23
23
  * @maximum 10
24
24
  */
25
25
  length?: number;
26
+ /**
27
+ * The mode of the otp-input.
28
+ *
29
+ * @default false
30
+ */
31
+ alphanumeric?: boolean;
26
32
  };
27
33
  /**
28
34
  * A row of input fields that are used to input a temporary secure pin code sent to the customer.
@@ -33,11 +39,11 @@ export type OTPInputProps = CommonProps<'id' | 'invalid' | 'name' | 'size'> & {
33
39
  * () => {
34
40
  * const [otpValue, setOtpValue] = useState('');
35
41
  *
36
- * <OTPInput name="2-auth" length={4} value={otpValue} onChange={setOtpValue} />;
42
+ * return <OTPInput name="2-auth" length={6} value={otpValue} onChange={setOtpValue} alphanumeric={false} />;
37
43
  * };
38
44
  *
39
45
  * @name OTPInput
40
46
  * @phase UXReview
41
47
  */
42
- export declare function OTPInput({ value: valueProp, onChange, name, id: idProp, length, size, invalid, }: OTPInputProps): import("react/jsx-runtime").JSX.Element;
48
+ export declare function OTPInput({ value: valueProp, onChange, name, id: idProp, length, size, invalid, alphanumeric, 'aria-label': ariaLabel, }: OTPInputProps): import("react/jsx-runtime").JSX.Element;
43
49
  /** Copyright 2025 Anywhere Real Estate - CC BY 4.0 */
@@ -1,6 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import './otp-input.css.js';
3
- import { useRef } from 'react';
4
3
  import { useId } from '../../hooks/useId';
5
4
  /**
6
5
  * A row of input fields that are used to input a temporary secure pin code sent to the customer.
@@ -11,49 +10,18 @@ import { useId } from '../../hooks/useId';
11
10
  * () => {
12
11
  * const [otpValue, setOtpValue] = useState('');
13
12
  *
14
- * <OTPInput name="2-auth" length={4} value={otpValue} onChange={setOtpValue} />;
13
+ * return <OTPInput name="2-auth" length={6} value={otpValue} onChange={setOtpValue} alphanumeric={false} />;
15
14
  * };
16
15
  *
17
16
  * @name OTPInput
18
17
  * @phase UXReview
19
18
  */
20
- export function OTPInput({ value: valueProp, onChange, name, id: idProp, length = 6, size = 'medium', invalid = false, }) {
19
+ export function OTPInput({ value: valueProp, onChange, name, id: idProp, length = 6, size = 'medium', invalid = false, alphanumeric = false, 'aria-label': ariaLabel = 'OTP input', }) {
21
20
  const id = useId(idProp);
22
- const value = valueProp?.slice(0, length) || '';
23
- const parentRef = useRef(null);
24
- const element = (index) => parentRef.current?.children[index + 1];
25
- const setIndex = (index, character) => {
26
- const charArray = value.split('');
27
- charArray[index] = character;
28
- return charArray.join('');
29
- };
30
- const updateValue = (next) => onChange(next.slice(0, length).toUpperCase());
31
- return (_jsxs("div", { "data-bspk": "otp-input", "data-invalid": invalid || undefined, "data-size": size || 'medium', id: id, ref: parentRef, children: [_jsx("input", { name: name, type: "hidden", value: value }), Array.from({ length }, (_, index) => (_jsx("span", { "aria-label": `OTP digit ${index + 1}`, "data-digit": index + 1, onClick: (e) => {
32
- if (value[index])
33
- return;
34
- // if a digit does not exist for the previous index then focus the previous input
35
- if (!value[index - 1]) {
36
- e.preventDefault();
37
- element(value.length)?.focus();
38
- }
39
- }, onKeyDown: (event) => {
40
- if (event.key === 'Backspace') {
41
- if (value) {
42
- // delete the last value if there is one and focus the first empty input
43
- const next = value.slice(0, -1);
44
- updateValue(next);
45
- element(next.length)?.focus();
46
- }
47
- }
48
- // if a single character key is pressed at at the current index and focus the next input
49
- if (event.key.length === 1) {
50
- updateValue(setIndex(index, event.key));
51
- element(index + 1)?.focus();
52
- }
53
- }, onPaste: (event) => {
54
- const pastedData = event.clipboardData.getData('text').trim();
55
- updateValue(pastedData);
56
- element(length - 1)?.focus();
57
- }, role: "textbox", tabIndex: 0, children: value?.[index] || '' }, index)))] }));
21
+ const value = valueProp || '';
22
+ const activeIndex = Math.min(value.length, length - 1);
23
+ return (_jsxs("div", { "data-bspk": "otp-input", "data-invalid": invalid || undefined, "data-size": size || 'medium', id: id, children: [_jsx("input", { "aria-label": ariaLabel, inputMode: alphanumeric ? 'text' : 'numeric', name: name, onChange: (event) => {
24
+ onChange(event.target.value.trim().toUpperCase().slice(0, length));
25
+ }, type: alphanumeric ? 'text' : 'number', value: value }), _jsx("span", { "data-digits": true, children: Array.from({ length }, (_, index) => (_jsx("span", { "data-active": index === activeIndex || undefined, "data-digit": true, children: value[index] }, index))) })] }));
58
26
  }
59
27
  //# sourceMappingURL=OTPInput.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"OTPInput.js","sourceRoot":"","sources":["../../../src/components/OTPInput/OTPInput.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"OTPInput.js","sourceRoot":"","sources":["../../../src/components/OTPInput/OTPInput.tsx"],"names":[],"mappings":";AAAA,OAAO,kBAAkB,CAAC;AAC1B,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAkCtC;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,QAAQ,CAAC,EACrB,KAAK,EAAE,SAAS,EAChB,QAAQ,EACR,IAAI,EACJ,EAAE,EAAE,MAAM,EACV,MAAM,GAAG,CAAC,EACV,IAAI,GAAG,QAAQ,EACf,OAAO,GAAG,KAAK,EACf,YAAY,GAAG,KAAK,EACpB,YAAY,EAAE,SAAS,GAAG,WAAW,GACzB;IACZ,MAAM,EAAE,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;IACzB,MAAM,KAAK,GAAG,SAAS,IAAI,EAAE,CAAC;IAC9B,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,CAAC,CAAC,CAAC;IAEvD,OAAO,CACH,4BAAe,WAAW,kBAAe,OAAO,IAAI,SAAS,eAAa,IAAI,IAAI,QAAQ,EAAE,EAAE,EAAE,EAAE,aAC9F,8BACgB,SAAS,EACrB,SAAS,EAAE,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,EAC5C,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE;oBAChB,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;gBACvE,CAAC,EACD,IAAI,EAAE,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,EACtC,KAAK,EAAE,KAAK,GACd,EACF,8CACK,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,CAClC,8BAAmB,KAAK,KAAK,WAAW,IAAI,SAAS,gCAChD,KAAK,CAAC,KAAK,CAAC,IADsD,KAAK,CAErE,CACV,CAAC,GACC,IACL,CACT,CAAC;AACN,CAAC"}
@@ -19,7 +19,12 @@ export const presets = [
19
19
  ];
20
20
  export const OTPInputExample = {
21
21
  containerStyle: { width: '100%' },
22
- defaultState: {},
22
+ defaultState: {
23
+ value: '',
24
+ length: 6,
25
+ name: 'OTP Input',
26
+ alphanumeric: false,
27
+ },
23
28
  disableProps: [],
24
29
  presets,
25
30
  render: ({ props, Component }) => _jsx(Component, { ...props }),
@@ -1 +1 @@
1
- {"version":3,"file":"OTPInputExample.js","sourceRoot":"","sources":["../../../src/components/OTPInput/OTPInputExample.tsx"],"names":[],"mappings":";AAGA,MAAM,CAAC,MAAM,OAAO,GAA4B;IAC5C;QACI,KAAK,EAAE,cAAc;QACrB,SAAS,EAAE;YACP,KAAK,EAAE,EAAE;YACT,MAAM,EAAE,CAAC;YACT,IAAI,EAAE,WAAW;SACpB;KACJ;IACD;QACI,KAAK,EAAE,cAAc;QACrB,SAAS,EAAE;YACP,KAAK,EAAE,EAAE;YACT,MAAM,EAAE,CAAC;YACT,IAAI,EAAE,WAAW;SACpB;KACJ;CACJ,CAAC;AAEF,MAAM,CAAC,MAAM,eAAe,GAAoC;IAC5D,cAAc,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE;IACjC,YAAY,EAAE,EAAE;IAChB,YAAY,EAAE,EAAE;IAChB,OAAO;IACP,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC,KAAC,SAAS,OAAK,KAAK,GAAI;IAC1D,QAAQ,EAAE,EAAE;CACf,CAAC"}
1
+ {"version":3,"file":"OTPInputExample.js","sourceRoot":"","sources":["../../../src/components/OTPInput/OTPInputExample.tsx"],"names":[],"mappings":";AAGA,MAAM,CAAC,MAAM,OAAO,GAA4B;IAC5C;QACI,KAAK,EAAE,cAAc;QACrB,SAAS,EAAE;YACP,KAAK,EAAE,EAAE;YACT,MAAM,EAAE,CAAC;YACT,IAAI,EAAE,WAAW;SACpB;KACJ;IACD;QACI,KAAK,EAAE,cAAc;QACrB,SAAS,EAAE;YACP,KAAK,EAAE,EAAE;YACT,MAAM,EAAE,CAAC;YACT,IAAI,EAAE,WAAW;SACpB;KACJ;CACJ,CAAC;AAEF,MAAM,CAAC,MAAM,eAAe,GAAoC;IAC5D,cAAc,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE;IACjC,YAAY,EAAE;QACV,KAAK,EAAE,EAAE;QACT,MAAM,EAAE,CAAC;QACT,IAAI,EAAE,WAAW;QACjB,YAAY,EAAE,KAAK;KACtB;IACD,YAAY,EAAE,EAAE;IAChB,OAAO;IACP,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC,KAAC,SAAS,OAAK,KAAK,GAAI;IAC1D,QAAQ,EAAE,EAAE;CACf,CAAC"}
@@ -1,13 +1,27 @@
1
1
  [data-bspk=otp-input] {
2
+ width: fit-content;
3
+ position: relative;
4
+ }
5
+ [data-bspk=otp-input] input[inputMode] {
6
+ position: absolute;
7
+ inset: 0;
8
+ opacity: 0;
9
+ border: none;
10
+ caret-color: transparent;
11
+ }
12
+ [data-bspk=otp-input] [data-digits] {
2
13
  display: flex;
3
14
  flex-direction: row;
4
15
  justify-content: center;
5
16
  gap: var(--spacing-sizing-02);
6
17
  font: var(--font);
7
18
  width: fit-content;
19
+ position: relative;
20
+ pointer-events: none;
8
21
  }
9
- [data-bspk=otp-input] [data-digit] {
22
+ [data-bspk=otp-input] [data-digits] [data-digit] {
10
23
  display: flex;
24
+ flex-direction: row;
11
25
  align-items: center;
12
26
  justify-content: center;
13
27
  border-radius: var(--radius-sm);
@@ -17,25 +31,12 @@
17
31
  width: var(--width);
18
32
  position: relative;
19
33
  outline: none;
34
+ text-align: center;
20
35
  }
21
- [data-bspk=otp-input] [data-digit]:hover:not(:focus)::after {
22
- background: var(--interaction-hover-opacity);
23
- content: "";
24
- position: absolute;
25
- inset: 0;
26
- border-radius: var(--radius-sm);
27
- }
28
- [data-bspk=otp-input] [data-digit]:active::after {
29
- background: var(--interaction-press-opacity);
30
- content: "";
31
- position: absolute;
32
- inset: 0;
33
- border-radius: var(--radius-sm);
34
- }
35
- [data-bspk=otp-input] [data-digit]:focus:not(:active) {
36
+ [data-bspk=otp-input] input[inputMode]:focus + [data-digits] [data-digit][data-active] {
36
37
  outline: solid 2px var(--stroke-neutral-focus);
37
38
  }
38
- [data-bspk=otp-input] [data-digit]:focus:not(:active):empty::before {
39
+ [data-bspk=otp-input] input[inputMode]:focus + [data-digits] [data-digit][data-active]:empty::before {
39
40
  content: "";
40
41
  width: 2px;
41
42
  height: calc(var(--caret-height) - 8px);
@@ -2,15 +2,29 @@
2
2
  * Do not edit this file directly. */
3
3
  const style = document.createElement('style');
4
4
  style.appendChild(document.createTextNode(`[data-bspk=otp-input] {
5
+ width: fit-content;
6
+ position: relative;
7
+ }
8
+ [data-bspk=otp-input] input[inputMode] {
9
+ position: absolute;
10
+ inset: 0;
11
+ opacity: 0;
12
+ border: none;
13
+ caret-color: transparent;
14
+ }
15
+ [data-bspk=otp-input] [data-digits] {
5
16
  display: flex;
6
17
  flex-direction: row;
7
18
  justify-content: center;
8
19
  gap: var(--spacing-sizing-02);
9
20
  font: var(--font);
10
21
  width: fit-content;
22
+ position: relative;
23
+ pointer-events: none;
11
24
  }
12
- [data-bspk=otp-input] [data-digit] {
25
+ [data-bspk=otp-input] [data-digits] [data-digit] {
13
26
  display: flex;
27
+ flex-direction: row;
14
28
  align-items: center;
15
29
  justify-content: center;
16
30
  border-radius: var(--radius-sm);
@@ -20,25 +34,12 @@ style.appendChild(document.createTextNode(`[data-bspk=otp-input] {
20
34
  width: var(--width);
21
35
  position: relative;
22
36
  outline: none;
37
+ text-align: center;
23
38
  }
24
- [data-bspk=otp-input] [data-digit]:hover:not(:focus)::after {
25
- background: var(--interaction-hover-opacity);
26
- content: "";
27
- position: absolute;
28
- inset: 0;
29
- border-radius: var(--radius-sm);
30
- }
31
- [data-bspk=otp-input] [data-digit]:active::after {
32
- background: var(--interaction-press-opacity);
33
- content: "";
34
- position: absolute;
35
- inset: 0;
36
- border-radius: var(--radius-sm);
37
- }
38
- [data-bspk=otp-input] [data-digit]:focus:not(:active) {
39
+ [data-bspk=otp-input] input[inputMode]:focus + [data-digits] [data-digit][data-active] {
39
40
  outline: solid 2px var(--stroke-neutral-focus);
40
41
  }
41
- [data-bspk=otp-input] [data-digit]:focus:not(:active):empty::before {
42
+ [data-bspk=otp-input] input[inputMode]:focus + [data-digits] [data-digit][data-active]:empty::before {
42
43
  content: "";
43
44
  width: 2px;
44
45
  height: calc(var(--caret-height) - 8px);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bspk/ui",
3
- "version": "1.3.10",
3
+ "version": "1.3.11",
4
4
  "license": "CC-BY-4.0",
5
5
  "type": "module",
6
6
  "files": [
@@ -1,6 +1,5 @@
1
1
  import { ChipGroup } from './ChipGroup';
2
2
  import { presets } from './ChipGroupExample';
3
- import { Chip } from '-/components/Chip';
4
3
  import { hasNoBasicA11yIssues } from '-/rtl/hasNoBasicA11yIssues';
5
4
  import { render } from '-/rtl/util';
6
5
 
@@ -9,22 +8,28 @@ describe('ChipGroup (RTL)', () => {
9
8
  it(
10
9
  `has no basic a11y issues - ${preset.label}`,
11
10
  hasNoBasicA11yIssues(
12
- <ChipGroup {...preset.propState}>
13
- <Chip label="suggestion 1" onClick={() => {}} />
14
- <Chip label="suggestion 2" onClick={() => {}} />
15
- <Chip label="suggestion 3" onClick={() => {}} />
16
- </ChipGroup>,
11
+ <ChipGroup
12
+ {...preset.propState}
13
+ items={[
14
+ { label: 'suggestion 1', onClick: () => {} },
15
+ { label: 'suggestion 2', onClick: () => {} },
16
+ { label: 'suggestion 3', onClick: () => {} },
17
+ ]}
18
+ />,
17
19
  ),
18
20
  );
19
21
  });
20
22
 
21
23
  it('renders', () => {
22
24
  const { getByText } = render(
23
- <ChipGroup {...presets[1].propState}>
24
- <Chip label="suggestion 1" onClick={() => {}} />
25
- <Chip label="suggestion 2" onClick={() => {}} />
26
- <Chip label="suggestion 3" onClick={() => {}} />
27
- </ChipGroup>,
25
+ <ChipGroup
26
+ {...presets[1].propState}
27
+ items={[
28
+ { label: 'suggestion 1', onClick: () => {} },
29
+ { label: 'suggestion 2', onClick: () => {} },
30
+ { label: 'suggestion 3', onClick: () => {} },
31
+ ]}
32
+ />,
28
33
  );
29
34
 
30
35
  expect(getByText('suggestion 1')).toBeInTheDocument();
@@ -1,23 +1,17 @@
1
1
  import './chip-group.scss';
2
2
 
3
- import { ReactNode } from 'react';
4
-
5
- import { ChipProps } from '-/components/Chip';
6
-
7
- export type ChipGroupItem = Pick<
8
- ChipProps,
9
- 'disabled' | 'flat' | 'label' | 'leadingIcon' | 'onClick' | 'selected' | 'trailingBadge' | 'trailingIcon'
10
- >;
3
+ import { Chip, ChipProps } from '-/components/Chip';
11
4
 
12
5
  export type ChipGroupProps = {
13
6
  /**
14
- * To allow chips to wrap. If set to false chips will scroll.
7
+ * Controls the overflow behavior of the chip group. If set to `scroll`, the chip group will be scrollable
8
+ * horizontally. If set to `wrap`, the chip group will wrap to multiple lines as needed.
15
9
  *
16
- * @default true
10
+ * @default wrap
17
11
  */
18
- wrap?: boolean;
19
- /** Only Chip components should be used as children. */
20
- children?: ReactNode;
12
+ overflow?: 'scroll' | 'wrap';
13
+ /** Only Chip components should be used as items. */
14
+ items?: ChipProps[];
21
15
  };
22
16
  /**
23
17
  * A component that manages the layout of a group of chips.
@@ -26,34 +20,22 @@ export type ChipGroupProps = {
26
20
  * import { Chip } from '@bspk/ui/Chip';
27
21
  * import { ChipGroup } from '@bspk/ui/ChipGroup';
28
22
  *
29
- * <ChipGroup wrap={false}>
30
- * <Chip
31
- * label="chip 1"
32
- * leadingIcon={<SvgLightbulb />}
33
- * onClick={() => action('Chip clicked!')}
34
- * trailingIcon={<SvgChevronRight />}
35
- * />
36
- * <Chip
37
- * label="chip 2"
38
- * leadingIcon={<SvgIcecream />}
39
- * onClick={() => action('Chip clicked!')}
40
- * trailingIcon={<SvgChevronRight />}
41
- * />
42
- * <Chip
43
- * label="chip 3"
44
- * leadingIcon={<SvgSignLanguage />}
45
- * onClick={() => action('Chip clicked!')}
46
- * trailingIcon={<SvgClose />}
47
- * />
48
- * </ChipGroup>;
23
+ * <ChipGroup
24
+ * overflow="scroll"
25
+ * items={[
26
+ * { label: 'chip 1', leadingIcon: <SvgLightbulb />, onClick: () => {}, trailingIcon: <SvgChevronRight /> },
27
+ * { label: 'chip 2', leadingIcon: <SvgIcecream />, onClick: () => {}, trailingIcon: <SvgChevronRight /> },
28
+ * { label: 'chip 3', leadingIcon: <SvgSignLanguage />, onClick: () => {}, trailingIcon: <SvgClose /> },
29
+ * ]}
30
+ * />;
49
31
  *
50
32
  * @name ChipGroup
51
33
  * @phase UXReview
52
34
  */
53
- export function ChipGroup({ children, wrap = true }: ChipGroupProps) {
35
+ export function ChipGroup({ overflow = 'wrap', items }: ChipGroupProps) {
54
36
  return (
55
- <div data-bspk="chip-group" data-wrap={wrap || undefined}>
56
- {children}
37
+ <div data-bspk="chip-group" data-scroll={overflow === 'scroll' || undefined}>
38
+ {items?.length ? items.map((item, idx) => <Chip {...item} key={item.label ?? idx} />) : null}
57
39
  </div>
58
40
  );
59
41
  }
@@ -7,53 +7,81 @@ import { SvgLightbulb } from '@bspk/icons/Lightbulb';
7
7
  import { SvgOpportunities } from '@bspk/icons/Opportunities';
8
8
  import { SvgSignLanguage } from '@bspk/icons/SignLanguage';
9
9
 
10
- import { Chip } from '-/components/Chip';
10
+ // import { Chip } from '-/components/Chip';
11
11
  import { ChipGroupProps } from '-/components/ChipGroup';
12
12
  import { ComponentExampleFn, Preset } from '-/utils/demo';
13
13
 
14
14
  export const presets: Preset<ChipGroupProps>[] = [
15
- { label: 'scroll', propState: { wrap: false } },
16
- { label: 'wrap', propState: { wrap: true } },
15
+ {
16
+ label: 'Scroll',
17
+ propState: {
18
+ overflow: 'scroll',
19
+ items: [
20
+ { label: 'chip 1', leadingIcon: <SvgLightbulb />, trailingIcon: <SvgChevronRight /> },
21
+ { label: 'chip 2', leadingIcon: <SvgIcecream />, trailingIcon: <SvgChevronRight /> },
22
+ { label: 'chip 3', leadingIcon: <SvgSignLanguage />, trailingIcon: <SvgClose /> },
23
+ { label: 'chip 4', leadingIcon: <SvgOpportunities />, trailingIcon: <SvgClose /> },
24
+ { label: 'chip 5', leadingIcon: <SvgCloud />, trailingIcon: <SvgKeyboardArrowDown /> },
25
+ ],
26
+ },
27
+ },
28
+ {
29
+ label: 'Scroll: Flat chips',
30
+ propState: {
31
+ overflow: 'scroll',
32
+ items: [
33
+ {
34
+ flat: true,
35
+ label: 'chip 1',
36
+ leadingIcon: <SvgLightbulb />,
37
+ trailingBadge: { count: 9, size: 'x-small' },
38
+ },
39
+ {
40
+ flat: true,
41
+ label: 'chip 2',
42
+ leadingIcon: <SvgIcecream />,
43
+ trailingBadge: { count: 2, size: 'x-small' },
44
+ },
45
+ { flat: true, label: 'chip 3', leadingIcon: <SvgSignLanguage />, trailingIcon: <SvgClose /> },
46
+ {
47
+ flat: true,
48
+ label: 'chip 4',
49
+ leadingIcon: <SvgOpportunities />,
50
+ trailingBadge: { count: 5, size: 'x-small' },
51
+ },
52
+ {
53
+ flat: true,
54
+ label: 'chip 5',
55
+ leadingIcon: <SvgCloud />,
56
+ trailingBadge: { count: 3, size: 'x-small' },
57
+ },
58
+ ],
59
+ },
60
+ },
17
61
  ];
18
62
 
19
63
  export const ChipGroupExample: ComponentExampleFn<ChipGroupProps> = ({ action }) => ({
20
64
  containerStyle: { width: '600px' },
21
65
  presets,
66
+ defaultState: {
67
+ overflow: 'wrap',
68
+ items: [
69
+ { label: 'chip 1', leadingIcon: <SvgLightbulb />, trailingIcon: <SvgChevronRight /> },
70
+ { label: 'chip 2', leadingIcon: <SvgIcecream />, trailingIcon: <SvgChevronRight /> },
71
+ { label: 'chip 3', leadingIcon: <SvgSignLanguage />, trailingIcon: <SvgClose /> },
72
+ { label: 'chip 4', leadingIcon: <SvgOpportunities />, trailingIcon: <SvgClose /> },
73
+ { label: 'chip 5', leadingIcon: <SvgCloud />, trailingIcon: <SvgKeyboardArrowDown /> },
74
+ ],
75
+ },
22
76
  render: ({ props, Component }) => {
23
- const handleChipInputClick = () => action('Chip clicked!');
24
77
  return (
25
- <Component {...props}>
26
- <Chip
27
- label="chip 1"
28
- leadingIcon={<SvgLightbulb />}
29
- onClick={handleChipInputClick}
30
- trailingIcon={<SvgChevronRight />}
31
- />
32
- <Chip
33
- label="chip 2"
34
- leadingIcon={<SvgIcecream />}
35
- onClick={handleChipInputClick}
36
- trailingIcon={<SvgChevronRight />}
37
- />
38
- <Chip
39
- label="chip 3"
40
- leadingIcon={<SvgSignLanguage />}
41
- onClick={handleChipInputClick}
42
- trailingIcon={<SvgClose />}
43
- />
44
- <Chip
45
- label="chip 4"
46
- leadingIcon={<SvgOpportunities />}
47
- onClick={handleChipInputClick}
48
- trailingIcon={<SvgClose />}
49
- />
50
- <Chip
51
- label="chip 5"
52
- leadingIcon={<SvgCloud />}
53
- onClick={handleChipInputClick}
54
- trailingIcon={<SvgKeyboardArrowDown />}
55
- />
56
- </Component>
78
+ <Component
79
+ {...props}
80
+ items={props.items?.map((item) => ({
81
+ ...item,
82
+ onClick: () => action('Chip clicked!'),
83
+ }))}
84
+ />
57
85
  );
58
86
  },
59
87
  });
@@ -2,10 +2,12 @@
2
2
  display: flex;
3
3
  gap: var(--spacing-sizing-02);
4
4
  width: 100%;
5
- overflow: auto;
5
+ flex-flow: row wrap;
6
+ padding-bottom: var(--spacing-sizing-01);
6
7
 
7
- &[data-wrap] {
8
- flex-flow: row wrap;
8
+ &[data-scroll] {
9
+ overflow: auto;
10
+ flex-wrap: nowrap;
9
11
  }
10
12
  }
11
13
 
@@ -12,8 +12,10 @@ describe('OTPInput (RTL)', () => {
12
12
  });
13
13
 
14
14
  it('renders', () => {
15
- const { getByLabelText } = render(<OTPInput onChange={() => {}} {...presets[0].propState} />);
15
+ const { getByLabelText } = render(
16
+ <OTPInput aria-label="OTP input" onChange={() => {}} {...presets[0].propState} />,
17
+ );
16
18
 
17
- expect(getByLabelText('OTP digit 1')).toBeInTheDocument();
19
+ expect(getByLabelText('OTP input')).toBeInTheDocument();
18
20
  });
19
21
  });
@@ -1,9 +1,8 @@
1
1
  import './otp-input.scss';
2
- import { useRef } from 'react';
3
2
  import { useId } from '-/hooks/useId';
4
3
  import { CommonProps } from '-/types/common';
5
4
 
6
- export type OTPInputProps = CommonProps<'id' | 'invalid' | 'name' | 'size'> & {
5
+ export type OTPInputProps = CommonProps<'aria-label' | 'id' | 'invalid' | 'name' | 'size'> & {
7
6
  /**
8
7
  * The value of the otp-input.
9
8
  *
@@ -26,6 +25,12 @@ export type OTPInputProps = CommonProps<'id' | 'invalid' | 'name' | 'size'> & {
26
25
  * @maximum 10
27
26
  */
28
27
  length?: number;
28
+ /**
29
+ * The mode of the otp-input.
30
+ *
31
+ * @default false
32
+ */
33
+ alphanumeric?: boolean;
29
34
  };
30
35
 
31
36
  /**
@@ -37,7 +42,7 @@ export type OTPInputProps = CommonProps<'id' | 'invalid' | 'name' | 'size'> & {
37
42
  * () => {
38
43
  * const [otpValue, setOtpValue] = useState('');
39
44
  *
40
- * <OTPInput name="2-auth" length={4} value={otpValue} onChange={setOtpValue} />;
45
+ * return <OTPInput name="2-auth" length={6} value={otpValue} onChange={setOtpValue} alphanumeric={false} />;
41
46
  * };
42
47
  *
43
48
  * @name OTPInput
@@ -51,70 +56,32 @@ export function OTPInput({
51
56
  length = 6,
52
57
  size = 'medium',
53
58
  invalid = false,
59
+ alphanumeric = false,
60
+ 'aria-label': ariaLabel = 'OTP input',
54
61
  }: OTPInputProps) {
55
62
  const id = useId(idProp);
56
- const value = valueProp?.slice(0, length) || '';
57
- const parentRef = useRef<HTMLDivElement | null>(null);
58
-
59
- const element = (index: number) => parentRef.current?.children[index + 1] as HTMLElement;
60
-
61
- const setIndex = (index: number, character: string) => {
62
- const charArray = value.split('');
63
- charArray[index] = character;
64
- return charArray.join('');
65
- };
66
-
67
- const updateValue = (next: string) => onChange(next.slice(0, length).toUpperCase());
63
+ const value = valueProp || '';
64
+ const activeIndex = Math.min(value.length, length - 1);
68
65
 
69
66
  return (
70
- <div
71
- data-bspk="otp-input"
72
- data-invalid={invalid || undefined}
73
- data-size={size || 'medium'}
74
- id={id}
75
- ref={parentRef}
76
- >
77
- <input name={name} type="hidden" value={value} />
78
- {Array.from({ length }, (_, index) => (
79
- <span
80
- aria-label={`OTP digit ${index + 1}`}
81
- data-digit={index + 1}
82
- key={index}
83
- onClick={(e) => {
84
- if (value[index]) return;
85
- // if a digit does not exist for the previous index then focus the previous input
86
- if (!value[index - 1]) {
87
- e.preventDefault();
88
- element(value.length)?.focus();
89
- }
90
- }}
91
- onKeyDown={(event) => {
92
- if (event.key === 'Backspace') {
93
- if (value) {
94
- // delete the last value if there is one and focus the first empty input
95
- const next = value.slice(0, -1);
96
- updateValue(next);
97
- element(next.length)?.focus();
98
- }
99
- }
100
-
101
- // if a single character key is pressed at at the current index and focus the next input
102
- if (event.key.length === 1) {
103
- updateValue(setIndex(index, event.key));
104
- element(index + 1)?.focus();
105
- }
106
- }}
107
- onPaste={(event) => {
108
- const pastedData = event.clipboardData.getData('text').trim();
109
- updateValue(pastedData);
110
- element(length - 1)?.focus();
111
- }}
112
- role="textbox"
113
- tabIndex={0}
114
- >
115
- {value?.[index] || ''}
116
- </span>
117
- ))}
67
+ <div data-bspk="otp-input" data-invalid={invalid || undefined} data-size={size || 'medium'} id={id}>
68
+ <input
69
+ aria-label={ariaLabel}
70
+ inputMode={alphanumeric ? 'text' : 'numeric'}
71
+ name={name}
72
+ onChange={(event) => {
73
+ onChange(event.target.value.trim().toUpperCase().slice(0, length));
74
+ }}
75
+ type={alphanumeric ? 'text' : 'number'}
76
+ value={value}
77
+ />
78
+ <span data-digits>
79
+ {Array.from({ length }, (_, index) => (
80
+ <span data-active={index === activeIndex || undefined} data-digit key={index}>
81
+ {value[index]}
82
+ </span>
83
+ ))}
84
+ </span>
118
85
  </div>
119
86
  );
120
87
  }
@@ -22,7 +22,12 @@ export const presets: Preset<OTPInputProps>[] = [
22
22
 
23
23
  export const OTPInputExample: ComponentExample<OTPInputProps> = {
24
24
  containerStyle: { width: '100%' },
25
- defaultState: {},
25
+ defaultState: {
26
+ value: '',
27
+ length: 6,
28
+ name: 'OTP Input',
29
+ alphanumeric: false,
30
+ },
26
31
  disableProps: [],
27
32
  presets,
28
33
  render: ({ props, Component }) => <Component {...props} />,
@@ -1,64 +1,69 @@
1
1
  [data-bspk='otp-input'] {
2
- display: flex;
3
- flex-direction: row;
4
- justify-content: center;
5
- gap: var(--spacing-sizing-02);
6
- font: var(--font);
7
2
  width: fit-content;
3
+ position: relative;
8
4
 
9
- [data-digit] {
5
+ input[inputMode] {
6
+ position: absolute;
7
+ inset: 0;
8
+ opacity: 0;
9
+ border: none;
10
+ caret-color: transparent;
11
+ }
12
+
13
+ [data-digits] {
10
14
  display: flex;
11
- align-items: center;
15
+ flex-direction: row;
12
16
  justify-content: center;
13
- border-radius: var(--radius-sm);
14
- border: 1px solid var(--stroke-neutral-base);
15
- flex-grow: 1;
16
- aspect-ratio: 1;
17
- width: var(--width);
17
+ gap: var(--spacing-sizing-02);
18
+ font: var(--font);
19
+ width: fit-content;
18
20
  position: relative;
19
- outline: none;
20
-
21
- &:hover:not(:focus)::after {
22
- background: var(--interaction-hover-opacity);
23
- content: '';
24
- position: absolute;
25
- inset: 0;
26
- border-radius: var(--radius-sm);
27
- }
21
+ pointer-events: none;
28
22
 
29
- &:active::after {
30
- background: var(--interaction-press-opacity);
31
- content: '';
32
- position: absolute;
33
- inset: 0;
23
+ [data-digit] {
24
+ display: flex;
25
+ flex-direction: row;
26
+ align-items: center;
27
+ justify-content: center;
34
28
  border-radius: var(--radius-sm);
29
+ border: 1px solid var(--stroke-neutral-base);
30
+ flex-grow: 1;
31
+ aspect-ratio: 1;
32
+ width: var(--width);
33
+ position: relative;
34
+ outline: none;
35
+ text-align: center;
35
36
  }
37
+ }
36
38
 
37
- &:focus:not(:active) {
38
- outline: solid 2px var(--stroke-neutral-focus);
39
-
40
- &:empty::before {
41
- // caret
42
- content: '';
43
- width: 2px;
44
- height: calc(var(--caret-height) - 8px);
45
- background: var(--stroke-neutral-high);
46
- animation: blink-caret 1s step-end infinite;
47
-
48
- @keyframes blink-caret {
49
- 0%,
50
- 100% {
51
- opacity: 0;
52
- }
39
+ input[inputMode]:focus + [data-digits] {
40
+ [data-digit] {
41
+ &[data-active] {
42
+ outline: solid 2px var(--stroke-neutral-focus);
53
43
 
54
- 50% {
55
- opacity: 1;
56
- }
44
+ &:empty::before {
45
+ // caret
46
+ content: '';
47
+ width: 2px;
48
+ height: calc(var(--caret-height) - 8px);
49
+ background: var(--stroke-neutral-high);
50
+ animation: blink-caret 1s step-end infinite;
57
51
  }
58
52
  }
59
53
  }
60
54
  }
61
55
 
56
+ @keyframes blink-caret {
57
+ 0%,
58
+ 100% {
59
+ opacity: 0;
60
+ }
61
+
62
+ 50% {
63
+ opacity: 1;
64
+ }
65
+ }
66
+
62
67
  &[data-size='small'] {
63
68
  --width: var(--spacing-sizing-08);
64
69
  --font: var(--subheader-medium);