@aurora-ds/components 1.5.0 → 1.6.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/esm/index.js CHANGED
@@ -359,8 +359,8 @@ const buildActionButtonRootStyle = (theme, variant, color, focusOptions) => {
359
359
  backgroundColor: intent.main,
360
360
  borderColor: intent.main,
361
361
  color: intent.on,
362
- ':hover:not(:disabled)': { backgroundColor: intent.hover, borderColor: intent.hover },
363
- ':active:not(:disabled)': { backgroundColor: intent.active, borderColor: intent.active },
362
+ ':hover:not(:disabled)': { backgroundColor: intent.hover },
363
+ ':active:not(:disabled)': { backgroundColor: intent.active },
364
364
  }
365
365
  : variant === 'outlined'
366
366
  ? {
@@ -368,7 +368,7 @@ const buildActionButtonRootStyle = (theme, variant, color, focusOptions) => {
368
368
  borderColor: intent.border,
369
369
  color: intent.fg,
370
370
  ':hover:not(:disabled)': { backgroundColor: intent.subtleHover, color: intent.fgHover },
371
- ':active:not(:disabled)': { backgroundColor: intent.subtleActive, borderColor: intent.active, color: intent.active },
371
+ ':active:not(:disabled)': { backgroundColor: intent.subtleActive, color: intent.active },
372
372
  }
373
373
  : {
374
374
  backgroundColor: 'transparent',
@@ -390,7 +390,7 @@ const buildActionButtonRootStyle = (theme, variant, color, focusOptions) => {
390
390
  height: DEFAULT_BUTTON_HEIGHT,
391
391
  cursor: 'pointer',
392
392
  outline: 'none',
393
- transition: `background-color ${theme.transition.normal}, border-color ${theme.transition.normal}, color ${theme.transition.normal}`,
393
+ transition: `background-color ${theme.transition.normal}, color ${theme.transition.normal}`,
394
394
  ...colorVariantStyles,
395
395
  ':focus-visible': getFocusRingStyles(theme, focusOptions),
396
396
  ':disabled': { cursor: 'not-allowed', opacity: theme.opacity.high },
@@ -3278,17 +3278,17 @@ const TEXTFIELD_WRAPPER_VARIANTS = createVariants((theme) => {
3278
3278
  return {
3279
3279
  base: {
3280
3280
  display: 'flex',
3281
- alignItems: 'center',
3282
- gap: theme.spacing.sm,
3281
+ // `stretch` lets the inner input fill the full height of the box so
3282
+ // clicking anywhere (including the vertical padding area) hits it.
3283
+ alignItems: 'stretch',
3283
3284
  boxSizing: 'border-box',
3284
3285
  borderWidth: '1px',
3285
3286
  borderStyle: 'solid',
3286
3287
  borderRadius: theme.radius.md,
3287
3288
  backgroundColor: c.surfacePaper,
3289
+ // No focus ring on text inputs: focus is conveyed by the status
3290
+ // border colour change (see the `status` variants below).
3288
3291
  transition: `border-color ${theme.transition.fast}`,
3289
- // Full, always-visible focus ring when the inner input is focused
3290
- // (single source of truth) — complements the status border colour.
3291
- ':focus-within': getFocusRingStyles(theme),
3292
3292
  '&[data-disabled]': {
3293
3293
  opacity: theme.opacity.high,
3294
3294
  backgroundColor: c.disabledMain,
@@ -3297,10 +3297,12 @@ const TEXTFIELD_WRAPPER_VARIANTS = createVariants((theme) => {
3297
3297
  },
3298
3298
  variants: {
3299
3299
  size: {
3300
+ // Horizontal padding lives on the input itself (see TEXTFIELD_INPUT_VARIANTS),
3301
+ // not here, so the whole bordered area is part of the clickable input.
3300
3302
  // Font-size is set on the wrapper so the native input inherits it via `fontSize: 'inherit'`.
3301
- sm: { height: '2rem', paddingLeft: theme.spacing.sm, paddingRight: theme.spacing.xs, fontSize: theme.fontSize.xs },
3302
- md: { height: '2.5rem', paddingLeft: theme.spacing.sm, paddingRight: theme.spacing.xs, fontSize: theme.fontSize.sm },
3303
- lg: { height: '3rem', paddingLeft: theme.spacing.md, paddingRight: theme.spacing.sm, fontSize: theme.fontSize.md },
3303
+ sm: { height: '2rem', fontSize: theme.fontSize.xs },
3304
+ md: { height: '2.5rem', fontSize: theme.fontSize.sm },
3305
+ lg: { height: '3rem', fontSize: theme.fontSize.md },
3304
3306
  },
3305
3307
  status: {
3306
3308
  default: {
@@ -3325,11 +3327,17 @@ const TEXTFIELD_WRAPPER_VARIANTS = createVariants((theme) => {
3325
3327
  defaultVariants: { size: 'md', status: 'default' },
3326
3328
  };
3327
3329
  }, { id: 'textfield-wrapper' });
3328
- const TEXTFIELD_STYLES = createStyles((theme) => ({
3329
- /** Visually hidden native input fills the remaining space inside the wrapper. */
3330
- input: {
3330
+ /**
3331
+ * The native input carries the horizontal padding and fills 100% of the
3332
+ * wrapper height, so the entire bordered box is part of the clickable input
3333
+ * (no dead zone between the text and the border).
3334
+ */
3335
+ const TEXTFIELD_INPUT_VARIANTS = createVariants((theme) => ({
3336
+ base: {
3331
3337
  flex: 1,
3332
3338
  minWidth: 0,
3339
+ height: '100%',
3340
+ boxSizing: 'border-box',
3333
3341
  border: 'none',
3334
3342
  outline: 'none',
3335
3343
  background: 'transparent',
@@ -3337,15 +3345,25 @@ const TEXTFIELD_STYLES = createStyles((theme) => ({
3337
3345
  fontFamily: 'inherit',
3338
3346
  fontSize: 'inherit',
3339
3347
  lineHeight: 'normal',
3340
- padding: '0',
3341
3348
  '&::placeholder': { color: theme.colors.textTertiary },
3342
3349
  '&:disabled': { cursor: 'not-allowed', color: theme.colors.textDisabled },
3343
3350
  },
3351
+ variants: {
3352
+ size: {
3353
+ sm: { paddingLeft: theme.spacing.sm, paddingRight: theme.spacing.sm },
3354
+ md: { paddingLeft: theme.spacing.sm, paddingRight: theme.spacing.sm },
3355
+ lg: { paddingLeft: theme.spacing.md, paddingRight: theme.spacing.md },
3356
+ },
3357
+ },
3358
+ defaultVariants: { size: 'md' },
3359
+ }), { id: 'textfield-input' });
3360
+ const TEXTFIELD_STYLES = createStyles((theme) => ({
3344
3361
  /** Wrapper for the start icon — aligned with the input baseline. Clickable to focus the input. */
3345
3362
  startIconWrap: {
3346
3363
  display: 'flex',
3347
3364
  alignItems: 'center',
3348
3365
  flexShrink: 0,
3366
+ paddingLeft: theme.spacing.sm,
3349
3367
  cursor: 'pointer',
3350
3368
  },
3351
3369
  /** Wrapper for end actions (custom content + optional password toggle). */
@@ -3353,6 +3371,7 @@ const TEXTFIELD_STYLES = createStyles((theme) => ({
3353
3371
  display: 'flex',
3354
3372
  alignItems: 'center',
3355
3373
  flexShrink: 0,
3374
+ paddingRight: theme.spacing.xs,
3356
3375
  gap: theme.spacing['2xs'],
3357
3376
  },
3358
3377
  }), { id: 'textfield-extra' });
@@ -3413,7 +3432,11 @@ const useTextField = ({ id, ref, type, size, endAction, }) => {
3413
3432
  */
3414
3433
  const TextField = ({ ref, label, helperText, size = 'md', status = 'default', startIcon: StartIcon, endAction, type, id, disabled, required, ...rest }) => {
3415
3434
  const { fieldId, helperId, mergedRef, isPassword, showPassword, togglePassword, resolvedType, iconSize, iconButtonSize, hasEndSection, focusInput, } = useTextField({ id, ref, type, size, endAction });
3416
- return (jsxs(Stack, { flexDirection: 'column', gap: 'xs', children: [label !== undefined && (jsxs(Text, { variant: 'label', fontSize: 'sm', fontWeight: 'medium', color: 'textSecondary', htmlFor: fieldId, children: [label, required && (jsx(Text, { variant: 'span', color: 'errorMain', "aria-hidden": true, children: ' *' }))] })), jsxs("div", { className: TEXTFIELD_WRAPPER_VARIANTS({ size, status }), "data-disabled": disabled || undefined, children: [StartIcon && (jsx("span", { className: TEXTFIELD_STYLES.startIconWrap, onClick: focusInput, "aria-hidden": true, children: jsx(Icon, { icon: StartIcon, size: iconSize, strokeColor: 'textSecondary' }) })), jsx("input", { ref: mergedRef, id: fieldId, type: resolvedType, disabled: disabled, required: required, "aria-required": required || undefined, "aria-invalid": status === 'error' || undefined, "aria-describedby": helperText !== undefined ? helperId : undefined, "aria-errormessage": status === 'error' && helperText !== undefined ? helperId : undefined, className: TEXTFIELD_STYLES.input, ...rest }), hasEndSection && (jsxs("span", { className: TEXTFIELD_STYLES.endActionWrap, children: [endAction, isPassword && (jsx(IconButton, { icon: showPassword ? EyeSlashIcon : EyeIcon, ariaLabel: showPassword ? 'Hide password' : 'Show password', variant: 'text', color: 'neutral', size: iconButtonSize, type: 'button', onClick: togglePassword }))] }))] }), helperText !== undefined && (jsx(FormHelperText, { id: helperId, status: status, children: helperText }))] }));
3435
+ // Associate the label with the input via `aria-labelledby` (and NOT `htmlFor`)
3436
+ // so the label stays accessible without natively focusing the input on click —
3437
+ // only clicking inside the bordered box should focus the field.
3438
+ const labelId = `${fieldId}-label`;
3439
+ return (jsxs(Stack, { flexDirection: 'column', gap: 'xs', children: [label !== undefined && (jsxs(Text, { variant: 'label', fontSize: 'sm', fontWeight: 'medium', color: 'textSecondary', id: labelId, children: [label, required && (jsx(Text, { variant: 'span', color: 'errorMain', "aria-hidden": true, children: ' *' }))] })), jsxs("div", { className: TEXTFIELD_WRAPPER_VARIANTS({ size, status }), "data-disabled": disabled || undefined, children: [StartIcon && (jsx("span", { className: TEXTFIELD_STYLES.startIconWrap, onClick: focusInput, "aria-hidden": true, children: jsx(Icon, { icon: StartIcon, size: iconSize, strokeColor: 'textSecondary' }) })), jsx("input", { ref: mergedRef, id: fieldId, type: resolvedType, disabled: disabled, required: required, "aria-required": required || undefined, "aria-invalid": status === 'error' || undefined, "aria-labelledby": label !== undefined ? labelId : undefined, "aria-describedby": helperText !== undefined ? helperId : undefined, "aria-errormessage": status === 'error' && helperText !== undefined ? helperId : undefined, className: TEXTFIELD_INPUT_VARIANTS({ size }), ...rest }), hasEndSection && (jsxs("span", { className: TEXTFIELD_STYLES.endActionWrap, children: [endAction, isPassword && (jsx(IconButton, { icon: showPassword ? EyeSlashIcon : EyeIcon, ariaLabel: showPassword ? 'Hide password' : 'Show password', variant: 'text', color: 'neutral', size: iconButtonSize, type: 'button', onClick: togglePassword }))] }))] }), helperText !== undefined && (jsx(FormHelperText, { id: helperId, status: status, children: helperText }))] }));
3417
3440
  };
3418
3441
  TextField.displayName = 'TextField';
3419
3442
 
@@ -4006,8 +4029,8 @@ const SELECT_TRIGGER_VARIANTS = createVariants((theme) => {
4006
4029
  transition: `border-color ${theme.transition.fast}`,
4007
4030
  outline: 'none',
4008
4031
  fontFamily: 'inherit',
4009
- // Full, always-visible focus ring (single source of truth).
4010
- ':focus-visible': getFocusRingStyles(theme),
4032
+ // No focus ring on the trigger: focus is conveyed by the border
4033
+ // colour change (see the `status` variants below).
4011
4034
  '&[data-open]': {
4012
4035
  borderColor: c.primaryMain,
4013
4036
  },
@@ -4044,6 +4067,9 @@ const SELECT_TRIGGER_VARIANTS = createVariants((theme) => {
4044
4067
  ':hover:not([data-disabled]):not([data-open])': {
4045
4068
  borderColor: c.borderStrong,
4046
4069
  },
4070
+ '&:focus-visible:not([data-open])': {
4071
+ borderColor: c.primaryMain,
4072
+ },
4047
4073
  },
4048
4074
  error: {
4049
4075
  borderColor: c.errorMain,
@@ -6746,6 +6772,67 @@ const Dialog = DialogBase;
6746
6772
  Dialog.Header = DialogHeader;
6747
6773
  Dialog.Body = DialogBody;
6748
6774
 
6775
+ /**
6776
+ * Manages keyboard navigation for a listbox-style menu.
6777
+ *
6778
+ * Binds ArrowDown, ArrowUp, Home, End, and Enter using `useKeyPress`.
6779
+ * Automatically skips disabled items and optionally wraps around (loop).
6780
+ * Resets focus when `enabled` toggles.
6781
+ */
6782
+ const useListKeyNav = ({ itemCount, enabled, onSelect, isDisabled, loop = true, initialIndex = 0, }) => {
6783
+ const [focusedIndex, setFocusedIndex] = useState(-1);
6784
+ const initialIndexRef = useRef(initialIndex);
6785
+ useEffect(() => {
6786
+ initialIndexRef.current = initialIndex;
6787
+ }, [initialIndex]);
6788
+ useEffect(() => {
6789
+ setFocusedIndex(enabled ? initialIndexRef.current : -1);
6790
+ }, [enabled]);
6791
+ const getNextIndex = useCallback((current, direction) => {
6792
+ if (itemCount === 0) {
6793
+ return -1;
6794
+ }
6795
+ let next = current + direction;
6796
+ for (let i = 0; i < itemCount; i++) {
6797
+ if (next < 0) {
6798
+ next = loop ? itemCount - 1 : 0;
6799
+ }
6800
+ if (next >= itemCount) {
6801
+ next = loop ? 0 : itemCount - 1;
6802
+ }
6803
+ if (!isDisabled?.(next)) {
6804
+ return next;
6805
+ }
6806
+ next += direction;
6807
+ }
6808
+ return current;
6809
+ }, [itemCount, loop, isDisabled]);
6810
+ useKeyPress({
6811
+ ArrowDown: (e) => {
6812
+ e.preventDefault();
6813
+ setFocusedIndex((prev) => getNextIndex(prev, 1));
6814
+ },
6815
+ ArrowUp: (e) => {
6816
+ e.preventDefault();
6817
+ setFocusedIndex((prev) => getNextIndex(prev, -1));
6818
+ },
6819
+ Home: (e) => {
6820
+ e.preventDefault();
6821
+ setFocusedIndex(getNextIndex(-1, 1));
6822
+ },
6823
+ End: (e) => {
6824
+ e.preventDefault();
6825
+ setFocusedIndex(getNextIndex(itemCount, -1));
6826
+ },
6827
+ Enter: () => {
6828
+ if (focusedIndex >= 0) {
6829
+ onSelect(focusedIndex);
6830
+ }
6831
+ },
6832
+ }, { enabled });
6833
+ return { focusedIndex, setFocusedIndex };
6834
+ };
6835
+
6749
6836
  const lightPalette = {
6750
6837
  // Surface
6751
6838
  surfaceBackground: '#f8fafc',
@@ -7154,5 +7241,5 @@ const darkTheme = createTheme({
7154
7241
  breakpoints: themeBreakpoints,
7155
7242
  });
7156
7243
 
7157
- export { Accordion, Alert, Article, Aside, Avatar, AvatarGroup, Backdrop, Badge, Box, Breadcrumb, Button, Card, Checkbox, DatePicker, DefaultErrorFallback, Dialog, Drawer, ErrorBoundary, Fab, Footer, Form, Grid, Header, Icon, IconButton, Image, InfoBubble, Link, LoaderScreen, Main, Menu, Nav, Pagination, RadioButton, RadioGroup, Section, Select, Separator, Skeleton, Stack, SvgImage, Switch, Table, Tabs, Text, TextField, ToggleButton, ToggleGroup, ToggleIconButton, Tooltip, darkTheme, lightTheme, useDrawerContext };
7244
+ export { Accordion, Alert, Article, Aside, Avatar, AvatarGroup, Backdrop, Badge, Box, Breadcrumb, Button, Card, Checkbox, DatePicker, DefaultErrorFallback, Dialog, Drawer, ErrorBoundary, Fab, Footer, Form, Grid, Header, Icon, IconButton, Image, InfoBubble, Link, LoaderScreen, Main, Menu, Nav, Pagination, RadioButton, RadioGroup, Section, Select, Separator, Skeleton, Stack, SvgImage, Switch, Table, Tabs, Text, TextField, ToggleButton, ToggleGroup, ToggleIconButton, Tooltip, darkTheme, lightTheme, useBodyScrollLock, useControllableState, useDrawerContext, useFocusTrap, useKeyPress, useListKeyNav, useMenuPosition, useMergedRefs, useTooltipPosition, useTransitionRender };
7158
7245
  //# sourceMappingURL=index.js.map