@skyscanner/backpack-web 41.3.0 → 41.5.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.
Files changed (36) hide show
  1. package/bpk-component-bubble/src/BpkBubble.module.css +1 -1
  2. package/bpk-component-card-list/src/BpkCardListRowRail/BpkCardListCarousel.module.css +1 -1
  3. package/bpk-component-code/index.d.ts +6 -0
  4. package/bpk-component-code/index.js +3 -1
  5. package/bpk-component-code/src/BpkCode.d.ts +9 -0
  6. package/bpk-component-code/src/BpkCode.js +14 -33
  7. package/bpk-component-code/src/BpkCodeBlock.d.ts +9 -0
  8. package/bpk-component-code/src/BpkCodeBlock.js +15 -28
  9. package/bpk-component-fieldset/src/BpkFieldset.js +0 -1
  10. package/bpk-component-label/index.d.ts +3 -0
  11. package/bpk-component-label/index.js +3 -1
  12. package/bpk-component-label/src/BpkLabel.d.ts +11 -0
  13. package/bpk-component-label/src/BpkLabel.js +11 -22
  14. package/bpk-component-navigation-tab-group/src/BpkNavigationTabGroup.js +3 -2
  15. package/bpk-component-nudger/src/BpkNudger.js +0 -1
  16. package/bpk-component-segmented-control/index.d.ts +3 -2
  17. package/bpk-component-segmented-control/index.js +2 -1
  18. package/bpk-component-segmented-control/src/BpkSegmentedControl.d.ts +36 -1
  19. package/bpk-component-segmented-control/src/BpkSegmentedControl.js +138 -13
  20. package/bpk-component-text/src/BpkText.module.css +1 -1
  21. package/bpk-component-theme-toggle/index.d.ts +4 -0
  22. package/bpk-component-theme-toggle/src/BpkThemeToggle.d.ts +16 -0
  23. package/bpk-component-theme-toggle/src/BpkThemeToggle.js +10 -7
  24. package/bpk-component-theme-toggle/src/theming.d.ts +136 -0
  25. package/bpk-component-theme-toggle/src/updateOnThemeChange.d.ts +42 -0
  26. package/bpk-component-theme-toggle/src/updateOnThemeChange.js +10 -8
  27. package/bpk-component-theme-toggle/src/utils.d.ts +3 -0
  28. package/bpk-component-theme-toggle/src/utils.js +1 -1
  29. package/bpk-mixins/_typography.scss +3 -3
  30. package/bpk-stylesheets/base.css +1 -1
  31. package/bpk-stylesheets/font.css +1 -1
  32. package/bpk-stylesheets/font.scss +423 -75
  33. package/bpk-stylesheets/index.scss +1 -1
  34. package/bpk-stylesheets/larken.css +1 -1
  35. package/bpk-stylesheets/larken.scss +268 -95
  36. package/package.json +2 -2
@@ -15,4 +15,4 @@
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
17
  */
