@transferwise/components 0.0.0-experimental-4658255 → 0.0.0-experimental-07a2614
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.
- package/build/inputs/SelectInput.js +2 -41
- package/build/inputs/SelectInput.js.map +1 -1
- package/build/inputs/SelectInput.mjs +2 -41
- package/build/inputs/SelectInput.mjs.map +1 -1
- package/build/inputs/_BottomSheet.js +29 -1
- package/build/inputs/_BottomSheet.js.map +1 -1
- package/build/inputs/_BottomSheet.mjs +30 -2
- package/build/inputs/_BottomSheet.mjs.map +1 -1
- package/build/main.css +17 -5
- package/build/styles/inputs/SelectInput.css +17 -5
- package/build/styles/main.css +17 -5
- package/build/types/inputs/SelectInput.d.ts +1 -20
- package/build/types/inputs/SelectInput.d.ts.map +1 -1
- package/build/types/inputs/_BottomSheet.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/common/bottomSheet/BottomSheet.test.story.tsx +94 -0
- package/src/inputs/SelectInput.css +17 -5
- package/src/inputs/SelectInput.spec.tsx +33 -0
- package/src/inputs/SelectInput.story.tsx +0 -94
- package/src/inputs/SelectInput.test.story.tsx +83 -0
- package/src/inputs/SelectInput.tsx +1 -84
- package/src/inputs/_BottomSheet.less +16 -3
- package/src/inputs/_BottomSheet.tsx +19 -3
- package/src/main.css +17 -5
- package/src/moneyInput/MoneyInput.test.story.tsx +101 -0
package/build/styles/main.css
CHANGED
|
@@ -3524,6 +3524,13 @@ html:not([dir="rtl"]) .np-flow-navigation--sm .np-flow-navigation__stepper {
|
|
|
3524
3524
|
padding-inline-start: 8px;
|
|
3525
3525
|
padding-inline-start: var(--size-8);
|
|
3526
3526
|
}
|
|
3527
|
+
.wds-select-input-scroll-freeze {
|
|
3528
|
+
scroll-behavior: unset !important;
|
|
3529
|
+
height: 100vh;
|
|
3530
|
+
}
|
|
3531
|
+
.wds-select-input-scroll-freeze body {
|
|
3532
|
+
height: 100vh;
|
|
3533
|
+
}
|
|
3527
3534
|
.np-bottom-sheet-v2-container {
|
|
3528
3535
|
position: relative;
|
|
3529
3536
|
z-index: 1060;
|
|
@@ -3537,6 +3544,8 @@ html:not([dir="rtl"]) .np-flow-navigation--sm .np-flow-navigation__stepper {
|
|
|
3537
3544
|
transition-property: opacity;
|
|
3538
3545
|
transition-timing-function: ease-out;
|
|
3539
3546
|
transition-duration: 300ms;
|
|
3547
|
+
will-change: transform;
|
|
3548
|
+
min-height: 100vh;
|
|
3540
3549
|
}
|
|
3541
3550
|
.np-bottom-sheet-v2-backdrop--closed {
|
|
3542
3551
|
opacity: 0;
|
|
@@ -3544,7 +3553,7 @@ html:not([dir="rtl"]) .np-flow-navigation--sm .np-flow-navigation__stepper {
|
|
|
3544
3553
|
.np-bottom-sheet-v2 {
|
|
3545
3554
|
position: fixed;
|
|
3546
3555
|
inset: 0px;
|
|
3547
|
-
bottom: env(
|
|
3556
|
+
bottom: env(safe-area-inset-bottom, 0);
|
|
3548
3557
|
margin-left: 8px;
|
|
3549
3558
|
margin-left: var(--size-8);
|
|
3550
3559
|
margin-right: 8px;
|
|
@@ -3554,18 +3563,21 @@ html:not([dir="rtl"]) .np-flow-navigation--sm .np-flow-navigation__stepper {
|
|
|
3554
3563
|
display: flex;
|
|
3555
3564
|
flex-direction: column;
|
|
3556
3565
|
justify-content: flex-end;
|
|
3566
|
+
height: calc(100vh - 64px - 40px);
|
|
3567
|
+
height: calc(100vh - var(--size-64) - 40px);
|
|
3557
3568
|
}
|
|
3558
3569
|
.np-bottom-sheet-v2-content {
|
|
3559
3570
|
display: flex;
|
|
3560
3571
|
flex-direction: column;
|
|
3561
3572
|
overflow: auto;
|
|
3562
|
-
border-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3573
|
+
border-radius: 32px;
|
|
3574
|
+
border-radius: var(--size-32);
|
|
3575
|
+
margin-bottom: 8px;
|
|
3576
|
+
margin-bottom: var(--size-8);
|
|
3566
3577
|
background-color: #ffffff;
|
|
3567
3578
|
background-color: var(--color-background-elevated);
|
|
3568
3579
|
box-shadow: 0 0 40px rgba(69, 71, 69, 0.2);
|
|
3580
|
+
will-change: transform;
|
|
3569
3581
|
}
|
|
3570
3582
|
.np-bottom-sheet-v2-content:focus {
|
|
3571
3583
|
outline: none;
|
|
@@ -32,25 +32,6 @@ export interface SelectInputProps<T = string, M extends boolean = false> {
|
|
|
32
32
|
multiple?: M;
|
|
33
33
|
placeholder?: string;
|
|
34
34
|
items: readonly SelectInputItem<NonNullable<T>>[];
|
|
35
|
-
/**
|
|
36
|
-
* Enables browser autocomplete integration through the search input.
|
|
37
|
-
* Accepts standard HTML autocomplete values (e.g., "country-name", "address-level1").
|
|
38
|
-
*
|
|
39
|
-
* Requires `filterable={true}` to enable the search input.
|
|
40
|
-
*
|
|
41
|
-
* @example
|
|
42
|
-
* <SelectInput
|
|
43
|
-
* name="country"
|
|
44
|
-
* autocomplete="country-name"
|
|
45
|
-
* filterable={true}
|
|
46
|
-
* items={[{
|
|
47
|
-
* type: 'option',
|
|
48
|
-
* value: 'GB',
|
|
49
|
-
* filterMatchers: ['United Kingdom', 'UK']
|
|
50
|
-
* }]}
|
|
51
|
-
* />
|
|
52
|
-
*/
|
|
53
|
-
autocomplete?: string;
|
|
54
35
|
defaultValue?: M extends true ? readonly T[] : T;
|
|
55
36
|
value?: M extends true ? readonly T[] : T;
|
|
56
37
|
compareValues?: (keyof NonNullable<T> & string) | ((a: T | undefined, b: T | undefined) => boolean);
|
|
@@ -86,7 +67,7 @@ export interface SelectInputProps<T = string, M extends boolean = false> {
|
|
|
86
67
|
onClose?: () => void;
|
|
87
68
|
onClear?: () => void;
|
|
88
69
|
}
|
|
89
|
-
export declare function SelectInput<T = string, M extends boolean = false>({ id: idProp, parentId, name, multiple, placeholder,
|
|
70
|
+
export declare function SelectInput<T = string, M extends boolean = false>({ id: idProp, parentId, name, multiple, placeholder, items, defaultValue, value: controlledValue, compareValues, renderValue, renderFooter, renderTrigger, filterable, filterPlaceholder, disabled, size, className, UNSAFE_triggerButtonProps, triggerRef: externalTriggerRef, onFilterChange, onChange, onOpen, onClose, onClear, }: SelectInputProps<T, M>): import("react").JSX.Element;
|
|
90
71
|
type SelectInputTriggerButtonElementType = 'button' | React.ComponentType;
|
|
91
72
|
export type SelectInputTriggerButtonProps<T extends SelectInputTriggerButtonElementType = 'button'> = Merge<React.ComponentPropsWithoutRef<T>, {
|
|
92
73
|
as?: T;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SelectInput.d.ts","sourceRoot":"","sources":["../../../src/inputs/SelectInput.tsx"],"names":[],"mappings":"AAIA,OAAO,EAGL,SAAS,EAQV,MAAM,OAAO,CAAC;AASf,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAKjC,OAAO,EAAsB,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAM1E,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAsBrD,MAAM,WAAW,qBAAqB,CAAC,CAAC,GAAG,MAAM;IAC/C,IAAI,EAAE,QAAQ,CAAC;IACf,KAAK,EAAE,CAAC,CAAC;IACT,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB,CAAC,CAAC,GAAG,MAAM;IAC9C,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,SAAS,CAAC;IACjB,OAAO,EAAE,SAAS,qBAAqB,CAAC,CAAC,CAAC,EAAE,CAAC;IAC7C,MAAM,CAAC,EAAE;QACP,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;KACjC,CAAC;CACH;AAED,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,WAAW,CAAC;CACnB;AAED,MAAM,MAAM,eAAe,CAAC,CAAC,GAAG,MAAM,IAClC,qBAAqB,CAAC,CAAC,CAAC,GACxB,oBAAoB,CAAC,CAAC,CAAC,GACvB,wBAAwB,CAAC;AAiE7B,MAAM,WAAW,gBAAgB,CAAC,CAAC,GAAG,MAAM,EAAE,CAAC,SAAS,OAAO,GAAG,KAAK;IACrE,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,CAAC,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,SAAS,eAAe,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAClD
|
|
1
|
+
{"version":3,"file":"SelectInput.d.ts","sourceRoot":"","sources":["../../../src/inputs/SelectInput.tsx"],"names":[],"mappings":"AAIA,OAAO,EAGL,SAAS,EAQV,MAAM,OAAO,CAAC;AASf,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAKjC,OAAO,EAAsB,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAM1E,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAsBrD,MAAM,WAAW,qBAAqB,CAAC,CAAC,GAAG,MAAM;IAC/C,IAAI,EAAE,QAAQ,CAAC;IACf,KAAK,EAAE,CAAC,CAAC;IACT,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB,CAAC,CAAC,GAAG,MAAM;IAC9C,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,SAAS,CAAC;IACjB,OAAO,EAAE,SAAS,qBAAqB,CAAC,CAAC,CAAC,EAAE,CAAC;IAC7C,MAAM,CAAC,EAAE;QACP,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;KACjC,CAAC;CACH;AAED,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,WAAW,CAAC;CACnB;AAED,MAAM,MAAM,eAAe,CAAC,CAAC,GAAG,MAAM,IAClC,qBAAqB,CAAC,CAAC,CAAC,GACxB,oBAAoB,CAAC,CAAC,CAAC,GACvB,wBAAwB,CAAC;AAiE7B,MAAM,WAAW,gBAAgB,CAAC,CAAC,GAAG,MAAM,EAAE,CAAC,SAAS,OAAO,GAAG,KAAK;IACrE,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,CAAC,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,SAAS,eAAe,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAClD,YAAY,CAAC,EAAE,CAAC,SAAS,IAAI,GAAG,SAAS,CAAC,EAAE,GAAG,CAAC,CAAC;IACjD,KAAK,CAAC,EAAE,CAAC,SAAS,IAAI,GAAG,SAAS,CAAC,EAAE,GAAG,CAAC,CAAC;IAC1C,aAAa,CAAC,EACV,CAAC,MAAM,WAAW,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,GAC/B,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,SAAS,EAAE,CAAC,EAAE,CAAC,GAAG,SAAS,KAAK,OAAO,CAAC,CAAC;IACtD,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,OAAO,KAAK,KAAK,CAAC,SAAS,CAAC;IACjF,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE;QACpB,YAAY,EAAE,OAAO,CAAC;QACtB,eAAe,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;KAC5C,KAAK,KAAK,CAAC,SAAS,CAAC;IACtB,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE;QACrB,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC;QACzB,gBAAgB,EAAE,OAAO,CAAC;QAC1B,KAAK,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC;QAChC,QAAQ,EAAE,OAAO,CAAC;QAClB,IAAI,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;QACzB,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;KAC/B,KAAK,KAAK,CAAC,SAAS,CAAC;IACtB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yBAAyB,CAAC,EAAE,wBAAwB,CAAC,iBAAiB,CAAC,GAAG;QACxE,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB,CAAC;IACF,gDAAgD;IAChD,UAAU,CAAC,EAAE,KAAK,CAAC,gBAAgB,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAC;IAC9D,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,KAAK,IAAI,CAAC;IACnF,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,SAAS,IAAI,GAAG,CAAC,EAAE,GAAG,CAAC,KAAK,IAAI,CAAC;IACrD,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB;AA+DD,wBAAgB,WAAW,CAAC,CAAC,GAAG,MAAM,EAAE,CAAC,SAAS,OAAO,GAAG,KAAK,EAAE,EACjE,EAAE,EAAE,MAAM,EACV,QAAQ,EACR,IAAI,EACJ,QAAQ,EACR,WAAW,EACX,KAAK,EACL,YAAY,EACZ,KAAK,EAAE,eAAe,EACtB,aAAa,EACb,WAAoB,EACpB,YAAY,EACZ,aAAoC,EACpC,UAAU,EACV,iBAAiB,EACjB,QAAQ,EACR,IAAW,EACX,SAAS,EACT,yBAAyB,EACzB,UAAU,EAAE,kBAAkB,EAC9B,cAAqB,EACrB,QAAQ,EACR,MAAM,EACN,OAAO,EACP,OAAO,GACR,EAAE,gBAAgB,CAAC,CAAC,EAAE,CAAC,CAAC,+BA6LxB;AAUD,KAAK,mCAAmC,GAAG,QAAQ,GAAG,KAAK,CAAC,aAAa,CAAC;AAE1E,MAAM,MAAM,6BAA6B,CACvC,CAAC,SAAS,mCAAmC,GAAG,QAAQ,IACtD,KAAK,CAAC,KAAK,CAAC,wBAAwB,CAAC,CAAC,CAAC,EAAE;IAAE,EAAE,CAAC,EAAE,CAAC,CAAA;CAAE,CAAC,CAAC;AAEzD,wBAAgB,wBAAwB,CAAC,CAAC,SAAS,mCAAmC,GAAG,QAAQ,EAAE,EACjG,EAAkB,EAClB,GAAG,SAAS,EACb,EAAE,6BAA6B,CAAC,CAAC,CAAC,+BAclC;AAsbD,MAAM,WAAW,6BAA6B;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CACxB;AAED,wBAAgB,wBAAwB,CAAC,EACvC,KAAK,EACL,IAAI,EACJ,WAAW,EACX,IAAI,GACL,EAAE,6BAA6B,+BAiD/B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"_BottomSheet.d.ts","sourceRoot":"","sources":["../../../src/inputs/_BottomSheet.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"_BottomSheet.d.ts","sourceRoot":"","sources":["../../../src/inputs/_BottomSheet.tsx"],"names":[],"mappings":"AAkBA,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,OAAO,CAAC;IACd,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE;QACrB,GAAG,EAAE,KAAK,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;QAChC,mBAAmB,EAAE,CAAC,mBAAmB,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,OAAO,CAAC,KAAK;YACvE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;SACxB,CAAC;KACH,KAAK,KAAK,CAAC,SAAS,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,eAAe,CAAC,EAAE,KAAK,CAAC,gBAAgB,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC7D,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;CACzB;AAeD,wBAAgB,WAAW,CAAC,EAC1B,IAAI,EACJ,aAAa,EACb,KAAK,EACL,eAAe,EACf,OAAc,EACd,QAAQ,EACR,OAAO,EACP,UAAU,GACX,EAAE,gBAAgB,+BAiGlB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@transferwise/components",
|
|
3
|
-
"version": "0.0.0-experimental-
|
|
3
|
+
"version": "0.0.0-experimental-07a2614",
|
|
4
4
|
"description": "Neptune React components",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -102,8 +102,8 @@
|
|
|
102
102
|
"@floating-ui/react": "^0.27.16",
|
|
103
103
|
"@headlessui/react": "^1.7.19",
|
|
104
104
|
"@popperjs/core": "^2.11.8",
|
|
105
|
-
"@react-aria/focus": "^3.21.
|
|
106
|
-
"@react-aria/overlays": "^3.
|
|
105
|
+
"@react-aria/focus": "^3.21.1",
|
|
106
|
+
"@react-aria/overlays": "^3.29.1",
|
|
107
107
|
"@react-spring/web": "~10.0.1",
|
|
108
108
|
"@transferwise/formatting": "^2.13.4",
|
|
109
109
|
"@transferwise/neptune-validation": "^3.3.1",
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { Meta, StoryObj } from '@storybook/react-webpack5';
|
|
3
|
+
import { userEvent, within } from 'storybook/test';
|
|
4
|
+
import { lorem100, lorem500 } from '../../test-utils';
|
|
5
|
+
import { Typography } from '../propsValues/typography';
|
|
6
|
+
import Alert from '../../alert';
|
|
7
|
+
import Body from '../../body';
|
|
8
|
+
import Button from '../../button';
|
|
9
|
+
import Title from '../../title';
|
|
10
|
+
import BottomSheet from './BottomSheet';
|
|
11
|
+
|
|
12
|
+
const wait = async (duration = 500) =>
|
|
13
|
+
new Promise<void>((resolve) => {
|
|
14
|
+
setTimeout(resolve, duration);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export default {
|
|
18
|
+
component: BottomSheet,
|
|
19
|
+
title: 'Dialogs/BottomSheet/tests',
|
|
20
|
+
tags: ['!autodocs'],
|
|
21
|
+
args: {
|
|
22
|
+
open: false,
|
|
23
|
+
},
|
|
24
|
+
} satisfies Meta<typeof BottomSheet>;
|
|
25
|
+
|
|
26
|
+
type Story = StoryObj<typeof BottomSheet>;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* This test ensures that when the SelectInput is used within a scrollable page,
|
|
30
|
+
* opening the dropdown does not cause any unwanted scrolling or layout shifts.
|
|
31
|
+
* Expected preview should start with green bar at the top, with yellow section
|
|
32
|
+
* not in the viewport.
|
|
33
|
+
*
|
|
34
|
+
* NB: This test is disabled in Chromatic as there's no obvious way to control <html/> element of a snapshot.
|
|
35
|
+
*/
|
|
36
|
+
export const SmoothScrollReset: Story = {
|
|
37
|
+
args: {
|
|
38
|
+
children: (
|
|
39
|
+
<>
|
|
40
|
+
<Title type={Typography.TITLE_SECTION}>Observe the document</Title>
|
|
41
|
+
<Alert className="m-t-2" type="warning">
|
|
42
|
+
Once the <code>BottomSheet</code> opens, the document underneath should be static and
|
|
43
|
+
should not scroll.
|
|
44
|
+
</Alert>
|
|
45
|
+
<Body as="p">{lorem100}</Body>
|
|
46
|
+
<Body as="p">{lorem100}</Body>
|
|
47
|
+
</>
|
|
48
|
+
),
|
|
49
|
+
},
|
|
50
|
+
decorators: [
|
|
51
|
+
(Story) => (
|
|
52
|
+
<>
|
|
53
|
+
<style>{'html { scroll-behavior: smooth; }'}</style>
|
|
54
|
+
<div style={{ maxWidth: 500 }}>
|
|
55
|
+
<Body>
|
|
56
|
+
<p>{lorem100}</p>
|
|
57
|
+
<p>{lorem100}</p>
|
|
58
|
+
</Body>
|
|
59
|
+
<Story />
|
|
60
|
+
<Body as="p" className="m-t-5 disabled">
|
|
61
|
+
{lorem500}
|
|
62
|
+
</Body>
|
|
63
|
+
</div>
|
|
64
|
+
</>
|
|
65
|
+
),
|
|
66
|
+
],
|
|
67
|
+
parameters: {
|
|
68
|
+
chromatic: {
|
|
69
|
+
disableSnapshot: true,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
play: async ({ canvasElement }) => {
|
|
73
|
+
document.documentElement.scrollTop = 400;
|
|
74
|
+
await wait(500);
|
|
75
|
+
const canvas = within(canvasElement);
|
|
76
|
+
await userEvent.click(canvas.getByRole('button'));
|
|
77
|
+
},
|
|
78
|
+
render: ({ open, ...args }) => {
|
|
79
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div>
|
|
83
|
+
<Button onClick={() => setIsOpen(true)}>Open BottomSheet</Button>
|
|
84
|
+
<BottomSheet
|
|
85
|
+
{...args}
|
|
86
|
+
open={isOpen}
|
|
87
|
+
onClose={() => {
|
|
88
|
+
setIsOpen(false);
|
|
89
|
+
}}
|
|
90
|
+
/>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
},
|
|
94
|
+
};
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
.wds-select-input-scroll-freeze {
|
|
2
|
+
scroll-behavior: unset !important;
|
|
3
|
+
height: 100vh;
|
|
4
|
+
}
|
|
5
|
+
.wds-select-input-scroll-freeze body {
|
|
6
|
+
height: 100vh;
|
|
7
|
+
}
|
|
1
8
|
.np-bottom-sheet-v2-container {
|
|
2
9
|
position: relative;
|
|
3
10
|
z-index: 1060;
|
|
@@ -11,6 +18,8 @@
|
|
|
11
18
|
transition-property: opacity;
|
|
12
19
|
transition-timing-function: ease-out;
|
|
13
20
|
transition-duration: 300ms;
|
|
21
|
+
will-change: transform;
|
|
22
|
+
min-height: 100vh;
|
|
14
23
|
}
|
|
15
24
|
.np-bottom-sheet-v2-backdrop--closed {
|
|
16
25
|
opacity: 0;
|
|
@@ -18,7 +27,7 @@
|
|
|
18
27
|
.np-bottom-sheet-v2 {
|
|
19
28
|
position: fixed;
|
|
20
29
|
inset: 0px;
|
|
21
|
-
bottom: env(
|
|
30
|
+
bottom: env(safe-area-inset-bottom, 0);
|
|
22
31
|
margin-left: 8px;
|
|
23
32
|
margin-left: var(--size-8);
|
|
24
33
|
margin-right: 8px;
|
|
@@ -28,18 +37,21 @@
|
|
|
28
37
|
display: flex;
|
|
29
38
|
flex-direction: column;
|
|
30
39
|
justify-content: flex-end;
|
|
40
|
+
height: calc(100vh - 64px - 40px);
|
|
41
|
+
height: calc(100vh - var(--size-64) - 40px);
|
|
31
42
|
}
|
|
32
43
|
.np-bottom-sheet-v2-content {
|
|
33
44
|
display: flex;
|
|
34
45
|
flex-direction: column;
|
|
35
46
|
overflow: auto;
|
|
36
|
-
border-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
47
|
+
border-radius: 32px;
|
|
48
|
+
border-radius: var(--size-32);
|
|
49
|
+
margin-bottom: 8px;
|
|
50
|
+
margin-bottom: var(--size-8);
|
|
40
51
|
background-color: #ffffff;
|
|
41
52
|
background-color: var(--color-background-elevated);
|
|
42
53
|
box-shadow: 0 0 40px rgba(69, 71, 69, 0.2);
|
|
54
|
+
will-change: transform;
|
|
43
55
|
}
|
|
44
56
|
.np-bottom-sheet-v2-content:focus {
|
|
45
57
|
outline: none;
|
|
@@ -3,13 +3,21 @@ import { userEvent } from '@testing-library/user-event';
|
|
|
3
3
|
|
|
4
4
|
import { render, mockMatchMedia, mockResizeObserver } from '../test-utils';
|
|
5
5
|
|
|
6
|
+
import { useScreenSize } from '../common/hooks/useScreenSize';
|
|
6
7
|
import { SelectInput, type SelectInputOptionItem, type SelectInputProps } from './SelectInput';
|
|
7
8
|
import { Field } from '../field/Field';
|
|
8
9
|
|
|
9
10
|
mockMatchMedia();
|
|
10
11
|
mockResizeObserver();
|
|
12
|
+
jest.mock('../common/hooks/useScreenSize');
|
|
11
13
|
|
|
12
14
|
describe('SelectInput', () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
(useScreenSize as jest.Mock).mockReturnValue(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(jest.clearAllMocks);
|
|
20
|
+
|
|
13
21
|
it('renders placeholder', () => {
|
|
14
22
|
render(
|
|
15
23
|
<SelectInput
|
|
@@ -309,4 +317,29 @@ describe('SelectInput', () => {
|
|
|
309
317
|
expect(listBox).not.toHaveAttribute('aria-labelledby');
|
|
310
318
|
});
|
|
311
319
|
});
|
|
320
|
+
|
|
321
|
+
describe('smooth scroll reset', () => {
|
|
322
|
+
const options: SelectInputOptionItem[] = [
|
|
323
|
+
{ type: 'option', value: 'Banana' },
|
|
324
|
+
{ type: 'option', value: 'Orange' },
|
|
325
|
+
{ type: 'option', value: 'Olive' },
|
|
326
|
+
];
|
|
327
|
+
|
|
328
|
+
beforeEach(() => {
|
|
329
|
+
(useScreenSize as jest.Mock).mockReturnValue(false);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
afterEach(jest.clearAllMocks);
|
|
333
|
+
|
|
334
|
+
it('toggles scroll-resetting class', async () => {
|
|
335
|
+
render(<SelectInput items={options} />);
|
|
336
|
+
expect(document.documentElement).not.toHaveClass('wds-select-input-scroll-freeze');
|
|
337
|
+
|
|
338
|
+
await userEvent.click(screen.getByRole('combobox'));
|
|
339
|
+
expect(document.documentElement).toHaveClass('wds-select-input-scroll-freeze');
|
|
340
|
+
|
|
341
|
+
await userEvent.click(document.documentElement);
|
|
342
|
+
expect(document.documentElement).not.toHaveClass('wds-select-input-scroll-freeze');
|
|
343
|
+
});
|
|
344
|
+
});
|
|
312
345
|
});
|
|
@@ -516,97 +516,3 @@ export const WithinModal: Story<Currency> = {
|
|
|
516
516
|
},
|
|
517
517
|
],
|
|
518
518
|
};
|
|
519
|
-
|
|
520
|
-
interface Country {
|
|
521
|
-
code: string;
|
|
522
|
-
name: string;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
const countries: Country[] = [
|
|
526
|
-
{ code: 'US', name: 'United States' },
|
|
527
|
-
{ code: 'GB', name: 'United Kingdom' },
|
|
528
|
-
{ code: 'CA', name: 'Canada' },
|
|
529
|
-
{ code: 'AU', name: 'Australia' },
|
|
530
|
-
{ code: 'DE', name: 'Germany' },
|
|
531
|
-
{ code: 'FR', name: 'France' },
|
|
532
|
-
{ code: 'JP', name: 'Japan' },
|
|
533
|
-
{ code: 'BR', name: 'Brazil' },
|
|
534
|
-
{ code: 'IN', name: 'India' },
|
|
535
|
-
{ code: 'CN', name: 'China' },
|
|
536
|
-
{ code: 'IT', name: 'Italy' },
|
|
537
|
-
{ code: 'ES', name: 'Spain' },
|
|
538
|
-
{ code: 'NL', name: 'Netherlands' },
|
|
539
|
-
{ code: 'CH', name: 'Switzerland' },
|
|
540
|
-
{ code: 'SE', name: 'Sweden' },
|
|
541
|
-
];
|
|
542
|
-
|
|
543
|
-
function countryOption(country: Country) {
|
|
544
|
-
return {
|
|
545
|
-
type: 'option',
|
|
546
|
-
value: country.code,
|
|
547
|
-
filterMatchers: [country.code, country.name],
|
|
548
|
-
} satisfies SelectInputItem;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
export const WithAutocomplete: Story<string> = {
|
|
552
|
-
args: {
|
|
553
|
-
name: 'country',
|
|
554
|
-
autocomplete: 'country-name',
|
|
555
|
-
placeholder: 'Select your country',
|
|
556
|
-
items: countries.map(countryOption),
|
|
557
|
-
renderValue: (countryCode, withinTrigger) => {
|
|
558
|
-
const country = countries.find((c) => c.code === countryCode);
|
|
559
|
-
return (
|
|
560
|
-
<SelectInputOptionContent
|
|
561
|
-
title={withinTrigger ? countryCode : country?.name || countryCode}
|
|
562
|
-
note={withinTrigger ? undefined : countryCode}
|
|
563
|
-
icon={<Flag code={countryCode} intrinsicSize={24} />}
|
|
564
|
-
/>
|
|
565
|
-
);
|
|
566
|
-
},
|
|
567
|
-
filterable: true,
|
|
568
|
-
filterPlaceholder: 'Type a country name',
|
|
569
|
-
size: 'lg',
|
|
570
|
-
},
|
|
571
|
-
render: function Render({ onChange, onClear, ...args }) {
|
|
572
|
-
const [selectedCountry, setSelectedCountry] = useState<string | undefined>(undefined);
|
|
573
|
-
|
|
574
|
-
return (
|
|
575
|
-
<div>
|
|
576
|
-
<form
|
|
577
|
-
method="post"
|
|
578
|
-
onSubmit={(e) => {
|
|
579
|
-
e.preventDefault();
|
|
580
|
-
console.log(
|
|
581
|
-
`Form submitted with country: ${selectedCountry}. This saves data for browser autocomplete!`,
|
|
582
|
-
);
|
|
583
|
-
}}
|
|
584
|
-
>
|
|
585
|
-
<div>
|
|
586
|
-
<label htmlFor="country-select" className="block text-sm font-medium mb-2">
|
|
587
|
-
Country Selection with Autocomplete:
|
|
588
|
-
</label>
|
|
589
|
-
<SelectInput
|
|
590
|
-
{...args}
|
|
591
|
-
id="country-select"
|
|
592
|
-
value={selectedCountry}
|
|
593
|
-
onChange={(country) => {
|
|
594
|
-
setSelectedCountry(country);
|
|
595
|
-
onChange?.(country);
|
|
596
|
-
console.log('Country selected via SelectInput:', country);
|
|
597
|
-
}}
|
|
598
|
-
onClear={() => {
|
|
599
|
-
setSelectedCountry(undefined);
|
|
600
|
-
onClear?.();
|
|
601
|
-
}}
|
|
602
|
-
/>
|
|
603
|
-
</div>
|
|
604
|
-
|
|
605
|
-
<Button type="submit" v2 className="m-t-2" data-testid="submit-btn">
|
|
606
|
-
Submit Form
|
|
607
|
-
</Button>
|
|
608
|
-
</form>
|
|
609
|
-
</div>
|
|
610
|
-
);
|
|
611
|
-
},
|
|
612
|
-
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-webpack5';
|
|
2
|
+
import { fn, type Mock, userEvent, within } from 'storybook/test';
|
|
3
|
+
import { allModes } from '../../.storybook/modes';
|
|
4
|
+
import { lorem5, lorem500 } from '../test-utils';
|
|
5
|
+
import { Field } from '../field/Field';
|
|
6
|
+
import Body from '../body';
|
|
7
|
+
import { SelectInput, type SelectInputProps } from './SelectInput';
|
|
8
|
+
|
|
9
|
+
const meta = {
|
|
10
|
+
title: 'Forms/SelectInput/tests',
|
|
11
|
+
component: SelectInput,
|
|
12
|
+
args: {
|
|
13
|
+
onFilterChange: fn() satisfies Mock,
|
|
14
|
+
onChange: fn() satisfies Mock,
|
|
15
|
+
onClose: fn() satisfies Mock,
|
|
16
|
+
onOpen: fn() satisfies Mock,
|
|
17
|
+
},
|
|
18
|
+
tags: ['!autodocs'],
|
|
19
|
+
} satisfies Meta<typeof SelectInput>;
|
|
20
|
+
export default meta;
|
|
21
|
+
|
|
22
|
+
type Story<T, M extends boolean = false> = StoryObj<SelectInputProps<T, M>>;
|
|
23
|
+
|
|
24
|
+
const wait = async (duration = 500) =>
|
|
25
|
+
new Promise<void>((resolve) => {
|
|
26
|
+
setTimeout(resolve, duration);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* This test ensures that when the SelectInput is used within a scrollable page,
|
|
31
|
+
* opening the dropdown does not cause any unwanted scrolling or layout shifts.
|
|
32
|
+
* Expected preview should start with green bar at the top, with yellow section
|
|
33
|
+
* not in the viewport.
|
|
34
|
+
*
|
|
35
|
+
* NB: This test is disabled in Chromatic as there's no obvious way to control <html/> element of a snapshot.
|
|
36
|
+
*/
|
|
37
|
+
export const SmoothScrollReset: Story<string> = {
|
|
38
|
+
args: {
|
|
39
|
+
items: Array.from({ length: 15 }).map((_, id) => ({
|
|
40
|
+
type: 'option',
|
|
41
|
+
value: `option ${id + 1}`,
|
|
42
|
+
})),
|
|
43
|
+
placeholder: 'Select option',
|
|
44
|
+
},
|
|
45
|
+
decorators: [
|
|
46
|
+
(Story) => (
|
|
47
|
+
<>
|
|
48
|
+
<style>{`html { scroll-behavior: smooth; }`}</style>
|
|
49
|
+
<div>
|
|
50
|
+
<div
|
|
51
|
+
className="d-flex align-items-center justify-content-center"
|
|
52
|
+
style={{
|
|
53
|
+
backgroundColor: 'var(--color-bright-yellow)',
|
|
54
|
+
height: 400,
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
This block should not be in the viewport.
|
|
58
|
+
</div>
|
|
59
|
+
<div style={{ height: 10, backgroundColor: 'var(--color-bright-green)' }} />
|
|
60
|
+
<Field id="el1" label={lorem5}>
|
|
61
|
+
<Story />
|
|
62
|
+
</Field>
|
|
63
|
+
<Body as="p">{lorem500}</Body>
|
|
64
|
+
</div>
|
|
65
|
+
</>
|
|
66
|
+
),
|
|
67
|
+
],
|
|
68
|
+
play: async ({ canvasElement }) => {
|
|
69
|
+
document.documentElement.scrollTop = 400;
|
|
70
|
+
await wait();
|
|
71
|
+
const canvas = within(canvasElement);
|
|
72
|
+
const triggerButton = canvas.getByRole('combobox');
|
|
73
|
+
await userEvent.click(triggerButton);
|
|
74
|
+
},
|
|
75
|
+
globals: {
|
|
76
|
+
viewport: { value: allModes.largeMobile.viewport, isRotated: false },
|
|
77
|
+
},
|
|
78
|
+
parameters: {
|
|
79
|
+
chromatic: {
|
|
80
|
+
disableSnapshot: true,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
};
|
|
@@ -155,25 +155,6 @@ export interface SelectInputProps<T = string, M extends boolean = false> {
|
|
|
155
155
|
multiple?: M;
|
|
156
156
|
placeholder?: string;
|
|
157
157
|
items: readonly SelectInputItem<NonNullable<T>>[];
|
|
158
|
-
/**
|
|
159
|
-
* Enables browser autocomplete integration through the search input.
|
|
160
|
-
* Accepts standard HTML autocomplete values (e.g., "country-name", "address-level1").
|
|
161
|
-
*
|
|
162
|
-
* Requires `filterable={true}` to enable the search input.
|
|
163
|
-
*
|
|
164
|
-
* @example
|
|
165
|
-
* <SelectInput
|
|
166
|
-
* name="country"
|
|
167
|
-
* autocomplete="country-name"
|
|
168
|
-
* filterable={true}
|
|
169
|
-
* items={[{
|
|
170
|
-
* type: 'option',
|
|
171
|
-
* value: 'GB',
|
|
172
|
-
* filterMatchers: ['United Kingdom', 'UK']
|
|
173
|
-
* }]}
|
|
174
|
-
* />
|
|
175
|
-
*/
|
|
176
|
-
autocomplete?: string;
|
|
177
158
|
defaultValue?: M extends true ? readonly T[] : T;
|
|
178
159
|
value?: M extends true ? readonly T[] : T;
|
|
179
160
|
compareValues?:
|
|
@@ -276,7 +257,6 @@ export function SelectInput<T = string, M extends boolean = false>({
|
|
|
276
257
|
name,
|
|
277
258
|
multiple,
|
|
278
259
|
placeholder,
|
|
279
|
-
autocomplete,
|
|
280
260
|
items,
|
|
281
261
|
defaultValue,
|
|
282
262
|
value: controlledValue,
|
|
@@ -477,15 +457,7 @@ export function SelectInput<T = string, M extends boolean = false>({
|
|
|
477
457
|
searchInputRef={searchInputRef}
|
|
478
458
|
listboxRef={listboxRef}
|
|
479
459
|
filterQuery={deferredFilterQuery}
|
|
480
|
-
autocomplete={autocomplete}
|
|
481
|
-
name={name}
|
|
482
460
|
onFilterChange={setFilterQuery}
|
|
483
|
-
onAutocompleteSelect={(matchedValue) => {
|
|
484
|
-
onChange?.(matchedValue as M extends true ? T[] : T);
|
|
485
|
-
if (!multiple) {
|
|
486
|
-
setOpen(false);
|
|
487
|
-
}
|
|
488
|
-
}}
|
|
489
461
|
{...getListBoxLabelProps()}
|
|
490
462
|
/>
|
|
491
463
|
</OptionsOverlay>
|
|
@@ -593,9 +565,6 @@ interface SelectInputOptionsProps<T = string>
|
|
|
593
565
|
onFilterChange: (query: string) => void;
|
|
594
566
|
listBoxLabel?: string;
|
|
595
567
|
listBoxLabelledBy?: string;
|
|
596
|
-
autocomplete?: string;
|
|
597
|
-
name?: string;
|
|
598
|
-
onAutocompleteSelect?: (value: T) => void;
|
|
599
568
|
}
|
|
600
569
|
|
|
601
570
|
function SelectInputOptions<T = string>({
|
|
@@ -612,9 +581,6 @@ function SelectInputOptions<T = string>({
|
|
|
612
581
|
onFilterChange,
|
|
613
582
|
listBoxLabel,
|
|
614
583
|
listBoxLabelledBy,
|
|
615
|
-
autocomplete,
|
|
616
|
-
name,
|
|
617
|
-
onAutocompleteSelect,
|
|
618
584
|
}: SelectInputOptionsProps<T>) {
|
|
619
585
|
const intl = useIntl();
|
|
620
586
|
const virtualiserHandlerRef = useRef<VirtualizerHandle>(null);
|
|
@@ -698,35 +664,6 @@ function SelectInputOptions<T = string>({
|
|
|
698
664
|
);
|
|
699
665
|
};
|
|
700
666
|
|
|
701
|
-
const findMatchingItem = (autocompleteValue: string): T | null => {
|
|
702
|
-
const flatOptions = items
|
|
703
|
-
.flatMap((item) =>
|
|
704
|
-
item.type === 'group' ? item.options : item.type === 'option' ? [item] : [],
|
|
705
|
-
)
|
|
706
|
-
.filter(
|
|
707
|
-
(item): item is SelectInputOptionItem<NonNullable<T>> =>
|
|
708
|
-
item.type === 'option' && item.value != null,
|
|
709
|
-
);
|
|
710
|
-
|
|
711
|
-
const exactMatch = flatOptions.find(
|
|
712
|
-
(option) =>
|
|
713
|
-
String(option.value) === autocompleteValue ||
|
|
714
|
-
option.filterMatchers?.some((matcher) => matcher === autocompleteValue),
|
|
715
|
-
);
|
|
716
|
-
|
|
717
|
-
if (exactMatch) {
|
|
718
|
-
return exactMatch.value;
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
const fuzzyMatch = flatOptions.find((option) =>
|
|
722
|
-
option.filterMatchers?.some((matcher) =>
|
|
723
|
-
matcher.toLowerCase().includes(autocompleteValue.toLowerCase()),
|
|
724
|
-
),
|
|
725
|
-
);
|
|
726
|
-
|
|
727
|
-
return fuzzyMatch ? fuzzyMatch.value : null;
|
|
728
|
-
};
|
|
729
|
-
|
|
730
667
|
return (
|
|
731
668
|
<ListboxBase.Options
|
|
732
669
|
as={SelectInputOptionsContainer}
|
|
@@ -747,8 +684,6 @@ function SelectInputOptions<T = string>({
|
|
|
747
684
|
<SearchInput
|
|
748
685
|
ref={searchInputRef}
|
|
749
686
|
id={id}
|
|
750
|
-
name={name}
|
|
751
|
-
autoComplete={autocomplete}
|
|
752
687
|
role="combobox"
|
|
753
688
|
shape="rectangle"
|
|
754
689
|
placeholder={filterPlaceholder}
|
|
@@ -768,26 +703,8 @@ function SelectInputOptions<T = string>({
|
|
|
768
703
|
onChange={(event) => {
|
|
769
704
|
// Free up resources and ensure not to go out of bounds when the
|
|
770
705
|
// resulting item count is less than before
|
|
771
|
-
const inputValue = event.currentTarget.value;
|
|
772
|
-
|
|
773
|
-
// Free up resources and ensure not to go out of bounds
|
|
774
706
|
setMountedIndexes([]);
|
|
775
|
-
onFilterChange(
|
|
776
|
-
}}
|
|
777
|
-
onInput={(event) => {
|
|
778
|
-
const inputValue = event.currentTarget.value;
|
|
779
|
-
const inputElement = event.currentTarget;
|
|
780
|
-
|
|
781
|
-
if (autocomplete && onAutocompleteSelect && inputValue) {
|
|
782
|
-
setTimeout(() => {
|
|
783
|
-
if (inputElement.value === inputValue && inputValue.length > 2) {
|
|
784
|
-
const matchedValue = findMatchingItem(inputValue);
|
|
785
|
-
if (matchedValue !== null) {
|
|
786
|
-
onAutocompleteSelect(matchedValue);
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
}, 50);
|
|
790
|
-
}
|
|
707
|
+
onFilterChange(event.currentTarget.value);
|
|
791
708
|
}}
|
|
792
709
|
/>
|
|
793
710
|
</div>
|