Skip to content

Commit fea9bea

Browse files
Merge branch 'main' into revert-1102-ashutosh/fix/upload-accept-filter-onChange
2 parents cd324b5 + d2f79bb commit fea9bea

11 files changed

Lines changed: 412 additions & 30 deletions

File tree

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,29 @@
22

33
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
44

5+
### [2.57.9](https://github.com/EightfoldAI/octuple/compare/v2.57.6...v2.57.9) (2026-07-06)
6+
7+
### Features
8+
9+
- **Dropdown:** add disableAutoFlip & disableReferenceTrackingOnScroll as Props ([#1135](https://github.com/EightfoldAI/octuple/issues/1135)) ([87ad268](https://github.com/EightfoldAI/octuple/commits/87ad268f978f18afb799f037349431a79bebbb5d))
10+
11+
### Bug Fixes
12+
13+
- **Select:** announce option groups and positions in improvedA11y listbox ([f8bec0a](https://github.com/EightfoldAI/octuple/commits/f8bec0aa5a2898c13396c32fae1a1d9f312c3aa8))
14+
- **Slider:** handle visually hidden labels ([#1125](https://github.com/EightfoldAI/octuple/issues/1125)) ([#1143](https://github.com/EightfoldAI/octuple/issues/1143)) ([10c27f6](https://github.com/EightfoldAI/octuple/commits/10c27f6da66c74ac98c3a9eaac16d1c826972651))
15+
- **Table:** add scroll tolerance so Scroller arrows reach the last column at fractional zoom ([#1140](https://github.com/EightfoldAI/octuple/issues/1140)) ([6e191b9](https://github.com/EightfoldAI/octuple/commits/6e191b9f5e6594ecb69cde2aeb2e75c01e3f40bf))
16+
17+
### [2.57.8](https://github.com/EightfoldAI/octuple/compare/v2.57.6...v2.57.8) (2026-07-03)
18+
19+
### Features
20+
21+
- **Dropdown:** add disableAutoFlip & disableReferenceTrackingOnScroll as Props ([#1135](https://github.com/EightfoldAI/octuple/issues/1135)) ([87ad268](https://github.com/EightfoldAI/octuple/commits/87ad268f978f18afb799f037349431a79bebbb5d))
22+
23+
### Bug Fixes
24+
25+
- **Select:** announce option groups and positions in improvedA11y listbox ([f8bec0a](https://github.com/EightfoldAI/octuple/commits/f8bec0aa5a2898c13396c32fae1a1d9f312c3aa8))
26+
- **Table:** add scroll tolerance so Scroller arrows reach the last column at fractional zoom ([#1140](https://github.com/EightfoldAI/octuple/issues/1140)) ([6e191b9](https://github.com/EightfoldAI/octuple/commits/6e191b9f5e6594ecb69cde2aeb2e75c01e3f40bf))
27+
528
### [2.57.7](https://github.com/EightfoldAI/octuple/compare/v2.57.6...v2.57.7) (2026-07-02)
629

730
### Bug Fixes

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@eightfold.ai/octuple",
3-
"version": "2.57.7",
3+
"version": "2.57.9",
44
"license": "MIT",
55
"description": "Eightfold Octuple Design System Component Library",
66
"sideEffects": [

src/components/Dropdown/Dropdown.test.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -974,4 +974,34 @@ describe('Dropdown', () => {
974974

975975
expect(referenceElement.getAttribute('aria-haspopup')).toBe('dialog');
976976
});
977+
978+
test('Should open normally with disableAutoFlip set', async () => {
979+
const { container, getByTestId } = render(
980+
<Dropdown {...dropdownProps} disableAutoFlip>
981+
<Button data-testid="dropdown-reference" text="Dropdown menu test" />
982+
</Dropdown>
983+
);
984+
const referenceElement = getByTestId('dropdown-reference');
985+
act(() => {
986+
userEvent.click(referenceElement);
987+
});
988+
await waitFor(() => screen.getByText('User profile 1'));
989+
expect(referenceElement.getAttribute('aria-expanded')).toBe('true');
990+
expect(container.querySelector('.dropdown-wrapper')).toBeTruthy();
991+
});
992+
993+
test('Should open normally with disableReferenceTrackingOnScroll set', async () => {
994+
const { container, getByTestId } = render(
995+
<Dropdown {...dropdownProps} disableReferenceTrackingOnScroll>
996+
<Button data-testid="dropdown-reference" text="Dropdown menu test" />
997+
</Dropdown>
998+
);
999+
const referenceElement = getByTestId('dropdown-reference');
1000+
act(() => {
1001+
userEvent.click(referenceElement);
1002+
});
1003+
await waitFor(() => screen.getByText('User profile 1'));
1004+
expect(referenceElement.getAttribute('aria-expanded')).toBe('true');
1005+
expect(container.querySelector('.dropdown-wrapper')).toBeTruthy();
1006+
});
9771007
});

src/components/Dropdown/Dropdown.tsx

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ export const Dropdown: FC<DropdownProps> = React.memo(
8282
overlayTabIndex = -1,
8383
overlayProps,
8484
toggleDropdownOnShiftTab = false,
85+
disableAutoFlip = false,
86+
disableReferenceTrackingOnScroll = false,
8587
},
8688
ref: React.ForwardedRef<DropdownRef>
8789
) => {
@@ -105,7 +107,11 @@ export const Dropdown: FC<DropdownProps> = React.memo(
105107
const { x, y, strategy, update, refs, context } = useFloating({
106108
placement,
107109
strategy: positionStrategy,
108-
middleware: [fOffset(offset), flip(), shift()],
110+
middleware: [
111+
fOffset(offset),
112+
...(disableAutoFlip ? [] : [flip()]),
113+
shift(),
114+
],
109115
});
110116

111117
const intervalRef: React.MutableRefObject<NodeJS.Timer> =
@@ -240,17 +246,34 @@ export const Dropdown: FC<DropdownProps> = React.memo(
240246
return autoUpdate(
241247
refs.reference.current,
242248
refs.floating.current,
243-
update
249+
update,
250+
{ ancestorScroll: !disableReferenceTrackingOnScroll }
244251
);
245-
}, [refs.reference, refs.floating, update]);
252+
}, [
253+
disableReferenceTrackingOnScroll,
254+
refs.reference,
255+
refs.floating,
256+
update,
257+
]);
246258

247259
// Add a new useEffect to update position when overlay content changes
248260
useEffect(() => {
249-
if (mergedVisible && refs.floating.current) {
250-
// Update the position when the overlay content changes
251-
update();
261+
if (
262+
disableReferenceTrackingOnScroll ||
263+
!mergedVisible ||
264+
!refs.floating.current
265+
) {
266+
return;
252267
}
253-
}, [overlay, mergedVisible, update, refs.floating]);
268+
// Update the position when the overlay content changes
269+
update();
270+
}, [
271+
disableReferenceTrackingOnScroll,
272+
overlay,
273+
mergedVisible,
274+
update,
275+
refs.floating,
276+
]);
254277

255278
const dropdownClasses: string = mergeClasses([
256279
dropdownClassNames,

src/components/Dropdown/Dropdown.types.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,31 @@ export interface DropdownProps {
192192
* The props of the overlay
193193
*/
194194
overlayProps?: React.HTMLAttributes<HTMLDivElement>;
195+
196+
/**
197+
* When true, the dropdown will not auto-flip to the opposite side when the
198+
* preferred placement overflows. Use to keep a fixed placement (e.g. anchored
199+
* below a trigger toward a scrollable region).
200+
*
201+
* CAUTION - this suppresses dropdown's auto-flip if the space on
202+
* the configured side is less than the dropdown-overlay height.
203+
* If the side fixed doesn't have a scrollable region, then the dropdown overlay is stuck.
204+
* @default false
205+
*/
206+
disableAutoFlip?: boolean;
207+
208+
/**
209+
* When true, the dropdown does not reposition when an ancestor of the reference
210+
* or floating element scrolls, or when the overlay content changes size while
211+
* open — it stays where it opened. Element resize and layout-shift tracking via
212+
* autoUpdate are unaffected. Use alongside disableAutoFlip when the reference is
213+
* inside a container that scrolls out from under a fixed-placement dropdown.
214+
*
215+
* CAUTION: this also suppresses the reposition that would otherwise run when the
216+
* overlay's own rendered content changes size while open.
217+
* @default false
218+
*/
219+
disableReferenceTrackingOnScroll?: boolean;
195220
}
196221

197222
export type DropdownRef = {

src/components/Slider/Slider.test.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,4 +441,40 @@ describe('Slider', () => {
441441
expect(thumbs[0]?.getAttribute('aria-valuetext')).toBe('20 percent');
442442
expect(thumbs[1]?.getAttribute('aria-valuetext')).toBe('80 percent');
443443
});
444+
445+
test('should render hidden handle label spans for range slider with ariaMinHandleLabel, ariaMaxHandleLabel and id', () => {
446+
const { container } = render(
447+
<Slider
448+
value={[20, 80]}
449+
id="test-slider"
450+
ariaMinHandleLabel="Lower bound"
451+
ariaMaxHandleLabel="Upper bound"
452+
/>
453+
);
454+
const lowerLabel = container.querySelector('#test-slider-0-handle-label');
455+
const upperLabel = container.querySelector('#test-slider-1-handle-label');
456+
expect(lowerLabel).toBeTruthy();
457+
expect(lowerLabel.textContent).toBe('Lower bound');
458+
expect(upperLabel).toBeTruthy();
459+
expect(upperLabel.textContent).toBe('Upper bound');
460+
});
461+
462+
test('should compose aria-labelledby with handle label id for range slider', () => {
463+
const { container } = render(
464+
<Slider
465+
value={[20, 80]}
466+
id="test-slider"
467+
ariaMinHandleLabel="Lower bound"
468+
ariaMaxHandleLabel="Upper bound"
469+
ariaLabelledBy="external-label"
470+
/>
471+
);
472+
const thumbs = container.querySelectorAll('input[type="range"]');
473+
expect(thumbs[0]?.getAttribute('aria-labelledby')).toBe(
474+
'external-label test-slider-0-handle-label'
475+
);
476+
expect(thumbs[1]?.getAttribute('aria-labelledby')).toBe(
477+
'external-label test-slider-1-handle-label'
478+
);
479+
});
444480
});

src/components/Slider/Slider.tsx

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ import { ResizeObserver } from '../../shared/ResizeObserver/ResizeObserver';
4949
import useOffset from './Hooks/useOffset';
5050
import { Breakpoints, useMatchMedia } from '../../hooks/useMatchMedia';
5151
import { useCanvasDirection } from '../../hooks/useCanvasDirection';
52-
import { mergeClasses } from '../../shared/utilities';
52+
import { mergeClasses, visuallyHidden } from '../../shared/utilities';
5353

5454
import styles from './slider.module.scss';
5555
import themedComponentStyles from './slider.theme.module.scss';
@@ -88,6 +88,8 @@ export const Slider: FC<SliderProps> = React.forwardRef(
8888
allowDisabledFocus = false,
8989
ariaLabel,
9090
ariaLabelledBy,
91+
ariaMaxHandleLabel,
92+
ariaMinHandleLabel,
9193
ariaValueText,
9294
autoFocus = false,
9395
classNames,
@@ -937,6 +939,24 @@ export const Slider: FC<SliderProps> = React.forwardRef(
937939
}
938940
}}
939941
/>
942+
{isRange && id && ariaMinHandleLabel && (
943+
<span
944+
key={`${getIdentifier(id, 0)}-handle-label`}
945+
id={`${getIdentifier(id, 0)}-handle-label`}
946+
style={visuallyHidden}
947+
>
948+
{ariaMinHandleLabel}
949+
</span>
950+
)}
951+
{isRange && id && ariaMaxHandleLabel && (
952+
<span
953+
key={`${getIdentifier(id, 1)}-handle-label`}
954+
id={`${getIdentifier(id, 1)}-handle-label`}
955+
style={visuallyHidden}
956+
>
957+
{ariaMaxHandleLabel}
958+
</span>
959+
)}
940960
{values.map((val: number, index: number) => (
941961
<Tooltip
942962
classNames={mergeClasses([
@@ -966,7 +986,11 @@ export const Slider: FC<SliderProps> = React.forwardRef(
966986
ref={ref}
967987
aria-disabled={mergedDisabled}
968988
aria-label={ariaLabel}
969-
aria-labelledby={ariaLabelledBy}
989+
aria-labelledby={
990+
isRange && id && ariaLabelledBy && (index === 0 ? ariaMinHandleLabel : ariaMaxHandleLabel)
991+
? `${ariaLabelledBy} ${getIdentifier(id, index)}-handle-label`
992+
: ariaLabelledBy
993+
}
970994
aria-valuetext={
971995
Array.isArray(ariaValueText)
972996
? ariaValueText[index]

src/components/Slider/Slider.types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,18 @@ export interface SliderInputProps
202202
* Associates the slider with a visible label element.
203203
*/
204204
ariaLabelledBy?: string;
205+
/**
206+
* Screen-reader-only label for the max handle in a range Slider.
207+
* Becomes a visually hidden <span> whose id is appended to aria-labelledby.
208+
* Only used when the Slider is a range (value is an array).
209+
*/
210+
ariaMaxHandleLabel?: string;
211+
/**
212+
* Screen-reader-only label for the min handle in a range Slider.
213+
* Becomes a visually hidden <span> whose id is appended to aria-labelledby.
214+
* Only used when the Slider is a range (value is an array).
215+
*/
216+
ariaMinHandleLabel?: string;
205217
/**
206218
* The Slider aria-valuetext attribute.
207219
* Provides a human-readable text alternative for the current value.

src/components/Table/Internal/Body/Scroller.tsx

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import styles from '../octable.module.scss';
2525
const BUTTON_HEIGHT: number = 36;
2626
const BUTTON_PADDING: number = 2;
2727
const DEFAULT_SCROLL_WIDTH: number = 100;
28+
const SCROLL_TOLERANCE: number = 1;
2829

2930
export const Scroller = React.forwardRef(
3031
<RecordType,>(
@@ -138,14 +139,14 @@ export const Scroller = React.forwardRef(
138139
.reverse()
139140
.find(
140141
(leftOffset: number) =>
141-
leftOffset > scrollBodyRef.current.scrollLeft
142+
leftOffset > scrollBodyRef.current.scrollLeft + SCROLL_TOLERANCE
142143
);
143144
} else {
144145
scrollLeft = scrollOffsets
145146
.map((offset) => -offset)
146147
.find(
147148
(leftOffset: number) =>
148-
leftOffset < scrollBodyRef.current.scrollLeft
149+
leftOffset < scrollBodyRef.current.scrollLeft - SCROLL_TOLERANCE
149150
);
150151
}
151152
} else {
@@ -155,12 +156,12 @@ export const Scroller = React.forwardRef(
155156
.reverse()
156157
.find(
157158
(leftOffset: number) =>
158-
leftOffset < scrollBodyRef.current.scrollLeft
159+
leftOffset < scrollBodyRef.current.scrollLeft - SCROLL_TOLERANCE
159160
);
160161
} else {
161162
scrollLeft = scrollOffsets.find(
162163
(leftOffset: number) =>
163-
leftOffset > scrollBodyRef.current.scrollLeft
164+
leftOffset > scrollBodyRef.current.scrollLeft + SCROLL_TOLERANCE
164165
);
165166
}
166167
}
@@ -181,20 +182,11 @@ export const Scroller = React.forwardRef(
181182
const bodyScrollLeft: number = scrollBodyRef.current?.scrollLeft || 0;
182183
const bodyScrollWidth: number = scrollBodyRef.current?.scrollWidth || 0;
183184
const bodyWidth: number = scrollBodyRef.current?.clientWidth || 0;
184-
const offset: 1 | -1 = direction === 'rtl' ? -1 : 1;
185-
const threshold: number = offset * (bodyScrollWidth - bodyWidth);
185+
const scrolled: number = Math.abs(bodyScrollLeft);
186+
const maxScroll: number = bodyScrollWidth - bodyWidth;
186187

187-
if (bodyScrollLeft === 0) {
188-
setStartButtonVisible(false);
189-
setEndButtonVisible(true);
190-
} else {
191-
setStartButtonVisible(true);
192-
if (bodyScrollLeft === threshold) {
193-
setEndButtonVisible(false);
194-
} else {
195-
setEndButtonVisible(true);
196-
}
197-
}
188+
setStartButtonVisible(scrolled > SCROLL_TOLERANCE);
189+
setEndButtonVisible(scrolled < maxScroll - SCROLL_TOLERANCE);
198190
};
199191

200192
useImperativeHandle(ref, () => ({

0 commit comments

Comments
 (0)