18
- .bpk-bubble{position:relative;display:inline-flex;width:auto;height:1.25rem;padding:0 .5rem;flex-direction:column;justify-content:center;border-radius:.25rem;background-color:#e70866;font-family:"Larken","Noto Sans Arabic","Noto Sans Hebrew","Noto Serif","Noto Serif Devanagari","Noto Serif Thai","Noto Sans SC","Noto Sans TC","Noto Sans JP","Noto Sans KR",sans-serif;text-align:center;white-space:nowrap;font-size:.75rem;line-height:1rem;font-weight:400}.bpk-bubble__arrow{position:absolute;bottom:-5px;left:50%;transform:translateX(-50%);color:#e70866}
18
+ .bpk-bubble{position:relative;display:inline-flex;width:auto;height:1.25rem;padding:0 .5rem;flex-direction:column;justify-content:center;border-radius:.25rem;background-color:#e70866;font-family:var(--bpk-larken-font-stack, "Larken", "Noto Sans Arabic", "Noto Serif Hebrew", "Noto Serif", "Noto Serif Devanagari", "Noto Serif Thai", "Noto Serif SC", "Noto Serif TC", "Noto Serif JP", "Noto Serif KR", sans-serif);text-align:center;white-space:nowrap;font-size:.75rem;line-height:1rem;font-weight:400}.bpk-bubble__arrow{position:absolute;bottom:-5px;left:50%;transform:translateX(-50%);color:#e70866}
@@ -15,4 +15,4 @@
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
17
  */
18
- .bpk-card-list-row-rail__row,.bpk-card-list-row-rail__rail{display:flex;overflow-x:hidden;box-sizing:border-box;gap:.25rem;margin-block:-1.5rem;padding-block:1.5rem;scroll-snap-stop:always;scroll-snap-type:x mandatory;scrollbar-width:none}@media(max-width: 32rem){.bpk-card-list-row-rail__row,.bpk-card-list-row-rail__rail{overflow-x:scroll}}.bpk-card-list-row-rail__row::-webkit-scrollbar,.bpk-card-list-row-rail__rail::-webkit-scrollbar{display:none}.bpk-card-list-row-rail__row__card,.bpk-card-list-row-rail__rail__card{position:relative;padding:0 .5rem;flex:0 0 calc((100% - .5rem*(var(--initially-shown-cards, 3) - 1))/var(--initially-shown-cards, 3));overflow:visible;box-sizing:border-box;scroll-snap-align:start}@media(max-width: 32rem){.bpk-card-list-row-rail__row__card,.bpk-card-list-row-rail__rail__card{flex:0 0 calc((100% - .5rem*(var(--initially-shown-cards, 3) - 1))/max(1,var(--initially-shown-cards, 3) - .8))}.bpk-card-list-row-rail__row__card:first-child,.bpk-card-list-row-rail__rail__card:first-child{padding-left:.25rem}html[dir=rtl] .bpk-card-list-row-rail__row__card:first-child,html[dir=rtl] .bpk-card-list-row-rail__rail__card:first-child{padding-right:.25rem;padding-left:.5rem}}.bpk-card-list-row-rail__rail{-webkit-overflow-scrolling:touch}
18
+ .bpk-card-list-row-rail__row,.bpk-card-list-row-rail__rail{--spacing-offset: 0.5rem;--carousel-card-gap: 1.25rem;display:flex;overflow-x:hidden;box-sizing:border-box;gap:var(--carousel-card-gap);margin-block:-1.5rem;margin-inline:-0.5rem;padding-block:1.5rem;padding-inline:.5rem;scroll-snap-stop:always;scroll-snap-type:x mandatory;scrollbar-width:none}@media(max-width: 32rem){.bpk-card-list-row-rail__row,.bpk-card-list-row-rail__rail{--spacing-offset: 1rem;--carousel-card-gap: 1rem;overflow-x:scroll}}.bpk-card-list-row-rail__row::-webkit-scrollbar,.bpk-card-list-row-rail__rail::-webkit-scrollbar{display:none}.bpk-card-list-row-rail__row__card,.bpk-card-list-row-rail__rail__card{position:relative;flex:0 0 calc((100% - (var(--carousel-card-gap)*(var(--initially-shown-cards, 3) - 1) + var(--spacing-offset)*2/var(--initially-shown-cards, 3)))/var(--initially-shown-cards, 3));overflow:visible;box-sizing:border-box;scroll-margin-inline:var(--spacing-offset);scroll-snap-align:start}@media(max-width: 32rem){.bpk-card-list-row-rail__row__card,.bpk-card-list-row-rail__rail__card{flex:0 0 calc((100% - var(--carousel-card-gap)*(var(--initially-shown-cards, 3) - 1))/max(1,var(--initially-shown-cards, 3) - .8))}}.bpk-card-list-row-rail__rail{-webkit-overflow-scrolling:touch}@media(max-width: 32rem){.bpk-card-list-row-rail__rail{margin-inline:calc(-1*var(--spacing-offset));padding-inline:var(--spacing-offset)}}
@@ -0,0 +1,6 @@
1
+ import BpkCode from './src/BpkCode';
2
+ import BpkCodeBlock from './src/BpkCodeBlock';
3
+ import type { Props as BpkCodeProps } from './src/BpkCode';
4
+ import type { Props as BpkCodeBlockProps } from './src/BpkCodeBlock';
5
+ export type { BpkCodeProps, BpkCodeBlockProps };
6
+ export { BpkCode, BpkCodeBlock };
@@ -14,6 +14,8 @@
14
14
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
- */import BpkCode from "./src/BpkCode";
17
+ */
18
+
19
+ import BpkCode from "./src/BpkCode";
18
20
  import BpkCodeBlock from "./src/BpkCodeBlock";
19
21
  export { BpkCode, BpkCodeBlock };
@@ -0,0 +1,9 @@
1
+ import type { ReactNode } from 'react';
2
+ export type Props = {
3
+ children: ReactNode;
4
+ alternate?: boolean;
5
+ className?: string;
6
+ [rest: string]: any;
7
+ };
8
+ declare const BpkCode: ({ alternate, children, className, ...rest }: Props) => import("react/jsx-runtime").JSX.Element;
9
+ export default BpkCode;
@@ -14,42 +14,23 @@
14
14
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
- */import PropTypes from 'prop-types';
17
+ */
18
+
18
19
  import { cssModules } from "../../bpk-react-utils";
19
20
  import STYLES from "./BpkCode.module.css";
20
21
  import { jsx as _jsx } from "react/jsx-runtime";
21
22
  const getClassName = cssModules(STYLES);
22
- const BpkCode = props => {
23
- const {
24
- alternate,
25
- children,
26
- className,
27
- ...rest
28
- } = props;
29
- const classNames = [getClassName('bpk-code')];
30
- if (alternate) {
31
- classNames.push(getClassName('bpk-code--alternate'));
32
- }
33
- if (className) {
34
- classNames.push(className);
35
- }
36
- return (
37
- /*#__PURE__*/
38
- // $FlowFixMe[cannot-spread-inexact] - inexact rest. See 'decisions/flowfixme.md'.
39
- _jsx("code", {
40
- className: classNames.join(' '),
41
- ...rest,
42
- children: children
43
- })
44
- );
45
- };
46
- BpkCode.propTypes = {
47
- children: PropTypes.node.isRequired,
48
- className: PropTypes.string,
49
- alternate: PropTypes.bool
50
- };
51
- BpkCode.defaultProps = {
52
- className: null,
53
- alternate: false
23
+ const BpkCode = ({
24
+ alternate = false,
25
+ children,
26
+ className,
27
+ ...rest
28
+ }) => {
29
+ const classNames = getClassName('bpk-code', alternate && 'bpk-code--alternate', className);
30
+ return /*#__PURE__*/_jsx("code", {
31
+ className: classNames,
32
+ ...rest,
33
+ children: children
34
+ });
54
35
  };
55
36
  export default BpkCode;
@@ -0,0 +1,9 @@
1
+ import type { ReactNode } from 'react';
2
+ export type Props = {
3
+ children: ReactNode;
4
+ alternate?: boolean;
5
+ className?: string;
6
+ [rest: string]: any;
7
+ };
8
+ declare const BpkCodeBlock: ({ alternate, children, className, ...rest }: Props) => import("react/jsx-runtime").JSX.Element;
9
+ export default BpkCodeBlock;
@@ -14,40 +14,27 @@
14
14
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
- */import PropTypes from 'prop-types';
17
+ */
18
+
18
19
  import { cssModules } from "../../bpk-react-utils";
19
20
  import STYLES from "./BpkCodeBlock.module.css";
20
21
  import { jsx as _jsx } from "react/jsx-runtime";
21
22
  const getClassName = cssModules(STYLES);
22
- const BpkCodeBlock = props => {
23
- const {
24
- alternate,
25
- children,
26
- className,
27
- ...rest
28
- } = props;
23
+ const BpkCodeBlock = ({
24
+ alternate = false,
25
+ children,
26
+ className,
27
+ ...rest
28
+ }) => {
29
29
  const preClassNames = getClassName('bpk-code__pre', alternate && 'bpk-code__pre--alternate', className);
30
30
  const codeClassNames = getClassName('bpk-code', 'bpk-code--block');
31
- return (
32
- /*#__PURE__*/
33
- // $FlowFixMe[cannot-spread-inexact] - inexact rest. See 'decisions/flowfixme.md'.
34
- _jsx("pre", {
35
- className: preClassNames,
36
- ...rest,
37
- children: /*#__PURE__*/_jsx("code", {
38
- className: codeClassNames,
39
- children: children
40
- })
31
+ return /*#__PURE__*/_jsx("pre", {
32
+ className: preClassNames,
33
+ ...rest,
34
+ children: /*#__PURE__*/_jsx("code", {
35
+ className: codeClassNames,
36
+ children: children
41
37
  })
42
- );
43
- };
44
- BpkCodeBlock.propTypes = {
45
- children: PropTypes.node.isRequired,
46
- alternate: PropTypes.bool,
47
- className: PropTypes.string
48
- };
49
- BpkCodeBlock.defaultProps = {
50
- alternate: false,
51
- className: null
38
+ });
52
39
  };
53
40
  export default BpkCodeBlock;
@@ -19,7 +19,6 @@
19
19
  import { cloneElement } from 'react';
20
20
  // @ts-expect-error Untyped import. See `decisions/imports-ts-suppressions.md`.
21
21
  import BpkFormValidation from "../../bpk-component-form-validation";
22
- // @ts-expect-error Untyped import. See `decisions/imports-ts-suppressions.md`.
23
22
  import BpkLabel from "../../bpk-component-label";
24
23
  import { cssModules } from "../../bpk-react-utils";
25
24
  import STYLES from "./BpkFieldset.module.css";
@@ -0,0 +1,3 @@
1
+ import BpkLabel from './src/BpkLabel';
2
+ export type { Props as BpkLabelProps } from './src/BpkLabel';
3
+ export default BpkLabel;
@@ -14,5 +14,7 @@
14
14
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
- */import BpkLabel from "./src/BpkLabel";
17
+ */
18
+
19
+ import BpkLabel from "./src/BpkLabel";
18
20
  export default BpkLabel;
@@ -0,0 +1,11 @@
1
+ import type { ComponentPropsWithoutRef, ReactNode } from 'react';
2
+ export type Props = {
3
+ children: ReactNode;
4
+ className?: string;
5
+ disabled?: boolean;
6
+ valid?: boolean | null;
7
+ required?: boolean;
8
+ white?: boolean;
9
+ } & ComponentPropsWithoutRef<'label'>;
10
+ declare const BpkLabel: ({ children, className, disabled, required, valid, white, ...rest }: Props) => import("react/jsx-runtime").JSX.Element;
11
+ export default BpkLabel;
@@ -14,14 +14,15 @@
14
14
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
- */import PropTypes from 'prop-types';
17
+ */
18
+
18
19
  import { cssModules } from "../../bpk-react-utils";
19
20
  import STYLES from "./BpkLabel.module.css";
20
21
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
21
22
  const getClassName = cssModules(STYLES);
22
23
  const BpkLabel = ({
23
24
  children,
24
- className = null,
25
+ className,
25
26
  disabled = false,
26
27
  required = false,
27
28
  valid = null,
@@ -30,25 +31,13 @@ const BpkLabel = ({
30
31
  }) => {
31
32
  const invalid = valid === false;
32
33
  const classNames = getClassName('bpk-label', white && 'bpk-label--white', invalid && 'bpk-label--invalid', disabled && 'bpk-label--disabled', white && disabled && 'bpk-label--disabled--white', className);
33
- return (
34
- /*#__PURE__*/
35
- // $FlowFixMe[cannot-spread-inexact] - inexact rest. See 'decisions/flowfixme.md'.
36
- _jsxs("label", {
37
- className: classNames,
38
- ...rest,
39
- children: [children, !disabled && required && /*#__PURE__*/_jsx("span", {
40
- className: getClassName('bpk-label__asterisk'),
41
- children: "*"
42
- })]
43
- })
44
- );
45
- };
46
- BpkLabel.propTypes = {
47
- children: PropTypes.node.isRequired,
48
- className: PropTypes.string,
49
- disabled: PropTypes.bool,
50
- valid: PropTypes.bool,
51
- required: PropTypes.bool,
52
- white: PropTypes.bool
34
+ return /*#__PURE__*/_jsxs("label", {
35
+ className: classNames,
36
+ ...rest,
37
+ children: [children, !disabled && required && /*#__PURE__*/_jsx("span", {
38
+ className: getClassName('bpk-label__asterisk'),
39
+ children: "*"
40
+ })]
41
+ });
53
42
  };
54
43
  export default BpkLabel;
@@ -82,6 +82,7 @@ const BpkNavigationTabGroup = ({
82
82
  children: tabs.map((tab, index) => {
83
83
  const selected = index === selectedTab;
84
84
  const {
85
+ badgeText,
85
86
  icon,
86
87
  text,
87
88
  ...tabWrapItem
@@ -103,10 +104,10 @@ const BpkNavigationTabGroup = ({
103
104
  textStyle: TEXT_STYLES.label2,
104
105
  children: text
105
106
  })]
106
- }), tab.badgeText && /*#__PURE__*/_jsx("span", {
107
+ }), badgeText && /*#__PURE__*/_jsx("span", {
107
108
  className: getClassName('bpk-navigation-tab-bubble-wrapper'),
108
109
  children: /*#__PURE__*/_jsx(BpkBubble, {
109
- children: tab.badgeText
110
+ children: badgeText
110
111
  })
111
112
  })]
112
113
  })
@@ -21,7 +21,6 @@ import BpkButton, { BUTTON_TYPES } from "../../bpk-component-button";
21
21
  import { withButtonAlignment } from "../../bpk-component-icon";
22
22
  import MinusIcon from "../../bpk-component-icon/sm/minus";
23
23
  import PlusIcon from "../../bpk-component-icon/sm/plus";
24
- // @ts-expect-error Untyped import. See `decisions/imports-ts-suppressions.md`.
25
24
  import BpkLabel from "../../bpk-component-label";
26
25
  import BpkText, { TEXT_STYLES } from "../../bpk-component-text";
27
26
  import { cssModules, setNativeValue } from "../../bpk-react-utils";
@@ -1,3 +1,4 @@
1
- import BpkSegmentedControl, { type Props as BpkSegmentControlProps } from './src/BpkSegmentedControl';
2
- export type { BpkSegmentControlProps };
1
+ import BpkSegmentedControl, { useSegmentedControlPanels, type Props as BpkSegmentControlProps, type TabPanelProps } from './src/BpkSegmentedControl';
2
+ export type { BpkSegmentControlProps, TabPanelProps };
3
+ export { useSegmentedControlPanels };
3
4
  export default BpkSegmentedControl;
@@ -16,5 +16,6 @@
16
16
  * limitations under the License.
17
17
  */
18
18
 
19
- import BpkSegmentedControl from "./src/BpkSegmentedControl";
19
+ import BpkSegmentedControl, { useSegmentedControlPanels } from "./src/BpkSegmentedControl";
20
+ export { useSegmentedControlPanels };
20
21
  export default BpkSegmentedControl;
@@ -6,16 +6,51 @@ export declare const SEGMENT_TYPES: {
6
6
  SurfaceContrast: string;
7
7
  };
8
8
  export type SegmentTypes = (typeof SEGMENT_TYPES)[keyof typeof SEGMENT_TYPES];
9
+ export type TabPanelProps = {
10
+ id: string;
11
+ role: 'tabpanel';
12
+ 'aria-labelledby': string;
13
+ hidden: boolean;
14
+ tabIndex: 0;
15
+ };
16
+ /**
17
+ * Custom hook to manage segmented control and its panels with automatic ID generation.
18
+ * Simplifies the API by eliminating the need to manually track IDs.
19
+ *
20
+ * Note: For optimal performance, memoize the buttonContents array in the parent component
21
+ * to prevent unnecessary recalculations (e.g., using useMemo or defining outside render).
22
+ *
23
+ * @param {Array<string | ReactNode>} buttonContents - Array of button content (strings or ReactNodes)
24
+ * @param {number} selectedIndex - Currently selected tab index
25
+ * @returns {Object} Object with controlProps (for BpkSegmentedControl) and getPanelProps function
26
+ */
27
+ export declare const useSegmentedControlPanels: (buttonContents: string[] | ReactNode[], selectedIndex: number) => {
28
+ controlProps: {
29
+ id: string;
30
+ buttonContents: string[] | ReactNode[];
31
+ selectedIndex: number;
32
+ };
33
+ getPanelProps: (index: number) => TabPanelProps;
34
+ };
9
35
  export type Props = {
10
36
  buttonContents: string[] | ReactNode[];
11
37
  /**
12
38
  * Accessible label for the segmented control group.
13
39
  */
14
40
  label?: string;
41
+ /**
42
+ * ID used to link the segmented control with its tab panels for accessibility.
43
+ * Created using controlProps from useSegmentedControlPanels hook.
44
+ */
45
+ id?: string;
15
46
  type?: SegmentTypes;
47
+ /**
48
+ * Callback fired when a tab is selected. Receives the index of the selected tab.
49
+ */
16
50
  onItemClick: (id: number) => void;
17
51
  selectedIndex: number;
18
52
  shadow?: boolean;
53
+ activationMode?: 'automatic' | 'manual';
19
54
  };
20
- declare const BpkSegmentedControl: ({ buttonContents, label, onItemClick, selectedIndex, shadow, type, }: Props) => import("react/jsx-runtime").JSX.Element;
55
+ declare const BpkSegmentedControl: ({ activationMode, buttonContents, id: providedId, label, onItemClick, selectedIndex, shadow, type, }: Props) => import("react/jsx-runtime").JSX.Element;
21
56
  export default BpkSegmentedControl;
@@ -16,8 +16,8 @@
16
16
  * limitations under the License.
17
17
  */
18
18
 
19
- import { useState } from 'react';
20
- import { cssModules } from "../../bpk-react-utils";
19
+ import { useId, useMemo, useRef, useState } from 'react';
20
+ import { cssModules, isRTL } from "../../bpk-react-utils";
21
21
  import STYLES from "./BpkSegmentedControl.module.css";
22
22
  import { jsx as _jsx } from "react/jsx-runtime";
23
23
  const getClassName = cssModules(STYLES);
@@ -27,40 +27,165 @@ export const SEGMENT_TYPES = {
27
27
  SurfaceDefault: 'surface-default',
28
28
  SurfaceContrast: 'surface-contrast'
29
29
  };
30
+ const getPanelId = (baseId, index) => `${baseId}-panel-${index}`;
31
+ const getTabId = (baseId, index) => `${baseId}-tab-${index}`;
32
+
33
+ /**
34
+ * Helper function to get accessibility props for tab panel elements.
35
+ * Use this to ensure proper ARIA relationships between tabs and their panels.
36
+ *
37
+ * Note: For a simpler API, consider using the useSegmentedControlPanels hook instead,
38
+ * which manages IDs automatically and reduces boilerplate.
39
+ * This function is kept for backward compatibility.
40
+ *
41
+ * @param {string} baseId - The base ID used to generate unique IDs for tabs and panels.
42
+ * @param {number} index - The index of the tab panel.
43
+ * @param {number} selectedIndex - The currently selected tab index.
44
+ * @returns {TabPanelProps} An object containing the necessary props for a tab panel.
45
+ */
46
+ const getTabPanelProps = (baseId, index, selectedIndex) => ({
47
+ id: getPanelId(baseId, index),
48
+ role: 'tabpanel',
49
+ 'aria-labelledby': getTabId(baseId, index),
50
+ hidden: index !== selectedIndex,
51
+ tabIndex: 0
52
+ });
53
+ const getContainerAriaProps = (providedId, label) => {
54
+ const props = {};
55
+ if (providedId) {
56
+ props.role = 'tablist';
57
+ props['aria-orientation'] = 'horizontal';
58
+ }
59
+ if (label) {
60
+ props['aria-label'] = label;
61
+ }
62
+ return props;
63
+ };
64
+ const getButtonAriaProps = (providedId, isSelected, panelId) => {
65
+ if (!providedId) {
66
+ return {};
67
+ }
68
+ return {
69
+ role: 'tab',
70
+ 'aria-selected': isSelected,
71
+ 'aria-controls': panelId
72
+ };
73
+ };
74
+ const getTabIndex = (providedId, isSelected) => {
75
+ if (!providedId) {
76
+ return undefined;
77
+ }
78
+ return isSelected ? 0 : -1;
79
+ };
80
+ const getNextIndex = (current, max) => current === max ? 0 : current + 1;
81
+ const getPrevIndex = (current, max) => current === 0 ? max : current - 1;
82
+
83
+ /**
84
+ * Custom hook to manage segmented control and its panels with automatic ID generation.
85
+ * Simplifies the API by eliminating the need to manually track IDs.
86
+ *
87
+ * Note: For optimal performance, memoize the buttonContents array in the parent component
88
+ * to prevent unnecessary recalculations (e.g., using useMemo or defining outside render).
89
+ *
90
+ * @param {Array<string | ReactNode>} buttonContents - Array of button content (strings or ReactNodes)
91
+ * @param {number} selectedIndex - Currently selected tab index
92
+ * @returns {Object} Object with controlProps (for BpkSegmentedControl) and getPanelProps function
93
+ */
94
+ export const useSegmentedControlPanels = (buttonContents, selectedIndex) => {
95
+ const baseId = useId();
96
+ const controlProps = useMemo(() => ({
97
+ id: baseId,
98
+ buttonContents,
99
+ selectedIndex
100
+ }), [baseId, buttonContents, selectedIndex]);
101
+ const getPanelProps = useMemo(() => index => getTabPanelProps(baseId, index, selectedIndex), [baseId, selectedIndex]);
102
+ return {
103
+ controlProps,
104
+ getPanelProps
105
+ };
106
+ };
30
107
  const BpkSegmentedControl = ({
108
+ activationMode = 'automatic',
31
109
  buttonContents,
110
+ id: providedId,
32
111
  label,
33
112
  onItemClick,
34
113
  selectedIndex,
35
114
  shadow = false,
36
115
  type = SEGMENT_TYPES.CanvasDefault
37
116
  }) => {
117
+ const buttonRefs = useRef([]);
118
+ const panelIds = useMemo(() => Array.from({
119
+ length: buttonContents.length
120
+ }, (_, i) => providedId ? getPanelId(providedId, i) : undefined), [providedId, buttonContents.length]);
121
+
122
+ // TODO: Consider removing internal state - component is controlled via selectedIndex prop.
123
+ // Internal state may cause sync issues if selectedIndex changes externally.
38
124
  const [selectedButton, setSelectedButton] = useState(selectedIndex);
39
- const handleButtonClick = id => {
40
- if (id !== selectedButton) {
41
- setSelectedButton(id);
42
- onItemClick(id);
125
+ const handleButtonClick = index => {
126
+ if (index !== selectedButton) {
127
+ setSelectedButton(index);
128
+ onItemClick(index);
129
+ }
130
+ };
131
+ const handleKeyDown = (event, currentIndex) => {
132
+ const lastIndex = buttonContents.length - 1;
133
+ const rtl = isRTL();
134
+ let newIndex = currentIndex;
135
+ switch (event.key) {
136
+ case 'ArrowRight':
137
+ newIndex = rtl ? getPrevIndex(currentIndex, lastIndex) : getNextIndex(currentIndex, lastIndex);
138
+ break;
139
+ case 'ArrowLeft':
140
+ newIndex = rtl ? getNextIndex(currentIndex, lastIndex) : getPrevIndex(currentIndex, lastIndex);
141
+ break;
142
+ case 'Home':
143
+ newIndex = 0;
144
+ break;
145
+ case 'End':
146
+ newIndex = lastIndex;
147
+ break;
148
+ case ' ':
149
+ case 'Enter':
150
+ event.preventDefault();
151
+ if (activationMode === 'manual') {
152
+ setSelectedButton(currentIndex);
153
+ onItemClick(currentIndex);
154
+ }
155
+ return;
156
+ default:
157
+ return;
158
+ }
159
+ event.preventDefault();
160
+ if (activationMode === 'automatic') {
161
+ setSelectedButton(newIndex);
162
+ onItemClick(newIndex);
43
163
  }
164
+ buttonRefs.current[newIndex]?.focus();
44
165
  };
45
166
  const containerStyling = getClassName('bpk-segmented-control-group', shadow && 'bpk-segmented-control-group-shadow');
46
167
  return /*#__PURE__*/_jsx("div", {
47
168
  className: containerStyling,
48
- role: "group",
49
- ...(label ? {
50
- 'aria-label': label
51
- } : {}),
169
+ ...getContainerAriaProps(providedId, label),
52
170
  children: buttonContents.map((content, index) => {
53
171
  const isSelected = index === selectedButton;
54
172
  const rightOfOption = index === selectedButton + 1;
55
173
  const buttonStyling = getClassName('bpk-segmented-control', `bpk-segmented-control--${type}`, isSelected && `bpk-segmented-control--${type}-selected`, rightOfOption && `bpk-segmented-control--${type}-rightOfOption`, shadow && isSelected && `bpk-segmented-control--${type}-selected-shadow`);
174
+ const buttonTabId = providedId ? getTabId(providedId, index) : undefined;
175
+ const tabIndexValue = getTabIndex(providedId, isSelected);
56
176
  return /*#__PURE__*/_jsx("button", {
57
- id: index.toString(),
177
+ ref: el => {
178
+ buttonRefs.current[index] = el;
179
+ },
180
+ id: buttonTabId,
58
181
  type: "button",
59
182
  onClick: () => handleButtonClick(index),
183
+ onKeyDown: event => handleKeyDown(event, index),
60
184
  className: buttonStyling,
61
- "aria-pressed": !!isSelected,
185
+ tabIndex: tabIndexValue,
186
+ ...getButtonAriaProps(providedId, isSelected, panelIds[index]),
62
187
  children: content
63
- }, `index-${index.toString()}`);
188
+ }, buttonTabId || `${index}`);
64
189
  })
65
190
  });
66
191
  };
@@ -15,4 +15,4 @@
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
17
  */
18
- .bpk-text{margin:0}.bpk-text--xs{font-size:.75rem;line-height:1rem;font-weight:400}.bpk-text--sm{font-size:.875rem;line-height:1.25rem;font-weight:400}.bpk-text--base{font-size:1rem;line-height:1.5rem;font-weight:400}.bpk-text--lg{font-size:1.25rem;line-height:1.75rem;font-weight:400}.bpk-text--xl{font-size:1.5rem;line-height:2rem;font-weight:400}.bpk-text--xxl{font-size:2rem;line-height:2.5rem;font-weight:700}.bpk-text--xxxl{font-size:2.5rem;line-height:3rem;font-weight:700}.bpk-text--xxxxl{font-size:3rem;line-height:3.5rem;font-weight:700;letter-spacing:-0.02em}.bpk-text--xxxxxl{font-size:4rem;line-height:4rem;font-weight:700;letter-spacing:-0.02em}.bpk-text--caption{font-size:.75rem;line-height:1rem;font-weight:400}.bpk-text--footnote{font-size:.875rem;line-height:1.25rem;font-weight:400}.bpk-text--label-1{font-size:1rem;line-height:1.5rem;font-weight:700}.bpk-text--label-2{font-size:.875rem;line-height:1.25rem;font-weight:700}.bpk-text--label-3{font-size:.75rem;line-height:1rem;font-weight:700}.bpk-text--body-default{font-size:1rem;line-height:1.5rem;font-weight:400}.bpk-text--body-longform{font-size:1.25rem;line-height:1.75rem;font-weight:400}.bpk-text--subheading{font-size:1.5rem;line-height:2rem;font-weight:400}.bpk-text--heading-1{font-size:2.5rem;line-height:3rem;font-weight:700}.bpk-text--heading-2{font-size:2rem;line-height:2.5rem;font-weight:700}.bpk-text--heading-3{font-size:1.5rem;line-height:1.75rem;font-weight:700}.bpk-text--heading-4{font-size:1.25rem;line-height:1.5rem;font-weight:700}.bpk-text--heading-5{font-size:1rem;line-height:1.25rem;font-weight:700}.bpk-text--hero-1{font-size:7.5rem;line-height:7.5rem;font-weight:900;letter-spacing:-0.04em}.bpk-text--hero-2{font-size:6rem;line-height:6rem;font-weight:900;letter-spacing:-0.04em}.bpk-text--hero-3{font-size:4.75rem;line-height:4.75rem;font-weight:900;letter-spacing:-0.04em}.bpk-text--hero-4{font-size:4rem;line-height:4rem;font-weight:900;letter-spacing:-0.04em}.bpk-text--hero-5{font-size:3rem;line-height:3rem;font-weight:900;letter-spacing:-0.04em}.bpk-text--hero-6{font-size:2.5rem;line-height:2.5rem;font-weight:900;letter-spacing:-0.04em}.bpk-text--editorial-1{font-family:"Larken","Noto Sans Arabic","Noto Sans Hebrew","Noto Serif","Noto Serif Devanagari","Noto Serif Thai","Noto Sans SC","Noto Sans TC","Noto Sans JP","Noto Sans KR",sans-serif;font-size:3rem;line-height:3.5rem;font-weight:300}.bpk-text--editorial-2{font-family:"Larken","Noto Sans Arabic","Noto Sans Hebrew","Noto Serif","Noto Serif Devanagari","Noto Serif Thai","Noto Sans SC","Noto Sans TC","Noto Sans JP","Noto Sans KR",sans-serif;font-size:2rem;line-height:2.5rem;font-weight:300}.bpk-text--editorial-3{font-family:"Larken","Noto Sans Arabic","Noto Sans Hebrew","Noto Serif","Noto Serif Devanagari","Noto Serif Thai","Noto Sans SC","Noto Sans TC","Noto Sans JP","Noto Sans KR",sans-serif;font-size:1.25rem;line-height:1.75rem;font-weight:400}.bpk-text.bpk-text--text-disabled{color:rgba(0,0,0,.2)}.bpk-text.bpk-text--text-disabled-on-dark{color:hsla(0,0%,100%,.5)}.bpk-text.bpk-text--text-error{color:#e70866}.bpk-text.bpk-text--text-hero{color:#0062e3}.bpk-text.bpk-text--text-link{color:#0062e3}.bpk-text.bpk-text--text-on-dark{color:#fff}.bpk-text.bpk-text--text-on-light{color:#161616}.bpk-text.bpk-text--text-primary{color:#161616}.bpk-text.bpk-text--text-primary-inverse{color:#fff}.bpk-text.bpk-text--text-secondary{color:#626971}.bpk-text.bpk-text--text-success{color:#0c838a}
18
+ .bpk-text{margin:0}.bpk-text--xs{font-size:.75rem;line-height:1rem;font-weight:400}.bpk-text--sm{font-size:.875rem;line-height:1.25rem;font-weight:400}.bpk-text--base{font-size:1rem;line-height:1.5rem;font-weight:400}.bpk-text--lg{font-size:1.25rem;line-height:1.75rem;font-weight:400}.bpk-text--xl{font-size:1.5rem;line-height:2rem;font-weight:400}.bpk-text--xxl{font-size:2rem;line-height:2.5rem;font-weight:700}.bpk-text--xxxl{font-size:2.5rem;line-height:3rem;font-weight:700}.bpk-text--xxxxl{font-size:3rem;line-height:3.5rem;font-weight:700;letter-spacing:-0.02em}.bpk-text--xxxxxl{font-size:4rem;line-height:4rem;font-weight:700;letter-spacing:-0.02em}.bpk-text--caption{font-size:.75rem;line-height:1rem;font-weight:400}.bpk-text--footnote{font-size:.875rem;line-height:1.25rem;font-weight:400}.bpk-text--label-1{font-size:1rem;line-height:1.5rem;font-weight:700}.bpk-text--label-2{font-size:.875rem;line-height:1.25rem;font-weight:700}.bpk-text--label-3{font-size:.75rem;line-height:1rem;font-weight:700}.bpk-text--body-default{font-size:1rem;line-height:1.5rem;font-weight:400}.bpk-text--body-longform{font-size:1.25rem;line-height:1.75rem;font-weight:400}.bpk-text--subheading{font-size:1.5rem;line-height:2rem;font-weight:400}.bpk-text--heading-1{font-size:2.5rem;line-height:3rem;font-weight:700}.bpk-text--heading-2{font-size:2rem;line-height:2.5rem;font-weight:700}.bpk-text--heading-3{font-size:1.5rem;line-height:1.75rem;font-weight:700}.bpk-text--heading-4{font-size:1.25rem;line-height:1.5rem;font-weight:700}.bpk-text--heading-5{font-size:1rem;line-height:1.25rem;font-weight:700}.bpk-text--hero-1{font-size:7.5rem;line-height:7.5rem;font-weight:900;letter-spacing:-0.04em}.bpk-text--hero-2{font-size:6rem;line-height:6rem;font-weight:900;letter-spacing:-0.04em}.bpk-text--hero-3{font-size:4.75rem;line-height:4.75rem;font-weight:900;letter-spacing:-0.04em}.bpk-text--hero-4{font-size:4rem;line-height:4rem;font-weight:900;letter-spacing:-0.04em}.bpk-text--hero-5{font-size:3rem;line-height:3rem;font-weight:900;letter-spacing:-0.04em}.bpk-text--hero-6{font-size:2.5rem;line-height:2.5rem;font-weight:900;letter-spacing:-0.04em}.bpk-text--editorial-1{font-family:var(--bpk-larken-font-stack, "Larken", "Noto Sans Arabic", "Noto Serif Hebrew", "Noto Serif", "Noto Serif Devanagari", "Noto Serif Thai", "Noto Serif SC", "Noto Serif TC", "Noto Serif JP", "Noto Serif KR", sans-serif);font-size:3rem;line-height:3.5rem;font-weight:300}.bpk-text--editorial-2{font-family:var(--bpk-larken-font-stack, "Larken", "Noto Sans Arabic", "Noto Serif Hebrew", "Noto Serif", "Noto Serif Devanagari", "Noto Serif Thai", "Noto Serif SC", "Noto Serif TC", "Noto Serif JP", "Noto Serif KR", sans-serif);font-size:2rem;line-height:2.5rem;font-weight:300}.bpk-text--editorial-3{font-family:var(--bpk-larken-font-stack, "Larken", "Noto Sans Arabic", "Noto Serif Hebrew", "Noto Serif", "Noto Serif Devanagari", "Noto Serif Thai", "Noto Serif SC", "Noto Serif TC", "Noto Serif JP", "Noto Serif KR", sans-serif);font-size:1.25rem;line-height:1.75rem;font-weight:400}.bpk-text.bpk-text--text-disabled{color:rgba(0,0,0,.2)}.bpk-text.bpk-text--text-disabled-on-dark{color:hsla(0,0%,100%,.5)}.bpk-text.bpk-text--text-error{color:#e70866}.bpk-text.bpk-text--text-hero{color:#0062e3}.bpk-text.bpk-text--text-link{color:#0062e3}.bpk-text.bpk-text--text-on-dark{color:#fff}.bpk-text.bpk-text--text-on-light{color:#161616}.bpk-text.bpk-text--text-primary{color:#161616}.bpk-text.bpk-text--text-primary-inverse{color:#fff}.bpk-text.bpk-text--text-secondary{color:#626971}.bpk-text.bpk-text--text-success{color:#0c838a}
@@ -0,0 +1,4 @@
1
+ import BpkThemeToggle from './src/BpkThemeToggle';
2
+ import updateOnThemeChange from './src/updateOnThemeChange';
3
+ export default BpkThemeToggle;
4
+ export { updateOnThemeChange };
@@ -0,0 +1,16 @@
1
+ import type { ChangeEvent } from 'react';
2
+ import { Component } from 'react';
3
+ type Props = Record<string, never>;
4
+ type State = {
5
+ selectedTheme: string;
6
+ };
7
+ declare class BpkThemeToggle extends Component<Props, State> {
8
+ constructor(props: Props);
9
+ componentDidMount(): void;
10
+ componentWillUnmount(): void;
11
+ handleKeyDown: (e: KeyboardEvent) => void;
12
+ handleChange: (e: ChangeEvent<HTMLSelectElement>) => void;
13
+ cycleTheme: () => void;
14
+ render(): import("react/jsx-runtime").JSX.Element;
15
+ }
16
+ export default BpkThemeToggle;
@@ -18,6 +18,7 @@
18
18
 
19
19
  import { Component } from 'react';
20
20
  import BpkLabel from "../../bpk-component-label";
21
+ // @ts-expect-error Untyped import. See `decisions/imports-ts-suppressions.md`.
21
22
  import BpkSelect from "../../bpk-component-select";
22
23
  import { cssModules } from "../../bpk-react-utils";
23
24
  import bpkCustomThemes from "./theming";
@@ -29,11 +30,13 @@ const getClassName = cssModules(STYLES);
29
30
  const availableThemes = Object.keys(bpkCustomThemes);
30
31
  const setTheme = theme => {
31
32
  const htmlElement = getHtmlElement();
32
- htmlElement.dispatchEvent(new CustomEvent(THEME_CHANGE_EVENT, {
33
- detail: {
34
- theme
35
- }
36
- }));
33
+ if (htmlElement) {
34
+ htmlElement.dispatchEvent(new CustomEvent(THEME_CHANGE_EVENT, {
35
+ detail: {
36
+ theme
37
+ }
38
+ }));
39
+ }
37
40
  };
38
41
  class BpkThemeToggle extends Component {
39
42
  constructor(props) {
@@ -58,7 +61,7 @@ class BpkThemeToggle extends Component {
58
61
  this.setState({
59
62
  selectedTheme
60
63
  });
61
- setTheme(bpkCustomThemes[selectedTheme]);
64
+ setTheme(selectedTheme ? bpkCustomThemes[selectedTheme] : undefined);
62
65
  };
63
66
  cycleTheme = () => {
64
67
  let {
@@ -73,7 +76,7 @@ class BpkThemeToggle extends Component {
73
76
  this.setState({
74
77
  selectedTheme
75
78
  });
76
- setTheme(bpkCustomThemes[selectedTheme]);
79
+ setTheme(selectedTheme ? bpkCustomThemes[selectedTheme] : undefined);
77
80
  };
78
81
  render() {
79
82
  const {