@khanacademy/wonder-blocks-filters 0.0.1 → 0.1.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/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # @khanacademy/wonder-blocks-filters
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - b0dfff3: Add `ExclusiveCategoryFilter`, a radio group for filtering a list by one category at a time. It has a required visible label, one tab stop, and arrow-key navigation that moves focus and selection together
8
+ - 8fc8e0a: ExclusiveCategoryFilter: Add visual styles from the Figma spec and the `startIcon` option property
9
+ - 16ddb19: Add new `@khanacademy/wonder-blocks-filters` package
10
+
11
+ ### Patch Changes
12
+
13
+ - b0dfff3: ExclusiveCategoryFilter: Sync the parent when the first option is chosen while `selectedValue` matches no option, use index-based option ids, follow the nearest `dir` for arrow keys, and render the label with `BodyText`
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018 Khan Academy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,101 @@
1
+ import * as React from "react";
2
+ import { StyleType } from "@khanacademy/wonder-blocks-core";
3
+ export type ExclusiveCategoryFilterOption = {
4
+ /**
5
+ * A unique value for the option. It is passed to `onChange` when the
6
+ * option is selected.
7
+ */
8
+ value: string;
9
+ /**
10
+ * The visible label for the option. It is also the option's accessible
11
+ * name.
12
+ */
13
+ label: string;
14
+ /**
15
+ * Optional icon shown before the label: a `PhosphorIcon` or `Icon`
16
+ * element, rendered as passed with the small size as the default.
17
+ *
18
+ * The icon is decorative unless it has an `aria-label`. Give it one when
19
+ * the icon adds meaning the label doesn't; it then becomes part of the
20
+ * option's accessible name.
21
+ */
22
+ startIcon?: React.ReactElement;
23
+ /**
24
+ * Optional id for the option. If not set, the option uses
25
+ * `${id}-option-${index}`.
26
+ */
27
+ id?: string;
28
+ /**
29
+ * Optional test ID for e2e testing. If not set, the option uses
30
+ * `${testId}-${value}` when the component has a `testId`.
31
+ */
32
+ testId?: string;
33
+ };
34
+ type Props = {
35
+ /**
36
+ * A unique id to use as the base of the ids for the elements within the
37
+ * component. If the `id` prop is not provided, a base unique id will be
38
+ * auto-generated.
39
+ *
40
+ * Here is how the id is used for the different elements in the component:
41
+ * - The root will have an id of `${id}`
42
+ * - The label will have an id formatted as `${id}-label`
43
+ * - Each option will have an id formatted as `${id}-option-${index}`,
44
+ * unless the option sets its own `id`.
45
+ */
46
+ id?: string;
47
+ /**
48
+ * Optional test ID for e2e testing. Here is how the test id is used for
49
+ * the different elements in the component:
50
+ * - The root will have a testId formatted as `${testId}`
51
+ * - Each option will have a testId formatted as `${testId}-${value}`,
52
+ * unless the option sets its own `testId`.
53
+ */
54
+ testId?: string;
55
+ /**
56
+ * The visible label for the group of options (e.g. "Filter by subject").
57
+ * It is also used as the accessible name of the radio group.
58
+ */
59
+ label: string;
60
+ /**
61
+ * The options to render, in order.
62
+ */
63
+ options: ReadonlyArray<ExclusiveCategoryFilterOption>;
64
+ /**
65
+ * The value of the selected option. Exactly one option is always
66
+ * selected.
67
+ *
68
+ * If it doesn't match any option, the first option is selected by
69
+ * default. `onChange` is not called until the user chooses an option.
70
+ */
71
+ selectedValue: string;
72
+ /**
73
+ * Called with the value of the newly selected option. It is not called
74
+ * when the selected option is clicked again, since the selection can't
75
+ * be cleared.
76
+ */
77
+ onChange: (value: string) => unknown;
78
+ /**
79
+ * Custom styles for the `ExclusiveCategoryFilter` component.
80
+ * - `root`: Styles the root element.
81
+ * - `label`: Styles the label element.
82
+ * - `options`: Styles the element that contains the options.
83
+ * - `option`: Styles all the option elements.
84
+ */
85
+ styles?: {
86
+ root?: StyleType;
87
+ label?: StyleType;
88
+ options?: StyleType;
89
+ option?: StyleType;
90
+ };
91
+ };
92
+ /**
93
+ * A group of options for filtering a list by one category at a time. Exactly
94
+ * one option is always selected.
95
+ *
96
+ * The component follows the ARIA APG Radio Group pattern: the options are
97
+ * radio buttons with a single tab stop, and the arrow keys move focus to the
98
+ * next or previous option and select it.
99
+ */
100
+ export declare const ExclusiveCategoryFilter: React.ForwardRefExoticComponent<Props & React.RefAttributes<HTMLDivElement>>;
101
+ export {};
@@ -0,0 +1,11 @@
1
+ import { jsx, jsxs } from 'react/jsx-runtime';
2
+ import * as React from 'react';
3
+ import { StyleSheet } from 'aphrodite';
4
+ import { addStyle, keys, View } from '@khanacademy/wonder-blocks-core';
5
+ import { focusStyles } from '@khanacademy/wonder-blocks-styles';
6
+ import { sizing, border, semanticColor } from '@khanacademy/wonder-blocks-tokens';
7
+ import { BodyText, styles as styles$1 } from '@khanacademy/wonder-blocks-typography';
8
+
9
+ const StyledButton=addStyle("button");function renderStartIcon(startIcon){return React.cloneElement(startIcon,{size:startIcon.props.size??"small"})}const ExclusiveCategoryFilter=React.forwardRef(function ExclusiveCategoryFilter(props,ref){const{id,testId,label,options,selectedValue,onChange,styles:stylesProp}=props;const generatedUniqueId=React.useId();const uniqueId=id??generatedUniqueId;const labelId=`${uniqueId}-label`;const optionRefs=React.useRef({});const matchingIndex=options.findIndex(option=>option.value===selectedValue);const selectedIndex=matchingIndex===-1?0:matchingIndex;const selectOption=React.useCallback(value=>{optionRefs.current[value]?.focus();if(value!==selectedValue){onChange(value);}},[onChange,selectedValue]);const handleKeyDown=React.useCallback(event=>{const currentIndex=options.findIndex(option=>option.value===event.currentTarget.value);const isRtl=event.currentTarget.closest("[dir]")?.getAttribute("dir")==="rtl";let nextIndex;switch(event.key){case keys.up:case isRtl&&keys.right:case!isRtl&&keys.left:{nextIndex=(currentIndex-1+options.length)%options.length;break}case keys.down:case isRtl&&keys.left:case!isRtl&&keys.right:{nextIndex=(currentIndex+1)%options.length;break}default:{return}}event.preventDefault();selectOption(options[nextIndex].value);},[options,selectOption]);if(options.length===0){return jsx(React.Fragment,{})}return jsxs(View,{ref:ref,id:uniqueId,testId:testId,style:[styles.root,stylesProp?.root],children:[jsx(BodyText,{tag:"span",weight:"semi",id:labelId,style:[styles.label,stylesProp?.label],children:label}),jsx(View,{role:"radiogroup","aria-labelledby":labelId,style:[styles.options,stylesProp?.options],children:options.map((option,index)=>{const checked=index===selectedIndex;const optionTestId=option.testId??(testId?`${testId}-${option.value}`:undefined);return jsxs(StyledButton,{ref:element=>{optionRefs.current[option.value]=element;},id:option.id??`${uniqueId}-option-${index}`,"data-testid":optionTestId,type:"button",role:"radio","aria-checked":checked,tabIndex:checked?0:-1,value:option.value,onClick:()=>selectOption(option.value),onKeyDown:handleKeyDown,style:[styles$1.BodyTextMediumSemiWeight,styles.option,checked&&styles.selected,stylesProp?.option],children:[option.startIcon&&renderStartIcon(option.startIcon),option.label]},option.value)})})]})});const mediumBorderPaddingInline=`calc(${sizing.size_160} - (${border.width.medium} - ${border.width.thin}))`;const styles=StyleSheet.create({root:{gap:sizing.size_080},label:{color:semanticColor.core.foreground.neutral.default,overflowWrap:"anywhere"},options:{flexDirection:"row",flexWrap:"wrap",gap:sizing.size_080},option:{display:"inline-flex",alignItems:"center",gap:sizing.size_080,boxSizing:"border-box",overflowWrap:"anywhere",minBlockSize:sizing.size_400,maxInlineSize:"100%",margin:0,paddingBlock:sizing.size_080,paddingInline:sizing.size_160,textAlign:"start",cursor:"pointer",color:semanticColor.core.foreground.neutral.subtle,backgroundColor:semanticColor.core.transparent,border:`${border.width.thin} solid ${semanticColor.core.border.neutral.subtle}`,borderRadius:border.radius.radius_240,...focusStyles.focus,[":hover:not([aria-checked='true'])"]:{borderColor:semanticColor.core.border.neutral.default},[":active:not([aria-checked='true'])"]:{borderWidth:border.width.medium,borderColor:semanticColor.core.border.neutral.default,paddingInline:mediumBorderPaddingInline}},selected:{color:semanticColor.core.foreground.instructive.default,backgroundColor:semanticColor.core.background.instructive.subtle,borderWidth:border.width.medium,borderColor:semanticColor.core.border.instructive.default,paddingInline:mediumBorderPaddingInline}});
10
+
11
+ export { ExclusiveCategoryFilter };
@@ -0,0 +1,2 @@
1
+ export { ExclusiveCategoryFilter } from "./components/exclusive-category-filter";
2
+ export type { ExclusiveCategoryFilterOption } from "./components/exclusive-category-filter";
package/dist/index.js ADDED
@@ -0,0 +1,35 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+ var React = require('react');
7
+ var aphrodite = require('aphrodite');
8
+ var wonderBlocksCore = require('@khanacademy/wonder-blocks-core');
9
+ var wonderBlocksStyles = require('@khanacademy/wonder-blocks-styles');
10
+ var wonderBlocksTokens = require('@khanacademy/wonder-blocks-tokens');
11
+ var wonderBlocksTypography = require('@khanacademy/wonder-blocks-typography');
12
+
13
+ function _interopNamespace(e) {
14
+ if (e && e.__esModule) return e;
15
+ var n = Object.create(null);
16
+ if (e) {
17
+ Object.keys(e).forEach(function (k) {
18
+ if (k !== 'default') {
19
+ var d = Object.getOwnPropertyDescriptor(e, k);
20
+ Object.defineProperty(n, k, d.get ? d : {
21
+ enumerable: true,
22
+ get: function () { return e[k]; }
23
+ });
24
+ }
25
+ });
26
+ }
27
+ n["default"] = e;
28
+ return Object.freeze(n);
29
+ }
30
+
31
+ var React__namespace = /*#__PURE__*/_interopNamespace(React);
32
+
33
+ const StyledButton=wonderBlocksCore.addStyle("button");function renderStartIcon(startIcon){return React__namespace.cloneElement(startIcon,{size:startIcon.props.size??"small"})}const ExclusiveCategoryFilter=React__namespace.forwardRef(function ExclusiveCategoryFilter(props,ref){const{id,testId,label,options,selectedValue,onChange,styles:stylesProp}=props;const generatedUniqueId=React__namespace.useId();const uniqueId=id??generatedUniqueId;const labelId=`${uniqueId}-label`;const optionRefs=React__namespace.useRef({});const matchingIndex=options.findIndex(option=>option.value===selectedValue);const selectedIndex=matchingIndex===-1?0:matchingIndex;const selectOption=React__namespace.useCallback(value=>{optionRefs.current[value]?.focus();if(value!==selectedValue){onChange(value);}},[onChange,selectedValue]);const handleKeyDown=React__namespace.useCallback(event=>{const currentIndex=options.findIndex(option=>option.value===event.currentTarget.value);const isRtl=event.currentTarget.closest("[dir]")?.getAttribute("dir")==="rtl";let nextIndex;switch(event.key){case wonderBlocksCore.keys.up:case isRtl&&wonderBlocksCore.keys.right:case!isRtl&&wonderBlocksCore.keys.left:{nextIndex=(currentIndex-1+options.length)%options.length;break}case wonderBlocksCore.keys.down:case isRtl&&wonderBlocksCore.keys.left:case!isRtl&&wonderBlocksCore.keys.right:{nextIndex=(currentIndex+1)%options.length;break}default:{return}}event.preventDefault();selectOption(options[nextIndex].value);},[options,selectOption]);if(options.length===0){return jsxRuntime.jsx(React__namespace.Fragment,{})}return jsxRuntime.jsxs(wonderBlocksCore.View,{ref:ref,id:uniqueId,testId:testId,style:[styles.root,stylesProp?.root],children:[jsxRuntime.jsx(wonderBlocksTypography.BodyText,{tag:"span",weight:"semi",id:labelId,style:[styles.label,stylesProp?.label],children:label}),jsxRuntime.jsx(wonderBlocksCore.View,{role:"radiogroup","aria-labelledby":labelId,style:[styles.options,stylesProp?.options],children:options.map((option,index)=>{const checked=index===selectedIndex;const optionTestId=option.testId??(testId?`${testId}-${option.value}`:undefined);return jsxRuntime.jsxs(StyledButton,{ref:element=>{optionRefs.current[option.value]=element;},id:option.id??`${uniqueId}-option-${index}`,"data-testid":optionTestId,type:"button",role:"radio","aria-checked":checked,tabIndex:checked?0:-1,value:option.value,onClick:()=>selectOption(option.value),onKeyDown:handleKeyDown,style:[wonderBlocksTypography.styles.BodyTextMediumSemiWeight,styles.option,checked&&styles.selected,stylesProp?.option],children:[option.startIcon&&renderStartIcon(option.startIcon),option.label]},option.value)})})]})});const mediumBorderPaddingInline=`calc(${wonderBlocksTokens.sizing.size_160} - (${wonderBlocksTokens.border.width.medium} - ${wonderBlocksTokens.border.width.thin}))`;const styles=aphrodite.StyleSheet.create({root:{gap:wonderBlocksTokens.sizing.size_080},label:{color:wonderBlocksTokens.semanticColor.core.foreground.neutral.default,overflowWrap:"anywhere"},options:{flexDirection:"row",flexWrap:"wrap",gap:wonderBlocksTokens.sizing.size_080},option:{display:"inline-flex",alignItems:"center",gap:wonderBlocksTokens.sizing.size_080,boxSizing:"border-box",overflowWrap:"anywhere",minBlockSize:wonderBlocksTokens.sizing.size_400,maxInlineSize:"100%",margin:0,paddingBlock:wonderBlocksTokens.sizing.size_080,paddingInline:wonderBlocksTokens.sizing.size_160,textAlign:"start",cursor:"pointer",color:wonderBlocksTokens.semanticColor.core.foreground.neutral.subtle,backgroundColor:wonderBlocksTokens.semanticColor.core.transparent,border:`${wonderBlocksTokens.border.width.thin} solid ${wonderBlocksTokens.semanticColor.core.border.neutral.subtle}`,borderRadius:wonderBlocksTokens.border.radius.radius_240,...wonderBlocksStyles.focusStyles.focus,[":hover:not([aria-checked='true'])"]:{borderColor:wonderBlocksTokens.semanticColor.core.border.neutral.default},[":active:not([aria-checked='true'])"]:{borderWidth:wonderBlocksTokens.border.width.medium,borderColor:wonderBlocksTokens.semanticColor.core.border.neutral.default,paddingInline:mediumBorderPaddingInline}},selected:{color:wonderBlocksTokens.semanticColor.core.foreground.instructive.default,backgroundColor:wonderBlocksTokens.semanticColor.core.background.instructive.subtle,borderWidth:wonderBlocksTokens.border.width.medium,borderColor:wonderBlocksTokens.semanticColor.core.border.instructive.default,paddingInline:mediumBorderPaddingInline}});
34
+
35
+ exports.ExclusiveCategoryFilter = ExclusiveCategoryFilter;
package/package.json CHANGED
@@ -1,19 +1,38 @@
1
1
  {
2
2
  "name": "@khanacademy/wonder-blocks-filters",
3
- "description": "Placeholder to allow Trusted Publishing configuration",
3
+ "description": "Filters are used to narrow the items shown in a list",
4
4
  "author": "Khan Academy",
5
5
  "license": "MIT",
6
- "version": "0.0.1",
6
+ "version": "0.1.0",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
10
+ "design": "v1",
10
11
  "repository": {
11
12
  "type": "git",
12
- "url": "git+https://github.com/Khan/wonder-blocks.git"
13
+ "url": "https://github.com/Khan/wonder-blocks.git",
14
+ "directory": "packages/wonder-blocks-filters"
13
15
  },
14
16
  "bugs": {
15
17
  "url": "https://github.com/Khan/wonder-blocks/issues"
16
18
  },
17
- "module": "index.js",
18
- "main": "index.js"
19
+ "main": "dist/index.js",
20
+ "module": "dist/es/index.js",
21
+ "types": "dist/index.d.ts",
22
+ "dependencies": {
23
+ "@khanacademy/wonder-blocks-core": "12.5.0",
24
+ "@khanacademy/wonder-blocks-styles": "0.4.1",
25
+ "@khanacademy/wonder-blocks-typography": "5.0.6",
26
+ "@khanacademy/wonder-blocks-tokens": "18.1.0"
27
+ },
28
+ "peerDependencies": {
29
+ "aphrodite": "^1.2.5",
30
+ "react": "18.2.0"
31
+ },
32
+ "devDependencies": {
33
+ "@khanacademy/wb-dev-build-settings": "3.3.0"
34
+ },
35
+ "scripts": {
36
+ "test": "echo \"Error: no test specified\" && exit 1"
37
+ }
19
38
  }