@kaizen/components 3.1.6 → 3.3.0
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/dist/cjs/src/Avatar/Avatar.cjs +18 -14
- package/dist/cjs/src/DateInput/DateInput/DateInput.cjs +1 -2
- package/dist/cjs/src/DatePicker/DatePicker.cjs +6 -4
- package/dist/cjs/src/Focusable/Focusable.cjs +5 -4
- package/dist/cjs/src/MultiSelect/MultiSelect.cjs +4 -2
- package/dist/cjs/src/RichTextEditor/RichTextEditor/RichTextEditor.cjs +4 -1
- package/dist/cjs/src/RichTextEditor/RichTextEditor/utils/inputrules.cjs +0 -1
- package/dist/cjs/src/RichTextEditor/utils/core/createRichTextEditor.cjs +2 -1
- package/dist/cjs/src/RichTextEditor/utils/core/hooks/useRichTextEditor.cjs +24 -9
- package/dist/cjs/src/SingleSelect/SingleSelect.cjs +4 -2
- package/dist/cjs/src/Tabs/subcomponents/TabList/TabList.cjs +8 -1
- package/dist/cjs/src/TimeField/TimeField.cjs +9 -4
- package/dist/cjs/src/TimeField/subcomponents/TimeSegment/TimeSegment.cjs +6 -4
- package/dist/esm/src/Avatar/Avatar.mjs +18 -15
- package/dist/esm/src/DateInput/DateInput/DateInput.mjs +1 -2
- package/dist/esm/src/DatePicker/DatePicker.mjs +6 -4
- package/dist/esm/src/Focusable/Focusable.mjs +5 -4
- package/dist/esm/src/MultiSelect/MultiSelect.mjs +4 -2
- package/dist/esm/src/RichTextEditor/RichTextEditor/RichTextEditor.mjs +4 -1
- package/dist/esm/src/RichTextEditor/RichTextEditor/utils/inputrules.mjs +0 -1
- package/dist/esm/src/RichTextEditor/utils/core/createRichTextEditor.mjs +2 -1
- package/dist/esm/src/RichTextEditor/utils/core/hooks/useRichTextEditor.mjs +24 -9
- package/dist/esm/src/SingleSelect/SingleSelect.mjs +4 -2
- package/dist/esm/src/Tabs/subcomponents/TabList/TabList.mjs +8 -1
- package/dist/esm/src/TimeField/TimeField.mjs +9 -4
- package/dist/esm/src/TimeField/subcomponents/TimeSegment/TimeSegment.mjs +6 -4
- package/dist/styles.css +15 -15
- package/dist/types/DateInput/DateInputWithIconButton/DateInputWithIconButton.d.ts +2 -2
- package/dist/types/DatePicker/DatePicker.d.ts +3 -2
- package/dist/types/Focusable/Focusable.d.ts +8 -3
- package/dist/types/Input/Input/Input.d.ts +1 -1
- package/dist/types/MultiSelect/MultiSelect.d.ts +3 -2
- package/dist/types/Notification/index.d.ts +1 -0
- package/dist/types/RichTextEditor/RichTextEditor/RichTextEditor.d.ts +2 -1
- package/dist/types/RichTextEditor/utils/core/createRichTextEditor.d.ts +1 -0
- package/dist/types/RichTextEditor/utils/core/hooks/useRichTextEditor.d.ts +3 -2
- package/dist/types/SingleSelect/SingleSelect.d.ts +3 -3
- package/dist/types/TextArea/TextArea.d.ts +1 -1
- package/dist/types/TimeField/TimeField.d.ts +1 -0
- package/dist/types/TimeField/subcomponents/TimeSegment/TimeSegment.d.ts +3 -1
- package/package.json +7 -7
- package/src/Avatar/Avatar.tsx +19 -11
- package/src/DateInput/DateInput/DateInput.tsx +1 -2
- package/src/DateInput/DateInputWithIconButton/DateInputWithIconButton.tsx +2 -2
- package/src/DatePicker/DatePicker.tsx +6 -3
- package/src/Focusable/Focusable.tsx +17 -7
- package/src/Input/Input/Input.tsx +1 -1
- package/src/MultiSelect/MultiSelect.tsx +4 -1
- package/src/Notification/index.ts +1 -0
- package/src/RichTextEditor/RichTextEditor/RichTextEditor.tsx +3 -0
- package/src/RichTextEditor/utils/core/createRichTextEditor.spec.ts +1 -1
- package/src/RichTextEditor/utils/core/createRichTextEditor.ts +2 -0
- package/src/RichTextEditor/utils/core/hooks/useRichTextEditor.spec.tsx +8 -3
- package/src/RichTextEditor/utils/core/hooks/useRichTextEditor.ts +20 -7
- package/src/SingleSelect/SingleSelect.tsx +5 -3
- package/src/Tabs/_docs/Tabs.spec.stories.tsx +39 -0
- package/src/Tabs/_docs/Tabs.stories.tsx +38 -2
- package/src/Tabs/subcomponents/TabList/TabList.tsx +9 -1
- package/src/TextArea/TextArea.tsx +1 -1
- package/src/TimeField/TimeField.tsx +17 -15
- package/src/TimeField/subcomponents/TimeSegment/TimeSegment.tsx +6 -3
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { useEffect, useId, useState } from 'react'
|
|
2
|
-
import { type UseFloatingReturn } from '@floating-ui/react-dom'
|
|
3
2
|
import { useButton } from '@react-aria/button'
|
|
4
3
|
import { HiddenSelect, useSelect } from '@react-aria/select'
|
|
4
|
+
import { mergeRefs } from '@react-aria/utils'
|
|
5
5
|
import { useSelectState, type SelectProps as AriaSelectProps } from '@react-stately/select'
|
|
6
6
|
import { type Key } from '@react-types/shared'
|
|
7
7
|
import classnames from 'classnames'
|
|
@@ -32,6 +32,7 @@ export type SingleSelectProps<Option extends SingleSelectOption = SingleSelectOp
|
|
|
32
32
|
*/
|
|
33
33
|
items: SingleSelectItem<Option>[]
|
|
34
34
|
id?: string
|
|
35
|
+
inputRef?: React.Ref<HTMLButtonElement>
|
|
35
36
|
/**
|
|
36
37
|
* Optional render function that allows custom rendering of the trigger button.
|
|
37
38
|
* The function receives the `selectToggleProps` and a `ref` to be applied
|
|
@@ -39,7 +40,7 @@ export type SingleSelectProps<Option extends SingleSelectOption = SingleSelectOp
|
|
|
39
40
|
*/
|
|
40
41
|
trigger?: (
|
|
41
42
|
selectToggleProps: SelectToggleProps & {
|
|
42
|
-
ref:
|
|
43
|
+
ref: React.Ref<HTMLButtonElement>
|
|
43
44
|
},
|
|
44
45
|
) => JSX.Element
|
|
45
46
|
/**
|
|
@@ -95,6 +96,7 @@ export const SingleSelect = <Option extends SingleSelectOption = SingleSelectOpt
|
|
|
95
96
|
isDisabled,
|
|
96
97
|
onSelectionChange,
|
|
97
98
|
portalContainerId,
|
|
99
|
+
inputRef,
|
|
98
100
|
...restProps
|
|
99
101
|
}: SingleSelectProps<Option>): JSX.Element => {
|
|
100
102
|
const { refs } = useFloating<HTMLButtonElement>()
|
|
@@ -153,7 +155,7 @@ export const SingleSelect = <Option extends SingleSelectOption = SingleSelectOpt
|
|
|
153
155
|
status,
|
|
154
156
|
'isDisabled': triggerProps.isDisabled,
|
|
155
157
|
isReversed,
|
|
156
|
-
'ref': refs.setReference,
|
|
158
|
+
'ref': mergeRefs(inputRef, refs.setReference),
|
|
157
159
|
'aria-describedby': classnames(validationMessage && validationId, description && descriptionId),
|
|
158
160
|
'aria-required': isRequired,
|
|
159
161
|
}
|
|
@@ -117,6 +117,45 @@ export const ArrowsShowingAndHidingRTL: Story = {
|
|
|
117
117
|
},
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
let scrollIntoViewCalls = 0
|
|
121
|
+
const originalScrollIntoView = HTMLElement.prototype.scrollIntoView
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The selected tab must NOT be scrolled into view on mount (KZN-3363) — only when
|
|
125
|
+
* the selection changes via user interaction. This protects users who render Tabs
|
|
126
|
+
* below the fold from having the page jump on load.
|
|
127
|
+
*/
|
|
128
|
+
export const ScrollsIntoViewOnSelectionOnly: Story = {
|
|
129
|
+
name: 'Scrolls into view on selection only',
|
|
130
|
+
beforeEach: () => {
|
|
131
|
+
scrollIntoViewCalls = 0
|
|
132
|
+
HTMLElement.prototype.scrollIntoView = function (): void {
|
|
133
|
+
scrollIntoViewCalls += 1
|
|
134
|
+
}
|
|
135
|
+
return () => {
|
|
136
|
+
HTMLElement.prototype.scrollIntoView = originalScrollIntoView
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
render: (args) => (
|
|
140
|
+
<div style={{ maxWidth: '500px' }}>
|
|
141
|
+
<Tabs defaultSelectedKey="one" {...args} />
|
|
142
|
+
</div>
|
|
143
|
+
),
|
|
144
|
+
play: async ({ canvasElement, step }) => {
|
|
145
|
+
const canvas = within(canvasElement.parentElement!)
|
|
146
|
+
|
|
147
|
+
await step('No scroll into view on mount', async () => {
|
|
148
|
+
expect(scrollIntoViewCalls).toBe(0)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
await step('Scrolls into view when a tab is selected', async () => {
|
|
152
|
+
const tabFour = await canvas.findByRole('tab', { name: 'Tab 4' })
|
|
153
|
+
await userEvent.click(tabFour)
|
|
154
|
+
await waitFor(() => expect(scrollIntoViewCalls).toBeGreaterThan(0))
|
|
155
|
+
})
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
|
|
120
159
|
export const AsyncLoaded: Story = {
|
|
121
160
|
render: () => {
|
|
122
161
|
const [selectedKey, setSelectedKey] = useState<Key>(0)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useState } from 'react'
|
|
2
2
|
import { type Meta, type StoryObj } from '@storybook/react'
|
|
3
|
-
import { Button } from '~components/
|
|
3
|
+
import { Button } from '~components/Button'
|
|
4
4
|
import { Text } from '~components/Text'
|
|
5
5
|
import { Tab, TabList, TabPanel, Tabs, type Key } from '../index'
|
|
6
6
|
|
|
@@ -82,8 +82,44 @@ export const Controlled: Story = {
|
|
|
82
82
|
<Text variant="body">Content 2</Text>
|
|
83
83
|
</TabPanel>
|
|
84
84
|
</Tabs>
|
|
85
|
-
<Button
|
|
85
|
+
<Button onClick={(): void => setSelectedKey('two')}>Switch to tab 2</Button>
|
|
86
86
|
</>
|
|
87
87
|
)
|
|
88
88
|
},
|
|
89
89
|
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* When a tab is selected via user interaction, `TabList` scrolls the selected tab
|
|
93
|
+
* into view within its horizontal strip. It deliberately does *not* do this on
|
|
94
|
+
* mount — so when `Tabs` render below the fold, landing on the page does not scroll
|
|
95
|
+
* the tabs into view or jump the page.
|
|
96
|
+
*
|
|
97
|
+
* This story pushes the `Tabs` below the fold with a full-viewport spacer. On load
|
|
98
|
+
* the page stays at the top. Scroll down and select a tab — only then does the
|
|
99
|
+
* selected tab scroll into view within the strip.
|
|
100
|
+
*/
|
|
101
|
+
export const BelowTheFold: Story = {
|
|
102
|
+
render: () => (
|
|
103
|
+
<div style={{ maxWidth: '760px' }}>
|
|
104
|
+
<div style={{ height: '100dvh' }}>
|
|
105
|
+
<Text variant="body">Scroll down — the tabs are rendered below the fold.</Text>
|
|
106
|
+
</div>
|
|
107
|
+
<Tabs defaultSelectedKey="tab-0">
|
|
108
|
+
<TabList aria-label="Lorem ipsum tabs">
|
|
109
|
+
{Array.from({ length: 8 }, (_, i) => (
|
|
110
|
+
<Tab key={i} id={`tab-${i}`}>
|
|
111
|
+
Lorem {i + 1}
|
|
112
|
+
</Tab>
|
|
113
|
+
))}
|
|
114
|
+
</TabList>
|
|
115
|
+
{Array.from({ length: 8 }, (_, i) => (
|
|
116
|
+
<TabPanel key={i} id={`tab-${i}`} className="p-24">
|
|
117
|
+
<Text variant="body">
|
|
118
|
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit — panel {i + 1}.
|
|
119
|
+
</Text>
|
|
120
|
+
</TabPanel>
|
|
121
|
+
))}
|
|
122
|
+
</Tabs>
|
|
123
|
+
</div>
|
|
124
|
+
),
|
|
125
|
+
}
|
|
@@ -44,6 +44,7 @@ export const TabList = (props: TabListProps): JSX.Element => {
|
|
|
44
44
|
const [containerElement, setContainerElement] = useState<HTMLElement | null>()
|
|
45
45
|
const tabListContext = useContext(TabListStateContext)
|
|
46
46
|
const selectedKey = tabListContext?.selectedKey
|
|
47
|
+
const prevSelectedKey = useRef(selectedKey)
|
|
47
48
|
|
|
48
49
|
useEffect(() => {
|
|
49
50
|
if (!isDocumentReady) {
|
|
@@ -107,7 +108,14 @@ export const TabList = (props: TabListProps): JSX.Element => {
|
|
|
107
108
|
return
|
|
108
109
|
}
|
|
109
110
|
|
|
110
|
-
//
|
|
111
|
+
// Only scroll the selected tab into view when the selection actually changes
|
|
112
|
+
// (i.e. user interaction). Skipping the no-op runs avoids scrolling the page
|
|
113
|
+
// on mount when the Tabs sit below the fold.
|
|
114
|
+
if (prevSelectedKey.current === selectedKey) {
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
prevSelectedKey.current = selectedKey
|
|
118
|
+
|
|
111
119
|
containerElement
|
|
112
120
|
?.querySelector('[role="tab"][data-selected=true]')
|
|
113
121
|
?.scrollIntoView({ block: 'nearest', inline: 'center' })
|
|
@@ -4,7 +4,7 @@ import { type OverrideClassName } from '~components/types/OverrideClassName'
|
|
|
4
4
|
import styles from './TextArea.module.css'
|
|
5
5
|
|
|
6
6
|
export type TextAreaProps = {
|
|
7
|
-
textAreaRef?: React.
|
|
7
|
+
textAreaRef?: React.Ref<HTMLTextAreaElement>
|
|
8
8
|
status?: 'default' | 'error' | 'caution'
|
|
9
9
|
/**
|
|
10
10
|
* Grows the input height as more content is added
|
|
@@ -27,6 +27,7 @@ export type TimeFieldProps = {
|
|
|
27
27
|
value: ValueType | null
|
|
28
28
|
status?: StatusType
|
|
29
29
|
validationMessage?: React.ReactNode
|
|
30
|
+
inputRef?: React.Ref<HTMLSpanElement>
|
|
30
31
|
} & OverrideClassName<Omit<TimeFieldStateOptions, OmittedTimeFieldProps>>
|
|
31
32
|
|
|
32
33
|
// This needed to be placed directly below the props because
|
|
@@ -49,6 +50,7 @@ const TimeFieldComponent = ({
|
|
|
49
50
|
validationMessage,
|
|
50
51
|
isDisabled,
|
|
51
52
|
classNameOverride,
|
|
53
|
+
inputRef,
|
|
52
54
|
...restProps
|
|
53
55
|
}: TimeFieldProps): JSX.Element => {
|
|
54
56
|
const reactId = useId()
|
|
@@ -79,7 +81,7 @@ const TimeFieldComponent = ({
|
|
|
79
81
|
const hasError = !!validationMessage && status === 'error'
|
|
80
82
|
const descriptionId = hasError ? `${id}-field-message` : undefined
|
|
81
83
|
|
|
82
|
-
const
|
|
84
|
+
const internalRef = React.useRef<HTMLDivElement>(null)
|
|
83
85
|
const { fieldProps, labelProps } = useTimeField(
|
|
84
86
|
{
|
|
85
87
|
...restProps,
|
|
@@ -88,36 +90,36 @@ const TimeFieldComponent = ({
|
|
|
88
90
|
'aria-describedby': descriptionId,
|
|
89
91
|
},
|
|
90
92
|
state,
|
|
91
|
-
|
|
93
|
+
internalRef,
|
|
92
94
|
)
|
|
95
|
+
const firstEditableIndex = state.segments.findIndex((s) => s.isEditable)
|
|
96
|
+
|
|
93
97
|
return (
|
|
94
98
|
<div className={classNameOverride}>
|
|
95
99
|
<Label disabled={state.isDisabled} {...labelProps} classNameOverride={styles.label}>
|
|
96
100
|
{label}
|
|
97
101
|
</Label>
|
|
98
102
|
<div className={styles.wrapper}>
|
|
99
|
-
{}
|
|
100
103
|
<div
|
|
101
104
|
{...fieldProps}
|
|
102
105
|
id={id}
|
|
103
|
-
ref={
|
|
106
|
+
ref={internalRef}
|
|
104
107
|
className={classnames(
|
|
105
108
|
styles.input,
|
|
106
109
|
state.isDisabled && styles.isDisabled,
|
|
107
110
|
status === 'error' && styles.error,
|
|
108
111
|
)}
|
|
109
112
|
>
|
|
110
|
-
{state.segments.map((segment, i) =>
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
})}
|
|
113
|
+
{state.segments.map((segment, i) => (
|
|
114
|
+
<TimeSegment
|
|
115
|
+
key={i}
|
|
116
|
+
segment={segment}
|
|
117
|
+
state={state}
|
|
118
|
+
inputRef={i === firstEditableIndex ? inputRef : undefined}
|
|
119
|
+
hasPadding={![8294, 8297].includes(segment.text.charCodeAt(0))}
|
|
120
|
+
// ^react-aria includes these characters to ensure correct RTL behaviour, but we want to avoid these adding random spacing
|
|
121
|
+
/>
|
|
122
|
+
))}
|
|
121
123
|
<div className={styles.focusRing} />
|
|
122
124
|
</div>
|
|
123
125
|
</div>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { useDateSegment } from '@react-aria/datepicker'
|
|
3
|
+
import { mergeRefs } from '@react-aria/utils'
|
|
3
4
|
import { type DateFieldState, type DateSegment } from '@react-stately/datepicker'
|
|
4
5
|
import classnames from 'classnames'
|
|
5
6
|
import { generateSegmentDisplayText } from './utils/generateSegmentDisplayText'
|
|
@@ -9,15 +10,17 @@ export type TimeSegmentProps = {
|
|
|
9
10
|
segment: DateSegment
|
|
10
11
|
state: DateFieldState
|
|
11
12
|
hasPadding?: boolean
|
|
13
|
+
inputRef?: React.Ref<HTMLSpanElement>
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
export const TimeSegment = ({
|
|
15
17
|
segment,
|
|
16
18
|
state,
|
|
17
19
|
hasPadding = true,
|
|
20
|
+
inputRef,
|
|
18
21
|
}: TimeSegmentProps): JSX.Element => {
|
|
19
|
-
const
|
|
20
|
-
const { segmentProps } = useDateSegment(segment, state,
|
|
22
|
+
const internalRef = React.useRef<HTMLSpanElement>(null)
|
|
23
|
+
const { segmentProps } = useDateSegment(segment, state, internalRef)
|
|
21
24
|
|
|
22
25
|
// Chrome has a bug where `contenteditable` elements receive focus from external clicks.
|
|
23
26
|
// This (in combination with the invisible character ​) creates boundaries
|
|
@@ -28,7 +31,7 @@ export const TimeSegment = ({
|
|
|
28
31
|
​
|
|
29
32
|
<span
|
|
30
33
|
{...segmentProps}
|
|
31
|
-
ref={
|
|
34
|
+
ref={mergeRefs<HTMLSpanElement>(internalRef, inputRef)}
|
|
32
35
|
className={classnames(
|
|
33
36
|
styles.timeSegment,
|
|
34
37
|
segment.type === 'literal' && styles.literal,
|